Telegram-iOS 源码分析:第五部分(AsyncDisplayKit)

版权声明
本文内容均为搬运,目的只为更方便的学习Telegram编码思维。

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

Telegram-iOS使用AsyncDisplayKit构建大多数UI。AsyncDisplayKit是项目的一个子模块,其中很多功能被移除,一些功能已在Swift中重新实现。本篇文章探讨项目中的组件结构和UI编程模式。

1.概述

AsyncDisplayKit是一个异步UI框架,最初是从Facebook诞生的。它已被Pinterest所采用,并于2017年重命名为Texture。其核心概念是node作为的抽象UIView使用,与React Virtual DOM的想法有点相似。节点是线程安全的,这有助于将复杂的UI操作从主线程中移出,例如图像解码,文本大小调整等。Nodes是轻量级的,这允许你在tables和collections里面不重复使用Cell

part5-asdk.png

如图所示,Telegram-iOS保留约35%的 AsyncDisplayKit代码(蓝色框表示),并删除其他代码。

  • 合并的最新一次提交是 ae2b3af9
  • 基础Nodes比如ASDisplayNode,ASControlNode,ASEditableTextNode,和ASScrollNode大多保持原状。
  • ASImageNode以及其子类被移除。Telegram使用MTProto而非HTTPS从数据中心下载文件。官方版本对网络图片的支持没有用,因此依赖库PINRemoteImage也被删除。
  • 所有官方的node containers被移除,比如ASCollectionNode,ASTableNode,ASViewController等。Tablesview controllers使用Swift重新实现并且移除了对IGListKit库的依赖。
  • Telegram-iOS项目更喜欢手动布局。所以CSS Flexbox的官方布局API被删除,yoga engine同样被删除。
  • 内部日志记录和调试功能支持已删除。

基本上来说,Telegram-iOS保留了最少的核心Node类,然后重写了几百个Node的子类。这些代码散布在子模块(如Display,TelegramUI,ItemListUI)以及支持主要Telegram UI功能的其他子模块中。

2.核心Node

part5-asdisplaynode.png

有一些节点类是构建应用程序用户界面的基本模块。让我们了解一下它们,如图所示。

带箭头的边缘表示右边的Node是左边Node的子类。没有边缘的相同级别的Nodes意味着它们与最左边的Node是相同的父类。

文本

TextNode,ImmediateTextNode和ASTextNode负责文本渲染。

public class TextNode: ASDisplayNode {
    public internal(set) var cachedLayout: TextNodeLayout?
    
    public static func asyncLayout(_ maybeNode: TextNode?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
}

public class ImmediateTextNode: TextNode {
    public var attributedText: NSAttributedString?
    public var textAlignment: NSTextAlignment = .natural
    public var truncationType: CTLineTruncationType = .end
    public var maximumNumberOfLines: Int = 1
    public var lineSpacing: CGFloat = 0.0
    public var insets: UIEdgeInsets = UIEdgeInsets()
    public var textShadowColor: UIColor?
    public var textStroke: (UIColor, CGFloat)?
    public var cutout: TextNodeCutout?
    public var truncationMode: NSLineBreakMode
    public var linkHighlightColor: UIColor?
    public var trailingLineWidth: CGFloat?
    public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
    public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
    public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
    ...
}

public class ASTextNode: ImmediateTextNode {
    override public var attributedText: NSAttributedString? {
        didSet {
            self.setNeedsLayout()
        }
    }
    ...
}

TextNode利用CoreText渲染NSAttributedString。它有一个计算基于行的文本布局的方法calculateLayout,并重写了父类方法draw以渲染文本。公开的方法asyncLayout用来异步调用布局计算并缓存结果。asyncLayout方法是被设计来供上一级调用的。否则,它将不会呈现任何内容,因为缓存的布局为nil。实现支持RTL和Accessibility是值得称赞的。

ImmediateTextNode通过添加更多属性来控制文本布局样式使得TextNode更加丰富。它还支持高亮显示和点击时间。

ASTextNode只需在设置attributedText属性时就是更新布局。这和官方AsyncDisplayKit里的不是同一个,尽管名字一样。

EditableTextNode扩展ASEditableTextNode以支持RTL输入检测。

Image

open class ASImageNode: ASDisplayNode {
    public var image: UIImage?
}

public class ImageNode: ASDisplayNode {
    public func setSignal(_ signal: Signal)
}

open class TransformImageNode: ASDisplayNode {
    public var imageUpdated: ((UIImage?) -> Void)?
    public var contentAnimations: TransformImageNodeContentAnimations = []
    
