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.0
self.node5.style.spacingAfter = 14.0
let 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.0
insetLayout.style.spacingAfter = 25.0
let bottomLayout = ASStackLayoutSpec.horizontal()
bottomLayout.justifyContent = .spaceBetween
bottomLayout.alignItems = .start
bottomLayout.children = [self.node4, self.node5]
bottomLayout.style.spacingAfter = 10.0
let stackLayout = ASStackLayoutSpec.vertical()
stackLayout.justifyContent = .start
stackLayout.alignItems = .stretch
stackLayout.children = [overlyLayout, insetLayout, bottomLayout]
return stackLayout
}
自己做了一个demo
? 图片使用的是ASNetworkImageNode
let imageView: ASNetworkImageNode = {
let v = ASNetworkImageNode(cache: ASNetImageManage.shared, downloader: ASNetImageManage.shared)
v.cornerRadius = 10
v.contentMode = .scaleAspectFill
return 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) in
if downloadProgress != nil {
callbackQueue.async(execute: {
let progress = expected == 0 ? 0 : received / expected
downloadProgress?(CGFloat(progress))
})
}
}) { (cachedImage, data, error, type, unknow, url) in
if let image = cachedImage {
callbackQueue.async(execute: { completion(image, nil, nil, nil) })
return
}
callbackQueue.async(execute: { completion(nil, error, nil, nil) })
}
weakOperation = operation
return 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) in
if 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 = 0
return 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 {
self.imageView.style.preferredSize = CGSize(width: KSCREENWIDTH - 60, height: CGFloat(photoHeight))
let imageLayout = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 12, left: 12, bottom: 0, right: 12), child: self.imageView)
self.middleLine.style.height = ASDimensionMake(1)
let lineLayout = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 0.5, bottom: 0, right: 0.5), child: middleLine)
lineLayout.style.spacingBefore = 11
lineLayout.style.spacingAfter = 0
let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 14, bottom: 0, right: 40), child: self.textView)
insetLayout.style.spacingBefore = 13
insetLayout.style.spacingAfter = 25
let stackLayout = ASStackLayoutSpec.vertical()
stackLayout.justifyContent = .start
stackLayout.alignItems = .stretch
stackLayout.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 = self
tableView.dataSource = self
tableView.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
func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
return resultModel?.list?.count ?? 0
}
func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
return { [weak self] in
if let model = self?.resultModel?.list?[indexPath.row] {
let node = TextureCell(model: model)
node.selectionStyle = .none
node.goAction = { [weak self] in
self?.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 系列教程 —— 集成、示例)这篇有点旧,代码出入比较大
|