当前位置: 代码迷 >> 综合 >> AsyncDisplaykit(Texture)技术分享
  详细解决方案

AsyncDisplaykit(Texture)技术分享

热度:64   发布时间:2024-01-09 15:26:57.0

AsyncDisplaykit(Texture)技术分享

官方Texture文档:https://texturegroup.org/docs/getting-started.html

github: https://github.com/TextureGroup/Texture
可以下载github里example的代码看

UIKit的绘制机制图解

CALayerdisplay方法由系统调用,用来更新layer的内容,如果layerdelegate对象,那么display方法将尝试调用delegatedisplayer:方法来更新layer的内容。如果delegate没有实现displaylayer:方法,则这个方法会创建一个backing store来保存原来的内容,然后调用layerdrawInContext:方法来填充back store。最后以新的back store替换layer之前内容达到更新layer的目的。通常UIKitCAlayerdelegateUIView对象

两种方式自定义CAlayer的内容

请添加图片描述

有时一个 layer会包含很多 sub-layer,而这些sub-layer并不需要响应触摸事件,也不需要进行动画和位置调整。ASDK为此实现了一个被称为pre-composing的技术,可以把这些sub-layer合成渲染为一张图片。开发时,ASNode已经替代了UIViewCALayer;直接使用各种Node控件并设置为layer backed后,ASNode甚至可以通过预合成来避免创建内部的UIView和CALayer。
通过这种方式,把一个大的层级,通过一个大的绘制方法绘制到一张图上,性能会获得很大提升。CPU避免了创建UIKit对象的资源消耗,GPU避免了多张texture合成和渲染的消耗,更少的bitmap也意味着更少的内存占用。

Node异步绘制

请添加图片描述

RunLoop机制

Run Loops运行循环。一个run loop是用来在先冲上管理事件异步到达的基础设施。一个run loop为线程监测一个或多个事件源。当事件到达的时候,系统唤醒线程并调度事件到run loop,然后分配给指定程序。如果没有事件出现和准备处理,run loop把线程置于休眠状态。

请添加图片描述

iOS一个runloop循环为1/60秒

ASNode把任务用ASAsyncTransaction(Group)封装并提交到一个全局的容器,并注册一个比coreAnimation优先级低的Observe,coreAnimation执行任务后再执行Node的任务的内容,并在合适的机会异步并发同步到主线程

Texture允许您将图像解码、文本大小和渲染以及其他昂贵的UI操作从主线程上移动,以保持主线程响应用户交互。ASLayoutSpec为每个节点提供一个来执行异步测量和布局

