OC版解决AsyncDisplayKit闪烁问题

解决Texture(原AsyncDisplayKit)的闪烁问题


AsyncDisplayKit 概览

本文借鉴原文

Facebook 的Paper团队给我们带来另一个很棒的库:AsyncDisplayKit。这个库能让你通过将图像解码、布局以及渲染操作放在后台线程,从而带来超级响应的用户界面,也就是说不再会因界面卡顿而阻断用户交互。

初次使用, 当享受其一帧不掉如丝般柔滑的手感时,ASTableNode和ASCollectionNode刷新时的闪烁一定让你几度崩溃,到AsyncDisplayKit的github上搜索闪烁相关issue,会出来100多个问题。闪烁是AsyncDisplayKit与生俱来的问题,闻名遐迩,而闪烁的体验非常糟糕。幸运的是,几经探索,AsyncDisplayKit的闪烁问题已经完美解决,这个完美指的是一帧不掉的同时没有任何闪烁,同时也没增加代码的复杂度。

本篇文章将着重讲解闪烁问题以及对应的解决方案。

AsyncDisplayKit的闪烁总体上分为两大类,

1)ASNetworkImageNode reload时的闪烁

当ASCellNode中包含ASNetworkImageNode时,reload这个cell, ASNetworkImageNode会异步从网络请求或者本地缓存中获取图片,请求到图片后再设置ASNetworkImageNode展示图片,但在异步过程中,ASNetworkImageNode会展示PlaceHolderImage, 从PlaceHolderImage->fetched image的展示替换导致闪烁发生,即使整个cell的数据不变, reload时由于图片的加载逻辑依然不变,仍然会闪烁,对比我们常用的SDWebImage和YYWebImage, 它们的设置逻辑是先同步检查是否有本地缓存,有直接显示,没有则展示placeholderImage, 等待加载完成再显示加载图片,展示逻辑即memory Cached image->placeholderImage->fetched image的逻辑,刷新的时候优先级的不同,因此不会闪烁。

AsyncDisplayKit官方给的修复思路是:

  ASNetworkImageNode *imageNode = [ASNetworkImageNode new];
  imageNode.placeholderFadeDuration = 3;
  imageNode.placeholderColor = [UIColor redColor];

这样修改后,确实没有闪烁,但要的效果并不是我们想要的,这只是将闪烁问题用时间控制到3秒而已,并没有实际解决问题。

上面说到SDWebImage和YYWebImage的设置思路,可以给我们提供一定的思考,如果我们继承一个ASNetworkImageNode, 将ASNetworkImageNode的设置逻辑改为有cached image展示cache image,没有则重新从网络请求,不是完美解决闪烁了嘛?!但事实并非如此,无论你怎么设置,同样都会闪烁。而我们知道在ASImageNode并不会出现这种问题,为什么不考虑适当的时机进行替换呢,当我们有缓存的时候直接用ASImageNode替换ASNetworkImgeNode, 在这里可能有人会问,这样整个cellNode的控件已经改变了!!!刷新怎么办?这其实和ASTableNode的展示机制有关系,它并不是类似tableView的cell重用机制,它所做的是每一个cellNode都是异步渲染加载的,重新刷新意味着控件的重新排列(最直白的话,没有用专业的术语)。言归正传,这里我们用最熟悉的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,相关常用属性如下

/** 网络地址 */

@property (nonatomic, copy) NSURL *URL;

/** 转场color */

@property (nonatomic, strong)UIColor *placeholderColor;

/** 静态image */

@property (nonatomic, strong)UIImage *image;

/** 转场时间 */

@property (nonatomic, assign)NSTimeInterval js_placeholderFadeDuration;

/** 空置图片 */

@property (nonatomic, strong)UIImage *defaultImage;

/**
 网络图片
 */
@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占位(通常是白图),布局完成时,才用渲染好的内容填充cell,placeholder到渲染好的内容切换引起闪烁。UITableViewCell因为都是同步,不存在占位图的情况,因此也就不会闪。

这个官方给出的解决方案是:

 cellNode.neverShowPlaceholders = YES;

这样设置以后,会让cell从异步加载衰退会同步状态,若reload某个indexPath的cell, 在渲染完成之前,主线程是卡死的,这就和tableView原始的加载方式一样了,但会比tableView速度快很多,因为UITableView的布局计算、资源解压、视图合成等都是在主线程进行,而ASTableNode则是多个线程并发进行,何况布局等还有缓存。但当页面布局很多,刷新cell很多的时候,下拉掉帧就比较明显,但我们知道ASTableNode具有预加载的相关设置,可以设置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秒的时间足以渲染完毕,这样就完美实现该异步异步,该同步同步,完美解决闪烁问题。如丝般滑顺。。。

该文在原作者的基础上加入了自己的理解,主要解决运用AsyncDisplayKit所导致的闪烁问题,欢迎大家提出问题,共同交流。

提示:由于个人对源码的实验分析, 导致原来下载崩溃(现在已经不存在该问题), 可在ImageCellNode.m中将_imageNode.view.contentMode = UIViewContentModeScaleAspectFill;该行注释掉.主要是该方法必须在主线程中运行, 如果想更改该属性, 可在didload方法中调整;最新demo 地址链接:demo  密码:aq6n

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容

  • 异步方法调用 异步方法调用或异步方法模式是(多线程)面向对象程序设计中用于异步调用对象的潜在的长期运行方法的一种设...
    路仟阅读 523评论 0 0
  • react导入依赖 react由两部分组成: react 包和 react-dom ,语法都是ES6 import...
    Nevermind阅读 142评论 0 0
  • 上班 1.volte word文档的书写 收获 1.vuex module 2.promise3.axios4.a...
    王zm阅读 158评论 0 1
  • 一:《问佛》 佛祖,贪欲,纠缠于梦醒时分 那些求而不得的痴心妄想 在一呼一吸之间,游离存活 念念不忘的名利,在漩涡...
    一泓夜雨阅读 311评论 0 0
  • adb devices时出现如下提示:no permissions (verify udev rules); se...
    zhujunhua阅读 285评论 0 0