源码阅读

LazyScrollView源码阅读笔记

2017-07-11  本文已影响43人  iOS不高级工程师王老师

LazyScrollView是一个类似TableView的高性能滚动视图,他的作者在开源的同时,提供了详细的内容介绍,如下:

LazyScrollView中文说明
LazyScrollView中文Demo说明

我的笔记只是我的一些补充内容与思考,以下是我的学习笔记

--

结构:非常简单&易懂

TMMuiLazyScrollView.png

--

当数据存在,所有的展示,都是从一次reload开始

- (void)reloadData
{
    //得到所有的item的位置,并按位置从上到下和从下到上,分别排序为2个数组
    [self creatScrollViewIndex];
    if (self.itemsFrames.count > 0) {
        
        CGRect visibleBounds = self.bounds;
        
        //根据self.bounds,得到需要复用的最大Y值和最小Y值
        CGFloat minY = CGRectGetMinY(visibleBounds) - RenderBufferWindow;
        CGFloat maxY = CGRectGetMaxY(visibleBounds) + RenderBufferWindow;
        
        //计算并展示需要展示的view,回收消失的view
        [self assembleSubviewsForReload:YES minY:minY maxY:maxY];
        
        //通过对比,计算复用view,出现的time
        //如果自己实现类似的Scrollview可以不是实现这个方法,只需要根据业务看是否刷新lastVisiblemuiID即可
        [self findViewsInVisibleRect];
    
    }
}

这里需要详细解释一下方法:

- (void)assembleSubviewsForReload:(BOOL)isReload minY:(CGFloat)minY maxY:(CGFloat)maxY
{
  
    //得到在Buffer下那些view在展示的区域内
    NSSet *itemShouldShowSet = [self showingItemIndexSetFrom:minY to:maxY];
    //得到在bounds下那些view在展示的区域内
    self.muiIDOfVisibleViews = [self showingItemIndexSetFrom:CGRectGetMinY(self.bounds) to:CGRectGetMaxY(self.bounds)];

    NSMutableSet  *recycledItems = [[NSMutableSet alloc] init];

    //如果之前有过一次reload,那么visibleItem会有数据,这个操作就是为了,找到那些view应该被回收,那些应该展示
    //第一次reload没有数据
    NSSet *visibles = [self.visibleItems copy];
    
    for (UIView *view in visibles)
    {
        //先确定view是否在展示区域,不在的被回收,在的加入要reload数组
        BOOL isToShow  = [itemShouldShowSet containsObject:view.muiID];
        
        if (!isToShow && view.reuseIdentifier.length > 0)
        {

            NSMutableSet *recycledIdentifierSet = [self recycledIdentifierSet:view.reuseIdentifier];
            [recycledIdentifierSet addObject:view];
            [view removeFromSuperview];
            [recycledItems addObject:view];
        }
        else if (isReload && view.muiID) {
            [self.shouldReloadItems addObject:view.muiID];
        }
    }
    
    //取差集
    [self.visibleItems minusSet:recycledItems];
    [recycledItems removeAllObjects];

    for (NSString *muiID in itemShouldShowSet)
    {
        BOOL shouldReload = isReload || [self.shouldReloadItems containsObject:muiID];
        if(![self isCellVisible:muiID] || [self.shouldReloadItems containsObject:muiID])
        {
            if (self.dataSource && [self.dataSource conformsToProtocol:@protocol(TMMuiLazyScrollViewDataSource)] &&
                [self.dataSource respondsToSelector:@selector(scrollView: itemByMuiID:)])
            {
                //如果调用了reload,或者shouldReloadItems包含了这个id,则从计算出来的visibleItems里寻找item
                if (shouldReload) {
                    self.currentVisibleItemMuiID = muiID;
                }
                else {
                    /*
                    如果没有调用reload,或者shouldReloadItems不包含了这个id,则创建一个新的view
                    在代理方法
                    - (UIView *)scrollView:(TMMuiLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID中
                    */
                    self.currentVisibleItemMuiID = nil;
                }
                
                UIView *viewToShow = [self.dataSource scrollView:self itemByMuiID:muiID];

                if ([viewToShow conformsToProtocol:@protocol(TMMuiLazyScrollViewCellProtocol)] &&
                    [viewToShow respondsToSelector:@selector(mui_afterGetView)]) {
                    [(UIView<TMMuiLazyScrollViewCellProtocol> *)viewToShow mui_afterGetView];
                }
                
                //如果没有加入visibleItems,加入visibleItems数组
                if (viewToShow)
                {
                    viewToShow.muiID = muiID;
                    if (![self.visibleItems containsObject:viewToShow]) {
                        [self.visibleItems addObject:viewToShow];
                    }
                }
            }
            //从应该要reload的数组里删除
            [self.shouldReloadItems removeObject:muiID];
        }
    }
}

