UIScrollview重用以及UIImage的坑

作者: TonyGor | 来源:发表于2017-01-06 15:12 被阅读175次

    UITableView中的UITableViewCell是可以重用的。

    什么是重用,字面意思就是把内容换一下,大体结构不发生变化的二次使用。打个比方就是家里做饭,一般都是吃完饭把餐具洗一下,下次再用,而不是吃完就扔,下次再买新的(一次性除外)。

    所以重用的好处就是节省,较少每次创建或销毁对象的开销,直观的感受就是你的TableView滑起来更流畅了,把成千上万条数据加载进去,内存也不会剧增,app也就不不容易崩了。

    以上都是我看文档或者别的大牛说的,自己没试验过,亲身感受也不真切,于是乎我打算自己尝试动手去实现重用机制,利用的就是UIScrollView。为什么要选UIScrollView,简单的原因就是它是UITableView的超类,只是后者实现了重用机制,前者没有。

    事先声明一点,为了作对比,我先做了一个非常简单的图片浏览器,由于是实验性质,所以一些细节并没有很好地去实现,我把主要精力放在对比没有使用重用机制和使用了重用机制的内存使用及滑动的流畅度上。

    先上图说明结果:

    没有重用机制 使用重用机制

    结果非常明显,没有使用重用机制的,虽然我也用了懒加载的模式,但是当我把所用图片都看一遍后,内存就蹭蹭蹭的往上涨了,没有下降的痕迹;而使用了重用机制,当只有load到大一点的图片的时候,内存占用才会上涨,但是过后又会降下来,可喜。

    接下来我会分享一下我是如何实现这个重用机制的。因为我是通过UITableView才想到这个重用的,所以我所有的思路都是参考我在使用UITableView时所做过的事,然后推测那样做的目的,再自行实现代码的。如果有不恰当的地方请各位斧正,谢谢。

    首先想到的是我在写TableViewCell的时候,在实现
    - (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
    这个方法的时候,第一句写的就是
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"XXX"];
    这句话的作用就是在Reusable池中dequeue一个可重用的,标记为XXX的cell出来,于是我们马上可以想到,要实现重用,目前需要以下几个要素:

    思路1.0

    • 存放可供重用的cell的Reusable池
    • 正在屏幕上显示的cell的Visible池
    • 从Reusable池中取出cell的操作,dequeue

    分析

    上述三个方面,前两个需要我们各生成一个存储对象,把用于展示的“cell”和可供重用的“cell”存放起来,那这里我们有哪些可供利用呢?NSMutableArrayNSMutableDictionary等?
    参考了其他大神的说法,Apple是利用了上述两者,我猜想用字典的原因应该是为不同的Identifier生成不同的key,然后每个key里面又是一个可变数组,存放对应Identifier的cell(纯属瞎猜)。考虑到这个图片浏览器目前功能简单,所以这里我选择的是NSMutableSet,具体好处后面会提到。
    至于第三点,取出cell的操作很显然就是一个方法,于是乎我们就可以先实现这么几个啦:

    @property (nonatomic) NSMutableSet *visibleViews;
    @property (nonatomic) NSMutableSet *reusableViews;
    
    - (ZoomingScrollView *)dequeReusableView {
        ZoomingScrollView *reusableView = [_reusableViews anyObject];
        if (reusableView == nil) {
            reusableView = [[ZoomingScrollView alloc] initWithFrame:kScreenFrame];
        } else {
            [_reusableViews removeObject:reusableView];
        }
        return reusableView;
    }
    
    - (ZoomingScrollView *)viewAtIndex:(NSUInteger) index {
        ZoomingScrollView *zoomingView = [self dequeReusableView];
        
        // 这里有没有什么问题?大家思考一下~
        UIImage *image = [UIImage imageNamed:imageName];
        [zoomingView setDisplayImage:image];
        
        CGFloat xPosition = index * kScreenFrame.size.width;
        CGRect viewFrame = CGRectMake(xPosition, 0, kScreenFrame.size.width, kScreenFrame.size.height);
        zoomingView.frame = viewFrame;
        zoomingView.tag = 1000 + index;
        
        [_visibleViews addObject:zoomingView];
        
        return zoomingView;
    }
    

    这里把viewAtIndex这个方法一并在类里实现了,其实这里应该要设计成由代理来实现,但是由于是实验,所以没有直接实现,这样比较直观。(后面我还是改成代理模式吧。。。这样的耦合性很高)

    dequeReusableView方法的作用就是从reusableViews取出可重用的view,当没有可以重用的view时,就新创建一个对象。最后返回这个view,这里的逻辑还是比较简单的。

    viewAtIndex:方法中,[_visibleViews addObject:zoomingView];这句的作用是把显示在屏幕上的view放到_visibleViews池中,然后给大家一个小小的问题,方法里用[UIImage imageNamed:imageName]这句导入图片,这种做好好不好,有什么问题~?

    目前为止,我们已经把思路1.0里的所有方面都实现了,理清一下思路,我们现在所做的方向,是从Reusable池中取出view,放到Visible池中显示到屏幕上,所以接下来我们要做的就是当view离开屏幕时,把无需显示的view从Visible池放入Reusable池中,还有即将进入屏幕的view要及时创建。

    思路2.0

    • 滑动时即将出现的view
    • 把过时的view从Visible池中放入Reusable池中
    • 清理过时的view中的数据(如图片,data等)

    分析

    向左向右滑动后,当前的view还没完全从屏幕中移走,此时千万不可把当前view放入到Reusable池中;
    其次要提前生成即将出现的view,生成的方法可以利用思路1.0中已经实现的viewAtIndex:方法;
    最后根据当前view的index,把index-1和index+1以外的view全部放入Reusable池中等待重用。继续上代码:

    - (void)showNewImage {
        ZoomingScrollView *previousView = nil;
        ZoomingScrollView *nextView = nil;
        
        NSInteger previousIndex = _currentIndex - 1;
        NSInteger nextIndex = _currentIndex + 1;
        
        // 滑动时最多保留3个图,n是当前页面数,加上左右的2个
        if (_currentIndex == 0) {
            // 第一张图
            previousIndex = 0;
        } else if (_currentIndex == kTotalImage - 1) {
            // 最后一张图
            nextIndex = kTotalImage - 1;
        }
        
        if (![self isShowingViewAtIndex:previousIndex]) {
            previousView = [self viewAtIndex:previousIndex];
        }
        if (![self isShowingViewAtIndex:nextIndex]) {
            nextView = [self viewAtIndex:nextIndex];
        }
        
        [_scrollView addSubview:previousView];
        [_scrollView addSubview:nextView];
        
        // 其余全部放到reusableViews里
        for (ZoomingScrollView *view in _visibleViews) {
            NSInteger viewIndex = view.tag - 1000;
            if (viewIndex < previousIndex || viewIndex > nextIndex) {
                [_reusableViews addObject:view];
                view.imageView.image = nil;
                // 记得从主视图中remove
                [view removeFromSuperview];
            }
        }
        
        // 从visibleViews里删除刚刚去掉的view
        [_visibleViews minusSet:_reusableViews];    
    }
    

    其实后来想了一下,到底需不需要保留左右两个view。因为我把ScrollView设成了pagingEnable,每次只滑过一个页面,所以在屏幕上最多也只会同时出现2个页面,那再减少保留一个view应该也是可以的。但是当时在写代码的时候老是会出现下标、Rect等问题(设想不周全。。。),所以最好为了保险起见还是多留一个吧……后面优化一下应该就不需要了~

    还有,这里回答一下为什么我选择NSMutableSet,原因是[_visibleViews minusSet:_reusableViews];,这里一句话就可以把Visible池中的过时view去掉,节省了遍历的时间,而且又方便。

    到目前为止,重用机制所需要的操作我们都实现了,那我们应该在什么时机去调用这些操作呢?
    由于在我们开始滑动的时候,无论是左滑还是右滑,只要滑动了,前一个或下一个view就会马上出现了,所以上面的showNewImage方法应该是在滑动开始时就马上调用。
    没错,我们应该要在UIScrollViewDelegate的方法- (void)scrollViewDidScroll:(UIScrollView *)scrollView中调用showNewImage方法。
    并且,当滑动结束后,及时更新当前显示的view的index。

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        [self showNewImage];
    }
    
    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
        _currentIndex = _scrollView.contentOffset.x / 320;
    }
    

    好了,到这里为止,重用机制的思路和调用时机都讲完了,其实里面的思路都比较简单,在实现过程中计较容易出错的是由于index弄错而导致了view显示不正确,或者整个app崩掉。这时要需要来个断点调试,把断点设在新加入的代码中,一步一步看各个变量,对象的值有没有异常。

    最后,回到思路1.0代码中的问题,那样的图片导入方法好不好,有没有问题?

    答案肯定是有问题的,而且问题大得很。
    UIImage *image = [UIImage imageNamed:imageName];导入的图就算在后面把image设成nil,图片也是不会自动释放的。这个方法适用场景是UI的背景图,或者需要大量重复使用同一个图片的地方。

    而我们的图片浏览器显然不符合上述情况,图片无法自动释放直接导致内存不断上涨,那再多的重用也是百搭……
    所以这里用UIImage *image = [UIImage imageWithContentsOfFile:path];更好,图片可以自动释放,正合我们心意~

    - (ZoomingScrollView *)viewAtIndex:(NSUInteger) index {
        ZoomingScrollView *zoomingView = [self dequeReusableView];
        
        NSString *imageName = [NSString stringWithFormat:@"%lu", index+1];
    //    // 这是一个坑爹的方法,用这个方法load的图无法自动释放
    //    UIImage *image = [UIImage imageNamed:imageName];
        NSString *path = [[NSBundle mainBundle] pathForResource:imageName ofType:@"jpg"];
        UIImage *image = [UIImage imageWithContentsOfFile:path];
        [zoomingView setDisplayImage:image];
        
        CGFloat xPosition = index * kScreenFrame.size.width;
        CGRect viewFrame = CGRectMake(xPosition, 0, kScreenFrame.size.width, kScreenFrame.size.height);
        zoomingView.frame = viewFrame;
        zoomingView.tag = 1000 + index;
        
        [_visibleViews addObject:zoomingView];
        
        return zoomingView;
    }
    

    先到这里吧,感谢观看,谢谢!
    如有不足,恳请不吝赐教!及时私信我吧~

    相关文章

      网友评论

      本文标题:UIScrollview重用以及UIImage的坑

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