解決Texture(原AsyncDisplayKit)的閃爍問題
AsyncDisplayKit 概覽
本文借鑒原文
Facebook 的Paper團隊給我們帶來另一個很棒的庫:AsyncDisplayKit法瑟。這個庫能讓你通過將圖像解碼癞志、布局以及渲染操作放在后臺線程宵喂,從而帶來超級響應的用戶界面,也就是說不再會因界面卡頓而阻斷用戶交互蜡娶。
初次使用, 當享受其一幀不掉如絲般柔滑的手感時,ASTableNode和ASCollectionNode刷新時的閃爍一定讓你幾度崩潰映穗,到AsyncDisplayKit的github上搜索閃爍相關(guān)issue窖张,會出來100多個問題。閃爍是AsyncDisplayKit與生俱來的問題蚁滋,聞名遐邇荤堪,而閃爍的體驗非常糟糕合陵。幸運的是,幾經(jīng)探索澄阳,AsyncDisplayKit的閃爍問題已經(jīng)完美解決拥知,這個完美指的是一幀不掉的同時沒有任何閃爍,同時也沒增加代碼的復雜度碎赢。
本篇文章將著重講解閃爍問題以及對應的解決方案低剔。
AsyncDisplayKit的閃爍總體上分為兩大類,
1)ASNetworkImageNode reload時的閃爍
當ASCellNode中包含ASNetworkImageNode時肮塞,reload這個cell, ASNetworkImageNode會異步從網(wǎng)絡請求或者本地緩存中獲取圖片襟齿,請求到圖片后再設置ASNetworkImageNode展示圖片,但在異步過程中枕赵,ASNetworkImageNode會展示PlaceHolderImage, 從PlaceHolderImage->fetched image的展示替換導致閃爍發(fā)生猜欺,即使整個cell的數(shù)據(jù)不變, reload時由于圖片的加載邏輯依然不變拷窜,仍然會閃爍开皿,對比我們常用的SDWebImage和YYWebImage, 它們的設置邏輯是先同步檢查是否有本地緩存,有直接顯示篮昧,沒有則展示placeholderImage赋荆, 等待加載完成再顯示加載圖片,展示邏輯即memory Cached image->placeholderImage->fetched image的邏輯懊昨,刷新的時候優(yōu)先級的不同窄潭,因此不會閃爍。
AsyncDisplayKit官方給的修復思路是:
? ASNetworkImageNode *imageNode = [ASNetworkImageNode new];
? imageNode.placeholderFadeDuration = 3;
? imageNode.placeholderColor = [UIColor redColor];
這樣修改后酵颁,確實沒有閃爍嫉你,但要的效果并不是我們想要的,這只是將閃爍問題用時間控制到3秒而已躏惋,并沒有實際解決問題均抽。
上面說到SDWebImage和YYWebImage的設置思路,可以給我們提供一定的思考其掂,如果我們繼承一個ASNetworkImageNode, 將ASNetworkImageNode的設置邏輯改為有cached image展示cache image油挥,沒有則重新從網(wǎng)絡請求,不是完美解決閃爍了嘛款熬?深寥!但事實并非如此,無論你怎么設置贤牛,同樣都會閃爍惋鹅。而我們知道在ASImageNode并不會出現(xiàn)這種問題,為什么不考慮適當?shù)臅r機進行替換呢殉簸,當我們有緩存的時候直接用ASImageNode替換ASNetworkImgeNode, 在這里可能有人會問闰集,這樣整個cellNode的控件已經(jīng)改變了9炼铩!武鲁!刷新怎么辦爽雄?這其實和ASTableNode的展示機制有關(guān)系,它并不是類似tableView的cell重用機制沐鼠,它所做的是每一個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)絡地址 */
@property (nonatomic, copy) NSURL *URL;
/** 轉(zhuǎn)場color */
@property (nonatomic, strong)UIColor *placeholderColor;
/** 靜態(tài)image */
@property (nonatomic, strong)UIImage *image;
/** 轉(zhuǎn)場時間 */
@property (nonatomic, assign)NSTimeInterval js_placeholderFadeDuration;
/** 空置圖片 */
@property (nonatomic, strong)UIImage *defaultImage;
/**
?網(wǎng)絡圖片
?*/
@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
使用時將JPNetworkImageNode當做ASNetworkImageNode即可
2)reloadCell和reloadData引起的閃爍
當reloadASTableNode或者ASCollectionNode的某個indexPath的cell時憔涉,也會閃爍订框。原因和ASNetworkImageNode很像,都是異步惹的禍兜叨。當異步計算cell的布局時穿扳,cell使用placeholder占位(通常是白圖),布局完成時浪腐,才用渲染好的內(nèi)容填充cell纵揍,placeholder到渲染好的內(nèi)容切換引起閃爍顿乒。UITableViewCell因為都是同步议街,不存在占位圖的情況,因此也就不會閃璧榄。
這個官方給出的解決方案是:
?cellNode.neverShowPlaceholders = YES;
這樣設置以后特漩,會讓cell從異步加載衰退會同步狀態(tài),若reload某個indexPath的cell, 在渲染完成之前骨杂,主線程是卡死的涂身,這就和tableView原始的加載方式一樣了,但會比tableView速度快很多搓蚪,因為UITableView的布局計算蛤售、資源解壓、視圖合成等都是在主線程進行妒潭,而ASTableNode則是多個線程并發(fā)進行悴能,何況布局等還有緩存。但當頁面布局很多雳灾,刷新cell很多的時候漠酿,下拉掉幀就比較明顯,但我們知道ASTableNode具有預加載的相關(guān)設置谎亩,可以設置leadingScreensForBatching減緩卡頓炒嘲,但仍然不完美宇姚,時間換空間而已。我們要做到的是該異步的異步夫凸,又能不卡頓浑劳,又可以預加載。為此提供解決方案:
#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
在此對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單個indexPath
?_tableNode.js_reloadIndexPaths = @[indexPath];
?? [_tableNode reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
reload整個tableNode
_tableNode.js_reloadIndexPaths = _tableNode.indexPathsForVisibleRows;
[self.tableNode reloadData];
我們將需要刷新的indexPath放入js_reloadIndexPaths, 加以判斷設置該indexPath回歸主線程呀洲,當渲染完畢后再設置可以異步加載,0.5秒的時間足以渲染完畢啼止,這樣就完美實現(xiàn)該異步異步道逗,該同步同步,完美解決閃爍問題献烦。如絲般滑順滓窍。。巩那。
該文在原作者的基礎(chǔ)上加入了自己的理解吏夯,主要解決運用AsyncDisplayKit所導致的閃爍問題,歡迎大家提出問題即横,共同交流噪生。
提示:由于個人對源碼的實驗分析, 導致原來下載崩潰(現(xiàn)在已經(jīng)不存在該問題), 可在ImageCellNode.m中將_imageNode.view.contentMode = UIViewContentModeScaleAspectFill;該行注釋掉.主要是該方法必須在主線程中運行, 如果想更改該屬性, 可在didload方法中調(diào)整;最新demo 地址鏈接:demo? 密碼:aq6n