    public func setSignal(_ signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, attemptSynchronously: Bool = false, dispatchOnDisplayLink: Bool = true)
    public func setOverlayColor(_ color: UIColor?, animated: Bool)
}

ASImageNode渲染UIImage并使用图像的Size作为Node的Size。同样,它和官方AsyncDisplayKit里的不是同一个Node。

ImageNode接受一个Signal来异步设置图像内容。仅由AvatarNode使用,尽管它的名称看起来很普通。

TransformImageNode是异步图片使用最广泛的类。它支持在更改图片时使用alpha动画,并支持颜色叠加。

Button

open class ASButtonNode: ASControlNode {
   public let titleNode: ImmediateTextNode
   public let highlightedTitleNode: ImmediateTextNode
   public let disabledTitleNode: ImmediateTextNode
   public let imageNode: ASImageNode
   public let disabledImageNode: ASImageNode
   public let backgroundImageNode: ASImageNode
   public let highlightedBackgroundImageNode: ASImageNode
}

open class HighlightTrackingButtonNode: ASButtonNode {
   public var highligthedChanged: (Bool) -> Void = { _ in }
}

open class HighlightableButtonNode: HighlightTrackingButtonNode {
   ...
}

ASButtonNode 为具有图片和标题的button,具有三个状态:normal, highlighted, disabled。

HighlightableButtonNode 在点击按钮时添加高亮动画。

Status

ActivityIndicator 模仿UIActivityIndicatorView样式并提供灵活的选项以自定义细节,例如颜色,直径和线宽。

Media

Telegram-iOS实现了一组丰富的组件来支持不同的媒体类型。本文只是大致了解,它值得在本系列中撰写一篇专门的文章,包括FFMpeg集成,第三方视频网站的应用内视频播放,贴纸动画等。

MediaPlayNode是MediaPlayer的子类,用于在AVSampleBufferDisplayLayer上渲染视频帧。

WebEmbedPlayerNode通过嵌入播放网页内的视频WKWebView。它支持来自Youtube,Vimeo,Twitch等的视频。

AnimatedStickerNode播放用于播放来自AnimatedStickerNodeSource的动画。

Bar

SearchBarNode,NavigationBar,TabBarNode,和ToolbarNode模仿了在UIKit中对应的功能。它还消除了各系统版本之间行为不一致的影响,UIKit内部对于开发人员是不可见的,这始终是个不太好的问题。

StatusBar 在系统状态栏区域显示呼叫中的文本通知。

List

ListView是为可滑动列表设计的最复杂的node类之一。正如我们从WWDC 2014中学到的那样,它利用隐藏的UIScrollView并借用其pan手势来获得滚动行为。除了管理列表中元素(无论大小)的可见性之外,它还提供其他简洁的功能,例如方便的项目标题,可自定义的滚动指示器,记录项目,滚动项目,捕捉边界等。

GridNode是另一个用于网格布局的滚动UI组件。它在项目中使用到的场景例如贴纸选择,墙纸设置等功能。

3. Controllers

part5-uiviewcontroller.png

ViewController使UIViewController像nodes层次结构的容器一样工作。与官方node控制器类ASViewController不同,它没有可见深度和智能预载之类的功能。

| @objc open class ViewController: UIViewController, ContainableController { |
|  | // the root content node |
|  | private var _displayNode: ASDisplayNode? |
|  | public final var displayNode: ASDisplayNode { |
|  | get { |
|  | if let value = self._displayNode { |
|  | return value |
|  | } |
|  | else { |
|  | self.loadDisplayNode() |
|  | ... |
|  | return self._displayNode! |
|  | } |
|  | } |
|  | ... |
|  | } |
|  | open func loadDisplayNode() |
|  | open func displayNodeDidLoad() |
|  |  |
|  | // shared components |
|  | public let statusBar: StatusBar |
|  | public let navigationBar: NavigationBar? |
|  | private(set) var toolbar: Toolbar? |
|  | private var scrollToTopView: ScrollToTopView? |
|  | // customizations of navigationBar |
|  | public var navigationOffset: CGFloat |
|  | open var navigationHeight: CGFloat |
|  | open var navigationInsetHeight: CGFloat |
|  | open var cleanNavigationHeight: CGFloat |
|  | open var visualNavigationInsetHeight: CGFloat |
|  | public var additionalNavigationBarHeight: CGFloat |
|  | } |

[view raw](https://gist.github.com/openaphid/23530e81ca4d4fa8336327e092a6ebf0/raw/950b1f1af5d90f02dedec1523f271d145d1b9301/ViewController.swift) [](https://gist.github.com/openaphid/23530e81ca4d4fa8336327e092a6ebf0#file-viewcontroller-swift)

每个ViewController通过一个root node来管理node层次结构,该root node存储在displayNode该类的属性中。loadDisplayNodedisplayNodeDidLoad方法实现我们在UIViewController中熟悉的懒加载行为。

作为基类,它为子类准备了几个共享的node组件:状态栏,导航栏,工具栏和返回到顶部功能。还有一些方便的属性可以自定义其导航栏,这对于普通的UIViewController而言仍然是一个麻烦的问题。

ViewController很少单独使用,项目中有100多个控制器子类用于不同的用户界面。两种UIKit中最常用的容器控制器UINavigationControllerUITabBarController,分别用NavigationControllerTabBarController重新实现。

open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate {
    private var _viewControllers: [ViewController] = []
    // NavigationControllerNode
    private var _displayNode: ASDisplayNode?
    private var theme: NavigationControllerTheme
    
    // manage layout and transition animation
    private func updateContainers(layout rawLayout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
    
    // push with a completion handler
    public func pushViewController(_ controller: ViewController, animated: Bool = true, completion: @escaping () -> Void)
}

// NavigationLayout.swift
enum RootNavigationLayout {
    case split([ViewController], [ViewController])
    case flat([ViewController])
}

// NavigationContainer.swift
final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate
    override func didLoad() {
        // the interactive pop gesture
        let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: ...)
    }
}

NavigationController扩展UINavigationController以借用其公共API,使得它可以像普通的UIViewController一样被使用。它在内部重写了以下所有内容:

