美文网首页iOS开发Texture
OC版解决AsyncDisplayKit闪烁问题

OC版解决AsyncDisplayKit闪烁问题

作者: 那年那月那花儿 | 来源:发表于2018-03-13 11:21 被阅读325次

    解决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桥接缓存问题,看解决方案:

    首先导入YYWebImage,引入自定义的两个宏

    #define HAVE_CACHE_IMAGE(str)   [[YYImageCache sharedCache] containsImageForKey:str] //判断是否存在缓存键值

    #define CACHE_IMAGE(str)    [[YYImageCache sharedCache] getImageForKey:str]//获取缓存图片

    自定义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;

    #import "JSNetworkImageNode.h"

    @interface JSNetworkImageNode ()

    /**网络图片*/

    @property (nonatomic, strong) ASNetworkImageNode *netImgNode;

    /**本地图片*/

    @property (nonatomic, strong) ASImageNode *imageNode;

    @end

    @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:HAVE_CACHE_IMAGE(self.URL.absoluteString) ? self.imageNode : self.netImgNode];

    }

    - (ASNetworkImageNode *)netImgNode{

        if (!_netImgNode) {

            _netImgNode = [[ASNetworkImageNode alloc] init];

            _netImgNode.delegate = self;

            _netImgNode.shouldCacheImage = NO;

        }

        return _netImgNode;

    }

    - (ASImageNode *)imageNode{

        if (!_imageNode) {

            _imageNode = [[ASImageNode alloc] init];

        }

        return _imageNode;

    }

    - (void)setURL:(NSURL *)URL{

        _URL = URL;

        if (HAVE_CACHE_IMAGE(_URL.absoluteString)) {

            self.imageNode.image = CACHE_IMAGE(_URL.absoluteString);

        } 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;

    }

    - (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image{

        [[YYImageCache sharedCache] setImage:image forKey:imageNode.URL.absoluteString];

    }

    @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  密码:7m3w

    相关文章

      网友评论

      • c30be4cf3f4f:替换了ASNetworkImageNode发现内存占用会明显增加。图片越多,内存占用越来越多。
        那年那月那花儿:@史多萌 谢谢你的反馈, 后面会持续优化......暂时可以定期作处理, 避免内存泄漏
      • Misscxuan:Demo 运行不了。
        那年那月那花儿:@Misscxuan Sorry, 里面有一行代码是我当初测试源码的特性, 重新上传的时候不小心加进去啦; 在ImageCellNode.m中把 _imageNode.view.contentMode = UIViewContentModeScaleAspectFill;这一行注释掉, 它是必须在主线程中的, 如果想调整可以didLoad方法中调整
        Misscxuan:@那年那月那花儿 运行直接崩溃
        2018-07-02 10:25:33.321519+0800 JPAsyncDisplayKit[2428:68891] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'This method must be called on the main thread'
        *** First throw call stack:
        (
        0 CoreFoundation 0x000000010fd8e12b __exceptionPreprocess + 171
        1 libobjc.A.dylib 0x0000000112372f41 objc_exception_throw + 48
        2 CoreFoundation 0x000000010fd932f2 +[NSException raise:format:arguments:] + 98
        3 Foundation 0x00000001109bdd69 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 193
        4 JPAsyncDisplayKit 0x000000010f0d4b01 -[ASDisplayNode view] + 1009
        5 JPAsyncDisplayKit 0x000000010ee824d2 -[ImageCellNode initWithModel:] + 514
        6 JPAsyncDisplayKit 0x000000010ee7ee2c __55-[ViewController tableNode:nodeBlockForRowAtIndexPath:]_block_invoke + 188
        7 JPAsyncDisplayKit 0x000000010f1cd6f8 __51-[ASTableView dataController:nodeBlockAtIndexPath:]_block_invoke_2 + 104
        8 JPAsyncDisplayKit 0x000000010f03e94f -[ASCollectionElement node] + 127
        9 JPAsyncDisplayKit 0x000000010f08c5ba __58-[ASDataController _allocateNodesFromElements:completion:]_block_invoke + 154
        那年那月那花儿:@Misscxuan 我试过Xcode9.4, 是可以的, 你再看看
      • Civel_Xu:Demo 是无法下载的
      • Civel_Xu:上拉刷新 会掉帧 卡顿 有晕倒这种情况吗
      • 老子不去:这个方法是只能解决因为图片带来的闪烁么?我没有用网络图片用这个方法reloadData之后还是没有用,依然是闪烁
        那年那月那花儿:@老子不去 方便把你的项目简化成demo发到我的邮箱吗? 因为不知道你的布局和逻辑, 说出的方案可能并不正确
        老子不去:@那年那月那花儿 放了,比之前好很多,不过有时候还是会闪
        那年那月那花儿:本地的用ASImageNode, reloadData的时候你是否将可见cellNode放入js_reloadIndexPaths ? cellNode的布局控件是怎样的? 欢迎你继续指正可能存在的问题, 也可以留下你的联系方式, 方便沟通交流...
      • 丶纳凉:JPNetworkImageNode 好像无效; reloadData 的时候还会闪
        丶纳凉:@那年那月那花儿 没有,就reloadData
        那年那月那花儿:类似于这样_tableNode.js_reloadIndexPaths = @[indexPath];
        [_tableNode reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
        那年那月那花儿:感谢你的评论, 可以商讨一下, reloadData的时候你是否将可见cell放入js_reloadIndexPaths?

      本文标题:OC版解决AsyncDisplayKit闪烁问题

      本文链接:https://www.haomeiwen.com/subject/wzwafftx.html