texture使用Flex布局(不是很懂,布局格式可以参考https://bawn.github.io/2017/12/Texture-Layout/?utm_source=tuicool&utm_medium=referral)

看图看似比autolayout快多了,跟frame布局相差无几

img

import AsyncDisplayKit 开启texture库

UIKit的控件和AS控件对应

Texture UIKit
ASDisplayNode UIView
ASCellNode UITableViewCell/UICollectionViewCell
ASTextNode UILabel
ASImageNode UIImageView
ASNetworkImageNode UIImageView
ASVideoNode AVPlayerLayer
ASControlNode UIControl
ASScrollNode UIScrollView
ASControlNode UIControl
ASEditableTextNode UITextView
ASMultiplexImageNode UIImageView
UITableView ASTableView
UICollectionView ASCollectionView
Texture UIKit
ASViewController UIViewController
ASTableNode UITableView
ASCollectionNode UICollectionView
ASPagerNode UICollectionView

使用ASViewController的好处:

  1. 保存内存。屏幕关闭的ASViewController将自动缩小其任何子数据的获取数据的大小和显示范围。这是大型应用程序内存管理的关键。
  2. ASVisibility功能。当在ASNavigationControllerASTabBarController中使用时,这些类知道使视图控制器可见所需的用户点击的确切数量。

布局

https://bawn.github.io/2017/12/Texture-Layout/?utm_source=tuicool&utm_medium=referral

https://www.jianshu.com/p/ecb682a6cde0

    • 布局规则 说明
      ASInsetLayoutSpec 插入布局
      ASOverlayLayoutSpec 覆盖布局
      ASBackgroundLayoutSpec 背景布局
      ASCenterLayoutSpec 中心布局
      ASRatioLayoutSpec 比例布局
      ASStackLayoutSpec 堆叠布局
      ASAbsoluteLayoutSpec 绝对布局

布局元素属性

属性 类型 描述
.style.width ASDimension 设置元素的宽度。 会被minWidth和maxWidth覆盖。默认为ASDimensionAuto
.style.height ASDimension 设置元素的高度。 会被minHeight和maxHeight覆盖。默认为ASDimensionAuto。
.style.minHeight ASDimension 设置元素的最大高度。 它防止height属性的已使用值变得大于为maxHeight指定的值。 maxHeight的值覆盖height,但minHeight覆盖maxHeight。默认为ASDimensionAuto
.style.maxHeight ASDimension 如果子元素的堆栈大小的总和大于最大大小
.style.minWidth ASDimension 设置元素的最小宽度。它防止width属性的使用值变得小于为minWidth指定的值。 minWidth的值覆盖maxWidth和width。默认为ASDimensionAuto
.style.maxWidth ASDimension 设置元素的最大宽度。 它防止width属性的使用值变得大于为maxWidth指定的值。 maxWidth的值覆盖width,但minWidth覆盖maxWidth。默认为ASDimensionAuto
.style.preferredSize **CGSize ** 提供布局元素的建议大小。 如果提供了可选的minSize或maxSize,且preferredSize超过这些,则将强制执行minSize或maxSize, 如果未提供此可选值,则布局元素的大小将默认为其提供的内在内容大小calculateSizeThatFits:

网址有个很好的例子

请添加图片描述

代码

 override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    self.node1.style.preferredSize = CGSize(width: constrainedSize.max.width, height: 136)self.node2.style.preferredSize = CGSize(width: 58, height: 25)self.node2.style.layoutPosition = CGPoint(x: 14.0, y: 95.0)self.node3.style.height = ASDimensionMake(37.0)self.node4.style.preferredSize = CGSize(width: 80, height: 20)self.node5.style.preferredSize = CGSize(width: 80, height: 20)self.node4.style.spacingBefore = 14.0self.node5.style.spacingAfter = 14.0let absoluteLayout = ASAbsoluteLayoutSpec(children: [self.node2])let overlyLayout = ASOverlayLayoutSpec(child: self.node1, overlay: absoluteLayout)let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(0, 14, 0, 14), child: self.node3)insetLayout.style.spacingBefore = 13.0insetLayout.style.spacingAfter = 25.0let bottomLayout = ASStackLayoutSpec.horizontal()bottomLayout.justifyContent = .spaceBetweenbottomLayout.alignItems = .startbottomLayout.children = [self.node4, self.node5]bottomLayout.style.spacingAfter = 10.0
// bottomLayout.style.width = ASDimensionMake(constrainedSize.max.width)let stackLayout = ASStackLayoutSpec.vertical()stackLayout.justifyContent = .startstackLayout.alignItems = .stretchstackLayout.children = [overlyLayout, insetLayout, bottomLayout]return stackLayout}

自己做了一个demo

请添加图片描述

? 图片使用的是ASNetworkImageNode

let imageView: ASNetworkImageNode = {
    let v = ASNetworkImageNode(cache: ASNetImageManage.shared, downloader: ASNetImageManage.shared)v.cornerRadius = 10v.contentMode = .scaleAspectFillreturn v}()

这里我使用了sdwebimage的代理

ASNetworkImageNode(cache: ASNetImageManage.shared, downloader: ASNetImageManage.shared)

改写ASNetworking的cache和downloader为sdwebimage的下载缓存机制

import SDWebImage
import AsyncDisplayKit
extension ASNetworkImageNode {
    static func imageNode() -> ASNetworkImageNode {
    return ASNetworkImageNode(cache: ASNetImageManage.shared, downloader: ASNetImageManage.shared)}
}class ASNetImageManage: NSObject, ASImageDownloaderProtocol, ASImageCacheProtocol {
    static let shared = ASNetImageManage()func downloadImage(with URL: URL, callbackQueue: DispatchQueue, downloadProgress: ASImageDownloaderProgress?, completion: @escaping ASImageDownloaderCompletion) -> Any? {
    weak var weakOperation: SDWebImageOperation?let operation = SDWebImageManager.shared.loadImage(with: URL, options: .retryFailed, progress: {
     (received, expected, url) inif downloadProgress != nil {
    callbackQueue.async(execute: {
    let progress = expected == 0 ? 0 : received / expecteddownloadProgress?(CGFloat(progress))})}}) {
     (cachedImage, data, error, type, unknow, url) inif let image = cachedImage {
    callbackQueue.async(execute: {
     completion(image, nil, nil, nil) })return}callbackQueue.async(execute: {
     completion(nil, error, nil, nil) })}weakOperation = operationreturn weakOperation}func cancelImageDownload(forIdentifier downloadIdentifier: Any) {
    if let downloadIdentifier = downloadIdentifier as? SDWebImageOperation {
    downloadIdentifier.cancel()}}func cachedImage(with URL: URL, callbackQueue: DispatchQueue, completion: @escaping ASImageCacherCompletion) {
    if let key = SDWebImageManager.shared.cacheKey(for: URL) {
    SDWebImageManager.shared.imageCache.queryImage(forKey: key, options: .allowInvalidSSLCertificates, context: nil) {
     (cachedImage, data, type) inif let image = cachedImage {
    callbackQueue.async(execute: {
     completion(image, ASImageCacheType.asynchronous) })return}callbackQueue.async(execute: {
     completion(nil, ASImageCacheType.asynchronous) })}}else {
    callbackQueue.async {
    completion(nil, ASImageCacheType.asynchronous)}}}
}

