Telegram-iOS 源码分析:第二部分(SSignalKi

2020-11-11  本文已影响0人  灰原丶逗
版权声明
本文内容均为搬运,目的只为更方便的学习Telegram编码思维。

如需查阅原作者文章,附赠原文章机票

介绍

Ťelegram-iOS在大多数模块中使用响应式编程。有三个框架可以在项目内部实现响应功能:

设计

信号(Signal)

Signal是捕获“随时间变化”概念的一个类。其特点如下所示:

// 伪代码
public final class Signal<T, E> {
    public init(_ generator: @escaping(Subscriber<T, E>) -> Disposable)
    
    public func start(next: ((T) -> Void)! = nil, 
                      error: ((E) -> Void)! = nil, 
                      completed: (() -> Void)! = nil) -> Disposable
}

为了创建一个Signal,它接受一个generator闭包,该闭包定义了生成数据(<T>),捕获异常(<E>)和更新完成状态的方式。一旦创建好,方法start就可以注册观察者闭包。

订阅(Subscriber)

Subscriber具有考虑线程安全性的逻辑,将数据分发给每个观察者闭包。

// 伪代码
public final class Subscriber<T, E> {
    private var next: ((T) -> Void)!
    private var error: ((E) -> Void)!
    private var completed: (() -> Void)!
    
    private var terminated = false
    
    public init(next: ((T) -> Void)! = nil, 
                error: ((E) -> Void)! = nil, 
                completed: (() -> Void)! = nil)
    
    public func putNext(_ next: T)
    public func putError(_ error: E)
    public func putCompletion()
}

当出现错误或执行完成时,订阅将被终止。此状态不可逆。

putNext将新数据发送到next闭包,只要订阅没有终止
putErrorerror闭包发送错误并终止订阅
putCompletion调用completed闭包终止订阅

操作符(Operators)

Signal定义了一系列的操作符来服务基础函数。这些基础函数根据它们的功能被划分为几类:CatchCombineDispatchLoopMappingMetaReduceSideEffectsSingleTake,和Timing
以一些映射操作符为例:

public func map<T, E, R>(_ f: @escaping(T) -> R) -> (Signal<T, E>) -> Signal<R, E>
public func filter<T, E>(_ f: @escaping(T) -> Bool) -> (Signal<T, E>) -> Signal<T, E>
public func flatMap<T, E, R>(_ f: @escaping (T) -> R) -> (Signal<T?, E>) -> Signal<R?, E>
public func mapError<T, E, R>(_ f: @escaping(E) -> R) -> (Signal<T, E>) -> Signal<T, R>

像操作符map()一样,进行转换闭包并返回一个函数以更改Signal的数据类型。

有一个方便的|>操作员可以将这些操作符像管道一样链接起来:

//自定义操作符   |>
precedencegroup PipeRight {
    associativity: left
    higherThan: DefaultPrecedence
}

infix operator |> : PipeRight

public func |> <T, U>(value: T, function: ((T) -> U)) -> U {
    return function(value)
}

该操作符|>也许是受到JavaScript中建议的管道操作启发。通过Swift的结尾闭包支持,可以直观地读取所有操作符的流水线:

// 伪代码
let anotherSignal = valueSignal
    |> filter { value -> Bool in
      ...
    }
    |> take(1)
    |> map { value -> AnotherValue in
      ...
    }
    |> deliverOnMainQueue

队列(Queue)

Queue类是在GCD之上的封装,用于管理用于在Signal中调度数据的队列。一般情况下,共有三个预设队列:globalMainQueue, globalDefaultQueue,globalBackgroundQueue。我认为没有任何机制可以避免过度分配到队列。

Disposable

Disposable协议定义了可以处理的东西。它通常与释放资源或取消任务相关。有四个类实现了这一协议,可以覆盖大多数使用情况,这四个类分别是:ActionDisposableMetaDisposableDisposableSet,和DisposableDict

Promise

Promise类和ValuePromise类是为多个观察者依赖同一个数据源的情况而构建的。Promise支持使用Signal来更新数据值,而ValuePromise定义为可以直接接受值更改。

用例

让我们查看项目中的一些实际用例,这些用例演示了SwiftSignalKit的使用模式。

#1请求授权

iOS强制应用程序在访问设备上的敏感信息(例如联系人相机位置等)之前,先向用户请求授权。在与朋友聊天时,Telegram-iOS具有将您的位置作为消息发送的功能。让我们看看它如何通过Signal获得位置授权。

工作流是可以由SwiftSignalKit建模的标准异步任务。DeviceAccess.swift的内部函数authorizationStatus返回一个Signal以检查当前授权状态:

public enum AccessType {
    case notDetermined
    case allowed
    case denied
    case restricted
    case unreachable
}

public static func authorizationStatus(subject: DeviceAccessSubject) -> Signal<AccessType, NoError> {
    switch subject {
        case .location:
            return Signal { subscriber in
                let status = CLLocationManager.authorizationStatus()
                switch status {
                    case .authorizedAlways, .authorizedWhenInUse:
                        subscriber.putNext(.allowed)
                    case .denied, .restricted:
                        subscriber.putNext(.denied)
                    case .notDetermined:
                        subscriber.putNext(.notDetermined)
                    @unknown default:
                        fatalError()
                }
                subscriber.putCompletion()
                return EmptyDisposable
            }
    }
}

LocationPickerControllerpresent出来时,它将观察来自authorizationStatus的信号,并在未确定许可的情况下调用DeviceAccess.authrizeAccess

Signal.start返回一个实例Disposable。最好的做法是将其保存在字段变量中,然后在deinit方法中释放。

override public func loadDisplayNode() {
    ...

    self.permissionDisposable = 
            (DeviceAccess.authorizationStatus(subject: .location(.send))
            |> deliverOnMainQueue)
            .start(next: { [weak self] next in
        guard let strongSelf = self else {
            return
        }
        switch next {
        case .notDetermined:
            DeviceAccess.authorizeAccess(
                    to: .location(.send),
                    present: { c, a in
                        // present an alert if user denied it
                        strongSelf.present(c, in: .window(.root), with: a)
                    },
                    openSettings: {
                       // guide user to open system settings
                        strongSelf.context.sharedContext.applicationBindings.openSettings()
                    })
        case .denied:
            strongSelf.controllerNode.updateState { state in
                var state = state
                // change the controller state to ask user to select a location
                state.forceSelection = true 
                return state
            }
        default:
            break
        }
    })
}

deinit {
    self.permissionDisposable?.dispose()
}

#2更改用户名

让我们来看一个更复杂的例子。Telegram允许每个用户更改UsernameSetupController中具有唯一性的用户名。用户名用于生成公共链接,以供其他人搜索到您。

part-2-username.png
实现应符合以下要求:

随时间变化的数据源共有三个:主题,当前帐户和编辑状态。主题和帐户是项目中的基本数据组件,因此有专用的信号:SharedAccountContext.presentationDataAccount.viewTracker.peerView。我将尝试在其他帖子中介绍它们。让我们集中讨论如何使用Signal逐步建模编辑状态。

  1. 结构体UsernameSetupControllerState定义了三个元素:正在输入的文本,验证状态和更新标志。并且提供了一些辅助方法来更新它并获取新实例。
struct UsernameSetupControllerState: Equatable {
    let editingPublicLinkText: String?
    let addressNameValidationStatus: AddressNameValidationStatus?
    let updatingAddressName: Bool
    ...
    
    func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?)
        -> UsernameSetupControllerState {
        return UsernameSetupControllerState(
                   editingPublicLinkText: editingPublicLinkText, 
                   addressNameValidationStatus: self.addressNameValidationStatus, 
                   updatingAddressName: self.updatingAddressName)
    }
    
    func withUpdatedAddressNameValidationStatus(
        _ addressNameValidationStatus: AddressNameValidationStatus?) 
        -> UsernameSetupControllerState {
        return UsernameSetupControllerState(
                   editingPublicLinkText: self.editingPublicLinkText, 
                   addressNameValidationStatus: addressNameValidationStatus, 
                   updatingAddressName: self.updatingAddressName)
    }
}

