UIScrollview重用以及UIImage的坑

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

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

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,127评论 25 707
  • 好文章,转载一下,有机会好好研究下 今天在研究SDWebImage和ASIHTTPRequest实现网络图片异步加...
    Apollo2016阅读 1,985评论 0 2
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,103评论 4 62
  • 工作不是很忙,恰好有看书码字的时间。 我从不认为自己是勇敢的,我希望自己是一朵被蝴蝶青睐的艳丽牡丹,可我却是一株渺...
    洁若依依阅读 286评论 0 0
  • 我们每个人都是天上掉下来的星星,来到人世间就变得十分微小,微小到尘埃里。但又不甘落寞,努力发光,希望在黑夜降临的时...
    风筝_92f3阅读 456评论 0 0