Swift Combine 学习(七):实践应用场景举例

  • Swift Combine 学习(一):Combine 初印象
  • Swift Combine 学习(二):发布者 Publisher
  • Swift Combine 学习(三):Subscription和 Subscriber
  • Swift Combine 学习(四):操作符 Operator
  • Swift Combine 学习(五):Backpressure和 Scheduler
  • Swift Combine 学习(六):自定义 Publisher 和 Subscriber
  • Swift Combine 学习(七):实践应用场景举例

    文章目录

      • 引言
      • Combine 应用
        • 网络请求处理
        • 请求链式调用
        • 并发请求
        • 用户界面更新
        • 数据绑定
      • Combine VS RxSwift
      • 实践中注意点
      • 结语

引言

在前面的系列文章中,我们已经深入学习了 Combine 框架的各个组成部分和使用方法。现在,是时候将这些理论知识付诸实践了。本文将通过实际的编程案例,展示 Combine 在日常开发中的应用场景,包括网络请求处理、用户界面交互、数据绑定等。通过这些实例,希望能够帮助您更好地理解如何在实际项目中使用 Combine。

Combine 应用

网络请求处理

Combine 非常适合处理网络请求。以下是一个使用 Combine 进行网络请求的示例:

  1. NetworkService 类定义了一个 fetchPosts() 方法,返回一个 AnyPublisher<[Post], Error>
  2. 使用 URLSession.shared.dataTaskPublisher 创建网络请求。
  3. 使用 map 操作符提取响应数据。
  4. 使用 decode 操作符将 JSON 数据解码为 [Post] 数组。
  5. 使用 @Published 属性包装器声明 posts 数组,当它的值改变时会自动通知。
  6. fetchPosts() 方法订阅网络请求的结果。
