Texture(ASDK)的理解和使用

主要是使用方面:包含详细的使用方法, UI 类 (ASTableCellNode, ASScollNode), 布局 (FlexBox布局), 优化 (ASImageNode, 对接本地 Kingfisher 图片下载) 等

Posted by poos on August 8, 2018

使用 Texture 有几大好处:

  1. 滑动性能快速提升

  2. 布局更简单了,而且支持横屏

  3. 点击处理更方便了,例如 Texture 的 TextNode 即支持点击;而且支持 hitTest 扩大点击范围

  4. 更多优化参数满足需求,例如 多层压缩为一层,不需要相应事件可以降级为 layer,自带 networkImage(待续)

10.22 更新 ASScrollNode 使用


img

写在文章前的话:有问题就去查文档和issues,错不了的

一份中文文档

源码分析

An ASDisplayNode is an abstraction over UIView and CALayer that allows you to perform calculations about a view hierarchy off the main thread, and could do rendering off the main thread as well.

原理大概就是重新实现大部分的 UI 控件,使用ASDisplayNode充分利用后台线程来完成复杂的布局和渲染,而通常UIKit的创建、布局计算和渲染绘制都集中在主线程。

简书:ASDK源码剖析

https://blog.csdn.net/yangyangzhang1990/article/details/52452707

使用 ASDK 性能调优 - 提升 iOS 界面的渲染性能

以上三篇文章可以读到 UIView 是如何渲染显示的,而 Texture 又是如何设计替代方案的。goole 上可以很容易搜索到更多内容,也可以查看官方 gtihub.

使用

ASTableNode 使用

正常像 tableview 使用即可,值得注意的是 ASTableNode 提供了一个 返回 ASCellNodeBlock 的代理方法,可以确保线程安全。

文档

It is recommended that you use the node block version of these methods so that your table node will be able to prepare and display all of its cells concurrently. This means that all subnode initialization methods can be run in the background. Make sure to keep ‘em thread safe.

使用实力如下:

普通

1
2
3
4
func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode {
    let model = Models.get(indexPath.row)
    return NewFriendsView.followCell(model: model)
}

使用Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {

    func returnCellNode() -> ASCellNode {
        let model = Models.get(indexPath.row)
        return NewFriendsView.followCell(model: model)
    }

    // this may be executed on a background thread - it is important to make sure it is thread safe
    let cellNodeBlock = { () -> ASCellNode in
        let cellNode = returnCellNode()
        return cellNode
    }

    return cellNodeBlock
}

ASCellNode 使用

文档

  • 使用
1
2
3
4
-init  Thread safe initialization.
-layoutSpecThatFits:  Return a layout spec that defines the layout of your cell.
-didLoad  Called on the main thread. Good place to add gesture recognizers, etc.
-layout  Also called on the main thread. Layout is complete after the call to super which means you can do any extra tweaking you need to do.

主要生命周期是上边四个函数,值得注意的是 -layoutSpecThatFits: 会返回布局方案。Texture 使用 FlexBox 布局,先简单提一句,下边会详细介绍。

  • neverShowPlaceholders

    Using this option does not eliminate all of the performance advantages of Texture. Normally, a cell has been preloading and is almost done when it reaches the screen, so the blocking time is very short. Even if the rangeTuningParameters are set to 0 this option outperforms UIKit. While the main thread is waiting, subnode display executes concurrently.

当渲染未结束时候不显示Placeholder,解决部分情况下闪烁问题。

布局

Texture 的布局灵感来自CSS Flexbox,通过 Layout Specs(布局样式)和 Layout Elements(布局元素)实现布局。

文档

网上也有大量资料就不再赘述了。

  • Specs

大概有一下布局样式,具体可以查看文档:

1
2
3
4
5
6
7
8
9
10
ASWrapperLayoutSpec
ASStackLayoutSpec
ASInsetLayoutSpec
ASOverlayLayoutSpec
ASBackgroundLayoutSpec
ASCenterLayoutSpec
ASRatioLayoutSpec
ASRelativeLayoutSpec
ASAbsoluteLayoutSpec
ASCornerLayoutSpec

提一下常用的:

ASStackLayoutSpec,安照横向或者竖向排列元素

ASBackgroundLayoutSpec,设置背景图片