UILabel

let textView: ASTextNode = {
    let v = ASTextNode()v.maximumNumberOfLines = 0return v}()

ASTextNode不自带font和textcolor,但也能从.view.font制作,他只提供了AttributeString的方法

textView.attributedText = NSAttributedString(string: model.name ?? "", attributes: convertToOptionalNSAttributedStringKeyDictionary(([convertFromNSAttributedStringKey(NSAttributedString.Key.font): UIFont.customFont(.pingFangSCRegular, ofSize: 14),convertFromNSAttributedStringKey(.foregroundColor): UIColor.lightGray])))

写cell以前是用

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

ASCellNode可以直接用来写,因为他是异步优化,所以model的传值是安全的,里面可以直接写数据的变化

init(model: listModel) {
    super.init()

布局就要使用override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {

    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    //imageView大小(宽为屏幕宽度-60,高为model给的高,为了把tableview变复杂)self.imageView.style.preferredSize = CGSize(width: KSCREENWIDTH - 60, height: CGFloat(photoHeight))//imageView插入布局(上左右间距12px)let imageLayout = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 12, left: 12, bottom: 0, right: 12), child: self.imageView)//middleLine高1pxself.middleLine.style.height = ASDimensionMake(1)//middleLine插入布局(左右间距0.5)let lineLayout = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 0.5, bottom: 0, right: 0.5), child: middleLine)//middleLine布局对比上一个view间距11lineLayout.style.spacingBefore = 11//middleLine布局对比下一个view间距0lineLayout.style.spacingAfter = 0//textView插入布局(左14,右40)let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 14, bottom: 0, right: 40), child: self.textView)//上边距insetLayout.style.spacingBefore = 13//下边距insetLayout.style.spacingAfter = 25//以垂直结构布局全部,第一个为imageview第二个为middleLine,第三个为textViewlet stackLayout = ASStackLayoutSpec.vertical()stackLayout.justifyContent = .startstackLayout.alignItems = .stretchstackLayout.children = [imageLayout, lineLayout, insetLayout]return stackLayout}

写完cell就直接写tableview(ASTableNode)

如果UIKit的东西有但AsyncDisplayKit没有,可以使用.view.来访问uikit

ASTableNode有自带的上下拉加载数据的delegate,但写的很烂

func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) {
    

直接用MJRefresh带的库好用

super.init(node: tableView)tableView.delegate = selftableView.dataSource = selftableView.view.mj_header = MJRefreshNormalHeader(refreshingBlock: {
    })tableView.view.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: {
    })tableView.view.separatorStyle = .none

tableNode是没有类似设置高度的方法,他是自动高度计算

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    

设置cell的个数和cell的样式是差不多,但要注意的是cell的设置是闭包return

//设置cell的个数
func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
    return resultModel?.list?.count ?? 0}
//设置cell的样式func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
    return {
     [weak self] inif let model = self?.resultModel?.list?[indexPath.row] {
    let node = TextureCell(model: model)node.selectionStyle = .nonenode.goAction = {
     [weak self] inself?.videoPlay()}return node}else {
    return ASCellNode()}}}

demo
Dropbox地址,如果没有翻墙的话可以私信我发给你
https://www.dropbox.com/sh/q0xrr6hcad4orwr/AAD1kfuyndVQqpQ6HPfLqWuHa?dl=0

引用文章

https://bawn.github.io/2017/12/Texture-Layout/?utm_source=tuicool&utm_medium=referral (Texture 布局篇)

https://www.jianshu.com/p/ecb682a6cde0 (AsyncDisplaykit(Texture)之布局篇)

https://blog.csdn.net/quanqinyang/article/details/54799517 (AsyncDisplayKit 系列教程 —— 集成、示例)这篇有点旧,代码出入比较大