swift中combine框架解析

本文深入解析Objective-C中NSDictionary和NSArray的数据结构实现,探讨了哈希表的拉链法解决冲突,以及 NSArray的动态扩容机制。理解这些原理有助于提升对Apple框架底层的理解和性能优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在Swift中,Combine 框架是在 iOS 13 及更高版本中推出的,Combine框架是一个用于处理异步编程的模式,它提供了一套强大的工具,用于组合异步事件流(例如网络请求、用户输入、定时器等)。让代码更加简洁、易读和可维护。它的核心在于 Publisher-Subscriber 模式以及强大的操作符链,能够高效地处理各种异步场景。

一、核心概念

  • Publisher:作为数据流的源头,它会产生一系列事件。
  • Subscriber:负责接收 Publisher 发出的事件。
  • Operator:属于中间处理节点,能够对数据流进行转换、过滤等操作。
  • Subscription:用于管理 Publisher 和 Subscriber 之间的连接。

二、主要组件

1.Publisher

Publisher是一个可以发送值、完成信号或错误的类型。它代表了异步数据流的生产者。例如,一个网络请求可以是一个Publisher,它会发送接收到的数据,或者在请求失败时发送错误。

有两种基本类型:

  • Just:仅发送单个值然后结束。
  • Empty:不发送任何值就直接结束。

另外,很多 Swift 原生类型都实现了 Publisher 协议,比如ArrayURLSession

let publisher1 = Just("Hello ,Combine")
        let subscriber1 = publisher1.sink { completion in
            print("完成:", completion)
        } receiveValue: { receiveValue in
            print("接收到值:", receiveValue)
            sleep(3)//添加3秒延迟,在3秒后才走到completion里面
        }
        
        
        let publisher2 = Empty<Any, Never>()
        let subscriber2 = publisher2.sink { completion in
            print("空发布者完成状态: \(completion)")
        } receiveValue: { _ in
            // 此闭包不会执行
            print("接收到值")
        }
        
        /**
         打印
         接收到值: Hello ,Combine
         完成: finished
         */
        
        /**
         打印
         空发布者完成状态: finished
         */
        
        let failPublisher1 = Fail<Int,CustomError>(error: .sampleError)
        failPublisher1.sink { completion in
            if case let .failure(error) = completion {
                print("接收到的错误: \(error)")
            }
        } receiveValue: { _ in
            // 此闭包不会执行
        }
        /**
         打印
         接收到的错误: sampleError
         */
        

2.Subscriber

Subscriber是一个可以接收来自Publisher值的类型。它定义了如何处理这些值(例如,更新UI或处理数据)。

  • Sink:通过闭包来处理接收到的值和完成事件。
  • Assign:可以将接收到的值赋给对象的属性。

Subscriber 是一个协议,定义了订阅者必须实现的方法,用于与发布者建立连接并处理其发送的内容:

public protocol Subscriber {
    // 订阅者接收的值的类型
    associatedtype Input
    // 订阅者可能接收的错误类型(必须遵循 Error 协议)
    associatedtype Failure: Error
    
    // 发布者调用此方法,向订阅者发送值
    func receive(_ input: Input) -> Subscribers.Demand
    
    // 发布者调用此方法,向订阅者发送完成信号(成功或失败)
    func receive(completion: Subscribers.Completion<Failure>)
    
    // 发布者调用此方法,与订阅者建立正式连接
    func receive(subscription: Subscription)
}

关键角色

  1. 接收数据:通过 receive(_:) 方法接收发布者发送的单个值。
  2. 处理完成:通过 receive(completion:) 方法处理发布者的终止信号(.finished 或 .failure)。
  3. 建立连接:通过 receive(subscription:) 方法接收 Subscription 对象,用于控制数据需求(如请求更多数据)。
  4. 控制需求receive(_:) 方法的返回值 Subscribers.Demand 用于告诉发布者还能接收多少数据(背压控制)。

常用订阅者实现

1. sink:闭包式订阅者

最常用的订阅方式,通过闭包处理接收的值和完成事件:

 let publisher = [1, 2, 3].publisher
        
        // 使用 sink 订阅
        let cancellable = publisher.sink { completion in
            switch completion {
                   case .finished:
                       print("数据流完成")
                   case .failure(let error):
                       print("发生错误:\(error)")
                   }
        } receiveValue: { value in
            print("接收的值:\(value)")
        }

        // 输出:
        // 接收的值:1
        // 接收的值:2
        // 接收的值:3
        // 数据流完成