import UIKit
import Combineenum NetworkError: Error {case invalidURLcase requestFailedcase decodingFailed
}struct Post: Codable {let id: Intlet title: Stringlet body: String
}class NetworkService {func fetchPosts() -> AnyPublisher<[Post], Error> {// 使用 JSONPlaceholder 提供的测试 APIguard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()}return URLSession.shared.dataTaskPublisher(for: url).tryMap { output inguard let response = output.response as? HTTPURLResponse,response.statusCode == 200 else {throw NetworkError.requestFailed}return output.data}.decode(type: [Post].self, decoder: JSONDecoder()).mapError { error -> Error inswitch error {case is URLError:return NetworkError.requestFailedcase is DecodingError:return NetworkError.decodingFaileddefault:return error}}.eraseToAnyPublisher()}
}class PostsViewModel: ObservableObject {@Published var posts: [Post] = []private var cancellables = Set<AnyCancellable>()private let networkService = NetworkService()func fetchPosts() {print("   开始获取帖子...")networkService.fetchPosts().receive(on: DispatchQueue.main).sink { completion inswitch completion {case .finished:print("✅ 成功获取帖子")case .failure(let error):print("❌ 获取帖子失败: \(error)")}} receiveValue: { [weak self] posts inself?.posts = postsprint("📝 获取到 \(posts.count) 条帖子")// 打印前2条帖子的标题posts.prefix(2).forEach { post inprint("   标题: \(post.title)")}}.store(in: &cancellables)}
}// test
func testNetworkRequest() {let viewModel = PostsViewModel()viewModel.fetchPosts()RunLoop.main.run(until: Date(timeIntervalSinceNow: 5))
}print("🍎开始网络请求测试")
testNetworkRequest()/*输出:
🍎 开始网络请求测试开始获取帖子...
📝 获取到 100 条帖子标题: sunt aut facere repellat provident occaecati excepturi optio reprehenderit标题: qui est esse
✅ 成功获取帖子
*/

请求链式调用

有时我们需要基于第一个请求的结果发起第二个请求:

import UIKit
import Combine
import Foundation
import SwiftUIstruct User: Codable {let id: Intlet name: String
}struct UserPosts: Codable {let user: Userlet posts: [Post]
}enum NetworkError: Error {case invalidURLcase requestFailedcase decodingFailed
}struct Post: Codable {let id: Intlet title: Stringlet body: String
}class NetworkService {func fetchPosts() -> AnyPublisher<[Post], Error> {// 使用 JSONPlaceholder 提供的测试 APIguard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()}return URLSession.shared.dataTaskPublisher(for: url).tryMap { output inguard let response = output.response as? HTTPURLResponse,response.statusCode == 200 else {throw NetworkError.requestFailed}return output.data}.decode(type: [Post].self, decoder: JSONDecoder()).mapError { error -> Error inswitch error {case is URLError:return NetworkError.requestFailedcase is DecodingError:return NetworkError.decodingFaileddefault:return error}}.eraseToAnyPublisher()}
}extension NetworkService {// fetch user infofunc fetchUser(id: Int) -> AnyPublisher<User, Error> {guard let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(id)") else {return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()}return URLSession.shared.dataTaskPublisher(for: url).tryMap { output inguard let response = output.response as? HTTPURLResponse,response.statusCode == 200 else {throw NetworkError.requestFailed}return output.data}.decode(type: User.self, decoder: JSONDecoder()).mapError { error -> Error inswitch error {case is URLError:return NetworkError.requestFailedcase is DecodingError:return NetworkError.decodingFaileddefault:return error}}.eraseToAnyPublisher()}// fetch user postfunc fetchPosts(userID: Int) -> AnyPublisher<[Post], Error> {guard let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(userID)/posts") else {return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()}return URLSession.shared.dataTaskPublisher(for: url).tryMap { output inguard let response = output.response as? HTTPURLResponse,response.statusCode == 200 else {throw NetworkError.requestFailed}return output.data}.decode(type: [Post].self, decoder: JSONDecoder()).mapError { error -> Error inswitch error {case is URLError:return NetworkError.requestFailedcase is DecodingError:return NetworkError.decodingFaileddefault:return error}}.eraseToAnyPublisher()}// 组合请求:fetch user info and postsfunc fetchUserAndPosts(userID: Int) -> AnyPublisher<UserPosts, Error> {fetchUser(id: userID).flatMap { user -> AnyPublisher<UserPosts, Error> inself.fetchPosts(userID: user.id).map { posts inUserPosts(user: user, posts: posts)}.eraseToAnyPublisher()}.eraseToAnyPublisher()}
}// ViewModel
class UserPostsViewModel: ObservableObject {@Published var userPosts: UserPosts?private var cancellables = Set<AnyCancellable>()private let networkService = NetworkService()func fetchUserAndPosts(userID: Int) {print("   开始获取用户 \(userID) 的信息和帖子...")networkService.fetchUserAndPosts(userID: userID).receive(on: DispatchQueue.main).sink { completion inswitch completion {case .finished:print("✅ 成功获取用户信息和帖子")case .failure(let error):print("❌ 获取失败: \(error)")}} receiveValue: { [weak self] userPosts inself?.userPosts = userPostsprint("\n📝 用户信息:")print("   ID: \(userPosts.user.id)")print("   名字: \(userPosts.user.name)")print("\n📝 该用户的帖子(前2条):")userPosts.posts.prefix(2).forEach { post inprint("   标题: \(post.title)")}}.store(in: &cancellables)}
}print("🍎开始组合请求测试")
testUserAndPosts()/*输出
🍎开始组合请求测试开始获取用户 1 的信息和帖子...📝 用户信息:ID: 1名字: Leanne Graham📝 该用户的帖子(前2条):标题: sunt aut facere repellat provident occaecati excepturi optio reprehenderit标题: qui est esse
✅ 成功获取用户信息和帖子
*/

并发请求

当需要同时发起多个请求并等待所有结果时:

func fetchMultipleUsers(ids: [Int]) -> AnyPublisher<[User], Error> {let publishers = ids.map { fetchUser(id: $0) }return Publishers.MergeMany(publishers).collect().eraseToAnyPublisher()
}func fetchUserAndProfile(userID: Int) -> AnyPublisher<(User, Profile), Error> {// 使用 zip 保顺序Publishers.Zip(fetchUser(id: userID),fetchProfile(userID: userID)).eraseToAnyPublisher()
}

用户界面更新

Combine 还可以用于响应式地更新用户界面。以下是一个简单的示例,展示如何使用 Combine 更新 UIKit 界面:

  1. 创建一个 UISearchBar 扩展,增加一个 textDidChangePublisher 属性。
extension UISearchBar {var textDidChangePublisher: AnyPublisher<String, Never> {NotificationCenter.default.publisher(for: UISearchBar.textDidChangeNotification, object: self).compactMap { $0.object as? UISearchBar }.map { $0.text ?? "" }.eraseToAnyPublisher()}
}class SearchViewModel {@Published private(set) var searchResults: [String] = []func search(query: String) {// 模拟网络请求DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {let results = (0..<10).map { "\(query) result \($0)" }self.searchResults = results}}
}
  1. setupBindings() 方法设置了两个主要的数据流:

    1. 搜索栏文本变化到搜索操作。
    2. 搜索结果到 UI 更新。
    class SearchViewController: UIViewController {// ... 声明一个 searchBar 、一个 tableviewprivate var viewModel = SearchViewModel()private var cancellables = Set<AnyCancellable>()override func viewDidLoad() {super.viewDidLoad()setupBindings()}private func setupBindings() {searchBar.textDidChangePublisher.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates().sink { [weak self] searchText inself?.viewModel.search(query: searchText)}.store(in: &cancellables)viewModel.$searchResults.receive(on: DispatchQueue.main).sink { [weak self] _ inself?.tableView.reloadData()}.store(in: &cancellables)}
    }
    
  2. 使用 @Published 属性包装器声明 searchResults,允许外部订阅,但只允许内部修改。search(query:) 方法模拟一个异步网络请求。

    class SearchViewModel {@Published private(set) var searchResults: [String] = []func search(query: String) {DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {let results = (0..<10).map { "\(query) result \($0)" }self.searchResults = results}}
    }
    

数据绑定

Combine 非常适合用于数据绑定,特别是在 MVVM 架构中。以下是一个简单的例子:

  1. User 类使用@Published 属性包装。
  2. 使用 Publishers.CombineLatest 来响应任一属性的变化。
  3. UserVC 订阅 ViewModel 的 displayName 属性,并更新 UI。
class User {@Published var name: String@Published var age: Intinit(name: String, age: Int) {self.name = nameself.age = age}
}class UserViewModel {@Published private(set) var displayName: String = ""private var user: Userprivate var cancellables = Set<AnyCancellable>()init(user: User) {self.user = usersetupBindings()}private func setupBindings() {Publishers.CombineLatest($user.name, $user.age).map { name, age inreturn "\(name) (\(age) years old)"}.assign(to: \.displayName, on: self).store(in: &cancellables)}
}class UserVC: UIViewController {// ... 声明一个 nameLabelprivate var viewModel: UserViewModel!private var cancellables = Set<AnyCancellable>()override func viewDidLoad() {super.viewDidLoad()let user = User(name: "Joy", age: 91)viewModel = UserViewModel(user: user)viewModel.$displayName.receive(on: DispatchQueue.main).sink { [weak self] displayName inself?.nameLabel.text = displayName}.store(in: &cancellables)}
}

Combine VS RxSwift

RxSwift 是 ReactiveX 中的一个。ReactiveX 还有 RxJava、RxJS、RxKotlin 等等。以下是一个简单的对照表:

RxSwiftCombine说明
ObservablePublisher发送数据的源
ObserverSubscriber接收数据的目标
DisposeBagSet管理订阅生命周期
SubjectSubject既可发送也可接收数据

以下是一些常用操作符的对照表:

RxSwiftCombine说明
mapmap值转换
flatMapflatMap转换为新的 Observable/Publisher
filterfilter过滤值
distinctUntilChangedremoveDuplicates去重
debouncedebounce防抖
throttlethrottle节流
mergemerge合并数据流
combineLatestcombineLatest组合最新值
zipzip配对组合

虽然 Combine 和 RxSwift 都是响应式编程框架,但它们有很多不同之处。如:

  1. 来源:

    • Combine 是 Apple 官方框架,内置于 Swift 和 iOS 13 及以上版本中。
    • RxSwift 是社区驱动的项目,适用于 iOS、macOS 和其他平台
  2. 语法基本概念相似,具体的 API 和命名有所不同。

  3. Combine 虽然内置了很多操作符,但还是比 Rxswift 少。Combine 和 RxSwift 的操作符对比 RxSwift to Combine Cheatsheet

  4. 功能集:

    • RxSwift 提供了更丰富的操作符,涵盖了更多场景。
    • Combine 的功能相对较少,但 API 设计更强调安全性和清晰的错误处理。
  5. 平台支持:

    • Combine 仅支持 Apple 平台(iOS 13+、macOS 10.15+、tvOS 13+、watchOS 6+)。
    • RxSwift 支持更广泛的平台和 iOS 版本。
  6. 学习曲线:

    • Combine 的学习曲线相对较缓,特别是对于已经熟悉 Swift 的开发者。
    • RxSwift 的学习曲线可能更陡峭,因为它包含了更多的概念和操作符。
  7. 性能

    RxSwift 是纯 Swift 实现,Combine 实际使用性能会更优。

  8. 版本支持

    Combine 支持的最低系统版本是 iOS 13。但是有开源的 OpenCombine 可以支持到 iOS 9。

以下是一个简单的对比示例:

1// Combine
let combinePublisher = (1...5).publisher.map { $0 * 2 }.filter { $0 > 5 }.sink { value inprint("Combine: \(value)")}// RxSwift
let rxObservable = Observable.from(1...5).map { $0 * 2 }.filter { $0 > 5 }.subscribe(onNext: { value inprint("RxSwift: \(value)")})
2
let disposeBag = DisposeBag()let observable = Observable.of(1, 2, 3, 4, 5, 6)// 用 RxSwift 的操作符进行过滤
observable.filter { $0 % 2 == 0 } // 只保留偶数.subscribe(onNext: { value inprint("RxSwift - Even number: \(value)")}).disposed(by: disposeBag)let publisher = [1, 2, 3, 4, 5, 6].publisher// 用 Combine 的操作符进行过滤
let cancellable = publisher.filter { $0 % 2 == 0 } .sink(receiveValue: { value inprint("Combine - Even number: \(value)")})

虽然语法略有不同,基本概念和操作是相似的。

实践中注意点

在使用 Combine 时,以下是一些性能优化、实践、常见错误:

  1. 合理使用调度器

    使用 receive(on:) 确保在正确的线程上执行操作。

    somePublisher.receive(on: DispatchQueue.main).sink { ... }
    
  2. 管理订阅生命周期

    始终存储和管理 AnyCancellable 对象,以确保在不再需要时取消订阅,防止内存泄漏,降低开销等。

    let publisher = [1, 2, 3, 4, 5].publishervar cancellable: AnyCancellable?func subscribeToPublisher() {cancellable = publisher.filter { $0 % 2 == 0 } .sink(receiveValue: { value inprint("Received value: \(value)")})
    }// 不再需要时,及时取消订阅
    func cancelSubscription() {cancellable?.cancel()
    }
    
  3. 共享昂贵资源

    使用 shareReplay 来共享昂贵的操作结果。如网络请求或复杂计算,这样可以避免重复执行相同的操作,从而提高性能。

    let sharedPublisher = someExpensivePublisher.shareReplay(1).eraseToAnyPublisher()
    
  4. 优化加载体验

    使用 prependappend 优化加载体验,以提供更流畅的用户体验。比如先显示缓存数据,然后更新为最新数据,减少用户的等待时间。

    let dataPublisher = loadDataFromCache().append(loadDataFromNetwork())
    
  5. 避免内存泄漏

    在闭包中使用 [weak self] 来避免循环引用。

    somePublisher.sink { [weak self] value inself?.balabala(value)}.store(in: &cancellables)
    
  6. Future 和 Just 这俩 Publisher 在初始化完成后会立即执行闭包里的逻辑,这就可能会造成不符合预期的执行流程错误。

    let IOHeavyTask = Future<Int, Never> { promise inprint("开始耗时计算...")// 模拟耗时操作Thread.sleep(forTimeInterval: 2)let result = 42print("计算完成")promise(.success(result))
    }Thread.sleep(forTimeInterval: 1)print("准备订阅")
    let cancellable = IOHeavyTask.sink { value inprint("收到结果: \(value)")
    }/*
    开始耗时计算...
    计算完成
    准备订阅
    收到结果: 42
    */
    

    在上面例子中,Future 在创建时就立即开始了耗时计算。在我们准备好订阅之前,计算就已经完成了。订阅者可能错过了计算过程,只能接收到最终结果。我们可以使用 Defferred,套在 Publisher 的外边,Deferred 允许延迟 Publisher 的创建,直到有订阅者订阅时才开始。

    let IOHeavyTask = Deferred {Future<Int, Never> { promise inprint("开始耗时计算...")// 模拟耗时操作Thread.sleep(forTimeInterval: 2)let result = 42print("计算完成")promise(.success(result))}
    }Thread.sleep(forTimeInterval: 1)print("准备订阅")
    let cancellable = IOHeavyTask.sink { value inprint("收到结果: \(value)")
    }/*
    准备订阅
    开始耗时计算...
    计算完成
    收到结果: 42
    */
    
  7. 有时候一些错误导致 Subscription 意外结束

    这里就不再举例了,简单说一下可以用 catch 来提供一个默认值或是替代的 Publisher 等等。

结语

简单说下关于 Combine 的个人愚见:

  1. 实用性:Combine 确实提供了强大的工具来处理异步编程和事件流。建议逐步将 Combine 整合到项目中,从简单的用例开始,如网络请求处理或简单的 UI 更新。这样可以在实践中学习,同时避免在整个项目中过度使用导致的复杂性。
  2. 性能 :在大多数情况下,Combine 的性能表现良好。不过在处理大量高频事件时,得注意内存使用和 CPU 占用。使用诸如 debouncethrottle 等操作符可以有效控制事件流,提高应用性能。
  3. 未来 :随着 App 开发 iOS 最低兼容将要来到 iOS 13,SwiftUI 普及,Combine 在 iOS 开发中的重要性可能会随之进一步提升。需要持续关注 WWDC 和 Apple 的文档更新。

保持务实,根据项目需求和团队能力来决定使用的程度。

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

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

相关文章

DC-2 靶场渗透

目录 环境搭建 开始渗透 扫存活 扫端口 扫服务 看一下80端口 看一下指纹信息 使用wpscan扫描用户名 再使用cewl生成字典 使用wpscan爆破密码 登陆 使用7744端口 查看shell rbash绕过 切换到jerry用户 添加环境变量 现在可以使用su命令了 提权 使用git提权 环…

如何在 Ubuntu 22.04 上优化 Apache 以应对高流量网站教程

简介 在本教程中&#xff0c;我们将学习如何优化 Apache 以应对高流量网站。 当运行高流量网站时&#xff0c;确保你的 Apache Web 服务器得到优化对于有效处理负载至关重要。在本指南中&#xff0c;我们将介绍配置 Apache 以提高性能和可扩展性的基本技巧。 为高流量网站优…

安卓NDK视觉开发——手机拍照文档边缘检测实现方法与库封装

一、项目创建 创建NDK项目有两种方式&#xff0c;一种从新创建整个项目&#xff0c;一个在创建好的项目添加NDK接口。 1.创建NDK项目 创建 一个Native C项目&#xff1a; 选择包名、API版本与算法交互的语言&#xff1a; 选择C版本&#xff1a; 创建完之后&#xff0c;可…

反向传播算法的偏置更新步骤

偏置的更新步骤 假设我们有一个三层神经网络&#xff08;输入层、隐藏层和输出层&#xff09;&#xff0c;并且每层的激活函数为 sigmoid 函数。我们需要更新隐藏层和输出层的偏置。以下是详细的步骤&#xff1a; 1. 计算误差项&#xff08;Error Term&#xff09; 输出层的…

Spring源码分析之事件机制——观察者模式(一)

目录 事件基类定义 事件监听器接口 事件发布者接口及实现 事件广播器实现 小小总结 Spring源码分析之事件机制——观察者模式&#xff08;一&#xff09;-CSDN博客 Spring源码分析之事件机制——观察者模式&#xff08;二&#xff09;-CSDN博客 Spring源码分析之事件机制…

Spring Security(maven项目) 3.0.2.4版本

前言&#xff1a; 通过实践而发现真理&#xff0c;又通过实践而证实真理和发展真理。从感性认识而能动地发展到理性认识&#xff0c;又从理性认识而能动地指导革命实践&#xff0c;改造主观世界和客观世界。实践、认识、再实践、再认识&#xff0c;这种形式&#xff0c;循环往…

stm32 移植RTL8201F(正点原子例程为例)

最近在工作中需要使用RTL8201F&#xff0c;在网上找了很多帖子&#xff0c;没有找到合适的&#xff0c;自己翻资料移植了一个。 模板工程使用的是正点原子的f407探索版的例程&#xff0c;原子使用的是LAN8720,需要把他的驱动修改成为我们自己用的RTL8201F。 1.将PHY_TYPE改成我…

Unity学习笔记(四)如何实现角色攻击、组合攻击

前言 本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记 实现动画 首先实现角色移动的动画&#xff0c;动画的实现过程在第二篇&#xff0c;这里仅展示效果 attack1 触发攻击动画 实现脚本 接下来我们通过 Animator 编辑动画之间的过渡&#…

redis的集群模式与ELK基础

一、redis的集群模式 1.主从复制 &#xff08;1&#xff09;概述 主从模式&#xff1a;这是redis高可用的基础&#xff0c;哨兵和集群都是建立在此基础之上。 主从模式和数据库的主从模式是一样的&#xff0c;主负责写入&#xff0c;然后把写入的数据同步到从服务器&#xff…

一套比较简单的仓储系统

自己编写的一套比较简单的仓储系统&#xff0c;多仓库&#xff0c;入库、出库、明细、统计等相关功能 基于偌依的单体架构&#xff1a;springbootvueuniapp 后端&#xff1a;https://download.csdn.net/download/qq_30641447/90214834 前端&#xff1a;https://download.csdn…

网络IP协议

IP&#xff08;Internet Protocol&#xff0c;网际协议&#xff09;是TCP/IP协议族中重要的协议&#xff0c;主要负责将数据包发送给目标主机。IP相当于OSI&#xff08;图1&#xff09;的第三层网络层。网络层的主要作用是失陷终端节点之间的通信。这种终端节点之间的通信也叫点…

设计模式 创建型 原型模式(Prototype Pattern)与 常见技术框架应用 解析

原型模式&#xff08;Prototype Pattern&#xff09;是一种创建型设计模式&#xff0c;其核心思想在于通过复制现有的对象&#xff08;原型&#xff09;来创建新的对象&#xff0c;而非通过传统的构造函数或类实例化方式。这种方式在需要快速创建大量相似对象时尤为高效&#x…

MySQL(二)MySQL DDL数据库定义语言

1. MySQL DDL数据库定义语言 1.1. MySQL定义语言 进入MySQL mysql -u root -p(回车后输入密码&#xff0c;即可进入mysq1)1.1.1. 数据库操作 &#xff08;1&#xff09;查看数据库 mysql>show databases;注:MySQL语句分隔符为“&#xff1b;”   mysql库很重要它里面有…

前端页面上传文件:解决 ERR_UPLOAD_FILE_CHANGED

文章目录 引言I 问题 ERR_UPLOAD_FILE_CHANGED问题重现步骤原因II 解决方法将文件转换为base64再转回file检测文件内容是否发生变更III 知识扩展发送一个包含文件和文本的multipart/form-data请求签名优化引言 文件上传应用场景:船舶设备的新增导入(基础信息:出厂编号)船舶…

学术写作中的各种流程图如何利用Kimi进行辅助构建?

目录 1.学术论文中的流程图 2.一些小实践 3.论文中严谨的实践流程 近期小编在思考使用AI工具制作流程图和思维导图&#xff0c;结果发现Kimi现在支持流程图了&#xff0c;Kimi在学术写作中的应用变得更加广泛和深入。随着AIGC技术的不断进步&#xff0c;Kimi的功能将更加强大…

竞品分析对于ASO优化的重要性

产品要想在拥有上千万款App的App Store中脱颖而出的话&#xff0c;要有绝对优势和运营方案&#xff0c;才有可能获得用户的关注。我们要分析和自身产品相似的功能&#xff0c;了解用户获取的途径等&#xff0c;从中获取对竞品有利的因素&#xff0c;明确自身产品的增加和优化方…

什么是485专用屏蔽线

文章目录 一,485屏蔽线基本概念二,485屏蔽线结构及工作原理1、结构2、工作原理三,485屏蔽线的应用一,485屏蔽线基本概念 485屏蔽线,顾名思义,是一种应用于RS-485通信系统的屏蔽线缆。RS_485是一种常用的有线通信协议,具有长距离、高速率、低成本等优点,广泛应用于工业自…

JavaScript BOMDOM

BOM window、document、location、navigator 和 screen等&#xff0c;把它们统称为BOM&#xff08;Browser Object Model&#xff0c;浏览器对象模型&#xff09; window对象 window对象位于BOM的顶层。由浏览器创建&#xff0c;也对应JavaScript本地对象Global &#xff1…

nginx高可用集群搭建

本文介绍nginx高可用集群的搭建。利用keepalived实时检查nginx进程是否存活、keepalived的虚拟ip技术&#xff0c;达到故障转移的目的。终端用户通过访问虚拟ip&#xff0c;感知不到实际发生的故障。架构图如下&#xff1a; 0、环境 Ubuntu&#xff1a;22.04.2 ltsnginx: 1.…

【开源】创建自动签到系统—QD框架

1. 介绍 QD是一个 基于 HAR 编辑器和 Tornado 服务端的 HTTP 定时任务自动执行 Web 框架。 主要通过抓包获取到HAR来制作任务模板&#xff0c;从而实现异步响应和发起HTTP请求 2. 需要环境 2.1 硬件需求 CPU&#xff1a;至少1核 内存&#xff1a;推荐 ≥ 1G 硬盘&#xff1a;推…