寻找复用view的逻辑

- (nullable UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier
{
    UIView *view = nil;
    if (self.currentVisibleItemMuiID) {
        NSSet *visibles = self.visibleItems;
        for (UIView *v in visibles) {
            if ([v.muiID isEqualToString:self.currentVisibleItemMuiID]) {
                view = v;
                break;
            }
        }
    }
    if (nil == view) {
        NSMutableSet *recycledIdentifierSet = [self recycledIdentifierSet:identifier];
        view = [recycledIdentifierSet anyObject];
        if (view)
        {
            //if exist reusable view , remove it from recycledSet.
            [recycledIdentifierSet removeObject:view];
            //NSLog(@"从复用池删除,此时复用池有 count = %ld",recycledIdentifierSet.count);
            //Then remove all gesture recognizers of it.
            view.gestureRecognizers = nil;
        }
    }
    if ([view conformsToProtocol:@protocol(TMMuiLazyScrollViewCellProtocol)] && [view respondsToSelector:@selector(mui_prepareForReuse)]) {
        [(UIView<TMMuiLazyScrollViewCellProtocol> *)view mui_prepareForReuse];
    }
    return view;
}
- (NSMutableSet *)recycledIdentifierSet:(NSString *)reuseIdentifier;
{
    if (reuseIdentifier.length == 0)
    {
        return nil;
    }
    
    //会把一类reuseIdentifier的view组合成一个可变集合,放入复用池
    NSMutableSet *result = [self.recycledIdentifierItemsDic objectForKey:reuseIdentifier];
    if (result == nil) {
        result = [[NSMutableSet alloc] init];
        [self.recycledIdentifierItemsDic setObject:result forKey:reuseIdentifier];
    }
    return result;
}

为什么这里的复用池使用了一个dict,里面根据reuseIdentifier放一个集合,我这里猜想是因为天猫本身可能会有很多类的view,如果都放入一个数组里,可能会导致如下问题:

A_view 10个
B_view 10个
C_view 10个

想找到A,却要遍历所有种类的view

for (int i = 0; i < 10+10+10 ;i++)
{
    if (a){
        break;
    }
}

--

如果是dict,只需要取出dict,得到set就可以遍历

NSSet *aSet = [dict objectForKey:@"xxx"];

for (int i = 0; i < aSet.count ;i++)
{
    if (a){
        break;
    }
}

个人认为:如果是要做类别很多的,而且view的frame相对不大的滚动视图,可以用这样的方式,如果view的frame很大,例如接近一屏幕,可以考虑直接放入一个数组即可.

--

buffer的概念

个人认为buffer的概念主要用于,优化scrollViewDidScroll里的计算时间,为了防止每一次scroll微小的滚动带来的计算消耗,源码如下

CGFloat currentY = scrollView.contentOffset.y;
CGFloat buffer = RenderBufferWindow / 2;
//如果大于buffer的值,才会进行计算
if (buffer < ABS(currentY - self.lastScrollOffset.y)) {
   self.lastScrollOffset = scrollView.contentOffset;
   [self assembleSubviews];
   [self findViewsInVisibleRect];

}

--

一些其他细节,在工程里,它大量用了集合NSSet,而非NSArray,具体为什么可以参考下面的链接:NSArray和NSSet的区别

我只粘贴一下精华,如下:

NSSet , NSMutableSet类声明编程接口对象,无序的集合,在内存中存储方式是不连续的

像NSArray,NSDictionary(都是有序的集合)类声明编程接口对象是有序集合,在内存中存储位置是连续的;

NSSet和我们常用NSArry区别是:在搜索一个一个元素时NSSet比NSArray效率高,主要是它用到了一个算法hash(散列,也可直译为哈希);

--

这份源码阅读笔记相对简单,如果想更详细的了解,建议大家还是去阅读源码(源码量不多,最多一天就读完),再加上作者的文章辅助,相信会对它的原理了解的更多,如果以后大家想自己实现一个类似这样的高性能视图,这份源码可能是一个不错的选择~

上一篇下一篇

猜你喜欢

热点阅读