解決Texture(原AsyncDisplayKit)的閃爍問題
AsyncDisplayKit 概覽
本文借鑒原文
Facebook 的Paper團(tuán)隊(duì)給我們帶來另一個(gè)很棒的庫:AsyncDisplayKit。這個(gè)庫能讓你通過將圖像解碼、布局以及渲染操作放在后臺(tái)線程,從而帶來超級(jí)響應(yīng)的用戶界面,也就是說不再會(huì)因界面卡頓而阻斷用戶交互。
初次使用, 當(dāng)享受其一幀不掉如絲般柔滑的手感時(shí),ASTableNode和ASCollectionNode刷新時(shí)的閃爍一定讓你幾度崩潰,到AsyncDisplayKit的github上搜索閃爍相關(guān)issue,會(huì)出來100多個(gè)問題。閃爍是AsyncDisplayKit與生俱來的問題,聞名遐邇,而閃爍的體驗(yàn)非常糟糕。幸運(yùn)的是,幾經(jīng)探索,AsyncDisplayKit的閃爍問題已經(jīng)完美解決,這個(gè)完美指的是一幀不掉的同時(shí)沒有任何閃爍,同時(shí)也沒增加代碼的復(fù)雜度。
本篇文章將著重講解閃爍問題以及對(duì)應(yīng)的解決方案。
AsyncDisplayKit的閃爍總體上分為兩大類,
1)ASNetworkImageNode reload時(shí)的閃爍
當(dāng)ASCellNode中包含ASNetworkImageNode時(shí),reload這個(gè)cell, ASNetworkImageNode會(huì)異步從網(wǎng)絡(luò)請(qǐng)求或者本地緩存中獲取圖片,請(qǐng)求到圖片后再設(shè)置ASNetworkImageNode展示圖片,但在異步過程中,ASNetworkImageNode會(huì)展示PlaceHolderImage, 從PlaceHolderImage->fetched image的展示替換導(dǎo)致閃爍發(fā)生,即使整個(gè)cell的數(shù)據(jù)不變, reload時(shí)由于圖片的加載邏輯依然不變,仍然會(huì)閃爍,對(duì)比我們常用的SDWebImage和YYWebImage, 它們的設(shè)置邏輯是先同步檢查是否有本地緩存,有直接顯示,沒有則展示placeholderImage, 等待加載完成再顯示加載圖片,展示邏輯即memory Cached image->placeholderImage->fetched image的邏輯,刷新的時(shí)候優(yōu)先級(jí)的不同,因此不會(huì)閃爍。
AsyncDisplayKit官方給的修復(fù)思路是:
? ASNetworkImageNode *imageNode = [ASNetworkImageNode new];
? imageNode.placeholderFadeDuration = 3;
? imageNode.placeholderColor = [UIColor redColor];
這樣修改后,確實(shí)沒有閃爍,但要的效果并不是我們想要的,這只是將閃爍問題用時(shí)間控制到3秒而已,并沒有實(shí)際解決問題。
上面說到SDWebImage和YYWebImage的設(shè)置思路,可以給我們提供一定的思考,如果我們繼承一個(gè)ASNetworkImageNode, 將ASNetworkImageNode的設(shè)置邏輯改為有cached image展示cache image,沒有則重新從網(wǎng)絡(luò)請(qǐng)求,不是完美解決閃爍了嘛?!但事實(shí)并非如此,無論你怎么設(shè)置,同樣都會(huì)閃爍。而我們知道在ASImageNode并不會(huì)出現(xiàn)這種問題,為什么不考慮適當(dāng)?shù)臅r(shí)機(jī)進(jìn)行替換呢,當(dāng)我們有緩存的時(shí)候直接用ASImageNode替換ASNetworkImgeNode, 在這里可能有人會(huì)問,這樣整個(gè)cellNode的控件已經(jīng)改變了?。?!刷新怎么辦?這其實(shí)和ASTableNode的展示機(jī)制有關(guān)系,它并不是類似tableView的cell重用機(jī)制,它所做的是每一個(gè)cellNode都是異步渲染加載的,重新刷新意味著控件的重新排列(最直白的話,沒有用專業(yè)的術(shù)語)。言歸正傳,這里我們用最熟悉的YYImageCache橋接緩存問題,方便自由管理緩存問題,? 看解決方案:
```
@interface JSWebImageManager : YYWebImageManager<ASImageCacheProtocol, ASImageDownloaderProtocol>?
@end
```
#import "JSWebImageManager.h"
@implementation JSWebImageManager
- (id)downloadImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue downloadProgress:(ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion{
??? @autoreleasepool {
??????? YYWebImageManager *manager = [YYWebImageManager sharedManager];
??????? __weak YYWebImageOperation *operation = nil;
??????? operation = [manager requestImageWithURL:URL
???????????????????????????????????????? options:YYWebImageOptionSetImageWithFadeAnimation
??????????????????????????????????????? progress:^(NSInteger receivedSize, NSInteger expectedSize) {
???????????????????????????????????????????
??????????????????????????????????????? }
?????????????????????????????????????? transform:nil
????????????????????????????????????? completion:^(UIImage * _Nullable image, NSURL * _Nonnull url, YYWebImageFromType from, YYWebImageStage stage, NSError * _Nullable error) {
????????????????????????????????????????? completion(image, error, operation);
????????????????????????????????????? }];
??????? return operation;
??? }
}
- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier {
??? if (![downloadIdentifier isKindOfClass:[YYWebImageOperation class]]) {
??????? return;
??? }
??? [(YYWebImageOperation *)downloadIdentifier cancel];
}
- (void)cachedImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue completion:(ASImageCacherCompletion)completion {
??? [self.cache getImageForKey:[self cacheKeyForURL:URL] withType:(YYImageCacheTypeAll) withBlock:^(UIImage * _Nullable image, YYImageCacheType type) {
??????? completion(image);
??????? if (image) {
??????????? dispatch_async(callbackQueue, ^{
??????????????? completion(image);
??????????? });
??????? } else {
??????????? dispatch_async(callbackQueue, ^{
??????????????? [self downloadImageWithURL:URL callbackQueue:callbackQueue downloadProgress:^(CGFloat progress) {
???????????????????
??????????????? } completion:^(id<ASImageContainerProtocol>? _Nullable image, NSError * _Nullable error, id? _Nullable downloadIdentifier) {
??????????????????? if (image) {
??????????????????????? completion(image);
??????????????????? }
??????????????? }];
??????????? });
??????? }
??? }];
}
@end
```
自定義JPNetworkImageNode(繼承自ASDisplayNode), 代替我們常用的ASNetworkImageNode,相關(guān)常用屬性如下
/** 網(wǎng)絡(luò)地址 */
@property (nonatomic, copy) NSURL *URL;
/** 轉(zhuǎn)場color */
@property (nonatomic, strong)UIColor *placeholderColor;
/** 靜態(tài)image */
@property (nonatomic, strong)UIImage *image;
/** 轉(zhuǎn)場時(shí)間 */
@property (nonatomic, assign)NSTimeInterval js_placeholderFadeDuration;
/** 空置圖片 */
@property (nonatomic, strong)UIImage *defaultImage;
/**
?網(wǎng)絡(luò)圖片
?*/
@property (nonatomic, strong) ASNetworkImageNode *netImgNode;
/**
?本地圖片
?*/
@property (nonatomic, strong) ASImageNode *imageNode;
```
#import "JSNetworkImageNode.h"
#import "JSWebImageManager.h"
@implementation JSNetworkImageNode
- (instancetype)init{
??? self = [super init];
??? if (self) {
??????? [self addSubnode:self.netImgNode];
??????? [self addSubnode:self.imageNode];
??? }
??? return self;
}
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
??? return [ASInsetLayoutSpec insetLayoutSpecWithInsets:(UIEdgeInsetsZero) child:!self.netImgNode.URL ? self.imageNode : self.netImgNode];
}
- (ASNetworkImageNode *)netImgNode{
??? if (!_netImgNode) {
??????? _netImgNode = [[ASNetworkImageNode alloc] initWithCache:JSWebImageManager.sharedManager downloader:JSWebImageManager.sharedManager];
??? }
??? return _netImgNode;
}
- (ASImageNode *)imageNode{
??? if (!_imageNode) {
??????? _imageNode = [[ASImageNode alloc] init];
??? }
??? return _imageNode;
}
- (void)setURL:(NSURL *)URL{
??? _URL = URL;
??? if ([YYImageCache.sharedCache containsImageForKey:[YYWebImageManager.sharedManager cacheKeyForURL:URL]]) {
??????? self.imageNode.image = [YYImageCache.sharedCache getImageForKey:[YYWebImageManager.sharedManager cacheKeyForURL:URL]];
??? } else {
??????? self.netImgNode.URL = _URL;
??? }
}
- (void)setPlaceholderColor:(UIColor *)placeholderColor{
??? self.netImgNode.placeholderColor = placeholderColor;
}
- (void)setImage:(UIImage *)image{
??? self.netImgNode.image = image;
}
- (void)setDefaultImage:(UIImage *)defaultImage{
??? self.netImgNode.defaultImage = defaultImage;
}
- (void)setJs_placeholderFadeDuration:(NSTimeInterval)js_placeholderFadeDuration{
??? self.netImgNode.placeholderFadeDuration = js_placeholderFadeDuration;
}
@end
使用時(shí)將JPNetworkImageNode當(dāng)做ASNetworkImageNode即可
2)reloadCell和reloadData引起的閃爍
當(dāng)reloadASTableNode或者ASCollectionNode的某個(gè)indexPath的cell時(shí),也會(huì)閃爍。原因和ASNetworkImageNode很像,都是異步惹的禍。當(dāng)異步計(jì)算cell的布局時(shí),cell使用placeholder占位(通常是白圖),布局完成時(shí),才用渲染好的內(nèi)容填充cell,placeholder到渲染好的內(nèi)容切換引起閃爍。UITableViewCell因?yàn)槎际峭?,不存在占位圖的情況,因此也就不會(huì)閃。
這個(gè)官方給出的解決方案是:
?cellNode.neverShowPlaceholders = YES;
這樣設(shè)置以后,會(huì)讓cell從異步加載衰退會(huì)同步狀態(tài),若reload某個(gè)indexPath的cell, 在渲染完成之前,主線程是卡死的,這就和tableView原始的加載方式一樣了,但會(huì)比tableView速度快很多,因?yàn)閁ITableView的布局計(jì)算、資源解壓、視圖合成等都是在主線程進(jìn)行,而ASTableNode則是多個(gè)線程并發(fā)進(jìn)行,何況布局等還有緩存。但當(dāng)頁面布局很多,刷新cell很多的時(shí)候,下拉掉幀就比較明顯,但我們知道ASTableNode具有預(yù)加載的相關(guān)設(shè)置,可以設(shè)置leadingScreensForBatching減緩卡頓,但仍然不完美,時(shí)間換空間而已。我們要做到的是該異步的異步,又能不卡頓,又可以預(yù)加載。為此提供解決方案:
#import@interface ASTableNode (ReloadIndexPaths)
@property (nonatomic, copy) NSArray *js_reloadIndexPaths;//需要刷新的indexPath
@end
import "ASTableNode+reloadIndexPaths.h"
#importstatic void *strKey = &strKey;
@implementation ASTableNode (reloadIndexPaths)
- (void)setJs_reloadIndexPaths:(NSArray *)js_reloadIndexPaths{
? ? objc_setAssociatedObject(self, &strKey, js_reloadIndexPaths, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSArray *)js_reloadIndexPaths{
? ? return objc_getAssociatedObject(self, &strKey);
}
@end
在此對(duì)ASTableNode類目添加新的屬性js_reloadIndexPaths,需要刷新的indexPath
?ASCellNode *(^ASCellNodeBlock)(void) = ^ASCellNode *() {
??????? ImageCellNode *cellNode = [[ImageCellNode alloc] initWithModel:_viewModel.dataArray[indexPath.row]];
??????? if ([tableNode.js_reloadIndexPaths containsObject:indexPath]) {
??????????? cellNode.neverShowPlaceholders = YES;
??????????? dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
??????????????? cellNode.neverShowPlaceholders = NO;
??????????? });
??????? } else {
??????????? cellNode.neverShowPlaceholders = NO;
??????? }
??????? return cellNode;
??? };
??? return ASCellNodeBlock;
reload單個(gè)indexPath
?_tableNode.js_reloadIndexPaths = @[indexPath];
?? [_tableNode reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
reload整個(gè)tableNode
_tableNode.js_reloadIndexPaths = _tableNode.indexPathsForVisibleRows;
[self.tableNode reloadData];
我們將需要刷新的indexPath放入js_reloadIndexPaths, 加以判斷設(shè)置該indexPath回歸主線程,當(dāng)渲染完畢后再設(shè)置可以異步加載,0.5秒的時(shí)間足以渲染完畢,這樣就完美實(shí)現(xiàn)該異步異步,該同步同步,完美解決閃爍問題。如絲般滑順。。。
該文在原作者的基礎(chǔ)上加入了自己的理解,主要解決運(yùn)用AsyncDisplayKit所導(dǎo)致的閃爍問題,歡迎大家提出問題,共同交流。
提示:由于個(gè)人對(duì)源碼的實(shí)驗(yàn)分析, 導(dǎo)致原來下載崩潰(現(xiàn)在已經(jīng)不存在該問題), 可在ImageCellNode.m中將_imageNode.view.contentMode = UIViewContentModeScaleAspectFill;該行注釋掉.主要是該方法必須在主線程中運(yùn)行, 如果想更改該屬性, 可在didload方法中調(diào)整;最新demo 地址鏈接:demo? 密碼:aq6n