ASInsetLayoutSpec,用来约束背景元素上下左右的距离

  • Elements (10.22更新

常用的 UI 类都可找到对应的 Node,button、image、text 等就不一一列举了。

提一下 ASNetworkImageNode ,可以从网络下载图片的 image node。下一个目录会详细介绍

自定义 node

1
2
3
4
let node = ASDisplayNode { () -> UIView in
	let view = SomeView()
	return view
}

ASScollNode

注意可以持有 scollContent 防止子 node 提前释放

var scollContent: ASStackLayoutSpec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// init 时候的一些参数示例

    let scoll = ASScrollNode().then { (node) in
        node.automaticallyManagesContentSize = true
        node.automaticallyManagesSubnodes = true
        node.autoresizesSubviews = true
        node.scrollableDirections = .right
//        node.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        node.backgroundColor = UIColor.white
        node.view.showsHorizontalScrollIndicator = false
    }


    // layout时候与其他控件的不同点

    scoll.layoutSpecBlock = { [weak self] _, _ in
        guard let `self` = self else { return ASLayoutSpec() }

        return self.layoutScoll()
    }


  • 布局的细微调整,Element Properties 和 API Sizing

Element Properties

API Sizing

同样提一下常用的:

style.flexShrink / style.flexGrow ,text node 等 大小可变的如果超出或者不足父 spaces 的情况下进行缩小和增长

hitTestSlop ,扩大点击范围

style.spacingAfter / style.spacingBefore ,元素排列时候的前后间距

style.preferredSize,大小

.style.flexBasis = ASDimensionMake(“40%”),还有按照父 spaces 的比例布局的

NetworkImageNode 闪烁 和 缓存

ASNetworkImageNode 使用的是 Texture 自己的图片缓存系统,所以就有和项目默认的图片缓存对接的问题。

ASNetworkImageNode 每次从网络加载(即使从缓存加载)都会先显示默认占位图,然后才显示下载的图片,所以在 reload table 会闪烁

  • 对接项目缓存

对接缓存需要实现 ASImageCacheProtocol 中的几个代理方法即可:

downloadImage(…) -> Any?

cancelImageDownload

cachedImage

示例例一个对接 Kingfisher 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import Kingfisher

extension ASNetworkImageNode {
    static func imageNode() -> ASNetworkImageNode {
        return ASNetworkImageNode(cache: ASImageManager.shared, downloader: ASImageManager.shared)
    }
}

class ASImageManager: NSObject, ASImageDownloaderProtocol, ASImageCacheProtocol {

    static let shared = ASImageManager()
    private override init() {}

    func downloadImage(with url: URL, callbackQueue: DispatchQueue, downloadProgress: ASImageDownloaderProgress?, completion: @escaping ASImageDownloaderCompletion) -> Any? {

        ImageDownloader.default.downloadTimeout = 30.0
        var operation: RetrieveImageDownloadTask?
        operation = ImageDownloader.default.downloadImage(with: url, progressBlock: { (received, expected) in
            if downloadProgress != nil {
                callbackQueue.async(execute: {
                    let progress = expected == 0 ? 0 : received / expected
                    downloadProgress?(CGFloat(progress))
                })
            }
        }, completionHandler: { (image, error, _, data) in
            if image != nil {
                callbackQueue.async(execute: { completion(image, error, nil, nil) })
                ImageCache.default.store(image!, original: data, forKey: url.cacheKey, toDisk: true)
            } else {
                callbackQueue.async(execute: { completion(nil, nil, operation, nil) })
            }
        })
        return operation
    }

    func cancelImageDownload(forIdentifier downloadIdentifier: Any) {
        if let task = downloadIdentifier as? RetrieveImageDownloadTask {
            task.cancel()
        }
    }

    func cachedImage(with url: URL, callbackQueue: DispatchQueue, completion: @escaping ASImageCacherCompletion) {
        ImageCache.default.retrieveImage(forKey: url.cacheKey, options: nil) { (img, _) in
            callbackQueue.async { completion(img) }
        }
    }
}
  • 图片 reload 闪烁

原理是 使用两个 node(ASNetworkImageNode 和 ASImageNode),无缓存即使用 ASNetworkImageNod 加载网络图片,有缓存即使用 ASImageNode 直接显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import AsyncDisplayKit

// MARK: - ASNetworkImageNode 以解决reload闪烁问题
class NetworkImageNode: ASDisplayNode {
    private var networkImageNode = ASNetworkImageNode.imageNode()
    private var imageNode = ASImageNode()

    var defaultImage: UIImage? {
        didSet {
            networkImageNode.defaultImage = defaultImage
            imageNode.image = defaultImage
        }
    }

    override var cornerRadius: CGFloat {
        didSet {
            networkImageNode.cornerRadius = cornerRadius
            imageNode.cornerRadius = cornerRadius
        }
    }

    var url: URL? {
        didSet {
            if let url = url {
                if ImageCache.default.imageCachedType(forKey: url.cacheKey).cached {
                    ImageCache.default.retrieveImage(forKey: url.cacheKey, options: nil, completionHandler: { (image, _) in
                        if let image = image {
                            self.imageNode.image = image
                        }
                    })
                } else {
                    self.networkImageNode.url = url
                }
            }
        }
    }

    override init() {
        super.init()
        addSubnode(networkImageNode)
        addSubnode(imageNode)
    }

    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        let url = networkImageNode.url?.absoluteString ?? ""
        return ASInsetLayoutSpec(insets: .zero, child: url.count > 0 ? networkImageNode : imageNode)
    }

    func addTarget(_ target: Any?, action: Selector, forControlEvents controlEvents: ASControlNodeEvent) {
        networkImageNode.addTarget(target, action: action, forControlEvents: controlEvents)
        imageNode.addTarget(target, action: action, forControlEvents: controlEvents)
    }
}

总结

学习和使用还是很快的,原理理解就要多看多实践才能理解透彻,尚未消化,所有原理方面简单列举一下。使用上就是多看 文档github, 问题解决会比较快。

这些资料可能帮到你:

AsyncDisplayKit 初窥

贝聊科技-AsyncDisplayKit近一年的使用体会及疑难点

AsyncDisplayKit官方文档翻译

AsyncDispalyKit reloadData刷新列表闪屏问题分析及解决方案

ASNetworkImageNode can NOT load animatedData sometime #2871

iOS 保持界面流畅的技巧