AsyncDisplaykit(Texture)技术分享
官方Texture文档:https://texturegroup.org/docs/getting-started.html
github: https://github.com/TextureGroup/Texture
可以下载github里example的代码看
UIKit的绘制机制图解
CALayer
的display
方法由系统调用,用来更新layer
的内容,如果layer
有delegate
对象,那么display
方法将尝试调用delegate
的displayer:
方法来更新layer
的内容。如果delegate
没有实现displaylayer:
方法,则这个方法会创建一个backing store
来保存原来的内容,然后调用layer
的drawInContext:
方法来填充back store
。最后以新的back store
替换layer
之前内容达到更新layer
的目的。通常UIKit
中CAlayer
的delegate
是UIView
对象
两种方式自定义CAlayer
的内容
有时一个 layer
会包含很多 sub-layer
,而这些sub-layer
并不需要响应触摸事件,也不需要进行动画和位置调整。ASDK
为此实现了一个被称为pre-composing
的技术,可以把这些sub-layer
合成渲染为一张图片。开发时,ASNode
已经替代了UIView
和 CALayer
;直接使用各种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布局相差无几
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
的好处:
- 保存内存。屏幕关闭的
ASViewController
将自动缩小其任何子数据的获取数据的大小和显示范围。这是大型应用程序内存管理的关键。 ASVisibility
功能。当在ASNavigationController
或ASTabBarController
中使用时,这些类知道使视图控制器可见所需的用户点击的确切数量。
布局
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 系列教程 —— 集成、示例)这篇有点旧,代码出入比较大