2. assign:绑定属性的订阅者

1. assign(to:on:) 基础示例

import Combine

class User {
    var name: String = "默认名称"
    var age: Int = 0
}

let user = User()
let namePublisher = ["Alice", "Bob", "Charlie"].publisher
let agePublisher = (20...22).publisher

// 将名字绑定到 user.name
let nameCancellable = namePublisher
    .assign(to: \.name, on: user)
    .store(in: &cancellables)

// 将年龄绑定到 user.age
let ageCancellable = agePublisher
    .assign(to: \.age, on: user)
    .store(in: &cancellables)

print(user.name) // 输出:Charlie(最后一个值)
print(user.age)  // 输出:22(最后一个值)

2. assign(to:) 示例(Swift 5.5+)

在当前作用域中直接绑定属性:

class ProfileViewModel {
    var username: String = ""
    var cancellables = Set<AnyCancellable>()
    
    func setup() {
        // 模拟网络请求获取用户名
        Just("SwiftDeveloper")
            .assign(to: &$username) // 直接绑定到当前对象的 username 属性
    }
}

 线程注意事项:默认在发布者发送值的线程更新属性,如果需要在主线程更新 UI,需配合 receive(on:)

URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .receive(on: DispatchQueue.main) // 切换到主线程
    .assign(to: \.data, on: self)
    .store(in: &cancellables)

错误处理

assign 要求发布者的错误类型必须是 Never(即不会发送错误)。如果发布者可能发送错误,需要先通过 catch 等操作符处理:

enum MyError: Error {
    case invalidData
}

// 可能发送错误的发布者
let failingPublisher = Fail<String, MyError>(error: .invalidData)

// 先处理错误,再使用 assign
failingPublisher
    .catch { _ in Just("默认值") } // 将错误转换为默认值
    .assign(to: \.name, on: user)
    .store(in: &cancellables)

手动实现 Subscriber 协议:

import Combine

// 自定义订阅者:只接收偶数
class EvenNumberSubscriber: Subscriber {
    // 接收 Int 类型的值,不处理错误
    typealias Input = Int
    typealias Failure = Never
    
    func receive(subscription: Subscription) {
        // 初始请求 2 个值
        subscription.request(.max(2))
    }
    
    func receive(_ input: Int) -> Subscribers.Demand {
        if input % 2 == 0 {
            print("接收偶数:\(input)")
            // 每接收一个偶数,再请求 1 个值
            return .max(1)
        } else {
            print("忽略奇数:\(input)")
            // 忽略奇数时不请求新值
            return .none
        }
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        print("完成:\(completion)")
    }
}

// 使用自定义订阅者
let numbers = [1, 2, 3, 4, 5, 6].publisher
let subscriber = EvenNumberSubscriber()
numbers.subscribe(subscriber)

// 输出:
// 忽略奇数:1
// 接收偶数:2
// 忽略奇数:3
// 接收偶数:4
// 忽略奇数:5
// 接收偶数:6
// 完成:finished

 背压控制(Demand)

Subscribers.Demand 用于控制订阅者能接收的数据量,避免数据过载,常见类型:

  • .none:不请求更多数据
  • .max(n):最多再接收 n 个数据
  • .unlimited:接收无限量数据(默认行为)

例如,在 receive(subscription:) 中设置初始需求:

func receive(subscription: Subscription) {
    // 初始请求 10 个数据
    subscription.request(.max(10))
}

生命周期管理

订阅者必须通过 AnyCancellable 管理生命周期,否则可能导致提前取消或内存泄漏:

 

class MyViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func setup() {
        Just("Hello")
            .sink { print($0) }
            .store(in: &cancellables) // 自动管理订阅生命周期
    }
}

     

    3.Operator

    Operator是对Publisher的操作,它允许你转换、过滤、组合和响应数据流。例如,你可以使用map来转换数据流中的值,使用filter来过滤值,或者使用combineLatest来组合多个数据流。

    • 转换类:像mapflatMap等。
    • 过滤类:例如filterremoveDuplicates
    • 合并类:如zipmerge
    • 时间类:像debouncethrottle

    1. 转换操作符(Transforming)

    对数据流中的值进行转换处理。

    操作符作用示例
    map将值从一种类型转换为另一种类型[1,2,3].publisher.map { $0 * 2 } → 输出 2,4,6
    flatMap将值转换为新的发布者,并合并其输出嵌套发布者展平处理
    compactMap过滤 nil 并解包可选值[1,nil,3].publisher.compactMap { $0 } → 输出 1,3
    mapError将错误类型转换为另一种错误类型统一错误处理

     示例:flatMap 处理嵌套发布者

    import Combine
    
    // 模拟每个用户ID对应的详情请求
    func fetchUserDetails(id: Int) -> AnyPublisher<String, Never> {
        Just("User \(id) Details")
            .delay(for: .seconds(1), scheduler: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
    // 转换用户ID流为用户详情流
    let userIds = [1, 2, 3].publisher
    userIds
        .flatMap { id in
            fetchUserDetails(id: id)
        }
        .sink { print($0) }
        .store(in: &cancellables)
    
    // 输出(间隔1秒):
    // User 1 Details
    // User 2 Details
    // User 3 Details

    2. 过滤操作符(Filtering)

    筛选或限制数据流中的值。

     

    操作符作用示例
    filter只保留满足条件的值[1,2,3,4].publisher.filter { $0 % 2 == 0 } → 输出 2,4
    removeDuplicates过滤连续重复的值[1,1,2,2,3].publisher.removeDuplicates() → 输出 1,2,3
    first(where:)只取第一个满足条件的值取第一个偶数
    last(where:)只取最后一个满足条件的值取最后一个偶数
    prefix只取前 n 个值prefix(2) 取前两个值

    示例:filter 与 removeDuplicates

    let numbers = [1, 2, 2, 3, 4, 4, 5].publisher
    numbers
        .filter { $0 > 2 }       // 保留大于2的值
        .removeDuplicates()      // 去重连续重复值
        .sink { print($0) }      // 输出:3,4,5
        .store(in: &cancellables)

    3. 组合操作符(Combining)

    将多个数据流合并或关联。

    操作符作用示例
    merge合并多个同类型发布者的输出合并两个整数流
    zip按顺序配对多个发布者的值(如第 n 个值配对)配对姓名和年龄流
    combineLatest当任意一个发布者发送新值时,组合所有发布者的最新值实时组合多个输入框的内容
    prepend在数据流前添加值或其他发布者的输出在现有流前插入初始值

    示例:combineLatest 实时组合数据

     

    let username = PassthroughSubject<String, Never>()
    let password = PassthroughSubject<String, Never>()
    
    // 组合最新的用户名和密码
    username
        .combineLatest(password)
        .sink { username, password in
            print("当前输入: 用户名=\(username), 密码=\(password)")
        }
        .store(in: &cancellables)
    
    username.send("alice")    // 输出:当前输入: 用户名=alice, 密码=(等待密码)
    password.send("123")      // 输出:当前输入: 用户名=alice, 密码=123
    username.send("bob")      // 输出:当前输入: 用户名=bob, 密码=123

    4. 时序操作符(Timing)

    控制数据发送的时间或节奏。

     

    操作符作用示例
    delay延迟发送所有值延迟 1 秒发送
    debounce等待指定时间无新值后,只发送最后一个值搜索输入防抖(停止输入后再请求)
    throttle指定时间内只发送第一个 / 最后一个值按钮点击节流
    measureInterval测量值之间的时间间隔计算帧率
    let searchInput = PassthroughSubject<String, Never>()
    
    // 输入停止0.5秒后才发送请求
    searchInput
        .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
        .sink { query in
            print("搜索请求: \(query)")
        }
        .store(in: &cancellables)
    
    // 模拟快速输入
    searchInput.send("sw")
    searchInput.send("swi")
    searchInput.send("swif")  // 0.5秒内无新输入,最终发送 "swif"

    5. 错误处理操作符(Error Handling)

    处理数据流中的错误。

    操作符作用示例
    catch捕获错误并返回备用发布者网络错误时返回本地缓存
    replaceError用默认值替换错误错误时返回空字符串
    retry错误时重试指定次数网络请求失败重试 3 次
    mapError将错误类型转换为另一种统一错误类型

    示例:retry 与 catch 处理网络错误

    enum NetworkError: Error {
        case timeout
    }
    
    // 模拟可能失败的网络请求
    func fetchData() -> AnyPublisher<String, NetworkError> {
        var attempt = 0
        return Future { promise in
            attempt += 1
            print("尝试第\(attempt)次请求")
            if attempt < 3 {
                promise(.failure(.timeout)) // 前2次失败
            } else {
                promise(.success("数据加载成功")) // 第3次成功
            }
        }
        .eraseToAnyPublisher()
    }
    
    fetchData()
        .retry(2)                // 失败时重试2次(共3次尝试)
        .catch { error in
            Just("使用缓存数据")   // 仍失败则返回缓存
        }
        .sink { print($0) }
        .store(in: &cancellables)
    
    // 输出:
    // 尝试第1次请求
    // 尝试第2次请求
    // 尝试第3次请求
    // 数据加载成功

    6. 生命周期操作符(Lifecycle)

    控制数据流的生命周期。

    操作符作用示例
    eraseToAnyPublisher隐藏发布者具体类型,返回 AnyPublisher封装内部实现,暴露统一接口
    breakpoint调试用,满足条件时触发断点特定值时中断调试
    handleEvents监听数据流各阶段事件(订阅、接收值、完成等)打印日志或执行副作用

    示例:handleEvents 监听数据流事件 

    [1,2,3].publisher
        .handleEvents(
            receiveSubscription: { _ in print("开始订阅") },
            receiveOutput: { value in print("即将发送: \(value)") },
            receiveCompletion: { _ in print("流完成") },
            receiveCancel: { print("订阅被取消") },
            receiveRequest: { demand in print("请求数据量: \(demand)") }
        )
        .sink { print("接收值: \($0)") }
        .store(in: &cancellables)

    操作符链的执行逻辑

    操作符链式调用时,数据会按顺序流经每个操作符: 

     

    publisher
        .map { $0 * 2 }       // 1. 先转换
        .filter { $0 > 5 }    // 2. 再过滤
        .debounce(...)        // 3. 再控制时序
        .sink { ... }         // 4. 最后处理

    每个操作符都会返回一个新的发布者,因此链中的每个步骤都是上下游关系。 

    合理使用操作符可以简化异步代码,避免 “回调地狱”,使逻辑更易读、易维护。

    4.Subject

    SubjectPublisherSubscriber的结合体,它既可以发送值也可以接收值。它可以用来在数据流中注入新的值或者控制何时发送值。例如,一个PassthroughSubject可以作为一个手动控制的异步事件源。

    • PassthroughSubject:只会转发接收到的最新值。
    • CurrentValueSubject:会存储当前值,并且会向新的订阅者发送这个当前值。
     4.1 PassthroughSubject
    //手动控制发布者
            let subject = PassthroughSubject<String, CustomError>()
            
            let subscription = subject.sink { completion in
                print("订阅1完成:", completion)
            } receiveValue: { value in
                print("订阅1接收到:", value)
            }
            
            subject.send("第一个消息")  // 输出:订阅1接收到: 第一个消息
            
            let subscription2 = subject.sink { completion in
                print("订阅2完成:", completion)
            } receiveValue: { value in
                print("订阅2接收到:", value)
            }
            
            subject.send("第二个消息")  // 输出:订阅1接收到: 第二个消息,订阅2接收到: 第二个消息
            //.finished和.failure二选一
            //subject.send(completion: .failure(.networkFailure))
            subject.send(completion: .finished)
    
    打印:
    订阅1接收到: 第一个消息
    订阅2接收到: 第二个消息
    订阅1接收到: 第二个消息
    订阅2完成: finished
    订阅1完成: finished

    实际应用场景

    1. 事件传递

    适合传递一次性事件(如按钮点击、状态变化通知等)

    class ButtonViewModel {
        // 按钮点击事件流
        private let tapSubject = PassthroughSubject<Void, Never>()
        
        // 提供外部订阅接口
        var tapPublisher: AnyPublisher<Void, Never> {
            tapSubject.eraseToAnyPublisher()
        }
        
        // 模拟按钮点击
        func buttonTapped() {
            tapSubject.send(())
        }
    }
    
    // 使用
    let viewModel = ButtonViewModel()
    viewModel.tapPublisher
        .sink {
            print("按钮被点击了")
        }
        .store(in: &cancellables)
    
    viewModel.buttonTapped()  // 输出:按钮被点击了

    2.数据转发

    作为数据流转节点,转发其他发布者的数据:

     

    // 模拟网络请求发布者
    let networkPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com")!)
        .map { $0.data }
        .eraseToAnyPublisher()
    
    // 创建PassthroughSubject转发数据
    let dataSubject = PassthroughSubject<Data, URLError>()
    
    // 订阅网络请求并转发数据
    networkPublisher
        .subscribe(dataSubject)
        .store(in: &cancellables)
    
    // 订阅subject获取数据
    dataSubject
        .sink(
            receiveCompletion: { completion in
                print("数据接收完成:\(completion)")
            },
            receiveValue: { data in
                print("收到数据:\(data.count)字节")
            }
        )
        .store(in: &cancellables)

     

    4.2 CurrentValueSubject
    let currentSubject = CurrentValueSubject<String, CustomError>("初始值")
            // 第一个订阅者
            let subscriber1 = currentSubject.sink { completion in
                print("订阅1完成:", completion)
            } receiveValue: { value in
                print("订阅1接收到:", value)
            }
            // 输出:订阅者1收到:初始值(新订阅者立即收到当前值)
            
            // 发送新值
            currentSubject.send("更新值1")
            // 输出:订阅者1收到:更新值1
            
            let subscriber2 = currentSubject.sink { completion in
                print("订阅2完成:", completion)
            } receiveValue: { value in
                print("订阅2接收到:", value)
            }
            // 输出:订阅1接收到: 更新值1 订阅者2收到:更新值1(新订阅者收到当前值)
            
            // 直接访问当前值
            print("当前值:\(currentSubject.value)") // 输出:当前值:更新值1
            
            // 发送完成信号
            currentSubject.send(completion: .finished)
    
            // 之后的发送会被忽略
            currentSubject.send("更新值2") // 无输出
            
            /**
             订阅1接收到: 初始值
             订阅1接收到: 更新值1
             订阅2接收到: 更新值1
             当前值:更新值1
             订阅2完成: finished
             订阅1完成: finished
             */

    实际应用场景

    1. 状态管理

    适合存储和传递应用状态(如用户信息、设置等):

    class UserManager {
        // 存储当前用户状态,初始为未登录
        let currentUser = CurrentValueSubject<User?, Never>(nil)
        
        func login(user: User) {
            currentUser.send(user) // 更新当前用户
        }
        
        func logout() {
            currentUser.send(nil) // 清除当前用户
        }
    }
    
    struct User {
        let id: String
        let name: String
    }
    
    // 使用
    let userManager = UserManager()
    userManager.currentUser.sink { user in
        if let user = user {
            print("当前登录用户:\(user.name)")
        } else {
            print("未登录")
        }
    }
    
    userManager.login(user: User(id: "1", name: "张三")) // 输出:当前登录用户:张三

    2. 与 UI 绑定

    在 UIKit/SwiftUI 中绑定界面元素:

    // SwiftUI示例
    class SettingsViewModel: ObservableObject {
        let volume = CurrentValueSubject<Double, Never>(0.5)
        
        func increaseVolume() {
            let newValue = min(volume.value + 0.1, 1.0)
            volume.send(newValue)
        }
    }
    
    struct VolumeView: View {
        @StateObject var viewModel = SettingsViewModel()
        @State var currentVolume: Double = 0
        
        var body: some View {
            VStack {
                Text("音量:\(currentVolume, specifier: "%.1f")")
                Button("增加音量") {
                    viewModel.increaseVolume()
                }
            }
            .onReceive(viewModel.volume) { newVolume in
                currentVolume = newVolume
            }
        }
    }

    3. 数据缓存

    临时存储最新数据,供新订阅者使用

     

    class DataRepository {
        private let latestData = CurrentValueSubject<DataModel?, Error>(nil)
        private var cancellables = Set<AnyCancellable>()
        
        func fetchData() {
            // 模拟网络请求
            URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
                .map { $0.data }
                .decode(type: DataModel.self, decoder: JSONDecoder())
                .sink(
                    receiveCompletion: { [weak self] completion in
                        if case let .failure(error) = completion {
                            self?.latestData.send(completion: .failure(error))
                        }
                    },
                    receiveValue: { [weak self] data in
                        self?.latestData.send(data) // 缓存最新数据
                    }
                )
                .store(in: &cancellables)
        }
        
        // 提供数据订阅接口
        func dataPublisher() -> AnyPublisher<DataModel?, Error> {
            return latestData.eraseToAnyPublisher()
        }
    }
    4.3 PassthroughSubject与 PassthroughSubject 的对比
    特性CurrentValueSubjectPassthroughSubject
    初始值需要提供不需要
    存储当前值
    新订阅者立即收到当前值只收到订阅后的新值
    访问当前值通过 value 属性无法直接访问

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值