enum AddressNameValidationStatus : Equatable {
    case checking
    case invalidFormat(TelegramCore.AddressNameFormatError)
    case availability(TelegramCore.AddressNameAvailability)
}

2.状态更改通过ValuePromise里的statePromise传播,它还提供了一种简洁的功能来省略重复的数据更新。还有一个stateValue保持最新状态,因为ValuePromise里的数据是不能访问的外面。这是项目内部常见的模式,即promise valuestate value相伴。

let statePromise = ValuePromise(UsernameSetupControllerState(), ignoreRepeated: true) 
let stateValue = Atomic(value: UsernameSetupControllerState()) 

3.验证过程可以在管道信号(piped Signal )中实现。操作符delay将请求保留0.3秒的延迟。对于快速键入的场景,步骤4中的设置将取消先前未发送的请求。

public enum AddressNameValidationStatus: Equatable {
    case checking
    case invalidFormat(AddressNameFormatError)
    case availability(AddressNameAvailability)
}

public func validateAddressNameInteractive(name: String)
                -> Signal<AddressNameValidationStatus, NoError> {
    if let error = checkAddressNameFormat(name) { // local check
        return .single(.invalidFormat(error))
    } else {
        return .single(.checking) // start to request backend
                |> then(addressNameAvailability(name: name) // the request
                |> delay(0.3, queue: Queue.concurrentDefaultQueue()) // in a delayed manner
                |> map { .availability($0) } // convert the result
        )
    }
}

4. 使用MetaDisposable持有信号,当TextFieldNodetext发生变化时,更新statePromisestateValue的数据。当调用checkAddressNameDisposable.set()时,前一个在第三步中触发操作符delayMetaDisposable在内部取消任务。

TextFieldNodeASDisplayNode的子类,并包装UITextField以进行文本输入。Telegram-iOS利用AsyncDisplayKit的异步呈现机制来使其复杂的消息UI平滑和响应。

let checkAddressNameDisposable = MetaDisposable()
...

if text.isEmpty {
    checkAddressNameDisposable.set(nil)
    statePromise.set(stateValue.modify {
        $0.withUpdatedEditingPublicLinkText(text)
          .withUpdatedAddressNameValidationStatus(nil)
    })
} else {
    checkAddressNameDisposable.set(
        (validateAddressNameInteractive(name: text) |> deliverOnMainQueue)
                .start(next: { (result: AddressNameValidationStatus) in
            statePromise.set(stateValue.modify {
                $0.withUpdatedAddressNameValidationStatus(result)
            })
        }))
}

5.combineLatest如果更改了三个信号,则操作员将三个信号组合起来以更新控制器UI。

let signal = combineLatest(
                 presentationData, 
                 statePromise.get() |> deliverOnMainQueue, 
                 peerView) {
  // update navigation button
  // update controller UI
}

结论

SSignalKit是Telegram-iOS的响应式编程解决方案。核心组件(如SignalPromise)与其他响应式框架的实现方式稍有不同。它已在各个模块中广泛使用,以将UI与数据更改连接起来。

设计鼓励大量使用闭包。有许多相互嵌套的闭包,这些闭包使很远的行得到缩进。该项目还喜欢将许多操作公开为灵活性的闭包。Telegram工程师如何保持代码质量并轻松调试信号,这仍然是我的课题。

上一篇下一篇

猜你喜欢

热点阅读