  • 直接管理子控制器。由于它只是一个简单的数组,因此可以自由调整以进行堆栈操作。
  • 过渡动画。您可以在ContainedViewLayoutTransition中找到所有动画详细信息。
  • 交互式pop手势。InteractiveTransitionGestureRecognizer可以在整个屏幕范围响应pop手势。
  • 像iPad这样的大屏设备分屏布局。它支持两种类型的布局:flatsplit。最好有一个容器控制器同时支持iPhone和iPad,而不需要花很多精力在容器控制器上UISplitViewController
  • 主题。通过theme属性可以很容易地自定义外观。

TabBarController只能在根屏幕中使用,它是ViewController的子类,而不是UITabBarController,因此不需要保留API。该规则同样适用于ActionSheetController,AlertController和ContextMenuController。这些实现完美地覆盖了系统视图控制器内部细节,在我看来,用户体验几乎相同。

ItemListController相当于UITableViewController管理一个ListView。它还支持自定义overlay node,search view和items排序。

4.布局

AsyncDisplayKit中的Flexbox布局系统已由混合布局机制取代:

// NavigationBar.swift
//   layout in the main thread
open class NavigationBar: ASDisplayNode {
    override open func layout() {
        super.layout()
        
        if let validLayout = self.validLayout, self.requestedLayout {
            self.requestedLayout = false
            self.updateLayout(size: validLayout.0, defaultHeight: validLayout.1, additionalHeight: validLayout.2, leftInset: validLayout.3, rightInset: validLayout.4, appearsHidden: validLayout.5, transition: .immediate)
        }
    }
    func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, transition: ContainedViewLayoutTransition)
}

// TabBarController.swift
//   layout in the main thread
open class TabBarController: ViewController {
    override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
        super.containerLayoutUpdated(layout, transition: transition)
        self.tabBarControllerNode.containerLayoutUpdated(layout, toolbar: self.currentController?.toolbar, transition: transition)
        ...
    }
}


// ListView.swift
//     asynchronously load visible items by the scrolling event
open class ListView: ASDisplayNode, ... {
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        self.updateScrollViewDidScroll(scrollView, synchronous: false)
    }
    private func updateScrollViewDidScroll(_ scrollView: UIScrollView, synchronous: Bool) {
        ...
        self.enqueueUpdateVisibleItems(synchronous: synchronous)
    }
    private func enqueueUpdateVisibleItems(synchronous: Bool) {
        ...
        strongSelf.updateVisibleItemsTransaction(synchronous: synchronous, completion:...)
    }
    private func updateVisibleItemsTransaction(synchronous: Bool, completion: @escaping () -> Void)
}
  • 所有布局都是手动完成的。显然,开发者不喜欢自动布局的概念。
  • 布局计算在简单UI的主线程中运行。布局代码可以放在node的layout方法中,也可以放在视图控制器的containerLayoutUpdated方法中。
  • ListView 为其item nodes构建灵活的布局机制,该机制支持同步和异步计算。

5.结论

Telegram集成AsyncDisplayKit的方式令人印象深刻。它在node之上重建整个UIKit组件生态,以提高效率和自由控制。尽管聊天气泡用户界面很复杂,但是聊天消息列表在旧设备上感觉很流畅。处理系统升级适配的代码很少,每年WWDC之后,大多数开发者总是要花一些“快乐的时光”。

你可能感兴趣的:(Telegram-iOS 源码分析:第五部分(AsyncDisplayKit))